From 13548deae8c8d92bf1646445088cd8aec88107ce Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 31 Aug 2026 00:33:29 +0800 Subject: [PATCH 01/60] feat: forbid deleting templates and images still used by projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleting a template with prune_images, deleting an image, or pruning orphan images only checked template references — a project node still pointing at the image (e.g. via hda_disk_image_backing_file) was left with a dangling reference and the project could no longer be opened. - Add controller helpers scanning every known project (opened projects via in-memory nodes, closed projects via their .gns3 file) for template and image usage; unreadable topologies are skipped - Guard DELETE /templates/{id}, DELETE /templates/{id}?prune_images, DELETE /images/{path} and /images/prune with a 409 listing the project names - Run all template-delete checks before any mutation so a refused deletion cannot leave the template gone while its images survive --- gns3server/api/routes/controller/images.py | 11 +- gns3server/api/routes/controller/templates.py | 48 +++++-- gns3server/controller/__init__.py | 108 +++++++++++++++ gns3server/controller/node.py | 4 + gns3server/services/templates.py | 9 ++ tests/api/routes/controller/test_images.py | 95 ++++++++++++++ tests/api/routes/controller/test_templates.py | 123 ++++++++++++++++++ tests/controller/test_controller.py | 71 ++++++++++ 8 files changed, 455 insertions(+), 14 deletions(-) diff --git a/gns3server/api/routes/controller/images.py b/gns3server/api/routes/controller/images.py index 70c49b098..fe27c20d8 100644 --- a/gns3server/api/routes/controller/images.py +++ b/gns3server/api/routes/controller/images.py @@ -197,11 +197,16 @@ async def prune_images( """ Prune images not attached to any template. + Images referenced by a node in any project (opened or closed) are kept. + Required privilege: Image.Allocate """ skip_images = get_builtin_disks() - await images_repo.prune_images(skip_images) + # a single pass over all projects' node properties protects every + # referenced file name at once + referenced_filenames = Controller.instance().collect_referenced_image_filenames() + await images_repo.prune_images(list(skip_images) + list(referenced_filenames)) @router.post( @@ -309,6 +314,10 @@ async def delete_image( template_names = ", ".join([template.name for template in templates]) raise ControllerError(f"Image '{image_path}' is used by one or more templates: {template_names}") + project_names = Controller.instance().find_projects_using_image(image.filename) + if project_names: + raise ControllerError(f"Image '{image_path}' is used by one or more projects: {', '.join(project_names)}") + try: os.remove(image.path) except OSError: diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py index 05fa6482c..6d59cd913 100644 --- a/gns3server/api/routes/controller/templates.py +++ b/gns3server/api/routes/controller/templates.py @@ -35,6 +35,7 @@ from gns3server.db.repositories.templates import TemplatesRepository from gns3server.services.templates import TemplatesService from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.images import ImagesRepository +from gns3server.controller import Controller from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError from gns3server.utils.images import get_builtin_disks @@ -135,28 +136,49 @@ async def delete_template( Required privilege: Template.Allocate """ + controller = Controller.instance() images = await templates_repo.get_template_images(template_id) - await TemplatesService(templates_repo).delete_template(template_id) - await rbac_repo.delete_all_ace_starting_with_path(f"/templates/{template_id}") + + # Run every usage check before mutating anything, so a refused deletion + # cannot leave the template gone while its images survive. + images_to_prune = [] if prune_images and images: skip_images = get_builtin_disks() + # collected lazily: a single pass over all projects' node properties + referenced_filenames = None for image in images: if image.filename in skip_images: continue - templates = await images_repo.get_image_templates(image.image_id) - if templates: - template_names = ", ".join([template.name for template in templates]) + # the template being deleted is still in the database at this + # point, exclude it from the other-templates check + other_templates = [ + template for template in await images_repo.get_image_templates(image.image_id) + if str(template.template_id) != str(template_id) + ] + if other_templates: + template_names = ", ".join([template.name for template in other_templates]) raise ControllerError(f"Image '{image.path}' is used by one or more templates: {template_names}") - try: - os.remove(image.path) - except OSError: - log.warning(f"Could not delete image file {image.path}") + if referenced_filenames is None: + referenced_filenames = controller.collect_referenced_image_filenames() + if image.filename in referenced_filenames: + project_names = controller.find_projects_using_image(image.filename) + raise ControllerError(f"Image '{image.path}' is used by one or more projects: {', '.join(project_names)}") + images_to_prune.append(image) - print(f"Deleting image '{image.path}'") - success = await images_repo.delete_image(image.path) - if not success: - raise ControllerError(f"Image '{image.path}' could not removed from the database") + await TemplatesService(templates_repo).delete_template(template_id) + await rbac_repo.delete_all_ace_starting_with_path(f"/templates/{template_id}") + + for image in images_to_prune: + try: + os.remove(image.path) + except OSError: + log.warning(f"Could not delete image file {image.path}") + + log.info(f"Deleting image '{image.path}'") + success = await images_repo.delete_image(image.path) + if not success: + raise ControllerError(f"Image '{image.path}' could not removed from the database") @router.get( diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 9593ee1cc..99416e202 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -38,6 +38,7 @@ from ..utils.images import default_images_directory from ..utils.asyncio import wait_run_in_executor from .project import Project +from .node import Node from .appliance import Appliance from .appliance_manager import ApplianceManager from .compute import Compute, ComputeError @@ -91,6 +92,35 @@ class _ProjectsDirectoryEventHandler(FileSystemEventHandler): return +def _iter_string_values(value): + """ + Yield every string contained in value, recursing into dicts and lists. + """ + + if isinstance(value, str): + yield value + elif isinstance(value, dict): + for child in value.values(): + yield from _iter_string_values(child) + elif isinstance(value, (list, tuple, set)): + for child in value: + yield from _iter_string_values(child) + + +def _image_referenced(node_properties, image_filename: str) -> bool: + """ + Whether a node's properties reference an image by file name. + + Image references live under type-specific property keys + (hda_disk_image, hda_disk_image_backing_file, image, initrd, path...) + and are stored either as bare file names or as absolute paths + depending on topology age, so every string value is matched on its + base name instead of walking a fixed key list. + """ + + return any(os.path.basename(string) == image_filename for string in _iter_string_values(node_properties)) + + class Controller: """ The controller is responsible to manage one or more computes. @@ -811,6 +841,84 @@ class Controller: raise ControllerError("A project name could not be allocated (node limit reached?)") return new_name + def _iter_project_nodes(self): + """ + Iterate over (project, node) for every known project, opened or closed. + + Opened projects yield controller Node objects; closed projects yield + the raw node dicts read from their .gns3 file. A project whose + topology cannot be read is skipped — it must not block the usage + checks that rely on this iterator. + """ + + for project in self._projects.values(): + try: + nodes = project.nodes.values() + except ControllerError: + continue + for node in nodes: + yield project, node + + def find_projects_using_template(self, template_id) -> list: + """ + Return the names of the projects with at least one node created + from this template. + + Used to forbid template deletion while any project still + references it. + """ + + template_id = str(template_id) + project_names = [] + for project, node in self._iter_project_nodes(): + if isinstance(node, Node): + node_template_id = node.template_id + else: + node_template_id = node.get("template_id") + if node_template_id and str(node_template_id) == template_id: + project_names.append(project.name) + break + return project_names + + def find_projects_using_image(self, image_filename: str) -> list: + """ + Return the names of the projects with at least one node whose + properties reference this image file. + + Used to forbid image deletion while any project still uses it. + """ + + project_names = [] + for project, node in self._iter_project_nodes(): + if isinstance(node, Node): + node_properties = node.properties + else: + node_properties = node.get("properties") or {} + if _image_referenced(node_properties, image_filename): + project_names.append(project.name) + break + return project_names + + def collect_referenced_image_filenames(self) -> set: + """ + Return every file name referenced by a node property across all + projects (opened or closed). + + The set deliberately over-approximates (any property string counts): + it protects images from pruning, so a false positive only keeps a + file alive. Used as a single-pass skip list for image pruning. + """ + + filenames = set() + for _project, node in self._iter_project_nodes(): + if isinstance(node, Node): + node_properties = node.properties + else: + node_properties = node.get("properties") or {} + for string in _iter_string_values(node_properties): + filenames.add(os.path.basename(string)) + return filenames + @property def projects(self): """ diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index cd474912f..8ea491b3c 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -162,6 +162,10 @@ class Node: def status(self): return self._status + @property + def template_id(self): + return self._template_id + @property def name(self): return self._name diff --git a/gns3server/services/templates.py b/gns3server/services/templates.py index c079608dd..66b793566 100644 --- a/gns3server/services/templates.py +++ b/gns3server/services/templates.py @@ -341,6 +341,15 @@ class TemplatesService: if self.get_builtin_template(template_id): raise ControllerForbiddenError(f"Template '{template_id}' cannot be deleted because it is built-in") + + template = await self.get_template(template_id) + project_names = self._controller.find_projects_using_template(template_id) + if project_names: + raise ControllerError( + f"Template '{template['name']}' cannot be deleted because it is used by " + f"one or more projects: {', '.join(project_names)}" + ) + if await self._templates_repo.delete_template(template_id): self._controller.notification.controller_emit("template.deleted", {"template_id": str(template_id)}) else: diff --git a/tests/api/routes/controller/test_images.py b/tests/api/routes/controller/test_images.py index 8a7a4d68c..ea0b2f587 100644 --- a/tests/api/routes/controller/test_images.py +++ b/tests/api/routes/controller/test_images.py @@ -16,6 +16,8 @@ # along with this program. If not, see . import os +import json +import uuid import pytest import hashlib @@ -282,6 +284,99 @@ class TestImageRoutes: images_in_db = await images_repo.get_images() assert len(images_in_db) == 0 + @staticmethod + async def _add_closed_project_with_node(controller: Controller, name: str, node: dict): + """ + Register a closed project whose .gns3 contains a single node, + mirroring how the controller picks up existing projects from disk. + """ + + project_dir = os.path.join(controller.projects_directory(), name) + os.makedirs(project_dir) + project_id = str(uuid.uuid4()) + topology = { + "name": name, + "project_id": project_id, + "topology": {"computes": [], "links": [], "drawings": [], "nodes": [node]}, + } + with open(os.path.join(project_dir, f"{name}.gns3"), "w+") as f: + json.dump(topology, f) + return await controller.add_project( + project_id=project_id, name=name, path=project_dir, + filename=f"{name}.gns3", status="closed", + ) + + async def test_image_delete_used_by_project( + self, + app: FastAPI, + client: AsyncClient, + controller: Controller, + db_session: AsyncSession, + tmpdir: str, + ) -> None: + """ + An image referenced by a node in a project must not be deleted, + even when the project is closed and no template uses the image. + """ + + image_path = os.path.join(tmpdir, "used.qcow2") + with open(image_path, "wb+") as f: + f.write(b'\x42\x42\x42\x42') + + images_repo = ImagesRepository(db_session) + await images_repo.add_image("used.qcow2", "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5") + + guarded_project = await self._add_closed_project_with_node( + controller, "Guarded", + {"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local", + "name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}}, + ) + + response = await client.delete(app.url_path_for("delete_image", image_path="used.qcow2")) + assert response.status_code == status.HTTP_409_CONFLICT + assert "Guarded" in response.json()["message"] + assert os.path.exists(image_path) + + # once no project uses it anymore the deletion goes through + controller.remove_project(guarded_project) + response = await client.delete(app.url_path_for("delete_image", image_path="used.qcow2")) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not os.path.exists(image_path) + + async def test_prune_images_keeps_project_referenced( + self, + app: FastAPI, + client: AsyncClient, + controller: Controller, + db_session: AsyncSession, + tmpdir: str, + ) -> None: + """ + Pruning must keep images referenced by a project node while + removing the unreferenced ones. + """ + + images_repo = ImagesRepository(db_session) + for filename in ("used.qcow2", "unused.qcow2"): + image_path = os.path.join(tmpdir, filename) + with open(image_path, "wb+") as f: + f.write(b'\x42\x42\x42\x42') + await images_repo.add_image(filename, "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5") + + await self._add_closed_project_with_node( + controller, "Guarded", + {"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local", + "name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}}, + ) + + response = await client.delete(app.url_path_for("prune_images")) + assert response.status_code == status.HTTP_204_NO_CONTENT + + assert await images_repo.get_image(os.path.join(tmpdir, "used.qcow2")) is not None + assert os.path.exists(os.path.join(tmpdir, "used.qcow2")) + assert await images_repo.get_image(os.path.join(tmpdir, "unused.qcow2")) is None + assert not os.path.exists(os.path.join(tmpdir, "unused.qcow2")) + async def test_image_upload_create_appliance( self, app: FastAPI, client: AsyncClient, diff --git a/tests/api/routes/controller/test_templates.py b/tests/api/routes/controller/test_templates.py index 75cae55d5..a1bf507e9 100644 --- a/tests/api/routes/controller/test_templates.py +++ b/tests/api/routes/controller/test_templates.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os +import json import shutil import pytest @@ -270,6 +271,128 @@ class TestTemplateRoutes: images = await images_repo.get_images() assert len(images) == 0 + @staticmethod + async def _add_closed_project_with_node(controller: Controller, name: str, node: dict): + """ + Register a closed project whose .gns3 contains a single node, + mirroring how the controller picks up existing projects from disk. + """ + + project_dir = os.path.join(controller.projects_directory(), name) + os.makedirs(project_dir) + project_id = str(uuid.uuid4()) + topology = { + "name": name, + "project_id": project_id, + "topology": {"computes": [], "links": [], "drawings": [], "nodes": [node]}, + } + with open(os.path.join(project_dir, f"{name}.gns3"), "w+") as f: + json.dump(topology, f) + # an explicit project_id marks this as an existing on-disk project + # (same path load_project takes at startup) + return await controller.add_project( + project_id=project_id, name=name, path=project_dir, + filename=f"{name}.gns3", status="closed", + ) + + async def test_template_delete_used_by_project( + self, + app: FastAPI, + client: AsyncClient, + controller: Controller, + ) -> None: + """ + A template referenced by a node in a project must not be deleted, + even when the project is closed. + """ + + template_id = str(uuid.uuid4()) + params = {"template_id": template_id, + "name": "VPCS_GUARDED", + "compute_id": "local", + "template_type": "vpcs"} + + response = await client.post(app.url_path_for("create_template"), json=params) + assert response.status_code == status.HTTP_201_CREATED + + guarded_project = await self._add_closed_project_with_node( + controller, "Guarded", + {"node_id": str(uuid.uuid4()), "node_type": "vpcs", "compute_id": "local", + "name": "n1", "template_id": template_id, "properties": {}}, + ) + + response = await client.delete(app.url_path_for("delete_template", template_id=template_id)) + assert response.status_code == status.HTTP_409_CONFLICT + assert "Guarded" in response.json()["message"] + + # the template survived the refused deletion + response = await client.get(app.url_path_for("get_template", template_id=template_id)) + assert response.status_code == status.HTTP_200_OK + + # once no project uses it anymore the deletion goes through + controller.remove_project(guarded_project) + response = await client.delete(app.url_path_for("delete_template", template_id=template_id)) + assert response.status_code == status.HTTP_204_NO_CONTENT + + async def test_template_delete_with_prune_images_used_by_project( + self, + app: FastAPI, + client: AsyncClient, + controller: Controller, + db_session: AsyncSession, + tmpdir: str, + ) -> None: + """ + Pruning an image still referenced by a project node must be refused + before any mutation: the template and the image file both survive. + """ + + image_path = os.path.join(tmpdir, "used.qcow2") + with open(image_path, "wb+") as f: + f.write(b'\x42\x42\x42\x42') + + images_repo = ImagesRepository(db_session) + await images_repo.add_image("used.qcow2", "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5") + + template_id = str(uuid.uuid4()) + params = {"template_id": template_id, + "name": "QEMU_GUARDED", + "compute_id": "local", + "hda_disk_image": "used.qcow2", + "template_type": "qemu"} + + response = await client.post(app.url_path_for("create_template"), json=params) + assert response.status_code == status.HTTP_201_CREATED + + # the node references the image but not the template: the template + # check passes, the image check must still refuse the prune + guarded_project = await self._add_closed_project_with_node( + controller, "Guarded", + {"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local", + "name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}}, + ) + + response = await client.delete( + app.url_path_for("delete_template", template_id=template_id), + params={"prune_images": True} + ) + assert response.status_code == status.HTTP_409_CONFLICT + assert "Guarded" in response.json()["message"] + + # neither the template nor the image file was touched + response = await client.get(app.url_path_for("get_template", template_id=template_id)) + assert response.status_code == status.HTTP_200_OK + assert os.path.exists(image_path) + + controller.remove_project(guarded_project) + response = await client.delete( + app.url_path_for("delete_template", template_id=template_id), + params={"prune_images": True} + ) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not os.path.exists(image_path) + assert await images_repo.get_image(image_path) is None + # async def test_create_node_from_template(self, controller_api, controller, project): # # id = str(uuid.uuid4()) diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index 4f158c9b3..ca689edf5 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -534,3 +534,74 @@ async def test_autoidlepc(controller): await controller.autoidlepc("local", "c7200", "test.bin", 512) assert node_mock.dynamips_auto_idlepc.called assert len(controller.projects) == 0 + + +@pytest.mark.asyncio +async def test_find_projects_using_template_and_images(controller): + """ + The template/image usage checks must see nodes of opened projects + (in-memory Node objects) as well as nodes of closed projects (raw + node dicts read back from the .gns3 file). + """ + + compute = MagicMock() + response = MagicMock() + response.json = {"console": 2048} + compute.post = AsyncioMagicMock(return_value=response) + + project1 = await controller.add_project(name="Test1") + project2 = await controller.add_project(name="Test2") + + template_id = str(uuid.uuid4()) + await project1.add_node( + compute, + "n1", + None, + node_type="vpcs", + template_id=template_id, + properties={"hda_disk_image": "/tmp/images/disk.qcow2", "hda_disk_image_backing_file": "base.qcow2"}, + ) + await project2.add_node(compute, "n2", None, node_type="vpcs", properties={}) + + # opened projects + assert controller.find_projects_using_template(template_id) == ["Test1"] + assert controller.find_projects_using_template(str(uuid.uuid4())) == [] + # image references are matched on file name whether stored as a bare + # name or as an absolute path + assert controller.find_projects_using_image("base.qcow2") == ["Test1"] + assert controller.find_projects_using_image("disk.qcow2") == ["Test1"] + assert controller.find_projects_using_image("unknown.qcow2") == [] + referenced = controller.collect_referenced_image_filenames() + assert "base.qcow2" in referenced + assert "disk.qcow2" in referenced + + # closed projects: same answers from the .gns3 file on disk + await project1.close() + await project2.close() + assert controller.find_projects_using_template(template_id) == ["Test1"] + assert controller.find_projects_using_image("base.qcow2") == ["Test1"] + assert controller.find_projects_using_image("disk.qcow2") == ["Test1"] + + +@pytest.mark.asyncio +async def test_find_projects_skips_unreadable_topology(controller): + """ + A project whose topology cannot be read must not break the usage checks. + """ + + compute = MagicMock() + response = MagicMock() + response.json = {"console": 2048} + compute.post = AsyncioMagicMock(return_value=response) + + project = await controller.add_project(name="Broken") + await project.add_node( + compute, "n1", None, node_type="vpcs", + template_id=str(uuid.uuid4()), properties={"hda_disk_image": "lost.qcow2"}, + ) + await project.close() + os.remove(project.topology_file) + + assert controller.find_projects_using_template(str(uuid.uuid4())) == [] + assert controller.find_projects_using_image("lost.qcow2") == [] + assert controller.collect_referenced_image_filenames() == set() From eed981fee31a5ca3709681d0ebf3bff6c25e22c0 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 31 Aug 2026 01:13:20 +0800 Subject: [PATCH 02/60] fix: tear down controller console WebSocket forwarding cleanly on client disconnect Closing a web console while the node kept streaming output crashed the ws_console/vnc_console handlers with an uncaught WebSocketDisconnect from the compute-to-client send path (only the opposite direction was guarded), producing a full ASGI traceback on every console close. Restructure both endpoints as symmetric forwarding tasks managed with asyncio.wait(FIRST_COMPLETED): exceptions from either direction are collected as task exceptions, the peer task is cancelled, the compute WebSocket is closed, and the client is notified. The receive loops now close and log on every exit path instead of only in the exception branch. Tests patch the in-memory ASGI transport to deliver a conformant websocket.disconnect on client close (it sends a non-conformant websocket.close that starlette receive() rejects). --- gns3server/api/routes/controller/nodes.py | 106 +++++++--- tests/api/routes/controller/test_nodes.py | 240 +++++++++++++++++++++- 2 files changed, 311 insertions(+), 35 deletions(-) diff --git a/gns3server/api/routes/controller/nodes.py b/gns3server/api/routes/controller/nodes.py index ed17a703e..482c56146 100644 --- a/gns3server/api/routes/controller/nodes.py +++ b/gns3server/api/routes/controller/nodes.py @@ -20,6 +20,7 @@ API routes for nodes. import aiohttp import asyncio +import contextlib import ipaddress from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, Response, status, Query, HTTPException @@ -707,14 +708,34 @@ async def ws_console( elif "bytes" in msg and msg["bytes"]: await ws_console_compute.send_bytes(msg["bytes"]) except WebSocketDisconnect: - await ws_console_compute.close() + pass + log.info( + f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" + f" console WebSocket" + ) + + async def ws_send(ws_console_compute): + """ + Receive WebSocket data from compute console WebSocket and forward to client. + """ + + try: + async for msg in ws_console_compute: + if msg.type == aiohttp.WSMsgType.TEXT: + await websocket.send_text(msg.data) + elif msg.type == aiohttp.WSMsgType.BINARY: + await websocket.send_bytes(msg.data) + elif msg.type == aiohttp.WSMsgType.ERROR: + break + except WebSocketDisconnect: + # the client disconnected while the compute was still streaming console output log.info( f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" f" console WebSocket" ) try: - # receive WebSocket data from compute console WebSocket and forward to client. + # forward WebSocket data in both directions between the client and the compute console WebSocket log.info(f"Forwarding console WebSocket to '{ws_console_compute_url}'") server_config = Config.instance().settings.Server user = server_config.compute_username @@ -728,20 +749,20 @@ async def ws_console( auth = aiohttp.BasicAuth(user, "") ssl_context = Controller.instance().ssl_context() async with HTTPClient.get_client().ws_connect(ws_console_compute_url, auth=auth, ssl_context=ssl_context) as ws: - asyncio.ensure_future(ws_receive(ws)) - async for msg in ws: - if msg.type == aiohttp.WSMsgType.TEXT: - await websocket.send_text(msg.data) - elif msg.type == aiohttp.WSMsgType.BINARY: - await websocket.send_bytes(msg.data) - elif msg.type == aiohttp.WSMsgType.ERROR: - break - except WebSocketDisconnect: - # the client disconnected while the compute was still streaming console output - log.info( - f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" - f" console WebSocket" - ) + tasks = [ + asyncio.ensure_future(ws_receive(ws)), + asyncio.ensure_future(ws_send(ws)), + ] + done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in done: + if task.exception(): + log.warning(f"Exception while forwarding console WebSocket data: {task.exception()}") + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) + # notify the client that the console session has ended (ignore if the client is already gone) + with contextlib.suppress(WebSocketDisconnect): + await websocket.close() except aiohttp.ClientError as e: log.error(f"Client error received when forwarding to compute console WebSocket: {e}") @@ -786,18 +807,39 @@ async def vnc_console( try: while True: - data = await websocket.receive_bytes() + msg = await websocket.receive() + if msg["type"] == "websocket.disconnect": + break + data = msg.get("bytes") if data: await vnc_console_compute.send_bytes(data) except WebSocketDisconnect: - await vnc_console_compute.close() + pass + log.info( + f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" + f" VNC console WebSocket" + ) + + async def vnc_send(vnc_console_compute): + """ + Receive binary WebSocket data from compute VNC console WebSocket and forward to client. + """ + + try: + async for msg in vnc_console_compute: + if msg.type == aiohttp.WSMsgType.BINARY: + await websocket.send_bytes(msg.data) + elif msg.type == aiohttp.WSMsgType.ERROR: + break + except WebSocketDisconnect: + # the client disconnected while the compute was still streaming VNC console output log.info( f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" f" VNC console WebSocket" ) try: - # receive binary data from compute VNC console WebSocket and forward to client + # forward WebSocket data in both directions between the client and the compute VNC console WebSocket log.info(f"Forwarding VNC console WebSocket to '{vnc_console_compute_url}'") server_config = Config.instance().settings.Server user = server_config.compute_username @@ -811,18 +853,20 @@ async def vnc_console( auth = aiohttp.BasicAuth(user, "") ssl_context = Controller.instance().ssl_context() async with HTTPClient.get_client().ws_connect(vnc_console_compute_url, auth=auth, ssl_context=ssl_context) as ws: - asyncio.ensure_future(vnc_receive(ws)) - async for msg in ws: - if msg.type == aiohttp.WSMsgType.BINARY: - await websocket.send_bytes(msg.data) - elif msg.type == aiohttp.WSMsgType.ERROR: - break - except WebSocketDisconnect: - # the client disconnected while the compute was still streaming VNC console output - log.info( - f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" - f" VNC console WebSocket" - ) + tasks = [ + asyncio.ensure_future(vnc_receive(ws)), + asyncio.ensure_future(vnc_send(ws)), + ] + done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in done: + if task.exception(): + log.warning(f"Exception while forwarding VNC console WebSocket data: {task.exception()}") + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) + # notify the client that the VNC session has ended (ignore if the client is already gone) + with contextlib.suppress(WebSocketDisconnect): + await websocket.close() except aiohttp.ClientError as e: log.error(f"Client error received when forwarding to compute VNC console WebSocket: {e}") diff --git a/tests/api/routes/controller/test_nodes.py b/tests/api/routes/controller/test_nodes.py index 02ca2c143..be115b1dd 100644 --- a/tests/api/routes/controller/test_nodes.py +++ b/tests/api/routes/controller/test_nodes.py @@ -16,9 +16,11 @@ # along with this program. If not, see . -import aiohttp +import asyncio +import contextlib import logging +import aiohttp import pytest from types import SimpleNamespace @@ -27,8 +29,11 @@ from typing import List, Optional from fastapi import FastAPI, HTTPException, WebSocketDisconnect, status from httpx import AsyncClient from pydantic import SecretStr +from httpx_ws import aconnect_ws +from httpx_ws import WebSocketDisconnect as HttpxWebSocketDisconnect +from httpx_ws.transport import ASGIWebSocketTransport, ASGIWebSocketAsyncNetworkStream -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from tests.utils import AsyncioMagicMock from gns3server.config import Config @@ -37,6 +42,8 @@ from gns3server.controller.project import Project from gns3server.controller.compute import Compute from gns3server.utils.http_client import HTTPClient from gns3server.api.routes.controller.nodes import ws_console, vnc_console +from gns3server.services import auth_service +from gns3server.services.authentication import DEFAULT_JWT_SECRET_KEY pytestmark = pytest.mark.asyncio @@ -681,6 +688,8 @@ class TestNodeRoutes: # compute.http_query.assert_called_with("POST", "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello/nested".format(project_id=project.id, node_id=node.id), data=b'hello', timeout=None, raw=True) + + class FakeComputeConsoleWebSocket: """ Stand-in for the aiohttp ClientWebSocketResponse returned when connecting @@ -738,13 +747,21 @@ class FakeClientWebSocket: async def receive(self) -> dict: - # the client is already gone from the receive side - return {"type": "websocket.disconnect"} + # the client never sends anything else and never completes a close + # handshake: block until the forwarding task is cancelled, like a real + # receive side waiting for a disconnect message that only arrives on + # the send path + await asyncio.Event().wait() async def receive_bytes(self) -> bytes: raise WebSocketDisconnect(code=1006) + async def close(self, code: int = 1000, reason: Optional[str] = None) -> None: + + # notification that the console session ended; nothing to record + pass + async def send_text(self, data: str) -> None: self._check_client_alive() @@ -855,3 +872,218 @@ class TestNodeConsoleWebSocketRoutes: assert websocket.sent == [] assert compute_ws.closed + + +class _FakeComputeWebSocket: + """ + Mimics the aiohttp client WebSocket the controller forwards console traffic to. + + Pass a list of WSMessage to emulate a compute that sends data then closes the + session, or None to emulate a busy console streaming binary frames forever + (until the controller cancels the forwarding task). + """ + + def __init__(self, messages=None) -> None: + self._messages = messages + self.sent = [] # frames received from the client + self.closed = False + self.stream_cancelled = False + + async def send_str(self, data: str) -> None: + self.sent.append(("text", data)) + + async def send_bytes(self, data: bytes) -> None: + self.sent.append(("bytes", data)) + + async def close(self) -> None: + self.closed = True + + def __aiter__(self): + return self + + async def __anext__(self): + if self._messages is not None: + if not self._messages: + raise StopAsyncIteration + return self._messages.pop(0) + try: + await asyncio.sleep(0.05) + except asyncio.CancelledError: + self.stream_cancelled = True + raise + return aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"console output", "") + + +class _FakeComputeWebSocketContext: + """Async context manager mimicking the object returned by aiohttp ws_connect().""" + + def __init__(self, websocket: _FakeComputeWebSocket) -> None: + self._websocket = websocket + + async def __aenter__(self) -> _FakeComputeWebSocket: + return self._websocket + + async def __aexit__(self, *exc_info) -> bool: + await self._websocket.close() + return False + + +# The in-memory ASGI WebSocket transport notifies the app of a client close with a +# non-conformant "websocket.close" message, which starlette's receive() rejects. +# Patch it to deliver "websocket.disconnect" like a real ASGI server (uvicorn) does. +_original_stream_send = ASGIWebSocketAsyncNetworkStream.send + + +async def _conforming_stream_send(self, message): + if message.get("type") == "websocket.close": + message = {"type": "websocket.disconnect", "code": message.get("code") or 1000} + await _original_stream_send(self, message) + + +class TestConsoleWebSocketRoutes: + """ + Walk the console/VNC WebSocket endpoints through the ASGI stack: data is + forwarded in both directions and both sides tear down cleanly when the + other goes away, instead of letting a WebSocketDisconnect escape as an + ASGI error. + """ + + @pytest.fixture + def node(self, project: Project, compute: Compute) -> Node: + + node = Node(project, compute, "test", node_type="qemu") + project._nodes[node.id] = node + return node + + @staticmethod + def _patches(fake_ws: _FakeComputeWebSocket): + """ + Make HTTPClient.get_client().ws_connect() yield the fake compute WebSocket, + and make the in-memory transport deliver a conformant disconnect message. + """ + + stack = contextlib.ExitStack() + http_client = MagicMock() + http_client.ws_connect = MagicMock(return_value=_FakeComputeWebSocketContext(fake_ws)) + stack.enter_context(patch( + "gns3server.api.routes.controller.nodes.HTTPClient.get_client", + return_value=http_client + )) + stack.enter_context(patch.object(ASGIWebSocketAsyncNetworkStream, "send", _conforming_stream_send)) + return stack + + @staticmethod + def _admin_token() -> str: + + return auth_service.create_access_token("admin", secret_key=DEFAULT_JWT_SECRET_KEY) + + @staticmethod + async def _wait_for_teardown(fake_ws: _FakeComputeWebSocket) -> None: + # the handler keeps running on the event loop after the client is gone + for _ in range(100): + if fake_ws.closed: + await asyncio.sleep(0.1) # give cancelled tasks a chance to finish + return + await asyncio.sleep(0.05) + + async def test_console_ws_client_disconnect( + self, + app: FastAPI, + client: AsyncClient, + project: Project, + compute: Compute, + node: Node, + caplog + ) -> None: + """ + A client disconnecting while the compute keeps streaming console output + must not raise: the forwarding tasks are cancelled and the compute + console WebSocket is closed. + """ + + fake_ws = _FakeComputeWebSocket(messages=None) + with self._patches(fake_ws), caplog.at_level(logging.INFO): + async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app=app)) as ws_client: + async with aconnect_ws( + app.url_path_for("ws_console", project_id=project.id, node_id=node.id), + ws_client, + params={"token": self._admin_token()}, + ) as ws: + # compute -> client + assert await ws.receive_bytes() == b"console output" + # client -> compute + await ws.send_text("dir") + await ws.send_bytes(b"\x01\x02") + await self._wait_for_teardown(fake_ws) + + assert fake_ws.sent == [("text", "dir"), ("bytes", b"\x01\x02")] + assert fake_ws.stream_cancelled, "the compute -> client forwarding task should have been cancelled" + assert fake_ws.closed, "the compute console WebSocket should have been closed" + assert any( + "has disconnected from controller console WebSocket" in record.getMessage() + for record in caplog.records + ) + + async def test_console_ws_compute_closes_session( + self, + app: FastAPI, + client: AsyncClient, + project: Project, + compute: Compute, + node: Node + ) -> None: + """ + When the compute closes the console WebSocket the client should receive + the frames sent before the close, then the close itself. + """ + + fake_ws = _FakeComputeWebSocket(messages=[ + aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"output", ""), + aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "done", ""), + ]) + with self._patches(fake_ws): + async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app=app)) as ws_client: + async with aconnect_ws( + app.url_path_for("ws_console", project_id=project.id, node_id=node.id), + ws_client, + params={"token": self._admin_token()}, + ) as ws: + assert await ws.receive_bytes() == b"output" + assert await ws.receive_text() == "done" + with pytest.raises(HttpxWebSocketDisconnect): + await ws.receive_bytes() + + assert fake_ws.closed + + async def test_vnc_console_ws_client_disconnect( + self, + app: FastAPI, + client: AsyncClient, + project: Project, + compute: Compute, + node: Node, + caplog + ) -> None: + """ + Same as the console test, for the VNC endpoint (binary frames only). + """ + + fake_ws = _FakeComputeWebSocket(messages=None) + with self._patches(fake_ws), caplog.at_level(logging.INFO): + async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app=app)) as ws_client: + async with aconnect_ws( + app.url_path_for("vnc_console", project_id=project.id, node_id=node.id), + ws_client, + params={"token": self._admin_token()}, + ) as ws: + assert await ws.receive_bytes() == b"console output" + await ws.send_bytes(b"\x01\x02") + await self._wait_for_teardown(fake_ws) + + assert fake_ws.sent == [("bytes", b"\x01\x02")] + assert fake_ws.stream_cancelled, "the compute -> client forwarding task should have been cancelled" + assert fake_ws.closed, "the compute VNC console WebSocket should have been closed" + assert any( + "has disconnected from controller VNC console WebSocket" in record.getMessage() + for record in caplog.records + ) From 823cf372870a0bbfc125bc306801e99db69fe0fc Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 31 Aug 2026 23:04:00 +0800 Subject: [PATCH 03/60] fix: list every project in template/image in-use refusal, not just the first The usage scans broke out of the whole (project, node) iteration at the first matching node, so deleting a template or image used by several projects reported only one project name while the error message says 'one or more projects'. Keep scanning and list each project once. Caught on a live server: an IOU L3 template used by two projects (one of them closed) was refused citing only one of them. --- gns3server/controller/__init__.py | 6 ++---- tests/controller/test_controller.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 99416e202..b1e16c87b 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -875,9 +875,8 @@ class Controller: node_template_id = node.template_id else: node_template_id = node.get("template_id") - if node_template_id and str(node_template_id) == template_id: + if node_template_id and str(node_template_id) == template_id and project.name not in project_names: project_names.append(project.name) - break return project_names def find_projects_using_image(self, image_filename: str) -> list: @@ -894,9 +893,8 @@ class Controller: node_properties = node.properties else: node_properties = node.get("properties") or {} - if _image_referenced(node_properties, image_filename): + if _image_referenced(node_properties, image_filename) and project.name not in project_names: project_names.append(project.name) - break return project_names def collect_referenced_image_filenames(self) -> set: diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index ca689edf5..dad07581e 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -575,11 +575,36 @@ async def test_find_projects_using_template_and_images(controller): assert "base.qcow2" in referenced assert "disk.qcow2" in referenced + # a second node from the same template in the same project must not + # list the project twice + await project1.add_node( + compute, + "n1-bis", + None, + node_type="vpcs", + template_id=template_id, + properties={}, + ) + assert controller.find_projects_using_template(template_id) == ["Test1"] + + # a second project using the same template and image must be listed too + await project2.add_node( + compute, + "n2-bis", + None, + node_type="vpcs", + template_id=template_id, + properties={"hda_disk_image_backing_file": "base.qcow2"}, + ) + assert controller.find_projects_using_template(template_id) == ["Test1", "Test2"] + assert controller.find_projects_using_image("base.qcow2") == ["Test1", "Test2"] + assert controller.find_projects_using_image("disk.qcow2") == ["Test1"] + # closed projects: same answers from the .gns3 file on disk await project1.close() await project2.close() - assert controller.find_projects_using_template(template_id) == ["Test1"] - assert controller.find_projects_using_image("base.qcow2") == ["Test1"] + assert controller.find_projects_using_template(template_id) == ["Test1", "Test2"] + assert controller.find_projects_using_image("base.qcow2") == ["Test1", "Test2"] assert controller.find_projects_using_image("disk.qcow2") == ["Test1"] From 28f0d9c009b4cb2f3c8ef05be2838e003f1254c0 Mon Sep 17 00:00:00 2001 From: Cristi Date: Fri, 4 Sep 2026 09:45:41 +0300 Subject: [PATCH 04/60] feat: Add Docker link carrier and interface status support --- gns3server/api/routes/compute/docker_nodes.py | 1 + gns3server/compute/docker/docker_vm.py | 180 ++++++++++++++++- gns3server/compute/ubridge/hypervisor.py | 8 +- tests/api/routes/compute/test_docker_nodes.py | 5 +- tests/compute/docker/test_docker_vm.py | 188 +++++++++++++++++- tests/compute/docker/test_vendor_docker_vm.py | 4 + tests/compute/ubridge/test_hypervisor.py | 31 +++ 7 files changed, 408 insertions(+), 9 deletions(-) diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index be55ec0f4..63d8bea91 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -307,6 +307,7 @@ async def update_docker_node_nio( if nio_data.filters: nio.filters = nio_data.filters nio.markers = nio_data.markers or {} + nio.suspend = nio_data.suspend await node.adapter_update_nio_binding(adapter_number, nio) return nio.asdict() diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 666844875..44a89f968 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -20,6 +20,8 @@ Docker container instance. import sys import asyncio +import contextlib +import json import shutil import psutil import shlex @@ -92,6 +94,7 @@ class DockerVM(BaseNode): "/sbin/udevadm", "/usr/bin/udevadm", ) + _INTERFACE_STATUS_RESYNC_INTERVAL = 10 def __init__( self, @@ -151,6 +154,10 @@ class DockerVM(BaseNode): self._permissions_fixed = True self._display = None self._closing = False + self._interface_monitor_writer = None + self._interface_monitor_task = None + self._interface_statuses = {} + self._interface_status_times = {} self._volumes = [] # Keep a list of created bridge @@ -863,6 +870,8 @@ class DockerVM(BaseNode): if state == "paused": await self.unpause() elif state == "running": + self.status = "started" + await self._start_interface_monitor() return else: @@ -911,6 +920,7 @@ class DockerVM(BaseNode): self._permissions_fixed = False self.status = "started" + await self._start_interface_monitor() log.debug( "Docker container '{name}' [{image}] started listen for {console_type} on {console}".format( name=self._name, image=self._image, console=self.console, console_type=self.console_type @@ -1226,7 +1236,9 @@ class DockerVM(BaseNode): Restart this Docker container. """ + await self._stop_interface_monitor() await self.manager.query("POST", f"containers/{self._cid}/restart") + await self._start_interface_monitor() log.debug("Docker container '{name}' [{image}] restarted".format(name=self._name, image=self._image)) def _cleanup_console_resources(self): @@ -1258,6 +1270,7 @@ class DockerVM(BaseNode): """ try: + await self._stop_interface_monitor() if self._console_websocket: await self._console_websocket.close() self._console_websocket = None @@ -1393,6 +1406,158 @@ class DockerVM(BaseNode): """ return f"eth{adapter_number}" + async def _start_interface_monitor(self): + """Monitor administrative state changes for this container's adapters. + + Docker does not expose guest ``ip link set`` changes as daemon events. + Run one small BusyBox loop through the Engine exec API and keep its + hijacked output stream open. The loop samples all GNS3 interfaces in + one process; notifications are emitted on changes and periodically to + resynchronize newly connected Web UI clients. + """ + + if self._interface_monitor_task and not self._interface_monitor_task.done(): + return + + await self._stop_interface_monitor() + if not self._cid or not self.adapters: + return + + interface_names = [self._get_container_ifname(adapter_number) for adapter_number in range(self.adapters)] + script = ( + "while :; do " + "for ifname do " + "flags=$(/gns3/bin/busybox cat \"/sys/class/net/$ifname/flags\" 2>/dev/null) || continue; " + "/gns3/bin/busybox printf '%s=%s\\n' \"$ifname\" \"$flags\"; " + "done; " + "/gns3/bin/busybox sleep 1; " + "done" + ) + + try: + result = await self.manager.query( + "POST", + f"containers/{self._cid}/exec", + data={ + "AttachStdout": True, + "AttachStderr": False, + "Tty": True, + "User": "root", + "Cmd": ["/gns3/bin/busybox", "sh", "-c", script, "sh", *interface_names], + }, + ) + exec_id = result["Id"] + reader, writer = await asyncio.open_unix_connection(self.manager._server_url) + body = json.dumps({"Detach": False, "Tty": True}) + request = ( + f"POST /v{self.manager._api_version}/exec/{exec_id}/start HTTP/1.1\r\n" + "Host: docker\r\n" + "Connection: Upgrade\r\n" + "Upgrade: tcp\r\n" + "Content-Type: application/json\r\n" + f"Content-Length: {len(body)}\r\n\r\n{body}" + ).encode() + writer.write(request) + await writer.drain() + headers = await asyncio.wait_for(reader.readuntil(b"\r\n\r\n"), timeout=5) + status_line = headers.split(b"\r\n", 1)[0] + if b" 101 " not in status_line and b" 200 " not in status_line: + raise DockerError(f"Docker interface monitor was rejected: {status_line.decode(errors='ignore')}") + except ( + DockerError, + OSError, + KeyError, + TypeError, + RuntimeError, + asyncio.IncompleteReadError, + asyncio.TimeoutError, + ) as e: + if 'writer' in locals(): + await self._close_interface_monitor_writer(writer) + log.warning("Could not monitor interfaces for Docker container '%s': %s", self.name, e) + return + + self._interface_statuses.clear() + self._interface_status_times.clear() + self._interface_monitor_writer = writer + self._interface_monitor_task = asyncio.create_task(self._read_interface_statuses(reader)) + + async def _stop_interface_monitor(self): + """Close the Docker exec stream and its reader task.""" + + task = self._interface_monitor_task + self._interface_monitor_task = None + if task: + task.cancel() + writer = self._interface_monitor_writer + self._interface_monitor_writer = None + if writer: + await self._close_interface_monitor_writer(writer) + if task: + with contextlib.suppress(asyncio.CancelledError): + await task + self._interface_statuses.clear() + self._interface_status_times.clear() + + @staticmethod + async def _close_interface_monitor_writer(writer): + with contextlib.suppress(Exception): + writer.close() + await writer.wait_closed() + + async def _read_interface_statuses(self, reader): + """Read ``ifname=flags`` records and emit changed adapter states.""" + + interfaces = { + self._get_container_ifname(adapter_number): adapter_number + for adapter_number in range(self.adapters) + } + try: + while True: + line = await reader.readline() + if not line: + break + record = line.decode(errors="replace").strip("\r\n") + ifname, separator, flags_text = record.partition("=") + adapter_number = interfaces.get(ifname) + if not separator or adapter_number is None: + continue + try: + is_up = bool(int(flags_text, 0) & 0x1) # Linux IFF_UP + except ValueError: + continue + status = "started" if is_up else "stopped" + now = asyncio.get_running_loop().time() + if ( + self._interface_statuses.get(adapter_number) == status + and now - self._interface_status_times.get(adapter_number, 0) + < self._INTERFACE_STATUS_RESYNC_INTERVAL + ): + continue + self._interface_statuses[adapter_number] = status + self._interface_status_times[adapter_number] = now + self.project.emit( + "node.interface_status", + { + "project_id": self.project.id, + "node_id": self.id, + "adapter_number": adapter_number, + "port_number": 0, + "status": status, + }, + ) + except asyncio.CancelledError: + raise + except (OSError, RuntimeError) as e: + log.debug("Docker interface monitor for '%s' stopped: %s", self.name, e) + finally: + if self._interface_monitor_task is asyncio.current_task(): + self._interface_monitor_task = None + writer = self._interface_monitor_writer + self._interface_monitor_writer = None + if writer: + await self._close_interface_monitor_writer(writer) + async def _add_ubridge_connection(self, nio, adapter_number): """ Creates a connection in uBridge. @@ -1424,7 +1589,7 @@ class DockerVM(BaseNode): await self._ubridge_send(f"bridge create {bridge_name}") self._bridges.add(bridge_name) await self._ubridge_send( - "bridge add_nio_tap bridge{adapter_number} {hostif}".format( + "bridge add_nio_tap bridge{adapter_number} {hostif} off".format( adapter_number=adapter_number, hostif=adapter.host_ifc ) ) @@ -1454,12 +1619,19 @@ class DockerVM(BaseNode): if nio: await self._connect_nio(adapter_number, nio) + await self._set_adapter_carrier(adapter_number, not nio.suspend) async def _get_namespace(self): result = await self.manager.query("GET", f"containers/{self._cid}/json") return int(result["State"]["Pid"]) + async def _set_adapter_carrier(self, adapter_number, connected): + """Replicate a Docker adapter's connection state on its TAP device.""" + + state = "on" if connected else "off" + await self._ubridge_send(f"bridge set_nio_tap_carrier bridge{adapter_number} {state}") + async def _connect_nio(self, adapter_number, nio): bridge_name = f"bridge{adapter_number}" @@ -1497,6 +1669,7 @@ class DockerVM(BaseNode): if self.status == "started" and self.ubridge: await self._connect_nio(adapter_number, nio) + await self._set_adapter_carrier(adapter_number, not nio.suspend) adapter.add_nio(0, nio) log.debug( @@ -1518,6 +1691,9 @@ class DockerVM(BaseNode): if bridge_name in self._bridges: await self._ubridge_apply_filters(bridge_name, nio.filters) await self._ubridge_apply_markers(bridge_name, nio) + if self.status == "started": + await self._set_adapter_carrier(adapter_number, not nio.suspend) + async def adapter_remove_nio_binding(self, adapter_number): """ Removes an adapter NIO binding. @@ -1540,6 +1716,8 @@ class DockerVM(BaseNode): if self.ubridge: nio = adapter.get_nio(0) bridge_name = f"bridge{adapter_number}" + if self.status == "started": + await self._set_adapter_carrier(adapter_number, False) await self._ubridge_send(f"bridge stop {bridge_name}") await self._ubridge_send( "bridge remove_nio_udp bridge{adapter} {lport} {rhost} {rport}".format( diff --git a/gns3server/compute/ubridge/hypervisor.py b/gns3server/compute/ubridge/hypervisor.py index 661da580c..faefcb872 100644 --- a/gns3server/compute/ubridge/hypervisor.py +++ b/gns3server/compute/ubridge/hypervisor.py @@ -160,10 +160,10 @@ class Hypervisor(UBridgeHypervisor): match = re.search(r"ubridge version ([0-9a-z\.]+)", output) if match: self._version = match.group(1) - # uBridge >= 1.2.0 is required for features this server now - # relies on: the AF_UNIX control channel (-U), the marker - # (mark) filter, and the brctl-backed builtin Ethernet Switch. - minimum_required_version = "1.2.0" + # uBridge >= 1.2.3 is required for the AF_UNIX control + # channel, marker filters, builtin Ethernet Switch support, + # and Docker TAP carrier control. + minimum_required_version = "1.2.3" if parse_version(self._version) < parse_version(minimum_required_version): raise UbridgeError(f"uBridge executable version must be >= {minimum_required_version}") else: diff --git a/tests/api/routes/compute/test_docker_nodes.py b/tests/api/routes/compute/test_docker_nodes.py index 278596dba..efadd604b 100644 --- a/tests/api/routes/compute/test_docker_nodes.py +++ b/tests/api/routes/compute/test_docker_nodes.py @@ -247,7 +247,8 @@ class TestDockerNodesRoutes: "lport": 4242, "rport": 4343, "rhost": "127.0.0.1", - "filters": {"packet_loss": 10} + "filters": {"packet_loss": 10}, + "suspend": False } url = app.url_path_for("compute:create_docker_node_nio", @@ -259,6 +260,7 @@ class TestDockerNodesRoutes: assert response.status_code == status.HTTP_201_CREATED assert response.json()["filters"] == {"packet_loss": 10} params["filters"] = {} + params["suspend"] = True url = app.url_path_for("compute:update_docker_node_nio", project_id=vm["project_id"], @@ -270,6 +272,7 @@ class TestDockerNodesRoutes: assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" assert response.json()["filters"] == {} + assert response.json()["suspend"] is True async def test_docker_delete_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 70d72b3bf..7aa5abb12 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -31,7 +31,7 @@ from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Err from gns3server.compute.docker import Docker -from unittest.mock import patch, MagicMock, call +from unittest.mock import AsyncMock, patch, MagicMock, call @pytest_asyncio.fixture @@ -48,6 +48,10 @@ async def vm(compute_project, manager): vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", aux_type="none") vm._cid = "e90e34656842" vm.mac_address = '02:42:3d:b7:93:00' + # Interface monitoring has its own focused tests below. Keep unrelated + # lifecycle tests isolated from a real Docker exec stream. + vm._start_interface_monitor = AsyncioMagicMock() + vm._stop_interface_monitor = AsyncioMagicMock() return vm @@ -1198,6 +1202,7 @@ async def test_start(vm, manager, free_console_port, tmpdir): assert vm._start_ubridge.called assert vm._start_console.called assert vm._start_aux.called + assert vm._start_interface_monitor.called assert vm.status == "started" @@ -1289,12 +1294,137 @@ async def test_start_unpause(vm): assert vm.status == "started" +@pytest.mark.asyncio +async def test_start_already_running_starts_interface_monitor(vm): + + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="running"): + await vm.start() + + assert vm.status == "started" + vm._start_interface_monitor.assert_called_once() + + @pytest.mark.asyncio async def test_restart(vm): with asyncio_patch("gns3server.compute.docker.Docker.query") as mock: await vm.restart() mock.assert_called_with("POST", "containers/e90e34656842/restart") + vm._stop_interface_monitor.assert_called_once() + vm._start_interface_monitor.assert_called_once() + + +@pytest.mark.asyncio +async def test_read_interface_statuses(vm): + + reader = MagicMock() + reader.readline = AsyncMock(side_effect=[ + b"eth0=0x1003\r\n", + b"eth0=0x1003\r\n", # duplicate must not emit again + b"eth0=0x1002\r\n", + b"unknown=0x1003\r\n", + b"eth0=invalid\r\n", + b"", + ]) + vm.project.emit = MagicMock() + + await DockerVM._read_interface_statuses(vm, reader) + + assert vm.project.emit.call_args_list == [ + call("node.interface_status", { + "project_id": vm.project.id, + "node_id": vm.id, + "adapter_number": 0, + "port_number": 0, + "status": "started", + }), + call("node.interface_status", { + "project_id": vm.project.id, + "node_id": vm.id, + "adapter_number": 0, + "port_number": 0, + "status": "stopped", + }), + ] + + +@pytest.mark.asyncio +async def test_read_interface_statuses_periodically_resynchronizes(vm): + + reader = MagicMock() + reader.readline = AsyncMock(side_effect=[b"eth0=0x1003\n", b"eth0=0x1003\n", b""]) + vm._INTERFACE_STATUS_RESYNC_INTERVAL = 0 + vm.project.emit = MagicMock() + + await DockerVM._read_interface_statuses(vm, reader) + + assert vm.project.emit.call_count == 2 + + +@pytest.mark.asyncio +async def test_start_interface_monitor(vm, manager): + + reader = MagicMock() + reader.readuntil = AsyncMock(return_value=b"HTTP/1.1 101 UPGRADED\r\n\r\n") + reader.readline = AsyncMock(return_value=b"") + writer = MagicMock() + writer.drain = AsyncMock() + writer.wait_closed = AsyncMock() + manager._api_version = "1.24" + + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "monitor-exec"}) as query: + with asyncio_patch("asyncio.open_unix_connection", return_value=(reader, writer)): + await DockerVM._start_interface_monitor(vm) + await asyncio.sleep(0) + + request = query.call_args + assert request.args[:2] == ("POST", "containers/e90e34656842/exec") + assert request.kwargs["data"]["Cmd"][-1] == "eth0" + assert vm._interface_monitor_writer is None + writer.close.assert_called_once() + + +@pytest.mark.asyncio +async def test_start_interface_monitor_is_idempotent(vm): + + vm._interface_monitor_task = MagicMock() + vm._interface_monitor_task.done.return_value = False + + with asyncio_patch("gns3server.compute.docker.Docker.query") as query: + await DockerVM._start_interface_monitor(vm) + + query.assert_not_called() + vm._stop_interface_monitor.assert_not_called() + + +@pytest.mark.asyncio +async def test_start_interface_monitor_failure_does_not_fail_node(vm, manager): + + reader = MagicMock() + reader.readuntil = AsyncMock(return_value=b"HTTP/1.1 500 Internal Server Error\r\n\r\n") + writer = MagicMock() + writer.drain = AsyncMock() + writer.wait_closed = AsyncMock() + manager._api_version = "1.24" + + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "monitor-exec"}): + with asyncio_patch("asyncio.open_unix_connection", return_value=(reader, writer)): + await DockerVM._start_interface_monitor(vm) + + assert vm._interface_monitor_task is None + writer.close.assert_called_once() + + +@pytest.mark.asyncio +async def test_start_interface_monitor_rejects_malformed_exec_response(vm): + + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=None): + with asyncio_patch("asyncio.open_unix_connection") as open_connection: + await DockerVM._start_interface_monitor(vm) + + open_connection.assert_not_called() + assert vm._interface_monitor_task is None @pytest.mark.asyncio @@ -1313,6 +1443,7 @@ async def test_stop(vm): assert mock.stop.called assert vm._ubridge_hypervisor is None assert vm._fix_permissions.called + assert vm._stop_interface_monitor.called @pytest.mark.asyncio @@ -1538,11 +1669,12 @@ async def test_add_ubridge_connection(vm): calls = [ call.send('bridge create bridge0'), - call.send("bridge add_nio_tap bridge0 tap-gns3-e0"), + call.send("bridge add_nio_tap bridge0 tap-gns3-e0 off"), call.send('docker move_to_ns tap-gns3-e0 42 eth0'), call.send('bridge add_nio_udp bridge0 4242 127.0.0.1 4343'), call.send('bridge start_capture bridge0 "/tmp/capture.pcap"'), call.send('bridge start bridge0'), + call.send('bridge set_nio_tap_carrier bridge0 on') ] assert 'bridge0' in vm._bridges # We need to check any_order otherwise mock is confused by asyncio @@ -1591,7 +1723,7 @@ async def test_add_ubridge_connection_none_nio(vm): calls = [ call.send('bridge create bridge0'), - call.send("bridge add_nio_tap bridge0 tap-gns3-e0"), + call.send("bridge add_nio_tap bridge0 tap-gns3-e0 off"), call.send('docker move_to_ns tap-gns3-e0 42 eth0'), ] @@ -1600,6 +1732,20 @@ async def test_add_ubridge_connection_none_nio(vm): vm._ubridge_hypervisor.assert_has_calls(calls, any_order=True) +@pytest.mark.asyncio +async def test_set_adapter_carrier(vm): + + vm._ubridge_send = AsyncioMagicMock() + + await vm._set_adapter_carrier(2, True) + await vm._set_adapter_carrier(2, False) + + vm._ubridge_send.assert_has_calls([ + call("bridge set_nio_tap_carrier bridge2 on"), + call("bridge set_nio_tap_carrier bridge2 off"), + ]) + + @pytest.mark.asyncio async def test_add_ubridge_connection_invalid_adapter_number(vm): @@ -1641,6 +1787,22 @@ async def test_adapter_add_nio_binding_1(vm): assert vm._ethernet_adapters[0].get_nio(0) == nio +@pytest.mark.asyncio +async def test_adapter_add_nio_binding_sets_carrier(vm): + + nio = vm.manager.create_nio({ + "type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1" + }) + vm.status = "started" + vm._ubridge_hypervisor = MagicMock() + vm._connect_nio = AsyncioMagicMock() + vm._set_adapter_carrier = AsyncioMagicMock() + + await vm.adapter_add_nio_binding(0, nio) + + vm._set_adapter_carrier.assert_called_once_with(0, True) + + @pytest.mark.asyncio async def test_adapter_udpate_nio_binding_bridge_not_started(vm): @@ -1656,6 +1818,24 @@ async def test_adapter_udpate_nio_binding_bridge_not_started(vm): assert vm._ubridge_apply_filters.called is False +@pytest.mark.asyncio +async def test_adapter_update_nio_binding_sets_suspended_carrier(vm): + + nio = vm.manager.create_nio({ + "type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1" + }) + nio.suspend = True + vm.status = "started" + vm._ubridge_hypervisor = MagicMock() + vm._bridges.add("bridge0") + vm._ubridge_apply_filters = AsyncioMagicMock() + vm._set_adapter_carrier = AsyncioMagicMock() + + await vm.adapter_update_nio_binding(0, nio) + + vm._set_adapter_carrier.assert_called_once_with(0, False) + + @pytest.mark.asyncio async def test_adapter_add_nio_binding_invalid_adapter(vm): @@ -1680,12 +1860,14 @@ async def test_adapter_remove_nio_binding(vm): "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio) await vm.adapter_add_nio_binding(0, nio) + vm.status = "started" with asyncio_patch("gns3server.compute.docker.DockerVM._ubridge_send") as delete_ubridge_mock: await vm.adapter_remove_nio_binding(0) assert vm._ethernet_adapters[0].get_nio(0) is None delete_ubridge_mock.assert_any_call('bridge stop bridge0') delete_ubridge_mock.assert_any_call('bridge remove_nio_udp bridge0 4242 127.0.0.1 4343') + delete_ubridge_mock.assert_any_call('bridge set_nio_tap_carrier bridge0 off') @pytest.mark.asyncio diff --git a/tests/compute/docker/test_vendor_docker_vm.py b/tests/compute/docker/test_vendor_docker_vm.py index 9f578c797..e453a1b62 100644 --- a/tests/compute/docker/test_vendor_docker_vm.py +++ b/tests/compute/docker/test_vendor_docker_vm.py @@ -80,6 +80,10 @@ def _make_vm(compute_project, manager, environment=None, console_type="docker_ex extra_volumes=extra_volumes or [], adapters=adapters, ) vm._cid = "e90e34656842" + # Interface monitoring belongs to DockerVM and is covered by its focused + # tests. Keep vendor-console tests independent from a Docker exec stream. + vm._start_interface_monitor = AsyncioMagicMock() + vm._stop_interface_monitor = AsyncioMagicMock() return vm diff --git a/tests/compute/ubridge/test_hypervisor.py b/tests/compute/ubridge/test_hypervisor.py index 126278ab0..7cba76a2c 100644 --- a/tests/compute/ubridge/test_hypervisor.py +++ b/tests/compute/ubridge/test_hypervisor.py @@ -159,6 +159,37 @@ async def test_stop_tcp_has_no_socket_to_unlink(tmp_path, monkeypatch): await hyp.stop() +# --------------------------------------------------------------------------- +# version requirement +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_requires_ubridge_1_2_3(tmp_path, monkeypatch): + + hyp = _make("unix", tmp_path, monkeypatch) + with patch( + "gns3server.compute.ubridge.hypervisor.subprocess_check_output", + new_callable=AsyncMock, + return_value="ubridge version 1.2.2", + ): + with pytest.raises(UbridgeError, match=r">= 1\.2\.3"): + await hyp._check_ubridge_version() + + +@pytest.mark.asyncio +async def test_accepts_ubridge_1_2_3(tmp_path, monkeypatch): + + hyp = _make("unix", tmp_path, monkeypatch) + with patch( + "gns3server.compute.ubridge.hypervisor.subprocess_check_output", + new_callable=AsyncMock, + return_value="ubridge version 1.2.3", + ): + await hyp._check_ubridge_version() + + assert hyp.version == "1.2.3" + + # --------------------------------------------------------------------------- # start: fail-fast on an immediately-exiting uBridge # --------------------------------------------------------------------------- From 5d08cc92a41907994d6fbb72a91c91a78a9120bd Mon Sep 17 00:00:00 2001 From: Cristi Date: Fri, 4 Sep 2026 17:37:36 +0300 Subject: [PATCH 05/60] fix: Fix failing tests and adress the MCP 2.x incompatibility failures --- mcp-requirements.txt | 2 +- tests/compute/docker/test_docker_vm.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/mcp-requirements.txt b/mcp-requirements.txt index 11f4e4fc8..0c1c2fcb6 100644 --- a/mcp-requirements.txt +++ b/mcp-requirements.txt @@ -5,4 +5,4 @@ # Or directly: pip install -r mcp-requirements.txt # ============================================================================== -fastmcp>=3.4.0 +fastmcp>=3.4.0,<4 diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 7aa5abb12..10336e2b1 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -1327,11 +1327,10 @@ async def test_read_interface_statuses(vm): b"eth0=invalid\r\n", b"", ]) - vm.project.emit = MagicMock() + with patch.object(vm.project, "emit") as emit: + await DockerVM._read_interface_statuses(vm, reader) - await DockerVM._read_interface_statuses(vm, reader) - - assert vm.project.emit.call_args_list == [ + assert emit.call_args_list == [ call("node.interface_status", { "project_id": vm.project.id, "node_id": vm.id, @@ -1355,11 +1354,10 @@ async def test_read_interface_statuses_periodically_resynchronizes(vm): reader = MagicMock() reader.readline = AsyncMock(side_effect=[b"eth0=0x1003\n", b"eth0=0x1003\n", b""]) vm._INTERFACE_STATUS_RESYNC_INTERVAL = 0 - vm.project.emit = MagicMock() + with patch.object(vm.project, "emit") as emit: + await DockerVM._read_interface_statuses(vm, reader) - await DockerVM._read_interface_statuses(vm, reader) - - assert vm.project.emit.call_count == 2 + assert emit.call_count == 2 @pytest.mark.asyncio From 9262dcfaef94b3cd99dbfa1022e07edd252ded73 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 1 Sep 2026 01:34:19 +0800 Subject: [PATCH 06/60] fix: normalize marker.match event tag to int to match the REST schema The WS event carried the tag as str (verbatim from the MARK signal) when present and as int when falling back to the registry, so one tag reached consumers as two different values. Parse the signal's decimal (the exact value we installed via 'mark tag ') and keep the registered int on malformed input; None only when neither side carries one. --- gns3server/compute/marker/marker_listener.py | 15 ++++++++++-- tests/compute/marker/test_marker_manager.py | 24 +++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/gns3server/compute/marker/marker_listener.py b/gns3server/compute/marker/marker_listener.py index 40cdd97b6..a30769c1e 100644 --- a/gns3server/compute/marker/marker_listener.py +++ b/gns3server/compute/marker/marker_listener.py @@ -114,13 +114,24 @@ class MarkerListener(asyncio.DatagramProtocol): # signals that carry no `link=`. signal_link = link if link and link != "-" else None + # Normalize the tag to int so the event matches the REST schema + # (MarkerCreate.tag is Optional[int]): the signal merely echoes the + # decimal we installed via `mark tag `, so parsing cannot + # fail for well-formed signals; a malformed value keeps the registered + # int, and the tag is None only when neither side carries one. + event_tag = registered_tag + if tag and tag != "-": + try: + event_tag = int(tag) + except ValueError: + pass # malformed signal tag: keep the registered value + event = { "project_id": project_id, "node_id": node_id, "link_id": signal_link or link_id, "filter": filter_name, - # Prefer the value carried in the signal; fall back to the one we registered. - "tag": tag if tag and tag != "-" else registered_tag, + "tag": event_tag, "ts": ts, "len": int(length) if length and length.isdigit() else 0, # Travel direction relative to the capture node (node_id above); diff --git a/tests/compute/marker/test_marker_manager.py b/tests/compute/marker/test_marker_manager.py index da495ae92..5917870d7 100644 --- a/tests/compute/marker/test_marker_manager.py +++ b/tests/compute/marker/test_marker_manager.py @@ -121,7 +121,11 @@ class TestMarkerListener: assert ev["node_id"] == "n1" assert ev["link_id"] == "l1" assert ev["filter"] == "f1" - assert ev["tag"] == "7" + # The event tag is normalized to int to match the REST schema — the + # signal echoes the decimal we installed, so str and int variants of + # the same tag must never reach consumers as different keys. + assert ev["tag"] == 7 + assert isinstance(ev["tag"], int) assert ev["ts"] == pytest.approx(1700000000.123456) assert ev["len"] == 98 # No dir= in the signal (legacy uBridge) → undirected. @@ -163,6 +167,24 @@ class TestMarkerListener: lis.datagram_received(b"MARK 2.0 node=n filter=f tag=- len=20\n", None) assert fmgr.events[0][1]["tag"] == 42 + def test_non_numeric_signal_tag_falls_back_to_registered(self): + # A corrupt/unknown signal value must not leak a str tag into the + # event stream — the registry's int wins. + fmgr = FakeMarkerManager() + fmgr.register("p", "n", "f", "l", tag=42) + lis = MarkerListener(fmgr) + lis.connection_made(None) + lis.datagram_received(b"MARK 2.0 node=n filter=f tag=oops len=20\n", None) + assert fmgr.events[0][1]["tag"] == 42 + + def test_no_tag_anywhere_is_none(self): + fmgr = FakeMarkerManager() + fmgr.register("p", "n", "f", "l", tag=None) + lis = MarkerListener(fmgr) + lis.connection_made(None) + lis.datagram_received(b"MARK 2.0 node=n filter=f tag=- len=20\n", None) + assert fmgr.events[0][1]["tag"] is None + def test_link_in_signal_overrides_registry_link(self): # Per-link attribution (contract §3.2/§3.3): the signal's `link=` is # authoritative and must disambiguate links sharing a node+filter — From c646d6df5a297d089fd8fdcf65fd100f5c0a7e8d Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 1 Sep 2026 01:34:33 +0800 Subject: [PATCH 07/60] feat: add tag-keyed aggregate replay over paused markers' pcaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markers on different links sharing a tag form one distributed capture session. Once every marker under the tag is paused (409 otherwise), three read-only endpoints replay it: - GET .../markers/tags/{tag}/replay/range merges the per-marker pcaps by scanning 16-byte record headers only (no tshark) into a timestamp- ordered frame list (per-second buckets above a 5000-frame cap) - GET .../replay/frames?ts=&window_ms= returns frames in [T, T+window]; an empty window is a normal empty array - GET .../replay/frame/detail lazily decodes one frame the user opened: raw bytes for the hex view read straight from the pcap, protocol tree from 'tshark -T pdml' mapped isomorphically to JSON (every attribute survives, values stay strings). tshark reads a /tmp scratch copy with a scratch HOME — hardened profiles deny it the project directory. Sort key is (ts, source file, frame number): ts is not unique across a merge. The ts parameter round-trips as the exact string from the frame list. Round-trip tests pin the PDML→JSON fidelity (element count and attribute coverage). --- docs/README.md | 5 +- docs/features/marker-tag-replay.md | 247 ++++++++++ gns3server/api/routes/controller/projects.py | 83 ++++ gns3server/controller/marker_replay.py | 428 ++++++++++++++++++ gns3server/controller/project.py | 11 + .../routes/controller/test_marker_replay.py | 190 ++++++++ tests/controller/test_marker_replay.py | 346 ++++++++++++++ 7 files changed, 1309 insertions(+), 1 deletion(-) create mode 100644 docs/features/marker-tag-replay.md create mode 100644 gns3server/controller/marker_replay.py create mode 100644 tests/api/routes/controller/test_marker_replay.py create mode 100644 tests/controller/test_marker_replay.py diff --git a/docs/README.md b/docs/README.md index c347479cc..bc3698989 100644 --- a/docs/README.md +++ b/docs/README.md @@ -75,6 +75,9 @@ Web-based packet capture analysis using Docker + xpra HTML5 client. Zero-install ### Marker (Traffic Insight) (`features/marker-traffic-insight.md`) Real-time traffic insight via per-link BPF markers and project-level inherited definitions. A marker taps a link in uBridge, emitting match notifications and pcap capture on BPF hit; definitions fan out to every capable link automatically. +### Marker Tag Replay (`features/marker-tag-replay.md`) +Aggregate playback across links keyed by `tag`: once every marker under a tag is paused, their pcaps merge into one timestamp-ordered timeline; frames are decoded on demand via tshark into an isomorphic JSON protocol tree. The cross-link delta of the same packet measures the intermediate node's forwarding latency. + ### Docker exec Console (Vendor NOS) (`features/docker-exec-console.md`) Console for vendor NOS containers (SR Linux, XRd, …) whose CLI is a TUI off PID 1: runs the vendor CLI via the Docker exec API, plus `GNS3_SKIP_INIT`/`GNS3_INTERFACE_NAMES` boot knobs and SKIP_INIT volume persistence. @@ -127,4 +130,4 @@ Quick-start guide for Ubuntu 24.04: install via PPA, set up dependencies, and ru --- -_Last updated: 2026-08-14_ +_Last updated: 2026-09-01_ diff --git a/docs/features/marker-tag-replay.md b/docs/features/marker-tag-replay.md new file mode 100644 index 000000000..d8439fed0 --- /dev/null +++ b/docs/features/marker-tag-replay.md @@ -0,0 +1,247 @@ + + +> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt. + +# Marker Tag Replay (Aggregate Playback) + +## Overview + +Replays traffic captured by [markers](marker-traffic-insight.md) **across links**, keyed by +`tag`. Markers on different links that share a tag form one *distributed capture session*; +once every marker under the tag is paused, their per-marker pcaps are merged into a single +timestamp-ordered timeline. The Web UI browses that timeline and fetches individual frames +on demand — each fetch decodes exactly one frame via `tshark` into a self-describing JSON +protocol tree. + +The unique observable: the delta between the same packet hitting two consecutive links +measures the **intermediate node's forwarding latency** (host view) — something a +single-link capture can never show. + +## Architecture + +```mermaid +graph TB + UI["Web UI"] + + subgraph Controller["Controller (replay endpoints)"] + GATE["Tag gate
(all markers under tag paused?)"] + SCAN["Timeline scan
(pcap record headers)"] + MAP["PDML → JSON
isomorphic mapper"] + end + + FS[("markers dir
{node}_{link}_{marker}.pcap")] + TS["tshark -T pdml
(one frame at a time)"] + TMP["/tmp scratch copy
(hardened-profile workaround)"] + + UI -->|"GET range / frames"| GATE + GATE --> SCAN + SCAN --> FS + UI -->|"GET frame detail (lazy)"| MAP + MAP --> TMP + TMP --> TS + MAP -->|"hex: raw bytes"| FS +``` + +Two deliberately separated performance regimes: + +| Path | Work | tshark | +|------|------|--------| +| Timeline | 16-byte record-header scan per frame, cross-file merge sort | **never invoked** — browsing works without tshark | +| Frame detail | locate frame → `tshark -T pdml -Y "frame.number == N"` → map to JSON | forked once per frame the user opens | + +tshark call count equals user clicks — no caching or rate limiting needed, and a tshark +failure (501/502) affects only that one frame, never timeline browsing. pcap files are +compute-side (`/project-files/markers/`); the initial scope is single-server +deployments (controller and compute in one process, direct file access) — remote computes +will reuse the existing capture-file proxy pattern. + +## Business Process + +```mermaid +sequenceDiagram + participant UI as Web UI + participant C as Controller + participant PC as markers dir (pcaps) + participant TS as tshark + + Note over UI,PC: ① configure — same tag on every link's marker + UI->>C: POST markers (bpf, tag=666) on each link + + Note over PC: ② capture — uBridge appends matches (flushed per packet), replay forbidden + UI->>C: GET range + C--xC: 409 (a marker under tag 666 is still enabled) + + Note over UI: ③ pause — every marker under the tag + UI->>C: PUT markers {"enabled": false} × each + + Note over UI,TS: ④ replay + UI->>C: GET /markers/tags/666/replay/range + C->>PC: scan record headers, merge sort + C-->>UI: {start, end, sources, frames} + UI->>C: GET frames?ts=T&window_ms=W + C-->>UI: frames in [T, T+W] — or {"frames": []} + UI->>C: GET frame/detail?ts=…&node_id=…&link_id=…&marker=… + C->>PC: locate frame, read raw bytes (hex) + C->>TS: -T pdml (reads the /tmp copy) + TS-->>C: PDML + C-->>UI: protocol tree + hex +``` + +## The tag gate + +Replay reads append-only pcaps, so it is only available while the data is at rest. Every +replay endpoint evaluates the same gate: walk every marker in the project carrying the +requested tag; if any has `enabled: true` → 409 (the response names them); a tag with no +markers at all → 404. + +| Marker state under the tag | pcap file | Replay | +|---------------------------|-----------|--------| +| any `enabled: true` (capturing) | growing | denied — 409 | +| all `enabled: false` (paused) | retained, frozen | **allowed** | +| deleted | file unlinked | no data | +| `bpf`/`tag`/`direction` changed (rebuild) | pcap reopened (truncated) — new session | prior history gone | + +- **Pause, not delete.** Deleting a marker (or its definition) deletes its pcap — replay + before deleting or the data is gone. +- **Pause → resume → pause is fine.** The pcap accumulates the full history; replay covers + everything up to the current pause point. +- uBridge flushes every matched packet to the pcap immediately (`pcap_dump_flush` per + packet under a mutex — verified in the uBridge source), so a pause boundary never loses + tail frames. + +## API Endpoints + +All read-only; JWT bearer token, privilege `Project.Audit`. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/range` | Timeline metadata + full frame list for the tag | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frames?ts=&window_ms=&limit=` | Frames with ts in `[T, T+window]`, merged across sources | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frame/detail?ts=&node_id=&link_id=&marker=` | Single frame: tshark protocol tree + raw hex | + +### `range` — the timeline + +```json +{ + "tag": 666, + "start": "1788196663.226372", + "end": "1788196713.706634", + "frame_count": 20, + "truncated": false, + "sources": [ + { "node_id": "b764c434…", "link_id": "316ef8fd…", "marker": "global-def-…", + "data_link_type": "DLT_EN10MB", "count": 10 } + ], + "frames": [ + { "ts": "1788196663.226372", "len": 98, "node_id": "b764c434…", + "link_id": "316ef8fd…", "marker": "global-def-…", "frame_number": 1 } + ] +} +``` + +- `frames` is the **full, merged, time-ordered list** (cap 5000) — the Web UI lays out the + whole timeline from one request. Over the cap, `frames` is omitted and per-second + `buckets` are returned instead with `truncated: true`. +- Each frame entry carries `(node_id, link_id, marker, frame_number)` — the locating + tuple for the detail request. + +### `frames` — point / window query + +A time with no frames is a normal, successful answer — an empty array, no sentinel strings: + +```json +GET …/replay/frames?ts=1788196700.000&window_ms=500 +→ { "frames": [] } +``` + +### `frame/detail` — lazy single-frame decode + +Invoked only when the user opens a frame. The `ts` must be the **exact string received in +the timeline/frame list** (round-tripped verbatim — never re-serialized through a float); +`node_id + link_id + marker` identify the pcap. The server re-resolves the ts against the +file, guarding against a capture rebuilt between the timeline view and this click. + +```json +{ + "ts": "1788196663.226372", + "source": { "node_id": "b764c434…", "link_id": "316ef8fd…", + "marker": "global-def-…", "frame_number": 1 }, + "tshark_version": "TShark (Wireshark) 4.6.7 …", + "field_count": 85, + "hex": "45000062…", + "tree": [ + { "element": "proto", "name": "ip", + "showname": "Internet Protocol Version 4, Src: 10.1.10.101, Dst: 203.0.113.1", + "children": [ + { "element": "field", "name": "ip.ttl", "show": "64", + "showname": "Time to Live: 64", "value": "40", "size": "1", + "pos": "22", "children": [] } + ] } + ] +} +``` + +- `tree` mirrors PDML **isomorphically**: every ``/`` becomes a node, every + XML attribute (`name`, `show`, `showname`, `value`, `size`, `pos`, `hide`, `mask`, + `unmaskedvalue`) becomes a JSON key, plus one structural key `element` (proto/field). + Nothing is selected out, nothing interpreted, and **all values stay strings** — numeric + conversion is the client's business. +- `hex` is the raw frame bytes read straight from the pcap (not via tshark); keeping + `pos`/`size` on every field enables Wireshark-style *click field → highlight bytes*. +- `field_count` is the mapped node count — a client-side sanity check. + +## Ordering and timestamps + +- `ts` is the pcap record timestamp (µs) written by uBridge at match time — a userspace + `gettimeofday()` instant measured after the packet has crossed the kernel twice. The + last digit or two are scheduling noise; microseconds are sufficient in a simulated + environment. +- The sort key is `(ts, source file, frame_number)` — ts alone is **not** unique (two + links can hit the same microsecond); the tiebreaker yields a stable, determined order + instead of a fictional one. Index structures must never use ts as a dict key, or + same-microsecond frames silently overwrite each other. +- The cross-link delta is the intermediate node's end-to-end forwarding latency + (veth/TAP → guest protocol stack → back to host), typically hundreds of microseconds to + milliseconds. UI labels should read *node forwarding latency (host view)*, not link + propagation delay. A live capture pair confirmed it end-to-end: same `ip.id`, + TTL 64→63, 509 µs between two links. + +## Fidelity guarantee (PDML → JSON) + +The conversion is an isomorphic structure map, not a semantic transform, with two rules: +**map every attribute** and **keep values as strings**. A round-trip test enforces both: +PDML element count equals JSON node count, and every XML attribute survives with an +identical JSON value (`tests/controller/test_marker_replay.py`). The raw frame bytes — +the one thing PDML genuinely does not contain — are covered by `hex` read directly from +the pcap. + +## Error Responses + +| Status | Description | +|--------|-------------| +| 401 | Not authenticated | +| 404 | Tag has no markers in the project; detail source unknown, or ts does not match the file (the capture may have been rebuilt) | +| 409 | Tag gate: a marker under the tag is still `enabled: true` (the response lists them) | +| 501 | tshark not installed / unavailable — affects detail only; the timeline never needs tshark | +| 502 | tshark failed or timed out (10 s); truncated output never reaches the mapper | + +## Notes + +- **Heterogeneous link types coexist.** Frames are never merged into a single pcap + (mergecap is deliberately not used) — each frame carries its source and is decoded + individually, so Ethernet and serial (cHDLC/PPP) markers can share one timeline. + Malformed packets are tshark's problem: it emits `[Malformed Packet]` as regular PDML + and carries on. +- **Hardened tshark profiles.** openSUSE-style profiles (AppArmor &c.) can deny tshark + access to the project directory and the user's home even though the server process can + read both. The detail path therefore copies the pcap to a real file under `/tmp` + (not a symlink — the profile resolves real paths) and gives tshark a scratch `HOME`; + the copy is unlinked afterwards. The hex view still reads the original file. +- **Tag type.** REST and the `marker.match` WS event both carry `tag` as `int` (the + listener normalizes); replay keys on that int value. +- **Follow-ups.** Remote-compute support via the existing capture-file proxy pattern; + convenience APIs (`GET …/markers/tags` to list tags, `POST …/markers/tags/{tag}/pause` + to batch-pause — a one-call path to the replayable state). diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index f9012a8ef..0595bde7b 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -44,6 +44,8 @@ from gns3server.controller.link import _UNSET from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError from gns3server.controller.import_project import import_project as import_controller_project from gns3server.controller.export_project import export_project as export_controller_project +from gns3server.controller import marker_replay +from gns3server.controller.marker_replay import TsharkError, TsharkMissingError from gns3server.utils.asyncio import aiozipstream from gns3server.utils.path import is_safe_path from gns3server.db.repositories.templates import TemplatesRepository @@ -219,6 +221,87 @@ def get_project_markers(project: Project = Depends(dep_project)) -> dict: return project.markers +@router.get( + "/{project_id}/markers/tags/{tag}/replay/range", + dependencies=[Depends(has_privilege("Project.Audit"))], +) +def replay_tag_range(tag: int, project: Project = Depends(dep_project)) -> dict: + """ + Aggregate replay timeline for a tag: merges the pcap of every marker + carrying ``tag`` into one timestamp-ordered view (design reference: + ``marker_replay`` module docstring). + + The tag gate applies: every marker under the tag must be paused + (``enabled: false``) — 409 otherwise. The response carries the timeline + bounds, per-source stats, and the full merged frame list while under the + frame cap (5000); above it the list is replaced by per-second buckets. + + Required privilege: Project.Audit + """ + + return marker_replay.build_timeline(project, tag) + + +@router.get( + "/{project_id}/markers/tags/{tag}/replay/frames", + dependencies=[Depends(has_privilege("Project.Audit"))], +) +def replay_tag_frames( + tag: int, + ts: str, + window_ms: int = 100, + limit: int = 1000, + project: Project = Depends(dep_project), +) -> dict: + """ + Frames with ts in ``[ts, ts + window_ms]`` merged across every source of + the tag. A time with no frames is a normal successful answer: + ``{"frames": []}``. The tag gate applies (409 while any marker captures). + + ``ts`` must be the exact string returned by the range response — never + re-serialize it through a float. + + Required privilege: Project.Audit + """ + + return marker_replay.query_frames(project, tag, ts, window_ms=window_ms, limit=limit) + + +@router.get( + "/{project_id}/markers/tags/{tag}/replay/frame/detail", + dependencies=[Depends(has_privilege("Project.Audit"))], +) +async def replay_tag_frame_detail( + tag: int, + ts: str, + node_id: str, + link_id: str, + marker: str, + project: Project = Depends(dep_project), +) -> dict: + """ + Decode exactly one frame (lazy — invoked when the user opens a frame, + never by the timeline itself): raw bytes for the hex view read straight + from the pcap, protocol tree from ``tshark -T pdml`` mapped isomorphically + to JSON (every PDML attribute survives, values stay strings). + + ``ts`` must be the exact string from the frame list; ``node_id`` + + ``link_id`` + ``marker`` identify the source pcap. The tag gate applies. + + Required privilege: Project.Audit + """ + + try: + return await marker_replay.decode_frame(project, tag, ts, node_id, link_id, marker) + except TsharkMissingError: + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="tshark is not installed on this server — frame detail is unavailable", + ) + except TsharkError as e: + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e)) + + # --------------------------------------------------------------------------- # Project-level marker definitions (global rules inherited by every link) # --------------------------------------------------------------------------- diff --git a/gns3server/controller/marker_replay.py b/gns3server/controller/marker_replay.py new file mode 100644 index 000000000..993bb31f8 --- /dev/null +++ b/gns3server/controller/marker_replay.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python +# +# Copyright (C) 2024 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Tag-keyed aggregate replay over paused markers' pcap files. + +Markers on different links sharing a ``tag`` form one distributed capture +session. This module merges their per-marker pcaps +(``/project-files/markers/{node_id}_{link_id}_{name}.pcap``) into a +single timestamp-ordered timeline and decodes individual frames on demand. + +Two deliberately separated performance regimes: + +* the timeline path reads only the 16-byte pcap record headers — tshark is + never invoked, so browsing works even where tshark is not installed; +* the detail path runs one ``tshark -T pdml`` per frame the caller asks about + (call count = user clicks) and maps the XML to JSON isomorphically — every + PDML attribute survives as a JSON key, values stay strings, nothing is + selected out or interpreted. + +Timestamps are uBridge's userspace ``gettimeofday`` at match time (µs, a +value measured after the packet has crossed the kernel twice — the last +digit or two are scheduling noise). A timestamp is NOT a unique key: the +merge sorts by ``(ts, source file, frame number)`` and index structures must +never use ts alone as a dict key, or same-microsecond frames silently +overwrite each other. +""" + +import asyncio +import logging +import os +import shutil +import struct +import tempfile +import xml.etree.ElementTree as ET + +from .controller_error import ControllerError, ControllerNotFoundError, ControllerBadRequestError + +log = logging.getLogger(__name__) + +# Full frame list is embedded in the range response while under this cap; +# above it the response degrades to start/end + per-second buckets so the +# client is never flooded by a high-traffic BPF. +FRAME_LIST_CAP = 5000 + +# One tshark decode per user click: a generous ceiling, not a rate limiter. +TSHARK_TIMEOUT_SECONDS = 10.0 + + +class TsharkMissingError(ControllerError): + """tshark is not installed (or not on PATH) — frame detail unavailable.""" + + +class TsharkError(ControllerError): + """tshark exited non-zero / timed out / produced unusable output.""" + + +# --------------------------------------------------------------------------- +# pcap record-header scanning (timeline path — no tshark) +# --------------------------------------------------------------------------- + +# magic → (byte order, timestamp unit). Both pcap families uBridge can write +# (libpcap default µs; ns variant accepted defensively) and both endiannesses. +_PCAP_MAGICS = { + 0xA1B2C3D4: ("<", 1), # little-endian, microseconds + 0xD4C3B2A1: (">", 1), # big-endian, microseconds + 0xA1B23C4D: ("<", 1000), # little-endian, nanoseconds + 0x4D3CB2A1: (">", 1000), # big-endian, nanoseconds +} + + +def _format_ts(sec: int, usec: int) -> str: + """Canonical ts string — the exact form clients must round-trip back.""" + + return f"{sec}.{usec:06d}" + + +def _parse_ts(ts: str) -> int: + """Parse a round-tripped ts string to integer microseconds (exact, no floats).""" + + try: + sec, _, frac = ts.partition(".") + usec = int(frac.ljust(6, "0")[:6]) if frac else 0 + return int(sec) * 1_000_000 + usec + except ValueError: + raise ControllerBadRequestError(f"Invalid timestamp: {ts!r}") + + +def scan_pcap_frames(path): + """ + Walk a pcap file reading only record headers. + + :returns: list of ``(ts_sec, ts_usec, incl_len)`` per frame, 1-based order. + Tolerates a truncated tail (stops when a record header claims more bytes + than the file holds) so a snapshot mid-write never raises. + """ + + frames = [] + try: + with open(path, "rb") as f: + data = f.read() + except OSError as e: + raise ControllerError(f"Cannot read marker pcap {path}: {e}") + + if len(data) < 24: + return frames # not even a global header — zero frames + magic = struct.unpack(" 0xFFFF or pos + 16 + incl_len > len(data): + break # truncated tail (snapshot mid-write / torn final record) + # Normalize ns pcaps to µs by truncation — uBridge writes µs anyway. + frames.append((ts_sec, ts_frac // unit if unit > 1 else ts_frac, incl_len)) + pos += 16 + incl_len + return frames + + +def read_frame_bytes(path, frame_number): + """ + Read one frame's raw bytes (hex view) straight from the pcap — never via + tshark. ``frame_number`` is 1-based (the same number tshark's + ``frame.number`` filter uses). + """ + + frames = scan_pcap_frames(path) + if not 1 <= frame_number <= len(frames): + return None + # Offset arithmetic mirrors the header scan: global header + every full + # record before the target + the target's own record header. + offset = 24 + sum(16 + incl for _s, _u, incl in frames[:frame_number - 1]) + 16 + incl_len = frames[frame_number - 1][2] + with open(path, "rb") as f: + f.seek(offset) + return f.read(incl_len).hex() + + +# --------------------------------------------------------------------------- +# Tag gate + timeline assembly +# --------------------------------------------------------------------------- + +def _tag_markers(project, tag): + """ + Every marker entry in the project carrying ``tag`` (flat + ``project.markers`` values: link_id / node_id / name keys included). + """ + + entries = [] + for key, info in project.markers.items(): + if info.get("tag") == tag: + link_id, _, name = key.partition("/") + entries.append({ + "node_id": info["node_id"], + "link_id": link_id, + "marker": name, + "enabled": info.get("enabled", True), + "data_link_type": info.get("data_link_type", "DLT_EN10MB"), + }) + return entries + + +def gate_tag(project, tag): + """ + Replay reads append-only pcaps, so it is only available while the data is + at rest: every marker under the tag must be paused. Raises 409 listing + the still-running markers, 404 when the tag has no markers at all. + """ + + entries = _tag_markers(project, tag) + if not entries: + raise ControllerNotFoundError(f"No markers with tag {tag} in project") + running = [f"{e['marker']} on link {e['link_id']}" for e in entries if e["enabled"]] + if running: + raise ControllerError( + f"Cannot replay tag {tag} while markers are capturing: {', '.join(running)}. " + "Pause every marker under the tag first." + ) + return entries + + +def _merged_frames(project, entries): + """ + Scan every source pcap and merge into one list sorted by + ``(ts, source file, frame number)`` — ts alone is not unique (two links + can hit the same microsecond); the tiebreaker yields a stable, determined + order instead of a fictional one. + """ + + markers_dir = project.markers_directory + merged = [] + sources = [] + for entry in entries: + pcap = os.path.join( + markers_dir, f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}.pcap" + ) + frames = scan_pcap_frames(pcap) if os.path.exists(pcap) else [] + sources.append({**{k: entry[k] for k in ("node_id", "link_id", "marker", "data_link_type")}, + "count": len(frames)}) + for frame_number, (sec, usec, incl_len) in enumerate(frames, start=1): + merged.append({ + "ts": _format_ts(sec, usec), + "ts_us": sec * 1_000_000 + usec, + "len": incl_len, + "node_id": entry["node_id"], + "link_id": entry["link_id"], + "marker": entry["marker"], + "frame_number": frame_number, + "_source": f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}", + }) + merged.sort(key=lambda f: (f["ts_us"], f["_source"], f["frame_number"])) + for frame in merged: + del frame["ts_us"] + del frame["_source"] + return merged, sources + + +def build_timeline(project, tag, frame_cap=FRAME_LIST_CAP): + """ + The ``range`` response: timeline bounds, per-source stats, and (under + ``frame_cap``) the full merged frame list for one-request timeline + layout. Over the cap the list is replaced by per-second buckets. + """ + + entries = gate_tag(project, tag) + frames, sources = _merged_frames(project, entries) + + response = { + "tag": tag, + "start": frames[0]["ts"] if frames else None, + "end": frames[-1]["ts"] if frames else None, + "frame_count": len(frames), + "truncated": len(frames) > frame_cap, + "sources": sources, + } + if len(frames) <= frame_cap: + response["frames"] = frames + else: + buckets = {} + for frame in frames: + second = _parse_ts(frame["ts"]) // 1_000_000 + buckets[second] = buckets.get(second, 0) + 1 + response["buckets"] = [ + {"ts": _format_ts(second, 0), "count": count} + for second, count in sorted(buckets.items()) + ] + return response + + +def query_frames(project, tag, ts, window_ms=100, limit=1000): + """ + Frames with ts in ``[T, T+window_ms]`` merged across sources. A time with + no frames is a normal, successful answer — ``{"frames": []}``. + """ + + entries = gate_tag(project, tag) + frames, _sources = _merged_frames(project, entries) + + start_us = _parse_ts(ts) + end_us = start_us + max(window_ms, 0) * 1000 + hits = [f for f in frames if start_us <= _parse_ts(f["ts"]) <= end_us] + return {"frames": hits[:max(limit, 0)]} + + +# --------------------------------------------------------------------------- +# Frame detail (tshark path — lazy, one frame per call) +# --------------------------------------------------------------------------- + +async def _tshark_version(): + try: + proc = await asyncio.create_subprocess_exec( + "tshark", "--version", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, + ) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=TSHARK_TIMEOUT_SECONDS) + return stdout.decode(errors="replace").splitlines()[0].strip() + except (OSError, asyncio.TimeoutError, IndexError): + raise TsharkMissingError("tshark is not available on this server") + + +def _tshark_scratch_copy(pcap): + """ + Copy the pcap to a scratch file under the system temp dir for tshark to + read. Hardened tshark profiles (AppArmor &c.) can deny it access to the + project directory / the user's home while still allowing /tmp — a real + copy, deliberately not a symlink, since the profile resolves real paths. + Caller must unlink the returned path. + """ + + fd, scratch = tempfile.mkstemp(suffix=".pcap", prefix="gns3-replay-") + os.close(fd) + shutil.copyfile(pcap, scratch) + return scratch + + +def _tshark_env(): + """Scratch HOME so tshark never even tries to read the user's home.""" + + env = dict(os.environ) + env["HOME"] = tempfile.gettempdir() + return env + + +def _pdml_to_nodes(element): + """ + Isomorphic PDML → JSON mapping: every XML attribute becomes a JSON key + verbatim (values stay strings), children nest under ``children``. The + element tag ("proto"/"field") is carried as ``element`` — the one + structural key beyond the attributes, so a renderer can tell a protocol + group from a leaf field (geninfo's tagless names make names unreliable). + """ + + return { + "element": element.tag, + **element.attrib, + "children": [_pdml_to_nodes(child) for child in element], + } + + +def _count_nodes(nodes): + return 1 + sum(_count_nodes(child) for child in nodes.get("children", [])) + + +async def decode_frame(project, tag, ts, node_id, link_id, marker): + """ + Decode exactly one frame: locate its pcap by source identity, verify the + round-tripped ts still matches the file (guards a rebuild between the + timeline view and this click), read the raw bytes for the hex view, and + map tshark's PDML of that single frame to JSON. + """ + + entries = gate_tag(project, tag) + entry = next( + (e for e in entries + if e["node_id"] == node_id and e["link_id"] == link_id and e["marker"] == marker), + None, + ) + if entry is None: + raise ControllerNotFoundError( + f"No marker '{marker}' with tag {tag} on link {link_id} captured by {node_id}" + ) + + pcap = os.path.join(project.markers_directory, f"{node_id}_{link_id}_{marker}.pcap") + if not os.path.exists(pcap): + raise ControllerNotFoundError(f"No capture file for marker '{marker}' (nothing ever matched)") + + frames = scan_pcap_frames(pcap) + # The ts must be the exact string the timeline returned; find the frame + # it identifies rather than trusting any position hint from the client. + frame_number = next( + (i for i, (sec, usec, _len) in enumerate(frames, start=1) + if _format_ts(sec, usec) == ts), + None, + ) + if frame_number is None: + raise ControllerNotFoundError( + f"No frame at ts {ts} in marker '{marker}' (the capture may have been rebuilt)" + ) + + raw_hex = read_frame_bytes(pcap, frame_number) + + if shutil.which("tshark") is None: + raise TsharkMissingError("tshark is not installed — frame detail is unavailable") + version = await _tshark_version() + + # Hand tshark a scratch copy under the temp dir: hardened profiles may + # deny it the project directory even though this process can read it + # (the hex view above reads the original directly). + scratch = _tshark_scratch_copy(pcap) + try: + try: + proc = await asyncio.create_subprocess_exec( + "tshark", "-r", scratch, "-T", "pdml", "-Y", f"frame.number == {frame_number}", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + env=_tshark_env(), + ) + stdout, stderr = await asyncio.wait_for( + proc.communicate(), timeout=TSHARK_TIMEOUT_SECONDS + ) + except OSError as e: + raise TsharkError(f"Could not run tshark: {e}") + except asyncio.TimeoutError: + raise TsharkError(f"tshark timed out after {TSHARK_TIMEOUT_SECONDS:.0f}s") + if proc.returncode != 0 or not stdout.strip(): + # Never feed truncated/failed output to the mapper. + raise TsharkError(f"tshark failed: {stderr.decode(errors='replace').strip()[:500]}") + finally: + try: + os.unlink(scratch) + except OSError: + pass + + try: + root = ET.fromstring(stdout) + except ET.ParseError as e: + raise TsharkError(f"Malformed PDML from tshark: {e}") + + packet = root.find("./packet") + tree = [_pdml_to_nodes(child) for child in packet] if packet is not None else [] + return { + "ts": ts, + "source": {"node_id": node_id, "link_id": link_id, "marker": marker, + "frame_number": frame_number}, + "tshark_version": version, + "field_count": sum(_count_nodes(node) for node in tree), + "hex": raw_hex, + "tree": tree, + } diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 01506a734..7f1fad683 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -504,6 +504,17 @@ class Project: os.makedirs(path, exist_ok=True) return path + @property + def markers_directory(self): + """ + Location of the marker pcap files (same layout the compute side writes + via ``markers_working_directory`` — single-server deployments share the + project directory, which is what tag replay reads). + """ + path = os.path.join(self._path, "project-files", "markers") + os.makedirs(path, exist_ok=True) + return path + @property def pictures_directory(self): """ diff --git a/tests/api/routes/controller/test_marker_replay.py b/tests/api/routes/controller/test_marker_replay.py new file mode 100644 index 000000000..9a19e291d --- /dev/null +++ b/tests/api/routes/controller/test_marker_replay.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +# +# Copyright (C) 2025 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +HTTP-route tests for the tag replay endpoints: the tag gate (409 while any +marker captures, 404 unknown tag), the merged timeline, window queries +(empty window = success), and the lazy frame detail (ts guard, isomorphic +JSON, 501 when tshark is unavailable). +""" + +import shutil +from unittest.mock import patch + +import pytest +from fastapi import FastAPI, status +from httpx import AsyncClient + +from gns3server.controller.project import Project +from gns3server.controller.udp_link import UDPLink + +from tests.controller.test_marker_replay import _write_pcap, _icmp_frame + +pytestmark = pytest.mark.asyncio + +tshark_present = pytest.mark.skipif(shutil.which("tshark") is None, reason="tshark not installed") + + +def _add_marker(project, tag, enabled, node_id, frames=None): + """Create a paused/capturing link+marker and optionally its pcap.""" + + link = UDPLink(project) + link._markers["icmp"] = {"bpf": "icmp", "tag": tag, "enabled": enabled, "color": None, + "highlight_duration": None, "capture_node_id": node_id, + "direction": None, "data_link_type": "DLT_EN10MB"} + project._links[link.id] = link + if frames is not None: + _write_pcap( + f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames + ) + return link + + +class TestReplayRoutes: + + async def test_range_409_while_capturing(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + _add_marker(project, tag=7, enabled=False, node_id="n1") + running = _add_marker(project, tag=7, enabled=True, node_id="n2") + + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7) + ) + assert response.status_code == status.HTTP_409_CONFLICT + assert "icmp" in response.json()["message"] + assert running.id in response.json()["message"] + + async def test_range_404_unknown_tag(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + _add_marker(project, tag=7, enabled=False, node_id="n1") + + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=99) + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + async def test_range_merges_sources_in_ts_order(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + # r1→r2 captures at t1 and t3; r2→r3 captures at t2 and t3 (same µs + # as source A's t3 — the tiebreak must keep both frames). + _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 500000, b"a" * 60), + (1693472002, 0, b"a" * 60), + ]) + _add_marker(project, tag=7, enabled=False, node_id="n2", frames=[ + (1693472001, 0, b"b" * 60), + (1693472002, 0, b"b" * 60), + ]) + + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7) + ) + assert response.status_code == status.HTTP_200_OK + body = response.json() + assert body["tag"] == 7 + assert body["frame_count"] == 4 + assert body["start"] == "1693472000.500000" + assert body["end"] == "1693472002.000000" + assert body["truncated"] is False + assert [f["node_id"] for f in body["frames"]] == ["n1", "n2", "n1", "n2"] + assert [f["ts"] for f in body["frames"]] == [ + "1693472000.500000", "1693472001.000000", + "1693472002.000000", "1693472002.000000", + ] + assert len(body["sources"]) == 2 + + async def test_frames_window_miss_is_empty_success( + self, app: FastAPI, client: AsyncClient, project: Project + ) -> None: + + _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 0, b"a" * 60), + ]) + + response = await client.get( + app.url_path_for("replay_tag_frames", project_id=project.id, tag=7), + params={"ts": "1693472001.000000", "window_ms": 100}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.json() == {"frames": []} + + async def test_frames_window_hit(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 0, b"a" * 60), + (1693472000, 150000, b"a" * 60), + ]) + + response = await client.get( + app.url_path_for("replay_tag_frames", project_id=project.id, tag=7), + params={"ts": "1693472000.000000", "window_ms": 150}, + ) + assert response.status_code == status.HTTP_200_OK + assert [f["ts"] for f in response.json()["frames"]] == [ + "1693472000.000000", "1693472000.150000" + ] + + async def test_detail_404_on_ts_mismatch(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + ]) + + response = await client.get( + app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), + params={"ts": "1.000000", "node_id": "n1", "link_id": link.id, "marker": "icmp"}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert "rebuilt" in response.json()["message"] + + async def test_detail_501_without_tshark(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + ]) + + with patch("gns3server.controller.marker_replay.shutil.which", return_value=None): + response = await client.get( + app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), + params={"ts": "1693472000.123456", "node_id": "n1", + "link_id": link.id, "marker": "icmp"}, + ) + assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED + + @tshark_present + async def test_detail_decodes_single_frame(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + ]) + + response = await client.get( + app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), + params={"ts": "1693472000.123456", "node_id": "n1", + "link_id": link.id, "marker": "icmp"}, + ) + assert response.status_code == status.HTTP_200_OK + body = response.json() + assert body["source"]["frame_number"] == 1 + assert body["hex"] == _icmp_frame().hex() + assert body["field_count"] > 10 + assert "tshark" in body["tshark_version"].lower() + + ip = next(p for p in body["tree"] if p.get("name") == "ip") + ttl = next(f for f in ip["children"] if f.get("name") == "ip.ttl") + # Values arrive as strings, exactly as tshark emitted them. + assert ttl["show"] == "64" and ttl["showname"] == "Time to Live: 64" diff --git a/tests/controller/test_marker_replay.py b/tests/controller/test_marker_replay.py new file mode 100644 index 000000000..e33fb9a8c --- /dev/null +++ b/tests/controller/test_marker_replay.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python +# +# Copyright (C) 2025 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Unit tests for the tag-keyed aggregate replay module (controller layer): + +* pcap record-header scanning (ts extraction, truncated-tail tolerance, + nanosecond-magic normalization) and raw-bytes reads for the hex view +* the tag gate (404 unknown tag, 409 while any marker captures) +* timeline assembly: cross-source merge ordered by (ts, source, frame number), + same-microsecond tiebreak, missing-pcap sources, frame-cap degradation to + per-second buckets +* window queries (inclusive bounds, empty-window success) +* the lazy frame detail: round-tripped-ts guard, and — where tshark is + installed — the isomorphic PDML → JSON mapping with a round-trip check + (element count and attribute coverage). +""" + +import os +import shutil +import struct + +import pytest +from types import SimpleNamespace + +from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError +from gns3server.controller.marker_replay import ( + build_timeline, + decode_frame, + query_frames, + read_frame_bytes, + scan_pcap_frames, + _format_ts, + _parse_ts, +) + +pytestmark = pytest.mark.asyncio + +PCAP_MAGIC_US = 0xA1B2C3D4 +PCAP_MAGIC_NS = 0xA1B23C4D + + +def _write_pcap(path, frames, magic=PCAP_MAGIC_US, snaplen=65535): + """frames: list of (sec, frac, payload bytes); frac is µs (or ns for the ns magic).""" + + with open(path, "wb") as f: + f.write(struct.pack("> 16: + s = (s & 0xFFFF) + (s >> 16) + return (~s) & 0xFFFF + + icmp = bytes([8, 0, 0, 0]) + struct.pack(">HHH", 1, 1, 0) + b"payload12" + icmp = icmp[:2] + struct.pack(">H", cksum(icmp)) + icmp[4:] + ip0 = struct.pack(">BBHHHBBH4s4s", 0x45, 0, 20 + len(icmp), 1, 0, 64, 1, 0, + bytes([10, 0, 0, 1]), bytes([10, 0, 0, 3])) + ip = ip0[:10] + struct.pack(">H", cksum(ip0)) + ip0[12:] + return bytes.fromhex("0200000000020200000000010800") + ip + icmp + + +def _fake_project(tmp_path, markers, markers_dir=None): + """markers: the flat project.markers shape ({'link/name': {..., node_id}}).""" + + return SimpleNamespace(markers=markers, markers_directory=str(markers_dir or tmp_path)) + + +def _marker_entry(tag, enabled=True, node_id="node-1"): + return {"bpf": "icmp", "tag": tag, "enabled": enabled, "color": None, + "highlight_duration": None, "capture_node_id": node_id, + "direction": None, "data_link_type": "DLT_EN10MB", + "node_id": node_id} + + +# --------------------------------------------------------------------------- +# pcap scanning +# --------------------------------------------------------------------------- + +class TestScanPcap: + + async def test_scans_frames_and_truncated_tail(self, tmp_path): + pcap = tmp_path / "a.pcap" + _write_pcap(pcap, [ + (1693472000, 123456, b"x" * 60), + (1693472001, 654321, b"y" * 40), + ]) + # Tear the final record in half: a snapshot mid-write must not raise. + data = bytearray(pcap.read_bytes()) + pcap.write_bytes(data[:len(data) - 20]) + + frames = scan_pcap_frames(str(pcap)) + assert frames == [(1693472000, 123456, 60)] + + async def test_ns_magic_normalized_to_us(self, tmp_path): + pcap = tmp_path / "ns.pcap" + _write_pcap(pcap, [(1693472000, 1500000, b"z" * 10)], magic=PCAP_MAGIC_NS) + assert scan_pcap_frames(str(pcap)) == [(1693472000, 1500, 10)] # 1.5 ms in µs + + async def test_read_frame_bytes_offsets(self, tmp_path): + pcap = tmp_path / "b.pcap" + _write_pcap(pcap, [ + (100, 0, b"first" + b"0" * 55), # 60 bytes + (200, 0, b"second"), # 6 bytes + ]) + assert read_frame_bytes(str(pcap), 2) == b"second".hex() + assert read_frame_bytes(str(pcap), 1) == (b"first" + b"0" * 55).hex() + assert read_frame_bytes(str(pcap), 3) is None + + async def test_ts_string_round_trip_is_exact(self): + ts = _format_ts(1693472000, 5) + assert ts == "1693472000.000005" + assert _parse_ts(ts) == 1693472000000005 + assert _parse_ts(_format_ts(1693472000, 123456)) == 1693472000123456 + + +# --------------------------------------------------------------------------- +# Tag gate + timeline +# --------------------------------------------------------------------------- + +class TestGateAndTimeline: + + async def test_unknown_tag_404(self, tmp_path): + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=1)}) + with pytest.raises(ControllerNotFoundError): + build_timeline(project, tag=7) + + async def test_gate_409_while_capturing(self, tmp_path): + project = _fake_project(tmp_path, { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + "linkB/icmp": _marker_entry(tag=7, enabled=True, node_id="n2"), + }) + with pytest.raises(ControllerError, match="linkB"): + build_timeline(project, tag=7) + + async def test_merge_orders_by_ts_with_stable_tiebreak(self, tmp_path): + # Two sources, deliberately interleaved in time, colliding on one µs. + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 500000, b"a" * 60), # t1 sourceA + (1693472002, 000000, b"a" * 60), # t3 sourceA + ]) + _write_pcap(tmp_path / "n2_linkB_icmp.pcap", [ + (1693472001, 000000, b"b" * 60), # t2 sourceB + (1693472002, 000000, b"b" * 60), # t3 sourceB — same µs as t3 sourceA + ]) + project = _fake_project(tmp_path, { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + "linkB/icmp": _marker_entry(tag=7, enabled=False, node_id="n2"), + }) + + timeline = build_timeline(project, tag=7) + assert timeline["frame_count"] == 4 + assert timeline["start"] == "1693472000.500000" + assert timeline["end"] == "1693472002.000000" + assert [f["node_id"] for f in timeline["frames"]] == ["n1", "n2", "n1", "n2"] + # Same-microsecond pair keeps both frames (a ts dict key would drop one). + assert [f["ts"] for f in timeline["frames"]][2:] == ["1693472002.000000"] * 2 + assert [f["frame_number"] for f in timeline["frames"]] == [1, 1, 2, 2] + assert {s["count"] for s in timeline["sources"]} == {2} + + async def test_missing_pcap_is_zero_count_source(self, tmp_path): + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False)}) + timeline = build_timeline(project, tag=7) + assert timeline["frame_count"] == 0 + assert timeline["start"] is None and timeline["end"] is None + assert timeline["frames"] == [] + assert timeline["sources"][0]["count"] == 0 + + async def test_over_cap_degrades_to_buckets(self, tmp_path): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 0, b"a" * 60), (1693472000, 500000, b"a" * 60), + (1693472001, 0, b"a" * 60), + ]) + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) + + timeline = build_timeline(project, tag=7, frame_cap=2) + assert timeline["truncated"] is True + assert "frames" not in timeline + assert timeline["buckets"] == [ + {"ts": "1693472000.000000", "count": 2}, + {"ts": "1693472001.000000", "count": 1}, + ] + + +# --------------------------------------------------------------------------- +# Window query +# --------------------------------------------------------------------------- + +class TestQueryFrames: + + def _project(self, tmp_path): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 0, b"a" * 60), + (1693472000, 150000, b"a" * 60), + (1693472005, 0, b"a" * 60), + ]) + return _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) + + async def test_window_inclusive_bounds(self, tmp_path): + result = query_frames(self._project(tmp_path), tag=7, ts="1693472000.000000", window_ms=150) + assert [f["ts"] for f in result["frames"]] == ["1693472000.000000", "1693472000.150000"] + + async def test_window_miss_is_empty_success(self, tmp_path): + result = query_frames(self._project(tmp_path), tag=7, ts="1693472001.000000", window_ms=100) + assert result == {"frames": []} + + async def test_limit_applies(self, tmp_path): + result = query_frames(self._project(tmp_path), tag=7, ts="1693472000.000000", + window_ms=150, limit=1) + assert len(result["frames"]) == 1 + + +# --------------------------------------------------------------------------- +# Frame detail (tshark path) +# --------------------------------------------------------------------------- + +tshark_present = pytest.mark.skipif(shutil.which("tshark") is None, reason="tshark not installed") + + +class TestDecodeFrame: + + def _project(self, tmp_path): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 123456, _icmp_frame()), + ]) + return _fake_project(tmp_path, { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + }) + + async def test_ts_mismatch_guard_404(self, tmp_path): + project = self._project(tmp_path) + with pytest.raises(ControllerNotFoundError, match="rebuilt"): + await decode_frame(project, tag=7, ts="1.000000", + node_id="n1", link_id="linkA", marker="icmp") + + async def test_unknown_source_404(self, tmp_path): + project = self._project(tmp_path) + with pytest.raises(ControllerNotFoundError): + await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="nobody", link_id="linkA", marker="icmp") + + async def test_decode_feeds_tshark_a_scratch_copy(self, tmp_path): + """Hardened tshark profiles deny the project dir — tshark must read a + /tmp copy (a real copy, not a symlink) that is unlinked afterwards.""" + + import tempfile + from unittest.mock import patch, AsyncMock + + observed = [] + PDML = (b'' + b'' + b'') + + class FakeProc: + returncode = 0 + + async def communicate(self): + return PDML, b"" + + async def fake_exec(*args, **kwargs): + r_index = args.index("-r") + observed.append((args[r_index + 1], kwargs.get("env"))) + return FakeProc() + + project = self._project(tmp_path) + with patch("gns3server.controller.marker_replay.shutil.which", return_value="tshark"), \ + patch("gns3server.controller.marker_replay._tshark_version", AsyncMock(return_value="tshark 4.6.7")), \ + patch("gns3server.controller.marker_replay.asyncio.create_subprocess_exec", side_effect=fake_exec): + detail = await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="n1", link_id="linkA", marker="icmp") + + assert detail["field_count"] == 2 # proto + field from the canned PDML + (scratch, env), = observed + original = str(tmp_path / "n1_linkA_icmp.pcap") + assert scratch != original + assert scratch.startswith(tempfile.gettempdir()) and scratch.endswith(".pcap") + assert env["HOME"] == tempfile.gettempdir() + assert not os.path.exists(scratch) # cleaned up after the decode + + @tshark_present + async def test_decode_isomorphic_mapping(self, tmp_path): + import asyncio + import xml.etree.ElementTree as ET + + project = self._project(tmp_path) + detail = await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="n1", link_id="linkA", marker="icmp") + + assert detail["source"]["frame_number"] == 1 + assert detail["hex"] == _icmp_frame().hex() + assert detail["field_count"] > 0 + assert "tshark" in detail["tshark_version"].lower() + + # Round-trip fidelity: node count equals the PDML element count + # (protos + fields, excluding the container itself)… + proc = await asyncio.create_subprocess_exec( + "tshark", "-r", str(tmp_path / "n1_linkA_icmp.pcap"), "-T", "pdml", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, + ) + stdout, _ = await proc.communicate() + packet = ET.fromstring(stdout).find("./packet") + xml_elements = [e for e in packet.iter() if e is not packet] + assert detail["field_count"] == len(xml_elements) + + # …and every XML attribute survives verbatim as a JSON string key. + def walk(element, node): + for key, value in element.attrib.items(): + assert node.get(key) == value + assert all(isinstance(v, str) for k, v in node.items() if k != "children") + for child, child_node in zip(element, node["children"]): + walk(child, child_node) + + for element, node in zip(packet, detail["tree"]): + walk(element, node) + + # Values stay strings (no numeric re-typing). + ttl = next( + f for p in detail["tree"] if p.get("name") == "ip" + for f in p["children"] if f.get("name") == "ip.ttl" + ) + assert ttl["show"] == "64" and ttl["showname"] == "Time to Live: 64" From 49c8a721036c23c0aaf3212841fea671d34c59fc Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 4 Sep 2026 14:07:35 +0800 Subject: [PATCH 08/60] fix: make the patched get_default_project_directory order-safe in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A product module first-imported while run_around_tests' autouse monkeypatch of gns3server.utils.path.get_default_project_directory is active (e.g. 'from gns3server.api.server import app' inside a test body) binds the patched lambda into its own namespace forever: module-level from-imports capture the object by value. Once the patch is reverted, that module keeps calling the stale lambda, which closed over the very test's tmppath — a directory deleted at that test's teardown. Every later test hitting psutil.disk_usage(get_default_project_directory()) then fails with FileNotFoundError, but only when the importing test runs before the API test files (full-suite collection order hides it). The replacement now resolves Config.instance().settings at call time instead of closing over the tmppath, so a frozen reference stays correct; the test that triggered this imports gns3_app at module top so no product import ever happens inside a patched window. Also pins pytest-random-order (inert without --random-order) and documents the order-independence rules for new tests in the gns3-api-test-writing skill, including the known-red legacy files whose tests share rows sequentially. --- .claude/skills/gns3-api-test-writing/SKILL.md | 15 +++++++++++++++ dev-requirements.txt | 3 ++- tests/conftest.py | 8 +++++++- tests/controller/test_compute.py | 5 ++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.claude/skills/gns3-api-test-writing/SKILL.md b/.claude/skills/gns3-api-test-writing/SKILL.md index 6149bf21f..991df03f2 100644 --- a/.claude/skills/gns3-api-test-writing/SKILL.md +++ b/.claude/skills/gns3-api-test-writing/SKILL.md @@ -56,3 +56,18 @@ Always pass `secret_key=DEFAULT_JWT_SECRET_KEY`: the autouse `run_around_tests` ## Failure-Diagnosis Heuristic **Passes in isolation, fails in a class run → suspect shared fixture state first** (`base_client.headers`, the `Config` singleton, class-scoped DB rows) — never the product code. Reproduce with `-k "test_a or test_b"` pairs to find the polluting test. Do not add debug prints to product code to chase test-order issues; make the test order-independent with explicit per-request headers instead. + +## Order Independence + +The suite runs in collection order by default, and a full run is green — but that hides order dependencies. Two real incident classes so far: + +1. **The frozen from-import** (fixed 2026-09): `run_around_tests` monkeypatches `gns3server.utils.path.get_default_project_directory` with a lambda. A product module first-imported *while that patch is active* (e.g. `from gns3server.api.server import app` written inside a test body) freezes the patched lambda into its namespace forever — later tests then get a deleted tmpdir path (`FileNotFoundError` from `psutil.disk_usage`). The patched lambda now resolves `Config.instance()` at call time, so freezing is benign — keep it that way. +2. **Sequential-scenario DB tests**: `tests/api/routes/controller/test_users.py`, `test_roles.py`, `test_pools.py`, `test_templates.py`, `test_images.py`, `test_groups.py`, `test_appliances.py`, `test_acl.py` and `tests/controller/test_rbac.py` build shared rows across tests within a file (a test asserts on users/roles created by earlier tests). They are known-red under any reordering — do not copy this pattern into new files. + +### Rules for new tests + +- **Import product modules at test-module top level**, never first-import inside a test body (an autouse fixture's monkeypatches are live there, and module import executes product `from`-imports). +- **Autouse patch replacements must resolve state at call time** (`Config.instance().settings...`), never close over test-local values (tmppaths, fixture objects) — a closed-over value survives into other tests if the replacement object gets frozen anywhere. +- **Verify a new test file is order-independent**: `venv/bin/python -m pytest tests/.py --random-order --random-order-seed=1 -q` (and a second seed). It must pass shuffled. `pytest-random-order` is pinned in `dev-requirements.txt`; it is inert unless `--random-order` is passed. +- A test that needs specific rows creates them itself (or via a fixture) — never relies on rows another test in the file created. +- Diagnosing a suspected order bug: rerun the exact failing pair with the seed printed by `--random-order` (`--random-order-seed=` reproduces it), then bisect to the polluting test. diff --git a/dev-requirements.txt b/dev-requirements.txt index 55aac6eaa..80d7da3b4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,4 +3,5 @@ flake8==7.3.0 pytest-timeout==2.4.0 pytest-asyncio==1.4.0 httpx==0.28.1 -httpx_ws==0.7.2 # upgrading leads to failures in tests \ No newline at end of file +httpx_ws==0.7.2 # upgrading leads to failures in tests +pytest-random-order==1.2.0 # opt-in: --random-order --random-order-bucket=global (legacy suites are not shuffle-clean yet, see .claude/skills/gns3-api-test-writing) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index f67b04839..b0994ebb8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -418,7 +418,13 @@ def run_around_tests(monkeypatch, config, port_manager): # avoid monitoring for new images while testing config.settings.Server.auto_discover_images = False - monkeypatch.setattr("gns3server.utils.path.get_default_project_directory", lambda *args: os.path.join(tmppath, 'projects')) + # Resolve the projects directory from the Config singleton at call time instead of + # closing over this test's tmppath: a from-import executed anywhere while this + # patch is active (e.g. the first import of gns3server.api.server from inside a + # test body) freezes the patched object into the importing module's namespace + # forever, and a closed-over path would then point at a deleted directory in + # every later test (order-dependent FileNotFoundError in psutil.disk_usage). + monkeypatch.setattr("gns3server.utils.path.get_default_project_directory", lambda *args: Config.instance().settings.Server.projects_path) # Force sys.platform to the original value. Because it seems not be restored correctly after each test sys.platform = sys.original_platform diff --git a/tests/controller/test_compute.py b/tests/controller/test_compute.py index 58ba606c4..671b8e7a0 100644 --- a/tests/controller/test_compute.py +++ b/tests/controller/test_compute.py @@ -25,6 +25,7 @@ from unittest.mock import patch, MagicMock from gns3server.controller.project import Project from gns3server.controller.compute import Compute +from gns3server.api.server import app as gns3_app from gns3server.controller.controller_error import ( ControllerError, ControllerNotFoundError, @@ -581,8 +582,10 @@ async def test_connect_notification_poison_frame_autoreconnects(compute, monkeyp compute._http_session = session # allow the reconnection to be scheduled during the test + # (import gns3_app at module top: a first import from inside a test body + # would execute module-level from-imports while the autouse fixture's + # monkeypatches are active, freezing patched objects into namespaces) monkeypatch.delattr(sys, "_called_from_test", raising=False) - from gns3server.api.server import app as gns3_app monkeypatch.setattr(gns3_app.state, "exiting", False) async def fake_connect(): From d4f2a429f409e9060255afe538c5b1948105e5c7 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 2 Sep 2026 22:33:23 +0800 Subject: [PATCH 09/60] docs: document that restarting capture nodes truncates marker pcaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A marker pcap's lifetime equals its uBridge's lifetime: a fresh uBridge reinstalls every desired marker (paused ones too — install first, then turn the filter off) and pcap_dump_open truncates. Server restart + project reopen without starting nodes is safe until a uBridge comes up (verified live); Docker nodes restart effectively on server restart via stale-container cleanup. Recorded in the tag-gate table, a lifecycle note, and a pcap_dump_open_append follow-up. --- docs/features/marker-tag-replay.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/features/marker-tag-replay.md b/docs/features/marker-tag-replay.md index d8439fed0..4d148a5c8 100644 --- a/docs/features/marker-tag-replay.md +++ b/docs/features/marker-tag-replay.md @@ -103,11 +103,19 @@ markers at all → 404. | all `enabled: false` (paused) | retained, frozen | **allowed** | | deleted | file unlinked | no data | | `bpf`/`tag`/`direction` changed (rebuild) | pcap reopened (truncated) — new session | prior history gone | +| capture node (re)started | pcap reopened (truncated) — new session | prior history gone | - **Pause, not delete.** Deleting a marker (or its definition) deletes its pcap — replay before deleting or the data is gone. - **Pause → resume → pause is fine.** The pcap accumulates the full history; replay covers everything up to the current pause point. +- **The replay window ends when nodes restart.** A pcap's lifetime equals its uBridge's + lifetime: a fresh uBridge reinstalls every desired marker — paused ones too (install + first, then turn the filter off) — and uBridge opens the pcap with truncate semantics + (`pcap_dump_open`, not `_append`). Server restart + project reopen **without starting + nodes** is safe: nothing touches the files until a uBridge comes up (verified live). + Docker nodes effectively restart on server restart as well (stale-container cleanup), + so their window is shorter still. - uBridge flushes every matched packet to the pcap immediately (`pcap_dump_flush` per packet under a mutex — verified in the uBridge source), so a pause boundary never loses tail frames. @@ -244,4 +252,6 @@ the pcap. listener normalizes); replay keys on that int value. - **Follow-ups.** Remote-compute support via the existing capture-file proxy pattern; convenience APIs (`GET …/markers/tags` to list tags, `POST …/markers/tags/{tag}/pause` - to batch-pause — a one-call path to the replayable state). + to batch-pause — a one-call path to the replayable state); uBridge-side + `pcap_dump_open_append` (with a linktype-header check on the existing file) so capture + history survives node restarts instead of being truncated on every reinstall. From e272ad915b8f5d3999ffb7f1a3b7a58368a51bd1 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 30 Aug 2026 12:16:12 +0800 Subject: [PATCH 10/60] feat: add IOL (iol-runner) Docker node support Run Cisco CML containerized IOL images (e.g. iol-xe/iol-xe:17-18-02, driven by virl.lab/cmd/iol-runner) as GNS3 Docker router nodes: - VendorDockerVM: generic GNS3_UNIX_SOCKET_NIO/GNS3_UNIX_SOCKET_DIR knobs wiring adapters through AF_UNIX datagram socket pairs (add_nio_unix cNN.sock sNN.sock) instead of TAP + move_to_ns; node creation fails if the socket dir is not a persisted volume. - IOLDockerVM (GNS3_IOL_RUNNER=1): forces skip-init + unix-socket NIO + /config,/tmp volumes, writes iol-config.json on every start (num-eth tracks adapters, runner drops to the server uid/gid so the sockets are reachable), pre-creates tmp/run, cleans stale sockets after unclean kills, and makes reload a graceful stop + full start (NVRAM flush + rewiring). Console stays telnet on PID 1 stdio. - Manager selects the node class from console_type or GNS3_* environment markers (create-time, like console_type). - Image-free tests (25) and feature documentation. --- docs/README.md | 3 + docs/features/iol-runner-docker.md | 118 +++++ gns3server/compute/docker/__init__.py | 15 +- gns3server/compute/docker/iol_docker_vm.py | 189 +++++++ gns3server/compute/docker/vendor_docker_vm.py | 106 ++++ tests/compute/docker/test_iol_docker_vm.py | 487 ++++++++++++++++++ 6 files changed, 917 insertions(+), 1 deletion(-) create mode 100644 docs/features/iol-runner-docker.md create mode 100644 gns3server/compute/docker/iol_docker_vm.py create mode 100644 tests/compute/docker/test_iol_docker_vm.py diff --git a/docs/README.md b/docs/README.md index c347479cc..50c432faf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -81,6 +81,9 @@ Console for vendor NOS containers (SR Linux, XRd, …) whose CLI is a TUI off PI ### Cisco XRd Control Plane (`features/vendor-nos-xrd.md`) Cisco XRd as a GNS3 Docker router: vendor path + shm/device injection (`GNS3_SHM_SIZE`/`GNS3_DEVICES`), config-file injection (`extra_configs`), udev masking (`GNS3_MASK_UDEV`) so privileged systemd containers don't disturb the host, and the host-readiness check. +### IOL Images with iol-runner (`features/iol-runner-docker.md`) +Cisco CML containerized IOL (e.g. `iol-xe/iol-xe:17-18-02`) as GNS3 Docker routers: generic unix-socket NIO (`GNS3_UNIX_SOCKET_NIO` — adapters wired via AF_UNIX datagram socket pairs instead of TAP/netns) plus `IOLDockerVM` (`GNS3_IOL_RUNNER=1` — per-start config generation, `/tmp/run` preparation, stale-socket cleanup, console on PID 1 stdio). + --- ## GNS3 AI Copilot (`gns3-copilot/`) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md new file mode 100644 index 000000000..10bca7685 --- /dev/null +++ b/docs/features/iol-runner-docker.md @@ -0,0 +1,118 @@ + + +> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt. + + +# IOL Images with iol-runner (Cisco CML containerized IOL) as Docker Nodes + +## Overview + +IOL images packaged with Cisco CML's container runner — for example +`iol-xe/iol-xe:17-18-02` (IOS-XE 17.18.02 IOL in a scratch image driven by +`iol-runner`, module `virl.lab/cmd/iol-runner`) — run as first-class GNS3 +Docker router nodes with **zero changes to the image**. The integration adds +two generic server mechanisms: + +1. **Unix-socket NIO** (`GNS3_UNIX_SOCKET_NIO=1`, on `VendorDockerVM`): link + adapters through per-interface AF_UNIX datagram sockets instead of a TAP + interface moved into the container's network namespace. +2. **`IOLDockerVM`** (marker `GNS3_IOL_RUNNER=1`): generates the runner's + config file per start, prepares its runtime directory and cleans up stale + sockets — the iol-runner-specific glue on top of the vendor path. + +## How the image works + +```mermaid +graph LR + subgraph Container["scratch container (PID 1)"] + RUNNER["/iol-runner -config /config/iol-config.json -stdio"] + IOL["IOL process
(IOS-XE 17.18.02)"] + NETIOMUX["netiomux"] + SOCKETS["/tmp/s00.sock (recv)
/tmp/c00.sock (send-to path)
… one pair per interface"] + RUNNER -->|"spawn -e/-s/-m + app id"| IOL + IOL -->|"netio bus /tmp/netio<uid>/"| NETIOMUX --> SOCKETS + end + subgraph Host + UBRIDGE["uBridge bridgeN
add_nio_unix c00.sock s00.sock
+ add_nio_udp (topology)"] + VOL["project-files/docker/<node>/tmp"] + end + SOCKETS <-->|"raw Ethernet frames
(bind volume dir)"| VOL <--> UBRIDGE +``` + +* **Console**: the runner muxes the IOS console onto PID 1 stdio (`-stdio` + entrypoint flag). The plain `console_type: "telnet"` attaches to it — no + `docker_exec` needed. The runner requires a TTY, which GNS3 always + allocates; without one the runner exits (`inappropriate ioctl for device`). +* **Networking**: the runner does not touch the container's network + namespace. Per interface N it creates, inside `/tmp`: a receive socket + `s%02d.sock` (frames sent there are injected into guest interface N) and a + send-to path `c%02d.sock` (whoever binds it receives the guest's frames). + Frames are **raw Ethernet**, one datagram per frame. Because `/tmp` is a + persisted volume (bind-mounted from the node directory), uBridge can bind + `cNN.sock` and send to `sNN.sock` on the host. +* **Licensing**: the image ships a self-consistent `/etc/hostid` + `.iourc` + pair, and the runner regenerates the license from the host ID at boot — + nothing to configure. +* **Persistence**: `/tmp/run/` inside the volume holds the NETMAP, the + startup-config (`config`, plain IOS format) and NVRAM (`nvram_00001`), so + the router's configuration survives stop/start and container recreation. + The generated config maps the runner to the server's uid/gid + (`user-id`/`group-id`), which is also what makes the `/tmp` sockets + reachable by uBridge and all volume files owned by the server user (no + permission-fix pass needed). + +## Template + +```json +{ + "name": "IOS-XE 17.18.02 IOL", + "template_type": "docker", + "image": "iol-xe/iol-xe:17-18-02", + "category": "router", + "symbol": ":/symbols/router.svg", + "adapters": 4, + "console_type": "telnet", + "environment": "GNS3_IOL_RUNNER=1", + "extra_volumes": ["/config", "/tmp"], + "memory": 2560 +} +``` + +`/config` and `/tmp` are auto-added even if omitted; listing them keeps the +template self-documenting. + +## Server mechanisms + +| Mechanism | Where | What it does | +|---|---|---| +| `GNS3_UNIX_SOCKET_NIO=1` | `VendorDockerVM` | `_add_ubridge_connection` override: `bridge create` + `bridge add_nio_unix /c{N:02d}.sock /s{N:02d}.sock` instead of TAP + `docker move_to_ns`. No TAP allocation, no `set_mac_addr`, namespace untouched. Fails node creation if the socket dir is not a persisted volume. | +| `GNS3_UNIX_SOCKET_DIR=` | `VendorDockerVM` | Socket directory (default `/tmp`). Any image whose agent exposes the `s%02d`/`c%02d` datagram pairs can use this without the IOL specifics. | +| `GNS3_IOL_RUNNER=1` | `IOLDockerVM` (selected in the manager) | Forces skip-init + unix-socket NIO + the two volumes; on every start writes `/config/iol-config.json` (`num-eth` = adapter count, `num-serial` = 0, memory from `GNS3_IOL_MEMORY`, default 2048), creates `/tmp/run/` (the IOL process dies without it) and removes stale `s/c??.sock` + `netio*` left by an unclean kill (`tmp/run` is never touched). | +| `restart()` hardening | `IOLDockerVM` | The base `docker restart` would boot the runner on a stale config and leave uBridge wired to the previous run's sockets; reload becomes graceful stop (SIGTERM → NVRAM flush) + full start. | + +`GNS3_STOP_TIMEOUT` (default 60) controls the SIGTERM grace period on stop. +Extra iol-runner flags can be passed via `start_command`, e.g. `-keep` +(L1 keepalives) or `-debug 9` (verbose `process.log` — very useful when +diagnosing wiring issues). + +## Notes and caveats + +* **Memory sizing**: `memory` caps the whole container; the IOL process gets + `GNS3_IOL_MEMORY` (default 2048 MB). Keep container memory at IOL memory + + ~512 MB headroom or the OOM-killer will shoot the router. +* **MAC addresses**: the `mac_address` template field and per-adapter custom + MACs are ignored — IOL derives its own scheme (`aabb.cc00.0XY0`). +* **Adapters**: change the adapter count while the node is stopped; the + config is regenerated on the next start and the runner creates the + matching socket set (IOL granularity is 4 ports per unit). +* **Stop before editing**: NVRAM is only flushed on a graceful stop (SIGTERM, + "cleanup done" in `process.log`); a kill loses the running-config changes + since the last `write memory`. +* **Class selection is create-time**: toggling `GNS3_IOL_RUNNER` via PUT + takes effect after a project reload (same as `docker_exec`). +* The startup-config lives at `project-files/docker//tmp/run/config`; + `extra_configs` targets under persisted volumes are warned against by the + generic create path — edit the file directly or paste via the console. diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index ed37ca626..f4027ea59 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -33,6 +33,7 @@ from gns3server.utils.asyncio import locking from gns3server.compute.base_manager import BaseManager from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM +from gns3server.compute.docker.iol_docker_vm import IOLDockerVM from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error, DockerHttp409Error log = logging.getLogger(__name__) @@ -62,9 +63,21 @@ class Docker(BaseManager): self._host_checked = False def _select_node_class(self, **kwargs): - """Select the node class based on console_type.""" + """ + Select the node class based on console_type and GNS3_* environment + markers. Like console_type, the environment is fixed at node creation + time: toggling a marker via PUT takes effect after a project reload. + """ if kwargs.get("console_type") == "docker_exec": return VendorDockerVM + environment = kwargs.get("environment") or "" + for line in environment.splitlines(): + line = line.strip().rstrip(",") + if line.startswith("GNS3_IOL_RUNNER="): + return IOLDockerVM + if line.startswith("GNS3_UNIX_SOCKET_NIO="): + # Generic capability, usable without the IOL specifics. + return VendorDockerVM return DockerVM async def create_node(self, name, project_id, node_id, *args, **kwargs): diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py new file mode 100644 index 000000000..1c03bc4af --- /dev/null +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -0,0 +1,189 @@ +# +# Copyright (C) 2025 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +IOL (IOS on Linux) Docker container subclass. + +Supports IOL images packaged with Cisco CML's container runner +(``iol-runner``, ``virl.lab/cmd/iol-runner``), e.g. ``iol-xe/iol-xe:17-18-02``: +a scratch image whose ENTRYPOINT is ``iol-runner -config /config/iol-config.json +-stdio``. The runner generates the license, writes the NETMAP, manages NVRAM +and muxes the IOS console onto PID 1 stdio (works with the plain ``telnet`` +console type; requires a TTY, which GNS3 always allocates). + +Networking does not use the container's network namespace at all: the runner's +netiomux exposes per-interface AF_UNIX datagram sockets in ``/tmp`` +(``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames), wired by +the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM. Because the +netio bus directory is private to the node's ``/tmp`` volume, the application +IDs are fixed constants with no cross-node collisions. + +This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker. +""" + +import glob +import json +import logging +import os +import shutil + +from gns3server.compute.docker.docker_error import DockerHttp404Error +from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM + +log = logging.getLogger(__name__) + + +class IOLDockerVM(VendorDockerVM): + """ + VendorDockerVM subclass for iol-runner images. + + Extra opt-in knob (beyond the inherited vendor ones): + + * ``GNS3_IOL_MEMORY=`` — IOL router memory passed via the generated + config (default 2048). The template ``memory`` field caps the whole + container: keep it at IOL memory + ~512 MB headroom or the kernel + OOM-killer will fire. + + The marker itself forces ``GNS3_SKIP_INIT`` and the unix-socket NIO wiring, + and auto-adds the ``/config`` and ``/tmp`` persistent volumes, so a + template containing only ``GNS3_IOL_RUNNER=1`` is fully configured. + """ + + _IOL_CONFIG_DIR = "/config" + _IOL_RUN_DIR = "/tmp/run" + + def _parse_vendor_environment(self): + + super()._parse_vendor_environment() + # The image has no shell (scratch): init.sh could neither run (its + # #!/bin/sh shebang doesn't exist) nor wait for eth interfaces that + # are never created. The console is IOS itself on PID 1 stdio. + self._gns3_init = False + self._unix_socket_nio = True + self._unix_socket_dir = "/tmp" + + self._iol_memory = 2048 + if self._environment: + for _line in self._environment.splitlines(): + _line = _line.strip().rstrip(",") + if _line.startswith("GNS3_IOL_MEMORY="): + try: + memory = int(_line.split("=", 1)[1].strip()) + if memory > 0: + self._iol_memory = memory + except ValueError: + pass + + def _persistent_volume_list(self, image_info, include_network_config=True): + """ + Override: the runner requires ``/config`` (its config file, generated + below) and ``/tmp`` (netiomux sockets, NETMAP, NVRAM) as persisted + volumes — /tmp because uBridge must reach the sockets on the host. + Auto-add both so a minimal template cannot be misconfigured. + """ + + volumes = super()._persistent_volume_list(image_info, include_network_config) + for needed in (self._IOL_CONFIG_DIR, "/tmp"): + if not any(needed == v or needed.startswith(v.rstrip("/") + "/") for v in volumes): + volumes.append(needed) + return volumes + + async def start(self): + + await self._prepare_iol_runtime() + await super().start() + + async def restart(self): + """ + Override: the base restart is a bare ``docker restart`` — the runner + would read a stale config (no adapter-count/memory refresh) and + uBridge would keep wiring to the previous run's sockets. Stop + gracefully (SIGTERM lets the runner flush NVRAM) and start again. + """ + + await self.stop(graceful=True) + await self.start() + + async def _prepare_iol_runtime(self): + """ + Regenerate the node's runtime files before the container starts. + + * ``/tmp/run/`` must exist or the IOL process dies at + boot (the runner writes NETMAP there but does not create it). + * ``/config/iol-config.json`` is rewritten on every + start so adapter-count and memory changes take effect. + * Ephemeral sockets left by a previous (possibly SIGKILLed) run are + removed — the runner rebinds them on boot and would fail on a + stale file. ``tmp/run`` is never touched: startup-config and NVRAM + persist there. + """ + + try: + state = await self._get_container_state() + except DockerHttp404Error: + state = "stopped" + + os.makedirs(os.path.join(self.working_dir, "tmp", "run"), exist_ok=True) + self._write_iol_config() + + if state == "running": + # Idempotent start of a live node: the sockets belong to the + # running runner; base start() will return early. + return + + tmp_dir = os.path.join(self.working_dir, "tmp") + for pattern in ("s??.sock", "c??.sock"): + for stale in glob.glob(os.path.join(tmp_dir, pattern)): + try: + os.unlink(stale) + except OSError: + pass + for netio_dir in glob.glob(os.path.join(tmp_dir, "netio*")): + shutil.rmtree(netio_dir, ignore_errors=True) + + def _write_iol_config(self): + """ + Write the runner's config file on the host side of the /config volume. + The runner drops to user-id/group-id after its setup, so everything it + creates inside the volumes is owned by the server user (which is also + what makes the /tmp sockets reachable by uBridge). + """ + + config = { + "binary": "/binary.iol", + "memory": self._iol_memory, + "num-eth": self.adapters, + "num-serial": 0, # GNS3 docker adapters are ethernet-only + "local-app": 1, + "remote-app": 2, + "user-id": os.getuid(), + "group-id": os.getgid(), + } + config_file = os.path.join(self.working_dir, "config", "iol-config.json") + os.makedirs(os.path.dirname(config_file), exist_ok=True) + with open(config_file, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2) + f.write("\n") + log.debug("Wrote iol-runner config for '%s': %s", self._name, config) + + async def _fix_permissions(self): + """ + Override: no-op. The generated config maps the runner to the server's + uid/gid, so no root-owned files ever appear in the volumes, and this + image has no shell for the container-side busybox pass anyway. + """ + + self._permissions_fixed = True diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index 6089663f2..8b8f5fb52 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -33,6 +33,7 @@ import logging import os import shutil +from gns3server.utils.asyncio import wait_for_file_creation from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error @@ -64,6 +65,20 @@ class VendorDockerVM(DockerVM): sessions on the shared exec. * ``GNS3_STOP_TIMEOUT=60`` — SIGTERM grace period in seconds when stopping the container (default 60; Docker SIGKILLs once it expires). + * ``GNS3_UNIX_SOCKET_NIO=1`` — wire adapters through AF_UNIX datagram + socket files instead of a TAP interface moved into the container's + network namespace. For images whose network agent exposes, per adapter + ``N``, a receive socket ``s%02d.sock`` and a send-to path ``c%02d.sock`` + (raw Ethernet frames, one datagram per frame) inside a persisted volume + directory — e.g. Cisco CML's iol-runner (see IOLDockerVM). uBridge + binds ``c{N:02d}.sock`` (its receive side) and sends to + ``s{N:02d}.sock``. No TAP is created, the container's network namespace + is untouched and the ``mac_address`` template field is ignored (the + image's agent owns the MAC scheme). + * ``GNS3_UNIX_SOCKET_DIR=`` — in-container directory holding the + socket files (default ``/tmp``). Must be a persisted volume + (extra_volumes) so uBridge can reach the sockets on the host; node + creation fails with an actionable error otherwise. """ def __init__(self, *args, **kwargs): @@ -86,6 +101,8 @@ class VendorDockerVM(DockerVM): self._console_cmd = None self._console_resize = True self._stop_timeout = 60 + self._unix_socket_nio = False + self._unix_socket_dir = "/tmp" if self._environment: for _line in self._environment.splitlines(): _line = _line.strip().rstrip(",") @@ -99,6 +116,12 @@ class VendorDockerVM(DockerVM): self._console_cmd = _line.split("=", 1)[1].strip() elif _line.startswith("GNS3_CONSOLE_RESIZE="): self._console_resize = _line.split("=", 1)[1].strip().lower() not in ("0", "false", "no") + elif _line.startswith("GNS3_UNIX_SOCKET_NIO="): + self._unix_socket_nio = _line.split("=", 1)[1].strip().lower() in ("1", "true", "yes") + elif _line.startswith("GNS3_UNIX_SOCKET_DIR="): + socket_dir = _line.split("=", 1)[1].strip().rstrip("/") or "/" + if os.path.isabs(socket_dir) and ".." not in socket_dir.split("/"): + self._unix_socket_dir = socket_dir elif _line.startswith("GNS3_STOP_TIMEOUT="): try: timeout = int(_line.split("=", 1)[1].strip()) @@ -142,6 +165,17 @@ class VendorDockerVM(DockerVM): are never shadowed by an empty mount. """ binds = super()._mount_binds(image_info) + if self._unix_socket_nio: + socket_dir = self._unix_socket_dir.rstrip("/") + if not any( + v.rstrip("/") == socket_dir or socket_dir.startswith(v.rstrip("/") + "/") + for v in self._volumes + ): + raise DockerError( + f"GNS3_UNIX_SOCKET_NIO requires socket directory '{self._unix_socket_dir}' of " + f"container '{self._name}' to be a persisted volume (add it to extra_volumes) " + f"so uBridge can reach the sockets on the host" + ) if self._gns3_init: return binds binds = [b for b in binds if b.get("Target") != "/gns3volumes/etc/network"] @@ -284,6 +318,78 @@ class VendorDockerVM(DockerVM): return self._interface_names[adapter_number] return f"eth{adapter_number}" + @property + def _unix_socket_host_dir(self): + """ + Host-side path of the in-container unix-socket directory: the bind + source of the persisted volume it lives in. + """ + return os.path.join(self.working_dir, os.path.relpath(self._unix_socket_dir, "/")) + + async def _add_ubridge_connection(self, nio, adapter_number): + """ + Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter through the + image's AF_UNIX datagram socket pair (raw Ethernet frames) instead of + a TAP interface moved into the container's network namespace. + + Per adapter N the image's network agent is expected to create, inside + GNS3_UNIX_SOCKET_DIR (a persisted volume, enforced by _mount_binds): + + * ``s{N:02d}.sock`` — its receive socket; frames sent there are + injected into guest interface N; + * ``c{N:02d}.sock`` — the path it sends guest-egress frames to. + + uBridge binds the c-socket as its receive side and sends to the + s-socket. No TAP is allocated, the namespace is untouched and guest + MAC addresses are whatever the image's agent uses. + """ + + if not self._unix_socket_nio: + return await super()._add_ubridge_connection(nio, adapter_number) + + try: + adapter = self._ethernet_adapters[adapter_number] + except IndexError: + raise DockerError( + "Adapter {adapter_number} doesn't exist on Docker container '{name}'".format( + name=self.name, adapter_number=adapter_number + ) + ) + + bridge_name = f"bridge{adapter_number}" + await self._ubridge_send(f"bridge create {bridge_name}") + self._bridges.add(bridge_name) + + host_dir = self._unix_socket_host_dir + local_sock = os.path.join(host_dir, f"c{adapter_number:02d}.sock") + remote_sock = os.path.join(host_dir, f"s{adapter_number:02d}.sock") + + # A c-socket left over from a previous ubridge run would fail its bind. + with contextlib.suppress(OSError): + os.unlink(local_sock) + + # The socket appears when the container's agent finishes its interface + # setup; wait instead of silently blackholing the adapter. + try: + await wait_for_file_creation(remote_sock, timeout=30) + except asyncio.TimeoutError: + raise DockerError( + f"Socket '{remote_sock}' for adapter {adapter_number} of container " + f"'{self._name}' did not appear within 30 seconds. Check that the " + f"container's port count covers adapter {adapter_number} and that " + f"'{self._unix_socket_dir}' is bind-mounted from the node directory." + ) + + await self._ubridge_send(f'bridge add_nio_unix {bridge_name} "{local_sock}" "{remote_sock}"') + adapter.host_ifc = local_sock # bookkeeping / removal logging only + log.debug( + "Adapter %d of container '%s' wired via unix sockets %s <-> %s", + adapter_number, self._name, local_sock, remote_sock, + ) + + if nio: + await self._connect_nio(adapter_number, nio) + def _cleanup_console_resources(self): """ Override: close the docker-exec pty socket, if any, so the next diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py new file mode 100644 index 000000000..8d12307ef --- /dev/null +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -0,0 +1,487 @@ +# +# Copyright (C) 2025 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Tests for the IOLDockerVM subclass (Cisco CML iol-runner images, e.g. +iol-xe/iol-xe:17-18-02) and its unix-socket NIO wiring. + +Image-free: everything is asserted against generated files, parsed knobs and +the uBridge command stream. +""" + +import asyncio +import glob +import json +import os +import uuid + +import pytest +import pytest_asyncio + +from unittest.mock import patch, MagicMock, call + +from tests.utils import asyncio_patch, AsyncioMagicMock + +from gns3server.compute.docker import Docker +from gns3server.compute.docker.docker_vm import DockerVM +from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM +from gns3server.compute.docker.iol_docker_vm import IOLDockerVM +from gns3server.compute.docker.docker_error import DockerError + + +# --------------------------------------------------------------------------- +# Helpers / fixtures +# --------------------------------------------------------------------------- + +IOL_ENTRYPOINT = ["/iol-runner", "-config", "/config/iol-config.json", "-stdio"] + + +def _create_response(entrypoint=None, volumes=None): + """Build the Docker /containers/create response (with image info merged).""" + return { + "Id": "e90e34656806", + "Warnings": [], + "Config": { + "Entrypoint": entrypoint or IOL_ENTRYPOINT, + "Cmd": [], + "Volumes": volumes or {}, + }, + } + + +@pytest_asyncio.fixture +async def manager(port_manager): + + m = Docker.instance() + m.port_manager = port_manager + return m + + +def _make_vm(compute_project, manager, environment="GNS3_IOL_RUNNER=1", + extra_volumes=None, adapters=4, console_type="telnet"): + """Build an IOLDockerVM with a fake cid (no create() called).""" + vm = IOLDockerVM( + "iol-xe-1", str(uuid.uuid4()), compute_project, manager, "iol-xe/iol-xe:17-18-02", + console_type=console_type, environment=environment, + extra_volumes=extra_volumes or [], adapters=adapters, + ) + vm._cid = "e90e34656842" + return vm + + +def _mock_start(vm, state="stopped"): + """Mock everything DockerVM.start() needs besides the runtime prep.""" + vm._get_container_state = AsyncioMagicMock(return_value=state) + vm._start_ubridge = AsyncioMagicMock() + vm._get_namespace = AsyncioMagicMock(return_value=42) + vm._add_ubridge_connection = AsyncioMagicMock() + vm._start_console_server = AsyncioMagicMock() + + +def _seed_proc(stdout=b"seedcid\n", returncode=0): + proc = MagicMock() + proc.communicate = AsyncioMagicMock(return_value=(stdout, b"")) + proc.returncode = returncode + return proc + + +# --------------------------------------------------------------------------- +# Factory selection +# --------------------------------------------------------------------------- + +def test_factory_selects_iol_for_env_marker(manager): + + assert manager._select_node_class(console_type="telnet", + environment="GNS3_IOL_RUNNER=1") is IOLDockerVM + + +def test_factory_tolerates_whitespace_and_comma(manager): + + assert manager._select_node_class(console_type="telnet", + environment=" GNS3_IOL_RUNNER=1,\nFOO=bar") is IOLDockerVM + + +def test_factory_docker_exec_wins_over_iol_marker(manager): + + assert manager._select_node_class(console_type="docker_exec", + environment="GNS3_IOL_RUNNER=1") is VendorDockerVM + + +def test_factory_plain_environment_is_base(manager): + + assert manager._select_node_class(console_type="telnet", + environment="FOO=bar\nGNS3_BAZ=nope") is DockerVM + + +def test_factory_generic_unix_knob_selects_vendor(manager): + + assert manager._select_node_class(console_type="telnet", + environment="GNS3_UNIX_SOCKET_NIO=1") is VendorDockerVM + + +# --------------------------------------------------------------------------- +# Knob parsing +# --------------------------------------------------------------------------- + +def test_marker_forces_skip_init_and_unix_nio(compute_project, manager): + + vm = _make_vm(compute_project, manager, environment="GNS3_IOL_RUNNER=1") + assert vm._gns3_init is False + assert vm._unix_socket_nio is True + assert vm._unix_socket_dir == "/tmp" + assert vm._iol_memory == 2048 + + +def test_iol_memory_knob(compute_project, manager): + + vm = _make_vm(compute_project, manager, + environment="GNS3_IOL_RUNNER=1\nGNS3_IOL_MEMORY=4096") + assert vm._iol_memory == 4096 + + vm = _make_vm(compute_project, manager, + environment="GNS3_IOL_RUNNER=1\nGNS3_IOL_MEMORY=notanumber") + assert vm._iol_memory == 2048 + + +# --------------------------------------------------------------------------- +# create() +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_create_keeps_image_entrypoint(compute_project, manager): + + with asyncio_patch("gns3server.compute.docker.Docker.list_images", + return_value=[{"image": "iol-xe"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", + return_value=_create_response()) as mock: + with patch("asyncio.subprocess.create_subprocess_exec", + return_value=_seed_proc()): + vm = _make_vm(compute_project, manager) + await vm.create() + sent = mock.call_args.kwargs["data"] + # the iol-runner entrypoint runs as PID 1, untouched + assert sent["Entrypoint"] == IOL_ENTRYPOINT + assert sent["Cmd"] == [] + + +@pytest.mark.asyncio +async def test_create_auto_adds_config_and_tmp_volumes(compute_project, manager): + + with asyncio_patch("gns3server.compute.docker.Docker.list_images", + return_value=[{"image": "iol-xe"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", + return_value=_create_response()) as mock: + with patch("asyncio.subprocess.create_subprocess_exec", + return_value=_seed_proc()): + vm = _make_vm(compute_project, manager, extra_volumes=[]) + await vm.create() + sent = mock.call_args.kwargs["data"] + targets = [m["Target"] for m in sent["HostConfig"]["Mounts"]] + # both volumes are forced and bound at their real in-container + # paths (skip-init retargeting), reachable by uBridge on the host + assert "/config" in targets + assert "/tmp" in targets + assert not any(t.startswith("/gns3volumes/") for t in targets) + vol_env = [v for v in sent["Env"] if v.startswith("GNS3_VOLUMES=")][0] + assert "/config" in vol_env and "/tmp" in vol_env + + +@pytest.mark.asyncio +async def test_create_start_command_becomes_runner_flags(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm.start_command = "-keep" + with asyncio_patch("gns3server.compute.docker.Docker.list_images", + return_value=[{"image": "iol-xe"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", + return_value=_create_response()) as mock: + with patch("asyncio.subprocess.create_subprocess_exec", + return_value=_seed_proc()): + await vm.create() + sent = mock.call_args.kwargs["data"] + # start_command is the container CMD = extra iol-runner flags + assert sent["Cmd"] == ["-keep"] + + +# --------------------------------------------------------------------------- +# start() — runtime preparation +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_start_writes_iol_config(compute_project, manager): + + vm = _make_vm(compute_project, manager, adapters=4) + _mock_start(vm) + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + await vm.start() + + with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f: + config = json.load(f) + assert config["binary"] == "/binary.iol" + assert config["num-eth"] == 4 + assert config["num-serial"] == 0 + assert config["local-app"] == 1 + assert config["remote-app"] == 2 + assert config["memory"] == 2048 + assert config["user-id"] == os.getuid() + assert config["group-id"] == os.getgid() + assert vm.status == "started" + + +@pytest.mark.asyncio +async def test_start_rewrites_config_on_adapter_change(compute_project, manager): + + vm = _make_vm(compute_project, manager, adapters=4) + _mock_start(vm) + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + await vm.start() + + vm.adapters = 8 + _mock_start(vm) + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + await vm.start() + + with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f: + assert json.load(f)["num-eth"] == 8 + + +@pytest.mark.asyncio +async def test_start_creates_run_dir(compute_project, manager): + + vm = _make_vm(compute_project, manager) + _mock_start(vm) + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + await vm.start() + + assert os.path.isdir(os.path.join(vm.working_dir, "tmp", "run")) + + +@pytest.mark.asyncio +async def test_start_cleans_stale_sockets_but_keeps_run(compute_project, manager): + + vm = _make_vm(compute_project, manager) + tmp_dir = os.path.join(vm.working_dir, "tmp") + os.makedirs(os.path.join(tmp_dir, "run"), exist_ok=True) + for name in ("s00.sock", "c00.sock", "s01.sock", "c01.sock"): + open(os.path.join(tmp_dir, name), "w").close() + os.makedirs(os.path.join(tmp_dir, "netio1000")) + open(os.path.join(tmp_dir, "run", "nvram_00001"), "w").close() + open(os.path.join(tmp_dir, "run", "config"), "w").close() + + _mock_start(vm, state="stopped") + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + await vm.start() + + assert glob.glob(os.path.join(tmp_dir, "s??.sock")) == [] + assert glob.glob(os.path.join(tmp_dir, "c??.sock")) == [] + assert not os.path.exists(os.path.join(tmp_dir, "netio1000")) + # the persistent runtime survives the cleanup + assert os.path.exists(os.path.join(tmp_dir, "run", "nvram_00001")) + assert os.path.exists(os.path.join(tmp_dir, "run", "config")) + + +@pytest.mark.asyncio +async def test_start_skips_cleanup_when_already_running(compute_project, manager): + + vm = _make_vm(compute_project, manager) + tmp_dir = os.path.join(vm.working_dir, "tmp") + os.makedirs(tmp_dir, exist_ok=True) + open(os.path.join(tmp_dir, "s00.sock"), "w").close() + + _mock_start(vm, state="running") + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + await vm.start() + + # live runner sockets must not be deleted behind its back + assert os.path.exists(os.path.join(tmp_dir, "s00.sock")) + + +@pytest.mark.asyncio +async def test_fix_permissions_is_noop(compute_project, manager): + + vm = _make_vm(compute_project, manager) + with patch("asyncio.subprocess.create_subprocess_exec") as mock_exec: + await vm._fix_permissions() + mock_exec.assert_not_called() + assert vm._permissions_fixed is True + + +@pytest.mark.asyncio +async def test_restart_is_graceful_stop_then_start(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm.stop = AsyncioMagicMock() + vm.start = AsyncioMagicMock() + await vm.restart() + vm.stop.assert_called_once_with(graceful=True) + vm.start.assert_called_once() + + +# --------------------------------------------------------------------------- +# Wiring — unix-socket NIO +# --------------------------------------------------------------------------- + +@pytest.mark.asyncio +async def test_add_ubridge_connection_unix_wiring(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm._ubridge_hypervisor = MagicMock() + host_dir = os.path.join(vm.working_dir, "tmp") + os.makedirs(host_dir, exist_ok=True) + # the runner's receive socket must exist (created by the container) + open(os.path.join(host_dir, "s00.sock"), "w").close() + + nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) + await vm._add_ubridge_connection(nio, 0) + + sent = [c for c in vm._ubridge_hypervisor.method_calls if "send" in str(c)] + flat = "\n".join(str(c) for c in sent) + assert call.send("bridge create bridge0") in sent + assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(host_dir, "c00.sock")}" ' + f'"{os.path.join(host_dir, "s00.sock")}"') in sent + assert "add_nio_udp bridge0 4242 127.0.0.1 4343" in flat + assert "bridge start bridge0" in flat + # the TAP/namespace path must not be used at all + assert "add_nio_tap" not in flat + assert "move_to_ns" not in flat + assert "set_mac_addr" not in flat + assert vm._ethernet_adapters[0].host_ifc == os.path.join(host_dir, "c00.sock") + + +@pytest.mark.asyncio +async def test_add_ubridge_connection_stale_local_socket_unlinked(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm._ubridge_hypervisor = MagicMock() + host_dir = os.path.join(vm.working_dir, "tmp") + os.makedirs(host_dir, exist_ok=True) + open(os.path.join(host_dir, "c00.sock"), "w").close() + open(os.path.join(host_dir, "s00.sock"), "w").close() + + await vm._add_ubridge_connection(None, 0) + assert not os.path.exists(os.path.join(host_dir, "c00.sock")) + + +@pytest.mark.asyncio +async def test_add_ubridge_connection_adapter_out_of_range(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm._ubridge_hypervisor = MagicMock() + with pytest.raises(DockerError): + await vm._add_ubridge_connection(None, 42) + + +@pytest.mark.asyncio +async def test_add_ubridge_connection_timeout_is_actionable(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm._ubridge_hypervisor = MagicMock() + + async def raise_timeout(path, timeout=60): + raise asyncio.TimeoutError() + + with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation", + side_effect=raise_timeout): + with pytest.raises(DockerError) as excinfo: + await vm._add_ubridge_connection(None, 0) + # the message names the adapter and the socket directory + assert "adapter 0" in str(excinfo.value) + assert "/tmp" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_add_ubridge_connection_without_nio_still_wires(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm._ubridge_hypervisor = MagicMock() + host_dir = os.path.join(vm.working_dir, "tmp") + os.makedirs(host_dir, exist_ok=True) + open(os.path.join(host_dir, "s00.sock"), "w").close() + + await vm._add_ubridge_connection(None, 0) + flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) + assert "bridge create bridge0" in flat + assert "add_nio_unix" in flat + # no link yet: no UDP NIO, no bridge start (matches base semantics) + assert "add_nio_udp" not in flat + assert "bridge start" not in flat + + +# --------------------------------------------------------------------------- +# Generic GNS3_UNIX_SOCKET_NIO knob on plain VendorDockerVM +# --------------------------------------------------------------------------- + +def test_env_unix_socket_nio_parsing(compute_project, manager): + + vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks") + assert vm._unix_socket_nio is True + assert vm._unix_socket_dir == "/var/run/socks" + + # invalid dirs are rejected, keeping the default + vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=yes\nGNS3_UNIX_SOCKET_DIR=../../etc") + assert vm._unix_socket_nio is True + assert vm._unix_socket_dir == "/tmp" + + # off by default / explicit off + vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", + console_type="docker_exec", environment="GNS3_SKIP_INIT=1") + assert vm._unix_socket_nio is False + + +@pytest.mark.asyncio +async def test_unix_socket_dir_must_be_a_volume(compute_project, manager): + + vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", + extra_volumes=[]) + with pytest.raises(DockerError) as excinfo: + vm._mount_binds({"Config": {"Volumes": {}}}) + assert "extra_volumes" in str(excinfo.value) + + vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", + extra_volumes=["/tmp"]) + binds = vm._mount_binds({"Config": {"Volumes": {}}}) + assert any(b["Target"] == "/tmp" for b in binds) + + +@pytest.mark.asyncio +async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manager): + + vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks") + vm._ubridge_hypervisor = MagicMock() + host_dir = os.path.join(vm.working_dir, "var", "run", "socks") + os.makedirs(host_dir, exist_ok=True) + open(os.path.join(host_dir, "s00.sock"), "w").close() + + await vm._add_ubridge_connection(None, 0) + flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) + assert f'"{os.path.join(host_dir, "s00.sock")}"' in flat + assert "add_nio_tap" not in flat From 42e26717e6361b721e17bc22dfc4d9fa930fdd54 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 30 Aug 2026 12:21:17 +0800 Subject: [PATCH 11/60] fix: alias long unix-socket NIO paths through the runtime dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AF_UNIX sun_path caps at 107 bytes and node volume directories (projects//project-files/docker//tmp) exceed it — uBridge rejects the NIO with 'invalid file path size'. When the wiring path is too long, create /gns3/unixio- as a symlink to the real volume directory (same trick as the uBridge control socket) and reference the alias in the uBridge commands; the symlink is removed when uBridge stops. Found during E2E with iol-xe:17-18-02. --- gns3server/compute/docker/vendor_docker_vm.py | 51 ++++++++++++++++++- tests/compute/docker/test_iol_docker_vm.py | 41 +++++++++++++-- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index 8b8f5fb52..4eba1a705 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -32,6 +32,7 @@ import json import logging import os import shutil +import tempfile from gns3server.utils.asyncio import wait_for_file_creation from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer @@ -103,6 +104,7 @@ class VendorDockerVM(DockerVM): self._stop_timeout = 60 self._unix_socket_nio = False self._unix_socket_dir = "/tmp" + self._unix_socket_aliases = set() if self._environment: for _line in self._environment.splitlines(): _line = _line.strip().rstrip(",") @@ -326,6 +328,50 @@ class VendorDockerVM(DockerVM): """ return os.path.join(self.working_dir, os.path.relpath(self._unix_socket_dir, "/")) + def _unix_socket_wiring_dir(self): + """ + Directory to reference in the uBridge unix-NIO commands. + + AF_UNIX paths are capped at 107 bytes (sun_path minus the NUL), and a + node volume directory (projects//project-files/docker//tmp) + alone is ~120 — uBridge would reject the NIO with "invalid file path + size". Long directories are therefore aliased through a symlink in the + runtime directory (same trick as the uBridge control socket). uBridge + resolves the symlink; the socket files themselves always live in the + persisted volume, and the container-side /tmp paths are unaffected. + """ + + host_dir = self._unix_socket_host_dir + if len(host_dir) + len("/c00.sock") <= 107: + return host_dir + + runtime_dir = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir() + alias = os.path.join(runtime_dir, "gns3", f"unixio-{self.id}") + try: + os.makedirs(os.path.dirname(alias), mode=0o700, exist_ok=True) + if os.path.lexists(alias): + os.unlink(alias) + os.symlink(host_dir, alias) + except OSError as e: + raise DockerError( + f"Could not create unix-socket alias '{alias}' for container '{self._name}': {e}" + ) + self._unix_socket_aliases.add(alias) + return alias + + async def _stop_ubridge(self): + """ + Override: also drop the short-path aliases created for the unix-socket + NIO (the sockets themselves are removed by the image's agent / the + next start's cleanup). + """ + + await super()._stop_ubridge() + for alias in self._unix_socket_aliases: + with contextlib.suppress(OSError): + os.unlink(alias) + self._unix_socket_aliases.clear() + async def _add_ubridge_connection(self, nio, adapter_number): """ Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter through the @@ -361,8 +407,9 @@ class VendorDockerVM(DockerVM): self._bridges.add(bridge_name) host_dir = self._unix_socket_host_dir - local_sock = os.path.join(host_dir, f"c{adapter_number:02d}.sock") - remote_sock = os.path.join(host_dir, f"s{adapter_number:02d}.sock") + wiring_dir = self._unix_socket_wiring_dir() + local_sock = os.path.join(wiring_dir, f"c{adapter_number:02d}.sock") + remote_sock = os.path.join(wiring_dir, f"s{adapter_number:02d}.sock") # A c-socket left over from a previous ubridge run would fail its bind. with contextlib.suppress(OSError): diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 8d12307ef..5986e4dd0 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -355,16 +355,17 @@ async def test_add_ubridge_connection_unix_wiring(compute_project, manager): sent = [c for c in vm._ubridge_hypervisor.method_calls if "send" in str(c)] flat = "\n".join(str(c) for c in sent) + wiring_dir = vm._unix_socket_wiring_dir() # may alias host_dir when long assert call.send("bridge create bridge0") in sent - assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(host_dir, "c00.sock")}" ' - f'"{os.path.join(host_dir, "s00.sock")}"') in sent + assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(wiring_dir, "c00.sock")}" ' + f'"{os.path.join(wiring_dir, "s00.sock")}"') in sent assert "add_nio_udp bridge0 4242 127.0.0.1 4343" in flat assert "bridge start bridge0" in flat # the TAP/namespace path must not be used at all assert "add_nio_tap" not in flat assert "move_to_ns" not in flat assert "set_mac_addr" not in flat - assert vm._ethernet_adapters[0].host_ifc == os.path.join(host_dir, "c00.sock") + assert vm._ethernet_adapters[0].host_ifc == os.path.join(wiring_dir, "c00.sock") @pytest.mark.asyncio @@ -483,5 +484,37 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage await vm._add_ubridge_connection(None, 0) flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) - assert f'"{os.path.join(host_dir, "s00.sock")}"' in flat + assert f'"{os.path.join(vm._unix_socket_wiring_dir(), "s00.sock")}"' in flat assert "add_nio_tap" not in flat + + +@pytest.mark.asyncio +async def test_long_socket_path_aliased_through_runtime_dir(compute_project, manager, monkeypatch, tmp_path): + """ + Node volume paths exceed sun_path's 107 bytes; the wiring must alias them + through a short symlink so uBridge accepts the NIO (and remove the alias + when uBridge stops). + """ + + from unittest.mock import PropertyMock + + long_dir = str(tmp_path / "a-very-long-project-directory-name" / ("x" * 40) / "tmp") + os.makedirs(long_dir, exist_ok=True) + open(os.path.join(long_dir, "s00.sock"), "w").close() + runtime_dir = tmp_path / "run" + monkeypatch.setenv("XDG_RUNTIME_DIR", str(runtime_dir)) + + vm = _make_vm(compute_project, manager) + vm._ubridge_hypervisor = MagicMock() + with patch.object(type(vm), "_unix_socket_host_dir", new_callable=PropertyMock, + return_value=long_dir): + await vm._add_ubridge_connection(None, 0) + alias = os.path.join(str(runtime_dir), "gns3", f"unixio-{vm.id}") + assert os.path.islink(alias) and os.readlink(alias) == long_dir + flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) + assert f'"{os.path.join(alias, "s00.sock")}"' in flat + assert long_dir not in flat # uBridge only ever sees the short path + + vm._ubridge_hypervisor.stop = AsyncioMagicMock() + await vm._stop_ubridge() + assert not os.path.lexists(alias) From 165f271bb83ae38ceb083357c9f09854b463c4e1 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 30 Aug 2026 13:25:20 +0800 Subject: [PATCH 12/60] refactor: reach unix-socket NIOs through the container root in /proc The unix-socket NIO no longer requires the socket directory to be a persisted volume: uBridge references the sockets through /proc//root/..., which is always well under the 107-byte sun_path cap (a project directory path alone exceeds it) and leaves the sockets ephemeral in the container's own filesystem. This replaces the runtime-dir symlink alias and its cleanup, and the mount-time volume enforcement. IOLDockerVM now persists only /tmp/run (the IOL working directory: startup-config + NVRAM) as a nested bind instead of the whole /tmp; stale socket cleanup is gone with it, as containers are recreated on every start. --- docs/features/iol-runner-docker.md | 49 ++--- gns3server/compute/docker/iol_docker_vm.py | 60 ++---- gns3server/compute/docker/vendor_docker_vm.py | 101 +++------- tests/compute/docker/test_iol_docker_vm.py | 177 ++++++------------ 4 files changed, 135 insertions(+), 252 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 10bca7685..c9b02841c 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -36,10 +36,11 @@ graph LR IOL -->|"netio bus /tmp/netio<uid>/"| NETIOMUX --> SOCKETS end subgraph Host - UBRIDGE["uBridge bridgeN
add_nio_unix c00.sock s00.sock
+ add_nio_udp (topology)"] - VOL["project-files/docker/<node>/tmp"] + UBRIDGE["uBridge bridgeN
add_nio_unix /proc/PID/root/tmp/cNN.sock …
+ add_nio_udp (topology)"] + VOL["project-files/docker/<node>/config
(+ tmp/run nested bind)"] end - SOCKETS <-->|"raw Ethernet frames
(bind volume dir)"| VOL <--> UBRIDGE + SOCKETS <-->|"raw Ethernet frames via
the container root in /proc"| UBRIDGE + UBRIDGE -.->|"iol-config.json +
persistent /tmp/run"| VOL ``` * **Console**: the runner muxes the IOS console onto PID 1 stdio (`-stdio` @@ -47,21 +48,27 @@ graph LR `docker_exec` needed. The runner requires a TTY, which GNS3 always allocates; without one the runner exits (`inappropriate ioctl for device`). * **Networking**: the runner does not touch the container's network - namespace. Per interface N it creates, inside `/tmp`: a receive socket - `s%02d.sock` (frames sent there are injected into guest interface N) and a - send-to path `c%02d.sock` (whoever binds it receives the guest's frames). - Frames are **raw Ethernet**, one datagram per frame. Because `/tmp` is a - persisted volume (bind-mounted from the node directory), uBridge can bind - `cNN.sock` and send to `sNN.sock` on the host. + namespace. Per interface N it creates, inside the container's `/tmp`: a + receive socket `s%02d.sock` (frames sent there are injected into guest + interface N) and a send-to path `c%02d.sock` (whoever binds it receives + the guest's frames). Frames are **raw Ethernet**, one datagram per frame. + uBridge reaches both through the container's root in `/proc` + (`/proc//root/tmp/…`) — a short path (AF_UNIX names are + capped at 107 bytes, which a project directory path alone would exceed) + that requires no volume mount and leaves the sockets ephemeral in the + container, fresh in every container GNS3 creates. * **Licensing**: the image ships a self-consistent `/etc/hostid` + `.iourc` pair, and the runner regenerates the license from the host ID at boot — nothing to configure. -* **Persistence**: `/tmp/run/` inside the volume holds the NETMAP, the - startup-config (`config`, plain IOS format) and NVRAM (`nvram_00001`), so - the router's configuration survives stop/start and container recreation. - The generated config maps the runner to the server's uid/gid - (`user-id`/`group-id`), which is also what makes the `/tmp` sockets - reachable by uBridge and all volume files owned by the server user (no +* **Persistence**: `/tmp/run` (the IOL working directory) holds the NETMAP, + the startup-config (`config`, plain IOS format) and NVRAM (`nvram_00001`). + It is the only `/tmp` path that needs to survive: GNS3 bind-mounts the + node directory's `tmp/run/` at `/tmp/run`, so the router's configuration + survives stop/start and container recreation while everything else in + `/tmp` stays ephemeral. The generated config maps the runner to the + server's uid/gid (`user-id`/`group-id`), so all files it creates are owned + by the server user — which is also what lets an unprivileged uBridge + traverse `/proc//root` to reach the container's sockets (no permission-fix pass needed). ## Template @@ -76,21 +83,21 @@ graph LR "adapters": 4, "console_type": "telnet", "environment": "GNS3_IOL_RUNNER=1", - "extra_volumes": ["/config", "/tmp"], + "extra_volumes": ["/config"], "memory": 2560 } ``` -`/config` and `/tmp` are auto-added even if omitted; listing them keeps the -template self-documenting. +`/config` and `/tmp/run` are auto-added even if omitted; listing `/config` +keeps the template self-documenting. ## Server mechanisms | Mechanism | Where | What it does | |---|---|---| -| `GNS3_UNIX_SOCKET_NIO=1` | `VendorDockerVM` | `_add_ubridge_connection` override: `bridge create` + `bridge add_nio_unix /c{N:02d}.sock /s{N:02d}.sock` instead of TAP + `docker move_to_ns`. No TAP allocation, no `set_mac_addr`, namespace untouched. Fails node creation if the socket dir is not a persisted volume. | -| `GNS3_UNIX_SOCKET_DIR=` | `VendorDockerVM` | Socket directory (default `/tmp`). Any image whose agent exposes the `s%02d`/`c%02d` datagram pairs can use this without the IOL specifics. | -| `GNS3_IOL_RUNNER=1` | `IOLDockerVM` (selected in the manager) | Forces skip-init + unix-socket NIO + the two volumes; on every start writes `/config/iol-config.json` (`num-eth` = adapter count, `num-serial` = 0, memory from `GNS3_IOL_MEMORY`, default 2048), creates `/tmp/run/` (the IOL process dies without it) and removes stale `s/c??.sock` + `netio*` left by an unclean kill (`tmp/run` is never touched). | +| `GNS3_UNIX_SOCKET_NIO=1` | `VendorDockerVM` | `_add_ubridge_connection` override: `bridge create` + `bridge add_nio_unix /proc//root/c{N:02d}.sock /proc//root/s{N:02d}.sock` instead of TAP + `docker move_to_ns`. No TAP allocation, no `set_mac_addr`, namespace untouched, no volume required. | +| `GNS3_UNIX_SOCKET_DIR=` | `VendorDockerVM` | In-container socket directory (default `/tmp`). Any image whose agent exposes the `s%02d`/`c%02d` datagram pairs can use this without the IOL specifics. | +| `GNS3_IOL_RUNNER=1` | `IOLDockerVM` (selected in the manager) | Forces skip-init + unix-socket NIO + the `/config` and `/tmp/run` volumes; on every start writes `/config/iol-config.json` (`num-eth` = adapter count, `num-serial` = 0, memory from `GNS3_IOL_MEMORY`, default 2048) and creates `/tmp/run/` (the IOL process dies without it). | | `restart()` hardening | `IOLDockerVM` | The base `docker restart` would boot the runner on a stale config and leave uBridge wired to the previous run's sockets; reload becomes graceful stop (SIGTERM → NVRAM flush) + full start. | `GNS3_STOP_TIMEOUT` (default 60) controls the SIGTERM grace period on stop. diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index 1c03bc4af..cc3302b81 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -25,22 +25,20 @@ and muxes the IOS console onto PID 1 stdio (works with the plain ``telnet`` console type; requires a TTY, which GNS3 always allocates). Networking does not use the container's network namespace at all: the runner's -netiomux exposes per-interface AF_UNIX datagram sockets in ``/tmp`` -(``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames), wired by -the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM. Because the -netio bus directory is private to the node's ``/tmp`` volume, the application -IDs are fixed constants with no cross-node collisions. +netiomux exposes per-interface AF_UNIX datagram sockets in the container's +``/tmp`` (``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames), +wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM +(uBridge reaches them through the container's root in /proc). Because the +netio bus directory is private to the container's /tmp, the application IDs +are fixed constants with no cross-node collisions. This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker. """ -import glob import json import logging import os -import shutil -from gns3server.compute.docker.docker_error import DockerHttp404Error from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM log = logging.getLogger(__name__) @@ -58,7 +56,7 @@ class IOLDockerVM(VendorDockerVM): OOM-killer will fire. The marker itself forces ``GNS3_SKIP_INIT`` and the unix-socket NIO wiring, - and auto-adds the ``/config`` and ``/tmp`` persistent volumes, so a + and auto-adds the ``/config`` and ``/tmp/run`` persistent volumes, so a template containing only ``GNS3_IOL_RUNNER=1`` is fully configured. """ @@ -90,13 +88,14 @@ class IOLDockerVM(VendorDockerVM): def _persistent_volume_list(self, image_info, include_network_config=True): """ Override: the runner requires ``/config`` (its config file, generated - below) and ``/tmp`` (netiomux sockets, NETMAP, NVRAM) as persisted - volumes — /tmp because uBridge must reach the sockets on the host. - Auto-add both so a minimal template cannot be misconfigured. + below) and ``/tmp/run`` (its working directory: startup-config and + NVRAM live there — NETMAP and the netiomux sockets are ephemeral and + stay in the container's own /tmp). Auto-add both so a minimal + template cannot be misconfigured. """ volumes = super()._persistent_volume_list(image_info, include_network_config) - for needed in (self._IOL_CONFIG_DIR, "/tmp"): + for needed in (self._IOL_CONFIG_DIR, self._IOL_RUN_DIR): if not any(needed == v or needed.startswith(v.rstrip("/") + "/") for v in volumes): volumes.append(needed) return volumes @@ -119,47 +118,28 @@ class IOLDockerVM(VendorDockerVM): async def _prepare_iol_runtime(self): """ - Regenerate the node's runtime files before the container starts. + Regenerate the node's runtime files before the container starts: * ``/tmp/run/`` must exist or the IOL process dies at boot (the runner writes NETMAP there but does not create it). * ``/config/iol-config.json`` is rewritten on every start so adapter-count and memory changes take effect. - * Ephemeral sockets left by a previous (possibly SIGKILLed) run are - removed — the runner rebinds them on boot and would fail on a - stale file. ``tmp/run`` is never touched: startup-config and NVRAM - persist there. - """ - try: - state = await self._get_container_state() - except DockerHttp404Error: - state = "stopped" + The netiomux sockets and the netio bus directory need no cleanup: + they live in the container's own /tmp, which is fresh in every + container GNS3 creates (containers are recreated on each start). + """ os.makedirs(os.path.join(self.working_dir, "tmp", "run"), exist_ok=True) self._write_iol_config() - if state == "running": - # Idempotent start of a live node: the sockets belong to the - # running runner; base start() will return early. - return - - tmp_dir = os.path.join(self.working_dir, "tmp") - for pattern in ("s??.sock", "c??.sock"): - for stale in glob.glob(os.path.join(tmp_dir, pattern)): - try: - os.unlink(stale) - except OSError: - pass - for netio_dir in glob.glob(os.path.join(tmp_dir, "netio*")): - shutil.rmtree(netio_dir, ignore_errors=True) - def _write_iol_config(self): """ Write the runner's config file on the host side of the /config volume. The runner drops to user-id/group-id after its setup, so everything it - creates inside the volumes is owned by the server user (which is also - what makes the /tmp sockets reachable by uBridge). + creates is owned by the server user — which is also what lets an + unprivileged uBridge traverse /proc//root to reach the + container's sockets. """ config = { diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index 4eba1a705..7e4abfd4d 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -32,7 +32,6 @@ import json import logging import os import shutil -import tempfile from gns3server.utils.asyncio import wait_for_file_creation from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer @@ -70,16 +69,16 @@ class VendorDockerVM(DockerVM): socket files instead of a TAP interface moved into the container's network namespace. For images whose network agent exposes, per adapter ``N``, a receive socket ``s%02d.sock`` and a send-to path ``c%02d.sock`` - (raw Ethernet frames, one datagram per frame) inside a persisted volume - directory — e.g. Cisco CML's iol-runner (see IOLDockerVM). uBridge - binds ``c{N:02d}.sock`` (its receive side) and sends to - ``s{N:02d}.sock``. No TAP is created, the container's network namespace - is untouched and the ``mac_address`` template field is ignored (the - image's agent owns the MAC scheme). + (raw Ethernet frames, one datagram per frame) inside the container — + e.g. Cisco CML's iol-runner (see IOLDockerVM). uBridge reaches the + sockets through the container's root in ``/proc`` (see + ``_unix_socket_wiring_dir``), so no volume mount is required; it binds + ``c{N:02d}.sock`` (its receive side) and sends to ``s{N:02d}.sock``. + No TAP is created, the container's network namespace is untouched and + the ``mac_address`` template field is ignored (the image's agent owns + the MAC scheme). * ``GNS3_UNIX_SOCKET_DIR=`` — in-container directory holding the - socket files (default ``/tmp``). Must be a persisted volume - (extra_volumes) so uBridge can reach the sockets on the host; node - creation fails with an actionable error otherwise. + socket files (default ``/tmp``). """ def __init__(self, *args, **kwargs): @@ -104,7 +103,6 @@ class VendorDockerVM(DockerVM): self._stop_timeout = 60 self._unix_socket_nio = False self._unix_socket_dir = "/tmp" - self._unix_socket_aliases = set() if self._environment: for _line in self._environment.splitlines(): _line = _line.strip().rstrip(",") @@ -167,17 +165,6 @@ class VendorDockerVM(DockerVM): are never shadowed by an empty mount. """ binds = super()._mount_binds(image_info) - if self._unix_socket_nio: - socket_dir = self._unix_socket_dir.rstrip("/") - if not any( - v.rstrip("/") == socket_dir or socket_dir.startswith(v.rstrip("/") + "/") - for v in self._volumes - ): - raise DockerError( - f"GNS3_UNIX_SOCKET_NIO requires socket directory '{self._unix_socket_dir}' of " - f"container '{self._name}' to be a persisted volume (add it to extra_volumes) " - f"so uBridge can reach the sockets on the host" - ) if self._gns3_init: return binds binds = [b for b in binds if b.get("Target") != "/gns3volumes/etc/network"] @@ -320,57 +307,20 @@ class VendorDockerVM(DockerVM): return self._interface_names[adapter_number] return f"eth{adapter_number}" - @property - def _unix_socket_host_dir(self): + async def _unix_socket_wiring_dir(self): """ - Host-side path of the in-container unix-socket directory: the bind - source of the persisted volume it lives in. - """ - return os.path.join(self.working_dir, os.path.relpath(self._unix_socket_dir, "/")) + Host-side directory to reference in the uBridge unix-NIO commands: + the container's socket directory reached through its root in /proc. - def _unix_socket_wiring_dir(self): - """ - Directory to reference in the uBridge unix-NIO commands. - - AF_UNIX paths are capped at 107 bytes (sun_path minus the NUL), and a - node volume directory (projects//project-files/docker//tmp) - alone is ~120 — uBridge would reject the NIO with "invalid file path - size". Long directories are therefore aliased through a symlink in the - runtime directory (same trick as the uBridge control socket). uBridge - resolves the symlink; the socket files themselves always live in the - persisted volume, and the container-side /tmp paths are unaffected. + The sockets live in the container's own filesystem (no volume mount + needed); /proc//root/ resolves to the same files uBridge + sees, from outside any namespace. This also keeps the path well under + the 107-byte sun_path cap — a node directory (projects//…) + alone exceeds it. """ - host_dir = self._unix_socket_host_dir - if len(host_dir) + len("/c00.sock") <= 107: - return host_dir - - runtime_dir = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir() - alias = os.path.join(runtime_dir, "gns3", f"unixio-{self.id}") - try: - os.makedirs(os.path.dirname(alias), mode=0o700, exist_ok=True) - if os.path.lexists(alias): - os.unlink(alias) - os.symlink(host_dir, alias) - except OSError as e: - raise DockerError( - f"Could not create unix-socket alias '{alias}' for container '{self._name}': {e}" - ) - self._unix_socket_aliases.add(alias) - return alias - - async def _stop_ubridge(self): - """ - Override: also drop the short-path aliases created for the unix-socket - NIO (the sockets themselves are removed by the image's agent / the - next start's cleanup). - """ - - await super()._stop_ubridge() - for alias in self._unix_socket_aliases: - with contextlib.suppress(OSError): - os.unlink(alias) - self._unix_socket_aliases.clear() + pid = await self._get_namespace() + return f"/proc/{pid}/root{self._unix_socket_dir}" async def _add_ubridge_connection(self, nio, adapter_number): """ @@ -379,15 +329,16 @@ class VendorDockerVM(DockerVM): a TAP interface moved into the container's network namespace. Per adapter N the image's network agent is expected to create, inside - GNS3_UNIX_SOCKET_DIR (a persisted volume, enforced by _mount_binds): + GNS3_UNIX_SOCKET_DIR: * ``s{N:02d}.sock`` — its receive socket; frames sent there are injected into guest interface N; * ``c{N:02d}.sock`` — the path it sends guest-egress frames to. uBridge binds the c-socket as its receive side and sends to the - s-socket. No TAP is allocated, the namespace is untouched and guest - MAC addresses are whatever the image's agent uses. + s-socket (both reached through /proc — see _unix_socket_wiring_dir). + No TAP is allocated, the namespace is untouched and guest MAC + addresses are whatever the image's agent uses. """ if not self._unix_socket_nio: @@ -406,8 +357,7 @@ class VendorDockerVM(DockerVM): await self._ubridge_send(f"bridge create {bridge_name}") self._bridges.add(bridge_name) - host_dir = self._unix_socket_host_dir - wiring_dir = self._unix_socket_wiring_dir() + wiring_dir = await self._unix_socket_wiring_dir() local_sock = os.path.join(wiring_dir, f"c{adapter_number:02d}.sock") remote_sock = os.path.join(wiring_dir, f"s{adapter_number:02d}.sock") @@ -424,7 +374,8 @@ class VendorDockerVM(DockerVM): f"Socket '{remote_sock}' for adapter {adapter_number} of container " f"'{self._name}' did not appear within 30 seconds. Check that the " f"container's port count covers adapter {adapter_number} and that " - f"'{self._unix_socket_dir}' is bind-mounted from the node directory." + f"its network agent creates the per-adapter socket pair in " + f"'{self._unix_socket_dir}'." ) await self._ubridge_send(f'bridge add_nio_unix {bridge_name} "{local_sock}" "{remote_sock}"') diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 5986e4dd0..4a82d9a27 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -23,7 +23,6 @@ the uBridge command stream. """ import asyncio -import glob import json import os import uuid @@ -98,6 +97,23 @@ def _seed_proc(stdout=b"seedcid\n", returncode=0): return proc +# A container PID that can never exist on a real host (Linux caps PIDs at +# 2^22): lets the wiring tests run against /proc paths without any risk of +# touching a live process's files. +_FAKE_PID = 4194304 + + +async def _no_wait(path, timeout=None): + """Stand-in for wait_for_file_creation: pretend the socket is there.""" + return None + + +def _mock_wiring(vm): + """Mock everything _add_ubridge_connection's unix-NIO path needs.""" + vm._ubridge_hypervisor = MagicMock() + vm._get_namespace = AsyncioMagicMock(return_value=_FAKE_PID) + + # --------------------------------------------------------------------------- # Factory selection # --------------------------------------------------------------------------- @@ -178,7 +194,7 @@ async def test_create_keeps_image_entrypoint(compute_project, manager): @pytest.mark.asyncio -async def test_create_auto_adds_config_and_tmp_volumes(compute_project, manager): +async def test_create_auto_adds_config_and_tmp_run_volumes(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "iol-xe"}]): @@ -190,13 +206,16 @@ async def test_create_auto_adds_config_and_tmp_volumes(compute_project, manager) await vm.create() sent = mock.call_args.kwargs["data"] targets = [m["Target"] for m in sent["HostConfig"]["Mounts"]] - # both volumes are forced and bound at their real in-container - # paths (skip-init retargeting), reachable by uBridge on the host + # /config (runner config) and /tmp/run (startup-config + NVRAM) + # are forced and bound at their real in-container paths + # (skip-init retargeting); the sockets stay in the container's + # own /tmp, reached via /proc, so /tmp itself is not a volume assert "/config" in targets - assert "/tmp" in targets + assert "/tmp/run" in targets + assert "/tmp" not in targets assert not any(t.startswith("/gns3volumes/") for t in targets) vol_env = [v for v in sent["Env"] if v.startswith("GNS3_VOLUMES=")][0] - assert "/config" in vol_env and "/tmp" in vol_env + assert "/config" in vol_env and "/tmp/run" in vol_env @pytest.mark.asyncio @@ -274,14 +293,16 @@ async def test_start_creates_run_dir(compute_project, manager): @pytest.mark.asyncio -async def test_start_cleans_stale_sockets_but_keeps_run(compute_project, manager): +async def test_start_does_no_host_side_socket_cleanup(compute_project, manager): + """ + Sockets live in the container's own /tmp (fresh in every container GNS3 + creates), so start() must not touch anything under the node's tmp/ + beyond creating tmp/run. + """ vm = _make_vm(compute_project, manager) tmp_dir = os.path.join(vm.working_dir, "tmp") os.makedirs(os.path.join(tmp_dir, "run"), exist_ok=True) - for name in ("s00.sock", "c00.sock", "s01.sock", "c01.sock"): - open(os.path.join(tmp_dir, name), "w").close() - os.makedirs(os.path.join(tmp_dir, "netio1000")) open(os.path.join(tmp_dir, "run", "nvram_00001"), "w").close() open(os.path.join(tmp_dir, "run", "config"), "w").close() @@ -290,31 +311,11 @@ async def test_start_cleans_stale_sockets_but_keeps_run(compute_project, manager with asyncio_patch("gns3server.compute.docker.Docker.query"): await vm.start() - assert glob.glob(os.path.join(tmp_dir, "s??.sock")) == [] - assert glob.glob(os.path.join(tmp_dir, "c??.sock")) == [] - assert not os.path.exists(os.path.join(tmp_dir, "netio1000")) - # the persistent runtime survives the cleanup + # the persistent runtime is untouched assert os.path.exists(os.path.join(tmp_dir, "run", "nvram_00001")) assert os.path.exists(os.path.join(tmp_dir, "run", "config")) -@pytest.mark.asyncio -async def test_start_skips_cleanup_when_already_running(compute_project, manager): - - vm = _make_vm(compute_project, manager) - tmp_dir = os.path.join(vm.working_dir, "tmp") - os.makedirs(tmp_dir, exist_ok=True) - open(os.path.join(tmp_dir, "s00.sock"), "w").close() - - _mock_start(vm, state="running") - with patch("gns3server.compute.docker.Docker.install_busybox"): - with asyncio_patch("gns3server.compute.docker.Docker.query"): - await vm.start() - - # live runner sockets must not be deleted behind its back - assert os.path.exists(os.path.join(tmp_dir, "s00.sock")) - - @pytest.mark.asyncio async def test_fix_permissions_is_noop(compute_project, manager): @@ -344,49 +345,36 @@ async def test_restart_is_graceful_stop_then_start(compute_project, manager): async def test_add_ubridge_connection_unix_wiring(compute_project, manager): vm = _make_vm(compute_project, manager) - vm._ubridge_hypervisor = MagicMock() - host_dir = os.path.join(vm.working_dir, "tmp") - os.makedirs(host_dir, exist_ok=True) - # the runner's receive socket must exist (created by the container) - open(os.path.join(host_dir, "s00.sock"), "w").close() + _mock_wiring(vm) nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) - await vm._add_ubridge_connection(nio, 0) + with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation", + side_effect=_no_wait): + await vm._add_ubridge_connection(nio, 0) sent = [c for c in vm._ubridge_hypervisor.method_calls if "send" in str(c)] flat = "\n".join(str(c) for c in sent) - wiring_dir = vm._unix_socket_wiring_dir() # may alias host_dir when long + local_sock = f"/proc/{_FAKE_PID}/root/tmp/c00.sock" + remote_sock = f"/proc/{_FAKE_PID}/root/tmp/s00.sock" + # the wiring path rides the container's root in /proc — well under + # sun_path's 107 bytes no matter how deep the project directory is + assert len(local_sock) <= 107 assert call.send("bridge create bridge0") in sent - assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(wiring_dir, "c00.sock")}" ' - f'"{os.path.join(wiring_dir, "s00.sock")}"') in sent + assert call.send(f'bridge add_nio_unix bridge0 "{local_sock}" "{remote_sock}"') in sent assert "add_nio_udp bridge0 4242 127.0.0.1 4343" in flat assert "bridge start bridge0" in flat # the TAP/namespace path must not be used at all assert "add_nio_tap" not in flat assert "move_to_ns" not in flat assert "set_mac_addr" not in flat - assert vm._ethernet_adapters[0].host_ifc == os.path.join(wiring_dir, "c00.sock") - - -@pytest.mark.asyncio -async def test_add_ubridge_connection_stale_local_socket_unlinked(compute_project, manager): - - vm = _make_vm(compute_project, manager) - vm._ubridge_hypervisor = MagicMock() - host_dir = os.path.join(vm.working_dir, "tmp") - os.makedirs(host_dir, exist_ok=True) - open(os.path.join(host_dir, "c00.sock"), "w").close() - open(os.path.join(host_dir, "s00.sock"), "w").close() - - await vm._add_ubridge_connection(None, 0) - assert not os.path.exists(os.path.join(host_dir, "c00.sock")) + assert vm._ethernet_adapters[0].host_ifc == local_sock @pytest.mark.asyncio async def test_add_ubridge_connection_adapter_out_of_range(compute_project, manager): vm = _make_vm(compute_project, manager) - vm._ubridge_hypervisor = MagicMock() + _mock_wiring(vm) with pytest.raises(DockerError): await vm._add_ubridge_connection(None, 42) @@ -395,7 +383,7 @@ async def test_add_ubridge_connection_adapter_out_of_range(compute_project, mana async def test_add_ubridge_connection_timeout_is_actionable(compute_project, manager): vm = _make_vm(compute_project, manager) - vm._ubridge_hypervisor = MagicMock() + _mock_wiring(vm) async def raise_timeout(path, timeout=60): raise asyncio.TimeoutError() @@ -404,21 +392,19 @@ async def test_add_ubridge_connection_timeout_is_actionable(compute_project, man side_effect=raise_timeout): with pytest.raises(DockerError) as excinfo: await vm._add_ubridge_connection(None, 0) - # the message names the adapter and the socket directory + # the message names the adapter and the exact wiring path assert "adapter 0" in str(excinfo.value) - assert "/tmp" in str(excinfo.value) + assert f"/proc/{_FAKE_PID}/root/tmp/s00.sock" in str(excinfo.value) @pytest.mark.asyncio async def test_add_ubridge_connection_without_nio_still_wires(compute_project, manager): vm = _make_vm(compute_project, manager) - vm._ubridge_hypervisor = MagicMock() - host_dir = os.path.join(vm.working_dir, "tmp") - os.makedirs(host_dir, exist_ok=True) - open(os.path.join(host_dir, "s00.sock"), "w").close() - - await vm._add_ubridge_connection(None, 0) + _mock_wiring(vm) + with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation", + side_effect=_no_wait): + await vm._add_ubridge_connection(None, 0) flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) assert "bridge create bridge0" in flat assert "add_nio_unix" in flat @@ -452,23 +438,16 @@ def test_env_unix_socket_nio_parsing(compute_project, manager): assert vm._unix_socket_nio is False -@pytest.mark.asyncio -async def test_unix_socket_dir_must_be_a_volume(compute_project, manager): +def test_unix_socket_dir_needs_no_volume(compute_project, manager): vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", console_type="docker_exec", environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", extra_volumes=[]) - with pytest.raises(DockerError) as excinfo: - vm._mount_binds({"Config": {"Volumes": {}}}) - assert "extra_volumes" in str(excinfo.value) - - vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", - console_type="docker_exec", - environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", - extra_volumes=["/tmp"]) + # sockets are reached through /proc, not through a volume bind — creating + # the container without the socket dir in extra_volumes is fine binds = vm._mount_binds({"Config": {"Volumes": {}}}) - assert any(b["Target"] == "/tmp" for b in binds) + assert not any(b["Target"] == "/tmp" for b in binds) @pytest.mark.asyncio @@ -477,44 +456,10 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", console_type="docker_exec", environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks") - vm._ubridge_hypervisor = MagicMock() - host_dir = os.path.join(vm.working_dir, "var", "run", "socks") - os.makedirs(host_dir, exist_ok=True) - open(os.path.join(host_dir, "s00.sock"), "w").close() - - await vm._add_ubridge_connection(None, 0) - flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) - assert f'"{os.path.join(vm._unix_socket_wiring_dir(), "s00.sock")}"' in flat - assert "add_nio_tap" not in flat - - -@pytest.mark.asyncio -async def test_long_socket_path_aliased_through_runtime_dir(compute_project, manager, monkeypatch, tmp_path): - """ - Node volume paths exceed sun_path's 107 bytes; the wiring must alias them - through a short symlink so uBridge accepts the NIO (and remove the alias - when uBridge stops). - """ - - from unittest.mock import PropertyMock - - long_dir = str(tmp_path / "a-very-long-project-directory-name" / ("x" * 40) / "tmp") - os.makedirs(long_dir, exist_ok=True) - open(os.path.join(long_dir, "s00.sock"), "w").close() - runtime_dir = tmp_path / "run" - monkeypatch.setenv("XDG_RUNTIME_DIR", str(runtime_dir)) - - vm = _make_vm(compute_project, manager) - vm._ubridge_hypervisor = MagicMock() - with patch.object(type(vm), "_unix_socket_host_dir", new_callable=PropertyMock, - return_value=long_dir): + _mock_wiring(vm) + with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation", + side_effect=_no_wait): await vm._add_ubridge_connection(None, 0) - alias = os.path.join(str(runtime_dir), "gns3", f"unixio-{vm.id}") - assert os.path.islink(alias) and os.readlink(alias) == long_dir - flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) - assert f'"{os.path.join(alias, "s00.sock")}"' in flat - assert long_dir not in flat # uBridge only ever sees the short path - - vm._ubridge_hypervisor.stop = AsyncioMagicMock() - await vm._stop_ubridge() - assert not os.path.lexists(alias) + flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) + assert f'"/proc/{_FAKE_PID}/root/var/run/socks/s00.sock"' in flat + assert "add_nio_tap" not in flat From d04a99f93474382aa12b58edfecd28bc426e0fc5 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 30 Aug 2026 14:13:35 +0800 Subject: [PATCH 13/60] refactor: wire unix-socket NIOs through a per-node runtime directory uBridge cannot reach sockets bind-mounted from the node's projects directory: the path exceeds AF_UNIX's 107-byte sun_path cap. The /proc detour from the previous commit is a dead end too - the runner drops from root to the server uid, which makes the process non-dumpable and its /proc//root unreadable for the unprivileged server. Instead, mirror how CML itself runs the image (source=/tmp, target=/tmp in its node definition): bind a per-node directory from the runtime directory (/gns3/unixio/, next to the uBridge control sockets) at the image's socket directory. The path is short, the directory is owned by the server user (whom the agent drops its privileges to), and it is removed with the node. A socket directory covered by a persisted volume keeps working as before. IOLDockerVM persists only /tmp/run (nested bind at /tmp/run) and cleans stale sockets/netio dirs from the socket directory on start, skipping the cleanup when the container is already running. A failed wiring now stops uBridge so the next start does not hit 'bridge already exist'. --- docs/features/iol-runner-docker.md | 44 +++-- gns3server/compute/docker/iol_docker_vm.py | 41 ++++- gns3server/compute/docker/vendor_docker_vm.py | 146 +++++++++++----- tests/compute/docker/test_iol_docker_vm.py | 156 ++++++++++++------ 4 files changed, 271 insertions(+), 116 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index c9b02841c..08d5a215c 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -36,10 +36,11 @@ graph LR IOL -->|"netio bus /tmp/netio<uid>/"| NETIOMUX --> SOCKETS end subgraph Host - UBRIDGE["uBridge bridgeN
add_nio_unix /proc/PID/root/tmp/cNN.sock …
+ add_nio_udp (topology)"] - VOL["project-files/docker/<node>/config
(+ tmp/run nested bind)"] + UBRIDGE["uBridge bridgeN
add_nio_unix …/gns3/unixio/<node>/cNN.sock …
+ add_nio_udp (topology)"] + RTDIR["/run/user/<uid>/gns3/unixio/<node>
(bind-mounted at /tmp)"] + VOL["project-files/docker/<node>/config
(+ tmp/run, nested bind at /tmp/run)"] end - SOCKETS <-->|"raw Ethernet frames via
the container root in /proc"| UBRIDGE + SOCKETS <-->|"same files, two spellings
(the /tmp bind)"| RTDIR <--> UBRIDGE UBRIDGE -.->|"iol-config.json +
persistent /tmp/run"| VOL ``` @@ -51,25 +52,29 @@ graph LR namespace. Per interface N it creates, inside the container's `/tmp`: a receive socket `s%02d.sock` (frames sent there are injected into guest interface N) and a send-to path `c%02d.sock` (whoever binds it receives - the guest's frames). Frames are **raw Ethernet**, one datagram per frame. - uBridge reaches both through the container's root in `/proc` - (`/proc//root/tmp/…`) — a short path (AF_UNIX names are - capped at 107 bytes, which a project directory path alone would exceed) - that requires no volume mount and leaves the sockets ephemeral in the - container, fresh in every container GNS3 creates. + the guest's frames). Frames are **raw Ethernet**, one datagram per frame — + the same two-mailbox convention uBridge's `add_nio_unix` natively speaks + (it binds the c-socket, sends to the s-socket). GNS3 bind-mounts a + per-node directory from the runtime directory + (`/run/user//gns3/unixio/`, next to the uBridge control + sockets) at the container's `/tmp`, so uBridge reaches the sockets as + plain host files: the path stays far under AF_UNIX's 107-byte `sun_path` + cap (a projects-tree node path alone exceeds it) and the directory is + owned by the server user, to whom the runner drops its privileges. This + mirrors how CML itself runs the image (`source=…/tmp,target=/tmp` in its + node definition). The directory is ephemeral and removed with the node. * **Licensing**: the image ships a self-consistent `/etc/hostid` + `.iourc` pair, and the runner regenerates the license from the host ID at boot — nothing to configure. * **Persistence**: `/tmp/run` (the IOL working directory) holds the NETMAP, the startup-config (`config`, plain IOS format) and NVRAM (`nvram_00001`). It is the only `/tmp` path that needs to survive: GNS3 bind-mounts the - node directory's `tmp/run/` at `/tmp/run`, so the router's configuration - survives stop/start and container recreation while everything else in - `/tmp` stays ephemeral. The generated config maps the runner to the - server's uid/gid (`user-id`/`group-id`), so all files it creates are owned - by the server user — which is also what lets an unprivileged uBridge - traverse `/proc//root` to reach the container's sockets (no - permission-fix pass needed). + node directory's `tmp/run/` at `/tmp/run` (nested inside the runtime-dir + bind), so the router's configuration survives stop/start and container + recreation while sockets, netio buses and runner logs stay ephemeral. + The generated config maps the runner to the server's uid/gid + (`user-id`/`group-id`), so all files it creates are owned by the server + user (no permission-fix pass needed). ## Template @@ -95,9 +100,9 @@ keeps the template self-documenting. | Mechanism | Where | What it does | |---|---|---| -| `GNS3_UNIX_SOCKET_NIO=1` | `VendorDockerVM` | `_add_ubridge_connection` override: `bridge create` + `bridge add_nio_unix /proc//root/c{N:02d}.sock /proc//root/s{N:02d}.sock` instead of TAP + `docker move_to_ns`. No TAP allocation, no `set_mac_addr`, namespace untouched, no volume required. | +| `GNS3_UNIX_SOCKET_NIO=1` | `VendorDockerVM` | `_add_ubridge_connection` override: `bridge create` + `bridge add_nio_unix /c{N:02d}.sock /s{N:02d}.sock` instead of TAP + `docker move_to_ns`. No TAP allocation, no `set_mac_addr`, namespace untouched. The socket directory is bound from a per-node runtime directory unless a persisted volume already covers it. | | `GNS3_UNIX_SOCKET_DIR=` | `VendorDockerVM` | In-container socket directory (default `/tmp`). Any image whose agent exposes the `s%02d`/`c%02d` datagram pairs can use this without the IOL specifics. | -| `GNS3_IOL_RUNNER=1` | `IOLDockerVM` (selected in the manager) | Forces skip-init + unix-socket NIO + the `/config` and `/tmp/run` volumes; on every start writes `/config/iol-config.json` (`num-eth` = adapter count, `num-serial` = 0, memory from `GNS3_IOL_MEMORY`, default 2048) and creates `/tmp/run/` (the IOL process dies without it). | +| `GNS3_IOL_RUNNER=1` | `IOLDockerVM` (selected in the manager) | Forces skip-init + unix-socket NIO + the `/config` and `/tmp/run` volumes; on every start writes `/config/iol-config.json` (`num-eth` = adapter count, `num-serial` = 0, memory from `GNS3_IOL_MEMORY`, default 2048), creates `/tmp/run/` (the IOL process dies without it) and removes stale sockets/netio dirs from the socket directory (`tmp/run` is never touched). | | `restart()` hardening | `IOLDockerVM` | The base `docker restart` would boot the runner on a stale config and leave uBridge wired to the previous run's sockets; reload becomes graceful stop (SIGTERM → NVRAM flush) + full start. | `GNS3_STOP_TIMEOUT` (default 60) controls the SIGTERM grace period on stop. @@ -112,6 +117,9 @@ diagnosing wiring issues). + ~512 MB headroom or the OOM-killer will shoot the router. * **MAC addresses**: the `mac_address` template field and per-adapter custom MACs are ignored — IOL derives its own scheme (`aabb.cc00.0XY0`). +* **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0` + (4 ports per unit, matching the adapter-count granularity) — startup + configs addressing `GigabitEthernet…` are rejected by the parser. * **Adapters**: change the adapter count while the node is stopped; the config is regenerated on the next start and the runner creates the matching socket set (IOL granularity is 4 ports per unit). diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index cc3302b81..6243f8a94 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -28,17 +28,22 @@ Networking does not use the container's network namespace at all: the runner's netiomux exposes per-interface AF_UNIX datagram sockets in the container's ``/tmp`` (``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames), wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM -(uBridge reaches them through the container's root in /proc). Because the -netio bus directory is private to the container's /tmp, the application IDs -are fixed constants with no cross-node collisions. +(uBridge reaches them through a per-node runtime directory bound at /tmp — +see ``VendorDockerVM._unix_socket_host_dir``). Because the netio bus +directory is private to the node, the application IDs are fixed constants +with no cross-node collisions. This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker. """ +import contextlib +import glob import json import logging import os +import shutil +from gns3server.compute.docker.docker_error import DockerHttp404Error from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM log = logging.getLogger(__name__) @@ -124,22 +129,40 @@ class IOLDockerVM(VendorDockerVM): boot (the runner writes NETMAP there but does not create it). * ``/config/iol-config.json`` is rewritten on every start so adapter-count and memory changes take effect. + * Sockets and netio bus directories left in the wiring directory by a + previous (possibly SIGKILLed) run are removed — the runner rebinds + them on boot and would fail on a stale file. - The netiomux sockets and the netio bus directory need no cleanup: - they live in the container's own /tmp, which is fresh in every - container GNS3 creates (containers are recreated on each start). + ``tmp/run`` (startup-config, NVRAM) is never touched. Neither is + anything while the container is already running (idempotent start of + a live node: the sockets belong to the running runner). """ + try: + state = await self._get_container_state() + except DockerHttp404Error: + state = "stopped" + os.makedirs(os.path.join(self.working_dir, "tmp", "run"), exist_ok=True) self._write_iol_config() + if state == "running": + return + + wiring_dir = self._unix_socket_wiring_dir() + for pattern in ("s??.sock", "c??.sock"): + for stale in glob.glob(os.path.join(wiring_dir, pattern)): + with contextlib.suppress(OSError): + os.unlink(stale) + for netio_dir in glob.glob(os.path.join(wiring_dir, "netio*")): + shutil.rmtree(netio_dir, ignore_errors=True) + def _write_iol_config(self): """ Write the runner's config file on the host side of the /config volume. The runner drops to user-id/group-id after its setup, so everything it - creates is owned by the server user — which is also what lets an - unprivileged uBridge traverse /proc//root to reach the - container's sockets. + creates is owned by the server user — which is also what lets the + (unprivileged) uBridge write into the node's socket directory. """ config = { diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index 7e4abfd4d..6e788a83d 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -32,6 +32,7 @@ import json import logging import os import shutil +import tempfile from gns3server.utils.asyncio import wait_for_file_creation from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer @@ -70,13 +71,16 @@ class VendorDockerVM(DockerVM): network namespace. For images whose network agent exposes, per adapter ``N``, a receive socket ``s%02d.sock`` and a send-to path ``c%02d.sock`` (raw Ethernet frames, one datagram per frame) inside the container — - e.g. Cisco CML's iol-runner (see IOLDockerVM). uBridge reaches the - sockets through the container's root in ``/proc`` (see - ``_unix_socket_wiring_dir``), so no volume mount is required; it binds + e.g. Cisco CML's iol-runner (see IOLDockerVM). uBridge binds ``c{N:02d}.sock`` (its receive side) and sends to ``s{N:02d}.sock``. - No TAP is created, the container's network namespace is untouched and - the ``mac_address`` template field is ignored (the image's agent owns - the MAC scheme). + Unless the directory is already covered by a persisted volume, a + per-node directory from the runtime directory is bind-mounted there — + owned by the server user (such agents drop privileges and cannot write + into a root-owned directory) and short enough for an AF_UNIX path, + which a node directory inside the projects tree exceeds. No TAP is + created, the container's network namespace is untouched and the + ``mac_address`` template field is ignored (the image's agent owns the + MAC scheme). * ``GNS3_UNIX_SOCKET_DIR=`` — in-container directory holding the socket files (default ``/tmp``). """ @@ -165,6 +169,22 @@ class VendorDockerVM(DockerVM): are never shadowed by an empty mount. """ binds = super()._mount_binds(image_info) + if self._unix_socket_nio: + socket_dir = self._unix_socket_dir.rstrip("/") + if not any( + v.rstrip("/") == socket_dir or socket_dir.startswith(v.rstrip("/") + "/") + for v in self._volumes + ): + # The image's network agent drops privileges before using the + # socket directory, so it must be writable by the server user: + # bind a per-node directory from the runtime directory (see + # _unix_socket_host_dir). + binds.append({ + "Type": "bind", + "Source": self._unix_socket_host_dir(), + "Target": socket_dir, + "BindOptions": {"Propagation": "rprivate"}, + }) if self._gns3_init: return binds binds = [b for b in binds if b.get("Target") != "/gns3volumes/etc/network"] @@ -307,20 +327,55 @@ class VendorDockerVM(DockerVM): return self._interface_names[adapter_number] return f"eth{adapter_number}" - async def _unix_socket_wiring_dir(self): + def _unix_socket_host_dir(self): """ - Host-side directory to reference in the uBridge unix-NIO commands: - the container's socket directory reached through its root in /proc. + Host-side directory bind-mounted at the in-container socket directory + when the latter is not already covered by a persisted volume: a + per-node directory under the runtime directory, next to the uBridge + control sockets. - The sockets live in the container's own filesystem (no volume mount - needed); /proc//root/ resolves to the same files uBridge - sees, from outside any namespace. This also keeps the path well under - the 107-byte sun_path cap — a node directory (projects//…) - alone exceeds it. + AF_UNIX paths are capped at 107 bytes (sun_path minus the NUL) — a + node directory inside the projects tree (projects// + project-files/docker//tmp) alone is ~120, so uBridge would + reject the NIO with "invalid file path size". The runtime directory + path stays short no matter where the projects live, and being owned + by the server user, the image's privilege-dropping agent can write + to it. """ - pid = await self._get_namespace() - return f"/proc/{pid}/root{self._unix_socket_dir}" + runtime_dir = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir() + host_dir = os.path.join(runtime_dir, "gns3", "unixio", self.id) + try: + os.makedirs(host_dir, mode=0o700, exist_ok=True) + except OSError as e: + raise DockerError( + f"Could not create unix-socket directory '{host_dir}' for container '{self._name}': {e}" + ) + return host_dir + + def _remove_unix_socket_host_dir(self): + """Best-effort removal of the per-node unix-socket directory.""" + + if self._unix_socket_nio: + shutil.rmtree(self._unix_socket_host_dir(), ignore_errors=True) + + def _unix_socket_wiring_dir(self): + """ + Directory referenced in the uBridge unix-NIO commands: the persisted + volume holding the socket directory, or the per-node runtime + directory bound there by _mount_binds. + """ + + socket_dir = self._unix_socket_dir.rstrip("/") + for volume in self._volumes: + if socket_dir == volume.rstrip("/") or socket_dir.startswith(volume.rstrip("/") + "/"): + return os.path.join(self.working_dir, os.path.relpath(socket_dir, "/")) + return self._unix_socket_host_dir() + + async def delete(self): + # The per-node socket directory is ephemeral; the node is not. + await super().delete() + self._remove_unix_socket_host_dir() async def _add_ubridge_connection(self, nio, adapter_number): """ @@ -336,9 +391,10 @@ class VendorDockerVM(DockerVM): * ``c{N:02d}.sock`` — the path it sends guest-egress frames to. uBridge binds the c-socket as its receive side and sends to the - s-socket (both reached through /proc — see _unix_socket_wiring_dir). - No TAP is allocated, the namespace is untouched and guest MAC - addresses are whatever the image's agent uses. + s-socket (both on the host side of the socket directory — see + _unix_socket_wiring_dir). No TAP is allocated, the namespace is + untouched and guest MAC addresses are whatever the image's agent + uses. """ if not self._unix_socket_nio: @@ -354,31 +410,37 @@ class VendorDockerVM(DockerVM): ) bridge_name = f"bridge{adapter_number}" - await self._ubridge_send(f"bridge create {bridge_name}") - self._bridges.add(bridge_name) - - wiring_dir = await self._unix_socket_wiring_dir() - local_sock = os.path.join(wiring_dir, f"c{adapter_number:02d}.sock") - remote_sock = os.path.join(wiring_dir, f"s{adapter_number:02d}.sock") - - # A c-socket left over from a previous ubridge run would fail its bind. - with contextlib.suppress(OSError): - os.unlink(local_sock) - - # The socket appears when the container's agent finishes its interface - # setup; wait instead of silently blackholing the adapter. try: - await wait_for_file_creation(remote_sock, timeout=30) - except asyncio.TimeoutError: - raise DockerError( - f"Socket '{remote_sock}' for adapter {adapter_number} of container " - f"'{self._name}' did not appear within 30 seconds. Check that the " - f"container's port count covers adapter {adapter_number} and that " - f"its network agent creates the per-adapter socket pair in " - f"'{self._unix_socket_dir}'." - ) + await self._ubridge_send(f"bridge create {bridge_name}") + self._bridges.add(bridge_name) - await self._ubridge_send(f'bridge add_nio_unix {bridge_name} "{local_sock}" "{remote_sock}"') + wiring_dir = self._unix_socket_wiring_dir() + local_sock = os.path.join(wiring_dir, f"c{adapter_number:02d}.sock") + remote_sock = os.path.join(wiring_dir, f"s{adapter_number:02d}.sock") + + # A c-socket left over from a previous ubridge run would fail its bind. + with contextlib.suppress(OSError): + os.unlink(local_sock) + + # The socket appears when the container's agent finishes its interface + # setup; wait instead of silently blackholing the adapter. + try: + await wait_for_file_creation(remote_sock, timeout=30) + except asyncio.TimeoutError: + raise DockerError( + f"Socket '{remote_sock}' for adapter {adapter_number} of container " + f"'{self._name}' did not appear within 30 seconds. Check that the " + f"container's port count covers adapter {adapter_number} and that " + f"its network agent creates the per-adapter socket pair in " + f"'{self._unix_socket_dir}'." + ) + + await self._ubridge_send(f'bridge add_nio_unix {bridge_name} "{local_sock}" "{remote_sock}"') + except Exception: + # A half-wired bridge would make the next start fail with + # "bridge already exist": uBridge stops with the failed start. + await self._stop_ubridge() + raise adapter.host_ifc = local_sock # bookkeeping / removal logging only log.debug( "Adapter %d of container '%s' wired via unix sockets %s <-> %s", diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 4a82d9a27..672325012 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -23,6 +23,7 @@ the uBridge command stream. """ import asyncio +import glob import json import os import uuid @@ -97,21 +98,25 @@ def _seed_proc(stdout=b"seedcid\n", returncode=0): return proc -# A container PID that can never exist on a real host (Linux caps PIDs at -# 2^22): lets the wiring tests run against /proc paths without any risk of -# touching a live process's files. -_FAKE_PID = 4194304 - - -async def _no_wait(path, timeout=None): - """Stand-in for wait_for_file_creation: pretend the socket is there.""" - return None +@pytest.fixture(autouse=True) +def runtime_dir(tmp_path, monkeypatch): + """ + Point the unix-socket runtime directory at a per-test temporary path so + the wiring/mount tests never create per-node directories in the real one. + """ + rt = tmp_path / "run" + monkeypatch.setenv("XDG_RUNTIME_DIR", str(rt)) + return rt def _mock_wiring(vm): """Mock everything _add_ubridge_connection's unix-NIO path needs.""" vm._ubridge_hypervisor = MagicMock() - vm._get_namespace = AsyncioMagicMock(return_value=_FAKE_PID) + + +def _wiring_dir(vm): + """The per-node socket directory this VM wires through.""" + return os.path.join(os.environ["XDG_RUNTIME_DIR"], "gns3", "unixio", vm.id) # --------------------------------------------------------------------------- @@ -205,14 +210,17 @@ async def test_create_auto_adds_config_and_tmp_run_volumes(compute_project, mana vm = _make_vm(compute_project, manager, extra_volumes=[]) await vm.create() sent = mock.call_args.kwargs["data"] - targets = [m["Target"] for m in sent["HostConfig"]["Mounts"]] + mounts = sent["HostConfig"]["Mounts"] + targets = [m["Target"] for m in mounts if m.get("Type") == "bind"] # /config (runner config) and /tmp/run (startup-config + NVRAM) # are forced and bound at their real in-container paths - # (skip-init retargeting); the sockets stay in the container's - # own /tmp, reached via /proc, so /tmp itself is not a volume + # (skip-init retargeting); /tmp is the ephemeral runtime-dir + # bind holding the netiomux sockets assert "/config" in targets assert "/tmp/run" in targets - assert "/tmp" not in targets + tmp_mounts = [m for m in mounts if m["Target"] == "/tmp"] + assert len(tmp_mounts) == 1 + assert tmp_mounts[0]["Source"] == _wiring_dir(vm) assert not any(t.startswith("/gns3volumes/") for t in targets) vol_env = [v for v in sent["Env"] if v.startswith("GNS3_VOLUMES=")][0] assert "/config" in vol_env and "/tmp/run" in vol_env @@ -293,27 +301,47 @@ async def test_start_creates_run_dir(compute_project, manager): @pytest.mark.asyncio -async def test_start_does_no_host_side_socket_cleanup(compute_project, manager): - """ - Sockets live in the container's own /tmp (fresh in every container GNS3 - creates), so start() must not touch anything under the node's tmp/ - beyond creating tmp/run. - """ +async def test_start_cleans_stale_sockets_but_keeps_run(compute_project, manager): vm = _make_vm(compute_project, manager) - tmp_dir = os.path.join(vm.working_dir, "tmp") - os.makedirs(os.path.join(tmp_dir, "run"), exist_ok=True) - open(os.path.join(tmp_dir, "run", "nvram_00001"), "w").close() - open(os.path.join(tmp_dir, "run", "config"), "w").close() + wiring_dir = _wiring_dir(vm) + os.makedirs(wiring_dir, exist_ok=True) + for name in ("s00.sock", "c00.sock", "s01.sock", "c01.sock"): + open(os.path.join(wiring_dir, name), "w").close() + os.makedirs(os.path.join(wiring_dir, "netio1000")) + run_dir = os.path.join(vm.working_dir, "tmp", "run") + os.makedirs(run_dir, exist_ok=True) + open(os.path.join(run_dir, "nvram_00001"), "w").close() + open(os.path.join(run_dir, "config"), "w").close() _mock_start(vm, state="stopped") with patch("gns3server.compute.docker.Docker.install_busybox"): with asyncio_patch("gns3server.compute.docker.Docker.query"): await vm.start() - # the persistent runtime is untouched - assert os.path.exists(os.path.join(tmp_dir, "run", "nvram_00001")) - assert os.path.exists(os.path.join(tmp_dir, "run", "config")) + assert glob.glob(os.path.join(wiring_dir, "s??.sock")) == [] + assert glob.glob(os.path.join(wiring_dir, "c??.sock")) == [] + assert not os.path.exists(os.path.join(wiring_dir, "netio1000")) + # the persistent runtime survives the cleanup + assert os.path.exists(os.path.join(run_dir, "nvram_00001")) + assert os.path.exists(os.path.join(run_dir, "config")) + + +@pytest.mark.asyncio +async def test_start_skips_cleanup_when_already_running(compute_project, manager): + + vm = _make_vm(compute_project, manager) + wiring_dir = _wiring_dir(vm) + os.makedirs(wiring_dir, exist_ok=True) + open(os.path.join(wiring_dir, "s00.sock"), "w").close() + + _mock_start(vm, state="running") + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + await vm.start() + + # live runner sockets must not be deleted behind its back + assert os.path.exists(os.path.join(wiring_dir, "s00.sock")) @pytest.mark.asyncio @@ -346,19 +374,18 @@ async def test_add_ubridge_connection_unix_wiring(compute_project, manager): vm = _make_vm(compute_project, manager) _mock_wiring(vm) + wiring_dir = _wiring_dir(vm) + os.makedirs(wiring_dir, exist_ok=True) + # the runner's receive socket must exist (created by the container) + open(os.path.join(wiring_dir, "s00.sock"), "w").close() nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) - with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation", - side_effect=_no_wait): - await vm._add_ubridge_connection(nio, 0) + await vm._add_ubridge_connection(nio, 0) sent = [c for c in vm._ubridge_hypervisor.method_calls if "send" in str(c)] flat = "\n".join(str(c) for c in sent) - local_sock = f"/proc/{_FAKE_PID}/root/tmp/c00.sock" - remote_sock = f"/proc/{_FAKE_PID}/root/tmp/s00.sock" - # the wiring path rides the container's root in /proc — well under - # sun_path's 107 bytes no matter how deep the project directory is - assert len(local_sock) <= 107 + local_sock = os.path.join(wiring_dir, "c00.sock") + remote_sock = os.path.join(wiring_dir, "s00.sock") assert call.send("bridge create bridge0") in sent assert call.send(f'bridge add_nio_unix bridge0 "{local_sock}" "{remote_sock}"') in sent assert "add_nio_udp bridge0 4242 127.0.0.1 4343" in flat @@ -370,6 +397,20 @@ async def test_add_ubridge_connection_unix_wiring(compute_project, manager): assert vm._ethernet_adapters[0].host_ifc == local_sock +@pytest.mark.asyncio +async def test_add_ubridge_connection_stale_local_socket_unlinked(compute_project, manager): + + vm = _make_vm(compute_project, manager) + _mock_wiring(vm) + wiring_dir = _wiring_dir(vm) + os.makedirs(wiring_dir, exist_ok=True) + open(os.path.join(wiring_dir, "c00.sock"), "w").close() + open(os.path.join(wiring_dir, "s00.sock"), "w").close() + + await vm._add_ubridge_connection(None, 0) + assert not os.path.exists(os.path.join(wiring_dir, "c00.sock")) + + @pytest.mark.asyncio async def test_add_ubridge_connection_adapter_out_of_range(compute_project, manager): @@ -384,6 +425,7 @@ async def test_add_ubridge_connection_timeout_is_actionable(compute_project, man vm = _make_vm(compute_project, manager) _mock_wiring(vm) + vm._stop_ubridge = AsyncioMagicMock() async def raise_timeout(path, timeout=60): raise asyncio.TimeoutError() @@ -394,7 +436,10 @@ async def test_add_ubridge_connection_timeout_is_actionable(compute_project, man await vm._add_ubridge_connection(None, 0) # the message names the adapter and the exact wiring path assert "adapter 0" in str(excinfo.value) - assert f"/proc/{_FAKE_PID}/root/tmp/s00.sock" in str(excinfo.value) + assert "s00.sock" in str(excinfo.value) + # uBridge must not survive a half-wired bridge: the retry would fail + # with "bridge already exist" + assert vm._stop_ubridge.called @pytest.mark.asyncio @@ -402,9 +447,11 @@ async def test_add_ubridge_connection_without_nio_still_wires(compute_project, m vm = _make_vm(compute_project, manager) _mock_wiring(vm) - with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation", - side_effect=_no_wait): - await vm._add_ubridge_connection(None, 0) + wiring_dir = _wiring_dir(vm) + os.makedirs(wiring_dir, exist_ok=True) + open(os.path.join(wiring_dir, "s00.sock"), "w").close() + + await vm._add_ubridge_connection(None, 0) flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) assert "bridge create bridge0" in flat assert "add_nio_unix" in flat @@ -438,16 +485,29 @@ def test_env_unix_socket_nio_parsing(compute_project, manager): assert vm._unix_socket_nio is False -def test_unix_socket_dir_needs_no_volume(compute_project, manager): +def test_unix_socket_dir_bound_from_runtime_dir(compute_project, manager): vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", console_type="docker_exec", environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", extra_volumes=[]) - # sockets are reached through /proc, not through a volume bind — creating - # the container without the socket dir in extra_volumes is fine + # the socket directory is an ephemeral per-node directory from the + # runtime dir, not a volume: writable by the (unprivileged) agent and + # short enough for AF_UNIX binds = vm._mount_binds({"Config": {"Volumes": {}}}) - assert not any(b["Target"] == "/tmp" for b in binds) + socket_binds = [b for b in binds if b.get("Target") == "/tmp"] + assert len(socket_binds) == 1 + assert socket_binds[0]["Type"] == "bind" + assert socket_binds[0]["Source"] == _wiring_dir(vm) + + # a socket dir already covered by a persisted volume gets no extra bind + vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", + extra_volumes=["/tmp"]) + binds = vm._mount_binds({"Config": {"Volumes": {}}}) + assert not any(b.get("Source") == _wiring_dir(vm) for b in binds) + assert any(b.get("Target") == "/tmp" for b in binds) @pytest.mark.asyncio @@ -457,9 +517,11 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage console_type="docker_exec", environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks") _mock_wiring(vm) - with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation", - side_effect=_no_wait): - await vm._add_ubridge_connection(None, 0) + wiring_dir = _wiring_dir(vm) + os.makedirs(wiring_dir, exist_ok=True) + open(os.path.join(wiring_dir, "s00.sock"), "w").close() + + await vm._add_ubridge_connection(None, 0) flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) - assert f'"/proc/{_FAKE_PID}/root/var/run/socks/s00.sock"' in flat + assert f'"{os.path.join(wiring_dir, "s00.sock")}"' in flat assert "add_nio_tap" not in flat From 28f8f665f7dd1c66395fd5eefc0e579f87b74975 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 30 Aug 2026 14:17:38 +0800 Subject: [PATCH 14/60] docs: template creation how-to for iol-runner nodes API/Web-UI creation steps, a field table with the extra_volumes=/tmp footgun, and a 3-step verification checklist. --- docs/features/iol-runner-docker.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 08d5a215c..ddd0178ec 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -78,6 +78,10 @@ graph LR ## Template +Create the template once via `POST /v3/templates` (authenticated — see the +API docs for the auth flow), or in the Web UI under +*Edit → Preferences → Docker templates → New* with the same fields: + ```json { "name": "IOS-XE 17.18.02 IOL", @@ -93,8 +97,23 @@ graph LR } ``` -`/config` and `/tmp/run` are auto-added even if omitted; listing `/config` -keeps the template self-documenting. +| Field | Value | Why | +|---|---|---| +| `environment` | `GNS3_IOL_RUNNER=1` | The switch that selects `IOLDockerVM` (skip-init, unix-socket NIO, auto volumes). Optional: `GNS3_IOL_MEMORY=` (default 2048). | +| `extra_volumes` | `["/config"]` | `/tmp/run` is auto-added. **Never add `/tmp`** — it would persist the socket directory into the projects tree and uBridge would reject the too-long AF_UNIX path. | +| `memory` | IOL memory + ~512 MB | Caps the whole container; below that the OOM-killer shoots the router. | +| `console_type` | `telnet` | The runner muxes the IOS console onto PID 1 stdio; `docker_exec` is not needed. | +| `adapters` | multiple of 4 | IOL port granularity; interfaces are `Ethernet0/0`-style. | + +### Verify + +1. Drop a node into a project and start it — the console shows the + `Linux Unix (i686)` banner within seconds. +2. `$XDG_RUNTIME_DIR/gns3/unixio//` contains `s00.sock`… (one + pair per adapter). +3. The startup-config lives at + `project-files/docker//tmp/run/config` (interface names + `Ethernet0/0`, not `GigabitEthernet0/0`). ## Server mechanisms From cd17306372a3c918cb55d4dc93eba959cad0dd21 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 30 Aug 2026 14:22:10 +0800 Subject: [PATCH 15/60] docs: docker memory is optional for iol-runner nodes 0 (the default) passes through as Docker's no-limit; the IOL-memory+512 guidance only applies when a cap is set. --- docs/features/iol-runner-docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index ddd0178ec..f76da6a4b 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -101,7 +101,7 @@ API docs for the auth flow), or in the Web UI under |---|---|---| | `environment` | `GNS3_IOL_RUNNER=1` | The switch that selects `IOLDockerVM` (skip-init, unix-socket NIO, auto volumes). Optional: `GNS3_IOL_MEMORY=` (default 2048). | | `extra_volumes` | `["/config"]` | `/tmp/run` is auto-added. **Never add `/tmp`** — it would persist the socket directory into the projects tree and uBridge would reject the too-long AF_UNIX path. | -| `memory` | IOL memory + ~512 MB | Caps the whole container; below that the OOM-killer shoots the router. | +| `memory` | optional; `0` (default) = no cap | Unset works — Docker applies no limit. When you do set a cap, keep it at IOL memory + ~512 MB, or the cgroup OOM-killer shoots the router. | | `console_type` | `telnet` | The runner muxes the IOS console onto PID 1 stdio; `docker_exec` is not needed. | | `adapters` | multiple of 4 | IOL port granularity; interfaces are `Ethernet0/0`-style. | From 4c818c6b3794d90f9b226e800c717d97de947648 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 30 Aug 2026 14:25:19 +0800 Subject: [PATCH 16/60] feat: show IOL-style interface names on iol-runner ports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name ports after the IOL interface they map to (Ethernet0/0, one 4-port unit per adapter range) via the _get_container_ifname override point, so the GNS3 UI matches the IOS CLI. Display only — the flat adapter number remains the socket index. --- docs/features/iol-runner-docker.md | 2 +- gns3server/compute/docker/iol_docker_vm.py | 9 +++++++++ tests/compute/docker/test_iol_docker_vm.py | 14 ++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index f76da6a4b..bdee70e80 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -103,7 +103,7 @@ API docs for the auth flow), or in the Web UI under | `extra_volumes` | `["/config"]` | `/tmp/run` is auto-added. **Never add `/tmp`** — it would persist the socket directory into the projects tree and uBridge would reject the too-long AF_UNIX path. | | `memory` | optional; `0` (default) = no cap | Unset works — Docker applies no limit. When you do set a cap, keep it at IOL memory + ~512 MB, or the cgroup OOM-killer shoots the router. | | `console_type` | `telnet` | The runner muxes the IOS console onto PID 1 stdio; `docker_exec` is not needed. | -| `adapters` | multiple of 4 | IOL port granularity; interfaces are `Ethernet0/0`-style. | +| `adapters` | multiple of 4 | IOL port granularity. Ports are shown as `Ethernet0/0`-style (one 4-port unit per adapter range), matching the IOS CLI. | ### Verify diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index 6243f8a94..b5cd588c7 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -105,6 +105,15 @@ class IOLDockerVM(VendorDockerVM): volumes.append(needed) return volumes + def _get_container_ifname(self, adapter_number): + """ + Override: name ports after the IOL interface they map to + (Ethernet0/0, Ethernet0/1, … — a new 4-port unit every four + adapters), so what the GNS3 UI shows matches the IOS CLI. Wiring is + unaffected: the flat adapter number remains the socket index. + """ + return f"Ethernet{adapter_number // 4}/{adapter_number % 4}" + async def start(self): await self._prepare_iol_runtime() diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 672325012..006eb5a77 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -177,6 +177,20 @@ def test_iol_memory_knob(compute_project, manager): assert vm._iol_memory == 2048 +# --------------------------------------------------------------------------- +# Port naming +# --------------------------------------------------------------------------- + +def test_ports_are_named_after_iol_interfaces(compute_project, manager): + + vm = _make_vm(compute_project, manager, adapters=8) + assert vm._get_container_ifname(0) == "Ethernet0/0" + assert vm._get_container_ifname(3) == "Ethernet0/3" + # the second 4-port unit starts its own slot numbering + assert vm._get_container_ifname(4) == "Ethernet1/0" + assert vm._get_container_ifname(7) == "Ethernet1/3" + + # --------------------------------------------------------------------------- # create() # --------------------------------------------------------------------------- From 9b55164064b6a87cd0cfff438df22d3c8538a6b2 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 30 Aug 2026 14:37:08 +0800 Subject: [PATCH 17/60] revert IOL port-name override; document custom_adapters instead _get_container_ifname only names the kernel interface inside the container's network namespace (single call site: docker move_to_ns) - IOL never uses that path, and the controller port list never reads it. Name the ports through the template's custom_adapters instead (the mechanism SR Linux appliances use for mgmt0/e1-1): adapter N shows as Ethernet{N/4}/{N%4}, matching the IOS CLI, with no server-side changes. --- docs/features/iol-runner-docker.md | 18 +++++++++++++----- gns3server/compute/docker/iol_docker_vm.py | 9 --------- tests/compute/docker/test_iol_docker_vm.py | 14 -------------- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index bdee70e80..465dd981d 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -93,7 +93,12 @@ API docs for the auth flow), or in the Web UI under "console_type": "telnet", "environment": "GNS3_IOL_RUNNER=1", "extra_volumes": ["/config"], - "memory": 2560 + "custom_adapters": [ + {"adapter_number": 0, "port_name": "Ethernet0/0"}, + {"adapter_number": 1, "port_name": "Ethernet0/1"}, + {"adapter_number": 2, "port_name": "Ethernet0/2"}, + {"adapter_number": 3, "port_name": "Ethernet0/3"} + ] } ``` @@ -101,17 +106,20 @@ API docs for the auth flow), or in the Web UI under |---|---|---| | `environment` | `GNS3_IOL_RUNNER=1` | The switch that selects `IOLDockerVM` (skip-init, unix-socket NIO, auto volumes). Optional: `GNS3_IOL_MEMORY=` (default 2048). | | `extra_volumes` | `["/config"]` | `/tmp/run` is auto-added. **Never add `/tmp`** — it would persist the socket directory into the projects tree and uBridge would reject the too-long AF_UNIX path. | +| `custom_adapters` | one entry per adapter | Names each port after the IOL interface it maps to: adapter `N` → `Ethernet{N/4}/{N%4}` (same mechanism SR Linux uses for `mgmt0`/`e1-1`). Raise the list up to `Ethernet7/3` (32 ports, the IOL maximum) when raising `adapters`; adapters beyond the list fall back to `eth{N}`. | | `memory` | optional; `0` (default) = no cap | Unset works — Docker applies no limit. When you do set a cap, keep it at IOL memory + ~512 MB, or the cgroup OOM-killer shoots the router. | | `console_type` | `telnet` | The runner muxes the IOS console onto PID 1 stdio; `docker_exec` is not needed. | -| `adapters` | multiple of 4 | IOL port granularity. Ports are shown as `Ethernet0/0`-style (one 4-port unit per adapter range), matching the IOS CLI. | +| `adapters` | multiple of 4 | IOL port granularity (one 4-port unit per range). | ### Verify -1. Drop a node into a project and start it — the console shows the +1. The node's port list shows `Ethernet0/0`… (from the template's + `custom_adapters`). +2. Drop a node into a project and start it — the console shows the `Linux Unix (i686)` banner within seconds. -2. `$XDG_RUNTIME_DIR/gns3/unixio//` contains `s00.sock`… (one +3. `$XDG_RUNTIME_DIR/gns3/unixio//` contains `s00.sock`… (one pair per adapter). -3. The startup-config lives at +4. The startup-config lives at `project-files/docker//tmp/run/config` (interface names `Ethernet0/0`, not `GigabitEthernet0/0`). diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index b5cd588c7..6243f8a94 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -105,15 +105,6 @@ class IOLDockerVM(VendorDockerVM): volumes.append(needed) return volumes - def _get_container_ifname(self, adapter_number): - """ - Override: name ports after the IOL interface they map to - (Ethernet0/0, Ethernet0/1, … — a new 4-port unit every four - adapters), so what the GNS3 UI shows matches the IOS CLI. Wiring is - unaffected: the flat adapter number remains the socket index. - """ - return f"Ethernet{adapter_number // 4}/{adapter_number % 4}" - async def start(self): await self._prepare_iol_runtime() diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 006eb5a77..672325012 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -177,20 +177,6 @@ def test_iol_memory_knob(compute_project, manager): assert vm._iol_memory == 2048 -# --------------------------------------------------------------------------- -# Port naming -# --------------------------------------------------------------------------- - -def test_ports_are_named_after_iol_interfaces(compute_project, manager): - - vm = _make_vm(compute_project, manager, adapters=8) - assert vm._get_container_ifname(0) == "Ethernet0/0" - assert vm._get_container_ifname(3) == "Ethernet0/3" - # the second 4-port unit starts its own slot numbering - assert vm._get_container_ifname(4) == "Ethernet1/0" - assert vm._get_container_ifname(7) == "Ethernet1/3" - - # --------------------------------------------------------------------------- # create() # --------------------------------------------------------------------------- From 0ca9ccc63710affa790281b609a7194e08aefe71 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 30 Aug 2026 21:33:38 +0800 Subject: [PATCH 18/60] feat: model IOL adapters as 4-port units like the IOU node type IOL interfaces come in 4-port units (Ethernet0/0-3, Ethernet1/0-3, ...), addressed like IOU as (adapter_number, port_number 0-3): - IOLDockerVM builds EthernetAdapter(interfaces=4) per adapter and asks the runner for adapters x 4 interfaces (num-eth). - DockerVM threads port_number through the NIO/capture API (the compute routes parsed it from the URL but dropped it); single-port adapters keep the historical bridge{N} names and command sequence, multi-port adapters get one bridge per port (bridge{a}_{p}). - The unix-socket NIO wiring addresses sockets flat across adapters: adapter x ports-per-adapter + port, so single-port images keep their exact socket layout. - The controller port list for GNS3_IOL_RUNNER docker nodes is generated by StandardPortFactory with the IOU naming (Ethernet{segment0}/{port0}, segment size 4); plain docker nodes keep eth{N}. --- docs/features/iol-runner-docker.md | 27 ++-- gns3server/api/routes/compute/docker_nodes.py | 14 +- gns3server/compute/docker/docker_vm.py | 153 +++++++++++------- gns3server/compute/docker/iol_docker_vm.py | 26 ++- gns3server/compute/docker/vendor_docker_vm.py | 30 ++-- gns3server/controller/node.py | 43 +++-- tests/compute/docker/test_docker_vm.py | 4 +- tests/compute/docker/test_iol_docker_vm.py | 54 ++++++- tests/controller/test_node.py | 26 +++ 9 files changed, 267 insertions(+), 110 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 465dd981d..64aeb3a72 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -89,16 +89,10 @@ API docs for the auth flow), or in the Web UI under "image": "iol-xe/iol-xe:17-18-02", "category": "router", "symbol": ":/symbols/router.svg", - "adapters": 4, + "adapters": 2, "console_type": "telnet", "environment": "GNS3_IOL_RUNNER=1", - "extra_volumes": ["/config"], - "custom_adapters": [ - {"adapter_number": 0, "port_name": "Ethernet0/0"}, - {"adapter_number": 1, "port_name": "Ethernet0/1"}, - {"adapter_number": 2, "port_name": "Ethernet0/2"}, - {"adapter_number": 3, "port_name": "Ethernet0/3"} - ] + "extra_volumes": ["/config"] } ``` @@ -106,19 +100,19 @@ API docs for the auth flow), or in the Web UI under |---|---|---| | `environment` | `GNS3_IOL_RUNNER=1` | The switch that selects `IOLDockerVM` (skip-init, unix-socket NIO, auto volumes). Optional: `GNS3_IOL_MEMORY=` (default 2048). | | `extra_volumes` | `["/config"]` | `/tmp/run` is auto-added. **Never add `/tmp`** — it would persist the socket directory into the projects tree and uBridge would reject the too-long AF_UNIX path. | -| `custom_adapters` | one entry per adapter | Names each port after the IOL interface it maps to: adapter `N` → `Ethernet{N/4}/{N%4}` (same mechanism SR Linux uses for `mgmt0`/`e1-1`). Raise the list up to `Ethernet7/3` (32 ports, the IOL maximum) when raising `adapters`; adapters beyond the list fall back to `eth{N}`. | +| `adapters` | number of 4-port units | The IOU convention: one adapter = `Ethernet0/0`–`Ethernet0/3`, two adapters add `Ethernet1/0`–`1/3`, … (8 units / 32 ports max). Ports are addressed as (adapter, port 0–3) and shown grouped in the UI. | | `memory` | optional; `0` (default) = no cap | Unset works — Docker applies no limit. When you do set a cap, keep it at IOL memory + ~512 MB, or the cgroup OOM-killer shoots the router. | | `console_type` | `telnet` | The runner muxes the IOS console onto PID 1 stdio; `docker_exec` is not needed. | -| `adapters` | multiple of 4 | IOL port granularity (one 4-port unit per range). | ### Verify -1. The node's port list shows `Ethernet0/0`… (from the template's - `custom_adapters`). +1. The node's port list shows the grouped IOL interfaces + (`Ethernet0/0`–`Ethernet1/3` for two adapters), addressed + (adapter, port). 2. Drop a node into a project and start it — the console shows the `Linux Unix (i686)` banner within seconds. 3. `$XDG_RUNTIME_DIR/gns3/unixio//` contains `s00.sock`… (one - pair per adapter). + pair per port). 4. The startup-config lives at `project-files/docker//tmp/run/config` (interface names `Ethernet0/0`, not `GigabitEthernet0/0`). @@ -147,9 +141,10 @@ diagnosing wiring issues). * **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0` (4 ports per unit, matching the adapter-count granularity) — startup configs addressing `GigabitEthernet…` are rejected by the parser. -* **Adapters**: change the adapter count while the node is stopped; the - config is regenerated on the next start and the runner creates the - matching socket set (IOL granularity is 4 ports per unit). +* **Adapters**: one adapter is a 4-port unit (the IOU model): change the + count while the node is stopped; the config is regenerated on the next + start (`num-eth` = adapters × 4) and the runner creates the matching + socket set. * **Stop before editing**: NVRAM is only flushed on a graceful stop (SIGTERM, "cleanup done" in `process.log`); a kill loses the running-config changes since the last `write memory`. diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index 63d8bea91..5070cd855 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -284,7 +284,7 @@ async def create_docker_node_nio( """ nio = Docker.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True)) - await node.adapter_add_nio_binding(adapter_number, nio) + await node.adapter_add_nio_binding(adapter_number, nio, port_number) return nio.asdict() @@ -302,13 +302,13 @@ async def update_docker_node_nio( The port number on the Docker node is always 0. """ - nio = node.get_nio(adapter_number) + nio = node.get_nio(adapter_number, port_number) nio.filters.clear() if nio_data.filters: nio.filters = nio_data.filters nio.markers = nio_data.markers or {} nio.suspend = nio_data.suspend - await node.adapter_update_nio_binding(adapter_number, nio) + await node.adapter_update_nio_binding(adapter_number, nio, port_number) return nio.asdict() @@ -327,7 +327,7 @@ async def delete_docker_node_nio( The port number on the Docker node is always 0. """ - await node.adapter_remove_nio_binding(adapter_number) + await node.adapter_remove_nio_binding(adapter_number, port_number) @router.post( @@ -346,7 +346,7 @@ async def start_docker_node_capture( """ pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name) - await node.start_capture(adapter_number, pcap_file_path) + await node.start_capture(adapter_number, pcap_file_path, port_number) return {"pcap_file_path": str(pcap_file_path)} @@ -365,7 +365,7 @@ async def stop_docker_node_capture( The port number on the Docker node is always 0. """ - await node.stop_capture(adapter_number) + await node.stop_capture(adapter_number, port_number) @router.get( @@ -382,7 +382,7 @@ async def stream_pcap_file( The port number on the Docker node is always 0. """ - nio = node.get_nio(adapter_number) + nio = node.get_nio(adapter_number, port_number) stream = Docker.instance().stream_pcap_file(nio, node.project.id) return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 44a89f968..b226a0f39 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -899,19 +899,24 @@ class DockerVM(BaseNode): await self._start_ubridge(require_privileged_access=True) for adapter_number in range(0, self.adapters): - nio = self._ethernet_adapters[adapter_number].get_nio(0) - async with self.manager.ubridge_lock: - try: - await self._add_ubridge_connection(nio, adapter_number) - except UbridgeNamespaceError: - log.error("Container %s failed to start", self.name) - await self.stop() + adapter = self._ethernet_adapters[adapter_number] + # Single-port adapters (the standard case) loop once, keeping + # the historical command sequence; multi-port adapters + # (e.g. IOL's 4-port units) get one bridge per port. + for port_number in range(0, adapter.interfaces): + nio = adapter.get_nio(port_number) + async with self.manager.ubridge_lock: + try: + await self._add_ubridge_connection(nio, adapter_number, port_number) + except UbridgeNamespaceError: + log.error("Container %s failed to start", self.name) + await self.stop() - # The container can crash soon after the start, this means we can not move the interface to the container namespace - logdata = await self._get_log() - for line in logdata.split("\n"): - log.error(line) - raise DockerError(logdata) + # The container can crash soon after the start, this means we can not move the interface to the container namespace + logdata = await self._get_log() + for line in logdata.split("\n"): + log.error(line) + raise DockerError(logdata) await self._start_console_server() @@ -1406,6 +1411,21 @@ class DockerVM(BaseNode): """ return f"eth{adapter_number}" + def _bridge_name(self, adapter_number, port_number=0): + """ + uBridge bridge name for an adapter port. Adapters with a single + port (every standard Docker node) keep the historical + "bridge{adapter}" name; multi-port adapters (e.g. IOL's 4-port + units) get one bridge per port. + + :param adapter_number: adapter number + :param port_number: port number on the adapter + """ + + if port_number: + return f"bridge{adapter_number}_{port_number}" + return f"bridge{adapter_number}" + async def _start_interface_monitor(self): """Monitor administrative state changes for this container's adapters. @@ -1558,12 +1578,14 @@ class DockerVM(BaseNode): if writer: await self._close_interface_monitor_writer(writer) - async def _add_ubridge_connection(self, nio, adapter_number): + async def _add_ubridge_connection(self, nio, adapter_number, port_number=0): """ Creates a connection in uBridge. :param nio: NIO instance or None if it's a dummy interface (if an interface is missing in ubridge you can't see it via ifconfig in the container) :param adapter_number: adapter number + :param port_number: port number on the adapter (standard Docker + adapters have a single port, so this is always 0 on the TAP path) """ try: @@ -1575,6 +1597,13 @@ class DockerVM(BaseNode): ) ) + if port_number and adapter.interfaces == 1: + raise DockerError( + "Port {port_number} doesn't exist on adapter {adapter_number} of Docker container '{name}'".format( + name=self.name, port_number=port_number, adapter_number=adapter_number + ) + ) + for index in range(4096): if f"tap-gns3-e{index}" not in psutil.net_if_addrs(): adapter.host_ifc = f"tap-gns3-e{str(index)}" @@ -1585,12 +1614,12 @@ class DockerVM(BaseNode): name=self.name, adapter_number=adapter_number ) ) - bridge_name = f"bridge{adapter_number}" + bridge_name = self._bridge_name(adapter_number, port_number) await self._ubridge_send(f"bridge create {bridge_name}") self._bridges.add(bridge_name) await self._ubridge_send( - "bridge add_nio_tap bridge{adapter_number} {hostif} off".format( - adapter_number=adapter_number, hostif=adapter.host_ifc + "bridge add_nio_tap {bridge_name} {hostif} off".format( + bridge_name=bridge_name, hostif=adapter.host_ifc ) ) @@ -1618,23 +1647,24 @@ class DockerVM(BaseNode): log.debug(f"Created adapter {adapter_number} with MAC address {mac_address} in namespace {self._namespace}") if nio: - await self._connect_nio(adapter_number, nio) - await self._set_adapter_carrier(adapter_number, not nio.suspend) + await self._connect_nio(adapter_number, nio, port_number) + await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number) async def _get_namespace(self): result = await self.manager.query("GET", f"containers/{self._cid}/json") return int(result["State"]["Pid"]) - async def _set_adapter_carrier(self, adapter_number, connected): + async def _set_adapter_carrier(self, adapter_number, connected, port_number=0): """Replicate a Docker adapter's connection state on its TAP device.""" + bridge_name = self._bridge_name(adapter_number, port_number) state = "on" if connected else "off" - await self._ubridge_send(f"bridge set_nio_tap_carrier bridge{adapter_number} {state}") + await self._ubridge_send(f"bridge set_nio_tap_carrier {bridge_name} {state}") - async def _connect_nio(self, adapter_number, nio): + async def _connect_nio(self, adapter_number, nio, port_number=0): - bridge_name = f"bridge{adapter_number}" + bridge_name = self._bridge_name(adapter_number, port_number) await self._ubridge_send( "bridge add_nio_udp {bridge_name} {lport} {rhost} {rport}".format( bridge_name=bridge_name, lport=nio.lport, rhost=nio.rhost, rport=nio.rport @@ -1650,12 +1680,13 @@ class DockerVM(BaseNode): await self._ubridge_apply_filters(bridge_name, nio.filters) await self._ubridge_apply_markers(bridge_name, nio) - async def adapter_add_nio_binding(self, adapter_number, nio): + async def adapter_add_nio_binding(self, adapter_number, nio, port_number=0): """ Adds an adapter NIO binding. :param adapter_number: adapter number - :param nio: NIO instance to add to the slot/port + :param nio: NIO instance to add to the adapter/port + :param port_number: port number on the adapter (0 for single-port adapters) """ try: @@ -1667,38 +1698,47 @@ class DockerVM(BaseNode): ) ) - if self.status == "started" and self.ubridge: - await self._connect_nio(adapter_number, nio) - await self._set_adapter_carrier(adapter_number, not nio.suspend) + if not adapter.port_exists(port_number): + raise DockerError( + "Port {port_number} doesn't exist on adapter {adapter_number} of Docker container '{name}'".format( + name=self.name, port_number=port_number, adapter_number=adapter_number + ) + ) - adapter.add_nio(0, nio) + if self.status == "started" and self.ubridge: + await self._connect_nio(adapter_number, nio, port_number) + await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number) + + adapter.add_nio(port_number, nio) log.debug( "Docker container '{name}' [{id}]: {nio} added to adapter {adapter_number}".format( name=self.name, id=self._id, nio=nio, adapter_number=adapter_number ) ) - async def adapter_update_nio_binding(self, adapter_number, nio): + async def adapter_update_nio_binding(self, adapter_number, nio, port_number=0): """ Update an adapter NIO binding. :param adapter_number: adapter number :param nio: NIO instance to update the adapter + :param port_number: port number on the adapter (0 for single-port adapters) """ if self.ubridge: - bridge_name = f"bridge{adapter_number}" + bridge_name = self._bridge_name(adapter_number, port_number) if bridge_name in self._bridges: await self._ubridge_apply_filters(bridge_name, nio.filters) await self._ubridge_apply_markers(bridge_name, nio) if self.status == "started": - await self._set_adapter_carrier(adapter_number, not nio.suspend) + await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number) - async def adapter_remove_nio_binding(self, adapter_number): + async def adapter_remove_nio_binding(self, adapter_number, port_number=0): """ Removes an adapter NIO binding. :param adapter_number: adapter number + :param port_number: port number on the adapter (0 for single-port adapters) :returns: NIO instance """ @@ -1712,20 +1752,20 @@ class DockerVM(BaseNode): ) ) - await self.stop_capture(adapter_number) + await self.stop_capture(adapter_number, port_number) if self.ubridge: - nio = adapter.get_nio(0) - bridge_name = f"bridge{adapter_number}" + nio = adapter.get_nio(port_number) + bridge_name = self._bridge_name(adapter_number, port_number) if self.status == "started": - await self._set_adapter_carrier(adapter_number, False) + await self._set_adapter_carrier(adapter_number, False, port_number) await self._ubridge_send(f"bridge stop {bridge_name}") await self._ubridge_send( - "bridge remove_nio_udp bridge{adapter} {lport} {rhost} {rport}".format( - adapter=adapter_number, lport=nio.lport, rhost=nio.rhost, rport=nio.rport + "bridge remove_nio_udp {bridge_name} {lport} {rhost} {rport}".format( + bridge_name=bridge_name, lport=nio.lport, rhost=nio.rhost, rport=nio.rport ) ) - adapter.remove_nio(0) + adapter.remove_nio(port_number) log.debug( "Docker VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format( @@ -1733,11 +1773,12 @@ class DockerVM(BaseNode): ) ) - def get_nio(self, adapter_number): + def get_nio(self, adapter_number, port_number=0): """ Gets an adapter NIO binding. :param adapter_number: adapter number + :param port_number: port number on the adapter (0 for single-port adapters) :returns: NIO instance """ @@ -1751,10 +1792,10 @@ class DockerVM(BaseNode): ) ) - nio = adapter.get_nio(0) + nio = adapter.get_nio(port_number) if not nio: - raise DockerError(f"Adapter {adapter_number} is not connected") + raise DockerError(f"Adapter {adapter_number} port {port_number} is not connected") return nio @@ -1800,46 +1841,49 @@ class DockerVM(BaseNode): await self.manager.pull_image(image, progress_callback=callback) - async def _start_ubridge_capture(self, adapter_number, output_file): + async def _start_ubridge_capture(self, adapter_number, output_file, port_number=0): """ Starts a packet capture in uBridge. :param adapter_number: adapter number :param output_file: PCAP destination file for the capture + :param port_number: port number on the adapter (0 for single-port adapters) """ - adapter = f"bridge{adapter_number}" + bridge_name = self._bridge_name(adapter_number, port_number) if not self.ubridge: raise DockerError("Cannot start the packet capture: uBridge is not running") - await self._ubridge_send(f'bridge start_capture {adapter} "{output_file}"') + await self._ubridge_send(f'bridge start_capture {bridge_name} "{output_file}"') - async def _stop_ubridge_capture(self, adapter_number): + async def _stop_ubridge_capture(self, adapter_number, port_number=0): """ Stops a packet capture in uBridge. :param adapter_number: adapter number + :param port_number: port number on the adapter (0 for single-port adapters) """ - adapter = f"bridge{adapter_number}" + bridge_name = self._bridge_name(adapter_number, port_number) if not self.ubridge: raise DockerError("Cannot stop the packet capture: uBridge is not running") - await self._ubridge_send(f"bridge stop_capture {adapter}") + await self._ubridge_send(f"bridge stop_capture {bridge_name}") - async def start_capture(self, adapter_number, output_file): + async def start_capture(self, adapter_number, output_file, port_number=0): """ Starts a packet capture. :param adapter_number: adapter number :param output_file: PCAP destination file for the capture + :param port_number: port number on the adapter (0 for single-port adapters) """ - nio = self.get_nio(adapter_number) + nio = self.get_nio(adapter_number, port_number) if nio.capturing: raise DockerError(f"Packet capture is already activated on adapter {adapter_number}") nio.start_packet_capture(output_file) if self.status == "started" and self.ubridge: - await self._start_ubridge_capture(adapter_number, output_file) + await self._start_ubridge_capture(adapter_number, output_file, port_number) log.debug( "Docker VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format( @@ -1847,19 +1891,20 @@ class DockerVM(BaseNode): ) ) - async def stop_capture(self, adapter_number): + async def stop_capture(self, adapter_number, port_number=0): """ Stops a packet capture. :param adapter_number: adapter number + :param port_number: port number on the adapter (0 for single-port adapters) """ - nio = self.get_nio(adapter_number) + nio = self.get_nio(adapter_number, port_number) if not nio.capturing: return nio.stop_packet_capture() if self.status == "started" and self.ubridge: - await self._stop_ubridge_capture(adapter_number) + await self._stop_ubridge_capture(adapter_number, port_number) log.debug( "Docker VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format( diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index 6243f8a94..df53f2a78 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -43,7 +43,9 @@ import logging import os import shutil +from gns3server.compute.adapters.ethernet_adapter import EthernetAdapter from gns3server.compute.docker.docker_error import DockerHttp404Error +from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM log = logging.getLogger(__name__) @@ -90,6 +92,28 @@ class IOLDockerVM(VendorDockerVM): except ValueError: pass + @DockerVM.adapters.setter + def adapters(self, adapters): + """ + Override: one IOL adapter is a 4-port unit — the IOU model. The + template's adapter count is the number of units (2 adapters = + Ethernet0/0-3 + Ethernet1/0-3); the generated config asks the runner + for adapters × 4 interfaces and links address ports as + (adapter_number, port_number 0-3). + """ + + if len(self._ethernet_adapters) == adapters: + return + + self._ethernet_adapters.clear() + for _ in range(0, adapters): + self._ethernet_adapters.append(EthernetAdapter(interfaces=4)) + + log.debug( + "IOL container '%s': number of 4-port Ethernet adapters set to %d", + self._name, adapters, + ) + def _persistent_volume_list(self, image_info, include_network_config=True): """ Override: the runner requires ``/config`` (its config file, generated @@ -168,7 +192,7 @@ class IOLDockerVM(VendorDockerVM): config = { "binary": "/binary.iol", "memory": self._iol_memory, - "num-eth": self.adapters, + "num-eth": self.adapters * 4, # every adapter is a 4-port unit "num-serial": 0, # GNS3 docker adapters are ethernet-only "local-app": 1, "remote-app": 2, diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index 6e788a83d..d6157eea4 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -377,14 +377,17 @@ class VendorDockerVM(DockerVM): await super().delete() self._remove_unix_socket_host_dir() - async def _add_ubridge_connection(self, nio, adapter_number): + async def _add_ubridge_connection(self, nio, adapter_number, port_number=0): """ - Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter through the - image's AF_UNIX datagram socket pair (raw Ethernet frames) instead of - a TAP interface moved into the container's network namespace. + Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter port through + the image's AF_UNIX datagram socket pair (raw Ethernet frames) + instead of a TAP interface moved into the container's network + namespace. - Per adapter N the image's network agent is expected to create, inside - GNS3_UNIX_SOCKET_DIR: + Ports are addressed flat across adapters: interface index = adapter + number × ports-per-adapter + port number (single-port adapters + reduce to the adapter number). Per interface N the image's network + agent is expected to create, inside GNS3_UNIX_SOCKET_DIR: * ``s{N:02d}.sock`` — its receive socket; frames sent there are injected into guest interface N; @@ -398,7 +401,7 @@ class VendorDockerVM(DockerVM): """ if not self._unix_socket_nio: - return await super()._add_ubridge_connection(nio, adapter_number) + return await super()._add_ubridge_connection(nio, adapter_number, port_number) try: adapter = self._ethernet_adapters[adapter_number] @@ -409,14 +412,15 @@ class VendorDockerVM(DockerVM): ) ) - bridge_name = f"bridge{adapter_number}" + interface_number = adapter_number * adapter.interfaces + port_number + bridge_name = self._bridge_name(adapter_number, port_number) try: await self._ubridge_send(f"bridge create {bridge_name}") self._bridges.add(bridge_name) wiring_dir = self._unix_socket_wiring_dir() - local_sock = os.path.join(wiring_dir, f"c{adapter_number:02d}.sock") - remote_sock = os.path.join(wiring_dir, f"s{adapter_number:02d}.sock") + local_sock = os.path.join(wiring_dir, f"c{interface_number:02d}.sock") + remote_sock = os.path.join(wiring_dir, f"s{interface_number:02d}.sock") # A c-socket left over from a previous ubridge run would fail its bind. with contextlib.suppress(OSError): @@ -443,12 +447,12 @@ class VendorDockerVM(DockerVM): raise adapter.host_ifc = local_sock # bookkeeping / removal logging only log.debug( - "Adapter %d of container '%s' wired via unix sockets %s <-> %s", - adapter_number, self._name, local_sock, remote_sock, + "Adapter %d port %d of container '%s' wired via unix sockets %s <-> %s", + adapter_number, port_number, self._name, local_sock, remote_sock, ) if nio: - await self._connect_nio(adapter_number, nio) + await self._connect_nio(adapter_number, nio, port_number) def _cleanup_console_resources(self): """ diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 8ea491b3c..d144cfa47 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -804,22 +804,35 @@ class Node: self._ports = DynamipsPortFactory(self._properties) return elif self._node_type == "docker": - for adapter_number in range(0, self._properties["adapters"]): - custom_adapter_settings = {} - if self.custom_adapters: - for custom_adapter in self.custom_adapters: - if custom_adapter["adapter_number"] == adapter_number: - custom_adapter_settings = custom_adapter - break - port_name = f"eth{adapter_number}" - port_name = custom_adapter_settings.get("port_name", port_name) - mac_address = custom_adapter_settings.get("mac_address") - if not mac_address and "mac_address" in self._properties: - mac_address = int_to_macaddress(macaddress_to_int(self._properties["mac_address"]) + adapter_number) + if "GNS3_IOL_RUNNER=" in (self._properties.get("environment") or ""): + # IOL adapters are 4-port units (the IOU model): ports are + # Ethernet0/0-3, Ethernet1/0-3, … addressed as + # (adapter_number, port_number 0-3). + self._ports = StandardPortFactory( + self._properties, + 4, + self._first_port_name, + "Ethernet{segment0}/{port0}", + 4, + self.custom_adapters, + ) + else: + for adapter_number in range(0, self._properties["adapters"]): + custom_adapter_settings = {} + if self.custom_adapters: + for custom_adapter in self.custom_adapters: + if custom_adapter["adapter_number"] == adapter_number: + custom_adapter_settings = custom_adapter + break + port_name = f"eth{adapter_number}" + port_name = custom_adapter_settings.get("port_name", port_name) + mac_address = custom_adapter_settings.get("mac_address") + if not mac_address and "mac_address" in self._properties: + mac_address = int_to_macaddress(macaddress_to_int(self._properties["mac_address"]) + adapter_number) - port = PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name) - port.mac_address = mac_address - self._ports.append(port) + port = PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name) + port.mac_address = mac_address + self._ports.append(port) elif self._node_type in ("ethernet_switch", "ethernet_hub"): # Basic node we don't want to have adapter number port_number = 0 diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 10336e2b1..71dbc2c39 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -1198,7 +1198,7 @@ async def test_start(vm, manager, free_console_port, tmpdir): await vm.start() mock_query.assert_called_with("POST", "containers/e90e34656842/start") - vm._add_ubridge_connection.assert_called_once_with(nio, 0) + vm._add_ubridge_connection.assert_called_once_with(nio, 0, 0) assert vm._start_ubridge.called assert vm._start_console.called assert vm._start_aux.called @@ -1253,7 +1253,7 @@ async def test_start_namespace_failed(vm, manager, free_console_port): await vm.start() mock_query.assert_any_call("POST", "containers/e90e34656842/start") - mock_add_ubridge_connection.assert_called_once_with(nio, 0) + mock_add_ubridge_connection.assert_called_once_with(nio, 0, 0) assert mock_start_ubridge.called assert vm.status == "stopped" diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 672325012..4f19cd8c8 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -259,7 +259,7 @@ async def test_start_writes_iol_config(compute_project, manager): with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f: config = json.load(f) assert config["binary"] == "/binary.iol" - assert config["num-eth"] == 4 + assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit assert config["num-serial"] == 0 assert config["local-app"] == 1 assert config["remote-app"] == 2 @@ -285,7 +285,7 @@ async def test_start_rewrites_config_on_adapter_change(compute_project, manager) await vm.start() with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f: - assert json.load(f)["num-eth"] == 8 + assert json.load(f)["num-eth"] == 32 # 8 adapters × 4 ports @pytest.mark.asyncio @@ -460,6 +460,56 @@ async def test_add_ubridge_connection_without_nio_still_wires(compute_project, m assert "bridge start" not in flat +# --------------------------------------------------------------------------- +# IOU-style port model — 1 adapter = 4 ethernet ports +# --------------------------------------------------------------------------- + +def test_adapters_are_four_port_units(compute_project, manager): + + vm = _make_vm(compute_project, manager, adapters=2) + assert vm.adapters == 2 + assert len(vm._ethernet_adapters) == 2 + for adapter in vm._ethernet_adapters: + assert adapter.interfaces == 4 + for port_number in range(4): + assert adapter.port_exists(port_number) + assert not adapter.port_exists(4) + + +@pytest.mark.asyncio +async def test_wiring_addresses_ports_within_adapters(compute_project, manager): + + vm = _make_vm(compute_project, manager, adapters=2) + _mock_wiring(vm) + wiring_dir = _wiring_dir(vm) + os.makedirs(wiring_dir, exist_ok=True) + # adapter 1, port 2 -> flat interface index 6 (1 * 4 + 2) + open(os.path.join(wiring_dir, "s06.sock"), "w").close() + + nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) + await vm._add_ubridge_connection(nio, 1, port_number=2) + + flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) + assert 'bridge add_nio_unix bridge1_2 ' in flat + assert f'"{os.path.join(wiring_dir, "c06.sock")}"' in flat + assert f'"{os.path.join(wiring_dir, "s06.sock")}"' in flat + assert "add_nio_udp bridge1_2 4242 127.0.0.1 4343" in flat + assert vm._ethernet_adapters[1].host_ifc == os.path.join(wiring_dir, "c06.sock") + + +@pytest.mark.asyncio +async def test_nio_binding_rejects_port_out_of_range(compute_project, manager): + + vm = _make_vm(compute_project, manager, adapters=2) + nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) + with pytest.raises(DockerError) as excinfo: + await vm.adapter_add_nio_binding(1, nio, port_number=4) + assert "Port 4" in str(excinfo.value) + + with pytest.raises(DockerError): + await vm.adapter_add_nio_binding(9, nio, port_number=0) + + # --------------------------------------------------------------------------- # Generic GNS3_UNIX_SOCKET_NIO knob on plain VendorDockerVM # --------------------------------------------------------------------------- diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index a20405637..9ba4c0ca6 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -46,6 +46,32 @@ def node(compute, project): return node +def test_docker_iol_ports_grouped_in_four_port_units(compute, project): + """ + IOL docker nodes (GNS3_IOL_RUNNER) model adapters as 4-port units: ports + are Ethernet0/0-3, Ethernet1/0-3, … addressed (adapter, port 0-3), like + the native IOU node type. Plain docker nodes keep the flat eth naming. + """ + + node = Node(project, compute, "iol", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"adapters": 2, "environment": "GNS3_IOL_RUNNER=1"}) + ports = node.ports + assert [p.asdict()["name"] for p in ports] == [ + "Ethernet0/0", "Ethernet0/1", "Ethernet0/2", "Ethernet0/3", + "Ethernet1/0", "Ethernet1/1", "Ethernet1/2", "Ethernet1/3", + ] + assert (ports[5].adapter_number, ports[5].port_number) == (1, 1) + assert ports[5].short_name == "e1/1" + + node = Node(project, compute, "web", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"adapters": 2}) + assert [p.asdict()["name"] for p in node.ports] == ["eth0", "eth1"] + + def test_name(compute, project): """ If node use a name template generate names From b17d021bf644bb81438719d8ed19cb253ff80a2c Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 4 Sep 2026 10:53:46 +0800 Subject: [PATCH 19/60] fix: derive per-node IOL app id so linked routers get distinct MACs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IOL derives interface MACs from its application ID. With a constant local-app every node shared the same MACs, and linked routers dropped each other's frames as MAC loops — ARP never resolved and pings were 100% lost even though the whole uBridge datapath was forwarding. Derive local-app from the node UUID (stable across restarts, kept in 1..1022) and align remote-app with the netiomux convention CML uses (1023). Verified end to end: two iol-xe nodes, Ethernet0/0 link, ARP resolves both ways, ping 4/5 (first loss = ARP), graceful stop/start keeps the NVRAM config. --- docs/features/iol-runner-docker.md | 5 ++++- gns3server/compute/docker/iol_docker_vm.py | 14 +++++++++----- tests/compute/docker/test_iol_docker_vm.py | 15 +++++++++++++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 64aeb3a72..0c16a1540 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -137,7 +137,10 @@ diagnosing wiring issues). `GNS3_IOL_MEMORY` (default 2048 MB). Keep container memory at IOL memory + ~512 MB headroom or the OOM-killer will shoot the router. * **MAC addresses**: the `mac_address` template field and per-adapter custom - MACs are ignored — IOL derives its own scheme (`aabb.cc00.0XY0`). + MACs are ignored — IOL derives its own scheme from the node's application + ID (`aabb.cc{app}{iface}`), e.g. `aabb.cc03.0400`. The ID is derived from + the node UUID so linked routers always get distinct MACs (nodes sharing an + ID would silently drop each other's frames as MAC loops). * **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0` (4 ports per unit, matching the adapter-count granularity) — startup configs addressing `GigabitEthernet…` are rejected by the parser. diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index df53f2a78..3dddf87b7 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -29,9 +29,8 @@ netiomux exposes per-interface AF_UNIX datagram sockets in the container's ``/tmp`` (``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames), wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM (uBridge reaches them through a per-node runtime directory bound at /tmp — -see ``VendorDockerVM._unix_socket_host_dir``). Because the netio bus -directory is private to the node, the application IDs are fixed constants -with no cross-node collisions. +see ``VendorDockerVM._unix_socket_host_dir``). The local application ID is +derived from the node ID so that linked nodes get distinct MACs. This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker. """ @@ -194,8 +193,13 @@ class IOLDockerVM(VendorDockerVM): "memory": self._iol_memory, "num-eth": self.adapters * 4, # every adapter is a 4-port unit "num-serial": 0, # GNS3 docker adapters are ethernet-only - "local-app": 1, - "remote-app": 2, + # IOL derives interface MACs from the local application ID + # (aabb.cc00.00); every node needs a distinct one or + # linked routers share MACs and drop each other's frames as loops. + # Derived from the node ID: stable across restarts, unique enough + # (CML allocates per-lab sequential IDs for the same reason). + "local-app": int(self.id.replace("-", ""), 16) % 1022 + 1, + "remote-app": 1023, # netiomux's fake peer application ID "user-id": os.getuid(), "group-id": os.getgid(), } diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 4f19cd8c8..28ea48afa 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -261,14 +261,25 @@ async def test_start_writes_iol_config(compute_project, manager): assert config["binary"] == "/binary.iol" assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit assert config["num-serial"] == 0 - assert config["local-app"] == 1 - assert config["remote-app"] == 2 + assert config["local-app"] == int(vm.id.replace("-", ""), 16) % 1022 + 1 + assert config["remote-app"] == 1023 assert config["memory"] == 2048 assert config["user-id"] == os.getuid() assert config["group-id"] == os.getgid() assert vm.status == "started" +def test_local_app_is_distinct_per_node(compute_project, manager): + # IOL derives interface MACs from the app ID: two nodes sharing one would + # drop each other's frames as MAC loops, so IDs must differ per node. + vm1 = _make_vm(compute_project, manager) + vm2 = _make_vm(compute_project, manager) + id1 = int(vm1.id.replace("-", ""), 16) % 1022 + 1 + id2 = int(vm2.id.replace("-", ""), 16) % 1022 + 1 + assert id1 != id2 + assert 1 <= id1 <= 1022 and 1 <= id2 <= 1022 + + @pytest.mark.asyncio async def test_start_rewrites_config_on_adapter_change(compute_project, manager): From 0119373690f5967230dfe9b2c656a2e9353ef7e3 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 4 Sep 2026 13:05:49 +0800 Subject: [PATCH 20/60] feat: allocate IOL Docker application IDs from a pool disjoint from IOU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IOL interface MACs derive from the application ID (aabb.cc{app}{iface}), so ids must be unique across opened projects sharing computes — the same reason IOU has its allocator. IOL Docker nodes draw from the upper half (512-1022, netiomux's fixed peer is 1023) so the two node types can neither collide nor starve each other; IOU behavior is unchanged. The controller sniffs the same GNS3_IOL_RUNNER environment marker the compute uses to select IOLDockerVM (both the nested-properties and template/top-level-kwarg shapes), stores the id in node properties like IOU does, and passes it through the Docker create payload. Without an allocation the compute falls back to a stable node-derived id in the same upper range. --- docs/features/iol-runner-docker.md | 10 ++++-- gns3server/compute/docker/iol_docker_vm.py | 38 ++++++++++++++++---- gns3server/controller/node.py | 3 +- gns3server/controller/project.py | 38 ++++++++++++++++++-- gns3server/schemas/compute/docker_nodes.py | 4 ++- gns3server/utils/application_id.py | 42 +++++++++++++++++++--- tests/compute/docker/test_iol_docker_vm.py | 25 ++++++++++--- tests/controller/test_project.py | 41 +++++++++++++++++++++ 8 files changed, 177 insertions(+), 24 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 0c16a1540..1b91301b3 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -138,9 +138,13 @@ diagnosing wiring issues). + ~512 MB headroom or the OOM-killer will shoot the router. * **MAC addresses**: the `mac_address` template field and per-adapter custom MACs are ignored — IOL derives its own scheme from the node's application - ID (`aabb.cc{app}{iface}`), e.g. `aabb.cc03.0400`. The ID is derived from - the node UUID so linked routers always get distinct MACs (nodes sharing an - ID would silently drop each other's frames as MAC loops). + ID (`aabb.cc{app}{iface}`), e.g. `aabb.cc03.0400`. The controller + allocates the ID at node creation from the upper half of the id space + (512–1022, disjoint from IOU's 1–511, limit 511 IOL Docker nodes across + opened projects sharing computes); without an allocation (raw compute API, + pre-allocation topologies) a stable node-derived fallback in the same + range is used. Nodes sharing an ID would silently drop each other's + frames as MAC loops. * **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0` (4 ports per unit, matching the adapter-count granularity) — startup configs addressing `GigabitEthernet…` are rejected by the parser. diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index 3dddf87b7..ce4c03e64 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -29,8 +29,10 @@ netiomux exposes per-interface AF_UNIX datagram sockets in the container's ``/tmp`` (``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames), wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM (uBridge reaches them through a per-node runtime directory bound at /tmp — -see ``VendorDockerVM._unix_socket_host_dir``). The local application ID is -derived from the node ID so that linked nodes get distinct MACs. +see ``VendorDockerVM._unix_socket_host_dir``). The controller allocates the +IOL application ID (upper half of the id space, disjoint from IOU's) so that +linked nodes get distinct MACs; without an allocation a stable node-derived +fallback is used. This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker. """ @@ -91,6 +93,27 @@ class IOLDockerVM(VendorDockerVM): except ValueError: pass + # Application ID allocated by the controller (upper half of the id + # space, disjoint from IOU's); None until the create payload carries it. + self._application_id = None + + @property + def application_id(self) -> int: + """ + IOL application ID: drives interface MACs (aabb.cc{app}{iface}) and + the NVRAM file name. Falls back to a stable per-node value in the + IOL Docker half of the id space when no allocation is available + (raw compute API use, topologies created before allocation existed). + """ + + if self._application_id is None: + return 512 + int(self.id.replace("-", ""), 16) % 511 + return self._application_id + + @application_id.setter + def application_id(self, value) -> None: + self._application_id = int(value) + @DockerVM.adapters.setter def adapters(self, adapters): """ @@ -194,11 +217,12 @@ class IOLDockerVM(VendorDockerVM): "num-eth": self.adapters * 4, # every adapter is a 4-port unit "num-serial": 0, # GNS3 docker adapters are ethernet-only # IOL derives interface MACs from the local application ID - # (aabb.cc00.00); every node needs a distinct one or - # linked routers share MACs and drop each other's frames as loops. - # Derived from the node ID: stable across restarts, unique enough - # (CML allocates per-lab sequential IDs for the same reason). - "local-app": int(self.id.replace("-", ""), 16) % 1022 + 1, + # (aabb.cc{app}{iface}); every node needs a distinct one or + # linked routers share MACs and drop each other's frames as + # loops. Allocated by the controller per node (upper half of + # the id space, disjoint from IOU's — CML does the same with + # its per-deployment iol_app_id). + "local-app": self.application_id, "remote-app": 1023, # netiomux's fake peer application ID "user-id": os.getuid(), "group-id": os.getgid(), diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index d144cfa47..e72dfbc9d 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -30,6 +30,7 @@ from .controller_error import ( from .node_types import BUILTIN_NODE_TYPES from .ports.port_factory import PortFactory, StandardPortFactory, DynamipsPortFactory from ..utils.images import images_directories +from ..utils.application_id import is_iol_runner_environment from ..utils import macaddress_to_int, int_to_macaddress from ..config import Config @@ -804,7 +805,7 @@ class Node: self._ports = DynamipsPortFactory(self._properties) return elif self._node_type == "docker": - if "GNS3_IOL_RUNNER=" in (self._properties.get("environment") or ""): + if is_iol_runner_environment(self._properties.get("environment")): # IOL adapters are 4-port units (the IOU model): ports are # Ethernet0/0-3, Ethernet1/0-3, … addressed as # (adapter_number, port_number 0-3). diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 01506a734..d9ebdfae8 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -40,7 +40,7 @@ from .udp_link import UDPLink from .link import _UNSET from ..config import Config from ..utils.path import check_path_allowed, get_default_project_directory -from ..utils.application_id import get_next_application_id +from ..utils.application_id import get_next_application_id, is_iol_runner_environment from ..utils.asyncio.pool import Pool from ..utils.packet_filter_validation import validate_bpf_syntax from ..utils.asyncio import locking @@ -69,6 +69,21 @@ def open_required(func): return wrapper +def _is_iol_docker_kwargs(kwargs) -> bool: + """ + Whether add_node() kwargs describe an iol-runner Docker node. The + environment can arrive nested in a ``properties`` dict or as a top-level + kwarg (the template path spreads it), matching the two shapes IOU + application-id injection handles. + """ + + if "properties" in kwargs.keys(): + environment = (kwargs.get("properties") or {}).get("environment") + else: + environment = kwargs.get("environment") + return is_iol_runner_environment(environment) + + class Project: """ A project inside a controller @@ -159,7 +174,7 @@ class Project: assert self._status != "closed" self.dump() - self._iou_id_lock = asyncio.Lock() + self._application_id_lock = asyncio.Lock() # Serialise the "ensure project exists on this compute" check in # _create_node: without it, concurrent node creations all pass the # `compute not in _project_created_on_compute` check before any has @@ -646,7 +661,7 @@ class Project: self._computes.append(compute.id) if node_type == "iou": - async with self._iou_id_lock: + async with self._application_id_lock: # IOU application IDs must be allocated serially to avoid duplicates. # The lock must also cover _create_node() because get_next_application_id() # checks in-memory nodes (self._nodes), which are only registered @@ -658,6 +673,23 @@ class Project: elif "application_id" not in kwargs.keys() and not kwargs.get("properties"): kwargs["application_id"] = get_next_application_id(self._controller.projects, self._computes) node = await self._create_node(compute, name, node_id, node_type, **kwargs) + elif node_type == "docker" and _is_iol_docker_kwargs(kwargs): + # IOL Docker nodes derive interface MACs from the application ID + # exactly like IOU; they draw from the disjoint upper half of the + # id space so the two node types can neither collide nor starve + # each other. + async with self._application_id_lock: + if "properties" in kwargs.keys(): + properties = kwargs.get("properties") or {} + if "application_id" not in properties: + properties["application_id"] = get_next_application_id( + self._controller.projects, self._computes, iol_docker=True + ) + elif "application_id" not in kwargs.keys(): + kwargs["application_id"] = get_next_application_id( + self._controller.projects, self._computes, iol_docker=True + ) + node = await self._create_node(compute, name, node_id, node_type, **kwargs) else: node = await self._create_node(compute, name, node_id, node_type, **kwargs) self.emit_notification("node.created", node.asdict()) diff --git a/gns3server/schemas/compute/docker_nodes.py b/gns3server/schemas/compute/docker_nodes.py index 7b39921f6..e3523cad8 100644 --- a/gns3server/schemas/compute/docker_nodes.py +++ b/gns3server/schemas/compute/docker_nodes.py @@ -69,7 +69,9 @@ class DockerCreate(DockerBase): Properties to create a Docker node. """ - pass + application_id: Optional[int] = Field( + None, ge=1, le=1022, description="IOL application ID for iol-runner images (allocated by the controller)" + ) class DockerUpdate(DockerBase): diff --git a/gns3server/utils/application_id.py b/gns3server/utils/application_id.py index 9c9d8b347..488fb70b8 100644 --- a/gns3server/utils/application_id.py +++ b/gns3server/utils/application_id.py @@ -13,6 +13,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +# from gns3server.controller.controller_error import ControllerError @@ -20,13 +21,32 @@ import logging log = logging.getLogger(__name__) +# IOU draws from the lower half: uBridge's iol_bridge uses application_id + 512 +# for its netio peer endpoint. IOL Docker nodes (iol-runner images) draw from +# the upper half: their netiomux peer is the fixed id 1023. Both node types +# derive interface MACs from the id (aabb.cc{app}{iface}), so the two pools +# must stay disjoint — a shared id silently blackholes traffic between the +# nodes as a MAC loop. +IOU_APPLICATION_ID_POOL = range(1, 512) +IOL_DOCKER_APPLICATION_ID_POOL = range(512, 1023) -def get_next_application_id(projects, computes): + +def is_iol_runner_environment(environment) -> bool: + """ + Whether a Docker node environment carries the GNS3_IOL_RUNNER marker — + the same signal the compute uses to select the IOLDockerVM class. + """ + + return "GNS3_IOL_RUNNER=" in (environment or "") + + +def get_next_application_id(projects, computes, iol_docker=False): """ Calculates free application_id from given nodes :param projects: all projects managed by controller :param computes: all computes used by the project + :param iol_docker: allocate for an IOL Docker (iol-runner) node instead of IOU :raises HTTPConflict when exceeds number :return: integer first free id """ @@ -38,12 +58,26 @@ def get_next_application_id(projects, computes): if project.status == "opened": nodes.extend(list(project.nodes.values())) - used = {n.properties["application_id"] for n in nodes if n.node_type == "iou" and n.compute.id in computes} - pool = set(range(1, 512)) + if iol_docker: + used = { + n.properties["application_id"] + for n in nodes + if n.node_type == "docker" + and n.compute.id in computes + and "application_id" in n.properties + and is_iol_runner_environment(n.properties.get("environment")) + } + pool = set(IOL_DOCKER_APPLICATION_ID_POOL) + limit = "511 IOL Docker nodes" + else: + used = {n.properties["application_id"] for n in nodes if n.node_type == "iou" and n.compute.id in computes} + pool = set(IOU_APPLICATION_ID_POOL) + limit = "512 nodes" try: application_id = (pool - used).pop() return application_id except KeyError: raise ControllerError( - "Cannot create a new IOU node (limit of 512 nodes across all opened projects using the same computes)" + f"Cannot create a new {'IOL Docker' if iol_docker else 'IOU'} node " + f"(limit of {limit} across all opened projects using the same computes)" ) diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 28ea48afa..b3aa8cbab 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -261,7 +261,7 @@ async def test_start_writes_iol_config(compute_project, manager): assert config["binary"] == "/binary.iol" assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit assert config["num-serial"] == 0 - assert config["local-app"] == int(vm.id.replace("-", ""), 16) % 1022 + 1 + assert config["local-app"] == 512 + int(vm.id.replace("-", ""), 16) % 511 assert config["remote-app"] == 1023 assert config["memory"] == 2048 assert config["user-id"] == os.getuid() @@ -274,10 +274,25 @@ def test_local_app_is_distinct_per_node(compute_project, manager): # drop each other's frames as MAC loops, so IDs must differ per node. vm1 = _make_vm(compute_project, manager) vm2 = _make_vm(compute_project, manager) - id1 = int(vm1.id.replace("-", ""), 16) % 1022 + 1 - id2 = int(vm2.id.replace("-", ""), 16) % 1022 + 1 - assert id1 != id2 - assert 1 <= id1 <= 1022 and 1 <= id2 <= 1022 + assert vm1.application_id != vm2.application_id + for vm in (vm1, vm2): + assert 512 <= vm.application_id <= 1022 + + +@pytest.mark.asyncio +async def test_allocated_application_id_overrides_fallback(compute_project, manager): + # The controller allocates the application ID (upper half of the id + # space); the node-derived hash is only a fallback without one. + vm = _make_vm(compute_project, manager) + vm.application_id = 700 + assert vm.application_id == 700 + _mock_start(vm) + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + await vm.start() + with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f: + config = json.load(f) + assert config["local-app"] == 700 @pytest.mark.asyncio diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 31a4a2e7a..373d05e10 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -296,6 +296,47 @@ async def test_add_node_iou(controller): assert node3.properties["application_id"] == 3 +@pytest.mark.asyncio +async def test_add_node_iol_docker(controller): + """ + IOL Docker nodes (GNS3_IOL_RUNNER marker) get an application ID from the + upper half of the id space, disjoint from IOU's lower half + """ + + compute = MagicMock() + compute.id = "local" + project = await controller.add_project(project_id=str(uuid.uuid4()), name="test1") + project.emit_notification = MagicMock() + + response = MagicMock() + compute.post = AsyncioMagicMock(return_value=response) + + # template shape: environment as a top-level kwarg + node1 = await project.add_node( + compute, "iol1", None, node_type="docker", image="iol-xe/iol-xe:17-18-02", environment="GNS3_IOL_RUNNER=1" + ) + # raw API shape: environment nested in properties + node2 = await project.add_node( + compute, + "iol2", + None, + node_type="docker", + properties={"image": "iol-xe/iol-xe:17-18-02", "environment": "GNS3_IOL_RUNNER=1"}, + ) + # plain docker nodes are left alone + node3 = await project.add_node( + compute, "web", None, node_type="docker", image="nginx", environment="FOO=1", adapters=1 + ) + + assert node1.properties["application_id"] == 512 + assert node2.properties["application_id"] == 513 + assert "application_id" not in node3.properties + + # IOU keeps its own pool: a subsequent IOU node still gets the lower half + node4 = await project.add_node(compute, "iou1", None, node_type="iou") + assert node4.properties["application_id"] == 1 + + @pytest.mark.asyncio async def test_add_node_iou_with_multiple_projects(controller): """ From 51b3c81859f351294067383551e241af66d05d7c Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 4 Sep 2026 14:07:35 +0800 Subject: [PATCH 21/60] fix: make the patched get_default_project_directory order-safe in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A product module first-imported while run_around_tests' autouse monkeypatch of gns3server.utils.path.get_default_project_directory is active (e.g. 'from gns3server.api.server import app' inside a test body) binds the patched lambda into its own namespace forever: module-level from-imports capture the object by value. Once the patch is reverted, that module keeps calling the stale lambda, which closed over the very test's tmppath — a directory deleted at that test's teardown. Every later test hitting psutil.disk_usage(get_default_project_directory()) then fails with FileNotFoundError, but only when the importing test runs before the API test files (full-suite collection order hides it). The replacement now resolves Config.instance().settings at call time instead of closing over the tmppath, so a frozen reference stays correct; the test that triggered this imports gns3_app at module top so no product import ever happens inside a patched window. Also pins pytest-random-order (inert without --random-order) and documents the order-independence rules for new tests in the gns3-api-test-writing skill, including the known-red legacy files whose tests share rows sequentially. --- .claude/skills/gns3-api-test-writing/SKILL.md | 15 +++++++++++++++ dev-requirements.txt | 3 ++- tests/conftest.py | 8 +++++++- tests/controller/test_compute.py | 5 ++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.claude/skills/gns3-api-test-writing/SKILL.md b/.claude/skills/gns3-api-test-writing/SKILL.md index 6149bf21f..991df03f2 100644 --- a/.claude/skills/gns3-api-test-writing/SKILL.md +++ b/.claude/skills/gns3-api-test-writing/SKILL.md @@ -56,3 +56,18 @@ Always pass `secret_key=DEFAULT_JWT_SECRET_KEY`: the autouse `run_around_tests` ## Failure-Diagnosis Heuristic **Passes in isolation, fails in a class run → suspect shared fixture state first** (`base_client.headers`, the `Config` singleton, class-scoped DB rows) — never the product code. Reproduce with `-k "test_a or test_b"` pairs to find the polluting test. Do not add debug prints to product code to chase test-order issues; make the test order-independent with explicit per-request headers instead. + +## Order Independence + +The suite runs in collection order by default, and a full run is green — but that hides order dependencies. Two real incident classes so far: + +1. **The frozen from-import** (fixed 2026-09): `run_around_tests` monkeypatches `gns3server.utils.path.get_default_project_directory` with a lambda. A product module first-imported *while that patch is active* (e.g. `from gns3server.api.server import app` written inside a test body) freezes the patched lambda into its namespace forever — later tests then get a deleted tmpdir path (`FileNotFoundError` from `psutil.disk_usage`). The patched lambda now resolves `Config.instance()` at call time, so freezing is benign — keep it that way. +2. **Sequential-scenario DB tests**: `tests/api/routes/controller/test_users.py`, `test_roles.py`, `test_pools.py`, `test_templates.py`, `test_images.py`, `test_groups.py`, `test_appliances.py`, `test_acl.py` and `tests/controller/test_rbac.py` build shared rows across tests within a file (a test asserts on users/roles created by earlier tests). They are known-red under any reordering — do not copy this pattern into new files. + +### Rules for new tests + +- **Import product modules at test-module top level**, never first-import inside a test body (an autouse fixture's monkeypatches are live there, and module import executes product `from`-imports). +- **Autouse patch replacements must resolve state at call time** (`Config.instance().settings...`), never close over test-local values (tmppaths, fixture objects) — a closed-over value survives into other tests if the replacement object gets frozen anywhere. +- **Verify a new test file is order-independent**: `venv/bin/python -m pytest tests/.py --random-order --random-order-seed=1 -q` (and a second seed). It must pass shuffled. `pytest-random-order` is pinned in `dev-requirements.txt`; it is inert unless `--random-order` is passed. +- A test that needs specific rows creates them itself (or via a fixture) — never relies on rows another test in the file created. +- Diagnosing a suspected order bug: rerun the exact failing pair with the seed printed by `--random-order` (`--random-order-seed=` reproduces it), then bisect to the polluting test. diff --git a/dev-requirements.txt b/dev-requirements.txt index 55aac6eaa..80d7da3b4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,4 +3,5 @@ flake8==7.3.0 pytest-timeout==2.4.0 pytest-asyncio==1.4.0 httpx==0.28.1 -httpx_ws==0.7.2 # upgrading leads to failures in tests \ No newline at end of file +httpx_ws==0.7.2 # upgrading leads to failures in tests +pytest-random-order==1.2.0 # opt-in: --random-order --random-order-bucket=global (legacy suites are not shuffle-clean yet, see .claude/skills/gns3-api-test-writing) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index f67b04839..b0994ebb8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -418,7 +418,13 @@ def run_around_tests(monkeypatch, config, port_manager): # avoid monitoring for new images while testing config.settings.Server.auto_discover_images = False - monkeypatch.setattr("gns3server.utils.path.get_default_project_directory", lambda *args: os.path.join(tmppath, 'projects')) + # Resolve the projects directory from the Config singleton at call time instead of + # closing over this test's tmppath: a from-import executed anywhere while this + # patch is active (e.g. the first import of gns3server.api.server from inside a + # test body) freezes the patched object into the importing module's namespace + # forever, and a closed-over path would then point at a deleted directory in + # every later test (order-dependent FileNotFoundError in psutil.disk_usage). + monkeypatch.setattr("gns3server.utils.path.get_default_project_directory", lambda *args: Config.instance().settings.Server.projects_path) # Force sys.platform to the original value. Because it seems not be restored correctly after each test sys.platform = sys.original_platform diff --git a/tests/controller/test_compute.py b/tests/controller/test_compute.py index 58ba606c4..671b8e7a0 100644 --- a/tests/controller/test_compute.py +++ b/tests/controller/test_compute.py @@ -25,6 +25,7 @@ from unittest.mock import patch, MagicMock from gns3server.controller.project import Project from gns3server.controller.compute import Compute +from gns3server.api.server import app as gns3_app from gns3server.controller.controller_error import ( ControllerError, ControllerNotFoundError, @@ -581,8 +582,10 @@ async def test_connect_notification_poison_frame_autoreconnects(compute, monkeyp compute._http_session = session # allow the reconnection to be scheduled during the test + # (import gns3_app at module top: a first import from inside a test body + # would execute module-level from-imports while the autouse fixture's + # monkeypatches are active, freezing patched objects into namespaces) monkeypatch.delattr(sys, "_called_from_test", raising=False) - from gns3server.api.server import app as gns3_app monkeypatch.setattr(gns3_app.state, "exiting", False) async def fake_connect(): From 5c285f631bf5eac9ee74721dad067668e1593625 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 1 Sep 2026 01:34:19 +0800 Subject: [PATCH 22/60] fix: normalize marker.match event tag to int to match the REST schema The WS event carried the tag as str (verbatim from the MARK signal) when present and as int when falling back to the registry, so one tag reached consumers as two different values. Parse the signal's decimal (the exact value we installed via 'mark tag ') and keep the registered int on malformed input; None only when neither side carries one. --- gns3server/compute/marker/marker_listener.py | 15 ++++++++++-- tests/compute/marker/test_marker_manager.py | 24 +++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/gns3server/compute/marker/marker_listener.py b/gns3server/compute/marker/marker_listener.py index 40cdd97b6..a30769c1e 100644 --- a/gns3server/compute/marker/marker_listener.py +++ b/gns3server/compute/marker/marker_listener.py @@ -114,13 +114,24 @@ class MarkerListener(asyncio.DatagramProtocol): # signals that carry no `link=`. signal_link = link if link and link != "-" else None + # Normalize the tag to int so the event matches the REST schema + # (MarkerCreate.tag is Optional[int]): the signal merely echoes the + # decimal we installed via `mark tag `, so parsing cannot + # fail for well-formed signals; a malformed value keeps the registered + # int, and the tag is None only when neither side carries one. + event_tag = registered_tag + if tag and tag != "-": + try: + event_tag = int(tag) + except ValueError: + pass # malformed signal tag: keep the registered value + event = { "project_id": project_id, "node_id": node_id, "link_id": signal_link or link_id, "filter": filter_name, - # Prefer the value carried in the signal; fall back to the one we registered. - "tag": tag if tag and tag != "-" else registered_tag, + "tag": event_tag, "ts": ts, "len": int(length) if length and length.isdigit() else 0, # Travel direction relative to the capture node (node_id above); diff --git a/tests/compute/marker/test_marker_manager.py b/tests/compute/marker/test_marker_manager.py index da495ae92..5917870d7 100644 --- a/tests/compute/marker/test_marker_manager.py +++ b/tests/compute/marker/test_marker_manager.py @@ -121,7 +121,11 @@ class TestMarkerListener: assert ev["node_id"] == "n1" assert ev["link_id"] == "l1" assert ev["filter"] == "f1" - assert ev["tag"] == "7" + # The event tag is normalized to int to match the REST schema — the + # signal echoes the decimal we installed, so str and int variants of + # the same tag must never reach consumers as different keys. + assert ev["tag"] == 7 + assert isinstance(ev["tag"], int) assert ev["ts"] == pytest.approx(1700000000.123456) assert ev["len"] == 98 # No dir= in the signal (legacy uBridge) → undirected. @@ -163,6 +167,24 @@ class TestMarkerListener: lis.datagram_received(b"MARK 2.0 node=n filter=f tag=- len=20\n", None) assert fmgr.events[0][1]["tag"] == 42 + def test_non_numeric_signal_tag_falls_back_to_registered(self): + # A corrupt/unknown signal value must not leak a str tag into the + # event stream — the registry's int wins. + fmgr = FakeMarkerManager() + fmgr.register("p", "n", "f", "l", tag=42) + lis = MarkerListener(fmgr) + lis.connection_made(None) + lis.datagram_received(b"MARK 2.0 node=n filter=f tag=oops len=20\n", None) + assert fmgr.events[0][1]["tag"] == 42 + + def test_no_tag_anywhere_is_none(self): + fmgr = FakeMarkerManager() + fmgr.register("p", "n", "f", "l", tag=None) + lis = MarkerListener(fmgr) + lis.connection_made(None) + lis.datagram_received(b"MARK 2.0 node=n filter=f tag=- len=20\n", None) + assert fmgr.events[0][1]["tag"] is None + def test_link_in_signal_overrides_registry_link(self): # Per-link attribution (contract §3.2/§3.3): the signal's `link=` is # authoritative and must disambiguate links sharing a node+filter — From a8fa529252a74bd9e885087a4be7214a1eb69d5e Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 1 Sep 2026 01:34:33 +0800 Subject: [PATCH 23/60] feat: add tag-keyed aggregate replay over paused markers' pcaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markers on different links sharing a tag form one distributed capture session. Once every marker under the tag is paused (409 otherwise), three read-only endpoints replay it: - GET .../markers/tags/{tag}/replay/range merges the per-marker pcaps by scanning 16-byte record headers only (no tshark) into a timestamp- ordered frame list (per-second buckets above a 5000-frame cap) - GET .../replay/frames?ts=&window_ms= returns frames in [T, T+window]; an empty window is a normal empty array - GET .../replay/frame/detail lazily decodes one frame the user opened: raw bytes for the hex view read straight from the pcap, protocol tree from 'tshark -T pdml' mapped isomorphically to JSON (every attribute survives, values stay strings). tshark reads a /tmp scratch copy with a scratch HOME — hardened profiles deny it the project directory. Sort key is (ts, source file, frame number): ts is not unique across a merge. The ts parameter round-trips as the exact string from the frame list. Round-trip tests pin the PDML→JSON fidelity (element count and attribute coverage). --- docs/README.md | 5 +- docs/features/marker-tag-replay.md | 247 ++++++++++ gns3server/api/routes/controller/projects.py | 83 ++++ gns3server/controller/marker_replay.py | 428 ++++++++++++++++++ gns3server/controller/project.py | 11 + .../routes/controller/test_marker_replay.py | 190 ++++++++ tests/controller/test_marker_replay.py | 346 ++++++++++++++ 7 files changed, 1309 insertions(+), 1 deletion(-) create mode 100644 docs/features/marker-tag-replay.md create mode 100644 gns3server/controller/marker_replay.py create mode 100644 tests/api/routes/controller/test_marker_replay.py create mode 100644 tests/controller/test_marker_replay.py diff --git a/docs/README.md b/docs/README.md index 50c432faf..55b7a9155 100644 --- a/docs/README.md +++ b/docs/README.md @@ -75,6 +75,9 @@ Web-based packet capture analysis using Docker + xpra HTML5 client. Zero-install ### Marker (Traffic Insight) (`features/marker-traffic-insight.md`) Real-time traffic insight via per-link BPF markers and project-level inherited definitions. A marker taps a link in uBridge, emitting match notifications and pcap capture on BPF hit; definitions fan out to every capable link automatically. +### Marker Tag Replay (`features/marker-tag-replay.md`) +Aggregate playback across links keyed by `tag`: once every marker under a tag is paused, their pcaps merge into one timestamp-ordered timeline; frames are decoded on demand via tshark into an isomorphic JSON protocol tree. The cross-link delta of the same packet measures the intermediate node's forwarding latency. + ### Docker exec Console (Vendor NOS) (`features/docker-exec-console.md`) Console for vendor NOS containers (SR Linux, XRd, …) whose CLI is a TUI off PID 1: runs the vendor CLI via the Docker exec API, plus `GNS3_SKIP_INIT`/`GNS3_INTERFACE_NAMES` boot knobs and SKIP_INIT volume persistence. @@ -130,4 +133,4 @@ Quick-start guide for Ubuntu 24.04: install via PPA, set up dependencies, and ru --- -_Last updated: 2026-08-14_ +_Last updated: 2026-09-01_ diff --git a/docs/features/marker-tag-replay.md b/docs/features/marker-tag-replay.md new file mode 100644 index 000000000..d8439fed0 --- /dev/null +++ b/docs/features/marker-tag-replay.md @@ -0,0 +1,247 @@ + + +> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt. + +# Marker Tag Replay (Aggregate Playback) + +## Overview + +Replays traffic captured by [markers](marker-traffic-insight.md) **across links**, keyed by +`tag`. Markers on different links that share a tag form one *distributed capture session*; +once every marker under the tag is paused, their per-marker pcaps are merged into a single +timestamp-ordered timeline. The Web UI browses that timeline and fetches individual frames +on demand — each fetch decodes exactly one frame via `tshark` into a self-describing JSON +protocol tree. + +The unique observable: the delta between the same packet hitting two consecutive links +measures the **intermediate node's forwarding latency** (host view) — something a +single-link capture can never show. + +## Architecture + +```mermaid +graph TB + UI["Web UI"] + + subgraph Controller["Controller (replay endpoints)"] + GATE["Tag gate
(all markers under tag paused?)"] + SCAN["Timeline scan
(pcap record headers)"] + MAP["PDML → JSON
isomorphic mapper"] + end + + FS[("markers dir
{node}_{link}_{marker}.pcap")] + TS["tshark -T pdml
(one frame at a time)"] + TMP["/tmp scratch copy
(hardened-profile workaround)"] + + UI -->|"GET range / frames"| GATE + GATE --> SCAN + SCAN --> FS + UI -->|"GET frame detail (lazy)"| MAP + MAP --> TMP + TMP --> TS + MAP -->|"hex: raw bytes"| FS +``` + +Two deliberately separated performance regimes: + +| Path | Work | tshark | +|------|------|--------| +| Timeline | 16-byte record-header scan per frame, cross-file merge sort | **never invoked** — browsing works without tshark | +| Frame detail | locate frame → `tshark -T pdml -Y "frame.number == N"` → map to JSON | forked once per frame the user opens | + +tshark call count equals user clicks — no caching or rate limiting needed, and a tshark +failure (501/502) affects only that one frame, never timeline browsing. pcap files are +compute-side (`/project-files/markers/`); the initial scope is single-server +deployments (controller and compute in one process, direct file access) — remote computes +will reuse the existing capture-file proxy pattern. + +## Business Process + +```mermaid +sequenceDiagram + participant UI as Web UI + participant C as Controller + participant PC as markers dir (pcaps) + participant TS as tshark + + Note over UI,PC: ① configure — same tag on every link's marker + UI->>C: POST markers (bpf, tag=666) on each link + + Note over PC: ② capture — uBridge appends matches (flushed per packet), replay forbidden + UI->>C: GET range + C--xC: 409 (a marker under tag 666 is still enabled) + + Note over UI: ③ pause — every marker under the tag + UI->>C: PUT markers {"enabled": false} × each + + Note over UI,TS: ④ replay + UI->>C: GET /markers/tags/666/replay/range + C->>PC: scan record headers, merge sort + C-->>UI: {start, end, sources, frames} + UI->>C: GET frames?ts=T&window_ms=W + C-->>UI: frames in [T, T+W] — or {"frames": []} + UI->>C: GET frame/detail?ts=…&node_id=…&link_id=…&marker=… + C->>PC: locate frame, read raw bytes (hex) + C->>TS: -T pdml (reads the /tmp copy) + TS-->>C: PDML + C-->>UI: protocol tree + hex +``` + +## The tag gate + +Replay reads append-only pcaps, so it is only available while the data is at rest. Every +replay endpoint evaluates the same gate: walk every marker in the project carrying the +requested tag; if any has `enabled: true` → 409 (the response names them); a tag with no +markers at all → 404. + +| Marker state under the tag | pcap file | Replay | +|---------------------------|-----------|--------| +| any `enabled: true` (capturing) | growing | denied — 409 | +| all `enabled: false` (paused) | retained, frozen | **allowed** | +| deleted | file unlinked | no data | +| `bpf`/`tag`/`direction` changed (rebuild) | pcap reopened (truncated) — new session | prior history gone | + +- **Pause, not delete.** Deleting a marker (or its definition) deletes its pcap — replay + before deleting or the data is gone. +- **Pause → resume → pause is fine.** The pcap accumulates the full history; replay covers + everything up to the current pause point. +- uBridge flushes every matched packet to the pcap immediately (`pcap_dump_flush` per + packet under a mutex — verified in the uBridge source), so a pause boundary never loses + tail frames. + +## API Endpoints + +All read-only; JWT bearer token, privilege `Project.Audit`. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/range` | Timeline metadata + full frame list for the tag | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frames?ts=&window_ms=&limit=` | Frames with ts in `[T, T+window]`, merged across sources | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frame/detail?ts=&node_id=&link_id=&marker=` | Single frame: tshark protocol tree + raw hex | + +### `range` — the timeline + +```json +{ + "tag": 666, + "start": "1788196663.226372", + "end": "1788196713.706634", + "frame_count": 20, + "truncated": false, + "sources": [ + { "node_id": "b764c434…", "link_id": "316ef8fd…", "marker": "global-def-…", + "data_link_type": "DLT_EN10MB", "count": 10 } + ], + "frames": [ + { "ts": "1788196663.226372", "len": 98, "node_id": "b764c434…", + "link_id": "316ef8fd…", "marker": "global-def-…", "frame_number": 1 } + ] +} +``` + +- `frames` is the **full, merged, time-ordered list** (cap 5000) — the Web UI lays out the + whole timeline from one request. Over the cap, `frames` is omitted and per-second + `buckets` are returned instead with `truncated: true`. +- Each frame entry carries `(node_id, link_id, marker, frame_number)` — the locating + tuple for the detail request. + +### `frames` — point / window query + +A time with no frames is a normal, successful answer — an empty array, no sentinel strings: + +```json +GET …/replay/frames?ts=1788196700.000&window_ms=500 +→ { "frames": [] } +``` + +### `frame/detail` — lazy single-frame decode + +Invoked only when the user opens a frame. The `ts` must be the **exact string received in +the timeline/frame list** (round-tripped verbatim — never re-serialized through a float); +`node_id + link_id + marker` identify the pcap. The server re-resolves the ts against the +file, guarding against a capture rebuilt between the timeline view and this click. + +```json +{ + "ts": "1788196663.226372", + "source": { "node_id": "b764c434…", "link_id": "316ef8fd…", + "marker": "global-def-…", "frame_number": 1 }, + "tshark_version": "TShark (Wireshark) 4.6.7 …", + "field_count": 85, + "hex": "45000062…", + "tree": [ + { "element": "proto", "name": "ip", + "showname": "Internet Protocol Version 4, Src: 10.1.10.101, Dst: 203.0.113.1", + "children": [ + { "element": "field", "name": "ip.ttl", "show": "64", + "showname": "Time to Live: 64", "value": "40", "size": "1", + "pos": "22", "children": [] } + ] } + ] +} +``` + +- `tree` mirrors PDML **isomorphically**: every ``/`` becomes a node, every + XML attribute (`name`, `show`, `showname`, `value`, `size`, `pos`, `hide`, `mask`, + `unmaskedvalue`) becomes a JSON key, plus one structural key `element` (proto/field). + Nothing is selected out, nothing interpreted, and **all values stay strings** — numeric + conversion is the client's business. +- `hex` is the raw frame bytes read straight from the pcap (not via tshark); keeping + `pos`/`size` on every field enables Wireshark-style *click field → highlight bytes*. +- `field_count` is the mapped node count — a client-side sanity check. + +## Ordering and timestamps + +- `ts` is the pcap record timestamp (µs) written by uBridge at match time — a userspace + `gettimeofday()` instant measured after the packet has crossed the kernel twice. The + last digit or two are scheduling noise; microseconds are sufficient in a simulated + environment. +- The sort key is `(ts, source file, frame_number)` — ts alone is **not** unique (two + links can hit the same microsecond); the tiebreaker yields a stable, determined order + instead of a fictional one. Index structures must never use ts as a dict key, or + same-microsecond frames silently overwrite each other. +- The cross-link delta is the intermediate node's end-to-end forwarding latency + (veth/TAP → guest protocol stack → back to host), typically hundreds of microseconds to + milliseconds. UI labels should read *node forwarding latency (host view)*, not link + propagation delay. A live capture pair confirmed it end-to-end: same `ip.id`, + TTL 64→63, 509 µs between two links. + +## Fidelity guarantee (PDML → JSON) + +The conversion is an isomorphic structure map, not a semantic transform, with two rules: +**map every attribute** and **keep values as strings**. A round-trip test enforces both: +PDML element count equals JSON node count, and every XML attribute survives with an +identical JSON value (`tests/controller/test_marker_replay.py`). The raw frame bytes — +the one thing PDML genuinely does not contain — are covered by `hex` read directly from +the pcap. + +## Error Responses + +| Status | Description | +|--------|-------------| +| 401 | Not authenticated | +| 404 | Tag has no markers in the project; detail source unknown, or ts does not match the file (the capture may have been rebuilt) | +| 409 | Tag gate: a marker under the tag is still `enabled: true` (the response lists them) | +| 501 | tshark not installed / unavailable — affects detail only; the timeline never needs tshark | +| 502 | tshark failed or timed out (10 s); truncated output never reaches the mapper | + +## Notes + +- **Heterogeneous link types coexist.** Frames are never merged into a single pcap + (mergecap is deliberately not used) — each frame carries its source and is decoded + individually, so Ethernet and serial (cHDLC/PPP) markers can share one timeline. + Malformed packets are tshark's problem: it emits `[Malformed Packet]` as regular PDML + and carries on. +- **Hardened tshark profiles.** openSUSE-style profiles (AppArmor &c.) can deny tshark + access to the project directory and the user's home even though the server process can + read both. The detail path therefore copies the pcap to a real file under `/tmp` + (not a symlink — the profile resolves real paths) and gives tshark a scratch `HOME`; + the copy is unlinked afterwards. The hex view still reads the original file. +- **Tag type.** REST and the `marker.match` WS event both carry `tag` as `int` (the + listener normalizes); replay keys on that int value. +- **Follow-ups.** Remote-compute support via the existing capture-file proxy pattern; + convenience APIs (`GET …/markers/tags` to list tags, `POST …/markers/tags/{tag}/pause` + to batch-pause — a one-call path to the replayable state). diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index f9012a8ef..0595bde7b 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -44,6 +44,8 @@ from gns3server.controller.link import _UNSET from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError from gns3server.controller.import_project import import_project as import_controller_project from gns3server.controller.export_project import export_project as export_controller_project +from gns3server.controller import marker_replay +from gns3server.controller.marker_replay import TsharkError, TsharkMissingError from gns3server.utils.asyncio import aiozipstream from gns3server.utils.path import is_safe_path from gns3server.db.repositories.templates import TemplatesRepository @@ -219,6 +221,87 @@ def get_project_markers(project: Project = Depends(dep_project)) -> dict: return project.markers +@router.get( + "/{project_id}/markers/tags/{tag}/replay/range", + dependencies=[Depends(has_privilege("Project.Audit"))], +) +def replay_tag_range(tag: int, project: Project = Depends(dep_project)) -> dict: + """ + Aggregate replay timeline for a tag: merges the pcap of every marker + carrying ``tag`` into one timestamp-ordered view (design reference: + ``marker_replay`` module docstring). + + The tag gate applies: every marker under the tag must be paused + (``enabled: false``) — 409 otherwise. The response carries the timeline + bounds, per-source stats, and the full merged frame list while under the + frame cap (5000); above it the list is replaced by per-second buckets. + + Required privilege: Project.Audit + """ + + return marker_replay.build_timeline(project, tag) + + +@router.get( + "/{project_id}/markers/tags/{tag}/replay/frames", + dependencies=[Depends(has_privilege("Project.Audit"))], +) +def replay_tag_frames( + tag: int, + ts: str, + window_ms: int = 100, + limit: int = 1000, + project: Project = Depends(dep_project), +) -> dict: + """ + Frames with ts in ``[ts, ts + window_ms]`` merged across every source of + the tag. A time with no frames is a normal successful answer: + ``{"frames": []}``. The tag gate applies (409 while any marker captures). + + ``ts`` must be the exact string returned by the range response — never + re-serialize it through a float. + + Required privilege: Project.Audit + """ + + return marker_replay.query_frames(project, tag, ts, window_ms=window_ms, limit=limit) + + +@router.get( + "/{project_id}/markers/tags/{tag}/replay/frame/detail", + dependencies=[Depends(has_privilege("Project.Audit"))], +) +async def replay_tag_frame_detail( + tag: int, + ts: str, + node_id: str, + link_id: str, + marker: str, + project: Project = Depends(dep_project), +) -> dict: + """ + Decode exactly one frame (lazy — invoked when the user opens a frame, + never by the timeline itself): raw bytes for the hex view read straight + from the pcap, protocol tree from ``tshark -T pdml`` mapped isomorphically + to JSON (every PDML attribute survives, values stay strings). + + ``ts`` must be the exact string from the frame list; ``node_id`` + + ``link_id`` + ``marker`` identify the source pcap. The tag gate applies. + + Required privilege: Project.Audit + """ + + try: + return await marker_replay.decode_frame(project, tag, ts, node_id, link_id, marker) + except TsharkMissingError: + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="tshark is not installed on this server — frame detail is unavailable", + ) + except TsharkError as e: + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e)) + + # --------------------------------------------------------------------------- # Project-level marker definitions (global rules inherited by every link) # --------------------------------------------------------------------------- diff --git a/gns3server/controller/marker_replay.py b/gns3server/controller/marker_replay.py new file mode 100644 index 000000000..993bb31f8 --- /dev/null +++ b/gns3server/controller/marker_replay.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python +# +# Copyright (C) 2024 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Tag-keyed aggregate replay over paused markers' pcap files. + +Markers on different links sharing a ``tag`` form one distributed capture +session. This module merges their per-marker pcaps +(``/project-files/markers/{node_id}_{link_id}_{name}.pcap``) into a +single timestamp-ordered timeline and decodes individual frames on demand. + +Two deliberately separated performance regimes: + +* the timeline path reads only the 16-byte pcap record headers — tshark is + never invoked, so browsing works even where tshark is not installed; +* the detail path runs one ``tshark -T pdml`` per frame the caller asks about + (call count = user clicks) and maps the XML to JSON isomorphically — every + PDML attribute survives as a JSON key, values stay strings, nothing is + selected out or interpreted. + +Timestamps are uBridge's userspace ``gettimeofday`` at match time (µs, a +value measured after the packet has crossed the kernel twice — the last +digit or two are scheduling noise). A timestamp is NOT a unique key: the +merge sorts by ``(ts, source file, frame number)`` and index structures must +never use ts alone as a dict key, or same-microsecond frames silently +overwrite each other. +""" + +import asyncio +import logging +import os +import shutil +import struct +import tempfile +import xml.etree.ElementTree as ET + +from .controller_error import ControllerError, ControllerNotFoundError, ControllerBadRequestError + +log = logging.getLogger(__name__) + +# Full frame list is embedded in the range response while under this cap; +# above it the response degrades to start/end + per-second buckets so the +# client is never flooded by a high-traffic BPF. +FRAME_LIST_CAP = 5000 + +# One tshark decode per user click: a generous ceiling, not a rate limiter. +TSHARK_TIMEOUT_SECONDS = 10.0 + + +class TsharkMissingError(ControllerError): + """tshark is not installed (or not on PATH) — frame detail unavailable.""" + + +class TsharkError(ControllerError): + """tshark exited non-zero / timed out / produced unusable output.""" + + +# --------------------------------------------------------------------------- +# pcap record-header scanning (timeline path — no tshark) +# --------------------------------------------------------------------------- + +# magic → (byte order, timestamp unit). Both pcap families uBridge can write +# (libpcap default µs; ns variant accepted defensively) and both endiannesses. +_PCAP_MAGICS = { + 0xA1B2C3D4: ("<", 1), # little-endian, microseconds + 0xD4C3B2A1: (">", 1), # big-endian, microseconds + 0xA1B23C4D: ("<", 1000), # little-endian, nanoseconds + 0x4D3CB2A1: (">", 1000), # big-endian, nanoseconds +} + + +def _format_ts(sec: int, usec: int) -> str: + """Canonical ts string — the exact form clients must round-trip back.""" + + return f"{sec}.{usec:06d}" + + +def _parse_ts(ts: str) -> int: + """Parse a round-tripped ts string to integer microseconds (exact, no floats).""" + + try: + sec, _, frac = ts.partition(".") + usec = int(frac.ljust(6, "0")[:6]) if frac else 0 + return int(sec) * 1_000_000 + usec + except ValueError: + raise ControllerBadRequestError(f"Invalid timestamp: {ts!r}") + + +def scan_pcap_frames(path): + """ + Walk a pcap file reading only record headers. + + :returns: list of ``(ts_sec, ts_usec, incl_len)`` per frame, 1-based order. + Tolerates a truncated tail (stops when a record header claims more bytes + than the file holds) so a snapshot mid-write never raises. + """ + + frames = [] + try: + with open(path, "rb") as f: + data = f.read() + except OSError as e: + raise ControllerError(f"Cannot read marker pcap {path}: {e}") + + if len(data) < 24: + return frames # not even a global header — zero frames + magic = struct.unpack(" 0xFFFF or pos + 16 + incl_len > len(data): + break # truncated tail (snapshot mid-write / torn final record) + # Normalize ns pcaps to µs by truncation — uBridge writes µs anyway. + frames.append((ts_sec, ts_frac // unit if unit > 1 else ts_frac, incl_len)) + pos += 16 + incl_len + return frames + + +def read_frame_bytes(path, frame_number): + """ + Read one frame's raw bytes (hex view) straight from the pcap — never via + tshark. ``frame_number`` is 1-based (the same number tshark's + ``frame.number`` filter uses). + """ + + frames = scan_pcap_frames(path) + if not 1 <= frame_number <= len(frames): + return None + # Offset arithmetic mirrors the header scan: global header + every full + # record before the target + the target's own record header. + offset = 24 + sum(16 + incl for _s, _u, incl in frames[:frame_number - 1]) + 16 + incl_len = frames[frame_number - 1][2] + with open(path, "rb") as f: + f.seek(offset) + return f.read(incl_len).hex() + + +# --------------------------------------------------------------------------- +# Tag gate + timeline assembly +# --------------------------------------------------------------------------- + +def _tag_markers(project, tag): + """ + Every marker entry in the project carrying ``tag`` (flat + ``project.markers`` values: link_id / node_id / name keys included). + """ + + entries = [] + for key, info in project.markers.items(): + if info.get("tag") == tag: + link_id, _, name = key.partition("/") + entries.append({ + "node_id": info["node_id"], + "link_id": link_id, + "marker": name, + "enabled": info.get("enabled", True), + "data_link_type": info.get("data_link_type", "DLT_EN10MB"), + }) + return entries + + +def gate_tag(project, tag): + """ + Replay reads append-only pcaps, so it is only available while the data is + at rest: every marker under the tag must be paused. Raises 409 listing + the still-running markers, 404 when the tag has no markers at all. + """ + + entries = _tag_markers(project, tag) + if not entries: + raise ControllerNotFoundError(f"No markers with tag {tag} in project") + running = [f"{e['marker']} on link {e['link_id']}" for e in entries if e["enabled"]] + if running: + raise ControllerError( + f"Cannot replay tag {tag} while markers are capturing: {', '.join(running)}. " + "Pause every marker under the tag first." + ) + return entries + + +def _merged_frames(project, entries): + """ + Scan every source pcap and merge into one list sorted by + ``(ts, source file, frame number)`` — ts alone is not unique (two links + can hit the same microsecond); the tiebreaker yields a stable, determined + order instead of a fictional one. + """ + + markers_dir = project.markers_directory + merged = [] + sources = [] + for entry in entries: + pcap = os.path.join( + markers_dir, f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}.pcap" + ) + frames = scan_pcap_frames(pcap) if os.path.exists(pcap) else [] + sources.append({**{k: entry[k] for k in ("node_id", "link_id", "marker", "data_link_type")}, + "count": len(frames)}) + for frame_number, (sec, usec, incl_len) in enumerate(frames, start=1): + merged.append({ + "ts": _format_ts(sec, usec), + "ts_us": sec * 1_000_000 + usec, + "len": incl_len, + "node_id": entry["node_id"], + "link_id": entry["link_id"], + "marker": entry["marker"], + "frame_number": frame_number, + "_source": f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}", + }) + merged.sort(key=lambda f: (f["ts_us"], f["_source"], f["frame_number"])) + for frame in merged: + del frame["ts_us"] + del frame["_source"] + return merged, sources + + +def build_timeline(project, tag, frame_cap=FRAME_LIST_CAP): + """ + The ``range`` response: timeline bounds, per-source stats, and (under + ``frame_cap``) the full merged frame list for one-request timeline + layout. Over the cap the list is replaced by per-second buckets. + """ + + entries = gate_tag(project, tag) + frames, sources = _merged_frames(project, entries) + + response = { + "tag": tag, + "start": frames[0]["ts"] if frames else None, + "end": frames[-1]["ts"] if frames else None, + "frame_count": len(frames), + "truncated": len(frames) > frame_cap, + "sources": sources, + } + if len(frames) <= frame_cap: + response["frames"] = frames + else: + buckets = {} + for frame in frames: + second = _parse_ts(frame["ts"]) // 1_000_000 + buckets[second] = buckets.get(second, 0) + 1 + response["buckets"] = [ + {"ts": _format_ts(second, 0), "count": count} + for second, count in sorted(buckets.items()) + ] + return response + + +def query_frames(project, tag, ts, window_ms=100, limit=1000): + """ + Frames with ts in ``[T, T+window_ms]`` merged across sources. A time with + no frames is a normal, successful answer — ``{"frames": []}``. + """ + + entries = gate_tag(project, tag) + frames, _sources = _merged_frames(project, entries) + + start_us = _parse_ts(ts) + end_us = start_us + max(window_ms, 0) * 1000 + hits = [f for f in frames if start_us <= _parse_ts(f["ts"]) <= end_us] + return {"frames": hits[:max(limit, 0)]} + + +# --------------------------------------------------------------------------- +# Frame detail (tshark path — lazy, one frame per call) +# --------------------------------------------------------------------------- + +async def _tshark_version(): + try: + proc = await asyncio.create_subprocess_exec( + "tshark", "--version", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, + ) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=TSHARK_TIMEOUT_SECONDS) + return stdout.decode(errors="replace").splitlines()[0].strip() + except (OSError, asyncio.TimeoutError, IndexError): + raise TsharkMissingError("tshark is not available on this server") + + +def _tshark_scratch_copy(pcap): + """ + Copy the pcap to a scratch file under the system temp dir for tshark to + read. Hardened tshark profiles (AppArmor &c.) can deny it access to the + project directory / the user's home while still allowing /tmp — a real + copy, deliberately not a symlink, since the profile resolves real paths. + Caller must unlink the returned path. + """ + + fd, scratch = tempfile.mkstemp(suffix=".pcap", prefix="gns3-replay-") + os.close(fd) + shutil.copyfile(pcap, scratch) + return scratch + + +def _tshark_env(): + """Scratch HOME so tshark never even tries to read the user's home.""" + + env = dict(os.environ) + env["HOME"] = tempfile.gettempdir() + return env + + +def _pdml_to_nodes(element): + """ + Isomorphic PDML → JSON mapping: every XML attribute becomes a JSON key + verbatim (values stay strings), children nest under ``children``. The + element tag ("proto"/"field") is carried as ``element`` — the one + structural key beyond the attributes, so a renderer can tell a protocol + group from a leaf field (geninfo's tagless names make names unreliable). + """ + + return { + "element": element.tag, + **element.attrib, + "children": [_pdml_to_nodes(child) for child in element], + } + + +def _count_nodes(nodes): + return 1 + sum(_count_nodes(child) for child in nodes.get("children", [])) + + +async def decode_frame(project, tag, ts, node_id, link_id, marker): + """ + Decode exactly one frame: locate its pcap by source identity, verify the + round-tripped ts still matches the file (guards a rebuild between the + timeline view and this click), read the raw bytes for the hex view, and + map tshark's PDML of that single frame to JSON. + """ + + entries = gate_tag(project, tag) + entry = next( + (e for e in entries + if e["node_id"] == node_id and e["link_id"] == link_id and e["marker"] == marker), + None, + ) + if entry is None: + raise ControllerNotFoundError( + f"No marker '{marker}' with tag {tag} on link {link_id} captured by {node_id}" + ) + + pcap = os.path.join(project.markers_directory, f"{node_id}_{link_id}_{marker}.pcap") + if not os.path.exists(pcap): + raise ControllerNotFoundError(f"No capture file for marker '{marker}' (nothing ever matched)") + + frames = scan_pcap_frames(pcap) + # The ts must be the exact string the timeline returned; find the frame + # it identifies rather than trusting any position hint from the client. + frame_number = next( + (i for i, (sec, usec, _len) in enumerate(frames, start=1) + if _format_ts(sec, usec) == ts), + None, + ) + if frame_number is None: + raise ControllerNotFoundError( + f"No frame at ts {ts} in marker '{marker}' (the capture may have been rebuilt)" + ) + + raw_hex = read_frame_bytes(pcap, frame_number) + + if shutil.which("tshark") is None: + raise TsharkMissingError("tshark is not installed — frame detail is unavailable") + version = await _tshark_version() + + # Hand tshark a scratch copy under the temp dir: hardened profiles may + # deny it the project directory even though this process can read it + # (the hex view above reads the original directly). + scratch = _tshark_scratch_copy(pcap) + try: + try: + proc = await asyncio.create_subprocess_exec( + "tshark", "-r", scratch, "-T", "pdml", "-Y", f"frame.number == {frame_number}", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + env=_tshark_env(), + ) + stdout, stderr = await asyncio.wait_for( + proc.communicate(), timeout=TSHARK_TIMEOUT_SECONDS + ) + except OSError as e: + raise TsharkError(f"Could not run tshark: {e}") + except asyncio.TimeoutError: + raise TsharkError(f"tshark timed out after {TSHARK_TIMEOUT_SECONDS:.0f}s") + if proc.returncode != 0 or not stdout.strip(): + # Never feed truncated/failed output to the mapper. + raise TsharkError(f"tshark failed: {stderr.decode(errors='replace').strip()[:500]}") + finally: + try: + os.unlink(scratch) + except OSError: + pass + + try: + root = ET.fromstring(stdout) + except ET.ParseError as e: + raise TsharkError(f"Malformed PDML from tshark: {e}") + + packet = root.find("./packet") + tree = [_pdml_to_nodes(child) for child in packet] if packet is not None else [] + return { + "ts": ts, + "source": {"node_id": node_id, "link_id": link_id, "marker": marker, + "frame_number": frame_number}, + "tshark_version": version, + "field_count": sum(_count_nodes(node) for node in tree), + "hex": raw_hex, + "tree": tree, + } diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index d9ebdfae8..b69a587b6 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -519,6 +519,17 @@ class Project: os.makedirs(path, exist_ok=True) return path + @property + def markers_directory(self): + """ + Location of the marker pcap files (same layout the compute side writes + via ``markers_working_directory`` — single-server deployments share the + project directory, which is what tag replay reads). + """ + path = os.path.join(self._path, "project-files", "markers") + os.makedirs(path, exist_ok=True) + return path + @property def pictures_directory(self): """ diff --git a/tests/api/routes/controller/test_marker_replay.py b/tests/api/routes/controller/test_marker_replay.py new file mode 100644 index 000000000..9a19e291d --- /dev/null +++ b/tests/api/routes/controller/test_marker_replay.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +# +# Copyright (C) 2025 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +HTTP-route tests for the tag replay endpoints: the tag gate (409 while any +marker captures, 404 unknown tag), the merged timeline, window queries +(empty window = success), and the lazy frame detail (ts guard, isomorphic +JSON, 501 when tshark is unavailable). +""" + +import shutil +from unittest.mock import patch + +import pytest +from fastapi import FastAPI, status +from httpx import AsyncClient + +from gns3server.controller.project import Project +from gns3server.controller.udp_link import UDPLink + +from tests.controller.test_marker_replay import _write_pcap, _icmp_frame + +pytestmark = pytest.mark.asyncio + +tshark_present = pytest.mark.skipif(shutil.which("tshark") is None, reason="tshark not installed") + + +def _add_marker(project, tag, enabled, node_id, frames=None): + """Create a paused/capturing link+marker and optionally its pcap.""" + + link = UDPLink(project) + link._markers["icmp"] = {"bpf": "icmp", "tag": tag, "enabled": enabled, "color": None, + "highlight_duration": None, "capture_node_id": node_id, + "direction": None, "data_link_type": "DLT_EN10MB"} + project._links[link.id] = link + if frames is not None: + _write_pcap( + f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames + ) + return link + + +class TestReplayRoutes: + + async def test_range_409_while_capturing(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + _add_marker(project, tag=7, enabled=False, node_id="n1") + running = _add_marker(project, tag=7, enabled=True, node_id="n2") + + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7) + ) + assert response.status_code == status.HTTP_409_CONFLICT + assert "icmp" in response.json()["message"] + assert running.id in response.json()["message"] + + async def test_range_404_unknown_tag(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + _add_marker(project, tag=7, enabled=False, node_id="n1") + + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=99) + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + async def test_range_merges_sources_in_ts_order(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + # r1→r2 captures at t1 and t3; r2→r3 captures at t2 and t3 (same µs + # as source A's t3 — the tiebreak must keep both frames). + _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 500000, b"a" * 60), + (1693472002, 0, b"a" * 60), + ]) + _add_marker(project, tag=7, enabled=False, node_id="n2", frames=[ + (1693472001, 0, b"b" * 60), + (1693472002, 0, b"b" * 60), + ]) + + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7) + ) + assert response.status_code == status.HTTP_200_OK + body = response.json() + assert body["tag"] == 7 + assert body["frame_count"] == 4 + assert body["start"] == "1693472000.500000" + assert body["end"] == "1693472002.000000" + assert body["truncated"] is False + assert [f["node_id"] for f in body["frames"]] == ["n1", "n2", "n1", "n2"] + assert [f["ts"] for f in body["frames"]] == [ + "1693472000.500000", "1693472001.000000", + "1693472002.000000", "1693472002.000000", + ] + assert len(body["sources"]) == 2 + + async def test_frames_window_miss_is_empty_success( + self, app: FastAPI, client: AsyncClient, project: Project + ) -> None: + + _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 0, b"a" * 60), + ]) + + response = await client.get( + app.url_path_for("replay_tag_frames", project_id=project.id, tag=7), + params={"ts": "1693472001.000000", "window_ms": 100}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.json() == {"frames": []} + + async def test_frames_window_hit(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 0, b"a" * 60), + (1693472000, 150000, b"a" * 60), + ]) + + response = await client.get( + app.url_path_for("replay_tag_frames", project_id=project.id, tag=7), + params={"ts": "1693472000.000000", "window_ms": 150}, + ) + assert response.status_code == status.HTTP_200_OK + assert [f["ts"] for f in response.json()["frames"]] == [ + "1693472000.000000", "1693472000.150000" + ] + + async def test_detail_404_on_ts_mismatch(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + ]) + + response = await client.get( + app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), + params={"ts": "1.000000", "node_id": "n1", "link_id": link.id, "marker": "icmp"}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert "rebuilt" in response.json()["message"] + + async def test_detail_501_without_tshark(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + ]) + + with patch("gns3server.controller.marker_replay.shutil.which", return_value=None): + response = await client.get( + app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), + params={"ts": "1693472000.123456", "node_id": "n1", + "link_id": link.id, "marker": "icmp"}, + ) + assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED + + @tshark_present + async def test_detail_decodes_single_frame(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + ]) + + response = await client.get( + app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), + params={"ts": "1693472000.123456", "node_id": "n1", + "link_id": link.id, "marker": "icmp"}, + ) + assert response.status_code == status.HTTP_200_OK + body = response.json() + assert body["source"]["frame_number"] == 1 + assert body["hex"] == _icmp_frame().hex() + assert body["field_count"] > 10 + assert "tshark" in body["tshark_version"].lower() + + ip = next(p for p in body["tree"] if p.get("name") == "ip") + ttl = next(f for f in ip["children"] if f.get("name") == "ip.ttl") + # Values arrive as strings, exactly as tshark emitted them. + assert ttl["show"] == "64" and ttl["showname"] == "Time to Live: 64" diff --git a/tests/controller/test_marker_replay.py b/tests/controller/test_marker_replay.py new file mode 100644 index 000000000..e33fb9a8c --- /dev/null +++ b/tests/controller/test_marker_replay.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python +# +# Copyright (C) 2025 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Unit tests for the tag-keyed aggregate replay module (controller layer): + +* pcap record-header scanning (ts extraction, truncated-tail tolerance, + nanosecond-magic normalization) and raw-bytes reads for the hex view +* the tag gate (404 unknown tag, 409 while any marker captures) +* timeline assembly: cross-source merge ordered by (ts, source, frame number), + same-microsecond tiebreak, missing-pcap sources, frame-cap degradation to + per-second buckets +* window queries (inclusive bounds, empty-window success) +* the lazy frame detail: round-tripped-ts guard, and — where tshark is + installed — the isomorphic PDML → JSON mapping with a round-trip check + (element count and attribute coverage). +""" + +import os +import shutil +import struct + +import pytest +from types import SimpleNamespace + +from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError +from gns3server.controller.marker_replay import ( + build_timeline, + decode_frame, + query_frames, + read_frame_bytes, + scan_pcap_frames, + _format_ts, + _parse_ts, +) + +pytestmark = pytest.mark.asyncio + +PCAP_MAGIC_US = 0xA1B2C3D4 +PCAP_MAGIC_NS = 0xA1B23C4D + + +def _write_pcap(path, frames, magic=PCAP_MAGIC_US, snaplen=65535): + """frames: list of (sec, frac, payload bytes); frac is µs (or ns for the ns magic).""" + + with open(path, "wb") as f: + f.write(struct.pack("> 16: + s = (s & 0xFFFF) + (s >> 16) + return (~s) & 0xFFFF + + icmp = bytes([8, 0, 0, 0]) + struct.pack(">HHH", 1, 1, 0) + b"payload12" + icmp = icmp[:2] + struct.pack(">H", cksum(icmp)) + icmp[4:] + ip0 = struct.pack(">BBHHHBBH4s4s", 0x45, 0, 20 + len(icmp), 1, 0, 64, 1, 0, + bytes([10, 0, 0, 1]), bytes([10, 0, 0, 3])) + ip = ip0[:10] + struct.pack(">H", cksum(ip0)) + ip0[12:] + return bytes.fromhex("0200000000020200000000010800") + ip + icmp + + +def _fake_project(tmp_path, markers, markers_dir=None): + """markers: the flat project.markers shape ({'link/name': {..., node_id}}).""" + + return SimpleNamespace(markers=markers, markers_directory=str(markers_dir or tmp_path)) + + +def _marker_entry(tag, enabled=True, node_id="node-1"): + return {"bpf": "icmp", "tag": tag, "enabled": enabled, "color": None, + "highlight_duration": None, "capture_node_id": node_id, + "direction": None, "data_link_type": "DLT_EN10MB", + "node_id": node_id} + + +# --------------------------------------------------------------------------- +# pcap scanning +# --------------------------------------------------------------------------- + +class TestScanPcap: + + async def test_scans_frames_and_truncated_tail(self, tmp_path): + pcap = tmp_path / "a.pcap" + _write_pcap(pcap, [ + (1693472000, 123456, b"x" * 60), + (1693472001, 654321, b"y" * 40), + ]) + # Tear the final record in half: a snapshot mid-write must not raise. + data = bytearray(pcap.read_bytes()) + pcap.write_bytes(data[:len(data) - 20]) + + frames = scan_pcap_frames(str(pcap)) + assert frames == [(1693472000, 123456, 60)] + + async def test_ns_magic_normalized_to_us(self, tmp_path): + pcap = tmp_path / "ns.pcap" + _write_pcap(pcap, [(1693472000, 1500000, b"z" * 10)], magic=PCAP_MAGIC_NS) + assert scan_pcap_frames(str(pcap)) == [(1693472000, 1500, 10)] # 1.5 ms in µs + + async def test_read_frame_bytes_offsets(self, tmp_path): + pcap = tmp_path / "b.pcap" + _write_pcap(pcap, [ + (100, 0, b"first" + b"0" * 55), # 60 bytes + (200, 0, b"second"), # 6 bytes + ]) + assert read_frame_bytes(str(pcap), 2) == b"second".hex() + assert read_frame_bytes(str(pcap), 1) == (b"first" + b"0" * 55).hex() + assert read_frame_bytes(str(pcap), 3) is None + + async def test_ts_string_round_trip_is_exact(self): + ts = _format_ts(1693472000, 5) + assert ts == "1693472000.000005" + assert _parse_ts(ts) == 1693472000000005 + assert _parse_ts(_format_ts(1693472000, 123456)) == 1693472000123456 + + +# --------------------------------------------------------------------------- +# Tag gate + timeline +# --------------------------------------------------------------------------- + +class TestGateAndTimeline: + + async def test_unknown_tag_404(self, tmp_path): + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=1)}) + with pytest.raises(ControllerNotFoundError): + build_timeline(project, tag=7) + + async def test_gate_409_while_capturing(self, tmp_path): + project = _fake_project(tmp_path, { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + "linkB/icmp": _marker_entry(tag=7, enabled=True, node_id="n2"), + }) + with pytest.raises(ControllerError, match="linkB"): + build_timeline(project, tag=7) + + async def test_merge_orders_by_ts_with_stable_tiebreak(self, tmp_path): + # Two sources, deliberately interleaved in time, colliding on one µs. + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 500000, b"a" * 60), # t1 sourceA + (1693472002, 000000, b"a" * 60), # t3 sourceA + ]) + _write_pcap(tmp_path / "n2_linkB_icmp.pcap", [ + (1693472001, 000000, b"b" * 60), # t2 sourceB + (1693472002, 000000, b"b" * 60), # t3 sourceB — same µs as t3 sourceA + ]) + project = _fake_project(tmp_path, { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + "linkB/icmp": _marker_entry(tag=7, enabled=False, node_id="n2"), + }) + + timeline = build_timeline(project, tag=7) + assert timeline["frame_count"] == 4 + assert timeline["start"] == "1693472000.500000" + assert timeline["end"] == "1693472002.000000" + assert [f["node_id"] for f in timeline["frames"]] == ["n1", "n2", "n1", "n2"] + # Same-microsecond pair keeps both frames (a ts dict key would drop one). + assert [f["ts"] for f in timeline["frames"]][2:] == ["1693472002.000000"] * 2 + assert [f["frame_number"] for f in timeline["frames"]] == [1, 1, 2, 2] + assert {s["count"] for s in timeline["sources"]} == {2} + + async def test_missing_pcap_is_zero_count_source(self, tmp_path): + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False)}) + timeline = build_timeline(project, tag=7) + assert timeline["frame_count"] == 0 + assert timeline["start"] is None and timeline["end"] is None + assert timeline["frames"] == [] + assert timeline["sources"][0]["count"] == 0 + + async def test_over_cap_degrades_to_buckets(self, tmp_path): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 0, b"a" * 60), (1693472000, 500000, b"a" * 60), + (1693472001, 0, b"a" * 60), + ]) + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) + + timeline = build_timeline(project, tag=7, frame_cap=2) + assert timeline["truncated"] is True + assert "frames" not in timeline + assert timeline["buckets"] == [ + {"ts": "1693472000.000000", "count": 2}, + {"ts": "1693472001.000000", "count": 1}, + ] + + +# --------------------------------------------------------------------------- +# Window query +# --------------------------------------------------------------------------- + +class TestQueryFrames: + + def _project(self, tmp_path): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 0, b"a" * 60), + (1693472000, 150000, b"a" * 60), + (1693472005, 0, b"a" * 60), + ]) + return _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) + + async def test_window_inclusive_bounds(self, tmp_path): + result = query_frames(self._project(tmp_path), tag=7, ts="1693472000.000000", window_ms=150) + assert [f["ts"] for f in result["frames"]] == ["1693472000.000000", "1693472000.150000"] + + async def test_window_miss_is_empty_success(self, tmp_path): + result = query_frames(self._project(tmp_path), tag=7, ts="1693472001.000000", window_ms=100) + assert result == {"frames": []} + + async def test_limit_applies(self, tmp_path): + result = query_frames(self._project(tmp_path), tag=7, ts="1693472000.000000", + window_ms=150, limit=1) + assert len(result["frames"]) == 1 + + +# --------------------------------------------------------------------------- +# Frame detail (tshark path) +# --------------------------------------------------------------------------- + +tshark_present = pytest.mark.skipif(shutil.which("tshark") is None, reason="tshark not installed") + + +class TestDecodeFrame: + + def _project(self, tmp_path): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 123456, _icmp_frame()), + ]) + return _fake_project(tmp_path, { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + }) + + async def test_ts_mismatch_guard_404(self, tmp_path): + project = self._project(tmp_path) + with pytest.raises(ControllerNotFoundError, match="rebuilt"): + await decode_frame(project, tag=7, ts="1.000000", + node_id="n1", link_id="linkA", marker="icmp") + + async def test_unknown_source_404(self, tmp_path): + project = self._project(tmp_path) + with pytest.raises(ControllerNotFoundError): + await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="nobody", link_id="linkA", marker="icmp") + + async def test_decode_feeds_tshark_a_scratch_copy(self, tmp_path): + """Hardened tshark profiles deny the project dir — tshark must read a + /tmp copy (a real copy, not a symlink) that is unlinked afterwards.""" + + import tempfile + from unittest.mock import patch, AsyncMock + + observed = [] + PDML = (b'' + b'' + b'') + + class FakeProc: + returncode = 0 + + async def communicate(self): + return PDML, b"" + + async def fake_exec(*args, **kwargs): + r_index = args.index("-r") + observed.append((args[r_index + 1], kwargs.get("env"))) + return FakeProc() + + project = self._project(tmp_path) + with patch("gns3server.controller.marker_replay.shutil.which", return_value="tshark"), \ + patch("gns3server.controller.marker_replay._tshark_version", AsyncMock(return_value="tshark 4.6.7")), \ + patch("gns3server.controller.marker_replay.asyncio.create_subprocess_exec", side_effect=fake_exec): + detail = await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="n1", link_id="linkA", marker="icmp") + + assert detail["field_count"] == 2 # proto + field from the canned PDML + (scratch, env), = observed + original = str(tmp_path / "n1_linkA_icmp.pcap") + assert scratch != original + assert scratch.startswith(tempfile.gettempdir()) and scratch.endswith(".pcap") + assert env["HOME"] == tempfile.gettempdir() + assert not os.path.exists(scratch) # cleaned up after the decode + + @tshark_present + async def test_decode_isomorphic_mapping(self, tmp_path): + import asyncio + import xml.etree.ElementTree as ET + + project = self._project(tmp_path) + detail = await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="n1", link_id="linkA", marker="icmp") + + assert detail["source"]["frame_number"] == 1 + assert detail["hex"] == _icmp_frame().hex() + assert detail["field_count"] > 0 + assert "tshark" in detail["tshark_version"].lower() + + # Round-trip fidelity: node count equals the PDML element count + # (protos + fields, excluding the container itself)… + proc = await asyncio.create_subprocess_exec( + "tshark", "-r", str(tmp_path / "n1_linkA_icmp.pcap"), "-T", "pdml", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, + ) + stdout, _ = await proc.communicate() + packet = ET.fromstring(stdout).find("./packet") + xml_elements = [e for e in packet.iter() if e is not packet] + assert detail["field_count"] == len(xml_elements) + + # …and every XML attribute survives verbatim as a JSON string key. + def walk(element, node): + for key, value in element.attrib.items(): + assert node.get(key) == value + assert all(isinstance(v, str) for k, v in node.items() if k != "children") + for child, child_node in zip(element, node["children"]): + walk(child, child_node) + + for element, node in zip(packet, detail["tree"]): + walk(element, node) + + # Values stay strings (no numeric re-typing). + ttl = next( + f for p in detail["tree"] if p.get("name") == "ip" + for f in p["children"] if f.get("name") == "ip.ttl" + ) + assert ttl["show"] == "64" and ttl["showname"] == "Time to Live: 64" From 9147deccaa79a3f3a6b22891179d899f3dbe4d45 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 2 Sep 2026 22:33:23 +0800 Subject: [PATCH 24/60] docs: document that restarting capture nodes truncates marker pcaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A marker pcap's lifetime equals its uBridge's lifetime: a fresh uBridge reinstalls every desired marker (paused ones too — install first, then turn the filter off) and pcap_dump_open truncates. Server restart + project reopen without starting nodes is safe until a uBridge comes up (verified live); Docker nodes restart effectively on server restart via stale-container cleanup. Recorded in the tag-gate table, a lifecycle note, and a pcap_dump_open_append follow-up. --- docs/features/marker-tag-replay.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/features/marker-tag-replay.md b/docs/features/marker-tag-replay.md index d8439fed0..4d148a5c8 100644 --- a/docs/features/marker-tag-replay.md +++ b/docs/features/marker-tag-replay.md @@ -103,11 +103,19 @@ markers at all → 404. | all `enabled: false` (paused) | retained, frozen | **allowed** | | deleted | file unlinked | no data | | `bpf`/`tag`/`direction` changed (rebuild) | pcap reopened (truncated) — new session | prior history gone | +| capture node (re)started | pcap reopened (truncated) — new session | prior history gone | - **Pause, not delete.** Deleting a marker (or its definition) deletes its pcap — replay before deleting or the data is gone. - **Pause → resume → pause is fine.** The pcap accumulates the full history; replay covers everything up to the current pause point. +- **The replay window ends when nodes restart.** A pcap's lifetime equals its uBridge's + lifetime: a fresh uBridge reinstalls every desired marker — paused ones too (install + first, then turn the filter off) — and uBridge opens the pcap with truncate semantics + (`pcap_dump_open`, not `_append`). Server restart + project reopen **without starting + nodes** is safe: nothing touches the files until a uBridge comes up (verified live). + Docker nodes effectively restart on server restart as well (stale-container cleanup), + so their window is shorter still. - uBridge flushes every matched packet to the pcap immediately (`pcap_dump_flush` per packet under a mutex — verified in the uBridge source), so a pause boundary never loses tail frames. @@ -244,4 +252,6 @@ the pcap. listener normalizes); replay keys on that int value. - **Follow-ups.** Remote-compute support via the existing capture-file proxy pattern; convenience APIs (`GET …/markers/tags` to list tags, `POST …/markers/tags/{tag}/pause` - to batch-pause — a one-call path to the replayable state). + to batch-pause — a one-call path to the replayable state); uBridge-side + `pcap_dump_open_append` (with a linktype-header check on the existing file) so capture + history survives node restarts instead of being truncated on every reinstall. From 8b3aafbbdc3701c50e5a8317c49b33bb9bbfa4b4 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 00:01:18 +0800 Subject: [PATCH 25/60] feat: IOU-style startup-config for IOL Docker nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Templates reference a config file with the GNS3_IOL_STARTUP_CONFIG environment knob; the controller materializes the file content into startup_config_content on node creation (sent once, knob consumed — the same pattern as the IOU startup_config mapping). The compute builds the content into the node's nvram_ at the next start using the IOU nvram_import utility (IOL and IOU share the nvram container format, verified against iol-xe 17.18.02): valid config at boot, no setup dialog, %h hostname substitution, hostname rewrite on rename. Semantics verified against the runner: IOL boots from NVRAM whenever it holds a config, so a plain stop/start never re-applies the startup config and 'write memory' survives restarts; an explicit content edit (PUT) is re-applied on the next start and wins over the saved config, like IOU. --- docs/features/iol-runner-docker.md | 42 +++++- gns3server/api/routes/compute/docker_nodes.py | 4 +- gns3server/compute/docker/iol_docker_vm.py | 123 +++++++++++++++- gns3server/controller/node.py | 43 ++++++ gns3server/schemas/compute/docker_nodes.py | 5 +- tests/api/routes/compute/test_docker_nodes.py | 46 +++++- tests/compute/docker/test_iol_docker_vm.py | 133 ++++++++++++++++++ tests/controller/test_node.py | 53 +++++++ 8 files changed, 440 insertions(+), 9 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 1b91301b3..03b263282 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -98,7 +98,7 @@ API docs for the auth flow), or in the Web UI under | Field | Value | Why | |---|---|---| -| `environment` | `GNS3_IOL_RUNNER=1` | The switch that selects `IOLDockerVM` (skip-init, unix-socket NIO, auto volumes). Optional: `GNS3_IOL_MEMORY=` (default 2048). | +| `environment` | `GNS3_IOL_RUNNER=1` | The switch that selects `IOLDockerVM` (skip-init, unix-socket NIO, auto volumes). Optional: `GNS3_IOL_MEMORY=` (default 2048), `GNS3_IOL_STARTUP_CONFIG=` (initial configuration, see below). | | `extra_volumes` | `["/config"]` | `/tmp/run` is auto-added. **Never add `/tmp`** — it would persist the socket directory into the projects tree and uBridge would reject the too-long AF_UNIX path. | | `adapters` | number of 4-port units | The IOU convention: one adapter = `Ethernet0/0`–`Ethernet0/3`, two adapters add `Ethernet1/0`–`1/3`, … (8 units / 32 ports max). Ports are addressed as (adapter, port 0–3) and shown grouped in the UI. | | `memory` | optional; `0` (default) = no cap | Unset works — Docker applies no limit. When you do set a cap, keep it at IOL memory + ~512 MB, or the cgroup OOM-killer shoots the router. | @@ -113,9 +113,42 @@ API docs for the auth flow), or in the Web UI under `Linux Unix (i686)` banner within seconds. 3. `$XDG_RUNTIME_DIR/gns3/unixio//` contains `s00.sock`… (one pair per port). -4. The startup-config lives at - `project-files/docker//tmp/run/config` (interface names - `Ethernet0/0`, not `GigabitEthernet0/0`). +4. A node created with a startup-config boots straight to the configured + hostname — no initial configuration dialog (interface names in the + config are `Ethernet0/0`, not `GigabitEthernet0/0`). + +## Startup configuration + +IOL Docker nodes load an initial configuration exactly like native IOU +nodes: the **template references a config file**, and every node created +from it boots with that configuration as its personal starting point. + +```json +"environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=iol-xe-base.txt" +``` + +* The file lives in the controller's configs directory (the `configs_path` + server setting, e.g. `~/.config/GNS3/3.1/configs`) — the same place IOU + and VPCS base configs live. `%h` in the content is replaced with the node + name at start. +* **Creation materializes it once**: the controller reads the file and + sends its content with the node; afterwards the knob is consumed and the + template file is never referenced again — editing it does not affect + existing nodes. +* **NVRAM is the source of truth at boot** (verified on the runner): the + content is built into the node's `tmp/run/nvram_` — IOL and IOU + share the nvram container format, so the server-side `nvram_import` + utility produces a file IOL boots from directly. Consequences: + * `write memory` survives stop/start and container recreation (a plain + restart never re-applies the startup-config over it). + * Editing a node's `startup_config_content` property (PUT) re-applies it + on the next start, overwriting what `write memory` had saved — the + explicit edit wins, as with IOU. + * Renaming a node rewrites the `hostname` line in its NVRAM, so a + duplicated/renamed node boots under its new name. +* **Resetting a node to its template config**: stop the node, delete + `project-files/docker//tmp/run/nvram_*`, then re-apply the content + (or recreate the node). ## Server mechanisms @@ -125,6 +158,7 @@ API docs for the auth flow), or in the Web UI under | `GNS3_UNIX_SOCKET_DIR=` | `VendorDockerVM` | In-container socket directory (default `/tmp`). Any image whose agent exposes the `s%02d`/`c%02d` datagram pairs can use this without the IOL specifics. | | `GNS3_IOL_RUNNER=1` | `IOLDockerVM` (selected in the manager) | Forces skip-init + unix-socket NIO + the `/config` and `/tmp/run` volumes; on every start writes `/config/iol-config.json` (`num-eth` = adapter count, `num-serial` = 0, memory from `GNS3_IOL_MEMORY`, default 2048), creates `/tmp/run/` (the IOL process dies without it) and removes stale sockets/netio dirs from the socket directory (`tmp/run` is never touched). | | `restart()` hardening | `IOLDockerVM` | The base `docker restart` would boot the runner on a stale config and leave uBridge wired to the previous run's sockets; reload becomes graceful stop (SIGTERM → NVRAM flush) + full start. | +| `GNS3_IOL_STARTUP_CONFIG=` | controller `Node._node_data` + `IOLDockerVM` | Startup-config plumbing (see above): the controller translates the file into `startup_config_content` on node creation (sent once); the compute builds it into `nvram_` at the next start via the IOU `nvram_import` utility and rewrites the hostname line on rename. | `GNS3_STOP_TIMEOUT` (default 60) controls the SIGTERM grace period on stop. Extra iol-runner flags can be passed via `start_command`, e.g. `-keep` diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index 5070cd855..4c7f319c3 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -140,6 +140,7 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D "extra_hosts", "extra_volumes", "extra_configs", + "startup_config_content", "memory", "cpus", ] @@ -147,7 +148,8 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D changed = False node_data = jsonable_encoder(node_data, exclude_unset=True) for prop in props: - if prop in node_data and node_data[prop] != getattr(node, prop): + # hasattr: startup_config_content only exists on IOLDockerVM + if prop in node_data and hasattr(node, prop) and node_data[prop] != getattr(node, prop): setattr(node, prop, node_data[prop]) changed = True # We don't call container.update for nothing because it will restart the container diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index ce4c03e64..096f3efad 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -42,12 +42,15 @@ import glob import json import logging import os +import re import shutil from gns3server.compute.adapters.ethernet_adapter import EthernetAdapter -from gns3server.compute.docker.docker_error import DockerHttp404Error +from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM +from gns3server.compute.iou.utils.iou_export import nvram_export +from gns3server.compute.iou.utils.iou_import import nvram_import log = logging.getLogger(__name__) @@ -66,10 +69,20 @@ class IOLDockerVM(VendorDockerVM): The marker itself forces ``GNS3_SKIP_INIT`` and the unix-socket NIO wiring, and auto-adds the ``/config`` and ``/tmp/run`` persistent volumes, so a template containing only ``GNS3_IOL_RUNNER=1`` is fully configured. + + Startup configuration follows the IOU model: a template may reference a + config file with the ``GNS3_IOL_STARTUP_CONFIG`` environment knob; the + controller materializes the file content into ``startup_config_content`` + when the node is created. The content is built into the node's NVRAM + (``tmp/run/nvram_``) on the next start — IOL boots straight from + NVRAM, so a plain stop/start never re-applies it and ``write memory`` + survives restarts. """ _IOL_CONFIG_DIR = "/config" _IOL_RUN_DIR = "/tmp/run" + # The runner launches IOL with a fixed 256KB nvram (-n 256) + _IOL_NVRAM_SIZE_KB = 256 def _parse_vendor_environment(self): @@ -97,6 +110,13 @@ class IOLDockerVM(VendorDockerVM): # space, disjoint from IOU's); None until the create payload carries it. self._application_id = None + # Startup-config content materialized by the controller from the file + # referenced by GNS3_IOL_STARTUP_CONFIG. Applied to the NVRAM at start + # time (not in the setter: the application id may not be final yet + # when the create payload is applied field by field). + self._startup_config_content = None + self._startup_config_dirty = False + @property def application_id(self) -> int: """ @@ -114,6 +134,99 @@ class IOLDockerVM(VendorDockerVM): def application_id(self, value) -> None: self._application_id = int(value) + @property + def startup_config_content(self): + """ + Startup-config content, delivered by the controller when the node is + created from a template carrying GNS3_IOL_STARTUP_CONFIG (or updated + with new content). + """ + + return self._startup_config_content + + @startup_config_content.setter + def startup_config_content(self, content): + """ + Record new startup-config content; it is built into the node's NVRAM + on the next start. IOL boots from NVRAM whenever it holds a config, so + the content is only pushed when it actually changes — a plain + stop/start never re-applies it and `write memory` survives restarts. + """ + + if not content or content == self._startup_config_content: + # An empty value is ignored: erasing the config is not supported + # (mirrors the IOU setter) and Web clients PUT "" for unset fields. + return + self._startup_config_content = content + self._startup_config_dirty = True + + def _iol_nvram_file(self) -> str: + """ + The IOL NVRAM file inside the persistent /tmp/run volume. The name + embeds the application id (nvram_00772-style, like CML). + """ + + return os.path.join(self.working_dir, "tmp", "run", f"nvram_{self.application_id:05d}") + + def _apply_pending_startup_config(self) -> None: + """ + Build the node's NVRAM from the recorded startup-config content. IOL + and IOU share the same NVRAM container format (startup-config stored + as text inside the nvram file system), so the IOU nvram_import + utility produces a file IOL boots from directly — valid config, no + initial configuration dialog. + """ + + content = self._startup_config_content.replace("%h", self._name) + nvram_file = self._iol_nvram_file() + os.makedirs(os.path.dirname(nvram_file), exist_ok=True) + try: + nvram = nvram_import(None, content.encode("utf-8"), None, self._IOL_NVRAM_SIZE_KB) + with open(nvram_file, "wb") as f: + f.write(nvram) + except (OSError, ValueError) as e: + raise DockerError(f"Could not write IOL startup-config to NVRAM of container '{self._name}': {e}") + log.debug("IOL container '%s': startup-config written to %s", self._name, nvram_file) + + @DockerVM.name.setter + def name(self, new_name): + """ + Override: keep the hostname line inside the NVRAM in sync with the + node name (IOU parity), so a renamed or duplicated node boots under + its new name. Skipped while a content change is pending — the next + start pushes the new content with the already-updated name. + """ + + if not self._startup_config_dirty: + nvram_file = self._iol_nvram_file() + if os.path.exists(nvram_file): + try: + with open(nvram_file, "rb") as f: + startup_config, _ = nvram_export(f.read()) + if startup_config: + content = re.sub( + r"hostname .+$", + "hostname " + new_name, + startup_config.decode("utf-8", errors="replace"), + flags=re.MULTILINE, + ) + nvram = nvram_import(None, content.encode("utf-8"), None, self._IOL_NVRAM_SIZE_KB) + with open(nvram_file, "wb") as f: + f.write(nvram) + except (OSError, ValueError) as e: + log.warning(f"Could not update hostname in NVRAM of IOL container '{self._name}': {e}") + super(IOLDockerVM, IOLDockerVM).name.__set__(self, new_name) + + def asdict(self): + """ + Override: expose the recorded startup-config content (empty for nodes + created before the knob existed or reloaded from a topology). + """ + + result = super().asdict() + result["startup_config_content"] = self._startup_config_content + return result + @DockerVM.adapters.setter def adapters(self, adapters): """ @@ -195,6 +308,14 @@ class IOLDockerVM(VendorDockerVM): if state == "running": return + # Pending startup-config is materialized here rather than in the + # property setter: at create-payload time the application id may not + # be final yet (the create route applies fields in schema order) and + # a running container must not have its NVRAM swapped mid-flight. + if self._startup_config_dirty: + self._apply_pending_startup_config() + self._startup_config_dirty = False + wiring_dir = self._unix_socket_wiring_dir() for pattern in ("s??.sock", "c??.sock"): for stale in glob.glob(os.path.join(wiring_dir, pattern)): diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index e72dfbc9d..c9189f8d6 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -40,6 +40,30 @@ import logging log = logging.getLogger(__name__) +def _extract_iol_startup_config_knob(environment): + """ + Split the GNS3_IOL_STARTUP_CONFIG knob out of a Docker environment + (iol-runner images): the value is a config file name, resolved by the + controller in its configs directory. + + :returns: (filename, environment without the knob line); filename is None + when the knob is absent or empty. + """ + + if not environment or "GNS3_IOL_STARTUP_CONFIG=" not in environment: + return None, environment + filename = None + kept = [] + for line in environment.splitlines(): + stripped = line.strip().rstrip(",") + if stripped.startswith("GNS3_IOL_STARTUP_CONFIG="): + if filename is None: + filename = stripped.split("=", 1)[1].strip() + else: + kept.append(line) + return filename or None, "\n".join(kept) + + class Node: # These properties are used only on controller and are not forwarded to the compute CONTROLLER_ONLY_PROPERTIES = [ @@ -573,6 +597,25 @@ class Node: data[v] = self._base_config_file_content(self._properties[k]) del data[k] del self._properties[k] # We send the file only one time + + # IOL runner (Docker) startup-config: translate the config file + # referenced by the GNS3_IOL_STARTUP_CONFIG environment knob into + # content, like the mappings above. Sent only once — afterwards + # the node's configuration lives in its NVRAM on the compute (a + # reloaded node is recreated without the knob and boots from the + # persistent NVRAM, preserving `write memory`). + if self._node_type == "docker": + filename, environment = _extract_iol_startup_config_knob(self._properties.get("environment")) + if filename: + content = self._base_config_file_content(filename) + if content: + data["startup_config_content"] = content + data["environment"] = environment + self._properties["environment"] = environment + else: + log.warning( + f"Cannot load IOL startup-config file '{filename}' for node '{self._name}': file not found" + ) data["name"] = self._name # For remote computes, convert absolute image paths to relative paths diff --git a/gns3server/schemas/compute/docker_nodes.py b/gns3server/schemas/compute/docker_nodes.py index e3523cad8..e2023d407 100644 --- a/gns3server/schemas/compute/docker_nodes.py +++ b/gns3server/schemas/compute/docker_nodes.py @@ -26,7 +26,7 @@ class DockerBase(BaseModel): Common Docker node properties. """ - @field_validator("start_command", "environment", "extra_hosts", mode="before") + @field_validator("start_command", "environment", "extra_hosts", "startup_config_content", mode="before") @classmethod def _empty_string_to_none(cls, value): # Web clients serialize empty form fields as "" while unset values are @@ -59,6 +59,9 @@ class DockerBase(BaseModel): extra_hosts: Optional[str] = Field(None, description="Docker extra hosts (added to /etc/hosts)") extra_volumes: Optional[List[str]] = Field(None, description="Additional directories to make persistent") extra_configs: Optional[List[ExtraConfig]] = Field(None, description="Configuration files injected into the container (bind-mounted read-only)") + startup_config_content: Optional[str] = Field( + None, description="Startup-config content (IOL runner images: materialized into the node's NVRAM at start)" + ) memory: Optional[int] = Field(None, ge=0, description="Maximum amount of memory the container can use in MB") cpus: Optional[float] = Field(None, ge=0, description="Maximum amount of CPU resources the container can use") custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adapters") diff --git a/tests/api/routes/compute/test_docker_nodes.py b/tests/api/routes/compute/test_docker_nodes.py index efadd604b..b9fa79fd8 100644 --- a/tests/api/routes/compute/test_docker_nodes.py +++ b/tests/api/routes/compute/test_docker_nodes.py @@ -20,9 +20,11 @@ import pytest_asyncio from fastapi import FastAPI, status from httpx import AsyncClient -from tests.utils import asyncio_patch -from unittest.mock import patch +from tests.utils import asyncio_patch, AsyncioMagicMock +from unittest.mock import patch, MagicMock +from gns3server.compute.docker import Docker +from gns3server.compute.docker.iol_docker_vm import IOLDockerVM from gns3server.compute.project import Project pytestmark = pytest.mark.asyncio @@ -98,6 +100,46 @@ class TestDockerNodesRoutes: assert response.json()["extra_hosts"] == "test:127.0.0.1" + async def test_docker_create_iol_startup_config( + self, app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + base_params: dict + ) -> None: + """ + The controller materializes the GNS3_IOL_STARTUP_CONFIG knob into + startup_config_content; the create route must carry it onto the + IOLDockerVM (plain Docker nodes have no such property and skip it). + """ + + params = dict(base_params) + params["image"] = "iol-xe/iol-xe:17-18-02" + params["environment"] = "GNS3_IOL_RUNNER=1" + params["startup_config_content"] = "hostname %h\n" + create_response = { + "Id": "8bd8153ea8f5", + "Warnings": [], + "Config": { + "Entrypoint": ["/iol-runner", "-config", "/config/iol-config.json", "-stdio"], + "Cmd": [], + "Volumes": {}, + }, + } + seed_proc = MagicMock() + seed_proc.communicate = AsyncioMagicMock(return_value=(b"seedcid\n", b"")) + seed_proc.returncode = 0 + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "iol-xe/iol-xe"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=create_response): + with patch("asyncio.subprocess.create_subprocess_exec", return_value=seed_proc): + response = await compute_client.post( + app.url_path_for("compute:create_docker_node", project_id=compute_project.id), json=params + ) + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["startup_config_content"] == "hostname %h\n" + vm = Docker.instance().get_node(response.json()["node_id"], project_id=str(compute_project.id)) + assert isinstance(vm, IOLDockerVM) + assert vm.startup_config_content == "hostname %h\n" + @pytest.mark.parametrize( "name, status_code", ( diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index b3aa8cbab..a5022eea2 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -40,6 +40,8 @@ from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM from gns3server.compute.docker.iol_docker_vm import IOLDockerVM from gns3server.compute.docker.docker_error import DockerError +from gns3server.compute.iou.utils.iou_export import nvram_export +from gns3server.compute.iou.utils.iou_import import nvram_import # --------------------------------------------------------------------------- @@ -601,3 +603,134 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) assert f'"{os.path.join(wiring_dir, "s00.sock")}"' in flat assert "add_nio_tap" not in flat + + +# --------------------------------------------------------------------------- +# Startup configuration +# --------------------------------------------------------------------------- + + +def _nvram_path(vm): + return os.path.join(vm.working_dir, "tmp", "run", f"nvram_{vm.application_id:05d}") + + +def _read_nvram(vm): + with open(_nvram_path(vm), "rb") as f: + return f.read() + + +async def _start(vm, state="stopped"): + _mock_start(vm, state=state) + with patch("gns3server.compute.docker.Docker.install_busybox"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + await vm.start() + + +@pytest.mark.asyncio +async def test_startup_config_built_into_nvram_at_start(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm.startup_config_content = "hostname %h\ninterface Ethernet0/0\n ip address dhcp\n no shutdown" + await _start(vm) + + startup, _ = nvram_export(_read_nvram(vm)) + content = startup.decode("utf-8") + # %h is substituted with the node name, like the IOU startup-config + assert f"hostname {vm.name}" in content + assert "ip address dhcp" in content + assert vm._startup_config_dirty is False # consumed + + +@pytest.mark.asyncio +async def test_plain_restart_never_reapplies_startup_config(compute_project, manager): + # IOL boots from NVRAM whenever it holds a config: a plain stop/start must + # not rebuild it or `write memory` done in the router would be lost. + vm = _make_vm(compute_project, manager) + vm.startup_config_content = "hostname RouterOne" + await _start(vm) + + # simulate `write memory`: IOS rewrites the NVRAM behind our back + with open(_nvram_path(vm), "wb") as f: + f.write(nvram_import(None, b"hostname RouterSaved\n", None, 256)) + + await _start(vm) # plain restart, no new content delivered + + startup, _ = nvram_export(_read_nvram(vm)) + assert b"hostname RouterSaved" in startup + + +@pytest.mark.asyncio +async def test_changed_content_reapplied_at_next_start(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm.startup_config_content = "hostname RouterOne" + await _start(vm) + vm.startup_config_content = "hostname RouterTwo" + assert vm._startup_config_dirty is True + await _start(vm) + + startup, _ = nvram_export(_read_nvram(vm)) + assert b"hostname RouterTwo" in startup + + +@pytest.mark.asyncio +async def test_empty_content_never_builds_nvram(compute_project, manager): + # Web clients PUT "" for unset fields: must be ignored, not treated as a + # config change (mirrors the IOU setter's erase protection). + vm = _make_vm(compute_project, manager) + vm.startup_config_content = "" + vm.startup_config_content = None + await _start(vm) + + assert not os.path.exists(_nvram_path(vm)) + + +@pytest.mark.asyncio +async def test_reput_unchanged_content_keeps_written_nvram(compute_project, manager): + # A full PUT that carries the same content again (WebUI round-trips what + # the GET returned) must not re-apply it over `write memory`. + vm = _make_vm(compute_project, manager) + vm.startup_config_content = "hostname RouterOne" + await _start(vm) + + with open(_nvram_path(vm), "wb") as f: # simulate `write memory` + f.write(nvram_import(None, b"hostname RouterSaved\n", None, 256)) + + vm.startup_config_content = "hostname RouterOne" # unchanged re-PUT + await _start(vm) + + startup, _ = nvram_export(_read_nvram(vm)) + assert b"hostname RouterSaved" in startup + + +@pytest.mark.asyncio +async def test_running_container_keeps_pending_config(compute_project, manager): + # The NVRAM is only (re)built on a genuine start of a stopped container: + # swapping it mid-flight would be lost to the next `write memory`. + vm = _make_vm(compute_project, manager) + vm.startup_config_content = "hostname RouterOne" + await _start(vm, state="running") + + assert not os.path.exists(_nvram_path(vm)) + assert vm._startup_config_dirty is True + + +@pytest.mark.asyncio +async def test_rename_rewrites_hostname_in_nvram(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm.startup_config_content = "hostname %h" + await _start(vm) + + vm.name = "renamed-1" # IOU parity: the node boots under its new name + + startup, _ = nvram_export(_read_nvram(vm)) + assert b"hostname renamed-1" in startup + + +def test_asdict_exposes_startup_config_content(compute_project, manager): + + vm = _make_vm(compute_project, manager) + assert vm.asdict()["startup_config_content"] is None + vm.startup_config_content = "hostname RouterOne" + assert vm.asdict()["startup_config_content"] == "hostname RouterOne" diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index 9ba4c0ca6..e4ed15bd6 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -72,6 +72,59 @@ def test_docker_iol_ports_grouped_in_four_port_units(compute, project): assert [p.asdict()["name"] for p in node.ports] == ["eth0", "eth1"] +def test_docker_iol_startup_config_materialized_once(compute, project, monkeypatch): + """ + The GNS3_IOL_STARTUP_CONFIG environment knob references a config file in + the controller's configs directory: like the IOU startup_config mapping, + its content is sent to the compute exactly once (the knob is consumed) — + afterwards the node's configuration lives in its NVRAM on the compute. + """ + + node = Node(project, compute, "iol", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=my-iol-config.txt"}) + monkeypatch.setattr(node, "_base_config_file_content", lambda path: "hostname %h\n") + + data = node._node_data() + assert data["startup_config_content"] == "hostname %h\n" + assert data["environment"] == "GNS3_IOL_RUNNER=1" + assert "GNS3_IOL_STARTUP_CONFIG" not in node.properties["environment"] + + # sent only once: a later sync (e.g. project reload) carries neither + data = node._node_data() + assert "startup_config_content" not in data + + +def test_docker_iol_startup_config_missing_file_keeps_knob(compute, project, monkeypatch): + + node = Node(project, compute, "iol", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=gone.txt"}) + monkeypatch.setattr(node, "_base_config_file_content", lambda path: None) + + data = node._node_data() + assert "startup_config_content" not in data + # the knob is kept so the config still applies once the file shows up + assert "GNS3_IOL_STARTUP_CONFIG" in node.properties["environment"] + + +def test_extract_iol_startup_config_knob_variants(): + + from gns3server.controller.node import _extract_iol_startup_config_knob + + assert _extract_iol_startup_config_knob(None) == (None, None) + assert _extract_iol_startup_config_knob("GNS3_IOL_RUNNER=1") == (None, "GNS3_IOL_RUNNER=1") + # whitespace and trailing commas are tolerated, like the compute env parsing + filename, environment = _extract_iol_startup_config_knob("GNS3_IOL_RUNNER=1,\n GNS3_IOL_STARTUP_CONFIG=cfg.txt ,") + assert filename == "cfg.txt" + assert environment == "GNS3_IOL_RUNNER=1," + # an empty value is treated as absent + filename, environment = _extract_iol_startup_config_knob("GNS3_IOL_STARTUP_CONFIG=") + assert filename is None + + def test_name(compute, project): """ If node use a name template generate names From 5685f40dd69f2b70f42eb5e6e0ad682ed61d74cb Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 00:26:04 +0800 Subject: [PATCH 26/60] fix: keep payload-delivered state across container (re)creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create() re-runs _parse_vendor_environment on every (re)create — and a stop removes the container, so every start recreates it. Resetting the controller-allocated application id there silently flipped nodes to the hash fallback after their first stop/start (MAC change, plus collision risk with the allocation pool), and dropped any pending startup-config delivered by a PUT. The application id and startup-config state now default via class attributes instead of being re-initialized by the parser, and the hash fallback is removed outright: starting an IOL node without a coordinated allocation raises an actionable error — an uncoordinated id could collide with the pool and blackhole traffic as a MAC loop. Found in the E2E run: node booted app id 512, came back as 566 (hash fallback) after one stop/start; the PUT-ed config edit vanished the same way. --- docs/features/iol-runner-docker.md | 13 +++--- gns3server/compute/docker/iol_docker_vm.py | 48 ++++++++++++-------- tests/compute/docker/test_iol_docker_vm.py | 52 ++++++++++++++++------ 3 files changed, 75 insertions(+), 38 deletions(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 03b263282..3d2c62fbe 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -140,7 +140,10 @@ from it boots with that configuration as its personal starting point. share the nvram container format, so the server-side `nvram_import` utility produces a file IOL boots from directly. Consequences: * `write memory` survives stop/start and container recreation (a plain - restart never re-applies the startup-config over it). + restart never re-applies the startup-config over it). The first + `write memory` after a server-built NVRAM asks for a one-time + `[confirm]` (the builder stamps an IOS 15.4 version marker into the + nvram header; press return — IOS then writes its own). * Editing a node's `startup_config_content` property (PUT) re-applies it on the next start, overwriting what `write memory` had saved — the explicit edit wins, as with IOU. @@ -175,10 +178,10 @@ diagnosing wiring issues). ID (`aabb.cc{app}{iface}`), e.g. `aabb.cc03.0400`. The controller allocates the ID at node creation from the upper half of the id space (512–1022, disjoint from IOU's 1–511, limit 511 IOL Docker nodes across - opened projects sharing computes); without an allocation (raw compute API, - pre-allocation topologies) a stable node-derived fallback in the same - range is used. Nodes sharing an ID would silently drop each other's - frames as MAC loops. + opened projects sharing computes). Starting a node without an allocation + (raw compute API, pre-allocation topologies) is an error, not a fallback — + an uncoordinated ID could collide with the pool and make nodes silently + drop each other's frames as MAC loops. * **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0` (4 ports per unit, matching the adapter-count granularity) — startup configs addressing `GigabitEthernet…` are rejected by the parser. diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index 096f3efad..38042de6d 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -31,8 +31,9 @@ wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM (uBridge reaches them through a per-node runtime directory bound at /tmp — see ``VendorDockerVM._unix_socket_host_dir``). The controller allocates the IOL application ID (upper half of the id space, disjoint from IOU's) so that -linked nodes get distinct MACs; without an allocation a stable node-derived -fallback is used. +linked nodes get distinct MACs; starting a node without an allocation is an +error, not a fallback — an uncoordinated id could collide with the pool and +blackhole traffic as a MAC loop. This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker. """ @@ -84,6 +85,17 @@ class IOLDockerVM(VendorDockerVM): # The runner launches IOL with a fixed 256KB nvram (-n 256) _IOL_NVRAM_SIZE_KB = 256 + # Payload-delivered state. Deliberately NOT initialized in + # _parse_vendor_environment(): create() re-runs that parser on every + # (re)create (a stop removes the container, so every start recreates it) + # to pick up environment changes — resetting these there would lose the + # controller-allocated application id (MACs would flip to the fallback + # hash, colliding with the allocation pool) and any pending + # startup-config delivered by a PUT. + _application_id = None + _startup_config_content = None + _startup_config_dirty = False + def _parse_vendor_environment(self): super()._parse_vendor_environment() @@ -106,28 +118,17 @@ class IOLDockerVM(VendorDockerVM): except ValueError: pass - # Application ID allocated by the controller (upper half of the id - # space, disjoint from IOU's); None until the create payload carries it. - self._application_id = None - - # Startup-config content materialized by the controller from the file - # referenced by GNS3_IOL_STARTUP_CONFIG. Applied to the NVRAM at start - # time (not in the setter: the application id may not be final yet - # when the create payload is applied field by field). - self._startup_config_content = None - self._startup_config_dirty = False - @property def application_id(self) -> int: """ IOL application ID: drives interface MACs (aabb.cc{app}{iface}) and - the NVRAM file name. Falls back to a stable per-node value in the - IOL Docker half of the id space when no allocation is available - (raw compute API use, topologies created before allocation existed). + the NVRAM file name. Allocated by the controller from the IOL Docker + half of the id space (disjoint from IOU's) — there is deliberately + no fallback: an id derived any other way could silently collide with + an allocation and blackhole traffic as a MAC loop. Starting a node + without one raises (see _prepare_iol_runtime). """ - if self._application_id is None: - return 512 + int(self.id.replace("-", ""), 16) % 511 return self._application_id @application_id.setter @@ -197,7 +198,7 @@ class IOLDockerVM(VendorDockerVM): start pushes the new content with the already-updated name. """ - if not self._startup_config_dirty: + if not self._startup_config_dirty and self._application_id is not None: nvram_file = self._iol_nvram_file() if os.path.exists(nvram_file): try: @@ -302,6 +303,15 @@ class IOLDockerVM(VendorDockerVM): except DockerHttp404Error: state = "stopped" + if self._application_id is None: + raise DockerError( + f"IOL container '{self._name}' has no application ID: nodes must be " + "created through the controller (which allocates one from the pool " + "shared with IOU), or created with an explicit application_id " + "(512-1022) on the compute API. Without a coordinated ID two nodes " + "would share MACs and drop each other's frames as loops." + ) + os.makedirs(os.path.join(self.working_dir, "tmp", "run"), exist_ok=True) self._write_iol_config() diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index a5022eea2..2bfc4f4ec 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -81,6 +81,8 @@ def _make_vm(compute_project, manager, environment="GNS3_IOL_RUNNER=1", extra_volumes=extra_volumes or [], adapters=adapters, ) vm._cid = "e90e34656842" + # mirrors the controller flow, which always delivers an allocated id + vm.application_id = 700 return vm @@ -263,7 +265,7 @@ async def test_start_writes_iol_config(compute_project, manager): assert config["binary"] == "/binary.iol" assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit assert config["num-serial"] == 0 - assert config["local-app"] == 512 + int(vm.id.replace("-", ""), 16) % 511 + assert config["local-app"] == 700 assert config["remote-app"] == 1023 assert config["memory"] == 2048 assert config["user-id"] == os.getuid() @@ -271,30 +273,33 @@ async def test_start_writes_iol_config(compute_project, manager): assert vm.status == "started" -def test_local_app_is_distinct_per_node(compute_project, manager): - # IOL derives interface MACs from the app ID: two nodes sharing one would - # drop each other's frames as MAC loops, so IDs must differ per node. - vm1 = _make_vm(compute_project, manager) - vm2 = _make_vm(compute_project, manager) - assert vm1.application_id != vm2.application_id - for vm in (vm1, vm2): - assert 512 <= vm.application_id <= 1022 +@pytest.mark.asyncio +async def test_missing_application_id_is_an_actionable_error(compute_project, manager): + # There is no fallback id on purpose: an uncoordinated one could collide + # with a pool allocation and blackhole traffic as a MAC loop. Nodes come + # through the controller, which always allocates. + vm = _make_vm(compute_project, manager) + vm._application_id = None + vm._get_container_state = AsyncioMagicMock(return_value="stopped") + + with pytest.raises(DockerError, match="no application ID"): + await vm._prepare_iol_runtime() @pytest.mark.asyncio -async def test_allocated_application_id_overrides_fallback(compute_project, manager): +async def test_allocated_application_id_used(compute_project, manager): # The controller allocates the application ID (upper half of the id - # space); the node-derived hash is only a fallback without one. + # space, disjoint from IOU's); the node uses it verbatim. vm = _make_vm(compute_project, manager) - vm.application_id = 700 - assert vm.application_id == 700 + vm.application_id = 701 + assert vm.application_id == 701 _mock_start(vm) with patch("gns3server.compute.docker.Docker.install_busybox"): with asyncio_patch("gns3server.compute.docker.Docker.query"): await vm.start() with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f: config = json.load(f) - assert config["local-app"] == 700 + assert config["local-app"] == 701 @pytest.mark.asyncio @@ -734,3 +739,22 @@ def test_asdict_exposes_startup_config_content(compute_project, manager): assert vm.asdict()["startup_config_content"] is None vm.startup_config_content = "hostname RouterOne" assert vm.asdict()["startup_config_content"] == "hostname RouterOne" + + +def test_reparse_on_recreate_keeps_payload_state(compute_project, manager): + # create() re-runs _parse_vendor_environment on every (re)create (a stop + # removes the container, so every start recreates it): payload-delivered + # state must survive the re-parse. Losing the allocated application id + # would flip MACs to the fallback hash (colliding with the allocation + # pool); losing the startup-config would drop pending PUT edits. + vm = _make_vm(compute_project, manager) + vm.application_id = 700 + vm.startup_config_content = "hostname RouterOne" + + vm._parse_vendor_environment() + + assert vm.application_id == 700 + assert vm.startup_config_content == "hostname RouterOne" + assert vm._startup_config_dirty is True + # environment-derived state still re-derives + assert vm._gns3_init is False and vm._unix_socket_nio is True From e318afef868e7b94c2964e6c025c52c76b01b648 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 00:36:07 +0800 Subject: [PATCH 27/60] feat: ship an iol-xe-base.txt base config for IOL Docker nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the IOU pattern: the file lives in gns3server/configs/ and the controller installs it into the user's configs directory on startup (never overwriting a customized copy). Referenced by the documented GNS3_IOL_STARTUP_CONFIG=iol-xe-base.txt template knob — without it a fresh install had no file for the knob to resolve and nodes booted to the initial configuration dialog. Also fixes the configs directory named in the docs (~/GNS3/configs, the configs_path server setting default — not ~/.config/GNS3/...). --- docs/features/iol-runner-docker.md | 8 +++++--- gns3server/configs/iol-xe-base.txt | 16 ++++++++++++++++ tests/controller/test_controller.py | 3 +++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 gns3server/configs/iol-xe-base.txt diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 3d2c62fbe..231d6732d 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -128,9 +128,11 @@ from it boots with that configuration as its personal starting point. ``` * The file lives in the controller's configs directory (the `configs_path` - server setting, e.g. `~/.config/GNS3/3.1/configs`) — the same place IOU - and VPCS base configs live. `%h` in the content is replaced with the node - name at start. + server setting, by default `~/GNS3/configs`) — the same place IOU and + VPCS base configs live. A minimal `iol-xe-base.txt` ships with the + server and is installed there on startup (never overwriting a + user-modified copy); an absolute path in the knob bypasses the directory + entirely. `%h` in the content is replaced with the node name at start. * **Creation materializes it once**: the controller reads the file and sends its content with the node; afterwards the knob is consumed and the template file is never referenced again — editing it does not affect diff --git a/gns3server/configs/iol-xe-base.txt b/gns3server/configs/iol-xe-base.txt new file mode 100644 index 000000000..fb275da85 --- /dev/null +++ b/gns3server/configs/iol-xe-base.txt @@ -0,0 +1,16 @@ +! +service timestamps debug datetime msec +service timestamps log datetime msec +! +hostname %h +! +no ip domain lookup +! +ip tcp synwait-time 5 +! +line con 0 + exec-timeout 0 0 + privilege level 15 + logging synchronous +! +end diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index dad07581e..412a9b019 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -462,6 +462,9 @@ async def test_install_base_configs(controller, config, tmpdir): await controller._install_base_configs() assert os.path.exists(str(tmpdir / 'iou_l3_base_startup-config.txt')) + # the IOL docker base config ships with the server too (referenced by the + # GNS3_IOL_STARTUP_CONFIG knob in the documented template) + assert os.path.exists(str(tmpdir / 'iol-xe-base.txt')) # Check is the file has not been overwritten with open(str(tmpdir / 'iou_l2_base_startup-config.txt')) as f: From 1ccea916c3eec4d38125b2380d5e405543527940 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 00:46:37 +0800 Subject: [PATCH 28/60] docs: roadmap for docker image types (vendor profiles) Design proposal for an image_type discriminator on Docker templates plus a compute-side profile registry: vendor parameters graduate from environment markers into schema-gated fields, generic-feature applicability becomes declared capability data, existing markers stay as a compatibility fallback. Explicitly not a new node type. Four profiles already exist to shape it (iol-runner, XRd, SR Linux prototype, FRR appliance); implementation follows the current PR series. --- docs/README.md | 9 ++ docs/design/docker-image-type.md | 144 +++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 docs/design/docker-image-type.md diff --git a/docs/README.md b/docs/README.md index 55b7a9155..ea1581292 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,6 +39,8 @@ docs/ │ ├── statistics-api.md # Aggregated statistics API for monitoring │ ├── vnc-websocket-console.md # Browser-based VNC console via WebSocket │ └── web-wireshark-business-process.md # Web Wireshark (Docker + xpra packet capture) +├── design/ # Design proposals & roadmaps (not yet implemented) +│ └── docker-image-type.md # Docker image types: vendor profile discriminator + registry ├── gns3-copilot/ # AI Copilot feature documentation │ ├── netmiko_devices.md # Netmiko supported devices (366 types) │ ├── template-based-configuration-roadmap.md # Future: template-based config with HITL @@ -89,6 +91,13 @@ Cisco CML containerized IOL (e.g. `iol-xe/iol-xe:17-18-02`) as GNS3 Docker route --- +## Design & Roadmaps (`design/`) + +### Docker Image Types (`design/docker-image-type.md`) +Proposed `image_type` discriminator on Docker templates plus a compute-side profile registry: vendor parameters graduate from environment markers into schema-gated fields, generic-feature applicability becomes declared capability data, and the existing markers remain as a compatibility fallback. Not a new node type — vendor images are content, not mechanism. + +--- + ## GNS3 AI Copilot (`gns3-copilot/`) ### Implemented Features diff --git a/docs/design/docker-image-type.md b/docs/design/docker-image-type.md new file mode 100644 index 000000000..db00cc345 --- /dev/null +++ b/docs/design/docker-image-type.md @@ -0,0 +1,144 @@ +# Docker Image Types (Vendor Profiles) — Roadmap + +Status: **proposal, not implemented**. This is the agreed direction for the +next iteration of vendor Docker support. Four vendor profiles already +exist across branches and prototypes — iol-runner (this branch), XRd and +the SR Linux prototype (vendor/skip-init family), and the FRR registry +appliance — enough evidence to justify the registry. Implement it as a +follow-up to the current PR series, shaped by all four rather than by +iol-runner alone. + +## Problem + +All Docker nodes share one template type (`template_type: "docker"`) and +one template schema. Vendor images (iol-runner, SR Linux, XRd, …) are +selected and configured today through **environment markers** — free-form +strings parsed by the compute (`GNS3_IOL_RUNNER`, `GNS3_IOL_MEMORY`, +`GNS3_IOL_STARTUP_CONFIG`, `GNS3_SKIP_INIT`, `GNS3_UNIX_SOCKET_NIO`, …). +Three pains have already surfaced: + +1. **Invisible and unvalidated.** The markers do not exist in the API + schema: no OpenAPI documentation, no validation, typo or bad value + silently falls back to a default (`GNS3_IOL_MEMORY=notanumber` → 2048). +2. **The shared-schema dilemma.** Adding a vendor-specific field to the + Docker template schema pollutes every Docker template with a field only + one vendor reads; not adding it pushes everything into environment + strings. The `startup_config` discussion (ended in the + `GNS3_IOL_STARTUP_CONFIG` knob) is the canonical example — the tension + exists because there is no legitimate discriminator dimension. +3. **Generic-feature applicability is implicit.** Which generic Docker + features apply to which image is only documented in code comments: + `mac_address` is meaningless under unix-socket NIO, `/etc/network` + seeding is dead weight for skip-init images, `extra_configs` targets + under persisted volumes get a (correct-but-noisy) shadow warning. + +## Non-goal: new node types + +A `template_type`/`node_type` per vendor (`"iol"`, `"srlinux"`, …) is +explicitly rejected. `node_type` is a top-level concept: it drives compute +module routing (`/projects/{id}/{node_type}/nodes`), capability +reporting, GUI node types and link handling. N vendor types that are all +Docker underneath would multiply routing and schema surface for zero +mechanism — vendor images are *content*, not mechanism, and GNS3's +appliance/template system is where content belongs. + +## Design + +### One discriminator field on Docker templates + +```json +{ + "template_type": "docker", + "image_type": "iol-runner", + "image": "iol-xe/iol-xe:17-18-02" +} +``` + +`image_type` is optional; absent (or `"generic"`) keeps today's plain +`DockerVM` behavior and marker sniffing. The profiles that exist today, +mapped to their current mechanisms and archetypes: + +| Profile | Current mechanism | Archetype | +|---|---|---| +| FRR appliance | plain `DockerVM`: init.sh `/etc/network` + `start_command` (frrinit.sh), console on PID 1 | generic | +| XRd | `VendorDockerVM` skip-init + `GNS3_SHM_SIZE`/`GNS3_DEVICES`, `extra_configs`, udev masking | vendor skip-init | +| SR Linux (prototype) | `VendorDockerVM` skip-init + `docker_exec` console + `GNS3_INTERFACE_NAMES` | vendor skip-init | +| iol-runner | `IOLDockerVM`: unix-socket NIO, per-start config generation, NVRAM startup-config | iol-runner | + +Possible first values: `generic`, `iol-runner`, and a value for the +skip-init vendor NOS family once its common shape settles (XRd and SR +Linux may end up sharing it or splitting — that is exactly what the +registry should decide with all four in front of us). + +### A compute-side registry + +```python +IMAGE_PROFILES = { + "iol-runner": { + "class": IOLDockerVM, + "fields": ("iol_memory", "startup_config"), # schema-gated + "capabilities": {...}, # see below + }, + ... +} +``` + +* **Class selection** moves from environment sniffing + (`Docker._select_node_class`) to the field, with the existing markers + kept as a fallback for templates created before the field existed + (zero-migration compatibility). +* **Vendor parameters graduate into schema fields**, gated by pydantic + conditional validation (accepted — and validated — only when + `image_type` matches). This resolves the shared-schema dilemma properly: + the field exists, but only means something for its profile. The + environment knobs remain as the wire-level compatibility entry. +* **Capability declaration** makes generic-feature applicability data + instead of comments: + + | Capability | plain Docker | vendor skip-init | iol-runner | + |---|---|---|---| + | `mac_address` honored | yes | yes | **no** (IOL derives MACs from the app id) | + | `/etc/network` seeding | yes | no | no | + | `extra_configs` targets under volumes | shadowed | works (real-path binds) | works | + | console | telnet/http/… | `docker_exec` | PID 1 stdio (telnet) | + | startup config | `extra_configs` injection | image-specific | nvram build (`nvram_import`) | + + Consumers: error/warning quality (don't warn about inapplicable + features), the WebUI (vendor-aware template forms — a bonus, not a + driver), and documentation generation. + +The existing class hierarchy (`DockerVM` → `VendorDockerVM` → +`IOLDockerVM`) is unchanged — the registry only externalizes selection +and declaration. + +## Migration & compatibility + +* Old templates (markers only) keep working via the fallback; a one-time + optional converter can rewrite markers → `image_type` + fields. +* The controller-side materialization conventions + (`GNS3_IOL_STARTUP_CONFIG` file → `startup_config_content`, sent once) + carry over unchanged — the field version references the same content + pipeline. + +## Roadmap steps + +1. Add `image_type` to the Docker template schema (+ DB column + + Alembic migration — see the three-place rule for template fields) and + wire class selection through the registry, markers as fallback. +2. Move iol-runner knobs to gated fields (`iol_memory`, + `startup_config`), deprecating-but-supporting the env forms. +3. Introduce the capability table and use it to silence inapplicable + warnings (`mac_address`, `/etc/network`, extra-config shadowing). +4. Revisit per-vendor *schemas* (a sub-model per profile) only if a + profile grows more than a handful of fields — not before. + +## Open questions + +* Field name: `image_type` vs `vendor` vs `runner` — decide when the + second profile lands; the value vocabulary should name the *runtime + contract*, not the vendor. +* Should appliances (`.gns3a`) carry `image_type` explicitly, or should + installation keep deriving it from the appliance's environment block? +* Where the gated vendor fields live long-term: flat on the Docker + template (simple, gated) vs nested `{"image_type": ..., "settings": + {...}}` (cleaner, more schema churn). From e365427d1b83c36b86e352c8f402e07b5eb2535f Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 00:55:12 +0800 Subject: [PATCH 29/60] docs: spell out IOL startup-config knob consumption in the node environment --- docs/features/iol-runner-docker.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index 231d6732d..c5b9fa56d 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -136,7 +136,12 @@ from it boots with that configuration as its personal starting point. * **Creation materializes it once**: the controller reads the file and sends its content with the node; afterwards the knob is consumed and the template file is never referenced again — editing it does not affect - existing nodes. + existing nodes. The knob also disappears from the node's `environment` + (`GNS3_IOL_RUNNER=1` remains — it re-selects the node class on every + recreation), so a node whose environment shows only `GNS3_IOL_RUNNER=1` + is the normal sign that the file was found and its content delivered; + when the file is missing the knob stays in the environment (with a + server-side warning) and the lookup is retried at the next creation. * **NVRAM is the source of truth at boot** (verified on the runner): the content is built into the node's `tmp/run/nvram_` — IOL and IOU share the nvram container format, so the server-side `nvram_import` From 8892f9dc91a1fdc4aa96e08246ae05604092e513 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 01:14:40 +0800 Subject: [PATCH 30/60] docs: record IOL console typing latency characteristics --- docs/features/iol-runner-docker.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/features/iol-runner-docker.md b/docs/features/iol-runner-docker.md index c5b9fa56d..893d77493 100644 --- a/docs/features/iol-runner-docker.md +++ b/docs/features/iol-runner-docker.md @@ -177,6 +177,18 @@ diagnosing wiring issues). ## Notes and caveats +* **Console typing latency**: single keystrokes are echoed on a ~50 ms + cadence — the image services console input on an internal poll. + Measured directly on the container console (bypassing the GNS3 server + entirely, through the same Docker attach endpoint it uses): keystroke + echo is bimodal — 1–11 ms when the keystroke lands just before a poll + tick, ~50–100 ms when it just misses one — while a pasted line is + picked up as one batch (~1.4 ms) and command output streams + back-to-back (inter-frame gaps ~0.01 ms). A control container on the + same attach endpoint echoes in ~1 ms, so the GNS3 telnet path is not a + factor; this matches IOS's low-priority console-input polling and there + is nothing to fix server-side. Paste long commands instead of typing + them. * **Memory sizing**: `memory` caps the whole container; the IOL process gets `GNS3_IOL_MEMORY` (default 2048 MB). Keep container memory at IOL memory + ~512 MB headroom or the OOM-killer will shoot the router. From e12ef7f27275980bfcf14df83720f8019ed6728a Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 21:58:44 +0800 Subject: [PATCH 31/60] tests: expect port_number in adapter carrier calls Upstream #2870's carrier tests assert the two-argument _set_adapter_carrier call; the 4-port-unit commit threads port_number through every carrier call site, so single-port adapters now pass 0. --- tests/compute/docker/test_docker_vm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 71dbc2c39..6c6e109a1 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -1798,7 +1798,7 @@ async def test_adapter_add_nio_binding_sets_carrier(vm): await vm.adapter_add_nio_binding(0, nio) - vm._set_adapter_carrier.assert_called_once_with(0, True) + vm._set_adapter_carrier.assert_called_once_with(0, True, 0) @pytest.mark.asyncio @@ -1831,7 +1831,7 @@ async def test_adapter_update_nio_binding_sets_suspended_carrier(vm): await vm.adapter_update_nio_binding(0, nio) - vm._set_adapter_carrier.assert_called_once_with(0, False) + vm._set_adapter_carrier.assert_called_once_with(0, False, 0) @pytest.mark.asyncio From 27b55b72f9dbd3117d5593df6db540f3a835f1d6 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 22:19:22 +0800 Subject: [PATCH 32/60] fix: keep upstream link-carrier commands off unix-socket NIO bridges Upstream #2870 added TAP carrier control (set_nio_tap_carrier) and a busybox interface-status monitor to the Docker base class. Both assume TAP wiring: unix-socket NIO bridges carry no TAP NIO, so uBridge rejects the carrier command ('bridge has no TAP NIO') and every link create, update or delete on a running iol-runner node would fail; the monitor polls eth{adapter} interfaces that do not exist in the container's network namespace (or misreports the Docker default eth0 as adapter 0). VendorDockerVM now short-circuits _set_adapter_carrier and the interface monitor under GNS3_UNIX_SOCKET_NIO; TAP-wired vendor nodes keep the base behavior. --- gns3server/compute/docker/vendor_docker_vm.py | 32 ++++++++++ tests/compute/docker/test_iol_docker_vm.py | 61 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index d6157eea4..b156209ae 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -454,6 +454,38 @@ class VendorDockerVM(DockerVM): if nio: await self._connect_nio(adapter_number, nio, port_number) + async def _set_adapter_carrier(self, adapter_number, connected, port_number=0): + """ + Override: with GNS3_UNIX_SOCKET_NIO the bridges carry only unix and + UDP NIOs — there is no TAP, and uBridge rejects the carrier command + ("bridge has no TAP NIO"), which would fail every link create, + update or delete on a running node. The link state is the socket + pair itself. + """ + + if not self._unix_socket_nio: + await super()._set_adapter_carrier(adapter_number, connected, port_number) + + async def _start_interface_monitor(self): + """ + Override: with GNS3_UNIX_SOCKET_NIO no GNS3-managed eth interface + exists in the container's network namespace — the busybox poll would + idle forever (or misreport the Docker default eth0 as adapter 0 + status). Vendor images with TAP wiring keep the base monitor. + """ + + if not self._unix_socket_nio: + await super()._start_interface_monitor() + + async def _stop_interface_monitor(self): + """ + Override: mirror _start_interface_monitor — there is nothing to stop + when the monitor never started under unix-socket NIO. + """ + + if not self._unix_socket_nio: + await super()._stop_interface_monitor() + def _cleanup_console_resources(self): """ Override: close the docker-exec pty socket, if any, so the next diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 2bfc4f4ec..4826b00a7 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -610,6 +610,67 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage assert "add_nio_tap" not in flat +# --------------------------------------------------------------------------- +# Upstream link-carrier / interface-monitor vs unix-socket NIO +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_link_operations_never_send_tap_carrier(compute_project, manager): + """ + uBridge rejects set_nio_tap_carrier on a bridge without a TAP NIO + ("bridge has no TAP NIO"), and unix-socket NIO bridges never have one: + every link create/update/delete on a running node would fail. The + vendor override must keep the carrier command out of all three paths. + """ + + vm = _make_vm(compute_project, manager) + vm._ubridge_hypervisor = MagicMock() # truthy ubridge, like a started node + vm._ubridge_send = AsyncioMagicMock() + vm.status = "started" + nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) + + await vm.adapter_add_nio_binding(0, nio, 0) + await vm.adapter_update_nio_binding(0, nio, 0) + await vm.adapter_remove_nio_binding(0, 0) + + commands = [str(c.args[0]) for c in vm._ubridge_send.call_args_list] + assert any(c.startswith("bridge add_nio_udp") for c in commands) + assert not any("set_nio_tap_carrier" in c for c in commands) + + +@pytest.mark.asyncio +async def test_interface_monitor_disabled_under_unix_nio(compute_project, manager): + + vm = _make_vm(compute_project, manager) + vm.manager.query = AsyncioMagicMock() + + await vm._start_interface_monitor() # must be a no-op: no eth NICs to poll + await vm._stop_interface_monitor() + + vm.manager.query.assert_not_called() + assert vm._interface_monitor_task is None + + +@pytest.mark.asyncio +async def test_tap_wired_vendor_keeps_carrier_and_monitor(compute_project, manager): + """ + Vendor nodes with TAP wiring (XRd, SR Linux, ...) keep the base + behavior: the guards only apply to unix-socket NIO. + """ + + vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", + console_type="docker_exec", environment="GNS3_SKIP_INIT=1") + with patch.object(DockerVM, "_set_adapter_carrier", new=AsyncioMagicMock()) as carrier, \ + patch.object(DockerVM, "_start_interface_monitor", new=AsyncioMagicMock()) as monitor: + await vm._set_adapter_carrier(0, True) + await vm._start_interface_monitor() + # the base-class attribute is replaced by a plain mock (no descriptor + # binding), so super() calls arrive without self + carrier.assert_called_once_with(0, True, 0) + monitor.assert_called_once_with() + + # --------------------------------------------------------------------------- # Startup configuration # --------------------------------------------------------------------------- From a841175fbf67b6d3503da5bca72504be935e6b10 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 5 Sep 2026 22:52:02 +0800 Subject: [PATCH 33/60] feat(copilot): make start_gns3_node immediate-return, add wait_seconds tool The waiting variant blocked on a fixed progress bar (up to 120s) even when every start command had already failed (e.g. the uBridge >= 1.2.3 409), burying the error behind two minutes of silence. Drop it: the single start_gns3_node now sends the commands and returns per-node results immediately (GNS3StartNodeQuickTool kept as an alias). Boot waiting moves to a dedicated wait_seconds tool the agent calls explicitly between start and status checks, with a 600s ceiling and liveness logging every 5s. --- .../agent/gns3_copilot/agent/gns3_copilot.py | 7 +- .../agent/gns3_copilot/tools_v2/__init__.py | 5 +- .../gns3_copilot/tools_v2/gns3_start_node.py | 327 ++---------------- .../agent/gns3_copilot/tools_v2/gns3_wait.py | 109 ++++++ 4 files changed, 140 insertions(+), 308 deletions(-) create mode 100644 gns3server/agent/gns3_copilot/tools_v2/gns3_wait.py diff --git a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py index b5173668c..7478301f8 100644 --- a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py +++ b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py @@ -94,6 +94,7 @@ from gns3server.agent.gns3_copilot.tools_v2 import GNS3StopNodeTool from gns3server.agent.gns3_copilot.tools_v2 import GNS3SuspendNodeTool from gns3server.agent.gns3_copilot.tools_v2 import GNS3TemplateTool from gns3server.agent.gns3_copilot.tools_v2 import GNS3UpdateNodeNameTool +from gns3server.agent.gns3_copilot.tools_v2 import GNS3WaitTool from gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko import VPCSCommands from gns3server.agent.gns3_copilot.tools_v2 import PacketAnalysisTool from gns3server.agent.gns3_copilot.skills import DeviceSkillsTool @@ -113,7 +114,8 @@ TEACHING_ASSISTANT_MODE_TOOLS = [ GNS3TemplateTool(), # Get GNS3 node templates GNS3CreateNodeTool(), # Create new nodes in GNS3 GNS3LinkTool(), # Create links between nodes - GNS3StartNodeTool(), # Start GNS3 nodes + GNS3StartNodeTool(), # Start GNS3 nodes (returns immediately) + GNS3WaitTool(), # Wait for nodes to boot (pair with start_gns3_node) GNS3UpdateNodeNameTool(), # Update node name ExecuteMultipleDeviceCommands(), # Execute show/display/debug commands # (READ-ONLY) @@ -127,7 +129,8 @@ LAB_AUTOMATION_ASSISTANT_MODE_TOOLS = [ GNS3TemplateTool(), # Get GNS3 node templates GNS3CreateNodeTool(), # Create new nodes in GNS3 GNS3LinkTool(), # Create links between nodes - GNS3StartNodeTool(), # Start GNS3 nodes + GNS3StartNodeTool(), # Start GNS3 nodes (returns immediately) + GNS3WaitTool(), # Wait for nodes to boot (pair with start_gns3_node) GNS3StopNodeTool(), # Stop GNS3 nodes GNS3SuspendNodeTool(), # Suspend GNS3 nodes (preserve state) GNS3UpdateNodeNameTool(), # Update node name diff --git a/gns3server/agent/gns3_copilot/tools_v2/__init__.py b/gns3server/agent/gns3_copilot/tools_v2/__init__.py index 49957fc79..18f122727 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/__init__.py +++ b/gns3server/agent/gns3_copilot/tools_v2/__init__.py @@ -40,7 +40,8 @@ Main modules: - vpcs_tools_netmiko: VPCS device configuration tool using Netmiko - gns3_create_node: GNS3 node creation tool - gns3_create_link: GNS3 link creation tool -- gns3_start_node: GNS3 node startup tool +- gns3_start_node: GNS3 node startup tool (immediate return, no boot wait) +- gns3_wait: Wait timer tool (pair with node start to let devices boot) - gns3_get_node_temp: GNS3 template retrieval tool - gns3_update_node_name: GNS3 node name update tool - gns3_packet_filter: GNS3 packet filter management tool @@ -62,6 +63,7 @@ from .gns3_start_node import GNS3StartNodeTool from .gns3_stop_node import GNS3StopNodeTool from .gns3_suspend_node import GNS3SuspendNodeTool from .gns3_update_node_name import GNS3UpdateNodeNameTool +from .gns3_wait import GNS3WaitTool from .packet_analysis_tool import PacketAnalysisTool # Dynamic version management @@ -89,6 +91,7 @@ __all__ = [ "GNS3SuspendNodeTool", "GNS3UpdateNodeNameTool", "GNS3TemplateTool", + "GNS3WaitTool", "PacketAnalysisTool", ] diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py index 8e749d35a..e7ae2dd82 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py @@ -26,13 +26,15 @@ GNS3 node startup tool for network device activation. -Provides functionality to start one or multiple nodes in GNS3 projects -with progress tracking and status monitoring. +Sends start commands and returns immediately — it never blocks on a fixed +boot timer. A failed start command (e.g. a 409 from the compute) is +reported in the same round-trip instead of after a two-minute progress +bar. Use the wait_seconds tool between this and any status check to give +nodes time to boot. """ import json import logging -import time from pprint import pprint from typing import Any @@ -48,290 +50,15 @@ from gns3server.agent.gns3_copilot.gns3_client.api_handlers import ( # Configure logging logger = logging.getLogger(__name__) -# Node startup time configuration by device type -# Based on typical boot times for different emulators -# Conservative timing to account for slower hardware environments -NODE_STARTUP_TIME = { - "vpcs": {"base": 15, "extra_per_node": 2}, # VPCS: Very fast startup - "iou": {"base": 25, "extra_per_node": 3}, # IOU: Fast startup - "default": {"base": 120, "extra_per_node": 10}, # Other devices: Conservative time -} - - -def calculate_startup_time(nodes: list) -> int: - """ - Calculate startup wait time based on node types. - - Strategy: - - If all nodes are fast devices (VPCS/IOU): use fast startup time - - If any node is a slow device: use conservative startup time - - Args: - nodes: List of node dicts with a "node_type" key - - Returns: - Calculated wait time in seconds - """ - if not nodes: - return 60 # Default: 60 seconds for empty list - - # Get all node types - node_types = [node.get("node_type") or "default" for node in nodes] - - # Check if all nodes are fast startup devices (VPCS or IOU) - fast_types = {"vpcs", "iou"} - all_fast = all(node_type in fast_types for node_type in node_types) - - if all_fast: - # Use fast startup time: base + (count - 1) * extra_per_node - # Use the largest base time among the fast devices - max_fast_base = max( - NODE_STARTUP_TIME[nt]["base"] - for nt in node_types if nt in fast_types - ) - # Use the smallest extra_per_node among the fast devices - min_fast_extra = min( - NODE_STARTUP_TIME[nt]["extra_per_node"] - for nt in node_types if nt in fast_types - ) - total_time = max_fast_base + (len(nodes) - 1) * min_fast_extra - logger.info( - "All fast devices detected (%s), using fast startup time: %ds", - node_types, - total_time - ) - return total_time - else: - # Use conservative startup time for mixed or slow devices - config = NODE_STARTUP_TIME["default"] - total_time = config["base"] + (len(nodes) - 1) * config["extra_per_node"] - logger.info( - "Mixed or slow devices detected (%s), using conservative startup time: %ds", - node_types, - total_time - ) - return total_time - - -def show_progress_bar( - duration: int = 120, interval: int = 1, node_count: int = 1 -) -> None: - """ - Display a simple text progress bar for node startup. - - Args: - duration: Total duration of the progress bar in seconds - interval: Update interval in seconds - node_count: Number of nodes being started - """ - print(f"Starting {node_count} node(s), please wait...") - for elapsed in range(duration): - # Calculate progress percentage - progress = (elapsed + 1) / duration * 100 - - # Create progress bar display - bar_length = 30 - filled_length = int(bar_length * elapsed // duration) - progress_string = ( - "=" * filled_length + ">" + " " * (bar_length - filled_length - 1) - ) - - # Print progress bar with node count - print(f"\r[{progress_string}] {progress:.1f}%", end="", flush=True) - time.sleep(interval) - - print(f"\n{node_count} node(s) startup completed!") - class GNS3StartNodeTool(BaseTool): """ A LangChain tool to start one or multiple nodes in a GNS3 project. - **Input**: - A JSON object with project_id and node_ids (list of node IDs). - Example: - { - "project_id": "uuid-of-project", - "node_ids": ["uuid-of-node-1", "uuid-of-node-2"] - } - - **Output**: - A dictionary with all nodes' details: - { - "project_id": "...", - "total_nodes": 2, - "successful": 2, - "failed": 0, - "nodes": [ - {"node_id": "...", "name": "...", "status": "..."}, - {"node_id": "...", "name": "...", "status": "..."} - ] - } - """ - - name: str = "start_gns3_node" - description: str = """ - Starts one or multiple nodes in a GNS3 project. - Input: JSON with project_id and node_ids (list of node IDs). - Returns: A dict with all nodes' details (success/failure status). - """ - - def _run( - self, - tool_input: str, - run_manager: CallbackManagerForToolRun | None = None, - ) -> dict[str, Any]: - try: - # Parse input JSON - input_data = json.loads(tool_input) - project_id = input_data.get("project_id") - node_ids = input_data.get("node_ids") - - # Validate input - if not project_id or not node_ids: - logger.error( - "Missing required fields: project_id or node_ids." - ) - return { - "error": "Missing required fields: " - "project_id and node_ids." - } - - if not isinstance(node_ids, list): - logger.error("node_ids must be a list.") - return {"error": "node_ids must be a list."} - - # Build handler context (JWT + server URL from request context) - logger.info("Connecting to GNS3 server...") - gns3_ctx = build_gns3_ctx() - - if gns3_ctx is None: - logger.error("Failed to create GNS3 connector") - return { - "error": "Failed to connect to GNS3 server. " - "Please check your configuration." - } - - # Phase 1: fetch node info (including node_type) in one call - logger.info( - "Retrieving node info for %d nodes in project %s...", - len(node_ids), - project_id, - ) - listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) - if "error" in listing: - return {"error": listing["error"]} - nodes_by_id = {n["node_id"]: n for n in listing["nodes"]} - nodes = [nodes_by_id[nid] for nid in node_ids if nid in nodes_by_id] - for node in nodes: - logger.info( - "Node %s (%s) type: %s", - node["node_id"], - node.get("name"), - node.get("node_type"), - ) - for nid in node_ids: - if nid not in nodes_by_id: - logger.error( - "Node %s not found in project %s", nid, project_id - ) - - # Calculate startup time based on node types - wait_time = calculate_startup_time(nodes) - - # Phase 2: send start commands for all nodes (parallel batch) - logger.info( - "Sending start commands for %d nodes in project %s...", - len(nodes), - project_id, - ) - start_results = start_node_handler( - {"project_id": project_id, "node_ids": [n["node_id"] for n in nodes]}, - gns3_ctx, - ) - for r in start_results: - if r.get("status") == "error": - logger.error( - "Failed to send start command for node %s: %s", - r.get("node_id"), - r.get("error"), - ) - else: - logger.info("Start command sent for node %s", r.get("node_id")) - - # Show progress bar with calculated wait time - show_progress_bar( - duration=wait_time, interval=1, node_count=len(nodes) - ) - - # Phase 3: get final status for all nodes (one call) - results = [] - logger.info("Retrieving status for %d nodes...", len(nodes)) - listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) - if "error" in listing: - return {"error": listing["error"]} - final_by_id = {n["node_id"]: n for n in listing["nodes"]} - for node in nodes: - node_info = final_by_id.get(node["node_id"], node) - results.append( - { - "node_id": node["node_id"], - "name": node_info.get("name") or "N/A", - "status": node_info.get("status") or "unknown", - } - ) - - # Handle nodes that failed to be retrieved initially - retrieved_node_ids = {node["node_id"] for node in nodes} - for node_id in node_ids: - if node_id not in retrieved_node_ids: - results.append( - { - "node_id": node_id, - "name": "N/A", - "status": "error", - "error": "Node not found during info retrieval", - } - ) - - # Analyze results - successful_nodes = [ - r for r in results if r.get("status") != "error" - ] - failed_nodes = [r for r in results if r.get("status") == "error"] - - # Construct final response - response = { - "project_id": project_id, - "total_nodes": len(node_ids), - "successful": len(successful_nodes), - "failed": len(failed_nodes), - "nodes": results, - } - - logger.info( - "Start operation completed: %d successful, %d failed", - len(successful_nodes), - len(failed_nodes), - ) - - return response - - except json.JSONDecodeError as e: - logger.error("Invalid JSON input: %s", e) - return {"error": f"Invalid JSON input: {e}"} - except Exception as e: - logger.error("Failed to start nodes: %s", e) - return {"error": f"Failed to start nodes: {str(e)}"} - - -class GNS3StartNodeQuickTool(BaseTool): - """ - A LangChain tool to start nodes in a GNS3 project WITHOUT waiting. - - This tool sends start commands to all nodes and immediately returns status, - without blocking for startup completion. Suitable for automated deployment - workflows where long waits would cause HTTP timeouts. + Sends the start commands in a parallel batch and returns each node's + status immediately — nodes keep booting in the background. It does NOT + wait for boot completion: follow up with the wait_seconds tool and a + status/topology check to confirm nodes actually came up. **Input**: A JSON object with project_id and node_ids (list of node IDs). @@ -350,19 +77,20 @@ class GNS3StartNodeQuickTool(BaseTool): "failed": 0, "nodes": [ {"node_id": "...", "name": "...", "status": "started"}, - {"node_id": "...", "name": "...", "status": "started"} + {"node_id": "...", "name": "...", "status": "error", "error": "..."} ], "note": "Start commands sent. Nodes are booting in background." } """ - name: str = "start_gns3_node_quick" + name: str = "start_gns3_node" description: str = """ - Starts nodes in a GNS3 project WITHOUT waiting for startup completion. - Use this for automated deployments to avoid HTTP timeouts. + Starts one or multiple nodes in a GNS3 project and returns immediately + (nodes boot in the background; start failures are reported right away). + After calling this, use wait_seconds (VPCS/IOU ~15-30s, IOS routers + ~60-120s, heavy NOS images 2-5min) before checking node status. Input: JSON with project_id and node_ids (list of node IDs). - Returns: Dict with nodes' details after start commands are sent. - NOTE: Nodes will continue booting in background after this tool returns. + Returns: Dict with per-node start command results. """ def _run( @@ -490,7 +218,7 @@ class GNS3StartNodeQuickTool(BaseTool): "nodes": results, "note": ( "Start commands sent. Nodes are booting in background. " - "Check node status later." + "Use wait_seconds, then check node status." ), } @@ -510,6 +238,11 @@ class GNS3StartNodeQuickTool(BaseTool): return {"error": f"Failed to start nodes: {str(e)}"} +# Backward-compat alias: the waiting variant was removed; both names now +# point at the immediate-return tool. +GNS3StartNodeQuickTool = GNS3StartNodeTool + + if __name__ == "__main__": # Test with single node print("=== Testing single node startup ===") @@ -524,19 +257,3 @@ if __name__ == "__main__": tool = GNS3StartNodeTool() result_single = tool._run(test_input_single) pprint(result_single) - - # Test with multiple nodes - print("\n=== Testing multiple nodes startup ===") - test_input_multiple = json.dumps( - { - "project_id": "", # Replace with actual project UUID - "node_ids": [ - "fbeda109-9a74-4d8c-a749-cc3847911a90", - # Replace with actual node UUIDs - "another-node-uuid-here", - "third-node-uuid-here", - ], - } - ) - result_multiple = tool._run(test_input_multiple) - pprint(result_multiple) diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_wait.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_wait.py new file mode 100644 index 000000000..d31e9e6d2 --- /dev/null +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_wait.py @@ -0,0 +1,109 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# GNS3-Copilot - AI-powered Network Lab Assistant for GNS3 +# +# This file is part of GNS3-Copilot project. +# +# GNS3-Copilot is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# GNS3-Copilot is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNS3-Copilot. If not, see . +# +# Copyright (C) 2025 Yue Guobin (岳国宾) +# Author: Yue Guobin (岳国宾) +# +# Project Home: https://github.com/yueguobin/gns3-copilot +# +""" + +GNS3-Copilot wait tool. + +start_gns3_node returns as soon as the start commands are accepted — nodes +keep booting in the background. This tool gives the agent a deliberate +pause it controls itself (instead of a hard-coded progress bar inside the +start tool), so the usual flow is: start_gns3_node → wait_seconds → check +node status / run show commands. +""" + +import json +import logging +import time +from typing import Any + +from langchain.tools import BaseTool +from langchain_core.callbacks import CallbackManagerForToolRun + +# Configure logging +logger = logging.getLogger(__name__) + +# Hard ceiling so a hallucinated "wait 999999" cannot wedge the agent loop. +MAX_WAIT_SECONDS = 600 +# Log a liveness line every few seconds so long waits are visible in the +# server log. +LOGBOOK_TICK = 5 + + +class GNS3WaitTool(BaseTool): + """ + A LangChain tool that sleeps for a given number of seconds. + + **Input**: + A JSON object with seconds (integer, 1-600). + Example: + {"seconds": 30} + + **Output**: + {"waited": 30} + """ + + name: str = "wait_seconds" + description: str = """ + Pause execution for a given number of seconds (1-600), then continue. + Use after start_gns3_node (which returns immediately) to let nodes + boot before checking status: VPCS/IOU ~15-30s, IOS/IOL routers + ~60-120s, heavy NOS images (XRd, SR Linux) 2-5min. Prefer several + short waits with a status check in between over one long blind wait. + Input: JSON with seconds (integer). + Returns: {"waited": }. + """ + + def _run( + self, + tool_input: str, + run_manager: CallbackManagerForToolRun | None = None, + ) -> dict[str, Any]: + try: + input_data = json.loads(tool_input) + seconds = input_data.get("seconds") + if isinstance(seconds, str) and seconds.strip().isdigit(): + seconds = int(seconds.strip()) + if not isinstance(seconds, int) or isinstance(seconds, bool): + return {"error": "seconds must be an integer (1-600)."} + if not 1 <= seconds <= MAX_WAIT_SECONDS: + return { + "error": f"seconds must be between 1 and {MAX_WAIT_SECONDS}." + } + + logger.info("Waiting %d seconds...", seconds) + waited = 0 + while waited < seconds: + tick = min(LOGBOOK_TICK, seconds - waited) + time.sleep(tick) + waited += tick + logger.info("Waited %d/%d seconds", waited, seconds) + return {"waited": waited} + + except json.JSONDecodeError as e: + logger.error("Invalid JSON input: %s", e) + return {"error": f"Invalid JSON input: {e}"} + except Exception as e: + logger.error("Wait tool failed: %s", e) + return {"error": f"Wait tool failed: {str(e)}"} From a8c96dd3f72ddae29c41bb0e1ef0545a000b0b51 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sun, 6 Sep 2026 00:15:43 +0800 Subject: [PATCH 34/60] fix: keep port_number in Docker NIO dispatch of the batch endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The project-open bulk path (_add_nio_binding / _get_existing_nio / _update_nio_binding in routes/compute/projects.py) dropped port_number for Docker nodes, unlike the per-node routes and the IOU branch. With multi-port docker adapters (iol-runner nodes model 4 ports per adapter, 0ca9ccc63) every batched NIO landed on port 0 where Adapter.add_nio() silently overwrites — the last entry per node won — so reopening a project clobbered the port-0 links with the port-1 NIOs: links ended up cross-wired between the wrong node pairs and the real links died (IOL direct-link ping failures after close/reopen, EXCESSCOLL storms from the phantom loops). --- gns3server/api/routes/compute/projects.py | 18 ++++++++++--- tests/api/routes/compute/test_projects.py | 31 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/gns3server/api/routes/compute/projects.py b/gns3server/api/routes/compute/projects.py index 7da980a08..54d7e460e 100644 --- a/gns3server/api/routes/compute/projects.py +++ b/gns3server/api/routes/compute/projects.py @@ -144,8 +144,14 @@ async def _add_nio_binding(node, adapter_number, port_number, nio): manager_name = type(node.manager).__name__ # Adapter-based nodes: docker / qemu / vmware / virtualbox take - # (adapter_number, nio); iou additionally takes port_number. - if manager_name in ("Docker", "Qemu", "VMware", "VirtualBox"): + # (adapter_number, nio); iou additionally takes port_number. Docker + # adapters can be multi-port (e.g. iol-runner nodes model 4 ports per + # adapter): dropping port_number would bind every NIO to port 0, where + # add_nio() silently overwrites — the last entry per node wins and links + # end up cross-wired (observed as dead direct links after reopen). + if manager_name == "Docker": + await node.adapter_add_nio_binding(adapter_number, nio, port_number) + elif manager_name in ("Qemu", "VMware", "VirtualBox"): await node.adapter_add_nio_binding(adapter_number, nio) elif manager_name == "IOU": await node.adapter_add_nio_binding(adapter_number, port_number, nio) @@ -176,7 +182,9 @@ def _get_existing_nio(node, adapter_number, port_number): """ manager_name = type(node.manager).__name__ - if manager_name in ("Docker", "Qemu", "VMware", "VirtualBox"): + if manager_name == "Docker": + return node.get_nio(adapter_number, port_number) + elif manager_name in ("Qemu", "VMware", "VirtualBox"): return node.get_nio(adapter_number) elif manager_name == "IOU": return node.get_nio(adapter_number, port_number) @@ -207,7 +215,9 @@ async def _update_nio_binding(node, adapter_number, port_number, nio): """ manager_name = type(node.manager).__name__ - if manager_name in ("Docker", "Qemu", "VMware", "VirtualBox"): + if manager_name == "Docker": + await node.adapter_update_nio_binding(adapter_number, nio, port_number) + elif manager_name in ("Qemu", "VMware", "VirtualBox"): await node.adapter_update_nio_binding(adapter_number, nio) elif manager_name == "IOU": await node.adapter_update_nio_binding(adapter_number, port_number, nio) diff --git a/tests/api/routes/compute/test_projects.py b/tests/api/routes/compute/test_projects.py index 949c0d891..6131db742 100644 --- a/tests/api/routes/compute/test_projects.py +++ b/tests/api/routes/compute/test_projects.py @@ -315,6 +315,37 @@ class TestBatchNIOEdgeCases: await _add_nio_binding(node, 1, 2, nio) node.adapter_add_nio_binding.assert_called_once_with(1, 2, nio) + @pytest.mark.asyncio + async def test_docker_dispatch_keeps_port_number(self): + """ + Docker adapters can be multi-port (iol-runner nodes model 4 ports per + adapter). Dropping port_number binds every NIO to port 0 where + add_nio() silently overwrites — reopened projects then end up with + cross-wired links (the last entry per node wins). + """ + from unittest.mock import AsyncMock, MagicMock + from gns3server.api.routes.compute.projects import ( + _add_nio_binding, + _get_existing_nio, + _update_nio_binding, + ) + + node = MagicMock() + type(node.manager).__name__ = "Docker" + node.adapter_add_nio_binding = AsyncMock() + node.adapter_update_nio_binding = AsyncMock() + nio = MagicMock() + node.get_nio = MagicMock(return_value=nio) + + await _add_nio_binding(node, 0, 1, nio) + node.adapter_add_nio_binding.assert_called_once_with(0, nio, 1) + + assert _get_existing_nio(node, 0, 1) is nio + node.get_nio.assert_called_once_with(0, 1) + + await _update_nio_binding(node, 0, 1, nio) + node.adapter_update_nio_binding.assert_called_once_with(0, nio, 1) + @pytest.mark.asyncio async def test_vpcs_dispatch_to_port_add_nio_binding(self): """_add_nio_binding dispatches VPCS to port_add_nio_binding.""" From 0c540abbf295e8746c66564eb9f9c3625c273ea5 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 8 Sep 2026 00:04:04 +0800 Subject: [PATCH 35/60] fix: stop leaking a global os.kill mock from the shutdown route test The bare 'os.kill = MagicMock()' was never restored, so every later test in the same process ran with a no-op kill. Any test that kills a child process and waits for it then hangs forever (resident sharkd sessions waiting on an immortal process). Use monkeypatch so the patch is undone. --- tests/api/routes/controller/test_controller.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/api/routes/controller/test_controller.py b/tests/api/routes/controller/test_controller.py index ddd33aa8e..43d672302 100644 --- a/tests/api/routes/controller/test_controller.py +++ b/tests/api/routes/controller/test_controller.py @@ -29,13 +29,18 @@ pytestmark = pytest.mark.asyncio class TestControllerRoutes: - async def test_shutdown_local(self, app: FastAPI, client: AsyncClient, config: Config) -> None: - - os.kill = MagicMock() + async def test_shutdown_local(self, app: FastAPI, client: AsyncClient, config: Config, monkeypatch) -> None: + + # monkeypatch (not bare assignment): a global `os.kill = MagicMock()` + # is never restored and poisons every later test that kills a + # subprocess — resident sharkd sessions would wait() forever on an + # immortal process. + kill_mock = MagicMock() + monkeypatch.setattr(os, "kill", kill_mock) config.settings.Server.local = True response = await client.post(app.url_path_for("shutdown")) assert response.status_code == status.HTTP_204_NO_CONTENT - assert os.kill.called + assert kill_mock.called async def test_shutdown_non_local(self, app: FastAPI, client: AsyncClient, config: Config) -> None: From 790c423c262dc6d5760bbdb310d4a3beddd08a6f Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 8 Sep 2026 00:04:04 +0800 Subject: [PATCH 36/60] feat: drive marker replay with resident sharkd sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sharkd (the Wireshark daemon) is now the single decode engine — the tshark/PDML path is gone, and without sharkd every replay endpoint returns 501 (no degraded mode: one engine, one rendering shape for the Web UI). - Frame entries gain packet-list columns from sharkd's frames RPC: src/dst/proto/info plus the Wireshark coloring hints bg/fg - range and frames accept ?filter=, applied before counting and slicing; invalid expressions are 400 carrying sharkd's original text; filters travel as single argv-style elements, capped at 2000 chars; filtered frames keep their original pcap frame numbers - frame detail returns sharkd's protocol tree with keys renamed into the REST contract (element/label/name/filter_expr/pos+size/expert/ generated/children): a census-verified closed key set, values untouched, unknown keys passed through verbatim, Wireshark-internal hf ids dropped. filter_expr gives the UI click-to-filter; pos/size drives hex highlighting (hex still read straight from the pcap) - one resident 'sharkd -' session per source pcap: lazy spawn, /tmp scratch copy + scratch HOME (hardened profiles), per-request (mtime,size) validation with respawn, LRU bound, per-session lock, per-RPC timeout, bounded close Timeline backbone (gate, record-header scan, merge ordering, canonical ts strings, hex reads) stays plain Python — identity and ordering never depend on the engine. --- docs/features/marker-tag-replay.md | 226 ++++---- gns3server/api/routes/controller/projects.py | 65 ++- gns3server/controller/marker_replay.py | 500 +++++++++++++----- .../routes/controller/test_marker_replay.py | 192 +++++-- tests/controller/test_marker_replay.py | 436 ++++++++++----- 5 files changed, 1018 insertions(+), 401 deletions(-) diff --git a/docs/features/marker-tag-replay.md b/docs/features/marker-tag-replay.md index 4d148a5c8..6b9c16a92 100644 --- a/docs/features/marker-tag-replay.md +++ b/docs/features/marker-tag-replay.md @@ -12,14 +12,18 @@ See LICENSE file for licensing information. Replays traffic captured by [markers](marker-traffic-insight.md) **across links**, keyed by `tag`. Markers on different links that share a tag form one *distributed capture session*; once every marker under the tag is paused, their per-marker pcaps are merged into a single -timestamp-ordered timeline. The Web UI browses that timeline and fetches individual frames -on demand — each fetch decodes exactly one frame via `tshark` into a self-describing JSON -protocol tree. +timestamp-ordered timeline. The Web UI browses that timeline (with Wireshark-style packet +list columns) and fetches individual frames on demand — each fetch decodes exactly one +frame via the resident **sharkd** daemon into a self-describing JSON protocol tree. The unique observable: the delta between the same packet hitting two consecutive links measures the **intermediate node's forwarding latency** (host view) — something a single-link capture can never show. +**sharkd is a hard requirement** (part of the Wireshark package). Without it every replay +endpoint returns 501 — there is deliberately no degraded mode; one engine, one rendering +shape for the Web UI. + ## Architecture ```mermaid @@ -28,35 +32,38 @@ graph TB subgraph Controller["Controller (replay endpoints)"] GATE["Tag gate
(all markers under tag paused?)"] - SCAN["Timeline scan
(pcap record headers)"] - MAP["PDML → JSON
isomorphic mapper"] + SCAN["Timeline backbone
(pcap record-header scan,
merge ordering, hex reads)"] + SESS["sharkd sessions
(one per source pcap)"] end FS[("markers dir
{node}_{link}_{marker}.pcap")] - TS["tshark -T pdml
(one frame at a time)"] - TMP["/tmp scratch copy
(hardened-profile workaround)"] + TMP["/tmp scratch copies
(hardened-profile workaround)"] + SK["sharkd -
(resident JSON-RPC on stdio)"] - UI -->|"GET range / frames"| GATE + UI -->|"GET range / frames [?filter=]"| GATE GATE --> SCAN - SCAN --> FS - UI -->|"GET frame detail (lazy)"| MAP - MAP --> TMP - TMP --> TS - MAP -->|"hex: raw bytes"| FS + SCAN -->|"columns / filter matches"| SESS + SESS --> TMP --> SK + SCAN -->|"hex: raw bytes"| FS + UI -->|"GET frame detail (lazy)"| SESS ``` -Two deliberately separated performance regimes: +Backbone vs engine, deliberately separated: -| Path | Work | tshark | -|------|------|--------| -| Timeline | 16-byte record-header scan per frame, cross-file merge sort | **never invoked** — browsing works without tshark | -| Frame detail | locate frame → `tshark -T pdml -Y "frame.number == N"` → map to JSON | forked once per frame the user opens | +- **Timeline backbone** (plain Python): the tag gate, 16-byte-per-frame pcap record-header + scan, cross-source merge ordering, canonical ts strings, and raw-bytes reads for the hex + view. Identity and ordering never depend on the engine. +- **Engine layer (sharkd)**: packet-list columns, display filters, and per-frame protocol + trees. One resident `sharkd -` process per source pcap (sharkd loads one file at a + time), spawned lazily on first use, addressed with one-line JSON-RPC on stdio. -tshark call count equals user clicks — no caching or rate limiting needed, and a tshark -failure (501/502) affects only that one frame, never timeline browsing. pcap files are -compute-side (`/project-files/markers/`); the initial scope is single-server -deployments (controller and compute in one process, direct file access) — remote computes -will reuse the existing capture-file proxy pattern. +Session lifecycle: each session is validated per request against the source pcap's +`(mtime, size)` — a mismatch (e.g. the capture node restarted and uBridge truncated the +pcap while paused) kills and respawns it. Sessions are LRU-bounded (8), each RPC has a +timeout and is serialized by a per-session lock (sharkd serves one request at a time). +sharkd reads a `/tmp` scratch copy of the pcap with a scratch `HOME` — hardened profiles +(AppArmor &c.) deny it the project directory and the user's home even though the server +process can read both. ## Business Process @@ -64,29 +71,22 @@ will reuse the existing capture-file proxy pattern. sequenceDiagram participant UI as Web UI participant C as Controller - participant PC as markers dir (pcaps) - participant TS as tshark + participant SK as sharkd session - Note over UI,PC: ① configure — same tag on every link's marker - UI->>C: POST markers (bpf, tag=666) on each link + Note over UI: ① configure — same tag on every link's marker + Note over C: ② capture — uBridge appends matches, replay forbidden (409) + Note over UI: ③ pause every marker under the tag - Note over PC: ② capture — uBridge appends matches (flushed per packet), replay forbidden - UI->>C: GET range - C--xC: 409 (a marker under tag 666 is still enabled) - - Note over UI: ③ pause — every marker under the tag - UI->>C: PUT markers {"enabled": false} × each - - Note over UI,TS: ④ replay - UI->>C: GET /markers/tags/666/replay/range - C->>PC: scan record headers, merge sort - C-->>UI: {start, end, sources, frames} - UI->>C: GET frames?ts=T&window_ms=W - C-->>UI: frames in [T, T+W] — or {"frames": []} + Note over UI,SK: ④ replay + UI->>C: GET /markers/tags/666/replay/range[?filter=…] + C->>C: gate → scan record headers → merge order + C->>SK: frames {filter, skip, limit} → columns + matches + C-->>UI: {start, end, sources, frames[] with src/dst/proto/info/bg/fg} + UI->>C: GET frames?ts=T&window_ms=W (paging — {"frames": []} on a miss) UI->>C: GET frame/detail?ts=…&node_id=…&link_id=…&marker=… - C->>PC: locate frame, read raw bytes (hex) - C->>TS: -T pdml (reads the /tmp copy) - TS-->>C: PDML + C->>C: hex straight from the pcap + C->>SK: frame {frame: N, proto: true} + SK-->>C: tree (keys renamed to the REST contract) C-->>UI: protocol tree + hex ``` @@ -110,25 +110,22 @@ markers at all → 404. - **Pause → resume → pause is fine.** The pcap accumulates the full history; replay covers everything up to the current pause point. - **The replay window ends when nodes restart.** A pcap's lifetime equals its uBridge's - lifetime: a fresh uBridge reinstalls every desired marker — paused ones too (install - first, then turn the filter off) — and uBridge opens the pcap with truncate semantics - (`pcap_dump_open`, not `_append`). Server restart + project reopen **without starting - nodes** is safe: nothing touches the files until a uBridge comes up (verified live). - Docker nodes effectively restart on server restart as well (stale-container cleanup), - so their window is shorter still. -- uBridge flushes every matched packet to the pcap immediately (`pcap_dump_flush` per - packet under a mutex — verified in the uBridge source), so a pause boundary never loses - tail frames. + lifetime: a fresh uBridge reinstalls every desired marker — paused ones too — and + uBridge opens the pcap with truncate semantics (`pcap_dump_open`, not `_append`). Server + restart + project reopen **without starting nodes** is safe: nothing touches the files + until a uBridge comes up. Docker nodes effectively restart on server restart as well + (stale-container cleanup), so their window is shorter still. ## API Endpoints -All read-only; JWT bearer token, privilege `Project.Audit`. +All read-only; JWT bearer token, privilege `Project.Audit`. All require sharkd — 501 +without it. | Method | Path | Description | |--------|------|-------------| -| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/range` | Timeline metadata + full frame list for the tag | -| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frames?ts=&window_ms=&limit=` | Frames with ts in `[T, T+window]`, merged across sources | -| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frame/detail?ts=&node_id=&link_id=&marker=` | Single frame: tshark protocol tree + raw hex | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/range[?filter=]` | Timeline metadata + full merged frame list with packet-list columns | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frames?ts=&window_ms=&limit=[&filter=]` | Frames with ts in `[T, T+window]`, merged across sources | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frame/detail?ts=&node_id=&link_id=&marker=` | Single frame: protocol tree + raw hex (lazy — one call per frame the user opens) | ### `range` — the timeline @@ -144,62 +141,95 @@ All read-only; JWT bearer token, privilege `Project.Audit`. "data_link_type": "DLT_EN10MB", "count": 10 } ], "frames": [ - { "ts": "1788196663.226372", "len": 98, "node_id": "b764c434…", - "link_id": "316ef8fd…", "marker": "global-def-…", "frame_number": 1 } + { "ts": "1788196663.226372", "len": 98, + "node_id": "b764c434…", "link_id": "316ef8fd…", + "marker": "global-def-…", "frame_number": 1, + "src": "10.1.10.101", "dst": "203.0.113.1", + "proto": "ICMP", "info": "Echo (ping) request id=0x6ed5, seq=1/0, ttl=64", + "bg": "ffffff", "fg": "000000" } ] } ``` -- `frames` is the **full, merged, time-ordered list** (cap 5000) — the Web UI lays out the - whole timeline from one request. Over the cap, `frames` is omitted and per-second - `buckets` are returned instead with `truncated: true`. +- `frames` is the **full, merged, time-ordered list** (cap 5000) — one request lays out + the whole timeline. Over the cap, `frames` is omitted and per-second `buckets` are + returned instead with `truncated: true` (check with `'frames' in response`, not null). +- Every frame entry carries the Wireshark packet-list columns — `src` / `dst` / `proto` + / `info` — plus the **coloring-rule hints `bg` / `fg`** (Wireshark's own palette + decisions, so the UI can color rows exactly like Wireshark without shipping the + colorization engine). Columns are `null` only for a frame the engine could not describe. - Each frame entry carries `(node_id, link_id, marker, frame_number)` — the locating - tuple for the detail request. + tuple for the detail request, and the link association for timeline/topology rendering + (`link_id` joins the Web UI's own link objects). -### `frames` — point / window query +### Display filter -A time with no frames is a normal, successful answer — an empty array, no sentinel strings: +`?filter=` on both `range` and `frames` is a Wireshark display filter, +applied **before** counting and slicing — `start` / `end` / `frame_count` / +`frames` | `buckets` are all computed on the matching frames only. Filtered frames keep +their original pcap frame numbers. The filter travels as one argv-style element (never +through a shell) and is capped at 2000 characters. An invalid expression is a **400** +whose message carries sharkd's original error text — suitable for inline display in the +filter bar, and distinct from the 409 gate / 404 unknown-tag semantics. + +### `frames` — point / window query (paging) + +A time with no frames is a normal, successful answer — an empty array, no sentinel +strings: ```json GET …/replay/frames?ts=1788196700.000&window_ms=500 → { "frames": [] } ``` +Paging is deliberately **ts + window_ms only** (no offset/limit over the filtered set): +the merge spans multiple pcaps, so slicing happens server-side on the merged stream +either way, windows align with timeline semantics, and the gate freezes the data (the +window answer is deterministic). + ### `frame/detail` — lazy single-frame decode -Invoked only when the user opens a frame. The `ts` must be the **exact string received in -the timeline/frame list** (round-tripped verbatim — never re-serialized through a float); -`node_id + link_id + marker` identify the pcap. The server re-resolves the ts against the -file, guarding against a capture rebuilt between the timeline view and this click. +Invoked only when the user opens a frame. The `ts` must be the **exact string received +in the timeline/frame list** (round-tripped verbatim — never re-serialized through a +float); `node_id + link_id + marker` identify the pcap. The server re-resolves the ts +against the file, guarding against a capture rebuilt between the timeline view and this +click. ```json { "ts": "1788196663.226372", "source": { "node_id": "b764c434…", "link_id": "316ef8fd…", "marker": "global-def-…", "frame_number": 1 }, - "tshark_version": "TShark (Wireshark) 4.6.7 …", "field_count": 85, - "hex": "45000062…", + "hex": "00005e00010a…", "tree": [ - { "element": "proto", "name": "ip", - "showname": "Internet Protocol Version 4, Src: 10.1.10.101, Dst: 203.0.113.1", - "children": [ - { "element": "field", "name": "ip.ttl", "show": "64", - "showname": "Time to Live: 64", "value": "40", "size": "1", - "pos": "22", "children": [] } + { "element": "proto", "label": "Internet Protocol Version 4, …", "children": [ + { "element": "field", "name": "ip.ttl", "label": "Time to Live: 64", + "filter_expr": "ip.ttl == 64", "pos": 22, "size": 1, "children": [] } ] } ] } ``` -- `tree` mirrors PDML **isomorphically**: every ``/`` becomes a node, every - XML attribute (`name`, `show`, `showname`, `value`, `size`, `pos`, `hide`, `mask`, - `unmaskedvalue`) becomes a JSON key, plus one structural key `element` (proto/field). - Nothing is selected out, nothing interpreted, and **all values stay strings** — numeric - conversion is the client's business. -- `hex` is the raw frame bytes read straight from the pcap (not via tshark); keeping - `pos`/`size` on every field enables Wireshark-style *click field → highlight bytes*. -- `field_count` is the mapped node count — a client-side sanity check. +The tree is sharkd's protocol tree with **keys renamed into the REST contract** — a +closed, protocol-independent key set (census-verified across ICMP / TCP / VLAN+OSPF +trees and pinned by a test), with values untouched: + +| sharkd | Contract key | Meaning | +|--------|--------------|---------| +| `t` | `element` | node type (`proto`, …) | +| `l` | `label` | display text | +| `fn` | `name` | field name (`ip.ttl`) | +| `f` | `filter_expr` | **ready-made display filter with the value baked in** — click-to-filter | +| `h` | `pos` + `size` | byte range — click field → highlight hex bytes | +| `s` | `expert` | expert severity name (`Chat` / `Warn` / …) | +| `g` | `generated` | generated-by-Wireshark flag | +| `n` | `children` | nested fields | +| `e` | *(dropped)* | Wireshark-internal hf id, unstable across versions | + +Unknown keys from a newer Wireshark pass through verbatim (never silently dropped); a +census test flags new keys for naming. `hex` is the raw frame bytes read straight from +the pcap; `field_count` is the mapped node count (client-side sanity check). ## Ordering and timestamps @@ -209,45 +239,33 @@ file, guarding against a capture rebuilt between the timeline view and this clic environment. - The sort key is `(ts, source file, frame_number)` — ts alone is **not** unique (two links can hit the same microsecond); the tiebreaker yields a stable, determined order - instead of a fictional one. Index structures must never use ts as a dict key, or - same-microsecond frames silently overwrite each other. + instead of a fictional one. - The cross-link delta is the intermediate node's end-to-end forwarding latency (veth/TAP → guest protocol stack → back to host), typically hundreds of microseconds to milliseconds. UI labels should read *node forwarding latency (host view)*, not link propagation delay. A live capture pair confirmed it end-to-end: same `ip.id`, TTL 64→63, 509 µs between two links. -## Fidelity guarantee (PDML → JSON) - -The conversion is an isomorphic structure map, not a semantic transform, with two rules: -**map every attribute** and **keep values as strings**. A round-trip test enforces both: -PDML element count equals JSON node count, and every XML attribute survives with an -identical JSON value (`tests/controller/test_marker_replay.py`). The raw frame bytes — -the one thing PDML genuinely does not contain — are covered by `hex` read directly from -the pcap. - ## Error Responses | Status | Description | |--------|-------------| +| 400 | Invalid display filter (message carries sharkd's original text) or filter longer than 2000 chars | | 401 | Not authenticated | | 404 | Tag has no markers in the project; detail source unknown, or ts does not match the file (the capture may have been rebuilt) | | 409 | Tag gate: a marker under the tag is still `enabled: true` (the response lists them) | -| 501 | tshark not installed / unavailable — affects detail only; the timeline never needs tshark | -| 502 | tshark failed or timed out (10 s); truncated output never reaches the mapper | +| 501 | sharkd not installed / unavailable — replay is unavailable, no degraded mode | +| 502 | sharkd failed or timed out (10 s per RPC) | ## Notes - **Heterogeneous link types coexist.** Frames are never merged into a single pcap (mergecap is deliberately not used) — each frame carries its source and is decoded individually, so Ethernet and serial (cHDLC/PPP) markers can share one timeline. - Malformed packets are tshark's problem: it emits `[Malformed Packet]` as regular PDML - and carries on. -- **Hardened tshark profiles.** openSUSE-style profiles (AppArmor &c.) can deny tshark - access to the project directory and the user's home even though the server process can - read both. The detail path therefore copies the pcap to a real file under `/tmp` - (not a symlink — the profile resolves real paths) and gives tshark a scratch `HOME`; - the copy is unlinked afterwards. The hex view still reads the original file. + Malformed packets are dissected like any other; sharkd marks them in the tree. +- **Session invalidation is cheap and total.** Every request stats the source pcap; a + rewritten file (mtime/size change) respawns the session — a paused-but-restarted + capture can never serve stale dissect state. - **Tag type.** REST and the `marker.match` WS event both carry `tag` as `int` (the listener normalizes); replay keys on that int value. - **Follow-ups.** Remote-compute support via the existing capture-file proxy pattern; diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index 0595bde7b..e1e4872cf 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -45,7 +45,7 @@ from gns3server.controller.controller_error import ControllerError, ControllerBa from gns3server.controller.import_project import import_project as import_controller_project from gns3server.controller.export_project import export_project as export_controller_project from gns3server.controller import marker_replay -from gns3server.controller.marker_replay import TsharkError, TsharkMissingError +from gns3server.controller.marker_replay import SharkdError, SharkdMissingError from gns3server.utils.asyncio import aiozipstream from gns3server.utils.path import is_safe_path from gns3server.db.repositories.templates import TemplatesRepository @@ -221,36 +221,62 @@ def get_project_markers(project: Project = Depends(dep_project)) -> dict: return project.markers +async def _replay_response(awaitable): + """Shared engine-error mapping for the replay endpoints: 501 when sharkd + (the hard engine requirement) is unavailable, 502 when it fails. Data + state errors (409 gate / 404 unknown tag) and filter errors (400) map + through the global handlers before this.""" + + try: + return await awaitable + except SharkdMissingError as e: + raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(e)) + except SharkdError as e: + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e)) + + @router.get( "/{project_id}/markers/tags/{tag}/replay/range", dependencies=[Depends(has_privilege("Project.Audit"))], ) -def replay_tag_range(tag: int, project: Project = Depends(dep_project)) -> dict: +async def replay_tag_range( + tag: int, + filter: Optional[str] = None, + project: Project = Depends(dep_project), +) -> dict: """ Aggregate replay timeline for a tag: merges the pcap of every marker - carrying ``tag`` into one timestamp-ordered view (design reference: - ``marker_replay`` module docstring). + carrying ``tag`` into one timestamp-ordered view. Every frame entry + carries Wireshark-style columns (``src`` / ``dst`` / ``proto`` / ``info`` + plus coloring hints ``bg`` / ``fg``). The tag gate applies: every marker under the tag must be paused (``enabled: false``) — 409 otherwise. The response carries the timeline bounds, per-source stats, and the full merged frame list while under the frame cap (5000); above it the list is replaced by per-second buckets. + ``filter`` is an optional Wireshark display filter applied **before** + counting and slicing — start / end / frame_count / frames | buckets are + all computed on the matching frames only. An invalid expression is a 400 + carrying sharkd's original error text (for inline display in the UI + filter bar). Requires sharkd — 501 without it. + Required privilege: Project.Audit """ - return marker_replay.build_timeline(project, tag) + return await _replay_response(marker_replay.build_timeline(project, tag, filter_expr=filter)) @router.get( "/{project_id}/markers/tags/{tag}/replay/frames", dependencies=[Depends(has_privilege("Project.Audit"))], ) -def replay_tag_frames( +async def replay_tag_frames( tag: int, ts: str, window_ms: int = 100, limit: int = 1000, + filter: Optional[str] = None, project: Project = Depends(dep_project), ) -> dict: """ @@ -259,12 +285,16 @@ def replay_tag_frames( ``{"frames": []}``. The tag gate applies (409 while any marker captures). ``ts`` must be the exact string returned by the range response — never - re-serialize it through a float. + re-serialize it through a float. ``filter`` (optional display filter) has + the same semantics as on the range endpoint. Requires sharkd — 501 + without it. Required privilege: Project.Audit """ - return marker_replay.query_frames(project, tag, ts, window_ms=window_ms, limit=limit) + return await _replay_response( + marker_replay.query_frames(project, tag, ts, window_ms=window_ms, limit=limit, filter_expr=filter) + ) @router.get( @@ -282,24 +312,21 @@ async def replay_tag_frame_detail( """ Decode exactly one frame (lazy — invoked when the user opens a frame, never by the timeline itself): raw bytes for the hex view read straight - from the pcap, protocol tree from ``tshark -T pdml`` mapped isomorphically - to JSON (every PDML attribute survives, values stay strings). + from the pcap, protocol tree from the resident sharkd session with keys + renamed into the REST contract (``element`` / ``label`` / ``name`` / + ``filter_expr`` / ``pos`` + ``size`` / ``expert`` / ``generated`` / + ``children``) — values untouched. ``ts`` must be the exact string from the frame list; ``node_id`` + ``link_id`` + ``marker`` identify the source pcap. The tag gate applies. + Requires sharkd — 501 without it. Required privilege: Project.Audit """ - try: - return await marker_replay.decode_frame(project, tag, ts, node_id, link_id, marker) - except TsharkMissingError: - raise HTTPException( - status_code=status.HTTP_501_NOT_IMPLEMENTED, - detail="tshark is not installed on this server — frame detail is unavailable", - ) - except TsharkError as e: - raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e)) + return await _replay_response( + marker_replay.decode_frame(project, tag, ts, node_id, link_id, marker) + ) # --------------------------------------------------------------------------- diff --git a/gns3server/controller/marker_replay.py b/gns3server/controller/marker_replay.py index 993bb31f8..13d3aba2f 100644 --- a/gns3server/controller/marker_replay.py +++ b/gns3server/controller/marker_replay.py @@ -16,37 +16,46 @@ # along with this program. If not, see . """ -Tag-keyed aggregate replay over paused markers' pcap files. +Tag-keyed aggregate replay over paused markers' pcaps, powered by sharkd. Markers on different links sharing a ``tag`` form one distributed capture session. This module merges their per-marker pcaps (``/project-files/markers/{node_id}_{link_id}_{name}.pcap``) into a -single timestamp-ordered timeline and decodes individual frames on demand. +single timestamp-ordered timeline and decodes frames on demand. -Two deliberately separated performance regimes: +Engine: **sharkd is a hard requirement** (the Wireshark resident daemon, +driven over one-line JSON-RPC on stdin/stdout). Without it every replay +endpoint returns 501 — there is deliberately no degraded mode. Timeline +assembly (gate, pcap record-header scan, merge ordering, hex reads) is plain +Python, but it is an implementation detail, not an availability promise. -* the timeline path reads only the 16-byte pcap record headers — tshark is - never invoked, so browsing works even where tshark is not installed; -* the detail path runs one ``tshark -T pdml`` per frame the caller asks about - (call count = user clicks) and maps the XML to JSON isomorphically — every - PDML attribute survives as a JSON key, values stay strings, nothing is - selected out or interpreted. +Session model — sharkd loads one file at a time, so there is one resident +session **per source pcap**: spawned lazily on first use, fed a ``/tmp`` +scratch copy (hardened profiles deny sharkd the project directory; a real +copy, not a symlink, since the profile resolves real paths) with a scratch +``HOME``, validated per request against the original's ``(mtime, size)`` — +a mismatch (e.g. the capture node restarted and uBridge truncated the pcap +while paused) kills and respawns the session. A per-pcap asyncio lock +serializes RPCs (sharkd serves one request at a time), each with a timeout. +An LRU cap bounds concurrent sessions. Timestamps are uBridge's userspace ``gettimeofday`` at match time (µs, a value measured after the packet has crossed the kernel twice — the last digit or two are scheduling noise). A timestamp is NOT a unique key: the merge sorts by ``(ts, source file, frame number)`` and index structures must never use ts alone as a dict key, or same-microsecond frames silently -overwrite each other. +overwrite each other. The canonical ts strings travel to clients verbatim +and must be round-tripped verbatim. """ import asyncio +import json import logging import os import shutil import struct import tempfile -import xml.etree.ElementTree as ET +import time from .controller_error import ControllerError, ControllerNotFoundError, ControllerBadRequestError @@ -57,20 +66,45 @@ log = logging.getLogger(__name__) # client is never flooded by a high-traffic BPF. FRAME_LIST_CAP = 5000 -# One tshark decode per user click: a generous ceiling, not a rate limiter. -TSHARK_TIMEOUT_SECONDS = 10.0 +# One JSON-RPC per sharkd session, serialized by a per-session lock. +RPC_TIMEOUT_SECONDS = 10.0 + +# Resident sharkd sessions are bounded; least-recently-used evicted first. +SESSION_MAX = 8 + +# Display filters travel as one argv element (never through a shell) and are +# capped to keep absurd expressions off the command line. +FILTER_MAX_LENGTH = 2000 + +# Batch size when draining sharkd's `frames` RPC (columns / filter matches). +_FRAMES_PAGE = 1000 -class TsharkMissingError(ControllerError): - """tshark is not installed (or not on PATH) — frame detail unavailable.""" +class SharkdMissingError(ControllerError): + """sharkd is not installed (or not on PATH) — replay is unavailable (501).""" -class TsharkError(ControllerError): - """tshark exited non-zero / timed out / produced unusable output.""" +class SharkdError(ControllerError): + """sharkd failed, timed out, or produced an unusable response (502).""" + + +class FilterError(ControllerBadRequestError): + """sharkd rejected the display filter — its message is carried verbatim + so the UI can show it inline in the filter bar (400, distinct from the + 409 gate / 404 unknown-tag semantics).""" + + +class _SharkdRpcError(Exception): + """Internal: a JSON-RPC error object from sharkd (code + message).""" + + def __init__(self, code, message): + super().__init__(message) + self.code = code + self.message = message # --------------------------------------------------------------------------- -# pcap record-header scanning (timeline path — no tshark) +# pcap record-header scanning (timeline backbone — engine-free) # --------------------------------------------------------------------------- # magic → (byte order, timestamp unit). Both pcap families uBridge can write @@ -140,8 +174,8 @@ def scan_pcap_frames(path): def read_frame_bytes(path, frame_number): """ Read one frame's raw bytes (hex view) straight from the pcap — never via - tshark. ``frame_number`` is 1-based (the same number tshark's - ``frame.number`` filter uses). + the engine. ``frame_number`` is 1-based (the same number sharkd's frame + RPC uses). """ frames = scan_pcap_frames(path) @@ -156,6 +190,273 @@ def read_frame_bytes(path, frame_number): return f.read(incl_len).hex() +# --------------------------------------------------------------------------- +# sharkd: process environment and scratch copies +# --------------------------------------------------------------------------- + +def _scratch_copy(pcap): + """ + Copy the pcap to a scratch file under the system temp dir for sharkd to + load. Hardened profiles (AppArmor &c.) can deny it access to the project + directory / the user's home while still allowing /tmp — a real copy, + deliberately not a symlink, since the profile resolves real paths. + Caller must unlink the returned path. + """ + + fd, scratch = tempfile.mkstemp(suffix=".pcap", prefix="gns3-replay-") + os.close(fd) + shutil.copyfile(pcap, scratch) + return scratch + + +def _engine_env(): + """Scratch HOME so sharkd never even tries to read the user's home.""" + + env = dict(os.environ) + env["HOME"] = tempfile.gettempdir() + return env + + +# --------------------------------------------------------------------------- +# sharkd: tree key renaming (the only transformation between sharkd and the +# REST contract — a closed, protocol-independent key set; values untouched) +# --------------------------------------------------------------------------- + +# Census-verified across ICMP / TCP / VLAN+OSPF trees: sharkd emits exactly +# these structural keys on every node regardless of protocol. Protocol +# semantics live in VALUES (field names, labels, filter expressions), which +# are never touched. +_KEY_RENAME = { + "t": "element", # node type ("proto", …) + "l": "label", # display text + "fn": "name", # field name (e.g. "ip.ttl") + "f": "filter_expr", # ready-made display filter with the value baked in + "s": "expert", # expert severity name ("Chat", "Warn", …) + "g": "generated", # generated-by-wireshark flag + "n": "children", # nested fields +} +# "h" → pos + size (byte range for hex highlighting) — handled specially. +# "e" is sharkd's internal header-field registry id — unstable across +# Wireshark versions and useless for rendering, so it is dropped. +_DROPPED_KEYS = {"e"} + + +def _rename_value(value): + """Recursive pass-through: rename known keys, drop none but 'e', + copy unknown keys verbatim (a future Wireshark adding a key never + silently loses data — the census test flags it for naming).""" + + if isinstance(value, dict): + out = {} + for key, item in value.items(): + if key in _DROPPED_KEYS: + continue + if key == "h" and isinstance(item, list) and len(item) == 2: + out["pos"], out["size"] = item + else: + out[_KEY_RENAME.get(key, key)] = _rename_value(item) + return out + if isinstance(value, list): + return [_rename_value(item) for item in value] + return value + + +def _count_tree_nodes(value): + if isinstance(value, dict): + return 1 + sum(_count_tree_nodes(item) for item in value.values()) + if isinstance(value, list): + return sum(_count_tree_nodes(item) for item in value) + return 0 + + +# --------------------------------------------------------------------------- +# sharkd sessions (one resident process per source pcap) +# --------------------------------------------------------------------------- + +class _SharkdSession: + """A resident `sharkd -` process with one pcap loaded, addressed through + line-oriented JSON-RPC. Serialized by an asyncio lock (sharkd serves one + request at a time).""" + + def __init__(self, pcap, scratch, proc, stat): + self.pcap = pcap + self.scratch = scratch + self.proc = proc + self.mtime_ns = stat.st_mtime_ns + self.size = stat.st_size + self.last_used = time.monotonic() + self.lock = asyncio.Lock() + self._next_id = 0 + + def matches(self, stat): + """True while the source pcap is byte-identical to what was loaded — + a fresh (mtime, size) would serve a rebuilt/truncated capture.""" + + return stat.st_mtime_ns == self.mtime_ns and stat.st_size == self.size + + def alive(self): + return self.proc.returncode is None + + def touch(self): + self.last_used = time.monotonic() + + async def rpc(self, method, params): + async with self.lock: + self._next_id += 1 + request = {"jsonrpc": "2.0", "id": self._next_id, "method": method, "params": params} + try: + self.proc.stdin.write((json.dumps(request) + "\n").encode()) + await self.proc.stdin.drain() + raw = await asyncio.wait_for(self.proc.stdout.readline(), RPC_TIMEOUT_SECONDS) + except asyncio.TimeoutError: + raise SharkdError(f"sharkd timed out after {RPC_TIMEOUT_SECONDS:.0f}s on {method!r}") + except OSError as e: + raise SharkdError(f"sharkd session died on {method!r}: {e}") + if not raw: + raise SharkdError(f"sharkd closed the session during {method!r}") + try: + response = json.loads(raw) + except ValueError as e: + raise SharkdError(f"Malformed sharkd response: {e}") + if "error" in response: + error = response["error"] + raise _SharkdRpcError(error.get("code"), str(error.get("message", ""))) + return response.get("result") + + async def close(self): + try: + if self.proc.returncode is None: + self.proc.kill() + # Bounded wait: a kill that fails to reap (blocked signals, + # mocked os.kill in tests, a wedged process) must never hang + # the caller — leak the process with a log instead. + try: + await asyncio.wait_for(self.proc.wait(), timeout=2.0) + except asyncio.TimeoutError: + log.warning("sharkd session for %s did not exit after kill", self.pcap) + except ProcessLookupError: + pass + try: + os.unlink(self.scratch) + except OSError: + pass + + +class _SharkdManager: + """Resident sharkd sessions keyed by source pcap path, LRU-bounded.""" + + def __init__(self): + self._sessions = {} + + async def session_for(self, pcap): + if shutil.which("sharkd") is None: + raise SharkdMissingError( + "sharkd is not available on this server — marker replay requires sharkd " + "(part of the Wireshark package)" + ) + stat = os.stat(pcap) + session = self._sessions.get(pcap) + if session is not None and session.matches(stat) and session.alive(): + session.touch() + return session + if session is not None: + await session.close() + del self._sessions[pcap] + # Bound resident sessions: evict the least recently used. + while len(self._sessions) >= SESSION_MAX: + victim = min(self._sessions.values(), key=lambda s: s.last_used) + await victim.close() + self._sessions.pop(victim.pcap, None) + session = await self._spawn(pcap, stat) + self._sessions[pcap] = session + return session + + async def _spawn(self, pcap, stat): + scratch = _scratch_copy(pcap) + try: + proc = await asyncio.create_subprocess_exec( + "sharkd", "-", + stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL, env=_engine_env(), + ) + except OSError as e: + try: + os.unlink(scratch) + except OSError: + pass + raise SharkdError(f"Could not run sharkd: {e}") + session = _SharkdSession(pcap, scratch, proc, stat) + try: + await session.rpc("load", {"file": scratch}) + except Exception as e: + await session.close() + raise SharkdError(f"sharkd failed to load {os.path.basename(pcap)}: {e}") + return session + + async def close_all(self): + for session in list(self._sessions.values()): + await session.close() + self._sessions.clear() + + +_manager = None + + +def _get_manager(): + global _manager + if _manager is None: + _manager = _SharkdManager() + return _manager + + +# --------------------------------------------------------------------------- +# Columns + display filter (sharkd `frames` RPC) +# --------------------------------------------------------------------------- + +async def _columns_for(pcap, filter_expr): + """ + One resident `frames` pass over the source pcap. Returns + ``{frame_number: {src, dst, proto, info, bg, fg}}``; with a + ``filter_expr`` the keys are exactly the matching frame numbers, so one + pass both filters and enriches the merge. + """ + + session = await _get_manager().session_for(pcap) + columns = {} + skip = 0 + while True: + # sharkd rejects skip=0 ("must be a positive integer") — only send it + # once there is actually something to skip. + params = {"limit": _FRAMES_PAGE} + if skip: + params["skip"] = skip + if filter_expr is not None: + params["filter"] = filter_expr + try: + rows = await session.rpc("frames", params) + except _SharkdRpcError as e: + if filter_expr is not None: + raise FilterError(f"Invalid display filter: {e.message}") + raise SharkdError(f"sharkd frames failed on {os.path.basename(pcap)}: {e.message}") + for row in rows: + try: + frame_number = int(row.get("num")) + except (TypeError, ValueError): + continue + cols = row.get("c") or [] + columns[frame_number] = { + "src": cols[2] or None if len(cols) > 2 else None, + "dst": cols[3] or None if len(cols) > 3 else None, + "proto": cols[4] or None if len(cols) > 4 else None, + "info": cols[6] or None if len(cols) > 6 else None, + "bg": row.get("bg"), + "fg": row.get("fg"), + } + if len(rows) < _FRAMES_PAGE: + return columns + skip += len(rows) + + # --------------------------------------------------------------------------- # Tag gate + timeline assembly # --------------------------------------------------------------------------- @@ -199,12 +500,14 @@ def gate_tag(project, tag): return entries -def _merged_frames(project, entries): +async def _merged_frames(project, entries, filter_expr=None): """ - Scan every source pcap and merge into one list sorted by + Scan every source pcap's record headers, ask sharkd for columns (and, + with a filter, the matching set), and merge into one list sorted by ``(ts, source file, frame number)`` — ts alone is not unique (two links can hit the same microsecond); the tiebreaker yields a stable, determined - order instead of a fictional one. + order instead of a fictional one. With a filter, only frames sharkd + matched survive, keeping their original pcap frame numbers. """ markers_dir = project.markers_directory @@ -215,19 +518,35 @@ def _merged_frames(project, entries): markers_dir, f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}.pcap" ) frames = scan_pcap_frames(pcap) if os.path.exists(pcap) else [] - sources.append({**{k: entry[k] for k in ("node_id", "link_id", "marker", "data_link_type")}, - "count": len(frames)}) + if frames: + columns = await _columns_for(pcap, filter_expr) + else: + columns = {} + source_key = f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}" + count = 0 for frame_number, (sec, usec, incl_len) in enumerate(frames, start=1): + if filter_expr is not None and frame_number not in columns: + continue + cols = columns.get(frame_number, {}) merged.append({ "ts": _format_ts(sec, usec), "ts_us": sec * 1_000_000 + usec, + "_source": source_key, "len": incl_len, "node_id": entry["node_id"], "link_id": entry["link_id"], "marker": entry["marker"], "frame_number": frame_number, - "_source": f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}", + "src": cols.get("src"), + "dst": cols.get("dst"), + "proto": cols.get("proto"), + "info": cols.get("info"), + "bg": cols.get("bg"), + "fg": cols.get("fg"), }) + count += 1 + sources.append({**{k: entry[k] for k in ("node_id", "link_id", "marker", "data_link_type")}, + "count": count}) merged.sort(key=lambda f: (f["ts_us"], f["_source"], f["frame_number"])) for frame in merged: del frame["ts_us"] @@ -235,15 +554,24 @@ def _merged_frames(project, entries): return merged, sources -def build_timeline(project, tag, frame_cap=FRAME_LIST_CAP): +def _validate_filter(filter_expr): + if filter_expr is not None and len(filter_expr) > FILTER_MAX_LENGTH: + raise ControllerBadRequestError( + f"Display filter too long (max {FILTER_MAX_LENGTH} characters)" + ) + + +async def build_timeline(project, tag, frame_cap=FRAME_LIST_CAP, filter_expr=None): """ The ``range`` response: timeline bounds, per-source stats, and (under ``frame_cap``) the full merged frame list for one-request timeline - layout. Over the cap the list is replaced by per-second buckets. + layout. Over the cap the list is replaced by per-second buckets. With a + ``filter_expr`` every figure is computed on the matching frames only. """ + _validate_filter(filter_expr) entries = gate_tag(project, tag) - frames, sources = _merged_frames(project, entries) + frames, sources = await _merged_frames(project, entries, filter_expr) response = { "tag": tag, @@ -267,14 +595,15 @@ def build_timeline(project, tag, frame_cap=FRAME_LIST_CAP): return response -def query_frames(project, tag, ts, window_ms=100, limit=1000): +async def query_frames(project, tag, ts, window_ms=100, limit=1000, filter_expr=None): """ Frames with ts in ``[T, T+window_ms]`` merged across sources. A time with no frames is a normal, successful answer — ``{"frames": []}``. """ + _validate_filter(filter_expr) entries = gate_tag(project, tag) - frames, _sources = _merged_frames(project, entries) + frames, _sources = await _merged_frames(project, entries, filter_expr) start_us = _parse_ts(ts) end_us = start_us + max(window_ms, 0) * 1000 @@ -283,70 +612,16 @@ def query_frames(project, tag, ts, window_ms=100, limit=1000): # --------------------------------------------------------------------------- -# Frame detail (tshark path — lazy, one frame per call) +# Frame detail (lazy — one frame per call, via the resident session) # --------------------------------------------------------------------------- -async def _tshark_version(): - try: - proc = await asyncio.create_subprocess_exec( - "tshark", "--version", - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, - ) - stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=TSHARK_TIMEOUT_SECONDS) - return stdout.decode(errors="replace").splitlines()[0].strip() - except (OSError, asyncio.TimeoutError, IndexError): - raise TsharkMissingError("tshark is not available on this server") - - -def _tshark_scratch_copy(pcap): - """ - Copy the pcap to a scratch file under the system temp dir for tshark to - read. Hardened tshark profiles (AppArmor &c.) can deny it access to the - project directory / the user's home while still allowing /tmp — a real - copy, deliberately not a symlink, since the profile resolves real paths. - Caller must unlink the returned path. - """ - - fd, scratch = tempfile.mkstemp(suffix=".pcap", prefix="gns3-replay-") - os.close(fd) - shutil.copyfile(pcap, scratch) - return scratch - - -def _tshark_env(): - """Scratch HOME so tshark never even tries to read the user's home.""" - - env = dict(os.environ) - env["HOME"] = tempfile.gettempdir() - return env - - -def _pdml_to_nodes(element): - """ - Isomorphic PDML → JSON mapping: every XML attribute becomes a JSON key - verbatim (values stay strings), children nest under ``children``. The - element tag ("proto"/"field") is carried as ``element`` — the one - structural key beyond the attributes, so a renderer can tell a protocol - group from a leaf field (geninfo's tagless names make names unreliable). - """ - - return { - "element": element.tag, - **element.attrib, - "children": [_pdml_to_nodes(child) for child in element], - } - - -def _count_nodes(nodes): - return 1 + sum(_count_nodes(child) for child in nodes.get("children", [])) - - async def decode_frame(project, tag, ts, node_id, link_id, marker): """ Decode exactly one frame: locate its pcap by source identity, verify the round-tripped ts still matches the file (guards a rebuild between the - timeline view and this click), read the raw bytes for the hex view, and - map tshark's PDML of that single frame to JSON. + timeline view and this click), read the raw bytes for the hex view + straight from the pcap, and rename the sharkd protocol tree into the + REST contract (closed key set, values untouched). """ entries = gate_tag(project, tag) @@ -378,51 +653,22 @@ async def decode_frame(project, tag, ts, node_id, link_id, marker): ) raw_hex = read_frame_bytes(pcap, frame_number) - - if shutil.which("tshark") is None: - raise TsharkMissingError("tshark is not installed — frame detail is unavailable") - version = await _tshark_version() - - # Hand tshark a scratch copy under the temp dir: hardened profiles may - # deny it the project directory even though this process can read it - # (the hex view above reads the original directly). - scratch = _tshark_scratch_copy(pcap) + session = await _get_manager().session_for(pcap) try: - try: - proc = await asyncio.create_subprocess_exec( - "tshark", "-r", scratch, "-T", "pdml", "-Y", f"frame.number == {frame_number}", - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - env=_tshark_env(), + result = await session.rpc("frame", {"frame": frame_number, "proto": True}) + except _SharkdRpcError as e: + if e.code == -8003: # frame number out of range — file changed under us + raise ControllerNotFoundError( + f"No frame at ts {ts} in marker '{marker}' (the capture may have been rebuilt)" ) - stdout, stderr = await asyncio.wait_for( - proc.communicate(), timeout=TSHARK_TIMEOUT_SECONDS - ) - except OSError as e: - raise TsharkError(f"Could not run tshark: {e}") - except asyncio.TimeoutError: - raise TsharkError(f"tshark timed out after {TSHARK_TIMEOUT_SECONDS:.0f}s") - if proc.returncode != 0 or not stdout.strip(): - # Never feed truncated/failed output to the mapper. - raise TsharkError(f"tshark failed: {stderr.decode(errors='replace').strip()[:500]}") - finally: - try: - os.unlink(scratch) - except OSError: - pass + raise SharkdError(f"sharkd frame failed: {e.message}") - try: - root = ET.fromstring(stdout) - except ET.ParseError as e: - raise TsharkError(f"Malformed PDML from tshark: {e}") - - packet = root.find("./packet") - tree = [_pdml_to_nodes(child) for child in packet] if packet is not None else [] + tree = _rename_value(result.get("tree", [])) return { "ts": ts, "source": {"node_id": node_id, "link_id": link_id, "marker": marker, "frame_number": frame_number}, - "tshark_version": version, - "field_count": sum(_count_nodes(node) for node in tree), + "field_count": _count_tree_nodes(tree), "hex": raw_hex, "tree": tree, } diff --git a/tests/api/routes/controller/test_marker_replay.py b/tests/api/routes/controller/test_marker_replay.py index 9a19e291d..2629ec110 100644 --- a/tests/api/routes/controller/test_marker_replay.py +++ b/tests/api/routes/controller/test_marker_replay.py @@ -16,27 +16,39 @@ # along with this program. If not, see . """ -HTTP-route tests for the tag replay endpoints: the tag gate (409 while any -marker captures, 404 unknown tag), the merged timeline, window queries -(empty window = success), and the lazy frame detail (ts guard, isomorphic -JSON, 501 when tshark is unavailable). +HTTP-route tests for the tag replay endpoints (sharkd edition): the tag gate +(409 while any marker captures, 404 unknown tag), the merged timeline with +columns, window queries (empty window = success), the display-filter +parameter (400 with sharkd's error text on a bad expression), the lazy frame +detail with the renamed sharkd tree, and 501 when sharkd is unavailable +(hard engine requirement, no degraded mode). """ import shutil from unittest.mock import patch import pytest +import pytest_asyncio from fastapi import FastAPI, status from httpx import AsyncClient from gns3server.controller.project import Project from gns3server.controller.udp_link import UDPLink +from gns3server.controller import marker_replay -from tests.controller.test_marker_replay import _write_pcap, _icmp_frame +from tests.controller.test_marker_replay import _write_pcap, _icmp_frame, _tcp_syn_frame, _cols pytestmark = pytest.mark.asyncio -tshark_present = pytest.mark.skipif(shutil.which("tshark") is None, reason="tshark not installed") +sharkd_present = pytest.mark.skipif(shutil.which("sharkd") is None, reason="sharkd not installed") + + +@pytest_asyncio.fixture +async def no_residual_sessions(): + yield + manager = marker_replay._manager + if manager is not None: + await manager.close_all() def _add_marker(project, tag, enabled, node_id, frames=None): @@ -77,18 +89,48 @@ class TestReplayRoutes: ) assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_range_merges_sources_in_ts_order(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + async def test_range_501_without_sharkd(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + + # A non-empty pcap: the engine must be consulted, and without sharkd + # the whole feature is unavailable (hard requirement, no degraded mode). + _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + ]) + + with patch("gns3server.controller.marker_replay.shutil.which", return_value=None): + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7) + ) + assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED + # The app's HTTPException handler unifies the body as {"message": …}. + assert "sharkd" in response.json()["message"] + + async def test_range_merges_sources_in_ts_order( + self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch + ) -> None: + + # Columns injected hermetically — the merge/order/tiebreak contract + # must not depend on the engine being installed. + r1, r2 = UDPLink(project), UDPLink(project) + project._links.update({r1.id: r1, r2.id: r2}) + + def _wire(link, node_id, frames): + link._markers["icmp"] = {"bpf": "icmp", "tag": 7, "enabled": False, "color": None, + "highlight_duration": None, "capture_node_id": node_id, + "direction": None, "data_link_type": "DLT_EN10MB"} + _write_pcap(f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames) # r1→r2 captures at t1 and t3; r2→r3 captures at t2 and t3 (same µs # as source A's t3 — the tiebreak must keep both frames). - _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 500000, b"a" * 60), - (1693472002, 0, b"a" * 60), - ]) - _add_marker(project, tag=7, enabled=False, node_id="n2", frames=[ - (1693472001, 0, b"b" * 60), - (1693472002, 0, b"b" * 60), - ]) + _wire(r1, "n1", [(1693472000, 500000, b"a" * 60), (1693472002, 0, b"a" * 60)]) + _wire(r2, "n2", [(1693472001, 0, b"b" * 60), (1693472002, 0, b"b" * 60)]) + + async def fake_columns(pcap, filter_expr): + name = pcap.rsplit("/", 1)[-1] + src = "10.0.0.1" if name.startswith("n1") else "10.0.0.2" + return {1: _cols(src=src), 2: _cols(src=src)} + + monkeypatch.setattr(marker_replay, "_columns_for", fake_columns) response = await client.get( app.url_path_for("replay_tag_range", project_id=project.id, tag=7) @@ -105,16 +147,25 @@ class TestReplayRoutes: "1693472000.500000", "1693472001.000000", "1693472002.000000", "1693472002.000000", ] + # Wireshark-style columns ride along on every frame entry. + assert body["frames"][0]["src"] == "10.0.0.1" + assert body["frames"][1]["src"] == "10.0.0.2" + assert body["frames"][0]["proto"] == "ICMP" assert len(body["sources"]) == 2 async def test_frames_window_miss_is_empty_success( - self, app: FastAPI, client: AsyncClient, project: Project + self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch ) -> None: _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ (1693472000, 0, b"a" * 60), ]) + async def fake_columns(pcap, filter_expr): + return {1: _cols()} + + monkeypatch.setattr(marker_replay, "_columns_for", fake_columns) + response = await client.get( app.url_path_for("replay_tag_frames", project_id=project.id, tag=7), params={"ts": "1693472001.000000", "window_ms": 100}, @@ -122,13 +173,20 @@ class TestReplayRoutes: assert response.status_code == status.HTTP_200_OK assert response.json() == {"frames": []} - async def test_frames_window_hit(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + async def test_frames_window_hit( + self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch + ) -> None: _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ (1693472000, 0, b"a" * 60), (1693472000, 150000, b"a" * 60), ]) + async def fake_columns(pcap, filter_expr): + return {1: _cols(), 2: _cols()} + + monkeypatch.setattr(marker_replay, "_columns_for", fake_columns) + response = await client.get( app.url_path_for("replay_tag_frames", project_id=project.id, tag=7), params={"ts": "1693472000.000000", "window_ms": 150}, @@ -151,22 +209,64 @@ class TestReplayRoutes: assert response.status_code == status.HTTP_404_NOT_FOUND assert "rebuilt" in response.json()["message"] - async def test_detail_501_without_tshark(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + @sharkd_present + async def test_range_columns_and_filter_end_to_end( + self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions + ) -> None: - link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + (1693472001, 0, _tcp_syn_frame()), + ]) + + # Unfiltered: both frames with real engine columns. + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7) + ) + assert response.status_code == status.HTTP_200_OK + frames = response.json()["frames"] + assert [f["proto"] for f in frames] == ["ICMP", "TCP"] + assert frames[0]["src"] == "10.0.0.1" and frames[0]["dst"] == "10.0.0.3" + assert "Echo" in frames[0]["info"] + assert frames[0]["frame_number"] == 1 + + # Display filter applied before counting: only the TCP frame survives. + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7), + params={"filter": "tcp"}, + ) + body = response.json() + assert body["frame_count"] == 1 + assert [f["frame_number"] for f in body["frames"]] == [2] # original pcap identity + assert body["start"] == "1693472001.000000" + + @sharkd_present + async def test_range_invalid_filter_is_400_with_sharkd_text( + self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions + ) -> None: + + _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ (1693472000, 123456, _icmp_frame()), ]) - with patch("gns3server.controller.marker_replay.shutil.which", return_value=None): - response = await client.get( - app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), - params={"ts": "1693472000.123456", "node_id": "n1", - "link_id": link.id, "marker": "icmp"}, - ) - assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7), + params={"filter": "this is (not valid"}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "filter" in response.json()["message"].lower() - @tshark_present - async def test_detail_decodes_single_frame(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + # Oversized filters are rejected before reaching the engine. + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7), + params={"filter": "x" * (marker_replay.FILTER_MAX_LENGTH + 1)}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @sharkd_present + async def test_detail_decodes_single_frame( + self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions + ) -> None: link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ (1693472000, 123456, _icmp_frame()), @@ -182,9 +282,35 @@ class TestReplayRoutes: assert body["source"]["frame_number"] == 1 assert body["hex"] == _icmp_frame().hex() assert body["field_count"] > 10 - assert "tshark" in body["tshark_version"].lower() - ip = next(p for p in body["tree"] if p.get("name") == "ip") - ttl = next(f for f in ip["children"] if f.get("name") == "ip.ttl") - # Values arrive as strings, exactly as tshark emitted them. - assert ttl["show"] == "64" and ttl["showname"] == "Time to Live: 64" + def find(node, name): + stack = node if isinstance(node, list) else [node] + for child in stack: + if child.get("name") == name: + return child + deep = find(child.get("children", []), name) + if deep is not None: + return deep + return None + + ttl = find(body["tree"], "ip.ttl") + assert ttl["label"] == "Time to Live: 64" + assert ttl["filter_expr"] == "ip.ttl == 64" + assert ttl["pos"] == 22 and ttl["size"] == 1 + + @sharkd_present + async def test_detail_501_when_sharkd_disappears( + self, app: FastAPI, client: AsyncClient, project: Project + ) -> None: + + link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + ]) + + with patch("gns3server.controller.marker_replay.shutil.which", return_value=None): + response = await client.get( + app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), + params={"ts": "1693472000.123456", "node_id": "n1", + "link_id": link.id, "marker": "icmp"}, + ) + assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED diff --git a/tests/controller/test_marker_replay.py b/tests/controller/test_marker_replay.py index e33fb9a8c..748c72d15 100644 --- a/tests/controller/test_marker_replay.py +++ b/tests/controller/test_marker_replay.py @@ -16,36 +16,47 @@ # along with this program. If not, see . """ -Unit tests for the tag-keyed aggregate replay module (controller layer): +Unit tests for the tag-keyed aggregate replay module (sharkd edition): * pcap record-header scanning (ts extraction, truncated-tail tolerance, nanosecond-magic normalization) and raw-bytes reads for the hex view -* the tag gate (404 unknown tag, 409 while any marker captures) -* timeline assembly: cross-source merge ordered by (ts, source, frame number), - same-microsecond tiebreak, missing-pcap sources, frame-cap degradation to - per-second buckets -* window queries (inclusive bounds, empty-window success) -* the lazy frame detail: round-tripped-ts guard, and — where tshark is - installed — the isomorphic PDML → JSON mapping with a round-trip check - (element count and attribute coverage). +* the tag gate (404 unknown tag, 409 while any marker captures) — engine-free +* timeline assembly with injected columns: cross-source merge ordered by + (ts, source, frame number), same-microsecond tiebreak, frame-cap + degradation to buckets, display-filter application before count/slice +* the tree key renaming (closed census key set, values untouched, unknown + keys pass through verbatim, internal hf ids dropped) +* the resident sharkd sessions — spawn/load/reuse, (mtime, size) + invalidation, LRU bound — against the real sharkd where installed, plus + the frame detail end-to-end (hex + renamed tree + filter expressions). """ import os import shutil import struct +from types import SimpleNamespace +from unittest.mock import patch import pytest -from types import SimpleNamespace -from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError +from gns3server.controller.controller_error import ( + ControllerBadRequestError, + ControllerError, + ControllerNotFoundError, +) +from gns3server.controller import marker_replay from gns3server.controller.marker_replay import ( + FilterError, + SharkdMissingError, build_timeline, decode_frame, query_frames, read_frame_bytes, scan_pcap_frames, + _count_tree_nodes, _format_ts, _parse_ts, + _rename_value, ) pytestmark = pytest.mark.asyncio @@ -53,6 +64,8 @@ pytestmark = pytest.mark.asyncio PCAP_MAGIC_US = 0xA1B2C3D4 PCAP_MAGIC_NS = 0xA1B23C4D +sharkd_present = pytest.mark.skipif(shutil.which("sharkd") is None, reason="sharkd not installed") + def _write_pcap(path, frames, magic=PCAP_MAGIC_US, snaplen=65535): """frames: list of (sec, frac, payload bytes); frac is µs (or ns for the ns magic).""" @@ -64,27 +77,39 @@ def _write_pcap(path, frames, magic=PCAP_MAGIC_US, snaplen=65535): f.write(payload) +def _cksum(data): + if len(data) % 2: + data = data + b"\x00" # RFC 1071 odd-length padding + s = 0 + for i in range(0, len(data), 2): + s += (data[i] << 8) + data[i + 1] + while s >> 16: + s = (s & 0xFFFF) + (s >> 16) + return (~s) & 0xFFFF + + def _icmp_frame(): """A minimal well-formed ICMP echo request (10.0.0.1 → 10.0.0.3).""" - def cksum(data): - if len(data) % 2: - data = data + b"\x00" # RFC 1071 odd-length padding - s = 0 - for i in range(0, len(data), 2): - s += (data[i] << 8) + data[i + 1] - while s >> 16: - s = (s & 0xFFFF) + (s >> 16) - return (~s) & 0xFFFF - icmp = bytes([8, 0, 0, 0]) + struct.pack(">HHH", 1, 1, 0) + b"payload12" - icmp = icmp[:2] + struct.pack(">H", cksum(icmp)) + icmp[4:] + icmp = icmp[:2] + struct.pack(">H", _cksum(icmp)) + icmp[4:] ip0 = struct.pack(">BBHHHBBH4s4s", 0x45, 0, 20 + len(icmp), 1, 0, 64, 1, 0, bytes([10, 0, 0, 1]), bytes([10, 0, 0, 3])) - ip = ip0[:10] + struct.pack(">H", cksum(ip0)) + ip0[12:] + ip = ip0[:10] + struct.pack(">H", _cksum(ip0)) + ip0[12:] return bytes.fromhex("0200000000020200000000010800") + ip + icmp +def _tcp_syn_frame(): + """A minimal TCP SYN (10.0.0.1:472 → 10.0.0.3:22).""" + + tcp = struct.pack(">HHIIBBHHH", 472, 22, 0, 0, 0x50, 0x02, 64240, 0, 0) + ip0 = struct.pack(">BBHHHBBH4s4s", 0x45, 0, 20 + len(tcp), 2, 0, 64, 6, 0, + bytes([10, 0, 0, 1]), bytes([10, 0, 0, 3])) + ip = ip0[:10] + struct.pack(">H", _cksum(ip0)) + ip0[12:] + tcp = tcp[:16] + struct.pack(">H", _cksum(ip0[12:] + tcp)) + tcp[18:] + return bytes.fromhex("0200000000020200000000010800") + ip + tcp + + def _fake_project(tmp_path, markers, markers_dir=None): """markers: the flat project.markers shape ({'link/name': {..., node_id}}).""" @@ -98,8 +123,26 @@ def _marker_entry(tag, enabled=True, node_id="node-1"): "node_id": node_id} +def _fake_columns(monkeypatch, mapping): + """Inject canned sharkd columns: mapping pcap-basename → {frame#: cols or None}. + + Frames absent from the dict get no columns; when the caller passes a + filter, the injected set IS the matching set (mirroring the real engine). + """ + + async def fake_columns_for(pcap, filter_expr): + return mapping.get(os.path.basename(pcap), {}) + + monkeypatch.setattr(marker_replay, "_columns_for", fake_columns_for) + + +def _cols(src="10.0.0.1", dst="10.0.0.3", proto="ICMP", info="Echo (ping) request"): + return {"src": src, "dst": dst, "proto": proto, "info": info, + "bg": "ffffff", "fg": "000000"} + + # --------------------------------------------------------------------------- -# pcap scanning +# pcap scanning (engine-free backbone) # --------------------------------------------------------------------------- class TestScanPcap: @@ -140,15 +183,15 @@ class TestScanPcap: # --------------------------------------------------------------------------- -# Tag gate + timeline +# Tag gate (engine-free — raised before any sharkd work) # --------------------------------------------------------------------------- -class TestGateAndTimeline: +class TestGate: async def test_unknown_tag_404(self, tmp_path): project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=1)}) with pytest.raises(ControllerNotFoundError): - build_timeline(project, tag=7) + await build_timeline(project, tag=7) async def test_gate_409_while_capturing(self, tmp_path): project = _fake_project(tmp_path, { @@ -156,9 +199,22 @@ class TestGateAndTimeline: "linkB/icmp": _marker_entry(tag=7, enabled=True, node_id="n2"), }) with pytest.raises(ControllerError, match="linkB"): - build_timeline(project, tag=7) + await build_timeline(project, tag=7) - async def test_merge_orders_by_ts_with_stable_tiebreak(self, tmp_path): + async def test_filter_length_capped(self, tmp_path, monkeypatch): + _fake_columns(monkeypatch, {}) + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False)}) + with pytest.raises(ControllerBadRequestError, match="too long"): + await build_timeline(project, tag=7, filter_expr="x" * (marker_replay.FILTER_MAX_LENGTH + 1)) + + +# --------------------------------------------------------------------------- +# Timeline assembly (columns injected — no engine needed) +# --------------------------------------------------------------------------- + +class TestTimeline: + + async def test_merge_orders_by_ts_with_stable_tiebreak(self, tmp_path, monkeypatch): # Two sources, deliberately interleaved in time, colliding on one µs. _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ (1693472000, 500000, b"a" * 60), # t1 sourceA @@ -168,12 +224,16 @@ class TestGateAndTimeline: (1693472001, 000000, b"b" * 60), # t2 sourceB (1693472002, 000000, b"b" * 60), # t3 sourceB — same µs as t3 sourceA ]) + _fake_columns(monkeypatch, { + "n1_linkA_icmp.pcap": {1: _cols(), 2: _cols()}, + "n2_linkB_icmp.pcap": {1: _cols(src="10.0.0.2"), 2: _cols(src="10.0.0.2")}, + }) project = _fake_project(tmp_path, { "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), "linkB/icmp": _marker_entry(tag=7, enabled=False, node_id="n2"), }) - timeline = build_timeline(project, tag=7) + timeline = await build_timeline(project, tag=7) assert timeline["frame_count"] == 4 assert timeline["start"] == "1693472000.500000" assert timeline["end"] == "1693472002.000000" @@ -181,24 +241,42 @@ class TestGateAndTimeline: # Same-microsecond pair keeps both frames (a ts dict key would drop one). assert [f["ts"] for f in timeline["frames"]][2:] == ["1693472002.000000"] * 2 assert [f["frame_number"] for f in timeline["frames"]] == [1, 1, 2, 2] + # Columns ride along verbatim. + assert timeline["frames"][0]["src"] == "10.0.0.1" + assert timeline["frames"][1]["src"] == "10.0.0.2" + assert timeline["frames"][0]["proto"] == "ICMP" assert {s["count"] for s in timeline["sources"]} == {2} - async def test_missing_pcap_is_zero_count_source(self, tmp_path): + async def test_columns_missing_for_a_frame_still_lists_it(self, tmp_path, monkeypatch): + # A frame the (injected) engine did not describe keeps its place with + # null columns — the timeline backbone never depends on the engine. + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [(1693472000, 0, b"a" * 60)]) + _fake_columns(monkeypatch, {}) # engine describes nothing + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) + + timeline = await build_timeline(project, tag=7) + frame = timeline["frames"][0] + assert frame["ts"] == "1693472000.000000" + assert frame["src"] is None and frame["info"] is None + + async def test_missing_pcap_is_zero_count_source(self, tmp_path, monkeypatch): + _fake_columns(monkeypatch, {}) project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False)}) - timeline = build_timeline(project, tag=7) + timeline = await build_timeline(project, tag=7) assert timeline["frame_count"] == 0 assert timeline["start"] is None and timeline["end"] is None assert timeline["frames"] == [] assert timeline["sources"][0]["count"] == 0 - async def test_over_cap_degrades_to_buckets(self, tmp_path): + async def test_over_cap_degrades_to_buckets(self, tmp_path, monkeypatch): _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ (1693472000, 0, b"a" * 60), (1693472000, 500000, b"a" * 60), (1693472001, 0, b"a" * 60), ]) + _fake_columns(monkeypatch, {"n1_linkA_icmp.pcap": {1: _cols(), 2: _cols(), 3: _cols()}}) project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) - timeline = build_timeline(project, tag=7, frame_cap=2) + timeline = await build_timeline(project, tag=7, frame_cap=2) assert timeline["truncated"] is True assert "frames" not in timeline assert timeline["buckets"] == [ @@ -206,40 +284,180 @@ class TestGateAndTimeline: {"ts": "1693472001.000000", "count": 1}, ] + async def test_filter_applies_before_count_and_slice(self, tmp_path, monkeypatch): + # Three frames; the injected "matching set" (what a real engine would + # return for the filter) contains only frames 1 and 3. + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 0, b"a" * 60), + (1693472001, 0, b"a" * 60), + (1693472002, 0, b"a" * 60), + ]) + _fake_columns(monkeypatch, {"n1_linkA_icmp.pcap": {1: _cols(), 3: _cols(proto="TCP")}}) + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) + + timeline = await build_timeline(project, tag=7, filter_expr="tcp") + assert timeline["frame_count"] == 2 + assert timeline["start"] == "1693472000.000000" + assert timeline["end"] == "1693472002.000000" + # frame numbers keep their ORIGINAL pcap identity through the filter. + assert [f["frame_number"] for f in timeline["frames"]] == [1, 3] + assert timeline["sources"][0]["count"] == 2 -# --------------------------------------------------------------------------- -# Window query -# --------------------------------------------------------------------------- class TestQueryFrames: - def _project(self, tmp_path): + def _project(self, tmp_path, monkeypatch): _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ (1693472000, 0, b"a" * 60), (1693472000, 150000, b"a" * 60), (1693472005, 0, b"a" * 60), ]) + _fake_columns(monkeypatch, {"n1_linkA_icmp.pcap": {i: _cols() for i in (1, 2, 3)}}) return _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) - async def test_window_inclusive_bounds(self, tmp_path): - result = query_frames(self._project(tmp_path), tag=7, ts="1693472000.000000", window_ms=150) + async def test_window_inclusive_bounds(self, tmp_path, monkeypatch): + result = await query_frames(self._project(tmp_path, monkeypatch), tag=7, + ts="1693472000.000000", window_ms=150) assert [f["ts"] for f in result["frames"]] == ["1693472000.000000", "1693472000.150000"] - async def test_window_miss_is_empty_success(self, tmp_path): - result = query_frames(self._project(tmp_path), tag=7, ts="1693472001.000000", window_ms=100) + async def test_window_miss_is_empty_success(self, tmp_path, monkeypatch): + result = await query_frames(self._project(tmp_path, monkeypatch), tag=7, + ts="1693472001.000000", window_ms=100) assert result == {"frames": []} - async def test_limit_applies(self, tmp_path): - result = query_frames(self._project(tmp_path), tag=7, ts="1693472000.000000", - window_ms=150, limit=1) + async def test_limit_applies(self, tmp_path, monkeypatch): + result = await query_frames(self._project(tmp_path, monkeypatch), tag=7, + ts="1693472000.000000", window_ms=150, limit=1) assert len(result["frames"]) == 1 # --------------------------------------------------------------------------- -# Frame detail (tshark path) +# Tree key renaming # --------------------------------------------------------------------------- -tshark_present = pytest.mark.skipif(shutil.which("tshark") is None, reason="tshark not installed") +class TestRename: + + async def test_renames_closed_key_set_and_drops_hf_id(self): + node = { + "t": "proto", "l": "Time to Live: 64", "fn": "ip.ttl", + "f": "ip.ttl == 64", "h": [22, 1], "s": None, "g": False, + "e": 8472, + "n": [{"l": "nested", "h": [23, 2], "n": []}], + } + renamed = _rename_value(node) + assert renamed == { + "element": "proto", "label": "Time to Live: 64", "name": "ip.ttl", + "filter_expr": "ip.ttl == 64", "pos": 22, "size": 1, + "expert": None, "generated": False, + "children": [{"label": "nested", "pos": 23, "size": 2, "children": []}], + } + + async def test_unknown_keys_pass_through_verbatim(self): + # A future Wireshark adding a key must never silently lose data. + node = {"l": "x", "future_key": {"deep": [1, 2]}, "n": []} + renamed = _rename_value(node) + assert renamed["future_key"] == {"deep": [1, 2]} + + async def test_count_tree_nodes_counts_dicts_only(self): + assert _count_tree_nodes({"a": [{"b": 1}, "str", 3]}) == 2 + + +# --------------------------------------------------------------------------- +# sharkd sessions + engine-backed behaviour +# --------------------------------------------------------------------------- + +class TestSessions: + + async def test_missing_sharkd_raises_501_error(self, tmp_path): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [(1693472000, 0, b"a" * 60)]) + project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) + with patch("gns3server.controller.marker_replay.shutil.which", return_value=None): + with pytest.raises(SharkdMissingError): + await build_timeline(project, tag=7) + + async def test_lru_bound_evicts_least_recently_used(self, tmp_path): + manager = marker_replay._SharkdManager() + + class FakeSession: + def __init__(self, pcap, used): + self.pcap, self.last_used = pcap, used + self.closed = False + self.mtime_ns, self.size = 0, 0 + + def matches(self, stat): + return False # always respawn → exercises the eviction path + + def alive(self): + return False + + def touch(self): + pass + + async def close(self): + self.closed = True + + fake_by_pcap = {} + + async def fake_spawn(pcap, stat): + session = FakeSession(pcap, 0) + fake_by_pcap[pcap] = session + return session + + with patch.object(manager, "_spawn", side_effect=fake_spawn): + for i in range(marker_replay.SESSION_MAX + 2): + pcap = tmp_path / f"pcap{i}" + _write_pcap(pcap, [(1693472000, 0, b"a" * 60)]) + await manager.session_for(str(pcap)) + # Bounded to SESSION_MAX; the earliest (least recently used) got evicted. + assert len(manager._sessions) == marker_replay.SESSION_MAX + assert fake_by_pcap[str(tmp_path / "pcap0")].closed is True + assert fake_by_pcap[str(tmp_path / "pcap1")].closed is True + + @sharkd_present + async def test_real_session_columns_and_filter(self, tmp_path): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 123456, _icmp_frame()), + (1693472001, 0, _tcp_syn_frame()), + ]) + manager = marker_replay._get_manager() + try: + columns = await marker_replay._columns_for(str(tmp_path / "n1_linkA_icmp.pcap"), None) + assert columns[1]["src"] == "10.0.0.1" + assert columns[1]["proto"] == "ICMP" + assert "Echo" in columns[1]["info"] + assert columns[2]["proto"] == "TCP" + + only_tcp = await marker_replay._columns_for(str(tmp_path / "n1_linkA_icmp.pcap"), "tcp") + assert set(only_tcp) == {2} + finally: + await manager.close_all() + + @sharkd_present + async def test_real_session_invalid_filter_raises_filter_error(self, tmp_path): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [(1693472000, 123456, _icmp_frame())]) + manager = marker_replay._get_manager() + try: + with pytest.raises(FilterError): + await marker_replay._columns_for(str(tmp_path / "n1_linkA_icmp.pcap"), "this is (not valid") + finally: + await manager.close_all() + + @sharkd_present + async def test_real_session_respawns_on_stat_change(self, tmp_path): + pcap = tmp_path / "n1_linkA_icmp.pcap" + _write_pcap(pcap, [(1693472000, 123456, _icmp_frame())]) + manager = marker_replay._get_manager() + try: + first = await manager.session_for(str(pcap)) + assert await manager.session_for(str(pcap)) is first # warm reuse + # Rewrite the file (simulating a truncated/rebuilt capture). + _write_pcap(pcap, [(1693473000, 0, _icmp_frame())]) + second = await manager.session_for(str(pcap)) + assert second is not first + columns = await marker_replay._columns_for(str(pcap), None) + assert set(columns) == {1} + finally: + await manager.close_all() class TestDecodeFrame: @@ -264,83 +482,65 @@ class TestDecodeFrame: await decode_frame(project, tag=7, ts="1693472000.123456", node_id="nobody", link_id="linkA", marker="icmp") - async def test_decode_feeds_tshark_a_scratch_copy(self, tmp_path): - """Hardened tshark profiles deny the project dir — tshark must read a - /tmp copy (a real copy, not a symlink) that is unlinked afterwards.""" - - import tempfile - from unittest.mock import patch, AsyncMock - - observed = [] - PDML = (b'' - b'' - b'') - - class FakeProc: - returncode = 0 - - async def communicate(self): - return PDML, b"" - - async def fake_exec(*args, **kwargs): - r_index = args.index("-r") - observed.append((args[r_index + 1], kwargs.get("env"))) - return FakeProc() - - project = self._project(tmp_path) - with patch("gns3server.controller.marker_replay.shutil.which", return_value="tshark"), \ - patch("gns3server.controller.marker_replay._tshark_version", AsyncMock(return_value="tshark 4.6.7")), \ - patch("gns3server.controller.marker_replay.asyncio.create_subprocess_exec", side_effect=fake_exec): + @sharkd_present + async def test_decode_end_to_end(self, tmp_path): + manager = marker_replay._get_manager() + try: + project = self._project(tmp_path) detail = await decode_frame(project, tag=7, ts="1693472000.123456", node_id="n1", link_id="linkA", marker="icmp") - - assert detail["field_count"] == 2 # proto + field from the canned PDML - (scratch, env), = observed - original = str(tmp_path / "n1_linkA_icmp.pcap") - assert scratch != original - assert scratch.startswith(tempfile.gettempdir()) and scratch.endswith(".pcap") - assert env["HOME"] == tempfile.gettempdir() - assert not os.path.exists(scratch) # cleaned up after the decode - - @tshark_present - async def test_decode_isomorphic_mapping(self, tmp_path): - import asyncio - import xml.etree.ElementTree as ET - - project = self._project(tmp_path) - detail = await decode_frame(project, tag=7, ts="1693472000.123456", - node_id="n1", link_id="linkA", marker="icmp") + finally: + await manager.close_all() assert detail["source"]["frame_number"] == 1 assert detail["hex"] == _icmp_frame().hex() - assert detail["field_count"] > 0 - assert "tshark" in detail["tshark_version"].lower() + assert detail["field_count"] > 10 - # Round-trip fidelity: node count equals the PDML element count - # (protos + fields, excluding the container itself)… - proc = await asyncio.create_subprocess_exec( - "tshark", "-r", str(tmp_path / "n1_linkA_icmp.pcap"), "-T", "pdml", - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, - ) - stdout, _ = await proc.communicate() - packet = ET.fromstring(stdout).find("./packet") - xml_elements = [e for e in packet.iter() if e is not packet] - assert detail["field_count"] == len(xml_elements) + # Renamed tree: every label/name present, filter expressions with + # values baked in, byte ranges for hex highlighting. + def find(node, name): + for child in node if isinstance(node, list) else node.get("children", []): + if child.get("name") == name: + return child + deep = find(child, name) + if deep is not None: + return deep + return None - # …and every XML attribute survives verbatim as a JSON string key. - def walk(element, node): - for key, value in element.attrib.items(): - assert node.get(key) == value - assert all(isinstance(v, str) for k, v in node.items() if k != "children") - for child, child_node in zip(element, node["children"]): - walk(child, child_node) + ttl = find(detail["tree"], "ip.ttl") + assert ttl is not None + assert ttl["label"] == "Time to Live: 64" + assert ttl["filter_expr"] == "ip.ttl == 64" + assert ttl["pos"] == 22 and ttl["size"] == 1 - for element, node in zip(packet, detail["tree"]): - walk(element, node) + @sharkd_present + async def test_key_census_closed_set(self, tmp_path): + """The rename correctness guarantee: across protocol-diverse real + trees, sharkd's raw key set stays within the census-known keys.""" - # Values stay strings (no numeric re-typing). - ttl = next( - f for p in detail["tree"] if p.get("name") == "ip" - for f in p["children"] if f.get("name") == "ip.ttl" - ) - assert ttl["show"] == "64" and ttl["showname"] == "Time to Live: 64" + pcap = tmp_path / "proto_mix.pcap" + _write_pcap(pcap, [ + (1693472000, 100000, _icmp_frame()), + (1693472001, 0, _tcp_syn_frame()), + ]) + manager = marker_replay._get_manager() + try: + session = await manager.session_for(str(pcap)) + known = set(marker_replay._KEY_RENAME) | {"h", "e"} + seen = set() + + def walk(value): + if isinstance(value, dict): + seen.update(value.keys()) + for item in value.values(): + walk(item) + elif isinstance(value, list): + for item in value: + walk(item) + + for frame_number in (1, 2): + result = await session.rpc("frame", {"frame": frame_number, "proto": True}) + walk(result.get("tree", [])) + assert seen <= known, f"unknown sharkd keys appeared: {seen - known}" + finally: + await manager.close_all() From 31ba82b8d3b8c32a9c7af6f79ec2356f6abdc63d Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 8 Sep 2026 22:00:00 +0800 Subject: [PATCH 37/60] docs: record live validation of the sharkd replay endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-data examples from a 9-link OSPF project (29 frames): range with full columns and Wireshark coloring, filter verified in all four regimes (match / zero-match / invalid 400 with sharkd's text / oversized), window hit and miss, and a frame detail whose filter_expr carries OSPF's multicast TTL (ip.ttl == 1) with byte ranges for hex highlighting. Three precision fixes found by auditing the doc against the code: the 501 applies when the engine is consulted (an empty-capture tag answers 200 without sharkd), sources[].count is post-filter like every other figure, and the unified {"message": …} error body is now stated. --- docs/features/marker-tag-replay.md | 66 ++++++++++++++++++------------ 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/docs/features/marker-tag-replay.md b/docs/features/marker-tag-replay.md index 6b9c16a92..033d25b79 100644 --- a/docs/features/marker-tag-replay.md +++ b/docs/features/marker-tag-replay.md @@ -21,8 +21,9 @@ measures the **intermediate node's forwarding latency** (host view) — somethin single-link capture can never show. **sharkd is a hard requirement** (part of the Wireshark package). Without it every replay -endpoint returns 501 — there is deliberately no degraded mode; one engine, one rendering -shape for the Web UI. +endpoint that needs the engine returns 501 — there is deliberately no degraded mode; one +engine, one rendering shape for the Web UI. (A tag whose sources captured nothing returns +an empty timeline without consulting the engine — an empty answer, not a degraded one.) ## Architecture @@ -131,22 +132,22 @@ without it. ```json { - "tag": 666, - "start": "1788196663.226372", - "end": "1788196713.706634", - "frame_count": 20, + "tag": 102, + "start": "1788369209.406812", + "end": "1788369219.249085", + "frame_count": 29, "truncated": false, "sources": [ - { "node_id": "b764c434…", "link_id": "316ef8fd…", "marker": "global-def-…", - "data_link_type": "DLT_EN10MB", "count": 10 } + { "node_id": "47703cad…", "link_id": "2697a7c6…", "marker": "global-ospf", + "data_link_type": "DLT_EN10MB", "count": 4 } ], "frames": [ - { "ts": "1788196663.226372", "len": 98, - "node_id": "b764c434…", "link_id": "316ef8fd…", - "marker": "global-def-…", "frame_number": 1, - "src": "10.1.10.101", "dst": "203.0.113.1", - "proto": "ICMP", "info": "Echo (ping) request id=0x6ed5, seq=1/0, ttl=64", - "bg": "ffffff", "fg": "000000" } + { "ts": "1788369209.406812", "len": 114, + "node_id": "47703cad…", "link_id": "2697a7c6…", + "marker": "global-ospf", "frame_number": 1, + "src": "10.0.12.1", "dst": "224.0.0.5", + "proto": "OSPF", "info": "Hello Packet", + "bg": "fff3d6", "fg": "12272e" } ] } ``` @@ -166,11 +167,12 @@ without it. `?filter=` on both `range` and `frames` is a Wireshark display filter, applied **before** counting and slicing — `start` / `end` / `frame_count` / -`frames` | `buckets` are all computed on the matching frames only. Filtered frames keep -their original pcap frame numbers. The filter travels as one argv-style element (never -through a shell) and is capped at 2000 characters. An invalid expression is a **400** -whose message carries sharkd's original error text — suitable for inline display in the -filter bar, and distinct from the 409 gate / 404 unknown-tag semantics. +`frames` | `buckets` and the per-source `sources[].count` are all computed on the +matching frames only. Filtered frames keep their original pcap frame numbers. The filter +travels as one argv-style element (never through a shell) and is capped at 2000 +characters. An invalid expression is a **400** whose message carries sharkd's original +error text — suitable for inline display in the filter bar, and distinct from the 409 +gate / 404 unknown-tag semantics. ### `frames` — point / window query (paging) @@ -197,15 +199,15 @@ click. ```json { - "ts": "1788196663.226372", - "source": { "node_id": "b764c434…", "link_id": "316ef8fd…", - "marker": "global-def-…", "frame_number": 1 }, - "field_count": 85, - "hex": "00005e00010a…", + "ts": "1788369209.406812", + "source": { "node_id": "47703cad…", "link_id": "2697a7c6…", + "marker": "global-ospf", "frame_number": 1 }, + "field_count": 89, + "hex": "01005e000005…", "tree": [ { "element": "proto", "label": "Internet Protocol Version 4, …", "children": [ - { "element": "field", "name": "ip.ttl", "label": "Time to Live: 64", - "filter_expr": "ip.ttl == 64", "pos": 22, "size": 1, "children": [] } + { "element": "field", "name": "ip.ttl", "label": "Time to Live: 1", + "filter_expr": "ip.ttl == 1", "pos": 22, "size": 1, "children": [] } ] } ] } @@ -248,6 +250,8 @@ the pcap; `field_count` is the mapped node count (client-side sanity check). ## Error Responses +All error bodies are `{"message": "…"}` (the app's unified format). + | Status | Description | |--------|-------------| | 400 | Invalid display filter (message carries sharkd's original text) or filter longer than 2000 chars | @@ -263,6 +267,16 @@ the pcap; `field_count` is the mapped node count (client-side sanity check). (mergecap is deliberately not used) — each frame carries its source and is decoded individually, so Ethernet and serial (cHDLC/PPP) markers can share one timeline. Malformed packets are dissected like any other; sharkd marks them in the tree. +- **Live validation (2026-09, 9-link OSPF project, 29 frames over 9 sources).** Cold + `range` (spawning all sharkd sessions) answered in 0.84 s with full columns and + Wireshark coloring; filters verified in all four regimes (match / zero-match with + `start: null`, invalid expression → 400 with sharkd's text, oversized → 400); window + hit and miss behaved per contract; a frame detail returned 89 nodes with + `filter_expr: "ip.ttl == 1"` (OSPF multicast TTL) and byte ranges for hex + highlighting. +- **Columns are re-fetched per request** (~90 ms per source against a loaded session). + The data is frozen while the gate passes, so a cache keyed on `(mtime, size)` is a + natural follow-up if list latency ever matters at many-source scale. - **Session invalidation is cheap and total.** Every request stats the source pcap; a rewritten file (mtime/size change) respawns the session — a paused-but-restarted capture can never serve stale dissect state. From ef5d81498a1d5c7f3a404f9394b5c26be8fdba2a Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Wed, 9 Sep 2026 00:53:06 +0800 Subject: [PATCH 38/60] feat: accept link= on the replay range and frames endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrows the merged frame stream to one capture source BEFORE counting, slicing and bucketing (frame_count / frames | buckets all recomputed on the narrowed set), AND-composing with the display filter. A pure identity filter applied before any engine work — only the selected link's pcap gets a sharkd pass, so link+filter is cheaper than filter alone. Two boundaries by contract with the Web UI: - sources[] stays the tag's stable inventory: every capture source listed with engine-free TOTAL counts, unaffected by link/filter — a source dropdown must not shrink when the view narrows (this also settles sources[].count on total counts rather than post-filter matches, which no spec ever required) - an unknown link_id matches nothing: frame_count 0, start null, empty frames/buckets — the same shape as a zero-match display filter, deliberately not a 404; an empty link param is treated as absent --- docs/features/marker-tag-replay.md | 30 +++++--- gns3server/api/routes/controller/projects.py | 26 +++++-- gns3server/controller/marker_replay.py | 38 ++++++---- .../routes/controller/test_marker_replay.py | 41 +++++++++++ tests/controller/test_marker_replay.py | 72 ++++++++++++++++++- 5 files changed, 180 insertions(+), 27 deletions(-) diff --git a/docs/features/marker-tag-replay.md b/docs/features/marker-tag-replay.md index 033d25b79..b29273635 100644 --- a/docs/features/marker-tag-replay.md +++ b/docs/features/marker-tag-replay.md @@ -124,8 +124,8 @@ without it. | Method | Path | Description | |--------|------|-------------| -| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/range[?filter=]` | Timeline metadata + full merged frame list with packet-list columns | -| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frames?ts=&window_ms=&limit=[&filter=]` | Frames with ts in `[T, T+window]`, merged across sources | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/range[?filter=&link=]` | Timeline metadata + full merged frame list with packet-list columns | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frames?ts=&window_ms=&limit=[&filter=&link=]` | Frames with ts in `[T, T+window]`, merged across sources | | GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frame/detail?ts=&node_id=&link_id=&marker=` | Single frame: protocol tree + raw hex (lazy — one call per frame the user opens) | ### `range` — the timeline @@ -167,12 +167,26 @@ without it. `?filter=` on both `range` and `frames` is a Wireshark display filter, applied **before** counting and slicing — `start` / `end` / `frame_count` / -`frames` | `buckets` and the per-source `sources[].count` are all computed on the -matching frames only. Filtered frames keep their original pcap frame numbers. The filter -travels as one argv-style element (never through a shell) and is capped at 2000 -characters. An invalid expression is a **400** whose message carries sharkd's original -error text — suitable for inline display in the filter bar, and distinct from the 409 -gate / 404 unknown-tag semantics. +`frames` | `buckets` are all computed on the matching frames only. Filtered frames keep +their original pcap frame numbers. The filter travels as one argv-style element (never +through a shell) and is capped at 2000 characters. An invalid expression is a **400** +whose message carries sharkd's original error text — suitable for inline display in the +filter bar, and distinct from the 409 gate / 404 unknown-tag semantics. + +### Capture-source selection + +`?link=` on both `range` and `frames` narrows the frame stream to one +capture source — a pure identity filter applied **before** any engine work (only the +selected link's pcap gets a sharkd pass), AND-composing with `filter`. Windows and the +histogram therefore always agree with the link-filtered view. Two boundaries by +design: + +- **`sources` is the stable inventory of the tag**: every capture source is always + listed, with engine-free **total** counts, unaffected by `link` / `filter` — a + source dropdown must not shrink when the view narrows. +- **An unknown `link_id` matches nothing**: `frame_count: 0`, `start: null`, empty + `frames` / `buckets` — the same shape as a zero-match display filter, deliberately + not a 404. ### `frames` — point / window query (paging) diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index e1e4872cf..66fe27c2c 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -242,6 +242,7 @@ async def _replay_response(awaitable): async def replay_tag_range( tag: int, filter: Optional[str] = None, + link: Optional[str] = None, project: Project = Depends(dep_project), ) -> dict: """ @@ -259,12 +260,20 @@ async def replay_tag_range( counting and slicing — start / end / frame_count / frames | buckets are all computed on the matching frames only. An invalid expression is a 400 carrying sharkd's original error text (for inline display in the UI - filter bar). Requires sharkd — 501 without it. + filter bar). + + ``link`` narrows the frame stream to one capture source (link_id), + AND-composing with ``filter``; ``sources`` always lists the tag's full + inventory regardless. An unknown link_id yields an empty timeline (same + shape as a zero-match filter), not a 404. Requires sharkd — 501 without + it. Required privilege: Project.Audit """ - return await _replay_response(marker_replay.build_timeline(project, tag, filter_expr=filter)) + return await _replay_response( + marker_replay.build_timeline(project, tag, filter_expr=filter, link_id=link) + ) @router.get( @@ -277,6 +286,7 @@ async def replay_tag_frames( window_ms: int = 100, limit: int = 1000, filter: Optional[str] = None, + link: Optional[str] = None, project: Project = Depends(dep_project), ) -> dict: """ @@ -285,15 +295,19 @@ async def replay_tag_frames( ``{"frames": []}``. The tag gate applies (409 while any marker captures). ``ts`` must be the exact string returned by the range response — never - re-serialize it through a float. ``filter`` (optional display filter) has - the same semantics as on the range endpoint. Requires sharkd — 501 - without it. + re-serialize it through a float. ``filter`` and ``link`` (optional + display filter / capture-source link_id) have the same semantics as on + the range endpoint — windows and the histogram always agree. Requires + sharkd — 501 without it. Required privilege: Project.Audit """ return await _replay_response( - marker_replay.query_frames(project, tag, ts, window_ms=window_ms, limit=limit, filter_expr=filter) + marker_replay.query_frames( + project, tag, ts, window_ms=window_ms, limit=limit, + filter_expr=filter, link_id=link, + ) ) diff --git a/gns3server/controller/marker_replay.py b/gns3server/controller/marker_replay.py index 13d3aba2f..5a0bdc945 100644 --- a/gns3server/controller/marker_replay.py +++ b/gns3server/controller/marker_replay.py @@ -500,7 +500,7 @@ def gate_tag(project, tag): return entries -async def _merged_frames(project, entries, filter_expr=None): +async def _merged_frames(project, entries, filter_expr=None, link_id=None): """ Scan every source pcap's record headers, ask sharkd for columns (and, with a filter, the matching set), and merge into one list sorted by @@ -508,6 +508,16 @@ async def _merged_frames(project, entries, filter_expr=None): can hit the same microsecond); the tiebreaker yields a stable, determined order instead of a fictional one. With a filter, only frames sharkd matched survive, keeping their original pcap frame numbers. + + ``link_id`` narrows the frame stream to one capture source **before** any + engine work (a pure identity filter — only the selected link's pcap gets + a sharkd pass) and AND-composes with ``filter_expr``. An unknown link + matches nothing: an empty stream, same shape as a zero-match display + filter — deliberately not a 404. + + ``sources`` is the stable inventory of the tag: EVERY capture source is + listed with engine-free total counts, unaffected by ``link_id`` / + ``filter_expr`` — the inventory must not shrink when the view narrows. """ markers_dir = project.markers_directory @@ -518,12 +528,16 @@ async def _merged_frames(project, entries, filter_expr=None): markers_dir, f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}.pcap" ) frames = scan_pcap_frames(pcap) if os.path.exists(pcap) else [] + # Inventory first: every source, engine-free totals. + sources.append({**{k: entry[k] for k in ("node_id", "link_id", "marker", "data_link_type")}, + "count": len(frames)}) + if link_id and entry["link_id"] != link_id: + continue # link narrows the stream before any engine work if frames: columns = await _columns_for(pcap, filter_expr) else: columns = {} source_key = f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}" - count = 0 for frame_number, (sec, usec, incl_len) in enumerate(frames, start=1): if filter_expr is not None and frame_number not in columns: continue @@ -544,9 +558,6 @@ async def _merged_frames(project, entries, filter_expr=None): "bg": cols.get("bg"), "fg": cols.get("fg"), }) - count += 1 - sources.append({**{k: entry[k] for k in ("node_id", "link_id", "marker", "data_link_type")}, - "count": count}) merged.sort(key=lambda f: (f["ts_us"], f["_source"], f["frame_number"])) for frame in merged: del frame["ts_us"] @@ -561,17 +572,18 @@ def _validate_filter(filter_expr): ) -async def build_timeline(project, tag, frame_cap=FRAME_LIST_CAP, filter_expr=None): +async def build_timeline(project, tag, frame_cap=FRAME_LIST_CAP, filter_expr=None, link_id=None): """ The ``range`` response: timeline bounds, per-source stats, and (under ``frame_cap``) the full merged frame list for one-request timeline layout. Over the cap the list is replaced by per-second buckets. With a - ``filter_expr`` every figure is computed on the matching frames only. + ``filter_expr`` and/or a ``link_id`` every figure is computed on the + matching frames only (``sources`` stays the full tag inventory). """ _validate_filter(filter_expr) entries = gate_tag(project, tag) - frames, sources = await _merged_frames(project, entries, filter_expr) + frames, sources = await _merged_frames(project, entries, filter_expr, link_id=link_id) response = { "tag": tag, @@ -595,15 +607,17 @@ async def build_timeline(project, tag, frame_cap=FRAME_LIST_CAP, filter_expr=Non return response -async def query_frames(project, tag, ts, window_ms=100, limit=1000, filter_expr=None): +async def query_frames(project, tag, ts, window_ms=100, limit=1000, filter_expr=None, link_id=None): """ - Frames with ts in ``[T, T+window_ms]`` merged across sources. A time with - no frames is a normal, successful answer — ``{"frames": []}``. + Frames with ts in ``[T, T+window_ms]`` merged across sources — narrowed + by ``filter_expr`` / ``link_id`` with the same semantics as the range + endpoint, so windowed seconds and the histogram always agree. A time + with no frames is a normal, successful answer — ``{"frames": []}``. """ _validate_filter(filter_expr) entries = gate_tag(project, tag) - frames, _sources = await _merged_frames(project, entries, filter_expr) + frames, _sources = await _merged_frames(project, entries, filter_expr, link_id=link_id) start_us = _parse_ts(ts) end_us = start_us + max(window_ms, 0) * 1000 diff --git a/tests/api/routes/controller/test_marker_replay.py b/tests/api/routes/controller/test_marker_replay.py index 2629ec110..24fdf5c8e 100644 --- a/tests/api/routes/controller/test_marker_replay.py +++ b/tests/api/routes/controller/test_marker_replay.py @@ -209,6 +209,47 @@ class TestReplayRoutes: assert response.status_code == status.HTTP_404_NOT_FOUND assert "rebuilt" in response.json()["message"] + async def test_range_link_param_narrows_and_keeps_sources( + self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch + ) -> None: + + r1, r2 = UDPLink(project), UDPLink(project) + project._links.update({r1.id: r1, r2.id: r2}) + + def _wire(link, node_id, frames): + link._markers["icmp"] = {"bpf": "icmp", "tag": 7, "enabled": False, "color": None, + "highlight_duration": None, "capture_node_id": node_id, + "direction": None, "data_link_type": "DLT_EN10MB"} + _write_pcap(f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames) + + _wire(r1, "n1", [(1693472000, 0, b"a" * 60), (1693472002, 0, b"a" * 60)]) + _wire(r2, "n2", [(1693472001, 0, b"b" * 60)]) + + async def fake_columns(pcap, filter_expr): + return {1: _cols(), 2: _cols()} + + monkeypatch.setattr(marker_replay, "_columns_for", fake_columns) + + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7), + params={"link": r1.id}, + ) + body = response.json() + assert body["frame_count"] == 2 + assert {f["link_id"] for f in body["frames"]} == {r1.id} + # The source dropdown keeps the whole tag inventory. + assert sorted(s["count"] for s in body["sources"]) == [1, 2] + + # Unknown link: 200 with an empty timeline, same shape as a zero-match + # filter — not a 404. + response = await client.get( + app.url_path_for("replay_tag_range", project_id=project.id, tag=7), + params={"link": "00000000-0000-0000-0000-000000000000"}, + ) + body = response.json() + assert body["frame_count"] == 0 and body["frames"] == [] and body["start"] is None + assert len(body["sources"]) == 2 + @sharkd_present async def test_range_columns_and_filter_end_to_end( self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions diff --git a/tests/controller/test_marker_replay.py b/tests/controller/test_marker_replay.py index 748c72d15..a2f992440 100644 --- a/tests/controller/test_marker_replay.py +++ b/tests/controller/test_marker_replay.py @@ -301,7 +301,77 @@ class TestTimeline: assert timeline["end"] == "1693472002.000000" # frame numbers keep their ORIGINAL pcap identity through the filter. assert [f["frame_number"] for f in timeline["frames"]] == [1, 3] - assert timeline["sources"][0]["count"] == 2 + # sources[] is the stable inventory: engine-free TOTAL counts, + # unaffected by link/filter (the WebUI source dropdown must not + # shrink when the view narrows). + assert timeline["sources"][0]["count"] == 3 + + +class TestLinkFilter: + """``link_id`` narrows the frame stream to one capture source before + counting/slicing/bucketing; sources stay the full inventory.""" + + def _project(self, tmp_path, monkeypatch, columns_override=None): + _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ + (1693472000, 500000, b"a" * 60), + (1693472002, 000000, b"a" * 60), + ]) + _write_pcap(tmp_path / "n2_linkB_icmp.pcap", [ + (1693472001, 000000, b"b" * 60), + (1693472002, 000000, b"b" * 60), + ]) + + async def fake_columns(pcap, filter_expr): + if columns_override is not None: + return await columns_override(os.path.basename(pcap), filter_expr) + src = "10.0.0.1" if "n1" in pcap else "10.0.0.2" + return {1: _cols(src=src), 2: _cols(src=src)} + + monkeypatch.setattr(marker_replay, "_columns_for", fake_columns) + return _fake_project(tmp_path, { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + "linkB/icmp": _marker_entry(tag=7, enabled=False, node_id="n2"), + }) + + async def test_link_narrows_before_count_and_slice(self, tmp_path, monkeypatch): + timeline = await build_timeline(self._project(tmp_path, monkeypatch), tag=7, link_id="linkA") + assert timeline["frame_count"] == 2 + assert timeline["start"] == "1693472000.500000" + assert [f["link_id"] for f in timeline["frames"]] == ["linkA", "linkA"] + # sources stay the FULL inventory with engine-free totals. + assert sorted((s["link_id"], s["count"]) for s in timeline["sources"]) == [ + ("linkA", 2), ("linkB", 2) + ] + + async def test_unknown_link_is_empty_success(self, tmp_path, monkeypatch): + timeline = await build_timeline(self._project(tmp_path, monkeypatch), tag=7, link_id="nope") + assert timeline["frame_count"] == 0 + assert timeline["start"] is None and timeline["end"] is None + assert timeline["frames"] == [] + assert len(timeline["sources"]) == 2 + + async def test_link_and_filter_compose_as_and(self, tmp_path, monkeypatch): + # The injected "matching set" contains only frame 2 per source — + # combined with link=linkA the view is exactly that one frame. + async def override(basename, filter_expr): + assert filter_expr == "tcp" + return {2: _cols(proto="TCP")} + + project = self._project(tmp_path, monkeypatch, columns_override=override) + timeline = await build_timeline(project, tag=7, filter_expr="tcp", link_id="linkA") + assert timeline["frame_count"] == 1 + assert timeline["frames"][0]["frame_number"] == 2 + assert timeline["frames"][0]["link_id"] == "linkA" + + async def test_empty_link_string_means_absent(self, tmp_path, monkeypatch): + timeline = await build_timeline(self._project(tmp_path, monkeypatch), tag=7, link_id="") + assert timeline["frame_count"] == 4 + + async def test_query_frames_window_over_link_stream(self, tmp_path, monkeypatch): + project = self._project(tmp_path, monkeypatch) + result = await query_frames(project, tag=7, ts="1693472002.000000", + window_ms=0, link_id="linkB") + assert [f["link_id"] for f in result["frames"]] == ["linkB"] class TestQueryFrames: From 5d91ca0efc76e32a5d4799ad67de88b7f459df10 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 11 Sep 2026 00:55:18 +0800 Subject: [PATCH 39/60] fix: harden sharkd replay sessions and serve the uncapped frame list Review-driven session/transport fixes (each reproduced live against sharkd 4.6.7 before fixing): - raise the RPC stream limit to 16 MB: a full 1000-row frames page measures ~190 KB against the 64 KB StreamReader default, which failed the request with a 500 and desynchronized the resident session; a line-over-limit ValueError is now treated as a transport failure - verify JSON-RPC reply ids: a timed-out request's late reply was served as the next request's answer; timeouts, dead pipes, malformed and stale replies now kill the session for good instead - make check-spawn atomic under one manager lock: concurrent requests for the same pcap double-spawned sharkd and leaked the loser (process plus /tmp scratch copy) forever - refcount sessions and evict idle only (LRU, cap raised 8 -> 16): a tag with more sources than the cap respawned every source on every request, and concurrent requests could get their session killed mid-RPC (spurious 502) - map FilterError to sharkd's filter rejection (-13002) only; other engine failures with a filter set are 502, not a client 400 - detail: accept an optional frame_number to disambiguate same-microsecond frames (ts is not unique within a pcap); drop the -8003 -> 404 mapping (the range is validated locally, engine errors are real faults); a failed hex read is a 404 instead of "hex": null - a pcap deleted mid-request is a 404, not a 500; the pcap-sized scratch copy runs off the event loop; server shutdown kills every resident session and drops its scratch directory - pin the packet-list layout through scratch-HOME Wireshark preferences: the column indexes are a contract the server owns (protocol-level column negotiation is rejected by sharkd 4.6.x) Range contract change (WebUI moved to an always-flat list): the merged frame list is returned in full, deliberately uncapped - truncated and per-second buckets are removed, frame_count always equals len(frames), and rendering cost is the client's concern (the window endpoint remains the incremental path). --- docs/features/marker-tag-replay.md | 92 +++- gns3server/api/routes/controller/projects.py | 18 +- gns3server/controller/marker_replay.py | 408 ++++++++++++------ gns3server/core/tasks.py | 5 + .../routes/controller/test_marker_replay.py | 31 +- tests/controller/test_marker_replay.py | 284 ++++++++++-- 6 files changed, 627 insertions(+), 211 deletions(-) diff --git a/docs/features/marker-tag-replay.md b/docs/features/marker-tag-replay.md index b29273635..51b35d3ac 100644 --- a/docs/features/marker-tag-replay.md +++ b/docs/features/marker-tag-replay.md @@ -41,7 +41,7 @@ graph TB TMP["/tmp scratch copies
(hardened-profile workaround)"] SK["sharkd -
(resident JSON-RPC on stdio)"] - UI -->|"GET range / frames [?filter=]"| GATE + UI -->|"GET range / frames [?filter=&link=]"| GATE GATE --> SCAN SCAN -->|"columns / filter matches"| SESS SESS --> TMP --> SK @@ -60,11 +60,44 @@ Backbone vs engine, deliberately separated: Session lifecycle: each session is validated per request against the source pcap's `(mtime, size)` — a mismatch (e.g. the capture node restarted and uBridge truncated the -pcap while paused) kills and respawns it. Sessions are LRU-bounded (8), each RPC has a -timeout and is serialized by a per-session lock (sharkd serves one request at a time). -sharkd reads a `/tmp` scratch copy of the pcap with a scratch `HOME` — hardened profiles -(AppArmor &c.) deny it the project directory and the user's home even though the server -process can read both. +pcap while paused) kills and respawns it. Sessions are refcounted while a request holds +them and the LRU bound (16) only ever evicts **idle** sessions, so neither a request's +own walk over a tag with more sources than the bound nor a concurrent request can have +its session killed mid-RPC. A single manager lock makes check-spawn atomic (concurrent +requests share one spawn instead of double-spawning a process nobody reaps). Each RPC +is serialized by a per-session lock (sharkd serves one request at a time), carries a +timeout and an **id check** — any transport failure (timeout, dead pipe, malformed or +stale reply, oversized line) kills the session for good, because a desynchronized +session would serve shifted results. sharkd reads a `/tmp` scratch **directory** +containing a copy of the pcap plus pinned Wireshark preferences (the column layout the +frames RPC is parsed against is a contract the server owns, immune to system or user +column customization) — hardened profiles (AppArmor &c.) deny it the project directory +and the user's home even though the server process can read both. The pcap copy runs +off the event loop, and server shutdown kills every session so no scratch copy leaks +into `/tmp` across restarts. + +### Resource bounds + +Three independent bounds, easily confused: + +| Bound | Value | Governs | +|-------|-------|---------| +| `_STREAM_LIMIT_BYTES` | 16 MB | Max **single JSON-RPC reply line** read from sharkd. A full 1000-row page measures ~190 KB against asyncio's 64 KB default — the trigger is the frame count in one pcap (≥ 1000), never the number of pcaps | +| `_FRAMES_PAGE` | 1000 | Rows fetched per `frames` RPC while draining one source's columns | +| `SESSION_MAX` | 16 | Resident **idle** sharkd sessions (LRU eviction) | + +The `range` frame list itself is uncapped by decision — the client owns the +rendering cost of a huge list. + +Process count follows usage, not the pcap inventory. sharkd loads one file per +process, so a session exists per source pcap **actually consulted** — a replay +request only touches its own tag's sources, and a session is either in use +(refcounted, held for a whole pagination drain rather than per RPC) or idle +(no holder — the only state the LRU cap ever evicts). At any moment the pool +holds at most `SESSION_MAX` idle sessions plus one per source being drained by +an in-flight request: a project with a thousand marker pcaps still runs at +most `16 + Σ(in-flight sources)` sharkd processes. Each process holds its pcap +in memory (~file size), which is the quantity the session cap primarily bounds. ## Business Process @@ -79,12 +112,12 @@ sequenceDiagram Note over UI: ③ pause every marker under the tag Note over UI,SK: ④ replay - UI->>C: GET /markers/tags/666/replay/range[?filter=…] + UI->>C: GET /markers/tags/666/replay/range[?filter=…&link=…] C->>C: gate → scan record headers → merge order C->>SK: frames {filter, skip, limit} → columns + matches C-->>UI: {start, end, sources, frames[] with src/dst/proto/info/bg/fg} UI->>C: GET frames?ts=T&window_ms=W (paging — {"frames": []} on a miss) - UI->>C: GET frame/detail?ts=…&node_id=…&link_id=…&marker=… + UI->>C: GET frame/detail?ts=…&node_id=…&link_id=…&marker=…[&frame_number=…] C->>C: hex straight from the pcap C->>SK: frame {frame: N, proto: true} SK-->>C: tree (keys renamed to the REST contract) @@ -126,7 +159,7 @@ without it. |--------|------|-------------| | GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/range[?filter=&link=]` | Timeline metadata + full merged frame list with packet-list columns | | GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frames?ts=&window_ms=&limit=[&filter=&link=]` | Frames with ts in `[T, T+window]`, merged across sources | -| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frame/detail?ts=&node_id=&link_id=&marker=` | Single frame: protocol tree + raw hex (lazy — one call per frame the user opens) | +| GET | `/v3/projects/{pid}/markers/tags/{tag}/replay/frame/detail?ts=&node_id=&link_id=&marker=[&frame_number=]` | Single frame: protocol tree + raw hex (lazy — one call per frame the user opens) | ### `range` — the timeline @@ -136,7 +169,6 @@ without it. "start": "1788369209.406812", "end": "1788369219.249085", "frame_count": 29, - "truncated": false, "sources": [ { "node_id": "47703cad…", "link_id": "2697a7c6…", "marker": "global-ospf", "data_link_type": "DLT_EN10MB", "count": 4 } @@ -152,9 +184,10 @@ without it. } ``` -- `frames` is the **full, merged, time-ordered list** (cap 5000) — one request lays out - the whole timeline. Over the cap, `frames` is omitted and per-second `buckets` are - returned instead with `truncated: true` (check with `'frames' in response`, not null). +- `frames` is the **full, merged, time-ordered list** — one request lays out the whole + timeline, **deliberately uncapped**: rendering a huge list is the client's concern, + and the window endpoint exists for incremental views. There is no `truncated` flag + and no bucket histogram; `frame_count` always equals `len(frames)`. - Every frame entry carries the Wireshark packet-list columns — `src` / `dst` / `proto` / `info` — plus the **coloring-rule hints `bg` / `fg`** (Wireshark's own palette decisions, so the UI can color rows exactly like Wireshark without shipping the @@ -166,12 +199,14 @@ without it. ### Display filter `?filter=` on both `range` and `frames` is a Wireshark display filter, -applied **before** counting and slicing — `start` / `end` / `frame_count` / -`frames` | `buckets` are all computed on the matching frames only. Filtered frames keep +applied **before** counting and slicing — `start` / `end` / `frame_count` / `frames` +are all computed on the matching frames only. Filtered frames keep their original pcap frame numbers. The filter travels as one argv-style element (never through a shell) and is capped at 2000 characters. An invalid expression is a **400** whose message carries sharkd's original error text — suitable for inline display in the -filter bar, and distinct from the 409 gate / 404 unknown-tag semantics. +filter bar, and distinct from the 409 gate / 404 unknown-tag semantics. Only sharkd's +filter-rejection error maps to 400; any other engine failure while a filter is set is a +502 (the filter was fine — the engine was not). ### Capture-source selection @@ -185,8 +220,7 @@ design: listed, with engine-free **total** counts, unaffected by `link` / `filter` — a source dropdown must not shrink when the view narrows. - **An unknown `link_id` matches nothing**: `frame_count: 0`, `start: null`, empty - `frames` / `buckets` — the same shape as a zero-match display filter, deliberately - not a 404. + `frames` — the same shape as a zero-match display filter, deliberately not a 404. ### `frames` — point / window query (paging) @@ -209,7 +243,10 @@ Invoked only when the user opens a frame. The `ts` must be the **exact string re in the timeline/frame list** (round-tripped verbatim — never re-serialized through a float); `node_id + link_id + marker` identify the pcap. The server re-resolves the ts against the file, guarding against a capture rebuilt between the timeline view and this -click. +click. Since ts is **not unique within one pcap** (same-microsecond frames are kept +deliberately), the optional `frame_number` — carried by every frame list entry — +disambiguates them: it must still land on the exact ts, and without it the first ts +match decodes. ```json { @@ -268,12 +305,12 @@ All error bodies are `{"message": "…"}` (the app's unified format). | Status | Description | |--------|-------------| -| 400 | Invalid display filter (message carries sharkd's original text) or filter longer than 2000 chars | +| 400 | Invalid display filter (sharkd's filter rejection only; message carries its original text) or filter longer than 2000 chars | | 401 | Not authenticated | -| 404 | Tag has no markers in the project; detail source unknown, or ts does not match the file (the capture may have been rebuilt) | +| 404 | Tag has no markers in the project; detail source unknown; `ts`/`frame_number` do not match the file (the capture may have been rebuilt); or the pcap vanished mid-request | | 409 | Tag gate: a marker under the tag is still `enabled: true` (the response lists them) | | 501 | sharkd not installed / unavailable — replay is unavailable, no degraded mode | -| 502 | sharkd failed or timed out (10 s per RPC) | +| 502 | sharkd failed, timed out (10 s per RPC), or answered out of sync — the session is killed and re-spawned on the next request | ## Notes @@ -293,7 +330,16 @@ All error bodies are `{"message": "…"}` (the app's unified format). natural follow-up if list latency ever matters at many-source scale. - **Session invalidation is cheap and total.** Every request stats the source pcap; a rewritten file (mtime/size change) respawns the session — a paused-but-restarted - capture can never serve stale dissect state. + capture can never serve stale dissect state. Transport failures are equally total: a + timeout, dead pipe, malformed or stale reply (id mismatch) kills the session instead + of leaving a poisoned one resident, and concurrent requests for the same pcap share a + single spawn. +- **The packet-list layout is pinned, not assumed.** sharkd runs with a scratch `HOME` + whose Wireshark preferences fix `gui.column.format` to exactly the four columns the + frame entries carry (personal config overrides any `/etc/wireshark` customization), so + the column indexes the server parses are a contract it owns. +- **Server shutdown kills every resident session** and drops its `/tmp` scratch + directory — nothing accumulates across restarts. - **Tag type.** REST and the `marker.match` WS event both carry `tag` as `int` (the listener normalizes); replay keys on that int value. - **Follow-ups.** Remote-compute support via the existing capture-file proxy pattern; diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index 66fe27c2c..26fb7d042 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -253,11 +253,12 @@ async def replay_tag_range( The tag gate applies: every marker under the tag must be paused (``enabled: false``) — 409 otherwise. The response carries the timeline - bounds, per-source stats, and the full merged frame list while under the - frame cap (5000); above it the list is replaced by per-second buckets. + bounds, per-source stats, and the full merged frame list — deliberately + uncapped (rendering a huge list is the client's concern; the window + endpoint exists for incremental views). ``filter`` is an optional Wireshark display filter applied **before** - counting and slicing — start / end / frame_count / frames | buckets are + counting and slicing — start / end / frame_count / frames are all computed on the matching frames only. An invalid expression is a 400 carrying sharkd's original error text (for inline display in the UI filter bar). @@ -321,6 +322,7 @@ async def replay_tag_frame_detail( node_id: str, link_id: str, marker: str, + frame_number: Optional[int] = None, project: Project = Depends(dep_project), ) -> dict: """ @@ -332,14 +334,18 @@ async def replay_tag_frame_detail( ``children``) — values untouched. ``ts`` must be the exact string from the frame list; ``node_id`` + - ``link_id`` + ``marker`` identify the source pcap. The tag gate applies. - Requires sharkd — 501 without it. + ``link_id`` + ``marker`` identify the source pcap. ``frame_number`` + (optional, from the frame list entry) disambiguates same-microsecond + frames on one link — without it the first ts match decodes. The tag gate + applies. Requires sharkd — 501 without it. Required privilege: Project.Audit """ return await _replay_response( - marker_replay.decode_frame(project, tag, ts, node_id, link_id, marker) + marker_replay.decode_frame( + project, tag, ts, node_id, link_id, marker, frame_number=frame_number + ) ) diff --git a/gns3server/controller/marker_replay.py b/gns3server/controller/marker_replay.py index 5a0bdc945..a3b9366e3 100644 --- a/gns3server/controller/marker_replay.py +++ b/gns3server/controller/marker_replay.py @@ -31,13 +31,18 @@ Python, but it is an implementation detail, not an availability promise. Session model — sharkd loads one file at a time, so there is one resident session **per source pcap**: spawned lazily on first use, fed a ``/tmp`` -scratch copy (hardened profiles deny sharkd the project directory; a real -copy, not a symlink, since the profile resolves real paths) with a scratch -``HOME``, validated per request against the original's ``(mtime, size)`` — +scratch directory (hardened profiles deny sharkd the project directory; a +real copy, not a symlink, since the profile resolves real paths) with a +scratch ``HOME`` whose Wireshark preferences pin the packet-list column +layout, validated per request against the original's ``(mtime, size)`` — a mismatch (e.g. the capture node restarted and uBridge truncated the pcap -while paused) kills and respawns the session. A per-pcap asyncio lock -serializes RPCs (sharkd serves one request at a time), each with a timeout. -An LRU cap bounds concurrent sessions. +while paused) kills and respawns the session. A per-session asyncio lock +serializes RPCs (sharkd serves one request at a time), each with a timeout +and an id check; any transport failure (timeout, dead pipe, malformed or +stale reply) kills the session for good — a desynced session must never +serve shifted results. A single manager lock makes check-spawn atomic (no +double spawn under concurrent requests) and sessions are refcounted while +in use, so the LRU cap only ever evicts idle ones. Timestamps are uBridge's userspace ``gettimeofday`` at match time (µs, a value measured after the packet has crossed the kernel twice — the last @@ -56,21 +61,20 @@ import shutil import struct import tempfile import time +from contextlib import asynccontextmanager from .controller_error import ControllerError, ControllerNotFoundError, ControllerBadRequestError log = logging.getLogger(__name__) -# Full frame list is embedded in the range response while under this cap; -# above it the response degrades to start/end + per-second buckets so the -# client is never flooded by a high-traffic BPF. -FRAME_LIST_CAP = 5000 - # One JSON-RPC per sharkd session, serialized by a per-session lock. RPC_TIMEOUT_SECONDS = 10.0 -# Resident sharkd sessions are bounded; least-recently-used evicted first. -SESSION_MAX = 8 +# Resident sharkd sessions are bounded; only IDLE sessions are ever evicted +# (least-recently-used first), so a request's own walk over a tag with more +# sources than the cap never kills a session out from under it — the +# population is trimmed back as uses drop instead. +SESSION_MAX = 16 # Display filters travel as one argv element (never through a shell) and are # capped to keep absurd expressions off the command line. @@ -79,6 +83,16 @@ FILTER_MAX_LENGTH = 2000 # Batch size when draining sharkd's `frames` RPC (columns / filter matches). _FRAMES_PAGE = 1000 +# A full `frames` page (1000 rows of pinned columns) measures ~190 KB against +# the 64 KB default StreamReader limit — over it, readline() raises and clears +# the stream, failing the request AND desyncing the resident session. Raise +# the ceiling well above any page (or single-frame tree) we can produce. +_STREAM_LIMIT_BYTES = 16 * 1024 * 1024 + +# sharkd's JSON-RPC error code for a rejected display filter — the only error +# that belongs to the client (400); everything else is an engine fault (502). +_ERR_INVALID_FILTER = -13002 + class SharkdMissingError(ControllerError): """sharkd is not installed (or not on PATH) — replay is unavailable (501).""" @@ -194,26 +208,47 @@ def read_frame_bytes(path, frame_number): # sharkd: process environment and scratch copies # --------------------------------------------------------------------------- -def _scratch_copy(pcap): +# The packet-list layout is pinned through Wireshark preferences in the +# scratch HOME: personal config overrides any system-wide customization +# (/etc/wireshark &c.), so the column indexes in _columns_for are a contract +# we own rather than an environment default. Exactly the four columns the +# frame entries consume — nothing else rides along in every page. +_PINNED_COLUMNS = ( + 'gui.column.format: "Source", "%s", "Destination", "%d", ' + '"Protocol", "%p", "Info", "%i"\n' +) + + +async def _prepare_scratch(pcap): """ - Copy the pcap to a scratch file under the system temp dir for sharkd to - load. Hardened profiles (AppArmor &c.) can deny it access to the project - directory / the user's home while still allowing /tmp — a real copy, - deliberately not a symlink, since the profile resolves real paths. - Caller must unlink the returned path. + Build a scratch directory under the system temp dir holding the pcap copy + for sharkd to load plus the pinned column preferences. Hardened profiles + (AppArmor &c.) can deny sharkd the project directory / the user's home + while still allowing /tmp — a real copy, deliberately not a symlink, + since the profile resolves real paths. Caller must rmtree the directory. + + :returns: ``(scratch_dir, scratch_pcap_path)`` """ - fd, scratch = tempfile.mkstemp(suffix=".pcap", prefix="gns3-replay-") - os.close(fd) - shutil.copyfile(pcap, scratch) - return scratch + scratch_dir = tempfile.mkdtemp(prefix="gns3-replay-") + scratch = os.path.join(scratch_dir, "capture.pcap") + # A pcap-sized copy has no business stalling the event loop — a 1 GB + # capture must not freeze every other request for the duration. + await asyncio.to_thread(shutil.copyfile, pcap, scratch) + prefs_dir = os.path.join(scratch_dir, ".config", "wireshark") + os.makedirs(prefs_dir, exist_ok=True) + with open(os.path.join(prefs_dir, "preferences"), "w") as f: + f.write(_PINNED_COLUMNS) + return scratch_dir, scratch -def _engine_env(): - """Scratch HOME so sharkd never even tries to read the user's home.""" +def _engine_env(scratch_dir): + """Scratch HOME (and XDG config) so sharkd never even tries to read the + user's home — and always reads OUR pinned preferences instead.""" env = dict(os.environ) - env["HOME"] = tempfile.gettempdir() + env["HOME"] = scratch_dir + env["XDG_CONFIG_HOME"] = os.path.join(scratch_dir, ".config") return env @@ -276,10 +311,13 @@ def _count_tree_nodes(value): class _SharkdSession: """A resident `sharkd -` process with one pcap loaded, addressed through line-oriented JSON-RPC. Serialized by an asyncio lock (sharkd serves one - request at a time).""" + request at a time). Any transport-level failure aborts the session for + good (see _abort) — a timed-out or desynced session must never answer + later requests with shifted results.""" - def __init__(self, pcap, scratch, proc, stat): + def __init__(self, pcap, scratch_dir, scratch, proc, stat): self.pcap = pcap + self.scratch_dir = scratch_dir self.scratch = scratch self.proc = proc self.mtime_ns = stat.st_mtime_ns @@ -287,6 +325,12 @@ class _SharkdSession: self.last_used = time.monotonic() self.lock = asyncio.Lock() self._next_id = 0 + # Refcount of in-use holders (the manager's `session` context); the + # LRU cap only ever evicts sessions with zero uses, and a detached + # session (replaced in the manager, e.g. its pcap was rebuilt) closes + # itself when its last holder releases it. + self._uses = 0 + self._detached = False def matches(self, stat): """True while the source pcap is byte-identical to what was loaded — @@ -303,26 +347,41 @@ class _SharkdSession: async def rpc(self, method, params): async with self.lock: self._next_id += 1 - request = {"jsonrpc": "2.0", "id": self._next_id, "method": method, "params": params} + request_id = self._next_id + request = {"jsonrpc": "2.0", "id": request_id, "method": method, "params": params} try: self.proc.stdin.write((json.dumps(request) + "\n").encode()) await self.proc.stdin.drain() raw = await asyncio.wait_for(self.proc.stdout.readline(), RPC_TIMEOUT_SECONDS) except asyncio.TimeoutError: - raise SharkdError(f"sharkd timed out after {RPC_TIMEOUT_SECONDS:.0f}s on {method!r}") - except OSError as e: - raise SharkdError(f"sharkd session died on {method!r}: {e}") + await self._abort(f"sharkd timed out after {RPC_TIMEOUT_SECONDS:.0f}s on {method!r}") + except (OSError, ValueError) as e: + # ValueError: a reply line over the StreamReader limit — the + # buffer is cleared, so the session is desynced either way. + await self._abort(f"sharkd session died on {method!r}: {e}") if not raw: - raise SharkdError(f"sharkd closed the session during {method!r}") + await self._abort(f"sharkd closed the session during {method!r}") try: response = json.loads(raw) except ValueError as e: - raise SharkdError(f"Malformed sharkd response: {e}") + await self._abort(f"Malformed sharkd response: {e}") + # The echoed id proves this reply belongs to THIS request: a stale + # reply left over from a timed-out predecessor (or any desync) + # must never be served as fresh data. + if response.get("id") != request_id: + await self._abort(f"sharkd reply id mismatch on {method!r} (session desynchronized)") if "error" in response: error = response["error"] raise _SharkdRpcError(error.get("code"), str(error.get("message", ""))) return response.get("result") + async def _abort(self, message): + """Kill the session for good (process + scratch copy) and raise — the + manager replaces it on the next acquire. Idempotent with close().""" + + await self.close() + raise SharkdError(message) + async def close(self): try: if self.proc.returncode is None: @@ -336,56 +395,112 @@ class _SharkdSession: log.warning("sharkd session for %s did not exit after kill", self.pcap) except ProcessLookupError: pass - try: - os.unlink(self.scratch) - except OSError: - pass + shutil.rmtree(self.scratch_dir, ignore_errors=True) class _SharkdManager: - """Resident sharkd sessions keyed by source pcap path, LRU-bounded.""" + """Resident sharkd sessions keyed by source pcap path. + + One asyncio lock covers check-spawn-insert-reap: the check-then-spawn + window spans the pcap copy, the fork and the load RPC, so without it two + concurrent requests for the same pcap double-spawn and the loser leaks + (process + scratch copy) forever. Sessions are refcounted while in use + (the `session` context manager) and the LRU cap only evicts IDLE + sessions — a tag with more sources than the cap, or a concurrent request, + can never have its session killed mid-RPC; the population is trimmed + back to the cap as uses drop (temporary overshoot is allowed).""" def __init__(self): self._sessions = {} + self._mu = asyncio.Lock() - async def session_for(self, pcap): + @asynccontextmanager + async def session(self, pcap): + session = await self._acquire(pcap) + try: + yield session + finally: + await self._release(session) + + async def _acquire(self, pcap): if shutil.which("sharkd") is None: raise SharkdMissingError( "sharkd is not available on this server — marker replay requires sharkd " "(part of the Wireshark package)" ) - stat = os.stat(pcap) - session = self._sessions.get(pcap) - if session is not None and session.matches(stat) and session.alive(): - session.touch() - return session - if session is not None: - await session.close() - del self._sessions[pcap] - # Bound resident sessions: evict the least recently used. - while len(self._sessions) >= SESSION_MAX: - victim = min(self._sessions.values(), key=lambda s: s.last_used) + try: + stat = os.stat(pcap) + except FileNotFoundError: + # Deleted between the directory listing and here (marker removed, + # project cleaned up) — not a server fault. + raise ControllerNotFoundError(f"Capture file {os.path.basename(pcap)} no longer exists") + async with self._mu: + session = self._sessions.get(pcap) + if session is not None and session.matches(stat) and session.alive(): + session._uses += 1 + session.touch() + return session + if session is not None: + # Rebuilt/truncated source or a dead session: replace it. An + # in-use one closes itself when its last holder releases it. + del self._sessions[pcap] + if session._uses == 0: + await session.close() + else: + session._detached = True + spawned = await self._spawn(pcap, stat) + spawned._uses += 1 + self._sessions[pcap] = spawned + victims = self._evict_locked() + for victim in victims: + await victim.close() + return spawned + + def _evict_locked(self): + """Caller holds ``_mu``. Pop (not close — that can block on the reap) + least-recently-used IDLE sessions down to the cap; if everything is + in use, allow the overshoot rather than killing a live request.""" + + victims = [] + while len(self._sessions) > SESSION_MAX: + idle = [s for s in self._sessions.values() if s._uses == 0] + if not idle: + break + victim = min(idle, key=lambda s: s.last_used) + del self._sessions[victim.pcap] + victims.append(victim) + return victims + + async def _release(self, session): + async with self._mu: + session._uses -= 1 + victims = [] + if session._uses == 0: + if session._detached: + # Replaced in the manager while still held — now orphaned. + victims.append(session) + elif not session.alive(): + # Failed mid-use (its RPC aborted it) — drop the corpse. + if self._sessions.get(session.pcap) is session: + del self._sessions[session.pcap] + else: + victims = self._evict_locked() + for victim in victims: await victim.close() - self._sessions.pop(victim.pcap, None) - session = await self._spawn(pcap, stat) - self._sessions[pcap] = session - return session async def _spawn(self, pcap, stat): - scratch = _scratch_copy(pcap) + scratch_dir, scratch = await _prepare_scratch(pcap) try: proc = await asyncio.create_subprocess_exec( "sharkd", "-", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.DEVNULL, env=_engine_env(), + stderr=asyncio.subprocess.DEVNULL, env=_engine_env(scratch_dir), + limit=_STREAM_LIMIT_BYTES, ) except OSError as e: - try: - os.unlink(scratch) - except OSError: - pass + shutil.rmtree(scratch_dir, ignore_errors=True) raise SharkdError(f"Could not run sharkd: {e}") - session = _SharkdSession(pcap, scratch, proc, stat) + session = _SharkdSession(pcap, scratch_dir, scratch, proc, stat) try: await session.rpc("load", {"file": scratch}) except Exception as e: @@ -394,9 +509,11 @@ class _SharkdManager: return session async def close_all(self): - for session in list(self._sessions.values()): + async with self._mu: + sessions = list(self._sessions.values()) + self._sessions.clear() + for session in sessions: await session.close() - self._sessions.clear() _manager = None @@ -409,6 +526,17 @@ def _get_manager(): return _manager +async def close_sessions(): + """Server shutdown hook: kill every resident session and drop its scratch + copy, so nothing leaks into /tmp across restarts.""" + + global _manager + manager = _manager + if manager is not None: + await manager.close_all() + _manager = None + + # --------------------------------------------------------------------------- # Columns + display filter (sharkd `frames` RPC) # --------------------------------------------------------------------------- @@ -419,42 +547,49 @@ async def _columns_for(pcap, filter_expr): ``{frame_number: {src, dst, proto, info, bg, fg}}``; with a ``filter_expr`` the keys are exactly the matching frame numbers, so one pass both filters and enriches the merge. + + The column layout is pinned by the scratch-HOME preferences (see + _PINNED_COLUMNS): ``c[0]``=src, ``c[1]``=dst, ``c[2]``=proto, ``c[3]``=info + is a layout we own, not an environment default. The whole pagination + loop holds the session so the LRU cap cannot evict it between pages. """ - session = await _get_manager().session_for(pcap) - columns = {} - skip = 0 - while True: - # sharkd rejects skip=0 ("must be a positive integer") — only send it - # once there is actually something to skip. - params = {"limit": _FRAMES_PAGE} - if skip: - params["skip"] = skip - if filter_expr is not None: - params["filter"] = filter_expr - try: - rows = await session.rpc("frames", params) - except _SharkdRpcError as e: + manager = _get_manager() + async with manager.session(pcap) as session: + columns = {} + skip = 0 + while True: + # sharkd rejects skip=0 ("must be a positive integer") — only send + # it once there is actually something to skip. + params = {"limit": _FRAMES_PAGE} + if skip: + params["skip"] = skip if filter_expr is not None: - raise FilterError(f"Invalid display filter: {e.message}") - raise SharkdError(f"sharkd frames failed on {os.path.basename(pcap)}: {e.message}") - for row in rows: + params["filter"] = filter_expr try: - frame_number = int(row.get("num")) - except (TypeError, ValueError): - continue - cols = row.get("c") or [] - columns[frame_number] = { - "src": cols[2] or None if len(cols) > 2 else None, - "dst": cols[3] or None if len(cols) > 3 else None, - "proto": cols[4] or None if len(cols) > 4 else None, - "info": cols[6] or None if len(cols) > 6 else None, - "bg": row.get("bg"), - "fg": row.get("fg"), - } - if len(rows) < _FRAMES_PAGE: - return columns - skip += len(rows) + rows = await session.rpc("frames", params) + except _SharkdRpcError as e: + if filter_expr is not None and e.code == _ERR_INVALID_FILTER: + raise FilterError(f"Invalid display filter: {e.message}") + # Anything else is an engine fault, not the client's filter. + raise SharkdError(f"sharkd frames failed on {os.path.basename(pcap)}: {e.message}") + for row in rows or []: + try: + frame_number = int(row.get("num")) + except (TypeError, ValueError): + continue + cols = row.get("c") or [] + columns[frame_number] = { + "src": cols[0] or None if len(cols) > 0 else None, + "dst": cols[1] or None if len(cols) > 1 else None, + "proto": cols[2] or None if len(cols) > 2 else None, + "info": cols[3] or None if len(cols) > 3 else None, + "bg": row.get("bg"), + "fg": row.get("fg"), + } + if len(rows or []) < _FRAMES_PAGE: + return columns + skip += len(rows or []) # --------------------------------------------------------------------------- @@ -572,39 +707,28 @@ def _validate_filter(filter_expr): ) -async def build_timeline(project, tag, frame_cap=FRAME_LIST_CAP, filter_expr=None, link_id=None): +async def build_timeline(project, tag, filter_expr=None, link_id=None): """ - The ``range`` response: timeline bounds, per-source stats, and (under - ``frame_cap``) the full merged frame list for one-request timeline - layout. Over the cap the list is replaced by per-second buckets. With a - ``filter_expr`` and/or a ``link_id`` every figure is computed on the - matching frames only (``sources`` stays the full tag inventory). + The ``range`` response: timeline bounds, per-source stats, and the full + merged frame list — deliberately uncapped (a flat list is the whole + contract; rendering a huge list is the client's concern, and the window + endpoint exists for incremental views). With a ``filter_expr`` and/or a + ``link_id`` every figure is computed on the matching frames only + (``sources`` stays the full tag inventory). """ _validate_filter(filter_expr) entries = gate_tag(project, tag) frames, sources = await _merged_frames(project, entries, filter_expr, link_id=link_id) - response = { + return { "tag": tag, "start": frames[0]["ts"] if frames else None, "end": frames[-1]["ts"] if frames else None, "frame_count": len(frames), - "truncated": len(frames) > frame_cap, "sources": sources, + "frames": frames, } - if len(frames) <= frame_cap: - response["frames"] = frames - else: - buckets = {} - for frame in frames: - second = _parse_ts(frame["ts"]) // 1_000_000 - buckets[second] = buckets.get(second, 0) + 1 - response["buckets"] = [ - {"ts": _format_ts(second, 0), "count": count} - for second, count in sorted(buckets.items()) - ] - return response async def query_frames(project, tag, ts, window_ms=100, limit=1000, filter_expr=None, link_id=None): @@ -629,13 +753,19 @@ async def query_frames(project, tag, ts, window_ms=100, limit=1000, filter_expr= # Frame detail (lazy — one frame per call, via the resident session) # --------------------------------------------------------------------------- -async def decode_frame(project, tag, ts, node_id, link_id, marker): +async def decode_frame(project, tag, ts, node_id, link_id, marker, frame_number=None): """ Decode exactly one frame: locate its pcap by source identity, verify the round-tripped ts still matches the file (guards a rebuild between the timeline view and this click), read the raw bytes for the hex view straight from the pcap, and rename the sharkd protocol tree into the REST contract (closed key set, values untouched). + + ``ts`` is not unique within one pcap (same-microsecond frames are kept + deliberately); an explicit ``frame_number`` from the frame list entry + disambiguates them and must still land on the exact ts. Without one the + first ts match decodes — fine unless two frames share a microsecond on + the same link. """ entries = gate_tag(project, tag) @@ -654,28 +784,38 @@ async def decode_frame(project, tag, ts, node_id, link_id, marker): raise ControllerNotFoundError(f"No capture file for marker '{marker}' (nothing ever matched)") frames = scan_pcap_frames(pcap) - # The ts must be the exact string the timeline returned; find the frame - # it identifies rather than trusting any position hint from the client. - frame_number = next( - (i for i, (sec, usec, _len) in enumerate(frames, start=1) - if _format_ts(sec, usec) == ts), - None, + rebuilt_message = ( + f"No frame at ts {ts} in marker '{marker}' (the capture may have been rebuilt)" ) if frame_number is None: - raise ControllerNotFoundError( - f"No frame at ts {ts} in marker '{marker}' (the capture may have been rebuilt)" + # The ts must be the exact string the timeline returned; find the frame + # it identifies rather than trusting any position hint from the client. + frame_number = next( + (i for i, (sec, usec, _len) in enumerate(frames, start=1) + if _format_ts(sec, usec) == ts), + None, ) + if frame_number is None: + raise ControllerNotFoundError(rebuilt_message) + else: + if not 1 <= frame_number <= len(frames): + raise ControllerNotFoundError(rebuilt_message) + sec, usec, _len = frames[frame_number - 1] + if _format_ts(sec, usec) != ts: + raise ControllerNotFoundError(rebuilt_message) raw_hex = read_frame_bytes(pcap, frame_number) - session = await _get_manager().session_for(pcap) - try: - result = await session.rpc("frame", {"frame": frame_number, "proto": True}) - except _SharkdRpcError as e: - if e.code == -8003: # frame number out of range — file changed under us - raise ControllerNotFoundError( - f"No frame at ts {ts} in marker '{marker}' (the capture may have been rebuilt)" - ) - raise SharkdError(f"sharkd frame failed: {e.message}") + if raw_hex is None: + # The header scan listed it but the bytes are gone — mid-write tail. + raise ControllerNotFoundError(rebuilt_message) + + async with _get_manager().session(pcap) as session: + try: + result = await session.rpc("frame", {"frame": frame_number, "proto": True}) + except _SharkdRpcError as e: + # The frame range was validated against the file above, so an + # engine error here is a real fault (502), not a client 404. + raise SharkdError(f"sharkd frame failed: {e.message}") tree = _rename_value(result.get("tree", [])) return { diff --git a/gns3server/core/tasks.py b/gns3server/core/tasks.py index f8e8dadc9..0b745a067 100644 --- a/gns3server/core/tasks.py +++ b/gns3server/core/tasks.py @@ -111,6 +111,11 @@ async def shutdown(app: FastAPI) -> None: auto_discover_images_task_handle.cancel() await HTTPClient.close_session() await MarkerManager.instance().stop() + # Kill resident sharkd sessions (marker replay) and drop their /tmp + # scratch copies before the process exits. + from gns3server.controller import marker_replay + + await marker_replay.close_sessions() await Controller.instance().stop() for module in MODULES: diff --git a/tests/api/routes/controller/test_marker_replay.py b/tests/api/routes/controller/test_marker_replay.py index 24fdf5c8e..b5db7398d 100644 --- a/tests/api/routes/controller/test_marker_replay.py +++ b/tests/api/routes/controller/test_marker_replay.py @@ -141,7 +141,6 @@ class TestReplayRoutes: assert body["frame_count"] == 4 assert body["start"] == "1693472000.500000" assert body["end"] == "1693472002.000000" - assert body["truncated"] is False assert [f["node_id"] for f in body["frames"]] == ["n1", "n2", "n1", "n2"] assert [f["ts"] for f in body["frames"]] == [ "1693472000.500000", "1693472001.000000", @@ -339,6 +338,36 @@ class TestReplayRoutes: assert ttl["filter_expr"] == "ip.ttl == 64" assert ttl["pos"] == 22 and ttl["size"] == 1 + @sharkd_present + async def test_detail_frame_number_disambiguates_same_ts( + self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions + ) -> None: + + # Two frames in the same microsecond: only the explicit frame number + # (from the frame list entry) tells them apart. + link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ + (1693472000, 123456, _icmp_frame()), + (1693472000, 123456, _tcp_syn_frame()), + ]) + common = {"ts": "1693472000.123456", "node_id": "n1", + "link_id": link.id, "marker": "icmp"} + + response = await client.get( + app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), + params=common, + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["source"]["frame_number"] == 1 + assert response.json()["hex"] == _icmp_frame().hex() + + response = await client.get( + app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), + params={**common, "frame_number": 2}, + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["source"]["frame_number"] == 2 + assert response.json()["hex"] == _tcp_syn_frame().hex() + @sharkd_present async def test_detail_501_when_sharkd_disappears( self, app: FastAPI, client: AsyncClient, project: Project diff --git a/tests/controller/test_marker_replay.py b/tests/controller/test_marker_replay.py index a2f992440..3c3ed252b 100644 --- a/tests/controller/test_marker_replay.py +++ b/tests/controller/test_marker_replay.py @@ -22,18 +22,22 @@ Unit tests for the tag-keyed aggregate replay module (sharkd edition): nanosecond-magic normalization) and raw-bytes reads for the hex view * the tag gate (404 unknown tag, 409 while any marker captures) — engine-free * timeline assembly with injected columns: cross-source merge ordered by - (ts, source, frame number), same-microsecond tiebreak, frame-cap - degradation to buckets, display-filter application before count/slice + (ts, source, frame number), same-microsecond tiebreak, the uncapped full + frame list, display-filter application before count/slice * the tree key renaming (closed census key set, values untouched, unknown keys pass through verbatim, internal hf ids dropped) * the resident sharkd sessions — spawn/load/reuse, (mtime, size) - invalidation, LRU bound — against the real sharkd where installed, plus - the frame detail end-to-end (hex + renamed tree + filter expressions). + invalidation, LRU bound (idle-only eviction), single spawn under + concurrent acquires, timeout/stale-reply aborts — against the real sharkd + where installed, plus the frame detail end-to-end (hex + renamed tree + + filter expressions, same-microsecond disambiguation). """ +import asyncio import os import shutil import struct +import sys from types import SimpleNamespace from unittest.mock import patch @@ -47,6 +51,7 @@ from gns3server.controller.controller_error import ( from gns3server.controller import marker_replay from gns3server.controller.marker_replay import ( FilterError, + SharkdError, SharkdMissingError, build_timeline, decode_frame, @@ -141,6 +146,30 @@ def _cols(src="10.0.0.1", dst="10.0.0.3", proto="ICMP", info="Echo (ping) reques "bg": "ffffff", "fg": "000000"} +class _FakeSession: + """Duck-typed _SharkdSession for manager-level tests (no engine).""" + + def __init__(self, pcap): + self.pcap = pcap + self.last_used = 0 + self.closed = False + self._uses = 0 + self._detached = False + self.mtime_ns, self.size = 0, 0 + + def matches(self, stat): + return True + + def alive(self): + return not self.closed + + def touch(self): + pass + + async def close(self): + self.closed = True + + # --------------------------------------------------------------------------- # pcap scanning (engine-free backbone) # --------------------------------------------------------------------------- @@ -268,7 +297,9 @@ class TestTimeline: assert timeline["frames"] == [] assert timeline["sources"][0]["count"] == 0 - async def test_over_cap_degrades_to_buckets(self, tmp_path, monkeypatch): + async def test_frame_list_is_uncapped(self, tmp_path, monkeypatch): + # The list is the whole contract — no truncation flag, no buckets, + # however many frames the tag holds. _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ (1693472000, 0, b"a" * 60), (1693472000, 500000, b"a" * 60), (1693472001, 0, b"a" * 60), @@ -276,13 +307,10 @@ class TestTimeline: _fake_columns(monkeypatch, {"n1_linkA_icmp.pcap": {1: _cols(), 2: _cols(), 3: _cols()}}) project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) - timeline = await build_timeline(project, tag=7, frame_cap=2) - assert timeline["truncated"] is True - assert "frames" not in timeline - assert timeline["buckets"] == [ - {"ts": "1693472000.000000", "count": 2}, - {"ts": "1693472001.000000", "count": 1}, - ] + timeline = await build_timeline(project, tag=7) + assert timeline["frame_count"] == 3 + assert len(timeline["frames"]) == 3 + assert "truncated" not in timeline and "buckets" not in timeline async def test_filter_applies_before_count_and_slice(self, tmp_path, monkeypatch): # Three frames; the injected "matching set" (what a real engine would @@ -445,43 +473,155 @@ class TestSessions: with pytest.raises(SharkdMissingError): await build_timeline(project, tag=7) - async def test_lru_bound_evicts_least_recently_used(self, tmp_path): + async def test_cap_evicts_idle_lru_only(self, tmp_path): manager = marker_replay._SharkdManager() - - class FakeSession: - def __init__(self, pcap, used): - self.pcap, self.last_used = pcap, used - self.closed = False - self.mtime_ns, self.size = 0, 0 - - def matches(self, stat): - return False # always respawn → exercises the eviction path - - def alive(self): - return False - - def touch(self): - pass - - async def close(self): - self.closed = True - - fake_by_pcap = {} + fakes = {} async def fake_spawn(pcap, stat): - session = FakeSession(pcap, 0) - fake_by_pcap[pcap] = session + session = _FakeSession(pcap) + fakes[pcap] = session return session with patch.object(manager, "_spawn", side_effect=fake_spawn): + # cap + 2 distinct pcaps, each acquired and released (idle again). for i in range(marker_replay.SESSION_MAX + 2): pcap = tmp_path / f"pcap{i}" _write_pcap(pcap, [(1693472000, 0, b"a" * 60)]) - await manager.session_for(str(pcap)) - # Bounded to SESSION_MAX; the earliest (least recently used) got evicted. + async with manager.session(str(pcap)): + pass + # Bounded to SESSION_MAX; the earliest (least recently used) closed. assert len(manager._sessions) == marker_replay.SESSION_MAX - assert fake_by_pcap[str(tmp_path / "pcap0")].closed is True - assert fake_by_pcap[str(tmp_path / "pcap1")].closed is True + assert fakes[str(tmp_path / "pcap0")].closed is True + assert fakes[str(tmp_path / "pcap1")].closed is True + assert fakes[str(tmp_path / "pcap2")].closed is False + + async def test_in_use_session_survives_cap_pressure(self, tmp_path): + manager = marker_replay._SharkdManager() + + async def fake_spawn(pcap, stat): + return _FakeSession(pcap) + + with patch.object(manager, "_spawn", side_effect=fake_spawn): + pcap0 = tmp_path / "pcap0" + _write_pcap(pcap0, [(1693472000, 0, b"a" * 60)]) + async with manager.session(str(pcap0)) as held: + # More sources than the cap while pcap0 is held open. + for i in range(1, marker_replay.SESSION_MAX + 4): + pcap = tmp_path / f"pcap{i}" + _write_pcap(pcap, [(1693472000, 0, b"a" * 60)]) + async with manager.session(str(pcap)): + pass + # A held session is never the eviction victim (no mid-RPC kill). + assert held.closed is False + assert str(pcap0) in manager._sessions + # Released → the population is trimmed back under the cap. + assert len(manager._sessions) <= marker_replay.SESSION_MAX + await manager.close_all() + + async def test_concurrent_acquire_spawns_once(self, tmp_path): + manager = marker_replay._SharkdManager() + spawns = [] + + async def slow_spawn(pcap, stat): + spawns.append(pcap) + await asyncio.sleep(0.05) # widen the check-then-spawn window + return _FakeSession(pcap) + + pcap = tmp_path / "n1_linkA_icmp.pcap" + _write_pcap(pcap, [(1693472000, 0, b"a" * 60)]) + with patch.object(manager, "_spawn", side_effect=slow_spawn): + first, second = await asyncio.gather( + manager._acquire(str(pcap)), manager._acquire(str(pcap)) + ) + assert first is second # concurrent requests share one spawn + assert len(spawns) == 1 + await manager.close_all() + + async def test_rpc_timeout_kills_session(self, tmp_path, monkeypatch): + monkeypatch.setattr(marker_replay, "RPC_TIMEOUT_SECONDS", 0.2) + pcap = tmp_path / "n1_linkA_icmp.pcap" + _write_pcap(pcap, [(1693472000, 0, b"a" * 60)]) + # A process that never answers: the session must die (never serve the + # late reply to a later request) instead of staying resident. + proc = await asyncio.create_subprocess_exec( + "sleep", "60", + stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, + ) + session = marker_replay._SharkdSession( + str(pcap), str(tmp_path / "scratch"), "unused", proc, os.stat(str(pcap)) + ) + with pytest.raises(SharkdError, match="timed out"): + await session.rpc("frames", {}) + assert session.alive() is False + assert proc.returncode is not None + + async def test_stale_reply_id_aborts_session(self, tmp_path): + # A "sharkd" that always answers with a foreign id — every reply is a + # stale one as far as the caller is concerned; serving it would be + # shifted data, so the session must abort instead. + fake = tmp_path / "fake_sharkd.py" + fake.write_text( + "import json, sys\n" + "for line in sys.stdin:\n" + " json.loads(line)\n" + " sys.stdout.write(json.dumps({'jsonrpc': '2.0', 'id': 424242, 'result': []}) + '\\n')\n" + " sys.stdout.flush()\n" + ) + pcap = tmp_path / "n1_linkA_icmp.pcap" + _write_pcap(pcap, [(1693472000, 0, b"a" * 60)]) + proc = await asyncio.create_subprocess_exec( + sys.executable, str(fake), + stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, + ) + session = marker_replay._SharkdSession( + str(pcap), str(tmp_path / "scratch"), "unused", proc, os.stat(str(pcap)) + ) + with pytest.raises(SharkdError, match="id mismatch"): + await session.rpc("frames", {}) + assert session.alive() is False + assert proc.returncode is not None + + async def test_filter_error_only_for_the_filter_code(self, tmp_path, monkeypatch): + manager = marker_replay._SharkdManager() + monkeypatch.setattr(marker_replay, "_manager", manager) + pcap = tmp_path / "n1_linkA_icmp.pcap" + _write_pcap(pcap, [(1693472000, 0, b"a" * 60)]) + + class _RpcFail(_FakeSession): + def __init__(self, pcap, code): + super().__init__(pcap) + self.code = code + + async def rpc(self, method, params): + raise marker_replay._SharkdRpcError(self.code, "boom") + + manager._sessions[str(pcap)] = _RpcFail(str(pcap), marker_replay._ERR_INVALID_FILTER) + with pytest.raises(FilterError): + await marker_replay._columns_for(str(pcap), "icmp") + # Same failure WITHOUT a filter is an engine fault, not a 400. + manager._sessions[str(pcap)] = _RpcFail(str(pcap), marker_replay._ERR_INVALID_FILTER) + with pytest.raises(SharkdError): + await marker_replay._columns_for(str(pcap), None) + # A non-filter engine error with a filter set is NOT the client's fault. + manager._sessions[str(pcap)] = _RpcFail(str(pcap), -9999) + with pytest.raises(SharkdError): + await marker_replay._columns_for(str(pcap), "icmp") + + @sharkd_present + async def test_real_pagination_over_1000_frames(self, tmp_path): + # A full page measures well over the 64 KB default StreamReader limit + # (~190 KB with realistic columns) — pagination must not blow up the + # stream (nor desync the resident session). + pcap = tmp_path / "big.pcap" + _write_pcap(pcap, [(1693472000 + i, 0, _icmp_frame()) for i in range(1001)]) + manager = marker_replay._get_manager() + try: + columns = await marker_replay._columns_for(str(pcap), None) + assert len(columns) == 1001 + assert columns[1001]["src"] == "10.0.0.1" + assert columns[1001]["proto"] == "ICMP" + finally: + await manager.close_all() @sharkd_present async def test_real_session_columns_and_filter(self, tmp_path): @@ -518,12 +658,14 @@ class TestSessions: _write_pcap(pcap, [(1693472000, 123456, _icmp_frame())]) manager = marker_replay._get_manager() try: - first = await manager.session_for(str(pcap)) - assert await manager.session_for(str(pcap)) is first # warm reuse + async with manager.session(str(pcap)) as first: + pass + async with manager.session(str(pcap)) as warm: + assert warm is first # warm reuse # Rewrite the file (simulating a truncated/rebuilt capture). _write_pcap(pcap, [(1693473000, 0, _icmp_frame())]) - second = await manager.session_for(str(pcap)) - assert second is not first + async with manager.session(str(pcap)) as second: + assert second is not first columns = await marker_replay._columns_for(str(pcap), None) assert set(columns) == {1} finally: @@ -552,6 +694,54 @@ class TestDecodeFrame: await decode_frame(project, tag=7, ts="1693472000.123456", node_id="nobody", link_id="linkA", marker="icmp") + async def test_explicit_frame_number_out_of_range_404(self, tmp_path): + project = self._project(tmp_path) + with pytest.raises(ControllerNotFoundError, match="rebuilt"): + await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="n1", link_id="linkA", marker="icmp", frame_number=5) + + async def test_explicit_frame_number_ts_mismatch_404(self, tmp_path): + # The frame number must still land on the exact round-tripped ts — + # a rebuilt capture cannot be decoded by stale coordinates. + project = self._project(tmp_path) + with pytest.raises(ControllerNotFoundError, match="rebuilt"): + await decode_frame(project, tag=7, ts="1693472001.000000", + node_id="n1", link_id="linkA", marker="icmp", frame_number=1) + + async def test_hex_read_failure_is_404_not_null_hex(self, tmp_path, monkeypatch): + project = self._project(tmp_path) + monkeypatch.setattr(marker_replay, "read_frame_bytes", lambda path, n: None) + with pytest.raises(ControllerNotFoundError, match="rebuilt"): + await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="n1", link_id="linkA", marker="icmp") + + @sharkd_present + async def test_same_microsecond_frames_disambiguated_by_frame_number(self, tmp_path): + # ts is not unique within one pcap: two frames share a microsecond, + # and only the explicit frame number tells them apart. + pcap = tmp_path / "n1_linkA_icmp.pcap" + icmp, tcp = _icmp_frame(), _tcp_syn_frame() + _write_pcap(pcap, [(1693472000, 123456, icmp), (1693472000, 123456, tcp)]) + project = _fake_project(tmp_path, { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + }) + manager = marker_replay._get_manager() + try: + default = await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="n1", link_id="linkA", marker="icmp") + second = await decode_frame(project, tag=7, ts="1693472000.123456", + node_id="n1", link_id="linkA", marker="icmp", + frame_number=2) + finally: + await manager.close_all() + # Without a frame number: first ts match. + assert default["source"]["frame_number"] == 1 + assert default["hex"] == icmp.hex() + # With one: exactly that frame's bytes and tree. + assert second["source"]["frame_number"] == 2 + assert second["hex"] == tcp.hex() + assert second["field_count"] > 10 + @sharkd_present async def test_decode_end_to_end(self, tmp_path): manager = marker_replay._get_manager() @@ -595,7 +785,6 @@ class TestDecodeFrame: ]) manager = marker_replay._get_manager() try: - session = await manager.session_for(str(pcap)) known = set(marker_replay._KEY_RENAME) | {"h", "e"} seen = set() @@ -608,9 +797,10 @@ class TestDecodeFrame: for item in value: walk(item) - for frame_number in (1, 2): - result = await session.rpc("frame", {"frame": frame_number, "proto": True}) - walk(result.get("tree", [])) + async with manager.session(str(pcap)) as session: + for frame_number in (1, 2): + result = await session.rpc("frame", {"frame": frame_number, "proto": True}) + walk(result.get("tree", [])) assert seen <= known, f"unknown sharkd keys appeared: {seen - known}" finally: await manager.close_all() From 673253d848cdd2ed92418fa67216cdb64bff258b Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 11 Sep 2026 23:49:51 +0800 Subject: [PATCH 40/60] ci: install sharkd in the test workflow and make manager tests hermetic The replay tests marked sharkd_present skip gracefully without the binary, but four manager-level tests (cap eviction, in-use protection, single spawn under concurrency, filter-error mapping) run entirely against fakes and died on the acquire-time PATH check instead - install wireshark-common (which ships /usr/bin/sharkd on the Ubuntu runner) so the real-engine tests actually run in CI, and patch the which() check in the fake-session tests so the suite stays green on machines without sharkd. --- .github/workflows/testing.yml | 4 ++++ tests/controller/test_marker_replay.py | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 20c1cf5ea..d0bfa4f1a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -30,6 +30,10 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install .[ai-features,dev] + - name: Install sharkd (marker replay engine) + run: | + sudo apt-get update + sudo apt-get install -y wireshark-common - name: Lint with flake8 run: | diff --git a/tests/controller/test_marker_replay.py b/tests/controller/test_marker_replay.py index 3c3ed252b..19888bbd0 100644 --- a/tests/controller/test_marker_replay.py +++ b/tests/controller/test_marker_replay.py @@ -170,6 +170,12 @@ class _FakeSession: self.closed = True +def _pretend_sharkd(monkeypatch): + """The manager-level tests run entirely against fakes — the sharkd binary + must not be a precondition (the suite stays green without the engine).""" + monkeypatch.setattr(marker_replay.shutil, "which", lambda name: "/usr/bin/sharkd") + + # --------------------------------------------------------------------------- # pcap scanning (engine-free backbone) # --------------------------------------------------------------------------- @@ -473,7 +479,8 @@ class TestSessions: with pytest.raises(SharkdMissingError): await build_timeline(project, tag=7) - async def test_cap_evicts_idle_lru_only(self, tmp_path): + async def test_cap_evicts_idle_lru_only(self, tmp_path, monkeypatch): + _pretend_sharkd(monkeypatch) manager = marker_replay._SharkdManager() fakes = {} @@ -495,7 +502,8 @@ class TestSessions: assert fakes[str(tmp_path / "pcap1")].closed is True assert fakes[str(tmp_path / "pcap2")].closed is False - async def test_in_use_session_survives_cap_pressure(self, tmp_path): + async def test_in_use_session_survives_cap_pressure(self, tmp_path, monkeypatch): + _pretend_sharkd(monkeypatch) manager = marker_replay._SharkdManager() async def fake_spawn(pcap, stat): @@ -518,7 +526,8 @@ class TestSessions: assert len(manager._sessions) <= marker_replay.SESSION_MAX await manager.close_all() - async def test_concurrent_acquire_spawns_once(self, tmp_path): + async def test_concurrent_acquire_spawns_once(self, tmp_path, monkeypatch): + _pretend_sharkd(monkeypatch) manager = marker_replay._SharkdManager() spawns = [] @@ -582,6 +591,7 @@ class TestSessions: assert proc.returncode is not None async def test_filter_error_only_for_the_filter_code(self, tmp_path, monkeypatch): + _pretend_sharkd(monkeypatch) manager = marker_replay._SharkdManager() monkeypatch.setattr(marker_replay, "_manager", manager) pcap = tmp_path / "n1_linkA_icmp.pcap" From 54d9d7c08fb1c2d418c8a2036194c7a84fe5d4fe Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 12 Sep 2026 23:04:28 +0800 Subject: [PATCH 41/60] fix: reclaim root-owned container files so docker nodes and projects can be deleted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stop-time permission pass necessarily runs before the container's processes exit, so files written during the shutdown window (syslog archives, trace flushes) and after any SIGKILL path stay owned by root on the host. An unprivileged server can neither chown nor delete them, which broke node deletion and project deletion. Reclaim them through the only privilege door a non-root server has: a one-shot throwaway container of the node's own image, entrypoint overridden to the GNS3 busybox (nothing of the guest boots), chowning the node directory back to the server user. It runs at the end of close() — project deletion rmtrees the directory right after the nodes close, so close must leave a clean tree — and as a retry fallback in delete(). The helper resolves the image by its create-time ID with --pull=never, so a stale or retagged image name cannot turn into a registry pull attempt. --- gns3server/compute/docker/docker_vm.py | 126 ++++++++++++++++++++++++- tests/compute/docker/test_docker_vm.py | 116 +++++++++++++++++++++++ 2 files changed, 241 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index b226a0f39..797f8fa7d 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -35,11 +35,13 @@ from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer from gns3server.utils.asyncio.raw_command_server import AsyncioRawCommandServer from gns3server.utils.asyncio import wait_for_file_creation from gns3server.utils.asyncio import monitor_process +from gns3server.utils.asyncio import wait_run_in_executor from gns3server.utils.get_resource import get_resource from gns3server.utils.hostname import is_rfc1123_hostname_valid from gns3server.utils import macaddress_to_int, int_to_macaddress from gns3server.compute.ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError +from gns3server.compute.compute_error import ComputeError from ..base_node import BaseNode from ..adapters.ethernet_adapter import EthernetAdapter @@ -148,6 +150,9 @@ class DockerVM(BaseNode): self._console_websocket = None self.extra_hosts = extra_hosts self._extra_volumes = extra_volumes or [] + # Image ID captured at create time (see _mount_binds): unlike the + # image tag it keeps resolving even if the tag is removed later. + self._image_id = None self._extra_configs = extra_configs or [] self._memory = memory self._cpus = cpus @@ -448,6 +453,7 @@ class DockerVM(BaseNode): "Target": "/gns3", "ReadOnly": True }] + self._image_id = image_info.get("Id") or self._image_id # We mount our own etc/network try: @@ -1024,6 +1030,101 @@ class DockerVM(BaseNode): else: self._permissions_fixed = True + def _directory_has_foreign_files(self, directory): + """ + Synchronous walk (run in an executor): return True as soon as an + entry not owned by the server user is found under ``directory``. + What we are looking for are files the container wrote as root — + the unprivileged server can neither chown nor delete them. + """ + + uid = os.getuid() + for root, dirs, files in os.walk(directory): + for entry in dirs + files: + try: + if os.stat(os.path.join(root, entry)).st_uid != uid: + return True + except OSError: + # An entry we cannot stat is not ours to delete either; + # treat it as foreign and let the reclaim sort it out. + return True + return False + + async def _reclaim_directory_ownership(self, directory): + """ + Best-effort reclaim of files the container left owned by root. + + The stop-time permission pass necessarily runs before the + container's processes exit, so anything they write during the + shutdown window (and everything after a SIGKILL path) lands as + root-owned on the host. An unprivileged server cannot chown or + delete those files, but it can ask Docker to run a throwaway + container of the node's own image — entrypoint overridden to the + GNS3 busybox, so nothing of the guest boots — and chown the tree + back from the container's root side. Docker is the only privilege + door a non-root server has. + + :returns: True when ``directory`` is (now) owned by the server + user, False when the reclaim could not run or failed. + """ + + if not os.path.exists(directory): + return True + try: + if not await wait_run_in_executor(self._directory_has_foreign_files, directory): + log.debug(f"Docker container '{self._name}': no foreign-owned files under '{directory}'") + return True + except OSError as e: + log.warning(f"Docker container '{self._name}': could not inspect '{directory}' for root-owned files: {e}") + return False + + uid, gid = os.getuid(), os.getgid() + try: + resources_path = self.manager.resources_path() + except OSError as e: + log.warning(f"Docker container '{self._name}': cannot access resources to reclaim '{directory}': {e}") + return False + + log.info(f"Docker container '{self._name}': reclaiming root-owned files under '{directory}' via a one-shot container") + # Prefer the image's own chown over the static busybox one: busybox's + # chown dlopens NSS modules from the image, which mismatch the static + # glibc and abort on NOS images whose glibc differs (same reasoning + # as the container-side pass in _fix_permissions). --pull=never keeps + # a stale tag reference from turning into a registry pull attempt, + # and the create-time image ID (when known) is immune to retagging. + image_ref = self._image_id or self._image + try: + process = await asyncio.subprocess.create_subprocess_exec( + "docker", "run", "--rm", "--network", "none", "--pull", "never", + "--entrypoint", "/gns3/bin/busybox", + "-v", f"{resources_path}:/gns3:ro", + "-v", f"{directory}:/target", + image_ref, + "sh", "-c", + "/gns3/bin/busybox chmod -R u+rwX /target" + f" && ( command -v chown >/dev/null 2>&1 && chown {uid}:{gid} -R /target" + f" || /gns3/bin/busybox chown {uid}:{gid} -R /target )", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + except OSError as e: + log.warning(f"Docker container '{self._name}': could not reclaim ownership of '{directory}': {e}") + return False + try: + _, stderr = await asyncio.wait_for(process.communicate(), timeout=60) + except asyncio.TimeoutError: + process.kill() + await process.communicate() + log.warning(f"Docker container '{self._name}': reclaiming '{directory}' timed out") + return False + if process.returncode != 0: + log.warning( + f"Docker container '{self._name}': reclaiming '{directory}' failed: " + f"{stderr.decode(errors='replace').strip() or f'exit code {process.returncode}'}" + ) + return False + return True + async def _start_vnc_process(self, restart=False): """ Starts the VNC process. @@ -1350,6 +1451,13 @@ class DockerVM(BaseNode): if not (await super().close()): return False await self.reset() + # Whatever the container's processes wrote after the stop-time + # permission pass (the shutdown window, or any SIGKILL path) is + # still owned by root on the host and would make the node + # directory undeletable for the unprivileged server. Reclaim it + # before close() returns: project deletion rmtrees the directory + # right after the nodes close. + await self._reclaim_directory_ownership(self.working_dir) async def reset(self, release_nio_udp_ports=True): @@ -1928,4 +2036,20 @@ class DockerVM(BaseNode): """ await self.close() - await super().delete() + try: + await super().delete() + except ComputeError as e: + # close() already reclaimed once; the retry covers states where + # it could not run (e.g. Docker was down) and Docker is back now. + if await self._reclaim_directory_ownership(self.working_dir): + try: + await super().delete() + return + except ComputeError: + pass + raise ComputeError( + f"Could not delete the node directory '{self.working_dir}': files left owned by " + f"root could not be reclaimed ({e}). Reclaim them manually with: " + f"docker run --rm -v \"{self.working_dir}\":/target --entrypoint /bin/sh " + f"{self._image} -c 'chown -R {os.getuid()}:{os.getgid()} /target'" + ) diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 6c6e109a1..8d04b5f83 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -22,10 +22,12 @@ import pytest_asyncio import uuid import os +from types import SimpleNamespace from unittest.mock import patch from tests.utils import asyncio_patch, AsyncioMagicMock from gns3server.compute.ubridge.ubridge_error import UbridgeNamespaceError +from gns3server.compute.compute_error import ComputeError from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error from gns3server.compute.docker import Docker @@ -2037,6 +2039,120 @@ async def test_fix_permission_not_running(vm): assert process.wait.called +# --------------------------------------------------------------------------- +# Ownership reclaim for files the container left owned by root +# --------------------------------------------------------------------------- + +def _reclaim_proc(returncode=0, stderr=b""): + + process = MagicMock() + process.communicate = AsyncioMagicMock(return_value=(b"", stderr)) + process.returncode = returncode + process.kill = MagicMock() + return process + + +@pytest.mark.asyncio +async def test_reclaim_skips_clean_directory(vm, tmp_path): + + (tmp_path / "file").write_text("owned by the server user") + with patch("asyncio.subprocess.create_subprocess_exec") as mock_exec: + assert await vm._reclaim_directory_ownership(str(tmp_path)) is True + mock_exec.assert_not_called() + + +@pytest.mark.asyncio +async def test_reclaim_skips_missing_directory(vm): + + with patch("asyncio.subprocess.create_subprocess_exec") as mock_exec: + assert await vm._reclaim_directory_ownership("/does/not/exist") is True + mock_exec.assert_not_called() + + +def test_directory_has_foreign_files_detects_foreign_owner(vm, tmp_path): + + (tmp_path / "root-owned").write_text("written by the container as root") + assert vm._directory_has_foreign_files(str(tmp_path)) is False + + # A directory the unprivileged server user cannot own: pretend every + # stat reports another uid (the walk only uses scandir for recursion). + with patch("os.stat", return_value=SimpleNamespace(st_uid=os.getuid() + 1)): + assert vm._directory_has_foreign_files(str(tmp_path)) is True + + +@pytest.mark.asyncio +async def test_reclaim_runs_helper_container(vm, tmp_path): + + directory = tmp_path / "node" + directory.mkdir() + vm._image_id = "sha256:8731fa5e0f0f" + + with patch.object(vm, "_directory_has_foreign_files", return_value=True): + with patch.object(vm.manager, "resources_path", return_value="/gns3-share"): + with patch("asyncio.subprocess.create_subprocess_exec", return_value=_reclaim_proc()) as mock_exec: + assert await vm._reclaim_directory_ownership(str(directory)) is True + + args, _ = mock_exec.call_args + assert args[:7] == ("docker", "run", "--rm", "--network", "none", "--pull", "never") + assert args[args.index("--entrypoint") + 1] == "/gns3/bin/busybox" + assert "/gns3-share:/gns3:ro" in args + assert f"{directory}:/target" in args + # The create-time image ID is used as the helper image (immune to retagging) + # and sits right before the CMD ("sh -c …"). + assert args[args.index("sh") - 1] == "sha256:8731fa5e0f0f" + script = args[args.index("-c") + 1] + assert "chown" in script and f"{os.getuid()}:{os.getgid()}" in script + assert "chmod -R u+rwX" in script + + +@pytest.mark.asyncio +async def test_reclaim_fails_when_helper_errors(vm, tmp_path): + + directory = tmp_path / "node" + directory.mkdir() + + with patch.object(vm, "_directory_has_foreign_files", return_value=True): + with patch.object(vm.manager, "resources_path", return_value="/gns3-share"): + with patch("asyncio.subprocess.create_subprocess_exec", + return_value=_reclaim_proc(returncode=1, stderr=b"chown: failed")): + assert await vm._reclaim_directory_ownership(str(directory)) is False + + +@pytest.mark.asyncio +async def test_close_reclaims_node_directory(vm, port_manager): + + with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="stopped"): + with asyncio_patch("gns3server.compute.docker.Docker.query"): + with patch.object(vm, "_reclaim_directory_ownership", new_callable=AsyncioMagicMock) as mock_reclaim: + await vm.close() + mock_reclaim.assert_called_once_with(vm.working_dir) + + +@pytest.mark.asyncio +async def test_delete_retries_after_reclaim(vm): + + with patch.object(vm, "close", new_callable=AsyncioMagicMock): + with patch.object(vm, "_reclaim_directory_ownership", new_callable=AsyncioMagicMock, return_value=True) as mock_reclaim: + # First rmtree hits the root-owned leftovers, the retry (after + # the reclaim) succeeds. + with patch("gns3server.compute.base_node.shutil.rmtree", + side_effect=[OSError("permission denied"), None]) as mock_rmtree: + await vm.delete() + assert mock_rmtree.call_count == 2 + mock_reclaim.assert_called_once_with(vm.working_dir) + + +@pytest.mark.asyncio +async def test_delete_reports_root_files_when_reclaim_fails(vm): + + with patch.object(vm, "close", new_callable=AsyncioMagicMock): + with patch.object(vm, "_reclaim_directory_ownership", new_callable=AsyncioMagicMock, return_value=False): + with patch("gns3server.compute.base_node.shutil.rmtree", + side_effect=OSError("permission denied")): + with pytest.raises(ComputeError, match="owned by root"): + await vm.delete() + + @pytest.mark.asyncio async def test_read_console_output_with_binary_mode(vm): From 4b239cc11a72211b198c0e9460c6bd2526e2b494 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Sat, 12 Sep 2026 23:15:52 +0800 Subject: [PATCH 42/60] fix: run the reclaim helper as root regardless of the image's default USER MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one-shot reclaim container inherited the image's baked-in USER: ghcr.io/nokia/srlinux runs as "user:user", so the "privileged" helper was exactly as unprivileged as the server itself — chmod/chown on files written by other uids (srlinux writes as a large internal uid) failed with EPERM and node/project deletion still broke, just with a different error. Pass --user 0:0 explicitly so the helper is root no matter what the image declares, and fix the manual reclaim hint the same way. Validated live on two stuck srlinux node directories (257/258 foreign-owned entries reclaimed to 0 in ~0.5 s each). --- gns3server/compute/docker/docker_vm.py | 10 +++++++--- tests/compute/docker/test_docker_vm.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 797f8fa7d..7f4812e7b 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -1092,10 +1092,14 @@ class DockerVM(BaseNode): # as the container-side pass in _fix_permissions). --pull=never keeps # a stale tag reference from turning into a registry pull attempt, # and the create-time image ID (when known) is immune to retagging. + # --user 0:0 overrides a USER baked into the image (e.g. + # ghcr.io/nokia/srlinux runs as "user"): without it the "privileged" + # helper is exactly as unprivileged as the server itself and + # chmod/chown fail with EPERM on files written by other uids. image_ref = self._image_id or self._image try: process = await asyncio.subprocess.create_subprocess_exec( - "docker", "run", "--rm", "--network", "none", "--pull", "never", + "docker", "run", "--rm", "--network", "none", "--pull", "never", "--user", "0:0", "--entrypoint", "/gns3/bin/busybox", "-v", f"{resources_path}:/gns3:ro", "-v", f"{directory}:/target", @@ -2049,7 +2053,7 @@ class DockerVM(BaseNode): pass raise ComputeError( f"Could not delete the node directory '{self.working_dir}': files left owned by " - f"root could not be reclaimed ({e}). Reclaim them manually with: " - f"docker run --rm -v \"{self.working_dir}\":/target --entrypoint /bin/sh " + f"another user could not be reclaimed ({e}). Reclaim them manually with: " + f"docker run --rm --user 0:0 -v \"{self.working_dir}\":/target --entrypoint /bin/sh " f"{self._image} -c 'chown -R {os.getuid()}:{os.getgid()} /target'" ) diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 8d04b5f83..92e790913 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -2093,7 +2093,7 @@ async def test_reclaim_runs_helper_container(vm, tmp_path): assert await vm._reclaim_directory_ownership(str(directory)) is True args, _ = mock_exec.call_args - assert args[:7] == ("docker", "run", "--rm", "--network", "none", "--pull", "never") + assert args[:9] == ("docker", "run", "--rm", "--network", "none", "--pull", "never", "--user", "0:0") assert args[args.index("--entrypoint") + 1] == "/gns3/bin/busybox" assert "/gns3-share:/gns3:ro" in args assert f"{directory}:/target" in args @@ -2149,7 +2149,7 @@ async def test_delete_reports_root_files_when_reclaim_fails(vm): with patch.object(vm, "_reclaim_directory_ownership", new_callable=AsyncioMagicMock, return_value=False): with patch("gns3server.compute.base_node.shutil.rmtree", side_effect=OSError("permission denied")): - with pytest.raises(ComputeError, match="owned by root"): + with pytest.raises(ComputeError, match="owned by another user"): await vm.delete() From c47781233253fc99ac003de1feaefb5eb0fe61e9 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 14 Sep 2026 00:36:54 +0800 Subject: [PATCH 43/60] feat: sync Docker images from the controller to remote computes When a Docker node is created on a remote compute whose Docker daemon does not have the image, the compute now raises ImageMissingError instead of blindly pulling from the Docker repository. The controller exports the image from the Docker daemon on its host (docker save stream) and streams it to the compute which loads it, so locally built or docker-loaded images work across computes. When the image is not available on the controller host either, the compute is asked to pull it from the Docker repository as a fallback. - add a POST /docker/images/load compute endpoint that streams a docker save tar into the Docker daemon - let Docker.http_query pass raw (non-dict) request bodies through so the tar can be streamed to the daemon - drop the inline pull from DockerVM.create() and the now unused DockerVM.pull_image wrapper --- gns3server/api/routes/compute/images.py | 10 ++++ gns3server/compute/docker/__init__.py | 63 +++++++++++++++++++++++-- gns3server/compute/docker/docker_vm.py | 18 ++----- gns3server/controller/node.py | 40 ++++++++++++++++ tests/api/routes/compute/test_images.py | 13 +++++ tests/compute/docker/test_docker.py | 49 +++++++++++++++++++ tests/compute/docker/test_docker_vm.py | 62 ++---------------------- tests/controller/test_node.py | 35 +++++++++++++- 8 files changed, 215 insertions(+), 75 deletions(-) diff --git a/gns3server/api/routes/compute/images.py b/gns3server/api/routes/compute/images.py index 2bde73906..fd49974a1 100644 --- a/gns3server/api/routes/compute/images.py +++ b/gns3server/api/routes/compute/images.py @@ -53,6 +53,16 @@ async def pull_docker_image(image: str = Body(..., embed=True, min_length=1, pat await docker_manager.pull_image(image, force=True) +@router.post("/docker/images/load", status_code=status.HTTP_204_NO_CONTENT) +async def load_docker_image(request: Request) -> None: + """ + Load a Docker image into the Docker daemon from a docker save tar stream. + """ + + docker_manager = Docker.instance() + await docker_manager.load_image(request.stream()) + + @router.get("/dynamips/images") async def get_dynamips_images() -> List[dict]: """ diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index f4027ea59..39ca9f20a 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -283,7 +283,12 @@ class Docker(BaseManager): :returns: HTTP response """ - data = json.dumps(data) + if isinstance(data, dict): + data = json.dumps(data) + headers = {"content-type": "application/json"} + else: + # not a dict (e.g. a Docker image tar stream): let aiohttp stream the raw body + headers = {"content-type": "application/x-tar"} if timeout is None: timeout = 60 * 60 * 24 * 31 # One month timeout @@ -301,9 +306,7 @@ class Docker(BaseManager): url, params=params, data=data, - headers={ - "content-type": "application/json", - }, + headers=headers, timeout=timeout, ) except aiohttp.ClientError as e: @@ -410,6 +413,58 @@ class Docker(BaseManager): if progress_callback: progress_callback(f"Success pulling image {image}") + @locking + async def load_image(self, stream, progress_callback=None): + """ + Load a Docker image into the Docker daemon from a docker save tar stream + + :param stream: An async iterable of bytes (the tar produced by docker save) + :param progress_callback: A function that receive a log message about image load progress + """ + + if progress_callback: + progress_callback("Loading Docker image from stream") + response = await self.http_query("POST", "images/load", data=stream, timeout=None) + # The load api will stream status via an HTTP JSON stream + content = "" + try: + while True: + try: + chunk = await response.content.read(CHUNK_SIZE) + except aiohttp.ServerDisconnectedError as e: + raise DockerError("Disconnected while loading Docker image") from e + except asyncio.TimeoutError as e: + raise DockerError("Timeout while loading Docker image") from e + if not chunk: + break + content += chunk.decode("utf-8", errors="ignore") + + try: + while True: + content = content.lstrip(" \r\n\t") + answer, index = json.JSONDecoder().raw_decode(content) + if not isinstance(answer, dict): + raise DockerError("Invalid response while loading Docker image") + error_detail = answer.get("errorDetail") + error = answer.get("error") + if not error and isinstance(error_detail, dict): + error = error_detail.get("message") + if error: + raise DockerError(error) + if "stream" in answer and progress_callback: + progress_callback(answer["stream"].rstrip()) + content = content[index:] + except ValueError: # Partial JSON + pass + + if content.strip(): + raise DockerError("Invalid response while loading Docker image") + finally: + response.close() + + if progress_callback: + progress_callback("Docker image loaded") + async def list_images(self): """ Gets Docker image list. diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 7f4812e7b..c364bc817 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -47,6 +47,7 @@ from ..base_node import BaseNode from ..adapters.ethernet_adapter import EthernetAdapter from ..nios.nio_udp import NIOUDP from .docker_error import DockerError, DockerHttp304Error, DockerHttp404Error, DockerHttp409Error +from ..error import ImageMissingError import logging @@ -574,9 +575,10 @@ class DockerVM(BaseNode): try: image_infos = await self._get_image_information() except DockerHttp404Error: - log.info("Image '{}' is missing, pulling it from Docker repository...".format(self._image)) - await self.pull_image(self._image) - image_infos = await self._get_image_information() + # the image is not on the local Docker daemon: raise ImageMissingError so the + # controller can sync it (docker save -> load from the controller host, or pull) + # and retry the node creation + raise ImageMissingError(self._image) if image_infos is None: raise DockerError(f"Cannot get information for image '{self._image}', please try again.") @@ -1943,16 +1945,6 @@ class DockerVM(BaseNode): ) ) - async def pull_image(self, image): - """ - Pulls an image from Docker repository - """ - - def callback(msg): - self.project.emit("log.info", {"message": msg}) - - await self.manager.pull_image(image, progress_callback=callback) - async def _start_ubridge_capture(self, adapter_number, output_file, port_number=0): """ Starts a packet capture in uBridge. diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index c9189f8d6..b674d1e6b 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -770,6 +770,9 @@ class Node: if the image exists """ + if self._node_type == "docker": + return await self._sync_missing_docker_image(img) + for directory in images_directories(type): image = os.path.join(directory, img) if os.path.exists(image): @@ -785,6 +788,43 @@ class Node: return True return False + async def _sync_missing_docker_image(self, image): + """ + Export a Docker image from the Docker daemon on the controller host and + stream it to the remote compute, or ask the compute to pull it when the + image is not available locally + """ + + from gns3server.compute.docker import Docker + from gns3server.compute.docker.docker_error import DockerError + + try: + response = await Docker.instance().http_query("GET", f"images/{image}/get", timeout=None) + except DockerError: + # the image is not on the Docker daemon of the controller host: ask the + # compute to pull it from the Docker repository as a fallback + self.project.emit_notification( + "log.info", + {"message": f"Docker image '{image}' is not on the controller host, " + f"asking compute '{self._compute.name}' to pull it"} + ) + await self._compute.post("/docker/images/pull", data={"image": image}, timeout=None) + return True + + self.project.emit_notification( + "log.info", + {"message": f"Syncing Docker image '{image}' to compute '{self._compute.name}'"} + ) + try: + await self._compute.post("/docker/images/load", data=response.content, timeout=None) + finally: + response.close() + self.project.emit_notification( + "log.info", + {"message": f"Docker image '{image}' has been synced to compute '{self._compute.name}'"} + ) + return True + async def dynamips_auto_idlepc(self): """ Compute the idle PC for a dynamips node diff --git a/tests/api/routes/compute/test_images.py b/tests/api/routes/compute/test_images.py index d4331557c..9aff40a3d 100644 --- a/tests/api/routes/compute/test_images.py +++ b/tests/api/routes/compute/test_images.py @@ -47,3 +47,16 @@ class TestImagesRoutes: ) mock.assert_not_called() assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT + + async def test_load_docker_image(self, app: FastAPI, compute_client: AsyncClient) -> None: + + with asyncio_patch("gns3server.compute.docker.Docker.load_image") as mock: + response = await compute_client.post( + app.url_path_for("compute:load_docker_image"), + content=b"docker-save-tar-bytes" + ) + mock.assert_called_once() + # the tar is streamed to the Docker daemon as an async iterable + stream = mock.call_args[0][0] + assert hasattr(stream, "__aiter__") + assert response.status_code == status.HTTP_204_NO_CONTENT diff --git a/tests/compute/docker/test_docker.py b/tests/compute/docker/test_docker.py index 7fbb49c06..c00d6c11b 100644 --- a/tests/compute/docker/test_docker.py +++ b/tests/compute/docker/test_docker.py @@ -255,6 +255,55 @@ async def test_pull_image_propagates_timeout(): response.close.assert_called_once() +@pytest.mark.asyncio +async def test_load_image(): + + class Content: + + def __init__(self): + self._chunks = [b'{"stream": "Loaded image ID: sha256:e90e34656806"}', b""] + + async def read(self, size): + return self._chunks.pop(0) + + response = MagicMock() + response.content = Content() + + async def tar_stream(): + yield b"tar-bytes" + + stream = tar_stream() + messages = [] + with asyncio_patch("gns3server.compute.docker.Docker.http_query", return_value=response) as mock: + await Docker.instance().load_image(stream, progress_callback=messages.append) + mock.assert_called_with("POST", "images/load", data=stream, timeout=None) + response.close.assert_called_once() + assert any("Loaded image" in message for message in messages) + + +@pytest.mark.asyncio +async def test_load_image_error(): + + class Content: + + def __init__(self): + self._chunks = [b'{"error": "invalid tar file"}', b""] + + async def read(self, size): + return self._chunks.pop(0) + + response = MagicMock() + response.content = Content() + + async def tar_stream(): + yield b"not-a-tar" + + with asyncio_patch("gns3server.compute.docker.Docker.http_query", return_value=response): + with pytest.raises(DockerError, match="invalid tar file"): + await Docker.instance().load_image(tar_stream()) + response.close.assert_called_once() + + @pytest.mark.asyncio async def test_docker_check_connection_docker_minimum_version(vm): diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 92e790913..a5d44d82b 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -30,6 +30,7 @@ from gns3server.compute.ubridge.ubridge_error import UbridgeNamespaceError from gns3server.compute.compute_error import ComputeError from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error +from gns3server.compute.error import ImageMissingError from gns3server.compute.docker import Docker @@ -555,65 +556,12 @@ async def test_create_environment_with_last_new_line_character(compute_project, @pytest.mark.asyncio async def test_create_image_not_available(compute_project, manager): - call = 0 - async def information(): - nonlocal call - if call == 0: - call += 1 - raise DockerHttp404Error("missing") - else: - return {} - - response = { - "Id": "e90e34656806", - "Warnings": [] - } - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu") - vm._get_image_information = MagicMock() - vm._get_image_information.side_effect = information - with asyncio_patch("gns3server.compute.docker.DockerVM.pull_image", return_value=True) as mock_pull: - with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: + vm._get_image_information = MagicMock(side_effect=DockerHttp404Error("missing")) + with asyncio_patch("gns3server.compute.docker.Docker.query") as query_mock: + with pytest.raises(ImageMissingError, match="ubuntu:latest"): await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { - "CapAdd": ["ALL"], - "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, - { - "Type": "bind", - "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" - } - ], - "Privileged": True, - "Memory": 0, - "NanoCpus": 0, - "UsernsMode": "host" - }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) - assert vm._cid == "e90e34656806" - mock_pull.assert_called_with("ubuntu:latest") + query_mock.assert_not_called() @pytest.mark.asyncio diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index e4ed15bd6..b650470e7 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -21,7 +21,8 @@ import uuid import os from unittest.mock import MagicMock, ANY -from tests.utils import AsyncioMagicMock +from tests.utils import AsyncioMagicMock, asyncio_patch +from gns3server.compute.docker.docker_error import DockerError from gns3server.controller.node import Node from gns3server.controller.project import Project @@ -693,6 +694,38 @@ async def test_upload_missing_image(compute, controller, images_dir): compute.post.assert_called_with("/qemu/images/linux.img", data=ANY, timeout=None) +@pytest.mark.asyncio +async def test_sync_missing_docker_image_from_controller_daemon(compute, controller): + + project = Project(str(uuid.uuid4()), controller=controller) + node = Node(project, compute, "demo", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"image": "gns3/frr:latest"}) + + response = MagicMock() + response.content = MagicMock() + with asyncio_patch("gns3server.compute.docker.Docker.http_query", return_value=response) as save_mock: + assert await node._upload_missing_image("docker", "gns3/frr:latest") is True + save_mock.assert_called_with("GET", "images/gns3/frr:latest/get", timeout=None) + compute.post.assert_called_with("/docker/images/load", data=response.content, timeout=None) + response.close.assert_called_once_with() + + +@pytest.mark.asyncio +async def test_sync_missing_docker_image_pull_fallback(compute, controller): + + project = Project(str(uuid.uuid4()), controller=controller) + node = Node(project, compute, "demo", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"image": "nginx:latest"}) + + with asyncio_patch("gns3server.compute.docker.Docker.http_query", side_effect=DockerError("404")): + assert await node._upload_missing_image("docker", "nginx:latest") is True + compute.post.assert_called_with("/docker/images/pull", data={"image": "nginx:latest"}, timeout=None) + + def test_update_label(node): """ The text in label need to be always the From be62e8c022d9d3d51f7ce0c48ed7e3f8f7202aa6 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Mon, 14 Sep 2026 00:55:18 +0800 Subject: [PATCH 44/60] feat: keep Docker images on computes consistent with the controller host Only relying on the image name lets a moved tag (e.g. a newer :latest) silently serve stale content from a compute that already has an image under the same name. When creating a Docker node, the controller now pins the image id (Id from the Docker daemon on the controller host) into the create payload. A compute holding a different image under the same tag reports the image as missing, which routes it through the image sync added by the previous commit and re-aligns the tag. No new template fields or database changes: the controller host daemon remains the source of truth and the pin is resolved per creation. When the image is not available on the controller host the pin is omitted and behavior is unchanged (the compute pulls from the repository). --- gns3server/api/routes/compute/docker_nodes.py | 3 +- gns3server/compute/docker/docker_vm.py | 15 +++++++++ gns3server/controller/node.py | 24 ++++++++++++++ gns3server/schemas/compute/docker_nodes.py | 6 ++++ tests/compute/docker/test_docker_vm.py | 32 +++++++++++++++++++ tests/controller/test_node.py | 22 +++++++++++++ 6 files changed, 101 insertions(+), 1 deletion(-) diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index 4c7f319c3..a5e48c57d 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -81,6 +81,7 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) extra_configs=node_data.get("extra_configs"), memory=node_data.get("memory", 0), cpus=node_data.get("cpus", 0), + image_digest=node_data.get("image_digest"), ) # Pop keys already consumed by create_node above so the setattr # fallback loop below only applies truly extra keys and does not @@ -89,7 +90,7 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) "console", "console_type", "console_resolution", "console_http_port", "console_http_path", "aux", "aux_type", "start_command", "environment", "adapters", "mac_address", "extra_hosts", "extra_volumes", "extra_configs", - "memory", "cpus", + "memory", "cpus", "image_digest", ): node_data.pop(key, None) for name, value in node_data.items(): diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index c364bc817..bfcc53a7e 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -122,6 +122,7 @@ class DockerVM(BaseNode): extra_configs=None, memory=0, cpus=0, + image_digest=None, ): if not is_rfc1123_hostname_valid(name): @@ -135,6 +136,10 @@ class DockerVM(BaseNode): if ":" not in image: image = f"{image}:latest" self._image = image + # image id expected by the controller (set when the image is available on + # the controller host): a different local image under the same tag is + # treated as missing so the controller re-syncs the expected version + self._image_digest = image_digest # assign through the property setters so creation and updates apply # the same value normalization (e.g. "" -> None) self.start_command = start_command @@ -583,6 +588,16 @@ class DockerVM(BaseNode): if image_infos is None: raise DockerError(f"Cannot get information for image '{self._image}', please try again.") + if self._image_digest and image_infos.get("Id") != self._image_digest: + # the tag exists but points to a different image (e.g. a moved :latest): + # report it as missing so the controller re-syncs the expected version + local_id = image_infos.get("Id") + log.info( + f"Image '{self._image}' version mismatch on this compute: " + f"local id '{local_id}' != expected '{self._image_digest}'" + ) + raise ImageMissingError(self._image) + available_cpus = psutil.cpu_count(logical=True) if self._cpus > available_cpus: raise DockerError( diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index b674d1e6b..452dfb984 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -465,6 +465,7 @@ class Node: data["node_id"] = self._id if self._node_type == "docker": timeout = None + await self._add_docker_image_digest(data) else: timeout = 1200 trial = 0 @@ -788,6 +789,29 @@ class Node: return True return False + async def _add_docker_image_digest(self, data): + """ + Pin the Docker image of a node to the image id found on the Docker daemon + of the controller host, so that a compute holding a different image under + the same tag (e.g. a moved :latest) gets it re-synced instead of silently + reusing the stale copy. Not set when the image is not on the controller host. + """ + + from gns3server.compute.docker import Docker + from gns3server.compute.docker.docker_error import DockerError + + image = data.get("image") + if not image: + return + try: + image_info = await Docker.instance().query("GET", f"images/{image}/json") + except DockerError: + # not available on the controller host: nothing to pin against + return + image_id = image_info.get("Id") + if image_id: + data["image_digest"] = image_id + async def _sync_missing_docker_image(self, image): """ Export a Docker image from the Docker daemon on the controller host and diff --git a/gns3server/schemas/compute/docker_nodes.py b/gns3server/schemas/compute/docker_nodes.py index e2023d407..d752b7586 100644 --- a/gns3server/schemas/compute/docker_nodes.py +++ b/gns3server/schemas/compute/docker_nodes.py @@ -75,6 +75,12 @@ class DockerCreate(DockerBase): application_id: Optional[int] = Field( None, ge=1, le=1022, description="IOL application ID for iol-runner images (allocated by the controller)" ) + image_digest: Optional[str] = Field( + None, pattern=r"^sha256:[a-f0-9]{64}$", + description="Image id the controller expects for 'image' (sha256:, resolved from the Docker " + "daemon on the controller host). When set and the compute holds a different image " + "under the same tag, the image is reported as missing so the controller re-syncs it" + ) class DockerUpdate(DockerBase): diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index a5d44d82b..51359b024 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -564,6 +564,38 @@ async def test_create_image_not_available(compute_project, manager): query_mock.assert_not_called() +@pytest.mark.asyncio +async def test_create_image_digest_match(compute_project, manager): + + response = { + "Id": "sha256:" + "a" * 64, + "Warnings": [] + } + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: + vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", + image_digest="sha256:" + "a" * 64) + await vm.create() + # the last query is the container creation: the digest check let it through + assert mock.call_args[0] == ("POST", "containers/create?name={}".format(vm.docker_name)) + assert vm._cid == "sha256:" + "a" * 64 + + +@pytest.mark.asyncio +async def test_create_image_digest_mismatch(compute_project, manager): + + response = { + "Id": "sha256:" + "b" * 64, + "Warnings": [] + } + vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", + image_digest="sha256:" + "a" * 64) + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as query_mock: + with pytest.raises(ImageMissingError, match="ubuntu:latest"): + await vm.create() + # only the image inspect happened: no container was created from the stale image + query_mock.assert_called_once_with("GET", "images/ubuntu:latest/json") + + @pytest.mark.asyncio async def test_create_with_user(compute_project, manager): diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index b650470e7..b62784052 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -726,6 +726,28 @@ async def test_sync_missing_docker_image_pull_fallback(compute, controller): compute.post.assert_called_with("/docker/images/pull", data={"image": "nginx:latest"}, timeout=None) +@pytest.mark.asyncio +async def test_create_docker_node_pins_image_digest(compute, controller): + + project = Project(str(uuid.uuid4()), controller=controller) + node = Node(project, compute, "demo", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"image": "gns3/frr:latest", "adapters": 1}) + + response = MagicMock() + response.status = 200 + response.json = {} + compute.post = AsyncioMagicMock(return_value=response) + + image_id = "sha256:" + "a" * 64 + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": image_id}): + assert await node.create() is True + # the image id from the controller host daemon is pinned into the create payload + data = compute.post.call_args[1]["data"] + assert data["image_digest"] == image_id + + def test_update_label(node): """ The text in label need to be always the From 84e7ead465345ff687e875859f6ee7f20d6f3f65 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 15 Sep 2026 01:14:44 +0800 Subject: [PATCH 45/60] fix: make node working directory deletion robust The rmtree error handler in BaseNode.delete() was chmod'ing the failed path to S_IWRITE (0o200). On POSIX this strips the search permission from directories, turning a transient deletion failure into a directory that can no longer be traversed or deleted. The node deletion itself silently "succeeds" (rmtree gives up once its error handler returns) and a later project deletion then fails with EACCES. The handler also never retried the failed operation, so it did not help on Windows either (the platform it was written for). The transient failure exists in practice: a concurrent MD5 checksum computation caching its result in the node directory (e.g. a properties request racing the deletion) can recreate a file after rmtree has listed the directory, making the final rmdir fail with ENOTEMPTY. - add the missing user permissions instead of replacing the whole mode, and retry the failed unlink/rmdir - retry the whole deletion a few times to absorb files recreated while the directory is being deleted - raise a ComputeError when the directory cannot be fully deleted instead of failing silently --- gns3server/compute/base_node.py | 33 ++++++++- tests/compute/docker/test_docker_vm.py | 15 ++++- tests/compute/test_base_node.py | 92 ++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 4 deletions(-) diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 65b816e5c..30fa856c5 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -340,14 +340,43 @@ class BaseNode: """ def set_rw(operation, name, exc): - os.chmod(name, stat.S_IWRITE) + # Add the missing user permissions instead of replacing the whole mode: chmod'ing + # a directory to S_IWRITE removes the search permission on POSIX systems, making + # the directory impossible to traverse or to delete. Also note that S_IWRITE + # clears the read-only attribute on Windows. + try: + if os.path.isdir(name) and not os.path.islink(name): + os.chmod(name, os.stat(name).st_mode | stat.S_IRWXU) + elif not os.path.islink(name): + os.chmod(name, os.stat(name).st_mode | stat.S_IRUSR | stat.S_IWUSR) + # retry the failed operation now that the permissions are fixed + # (retrying os.scandir is not possible, the directory is handled by the + # retry loop below) + if operation in (os.unlink, os.rmdir): + operation(name) + except OSError: + pass directory = self.project.node_working_directory(self) - if os.path.exists(directory): + # Retry the deletion: a concurrent task (e.g. a MD5 checksum computation caching + # its result in the node directory) can recreate a file while the directory is + # being deleted, and shutil.rmtree silently gives up when its error handler returns + for attempt in range(3): + if not os.path.exists(directory): + return try: await wait_run_in_executor(shutil.rmtree, directory, onerror=set_rw) except OSError as e: raise ComputeError(f"Could not delete the node working directory: {e}") + if not os.path.exists(directory): + return + if attempt == 2: + raise ComputeError( + f"Could not delete the node working directory '{directory}': a file may have been " + "recreated in it or could not be removed" + ) + log.warning(f"Could not completely delete the node working directory '{directory}', retrying") + await asyncio.sleep(0.1) def start(self): """ diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 51359b024..5ad4eb4f8 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -21,6 +21,7 @@ import pytest import pytest_asyncio import uuid import os +import shutil from types import SimpleNamespace from unittest.mock import patch @@ -2111,12 +2112,22 @@ async def test_close_reclaims_node_directory(vm, port_manager): @pytest.mark.asyncio async def test_delete_retries_after_reclaim(vm): + real_rmtree = shutil.rmtree + calls = 0 + + def rmtree_first_fails_then_deletes(directory, onerror=None, **kwargs): + nonlocal calls + calls += 1 + if calls == 1: + raise OSError("permission denied") + real_rmtree(directory, onerror=onerror) + with patch.object(vm, "close", new_callable=AsyncioMagicMock): with patch.object(vm, "_reclaim_directory_ownership", new_callable=AsyncioMagicMock, return_value=True) as mock_reclaim: # First rmtree hits the root-owned leftovers, the retry (after - # the reclaim) succeeds. + # the reclaim) succeeds and really deletes the directory. with patch("gns3server.compute.base_node.shutil.rmtree", - side_effect=[OSError("permission denied"), None]) as mock_rmtree: + side_effect=rmtree_first_fails_then_deletes) as mock_rmtree: await vm.delete() assert mock_rmtree.call_count == 2 mock_reclaim.assert_called_once_with(vm.working_dir) diff --git a/tests/compute/test_base_node.py b/tests/compute/test_base_node.py index 7eca945e1..6420d905b 100644 --- a/tests/compute/test_base_node.py +++ b/tests/compute/test_base_node.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os +import shutil from collections import OrderedDict import pytest @@ -24,6 +25,7 @@ import pytest_asyncio from tests.utils import asyncio_patch, AsyncioMagicMock from unittest.mock import patch, MagicMock +from gns3server.compute.compute_error import ComputeError from gns3server.compute.vpcs.vpcs_vm import VPCSVM from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.error import NodeError @@ -507,3 +509,93 @@ async def test_console_websocket_client_disconnect_while_node_output_streams( "has disconnected from compute console WebSocket while node output" in r.message for r in caplog.records ) + + +@pytest.mark.asyncio +async def test_delete_node_working_directory(node): + + working_dir = node.working_dir + with open(os.path.join(working_dir, "test.txt"), "w") as f: + f.write("TEST") + await node.delete() + assert not os.path.exists(working_dir) + + +@pytest.mark.asyncio +async def test_delete_directory_without_user_permissions(node): + # regression test: a failed deletion must not chmod the directory to S_IWRITE (0o200), + # which removes the search permission and makes the directory undeletable + working_dir = node.working_dir + with open(os.path.join(working_dir, "test.txt"), "w") as f: + f.write("TEST") + os.chmod(working_dir, 0o200) + try: + await node.delete() + assert not os.path.exists(working_dir) + finally: + # restore the permissions so a failed test does not leave an undeletable + # directory behind on the shared project path + if os.path.exists(working_dir): + os.chmod(working_dir, 0o700) + + +@pytest.mark.asyncio +async def test_delete_directory_with_readonly_entries(node): + + working_dir = node.working_dir + with open(os.path.join(working_dir, "test.txt"), "w") as f: + f.write("TEST") + os.chmod(os.path.join(working_dir, "test.txt"), 0o000) + os.chmod(working_dir, 0o500) # remove the write permission + try: + await node.delete() + assert not os.path.exists(working_dir) + finally: + if os.path.exists(working_dir): + os.chmod(working_dir, 0o700) + + +@pytest.mark.asyncio +async def test_delete_directory_with_file_recreated_during_deletion(node, monkeypatch): + # regression test: a concurrent MD5 checksum computation can cache its result in the + # node directory while it is being deleted, recreating a file after shutil.rmtree + # has listed the directory (rmtree then silently gives up on the final rmdir) + working_dir = node.working_dir + with open(os.path.join(working_dir, "hda_disk_image.md5sum"), "w") as f: + f.write("0" * 32) + real_rmtree = shutil.rmtree + calls = 0 + + def rmtree_recreating_a_file(directory, onerror=None, **kwargs): + nonlocal calls + calls += 1 + if calls == 1: + # delete the entries but leave one behind, as if the final rmdir + # had failed with ENOTEMPTY after the error handler returned + for entry in os.listdir(directory): + os.remove(os.path.join(directory, entry)) + with open(os.path.join(directory, "hda_disk_image.md5sum"), "w") as f: + f.write("0" * 32) + return + real_rmtree(directory, onerror=onerror, **kwargs) + + monkeypatch.setattr("gns3server.compute.base_node.shutil.rmtree", rmtree_recreating_a_file) + await node.delete() + assert calls == 2 + assert not os.path.exists(working_dir) + + +@pytest.mark.asyncio +async def test_delete_directory_failure_raises(node, monkeypatch): + + working_dir = node.working_dir + with open(os.path.join(working_dir, "test.txt"), "w") as f: + f.write("TEST") + + def rmtree_not_deleting(directory, onerror=None, **kwargs): + pass # simulate a persistent deletion failure + + monkeypatch.setattr("gns3server.compute.base_node.shutil.rmtree", rmtree_not_deleting) + with pytest.raises(ComputeError): + await node.delete() + assert os.path.exists(working_dir) From 59a367b88d8ea01972a996952447f2f189c5d97e Mon Sep 17 00:00:00 2001 From: Cristi Date: Fri, 18 Sep 2026 15:06:17 +0300 Subject: [PATCH 46/60] Allow projects containing missing images to open in a degraded state instead of failing to load --- gns3server/controller/node.py | 213 ++++++++++++++- gns3server/controller/project.py | 91 ++++++- gns3server/controller/udp_link.py | 13 + gns3server/schemas/controller/nodes.py | 17 ++ tests/api/routes/controller/test_nodes.py | 13 + tests/controller/test_node.py | 313 +++++++++++++++++++++- tests/controller/test_project.py | 243 ++++++++++++++++- tests/controller/test_udp_link.py | 38 +++ 8 files changed, 923 insertions(+), 18 deletions(-) diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 452dfb984..8b7b83193 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -40,6 +40,26 @@ import logging log = logging.getLogger(__name__) +# Image properties of a node, mapped by node type. The value is the image +# "type" used by the image manager (see utils.images). Docker images are not +# registered in the image database, they are matched by tag instead. +IMAGE_PROPERTIES_BY_NODE_TYPE = { + "qemu": { + "hda_disk_image": "qemu", + "hdb_disk_image": "qemu", + "hdc_disk_image": "qemu", + "hdd_disk_image": "qemu", + "cdrom_image": "qemu", + "initrd": "qemu", + "kernel_image": "qemu", + "bios_image": "qemu", + }, + "dynamips": {"image": "ios"}, + "iou": {"path": "iou"}, + "docker": {"image": "docker"}, +} + + def _extract_iol_startup_config_knob(environment): """ Split the GNS3_IOL_STARTUP_CONFIG knob out of a Docker environment @@ -143,6 +163,11 @@ class Node: self._netmiko_device_type = None self._default_username = None self._default_password = None + # Set when the node cannot be created on its compute because one or + # more of its images are missing. The node is kept on the controller + # (topology and links are preserved) until the user provides a + # compatible replacement image. + self._missing_images = [] # This properties will be recomputed ignore_properties = ("width", "height", "hover_symbol") @@ -187,6 +212,18 @@ class Node: def status(self): return self._status + @property + def missing_image(self): + """ + :returns: True if at least one image required by this node is missing + and the node could therefore not be created on its compute. + """ + return len(self._missing_images) > 0 + + @property + def missing_images(self): + return self._missing_images + @property def template_id(self): return self._template_id @@ -457,9 +494,14 @@ class Node: def links(self): return self._links - async def create(self): + async def create(self, allow_missing_image=False): """ Create the node on the compute + + :param allow_missing_image: when True, if an image is missing and + cannot be uploaded/synced, the node is not created on the compute + but is kept on the controller and flagged with the missing image(s) + instead of raising. """ data = self._node_data() data["node_id"] = self._id @@ -469,6 +511,8 @@ class Node: else: timeout = 1200 trial = 0 + last_missing_image = None + last_missing_error = None while trial != 6: try: response = await self._compute.post( @@ -477,17 +521,126 @@ class Node: except ComputeConflictError as e: response = e.response() if response.get("exception") == "ImageMissingError": - res = await self._upload_missing_image(self._node_type, response["image"]) + last_missing_error = e + last_missing_image = response.get("image") + res = False + if last_missing_image: + try: + res = await self._upload_missing_image(self._node_type, last_missing_image) + except ControllerError as upload_error: + # For Docker the image is pulled from a registry: a + # failed pull (unknown tag, no network, private image) + # must not abort the whole project. Treat it as a + # missing image so the user can pick another one. + if not allow_missing_image: + raise + log.warning( + "Could not provide missing image '%s' for node '%s' [%s]: %s", + last_missing_image, self._name, self._id, upload_error + ) if not res: + if allow_missing_image: + missing_images = self._compute_missing_images(last_missing_image) + # A degraded node must always identify at least one + # missing image. Otherwise the controller would keep + # a node that was never created on the compute while + # exposing it as healthy. + if not missing_images: + raise e + self._missing_images = missing_images + log.warning( + "Node '%s' [%s] is kept in degraded state, missing image(s): %s", + self._name, self._id, ", ".join(m["image"] for m in self._missing_images) + ) + return False raise e else: raise e else: await self.parse_node_response(response.json) + self._missing_images = [] return True trial += 1 + if allow_missing_image: + # The image was repeatedly reported missing (e.g. the upload/sync + # seemed to succeed but did not). Keep the node in degraded state. + self._missing_images = self._compute_missing_images(last_missing_image) + log.warning( + "Node '%s' [%s] could not be created, missing image(s): %s", + self._name, self._id, ", ".join(m["image"] for m in self._missing_images) + ) + elif last_missing_error is not None: + # Uploading appeared to succeed, but the compute rejected every + # retry. Do not let the caller register a controller-only node as + # healthy. + raise last_missing_error return False + def _image_available(self, image_type, image): + """ + Check whether an image is available on the controller (and can + therefore be uploaded to the compute when needed). + + :param image_type: image type (e.g. "qemu", "ios", "iou") + :param image: image filename or path + """ + + if not image: + return True + if image_type == "docker": + # Docker images are not stored on the controller filesystem + return True + try: + directories = images_directories(image_type) + except NotImplementedError: + return True + for directory in directories: + if os.path.exists(os.path.join(directory, image)): + return True + return False + + def _compute_missing_images(self, fallback_image=None): + """ + Build the list of images referenced by this node that are not + available on the controller. + + :param fallback_image: image reported as missing by the compute, always + included (used for Docker images and images not directly mapped to + a node property). + """ + + missing = [] + mapping = IMAGE_PROPERTIES_BY_NODE_TYPE.get(self._node_type, {}) + properties = self._properties or {} + for prop, image_type in mapping.items(): + value = properties.get(prop) + if not value: + continue + if not self._image_available(image_type, value): + missing.append({"property": prop, "image": value, "image_type": image_type}) + if fallback_image: + prop = next((p for p in mapping if properties.get(p) == fallback_image), None) + if prop is None: + fallback_basename = os.path.basename(fallback_image) + prop = next( + ( + p + for p in mapping + if properties.get(p) and os.path.basename(properties[p]) == fallback_basename + ), + None, + ) + if prop is None and mapping: + prop = next(iter(mapping)) + already_reported = any( + m["image"] == fallback_image or (prop is not None and m["property"] == prop) + for m in missing + ) + if not already_reported: + image_type = mapping.get(prop, self._node_type) + missing.append({"property": prop, "image": fallback_image, "image_type": image_type}) + return missing + async def update(self, **kwargs): """ Update the node on the compute @@ -499,6 +652,9 @@ class Node: update_compute = False old_json = self.asdict() old_name = self._name + old_properties = copy.deepcopy(self._properties) + old_custom_adapters = copy.deepcopy(self._custom_adapters) + old_missing_images = copy.deepcopy(self._missing_images) compute_properties = None # Update node properties with additional elements @@ -522,8 +678,32 @@ class Node: if compute_properties and "custom_adapters" in compute_properties: # we need to check custom adapters to update the custom port names self.custom_adapters = compute_properties["custom_adapters"] + if self.missing_image and compute_properties is not None: + # The node was never created on the compute (missing image). Apply + # the new properties locally so create() can use them. + self._properties = compute_properties self._list_ports() if update_compute: + if self.missing_image: + # Try to create the node on the compute now that an image may + # have been provided. If it is still missing the node simply + # remains in its degraded state. + try: + created = await self.create(allow_missing_image=True) + except Exception: + # create() can reject the replacement for reasons other + # than a missing image. Keep the controller state aligned + # with the node that is still absent from the compute. + self._properties = old_properties + self._custom_adapters = old_custom_adapters + self._missing_images = old_missing_images + self._list_ports() + raise + if created: + await self.project.restore_deferred_links(self) + self.project.emit_notification("node.updated", self.asdict()) + self.project.dump() + return data = self._node_data(properties=compute_properties) try: response = await self.put(None, data=data) @@ -666,6 +846,25 @@ class Node: """ Start a node """ + if self.missing_image: + images = ", ".join(m["image"] for m in self._missing_images) + raise ControllerError( + f"Cannot start node '{self._name}': the required image(s) ({images}) " + f"are missing. Please provide a compatible image." + ) + deferred_links = [link for link in self.links if link.deferred] + if deferred_links: + await self.project.restore_deferred_links(self) + failed_links = [ + link + for link in deferred_links + if link.deferred and not any(endpoint["node"].missing_image for endpoint in link._nodes) + ] + if failed_links: + raise ControllerError( + f"Cannot start node '{self._name}': {len(failed_links)} deferred link(s) " + "could not be restored. Please try again." + ) try: # For IOU: we need to send the licence everytime we start a node if self.node_type == "iou": @@ -681,6 +880,8 @@ class Node: """ Stop a node """ + if self.missing_image: + return try: await self.post("/stop", timeout=240, dont_connect=True) # We don't care if a node is down at this step @@ -756,6 +957,10 @@ class Node: """ HTTP post on the node """ + if self.missing_image and path is None: + # The node was never created on the compute (missing image). Any + # partial object there is cleaned up when the project is closed. + return None if path is None: return await self._compute.delete( f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}", **kwargs @@ -1018,7 +1223,9 @@ class Node: "status": self._status, "console_host": str(self._compute.console_host), "node_directory": self._node_directory, - "ports": [port.asdict() for port in self.ports] + "ports": [port.asdict() for port in self.ports], + "missing_image": self.missing_image, + "missing_images": self._missing_images, } topology.update(additional_data) return topology diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index b69a587b6..24511d40b 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -632,7 +632,7 @@ class Project: node = await self.add_node(compute, name, node_id, node_type=node_type, **template) return node - async def _create_node(self, compute, name, node_id, node_type=None, **kwargs): + async def _create_node(self, compute, name, node_id, node_type=None, allow_missing_image=False, **kwargs): node = Node(self, compute, name, node_id=node_id, node_type=node_type, **kwargs) # Hold the lock across the check + POST + register so that concurrent @@ -651,17 +651,21 @@ class Project: await compute.post("/projects", data=data) self._project_created_on_compute.add(compute) - await node.create() + await node.create(allow_missing_image=allow_missing_image) self._nodes[node.id] = node return node @open_required - async def add_node(self, compute, name, node_id, dump=True, node_type=None, **kwargs): + async def add_node( + self, compute, name, node_id, dump=True, node_type=None, allow_missing_image=False, **kwargs + ): """ Create a node or return an existing node :param dump: Dump topology to disk + :param allow_missing_image: Keep the node on the controller in a + degraded state instead of failing when its image is missing :param kwargs: See the documentation of node """ @@ -683,7 +687,9 @@ class Project: ) elif "application_id" not in kwargs.keys() and not kwargs.get("properties"): kwargs["application_id"] = get_next_application_id(self._controller.projects, self._computes) - node = await self._create_node(compute, name, node_id, node_type, **kwargs) + node = await self._create_node( + compute, name, node_id, node_type, allow_missing_image=allow_missing_image, **kwargs + ) elif node_type == "docker" and _is_iol_docker_kwargs(kwargs): # IOL Docker nodes derive interface MACs from the application ID # exactly like IOU; they draw from the disjoint upper half of the @@ -700,9 +706,13 @@ class Project: kwargs["application_id"] = get_next_application_id( self._controller.projects, self._computes, iol_docker=True ) - node = await self._create_node(compute, name, node_id, node_type, **kwargs) + node = await self._create_node( + compute, name, node_id, node_type, allow_missing_image=allow_missing_image, **kwargs + ) else: - node = await self._create_node(compute, name, node_id, node_type, **kwargs) + node = await self._create_node( + compute, name, node_id, node_type, allow_missing_image=allow_missing_image, **kwargs + ) self.emit_notification("node.created", node.asdict()) if dump: self.dump() @@ -975,6 +985,20 @@ class Project: # a link should have 2 attached nodes, this can happen with corrupted projects await self.delete_link(link.id, force_delete=True) return None + if any(n["node"].missing_image for n in link._nodes): + # One of the endpoints could not be created on its compute because + # an image is missing. Keep the link on the controller (so the + # topology is preserved) but defer the NIO creation until the + # missing image is resolved. + for n in link._nodes: + n["node"].add_link(link) + n["port"].link = link + link._deferred = True + log.info( + "Project '%s' [%s]: deferring link %s until missing image(s) are resolved", + self._name, self._id, link.id, + ) + return None # Apply project-level marker definitions onto the link's memory # (memory_only) before _prepare() so the inherited markers ride the # batch NIO dispatch — zero extra HTTP round-trips. The final @@ -987,6 +1011,33 @@ class Project: entries = await link._prepare() return (link, entries) + async def restore_deferred_links(self, node): + """ + Create on the computes the NIOs of the links that were deferred while + one of their endpoints had a missing image. Called once the node has + been successfully created. + + :param node: node that has just been created on its compute + """ + + restored = [] + for link in list(node.links): + if not link.deferred: + continue + if any(n["node"].missing_image for n in link._nodes): + # the other endpoint is still missing an image + continue + try: + await link.create() + link._deferred = False + restored.append(link) + except Exception as e: + log.exception("Could not restore deferred link %s: %s", link.id, e) + for link in restored: + self.emit_notification("link.updated", link.asdict()) + if restored: + self.dump() + @open_required async def add_link(self, link_id=None, dump=True): """ @@ -1867,7 +1918,15 @@ class Project: log.info("Project '%s' [%s]: loading %d nodes...", self._name, self._id, len(nodes_to_create)) pool = Pool(concurrency=100) for compute, name, node_id, node_data in nodes_to_create: - pool.append(self.add_node, compute, name, node_id, dump=False, **node_data) + pool.append( + self.add_node, + compute, + name, + node_id, + dump=False, + allow_missing_image=True, + **node_data, + ) await pool.join() log.info("Project '%s' [%s]: loaded %d nodes", self._name, self._id, len(nodes_to_create)) # Pre-allocate UDP ports for all links in batch to reduce HTTP round-trips @@ -1875,9 +1934,12 @@ class Project: for link_data in topology.get("links", []): if "link_id" not in link_data.keys(): continue - for node_link in link_data.get("nodes", []): - node = self._nodes.get(node_link["node_id"]) - if node: + link_nodes = [self._nodes.get(nl["node_id"]) for nl in link_data.get("nodes", [])] + if any(node is not None and node.missing_image for node in link_nodes): + # the link will be deferred, no NIO/port will be created now + continue + for node in link_nodes: + if node is not None: ports_per_compute[node.compute.id] = ports_per_compute.get(node.compute.id, 0) + 1 for compute in self.computes: count = ports_per_compute.get(compute.id, 0) @@ -2215,7 +2277,7 @@ class Project: """ Start all nodes (except always-running types like Ethernet switch, Cloud, NAT, etc.) """ - nodes_to_start = [n for n in self.nodes.values() if not n.is_always_running()] + nodes_to_start = [n for n in self.nodes.values() if not n.is_always_running() and not n.missing_image] if not nodes_to_start: return log.info("Project '%s' [%s]: starting %d nodes...", self._name, self._id, len(nodes_to_start)) @@ -2230,7 +2292,7 @@ class Project: """ Stop all nodes (except always-running types like Ethernet switch, Cloud, NAT, etc.) """ - nodes_to_stop = [n for n in self.nodes.values() if not n.is_always_running()] + nodes_to_stop = [n for n in self.nodes.values() if not n.is_always_running() and not n.missing_image] if not nodes_to_stop: return log.info("Project '%s' [%s]: stopping %d nodes...", self._name, self._id, len(nodes_to_stop)) @@ -2247,6 +2309,8 @@ class Project: """ pool = Pool(concurrency=50) for node in self.nodes.values(): + if node.missing_image: + continue pool.append(node.suspend) await pool.join() @@ -2258,6 +2322,8 @@ class Project: pool = Pool(concurrency=3) for node in self.nodes.values(): + if node.missing_image: + continue pool.append(node.reset_console) await pool.join() @@ -2347,4 +2413,3 @@ class Project: def __repr__(self): return f"" - diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index bc6cf2d52..766958bc4 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -42,6 +42,10 @@ class UDPLink(Link): super().__init__(project, link_id=link_id) self._created = False self._link_data = [] + # True when the link could not be created on the computes because one + # of its endpoints has a missing image. The NIO creation is retried + # once the image is resolved. + self._deferred = False @property def debug_link_data(self): @@ -49,6 +53,11 @@ class UDPLink(Link): Use for the debug exports """ return self._link_data + + @property + def deferred(self): + """Whether NIO creation is waiting for missing node images.""" + return self._deferred def _get_node_filters(self, node1, node2): """ @@ -258,6 +267,10 @@ class UDPLink(Link): Delete the link and free the resources """ if not self._created: + if self._deferred: + # There is no NIO on the computes to delete, but the local + # back-references created while loading must be cleared. + await super().delete() return try: node1 = self._nodes[0]["node"] diff --git a/gns3server/schemas/controller/nodes.py b/gns3server/schemas/controller/nodes.py index 85672ffb8..048ff1364 100644 --- a/gns3server/schemas/controller/nodes.py +++ b/gns3server/schemas/controller/nodes.py @@ -99,6 +99,16 @@ class NodePort(BaseModel): mac_address: Union[str, None] = Field(None, pattern="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$") +class MissingImage(BaseModel): + """ + A missing image referenced by a node. + """ + + property: Optional[str] = Field(None, description="Node property referencing the image") + image: str = Field(..., description="Requested image filename") + image_type: Optional[str] = Field(None, description="Type of image (qemu/ios/iou/docker)") + + class NodeBase(BaseModel): """ Node data. @@ -182,6 +192,13 @@ class Node(NodeBase): None, description="Console host. Warning if the host is 0.0.0.0 or :: (listen on all interfaces) you need to use the same address you use to connect to the controller", ) + missing_image: bool = Field( + False, + description="True when the node could not be created on its compute because a required image is missing. Read only", + ) + missing_images: List[MissingImage] = Field( + default_factory=list, description="List of missing images referenced by the node. Read only" + ) class NodeDuplicate(BaseModel): diff --git a/tests/api/routes/controller/test_nodes.py b/tests/api/routes/controller/test_nodes.py index be115b1dd..6183503d7 100644 --- a/tests/api/routes/controller/test_nodes.py +++ b/tests/api/routes/controller/test_nodes.py @@ -103,7 +103,20 @@ class TestNodeRoutes: response = await client.get(app.url_path_for("get_nodes", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert response.json()[0]["name"] == "test" + async def test_list_node_with_missing_image( + self, app: FastAPI, client: AsyncClient, project: Project, node: Node + ) -> None: + node._missing_images = [ + {"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"} + ] + response = await client.get(app.url_path_for("get_nodes", project_id=project.id)) + assert response.status_code == status.HTTP_200_OK + data = response.json()[0] + assert data["missing_image"] is True + assert data["missing_images"] == [ + {"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"} + ] @pytest.mark.parametrize( "tags, expected_match", diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index b62784052..42fa889b5 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -26,6 +26,7 @@ from gns3server.compute.docker.docker_error import DockerError from gns3server.controller.node import Node from gns3server.controller.project import Project +from gns3server.controller.controller_error import ComputeConflictError, ControllerError @pytest.fixture @@ -234,7 +235,9 @@ def test_json(node, compute): "port_number": 0, "short_name": "e0" } - ] + ], + "missing_image": False, + "missing_images": [] } assert node.asdict(topology_dump=True) == { @@ -319,6 +322,314 @@ async def test_create_image_missing(node, compute): #assert node._upload_missing_image.called is True +@pytest.mark.asyncio +async def test_create_image_missing_kept_in_degraded_state(project, compute, tmpdir, config): + """ + With allow_missing_image=True a node whose image cannot be provided is kept + on the controller and flagged instead of aborting the whole project open. + """ + + config.settings.Server.images_path = str(tmpdir) + node = Node(project, compute, "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"hda_disk_image": "missing.qcow2", "ram": 256}) + + async def resp(*args, **kwargs): + raise ComputeConflictError( + "/projects/{}/qemu/nodes".format(project.id), + {"message": "The image is missing", "image": "missing.qcow2", "exception": "ImageMissingError"} + ) + + compute.post = AsyncioMagicMock(side_effect=resp) + node._upload_missing_image = AsyncioMagicMock(return_value=False) + + assert await node.create(allow_missing_image=True) is False + assert node.missing_image is True + assert node.missing_images == [ + {"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"} + ] + + +@pytest.mark.asyncio +async def test_create_image_missing_raises_by_default(project, compute, tmpdir, config): + """ + Without allow_missing_image the historical behaviour is preserved: the + ImageMissingError is re-raised. + """ + + config.settings.Server.images_path = str(tmpdir) + node = Node(project, compute, "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"hda_disk_image": "missing.qcow2", "ram": 256}) + + async def resp(*args, **kwargs): + raise ComputeConflictError( + "/projects/{}/qemu/nodes".format(project.id), + {"message": "The image is missing", "image": "missing.qcow2", "exception": "ImageMissingError"} + ) + + compute.post = AsyncioMagicMock(side_effect=resp) + node._upload_missing_image = AsyncioMagicMock(return_value=False) + + with pytest.raises(ComputeConflictError): + await node.create() + assert node.missing_image is False + + +@pytest.mark.asyncio +async def test_create_image_missing_raises_after_upload_retries(project, compute): + node = Node( + project, + compute, + "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"hda_disk_image": "missing.qcow2", "ram": 256}, + ) + + async def resp(*args, **kwargs): + raise ComputeConflictError( + f"/projects/{project.id}/qemu/nodes", + {"message": "missing", "image": "missing.qcow2", "exception": "ImageMissingError"}, + ) + + compute.post = AsyncioMagicMock(side_effect=resp) + node._upload_missing_image = AsyncioMagicMock(return_value=True) + + with pytest.raises(ComputeConflictError): + await node.create() + assert compute.post.call_count == 6 + assert node.missing_image is False + + +@pytest.mark.asyncio +async def test_create_image_missing_without_image_name_is_not_degraded(project, compute): + """A malformed conflict must not leave a controller-only node marked healthy.""" + + node = Node( + project, + compute, + "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"hda_disk_image": "present.qcow2", "ram": 256}, + ) + + async def resp(*args, **kwargs): + raise ComputeConflictError( + f"/projects/{project.id}/qemu/nodes", + {"message": "The image is missing", "exception": "ImageMissingError"}, + ) + + compute.post = AsyncioMagicMock(side_effect=resp) + node._image_available = MagicMock(return_value=True) + + with pytest.raises(ComputeConflictError): + await node.create(allow_missing_image=True) + assert node.missing_image is False + + +def test_compute_missing_images_matches_reported_basename_to_property(project, compute): + node = Node( + project, + compute, + "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={ + "hda_disk_image": "/images/qemu/disk.qcow2", + "cdrom_image": "/images/qemu/installer.iso", + "ram": 256, + }, + ) + node._image_available = MagicMock(return_value=True) + + assert node._compute_missing_images("installer.iso") == [ + {"property": "cdrom_image", "image": "installer.iso", "image_type": "qemu"} + ] + + +def test_compute_missing_images_deduplicates_path_and_reported_basename(project, compute): + node = Node( + project, + compute, + "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"cdrom_image": "/images/qemu/installer.iso", "ram": 256}, + ) + node._image_available = MagicMock(return_value=False) + + assert node._compute_missing_images("installer.iso") == [ + { + "property": "cdrom_image", + "image": "/images/qemu/installer.iso", + "image_type": "qemu", + } + ] + + +@pytest.mark.asyncio +async def test_create_docker_image_missing_after_failed_pull(project, compute): + """ + A Docker image that cannot be pulled from the registry keeps the node in a + degraded state instead of aborting the project open. + """ + + node = Node(project, compute, "web", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"image": "ghost:latest", "adapters": 1}) + + async def resp(*args, **kwargs): + raise ComputeConflictError( + "/projects/{}/docker/nodes".format(project.id), + {"message": "The image is missing", "image": "ghost:latest", "exception": "ImageMissingError"} + ) + + compute.post = AsyncioMagicMock(side_effect=resp) + node._upload_missing_image = AsyncioMagicMock( + side_effect=ControllerError("Failed to pull Docker image 'ghost:latest'") + ) + + assert await node.create(allow_missing_image=True) is False + assert node.missing_image is True + assert node.missing_images == [ + {"property": "image", "image": "ghost:latest", "image_type": "docker"} + ] + + +@pytest.mark.asyncio +async def test_create_docker_image_missing_pull_error_raises_by_default(project, compute): + """ + Without allow_missing_image a failed Docker pull is still surfaced. + """ + + node = Node(project, compute, "web", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"image": "ghost:latest", "adapters": 1}) + + async def resp(*args, **kwargs): + raise ComputeConflictError( + "/projects/{}/docker/nodes".format(project.id), + {"message": "The image is missing", "image": "ghost:latest", "exception": "ImageMissingError"} + ) + + compute.post = AsyncioMagicMock(side_effect=resp) + node._upload_missing_image = AsyncioMagicMock( + side_effect=ControllerError("Failed to pull Docker image 'ghost:latest'") + ) + + with pytest.raises(ControllerError): + await node.create() + + +def test_compute_missing_images_lists_all_unavailable_slots(project, compute, tmpdir, config): + """ + All the image slots of a multi-image node are reported, not only the first + one the compute complained about. + """ + + config.settings.Server.images_path = str(tmpdir) + node = Node(project, compute, "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={ + "hda_disk_image": "present.qcow2", + "hdc_disk_image": "missing.qcow2", + "initrd": "missing.initrd", + "ram": 256, + }) + # make one image available on the controller + os.makedirs(os.path.join(str(tmpdir), "QEMU"), exist_ok=True) + with open(os.path.join(str(tmpdir), "QEMU", "present.qcow2"), "w") as f: + f.write("x") + + missing = node._compute_missing_images() + assert {m["property"] for m in missing} == {"hdc_disk_image", "initrd"} + assert all(m["image_type"] == "qemu" for m in missing) + + +@pytest.mark.asyncio +async def test_start_missing_image(node): + + node._missing_images = [ + {"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"} + ] + with pytest.raises(ControllerError): + await node.start() + + +@pytest.mark.asyncio +async def test_update_recreates_missing_image_node(project, compute, node): + """ + Updating the image of a degraded node creates it on the compute and clears + the missing image state. + """ + + node._missing_images = [ + {"property": "image", "image": "missing.image", "image_type": "ios"} + ] + response = MagicMock() + response.json = {"console": 2048} + compute.post = AsyncioMagicMock(return_value=response) + project.restore_deferred_links = AsyncioMagicMock() + + await node.update(properties={"image": "available.image"}) + assert node.missing_image is False + assert node.missing_images == [] + assert project.restore_deferred_links.called is True + + +@pytest.mark.asyncio +async def test_update_missing_image_node_rolls_back_after_create_error(project, compute, node): + original_properties = {"image": "missing.image"} + node._properties = original_properties.copy() + node._missing_images = [ + {"property": "image", "image": "missing.image", "image_type": "ios"} + ] + compute.post = AsyncioMagicMock(side_effect=ControllerError("create failed")) + + with pytest.raises(ControllerError): + await node.update(properties={"image": "invalid.image"}) + + assert node.properties == original_properties + assert node.missing_images == [ + {"property": "image", "image": "missing.image", "image_type": "ios"} + ] + + +@pytest.mark.asyncio +async def test_start_retries_ready_deferred_links(node, project): + peer = MagicMock() + peer.missing_image = False + link = MagicMock() + link.deferred = True + link._nodes = [{"node": node}, {"node": peer}] + node._links.add(link) + project.restore_deferred_links = AsyncioMagicMock() + + with pytest.raises(ControllerError, match="deferred link"): + await node.start() + + project.restore_deferred_links.assert_called_once_with(node) + + +@pytest.mark.asyncio +async def test_stop_missing_image_node_does_not_contact_compute(node, compute): + node._missing_images = [ + {"property": "image", "image": "missing.image", "image_type": "ios"} + ] + compute.post = AsyncioMagicMock() + + await node.stop() + + compute.post.assert_not_called() + + @pytest.mark.asyncio async def test_create_base_script(node, config, compute, tmpdir): diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 373d05e10..978a5d4d8 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -17,6 +17,7 @@ # along with this program. If not, see . import os +import json import uuid import pytest import pytest_asyncio @@ -28,7 +29,12 @@ from uuid import uuid4 from gns3server.controller.project import Project from gns3server.controller.node import Node from gns3server.controller.ports.ethernet_port import EthernetPort -from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError, ControllerForbiddenError +from gns3server.controller.controller_error import ( + ControllerError, + ControllerNotFoundError, + ControllerForbiddenError, + ComputeConflictError, +) from gns3server.config import Config @@ -1006,6 +1012,19 @@ async def test_stop_all(project): assert len(compute.post.call_args_list) == 10 +@pytest.mark.asyncio +async def test_stop_all_skips_nodes_with_missing_images(project): + node = MagicMock() + node.is_always_running.return_value = False + node.missing_image = True + node.stop = AsyncioMagicMock() + project._nodes[node.id] = node + + await project.stop_all() + + node.stop.assert_not_called() + + @pytest.mark.asyncio async def test_suspend_all(project): @@ -1023,6 +1042,18 @@ async def test_suspend_all(project): assert len(compute.post.call_args_list) == 10 +@pytest.mark.asyncio +async def test_suspend_all_skips_nodes_with_missing_images(project): + node = MagicMock() + node.missing_image = True + node.suspend = AsyncioMagicMock() + project._nodes[node.id] = node + + await project.suspend_all() + + node.suspend.assert_not_called() + + @pytest.mark.asyncio async def test_console_reset_all(project): @@ -1040,6 +1071,18 @@ async def test_console_reset_all(project): assert len(compute.post.call_args_list) == 10 +@pytest.mark.asyncio +async def test_console_reset_all_skips_nodes_with_missing_images(project): + node = MagicMock() + node.missing_image = True + node.reset_console = AsyncioMagicMock() + project._nodes[node.id] = node + + await project.reset_console_all() + + node.reset_console.assert_not_called() + + @pytest.mark.asyncio async def test_node_name(project): @@ -1085,3 +1128,201 @@ async def test_duplicate_node(project): }) new_node = await project.duplicate_node(original, 42, 10, 11) assert new_node.x == 42 + + +@pytest.mark.asyncio +async def test_add_node_missing_image_kept_in_degraded_state(controller): + """ + A node whose image is missing is kept on the controller with the missing + image list instead of failing, when allow_missing_image is set. + """ + + compute = MagicMock() + compute.id = "local" + compute.connected = True + project = Project(controller=controller, name="Test") + project.emit_notification = MagicMock() + + async def post(url, data=None, **kwargs): + if url.endswith("/qemu/nodes"): + raise ComputeConflictError( + url, {"message": "missing", "image": "missing.qcow2", "exception": "ImageMissingError"} + ) + response = MagicMock() + response.json = {} + return response + + compute.post = AsyncioMagicMock(side_effect=post) + + node = await project.add_node( + compute, + "r1", + None, + node_type="qemu", + allow_missing_image=True, + properties={"hda_disk_image": "missing.qcow2", "ram": 256}, + ) + assert node.missing_image is True + assert node.id in project._nodes + assert node.missing_images == [ + {"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"} + ] + + +@pytest.mark.asyncio +async def test_open_with_missing_image_defers_links(controller, projects_dir): + """ + Opening a project with a missing image succeeds, keeps the degraded node + and its links in the topology, but does not create the NIOs. + """ + + project_id = "3c1be6f9-b4ba-4737-b209-63c47c23359f" + qemu_id = "11111111-1111-1111-1111-111111111111" + iou_id = "33333333-3333-3333-3333-333333333333" + vpcs_id = "22222222-2222-2222-2222-222222222222" + link_id = "5a3e3a64-e853-4055-9503-4a14e01290f1" + + topology = { + "auto_close": True, + "auto_open": False, + "auto_start": False, + "name": "demo", + "project_id": project_id, + "revision": 5, + "topology": { + "computes": [], + "drawings": [], + "links": [ + { + "link_id": link_id, + "nodes": [ + {"adapter_number": 0, "node_id": qemu_id, "port_number": 0}, + {"adapter_number": 0, "node_id": vpcs_id, "port_number": 0}, + ], + } + ], + "nodes": [ + { + "compute_id": "local", + "name": "R1", + "node_id": qemu_id, + "node_type": "qemu", + "properties": {"hda_disk_image": "missing.qcow2", "adapters": 1, "ram": 256}, + "symbol": ":/symbols/router.svg", + "x": 0, + "y": 0, + }, + { + "compute_id": "local", + "name": "IOU1", + "node_id": iou_id, + "node_type": "iou", + "properties": {"path": "missing.bin"}, + "symbol": ":/symbols/router.svg", + "x": 200, + "y": 0, + }, + { + "compute_id": "local", + "name": "PC1", + "node_id": vpcs_id, + "node_type": "vpcs", + "properties": {}, + "symbol": ":/symbols/computer.svg", + "x": 100, + "y": 0, + }, + ], + }, + "type": "topology", + "version": "2.0.0", + } + + project_dir = os.path.join(projects_dir, "demo") + os.makedirs(project_dir, exist_ok=True) + with open(os.path.join(project_dir, "demo.gns3"), "w+") as f: + json.dump(topology, f) + + compute = MagicMock() + compute.id = "local" + compute.connected = True + compute.name = "local" + compute.console_host = "127.0.0.1" + controller._computes["local"] = compute + + async def post(url, data=None, **kwargs): + if url.endswith("/qemu/nodes"): + raise ComputeConflictError( + url, {"message": "missing", "image": "missing.qcow2", "exception": "ImageMissingError"} + ) + if url.endswith("/iou/nodes"): + raise ComputeConflictError( + url, {"message": "missing", "image": "missing.bin", "exception": "ImageMissingError"} + ) + response = MagicMock() + if "ports/udp/batch" in url: + response.json = {"udp_ports": [20000]} + else: + response.json = {} + return response + + compute.post = AsyncioMagicMock(side_effect=post) + + project = Project( + name="demo", + project_id=project_id, + path=project_dir, + controller=controller, + filename="demo.gns3", + status="closed", + ) + + await project.open() + assert project.status == "opened" + + qemu_node = project.get_node(qemu_id) + assert qemu_node.missing_image is True + assert qemu_node.missing_images[0]["image"] == "missing.qcow2" + + iou_node = project.get_node(iou_id) + assert iou_node.missing_image is True + assert iou_node.missing_images[0]["image"] == "missing.bin" + assert iou_node.missing_images[0]["image_type"] == "iou" + + link = project._links[link_id] + assert link._deferred is True + assert link.created is False + assert link in qemu_node.links + + +@pytest.mark.asyncio +async def test_restore_deferred_links(project): + """ + Once a degraded node is created, the deferred links are created on the + computes and no longer flagged as deferred. + """ + + compute = MagicMock() + compute.id = "local" + response = MagicMock() + response.json = {"console": 2048} + compute.post = AsyncioMagicMock(return_value=response) + + node1 = await project.add_node(compute, "n1", None, node_type="vpcs", properties={}) + node2 = await project.add_node(compute, "n2", None, node_type="vpcs", properties={}) + + link = await project.add_link() + await link.add_node(node1, 0, 0, batch=True) + await link.add_node(node2, 0, 0, batch=True) + link._nodes[0]["port"].link = link + link._nodes[1]["port"].link = link + node1.add_link(link) + node2.add_link(link) + link._deferred = True + link.create = AsyncioMagicMock() + + project.emit_notification = MagicMock() + await project.restore_deferred_links(node1) + + assert link._deferred is False + assert link.create.called is True diff --git a/tests/controller/test_udp_link.py b/tests/controller/test_udp_link.py index e5045cc36..f8934b1b4 100644 --- a/tests/controller/test_udp_link.py +++ b/tests/controller/test_udp_link.py @@ -187,6 +187,44 @@ async def test_delete(project): compute2.delete.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), timeout=120) +@pytest.mark.asyncio +async def test_delete_deferred_link_clears_local_references(project): + node1 = Node(project, MagicMock(), "node1", node_type="vpcs") + node1._ports = [EthernetPort("E0", 0, 0, 0)] + node2 = Node(project, MagicMock(), "node2", node_type="vpcs") + node2._ports = [EthernetPort("E0", 0, 0, 0)] + link = UDPLink(project) + + await link.add_node(node1, 0, 0, batch=True) + await link.add_node(node2, 0, 0, batch=True) + for entry in link._nodes: + entry["node"].add_link(link) + entry["port"].link = link + link._deferred = True + + await link.delete() + + assert link not in node1.links + assert link not in node2.links + assert node1.get_port(0, 0).link is None + assert node2.get_port(0, 0).link is None + + +@pytest.mark.asyncio +async def test_delete_uncreated_non_deferred_link_preserves_existing_behavior(project): + node = Node(project, MagicMock(), "node1", node_type="vpcs") + node._ports = [EthernetPort("E0", 0, 0, 0)] + link = UDPLink(project) + await link.add_node(node, 0, 0, batch=True) + node.add_link(link) + node.get_port(0, 0).link = link + + await link.delete() + + assert link in node.links + assert node.get_port(0, 0).link is link + + @pytest.mark.asyncio async def test_reset(project): """ From da16dc987434a41e065d9c341e55ca3df22e1af5 Mon Sep 17 00:00:00 2001 From: Cristi Date: Fri, 18 Sep 2026 23:58:49 +0300 Subject: [PATCH 47/60] fix(qemu): Fixed missing image replacement for linked-clone nodes --- gns3server/api/routes/compute/qemu_nodes.py | 26 ++++- gns3server/compute/qemu/qemu_vm.py | 3 + gns3server/controller/node.py | 110 +++++++++++++---- gns3server/schemas/compute/qemu_nodes.py | 5 +- tests/api/routes/compute/test_qemu_nodes.py | 123 ++++++++++++++++++++ tests/compute/qemu/test_qemu_vm.py | 13 +++ tests/controller/test_node.py | 55 +++++++++ 7 files changed, 308 insertions(+), 27 deletions(-) diff --git a/gns3server/api/routes/compute/qemu_nodes.py b/gns3server/api/routes/compute/qemu_nodes.py index efd6f7d74..ed9345308 100644 --- a/gns3server/api/routes/compute/qemu_nodes.py +++ b/gns3server/api/routes/compute/qemu_nodes.py @@ -64,6 +64,7 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s qemu = Qemu.instance() node_data = jsonable_encoder(node_data, exclude_unset=True) + disk_images_to_reset = set(node_data.pop("disk_images_to_reset", [])) vm = await qemu.create_node( node_data.pop("name"), str(project_id), @@ -80,12 +81,35 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s # update the disk image with the backing file if provided # this is needed when duplicating a node that uses backed disk images drives = ["a", "b", "c", "d"] - for disk_index, drive in enumerate(drives): + for drive in drives: disk_image_backing_file = node_data.get(f"hd{drive}_disk_image_backing_file") if disk_image_backing_file: log.debug(f"Updating disk image for drive {drive} with backing file {disk_image_backing_file}") node_data[f"hd{drive}_disk_image"] = disk_image_backing_file + # Validate every explicitly replaced disk before removing its stale + # overlay. Other unresolved disks may still make this create request fail, + # but a valid replacement applied in stages must not retain its old layer. + for drive in drives: + disk_image_property = f"hd{drive}_disk_image" + if disk_image_property in disk_images_to_reset: + replacement_image = node_data.get(disk_image_property) + if replacement_image: + vm.manager.get_abs_image_path(replacement_image, vm.working_dir) + local_disk_name = f"hd{drive}_disk.qcow2" + local_disk = os.path.join(vm.working_dir, local_disk_name) + if vm.linked_clone and os.path.exists(local_disk): + # A degraded linked clone is being assigned a new base image. + # Its old overlay depends on the unavailable base and cannot + # safely be rebased onto an arbitrary replacement. Discard it + # so start creates a fresh overlay from the selected image. + log.info( + "Removing stale linked-clone disk '%s' before using replacement image '%s'", + local_disk, + node_data.get(disk_image_property), + ) + vm.delete_disk_image(local_disk_name) + for name, value in node_data.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 439523c65..d6b0318b5 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -1777,6 +1777,9 @@ class QemuVM(BaseNode): try: os.remove(disk_path) + md5sum_path = disk_path + ".md5sum" + if os.path.exists(md5sum_path): + os.remove(md5sum_path) except OSError as e: raise QemuError(f"Could not delete '{disk_name}' disk image: {e}") diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 8b7b83193..9cbfd9292 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -494,7 +494,7 @@ class Node: def links(self): return self._links - async def create(self, allow_missing_image=False): + async def create(self, allow_missing_image=False, disk_images_to_reset=None): """ Create the node on the compute @@ -502,9 +502,13 @@ class Node: cannot be uploaded/synced, the node is not created on the compute but is kept on the controller and flagged with the missing image(s) instead of raising. + :param disk_images_to_reset: QEMU disk properties whose linked-clone + overlays must be recreated after replacing a missing base image. """ data = self._node_data() data["node_id"] = self._id + if self._node_type == "qemu" and disk_images_to_reset: + data["disk_images_to_reset"] = sorted(disk_images_to_reset) if self._node_type == "docker": timeout = None await self._add_docker_image_digest(data) @@ -576,6 +580,41 @@ class Node: raise last_missing_error return False + def _find_local_image(self, image_type, image): + """Return an image path contained in an allowed image directory.""" + + if not image or image_type == "docker": + return None + try: + directories = images_directories(image_type) + except NotImplementedError: + return None + requested = os.path.normpath(image) + for directory in directories: + root = os.path.realpath(directory) + candidate = os.path.realpath(os.path.join(root, requested)) + try: + contained = os.path.commonpath((root, candidate)) == root + except ValueError: + contained = False + if contained and os.path.isfile(candidate): + return candidate + + # Image API records expose a basename separately from their + # controller-local absolute path. Search subfolders when that + # portable basename is used by a node or remote compute. + if os.path.basename(requested) == requested: + for current_root, _, filenames in os.walk(root): + if requested in filenames: + nested_candidate = os.path.realpath(os.path.join(current_root, requested)) + try: + nested_contained = os.path.commonpath((root, nested_candidate)) == root + except ValueError: + nested_contained = False + if nested_contained and os.path.isfile(nested_candidate): + return nested_candidate + return None + def _image_available(self, image_type, image): """ Check whether an image is available on the controller (and can @@ -590,14 +629,7 @@ class Node: if image_type == "docker": # Docker images are not stored on the controller filesystem return True - try: - directories = images_directories(image_type) - except NotImplementedError: - return True - for directory in directories: - if os.path.exists(os.path.join(directory, image)): - return True - return False + return self._find_local_image(image_type, image) is not None def _compute_missing_images(self, fallback_image=None): """ @@ -641,6 +673,29 @@ class Node: missing.append({"property": prop, "image": fallback_image, "image_type": image_type}) return missing + def _remove_replaced_image_metadata(self, old_properties, new_properties): + """Remove metadata that still refers to an image being replaced.""" + + replaced_properties = { + missing.get("property") + for missing in self._missing_images + if missing.get("property") + and old_properties.get(missing["property"]) != new_properties.get(missing["property"]) + } + if self._node_type == "qemu": + for prop in replaced_properties: + new_properties.pop(f"{prop}_md5sum", None) + if prop in {"hda_disk_image", "hdb_disk_image", "hdc_disk_image", "hdd_disk_image"}: + # The QEMU create route treats a backing-file value as the + # authoritative base image. Keeping the old value would + # silently override the replacement selected by the user. + new_properties.pop(f"{prop}_backing_file", None) + elif self._node_type == "dynamips" and "image" in replaced_properties: + new_properties.pop("image_md5sum", None) + elif self._node_type == "iou" and "path" in replaced_properties: + new_properties.pop("md5sum", None) + return replaced_properties + async def update(self, **kwargs): """ Update the node on the compute @@ -665,7 +720,7 @@ class Node: # We update properties on the compute and wait for the answer from the compute node if prop == "properties": - compute_properties = kwargs[prop] + compute_properties = copy.deepcopy(kwargs[prop]) else: if ( prop == "name" @@ -681,7 +736,10 @@ class Node: if self.missing_image and compute_properties is not None: # The node was never created on the compute (missing image). Apply # the new properties locally so create() can use them. + replaced_image_properties = self._remove_replaced_image_metadata(old_properties, compute_properties) self._properties = compute_properties + else: + replaced_image_properties = set() self._list_ports() if update_compute: if self.missing_image: @@ -689,7 +747,10 @@ class Node: # have been provided. If it is still missing the node simply # remains in its degraded state. try: - created = await self.create(allow_missing_image=True) + created = await self.create( + allow_missing_image=True, + disk_images_to_reset=replaced_image_properties, + ) except Exception: # create() can reject the replacement for reasons other # than a missing image. Keep the controller state aligned @@ -979,20 +1040,19 @@ class Node: if self._node_type == "docker": return await self._sync_missing_docker_image(img) - for directory in images_directories(type): - image = os.path.join(directory, img) - if os.path.exists(image): - self.project.emit_notification("log.info", {"message": f"Uploading missing image {img}"}) - try: - with open(image, "rb") as f: - await self._compute.post( - f"/{self._node_type}/images/{os.path.basename(img)}", data=f, timeout=None - ) - except OSError as e: - raise ControllerError(f"Can't upload {image}: {str(e)}") - self.project.emit_notification("log.info", {"message": f"Upload finished for {img}"}) - return True - return False + image = self._find_local_image(type, img) + if image is None: + return False + self.project.emit_notification("log.info", {"message": f"Uploading missing image {img}"}) + try: + with open(image, "rb") as f: + await self._compute.post( + f"/{self._node_type}/images/{os.path.basename(img)}", data=f, timeout=None + ) + except OSError as e: + raise ControllerError(f"Can't upload {image}: {str(e)}") + self.project.emit_notification("log.info", {"message": f"Upload finished for {img}"}) + return True async def _add_docker_image_digest(self, data): """ diff --git a/gns3server/schemas/compute/qemu_nodes.py b/gns3server/schemas/compute/qemu_nodes.py index 999a53b1a..658fb26d5 100644 --- a/gns3server/schemas/compute/qemu_nodes.py +++ b/gns3server/schemas/compute/qemu_nodes.py @@ -221,7 +221,10 @@ class QemuCreate(QemuBase): Properties to create a Qemu node. """ - pass + disk_images_to_reset: Optional[List[str]] = Field( + None, + description="Disk image properties whose stale linked-clone overlays must be recreated", + ) class QemuUpdate(QemuBase): diff --git a/tests/api/routes/compute/test_qemu_nodes.py b/tests/api/routes/compute/test_qemu_nodes.py index 688adf9c2..1bed9cf39 100644 --- a/tests/api/routes/compute/test_qemu_nodes.py +++ b/tests/api/routes/compute/test_qemu_nodes.py @@ -20,6 +20,7 @@ import pytest_asyncio import os import stat import shutil +import uuid from fastapi import FastAPI, status from httpx import AsyncClient @@ -144,6 +145,128 @@ class TestQemuNodesRoutes: assert response.json()["project_id"] == compute_project.id assert response.json()["ram"] == 1024 assert response.json()["hda_disk_image"] == "linux载.img" + + async def test_qemu_create_discards_stale_overlay_for_replacement( + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + base_params: dict, + fake_qemu_vm: str + ): + + node_id = str(uuid.uuid4()) + working_dir = os.path.join(compute_project.path, "project-files", "qemu", node_id) + os.makedirs(working_dir) + stale_overlay = os.path.join(working_dir, "hda_disk.qcow2") + with open(stale_overlay, "w+") as f: + f.write("stale linked clone") + stale_checksum = stale_overlay + ".md5sum" + with open(stale_checksum, "w+") as f: + f.write("0" * 32) + + params = { + **base_params, + "node_id": node_id, + "linked_clone": True, + "hda_disk_image": "linux载.img", + "disk_images_to_reset": ["hda_disk_image"], + } + response = await compute_client.post( + app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params + ) + + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["hda_disk_image"] == "linux载.img" + assert not os.path.exists(stale_overlay) + assert not os.path.exists(stale_checksum) + + async def test_qemu_create_preserves_existing_overlay_without_reset_marker( + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + base_params: dict, + fake_qemu_vm: str + ): + + node_id = str(uuid.uuid4()) + working_dir = os.path.join(compute_project.path, "project-files", "qemu", node_id) + os.makedirs(working_dir) + overlay = os.path.join(working_dir, "hda_disk.qcow2") + shutil.copy("tests/resources/empty8G.qcow2", overlay) + + params = { + **base_params, + "node_id": node_id, + "linked_clone": True, + "hda_disk_image": "linux载.img", + } + response = await compute_client.post( + app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params + ) + + assert response.status_code == status.HTTP_201_CREATED + assert os.path.exists(overlay) + + async def test_qemu_create_preserves_overlay_when_replacement_is_missing( + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + base_params: dict, + fake_qemu_vm: str + ): + + node_id = str(uuid.uuid4()) + working_dir = os.path.join(compute_project.path, "project-files", "qemu", node_id) + os.makedirs(working_dir) + overlay = os.path.join(working_dir, "hda_disk.qcow2") + shutil.copy("tests/resources/empty8G.qcow2", overlay) + + params = { + **base_params, + "node_id": node_id, + "linked_clone": True, + "hda_disk_image": "does-not-exist.qcow2", + "disk_images_to_reset": ["hda_disk_image"], + } + response = await compute_client.post( + app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params + ) + + assert response.status_code == status.HTTP_409_CONFLICT + assert os.path.exists(overlay) + + async def test_qemu_create_resets_valid_replacement_when_another_disk_is_missing( + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + base_params: dict, + fake_qemu_vm: str + ): + + node_id = str(uuid.uuid4()) + working_dir = os.path.join(compute_project.path, "project-files", "qemu", node_id) + os.makedirs(working_dir) + stale_overlay = os.path.join(working_dir, "hda_disk.qcow2") + shutil.copy("tests/resources/empty8G.qcow2", stale_overlay) + + params = { + **base_params, + "node_id": node_id, + "linked_clone": True, + "hda_disk_image": "linux载.img", + "hdb_disk_image": "does-not-exist.qcow2", + "disk_images_to_reset": ["hda_disk_image"], + } + response = await compute_client.post( + app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params + ) + + assert response.status_code == status.HTTP_409_CONFLICT + assert not os.path.exists(stale_overlay) @pytest.mark.parametrize( diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index 39da0f7ee..fa16bb560 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -806,6 +806,19 @@ def test_hda_disk_image(vm, images_dir): assert vm.hda_disk_image == force_unix_path(os.path.join(images_dir, "QEMU", "test2")) +def test_delete_disk_image_removes_cached_checksum(vm): + + disk_path = os.path.join(vm.working_dir, "hda_disk.qcow2") + checksum_path = disk_path + ".md5sum" + open(disk_path, "w+").close() + open(checksum_path, "w+").close() + + vm.delete_disk_image("hda_disk.qcow2") + + assert not os.path.exists(disk_path) + assert not os.path.exists(checksum_path) + + @pytest.mark.asyncio async def test_hda_disk_image_non_linked_clone(vm, images_dir, compute_project, manager, fake_qemu_binary): """ diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index 42fa889b5..8ce0d428b 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -584,6 +584,45 @@ async def test_update_recreates_missing_image_node(project, compute, node): assert project.restore_deferred_links.called is True +@pytest.mark.asyncio +async def test_update_replaces_qemu_linked_clone_backing_image(project, compute): + """Stale linked-clone metadata must not override a replacement image.""" + + qemu_node = Node( + project, + compute, + "old-qemu", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={ + "hda_disk_image": "hda_disk.qcow2", + "hda_disk_image_backing_file": "missing.qcow2", + "hda_disk_image_md5sum": "old-checksum", + }, + ) + qemu_node._missing_images = [ + {"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"} + ] + response = MagicMock() + response.json = {"console": 2048} + compute.post = AsyncioMagicMock(return_value=response) + project.restore_deferred_links = AsyncioMagicMock() + + await qemu_node.update( + properties={ + **qemu_node.properties, + "hda_disk_image": "replacement.qcow2", + } + ) + + request_data = compute.post.call_args.kwargs["data"] + assert request_data["hda_disk_image"] == "replacement.qcow2" + assert request_data["disk_images_to_reset"] == ["hda_disk_image"] + assert "hda_disk_image_backing_file" not in request_data + assert "hda_disk_image_md5sum" not in request_data + assert qemu_node.missing_image is False + + @pytest.mark.asyncio async def test_update_missing_image_node_rolls_back_after_create_error(project, compute, node): original_properties = {"image": "missing.image"} @@ -1005,6 +1044,22 @@ async def test_upload_missing_image(compute, controller, images_dir): compute.post.assert_called_with("/qemu/images/linux.img", data=ANY, timeout=None) +@pytest.mark.asyncio +async def test_upload_missing_image_from_nested_directory(compute, controller, images_dir): + + project = Project(str(uuid.uuid4()), controller=controller) + node = Node(project, compute, "demo", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"hda_disk_image": "nested.img"}) + nested_dir = os.path.join(images_dir, "vendor") + os.makedirs(nested_dir) + open(os.path.join(nested_dir, "nested.img"), "w+").close() + + assert await node._upload_missing_image("qemu", "nested.img") is True + compute.post.assert_called_with("/qemu/images/nested.img", data=ANY, timeout=None) + + @pytest.mark.asyncio async def test_sync_missing_docker_image_from_controller_daemon(compute, controller): From 34923e46e055b8aa1b14dc35b37411d3a6cfeb9a Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 22 Sep 2026 14:56:58 +0200 Subject: [PATCH 48/60] chore(docs): update gns3_server.conf sample about the default NAT interface --- gns3server/config_samples/gns3_server.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/config_samples/gns3_server.conf b/gns3server/config_samples/gns3_server.conf index c3eb76bc9..275f26358 100644 --- a/gns3server/config_samples/gns3_server.conf +++ b/gns3server/config_samples/gns3_server.conf @@ -127,8 +127,8 @@ compute_password = gns3 allowed_interfaces = eth0,eth1,virbr0 ; Specify the NAT interface to be used by the NAT node -; Default is virbr0 on Linux (requires libvirt) and vmnet8 for other platforms (requires VMware) -default_nat_interface = vmnet10 +; Default is virbr0 on Linux (requires libvirt) +default_nat_interface = virbr0 ; Enable the built-in templates enable_builtin_templates = True From 8dfe05df3db5cf3d1ca8130728740ccf0fa09317 Mon Sep 17 00:00:00 2001 From: Mark Paronyan Date: Sun, 27 Sep 2026 15:43:18 +0300 Subject: [PATCH 49/60] chore: justfile with mypy, ruff, pytest checks --- .coveragerc | 7 + .gitignore | 4 + dev-requirements.txt | 7 +- gns3server/agent/web_wireshark/__init__.py | 0 justfile | 39 +++ pyproject.toml | 374 +++++++++++++++++++++ 6 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 gns3server/agent/web_wireshark/__init__.py create mode 100644 justfile diff --git a/.coveragerc b/.coveragerc index b824f504d..9f66baaf0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,9 @@ +[run] +source = gns3server +branch = True + +[report] +fail_under = 55 + [paths] source = gns3server diff --git a/.gitignore b/.gitignore index 7677a0718..b5ce8fdc4 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,10 @@ pip-log.txt .tox .cache nosetests.xml +coverage.xml +htmlcov/ +.mypy_cache/ +.ruff_cache/ # Translations *.mo diff --git a/dev-requirements.txt b/dev-requirements.txt index 80d7da3b4..2f2d2c10b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,10 @@ pytest==9.1.1 -flake8==7.3.0 pytest-timeout==2.4.0 pytest-asyncio==1.4.0 httpx==0.28.1 httpx_ws==0.7.2 # upgrading leads to failures in tests -pytest-random-order==1.2.0 # opt-in: --random-order --random-order-bucket=global (legacy suites are not shuffle-clean yet, see .claude/skills/gns3-api-test-writing) \ No newline at end of file +pytest-random-order==1.2.0 # opt-in: --random-order --random-order-bucket=global (legacy suites are not shuffle-clean yet, see .claude/skills/gns3-api-test-writing) +pytest-cov==7.1.0 +ruff==0.16.9 +mypy==2.3.1 +rust-just==1.58.0 # provides the `just` command runner, see justfile diff --git a/gns3server/agent/web_wireshark/__init__.py b/gns3server/agent/web_wireshark/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/justfile b/justfile new file mode 100644 index 000000000..b07fefc81 --- /dev/null +++ b/justfile @@ -0,0 +1,39 @@ +set shell := ["bash", "-eu", "-o", "pipefail", "-c"] + +_default: + @just --list --unsorted --list-submodules + +# Install the server with all optional features and dev tools +[group('dev')] +install: + python -m pip install --upgrade pip + python -m pip install -e '.[ai-features,dev]' + +# Apply ruff autofixes for the enforced rules +[group('dev')] +format: + python -m ruff check --fix + python -m ruff format + +# Run ruff checks (no fixes) +[group('dev')] +lint: + python -m ruff check --exit-non-zero-on-fix + python -m ruff format --check --diff + +[group('dev')] +type-check: + python -m mypy + +# Run tests with coverage, e.g. `just unit tests/controller` +[group('test')] +unit *args='': + python -m pytest --cov --cov-report=term-missing:skip-covered --cov-report=xml {{ args }} + +[group('test')] +package: + pip check + +# Run all checks +[group('test')] +test: package lint type-check unit diff --git a/pyproject.toml b/pyproject.toml index e47c297ff..5d4286bcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,3 +55,377 @@ gns3server-uninstall-ai-features = "gns3server.utils.uninstall_ai_features:main" [tool.setuptools] packages = ["gns3server"] + + +[tool.ruff] +target-version = "py310" +line-length = 120 + +[tool.ruff.lint] +select = [ + "ASYNC", # flake8-async + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C90", # mccabe + "DTZ", # flake8-datetimez + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "LOG", # flake8-logging + "PLE", # pylint errors + "PLW", # pylint warnings + "RUF", # ruff-specific rules + "S", # flake8-bandit + "T10", # flake8-debugger + "UP", # pyupgrade + "W", # pycodestyle warnings +] +# Baseline: widespread rules that the existing code still violates +# (violation count at baseline time). Remove an entry, and fix its +# violations, in a dedicated small PR. Never add new entries here: +# fix the code or use a targeted "# noqa: CODE" with a reason. +ignore = [ + "ASYNC230", # 240 blocking-open-call-in-async-function + "ASYNC240", # 235 blocking-path-method-in-async-function + "B007", # 32 unused-loop-control-variable + "B008", # 119 function-call-in-default-argument + "B904", # 365 raise-without-from-inside-except + "BLE001", # 214 blind-except + "E402", # 129 module-import-not-at-top-of-file + "E501", # 655 line-too-long + "F401", # 300 unused-import + "F541", # 16 f-string-missing-placeholders + "F841", # 32 unused-variable + "I001", # 441 unsorted-imports + "PLW0603", # 21 global-statement + "PLW2901", # 30 redefined-loop-name + "RUF010", # 65 explicit-f-string-type-conversion + "RUF012", # 23 mutable-class-default + "RUF013", # 40 implicit-optional + "RUF036", # 22 none-not-at-end-of-union + "RUF059", # 29 unused-unpacked-variable + "S101", # 19 assert + "S104", # 22 hardcoded-bind-all-interfaces + "S110", # 23 try-except-pass + "UP006", # 270 non-pep585-annotation + "UP007", # 81 non-pep604-annotation-union + "UP009", # 51 utf8-encoding-declaration + "UP032", # 560 f-string + "UP035", # 106 deprecated-import + "UP045", # 1100 non-pep604-annotation-optional + "W293", # 661 blank-line-with-whitespace +] + +[tool.ruff.lint.mccabe] +max-complexity = 15 + +[tool.ruff.lint.flake8-bugbear] +# FastAPI dependency markers are meant to be used as defaults: +extend-immutable-calls = [ + "fastapi.Body", + "fastapi.Cookie", + "fastapi.Depends", + "fastapi.File", + "fastapi.Form", + "fastapi.Header", + "fastapi.Path", + "fastapi.Query", + "fastapi.Security", +] + +[tool.ruff.lint.per-file-ignores] +# Tests use asserts and hardcoded credentials / temp paths: +"tests/**" = ["S101", "S105", "S106", "S108"] +# Baseline: existing violations of rules that are enforced everywhere else. +# Remove entries one small PR at a time. Never add new ones. +"gns3server/agent/__init__.py" = ["LOG015", "RUF022"] +"gns3server/agent/gns3_copilot/agent/context_manager.py" = ["RUF005", "RUF100"] +"gns3server/agent/gns3_copilot/agent/gns3_copilot.py" = ["DTZ003"] +"gns3server/agent/gns3_copilot/agent_service.py" = ["C901", "DTZ003"] +"gns3server/agent/gns3_copilot/chat_sessions_repository.py" = ["DTZ003", "S608"] +"gns3server/agent/gns3_copilot/gns3_client/__init__.py" = ["RUF022"] +"gns3server/agent/gns3_copilot/gns3_client/context_helpers.py" = ["RUF022"] +"gns3server/agent/gns3_copilot/skills/__init__.py" = ["RUF022"] +"gns3server/agent/gns3_copilot/skills/loader.py" = ["UP015"] +"gns3server/agent/gns3_copilot/skills/manager.py" = ["UP037"] +"gns3server/agent/gns3_copilot/skills/registry.py" = ["C901"] +"gns3server/agent/gns3_copilot/tools_v2/__init__.py" = ["RUF022"] +"gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py" = ["RUF100"] +"gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py" = ["RUF100"] +"gns3server/agent/gns3_copilot/tools_v2/gns3_packet_filter.py" = ["PLW1510", "S603", "S607"] +"gns3server/agent/gns3_copilot/tools_v2/packet_analysis_tool.py" = ["PLW1510", "RUF005", "S603", "S607"] +"gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py" = ["RUF100"] +"gns3server/agent/gns3_copilot/utils/__init__.py" = ["RUF022"] +"gns3server/agent/gns3_copilot/utils/custom_netmiko/__init__.py" = ["RUF100"] +"gns3server/agent/gns3_copilot/utils/custom_netmiko/huawei_ce.py" = ["S107"] +"gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py" = ["LOG015"] +"gns3server/agent/gns3_copilot/utils/custom_netmiko/scripts/list_netmiko_telnet_devices.py" = ["DTZ005", "UP024"] +"gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_huawei_ce.py" = ["RUF100"] +"gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_ruijie_telnet.py" = ["RUF100"] +"gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_vpcs_telnet.py" = ["RUF100"] +"gns3server/agent/gns3_copilot/utils/custom_netmiko/vpcs_telnet.py" = ["S107"] +"gns3server/agent/gns3_copilot/utils/gns3_drawing_utils.py" = ["RUF100"] +"gns3server/agent/gns3_copilot/utils/message_converters.py" = ["RUF019"] +"gns3server/agent/gns3_copilot/utils/parse_tool_content.py" = ["C901", "DTZ003"] +"gns3server/agent/mcp/__init__.py" = ["RUF003"] +"gns3server/agent/mcp/device_config.py" = ["E741"] +"gns3server/agent/web_wireshark/manager.py" = ["C901"] +"gns3server/agent/web_wireshark/setup_wireshark_image.py" = ["PLW1510", "S603", "S607"] +"gns3server/agent/web_wireshark/stats.py" = ["ASYNC221", "PLW1510", "S603", "S607"] +"gns3server/api/routes/compute/notifications.py" = ["W291"] +"gns3server/api/routes/compute/qemu_nodes.py" = ["F811"] +"gns3server/api/routes/controller/controller.py" = ["UP012"] +"gns3server/api/routes/controller/dependencies/authentication.py" = ["S105", "S106"] +"gns3server/api/routes/controller/links.py" = ["E722"] +"gns3server/api/routes/controller/netmiko.py" = ["RUF100"] +"gns3server/api/routes/controller/nodes.py" = ["C901", "RUF019", "UP024"] +"gns3server/api/routes/controller/projects.py" = ["UP012"] +"gns3server/api/routes/controller/templates.py" = ["S324"] +"gns3server/api/routes/controller/users.py" = ["S105", "S106"] +"gns3server/api/routes/index.py" = ["RUF021"] +"gns3server/compute/base_node.py" = ["C901", "RUF019"] +"gns3server/compute/builtin/nodes/cloud.py" = ["E713"] +"gns3server/compute/docker/__init__.py" = ["B006", "C901"] +"gns3server/compute/docker/docker_vm.py" = ["B006", "C901", "E731", "RUF003", "RUF006", "S108", "UP031"] +"gns3server/compute/docker/iol_docker_vm.py" = ["RUF002", "S108"] +"gns3server/compute/docker/vendor_docker_vm.py" = ["RUF002", "S108"] +"gns3server/compute/dynamips/__init__.py" = ["C901"] +"gns3server/compute/dynamips/dynamips_hypervisor.py" = ["C901"] +"gns3server/compute/dynamips/nodes/router.py" = ["F522", "RUF006"] +"gns3server/compute/iou/iou_vm.py" = ["C901", "S108", "S324"] +"gns3server/compute/iou/utils/iou_export.py" = ["C901", "RUF003"] +"gns3server/compute/project.py" = ["DTZ006", "S324"] +"gns3server/compute/qemu/qemu_vm.py" = ["ASYNC220", "B009", "C901", "RUF003", "S603"] +"gns3server/compute/qemu/utils/ziputils.py" = ["RUF005", "UP031"] +"gns3server/compute/ubridge/ubridge_hypervisor.py" = ["C901"] +"gns3server/compute/virtualbox/virtualbox_vm.py" = ["C901", "RUF005", "S314"] +"gns3server/compute/vmware/__init__.py" = ["C901", "UP015"] +"gns3server/compute/vmware/vmware_vm.py" = ["C901"] +"gns3server/compute/vpcs/vpcs_vm.py" = ["B018"] +"gns3server/config.py" = ["B905", "C901"] +"gns3server/controller/__init__.py" = ["S311"] +"gns3server/controller/appliance_to_template.py" = ["C901"] +"gns3server/controller/compute.py" = ["B006", "C901"] +"gns3server/controller/drawing.py" = ["S314", "S324"] +"gns3server/controller/export_project.py" = ["C901", "DTZ006"] +"gns3server/controller/gns3vm/hyperv_gns3_vm.py" = ["RUF015", "S608"] +"gns3server/controller/gns3vm/virtualbox_gns3_vm.py" = ["C901"] +"gns3server/controller/import_project.py" = ["C901"] +"gns3server/controller/node.py" = ["C901", "PLW1641"] +"gns3server/controller/node_types.py" = ["W292"] +"gns3server/controller/ports/port_factory.py" = ["B020", "C901"] +"gns3server/controller/project.py" = ["ASYNC110", "C901", "RUF006", "RUF019"] +"gns3server/controller/topology.py" = ["C901"] +"gns3server/controller/udp_link.py" = ["C901"] +"gns3server/db/models/base.py" = ["UP031"] +"gns3server/db/repositories/llm_model_configs.py" = ["DTZ003", "RUF005", "RUF019"] +"gns3server/db/repositories/rbac.py" = ["C901"] +"gns3server/db/tasks.py" = ["B009", "RUF006", "RUF019", "UP004"] +"gns3server/db_migrations/versions/7ceeddd9c9a8_init.py" = ["W291"] +"gns3server/db_migrations/versions/ec4b7b198555_convert_pickletype_fields_to_json.py" = ["S301", "S608"] +"gns3server/logger.py" = ["PLW0127", "UP031"] +"gns3server/main.py" = ["UP031"] +"gns3server/schemas/controller/settings.py" = ["S105"] +"gns3server/schemas/controller/templates/docker_templates.py" = ["W292"] +"gns3server/schemas/controller/tokens.py" = ["S105"] +"gns3server/server.py" = ["DTZ011", "RUF006", "UP036"] +"gns3server/services/authentication.py" = ["S105"] +"gns3server/services/templates.py" = ["W292"] +"gns3server/utils/__init__.py" = ["S324", "UP031"] +"gns3server/utils/asyncio/__init__.py" = ["RUF006"] +"gns3server/utils/asyncio/aiozipstream.py" = ["C901", "UP031"] +"gns3server/utils/asyncio/raw_command_server.py" = ["B006"] +"gns3server/utils/asyncio/telnet_server.py" = ["RUF006"] +"gns3server/utils/images.py" = ["RUF021", "S324"] +"gns3server/utils/packet_filter_validation.py" = ["PLW1510", "S603", "S607", "W292"] +"gns3server/utils/picture.py" = ["C901"] +"gns3server/utils/port_allocator.py" = ["S324"] +"gns3server/utils/uninstall_ai_features.py" = ["PLW1510", "S603", "UP015"] +"gns3server/utils/vmnet.py" = ["S605", "S607"] +"gns3server/utils/websocket_to_websocket.py" = ["C901"] +"gns3server/utils/zipfile_zstd/__init__.py" = ["F403"] +"gns3server/version.py" = ["S607"] +"scripts/extract_mermaid.py" = ["C901", "PLW1510", "S603", "S607"] +"scripts/tag.py" = ["S603", "S607"] +"scripts/welcome.py" = ["C901", "E722", "PLW1510", "S108", "S112", "S603", "S605", "S606", "S607", "W291"] +"tests/agent/mcp/test_handlers.py" = ["B905"] +"tests/agent/mcp/test_tool_params.py" = ["C901"] +"tests/api/routes/controller/test_images.py" = ["S324", "W292"] +"tests/api/routes/controller/test_links.py" = ["RUF015", "W292"] +"tests/api/routes/controller/test_users.py" = ["F811"] +"tests/compute/docker/test_docker.py" = ["B018"] +"tests/compute/docker/test_docker_vm.py" = ["F811", "UP004"] +"tests/compute/docker/test_iol_docker_vm.py" = ["RUF003", "RUF015"] +"tests/compute/docker/test_vendor_docker_vm.py" = ["RUF015"] +"tests/compute/qemu/test_qemu_vm.py" = ["E741"] +"tests/compute/test_project.py" = ["RUF003", "RUF064", "S103"] +"tests/compute/virtualbox/test_virtualbox_manager.py" = ["B011"] +"tests/compute/vpcs/test_vpcs_vm.py" = ["RUF003"] +"tests/controller/gns3vm/test_vmware_gns3_vm.py" = ["UP015"] +"tests/controller/test_export_project.py" = ["E713"] +"tests/controller/test_import_project.py" = ["F811"] +"tests/controller/test_marker.py" = ["B006"] +"tests/controller/test_node.py" = ["E711", "F811"] +"tests/controller/test_project.py" = ["RUF015"] +"tests/controller/test_snapshot.py" = ["RUF015"] +"tests/controller/test_udp_link.py" = ["B006"] +"tests/test_server.py" = ["E722"] +"tests/test_topologies.py" = ["B011"] +"tests/test_utils.py" = ["F403", "F405"] +"tests/utils/test_asyncio.py" = ["B017"] +"tests/utils/test_packet_filter_validation.py" = ["W292"] + + +[tool.mypy] +files = ["gns3server"] +plugins = ["pydantic.mypy"] +# Third-party packages without type hints / stubs: +ignore_missing_imports = true +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = true +strict_equality = true +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] + +[[tool.mypy.overrides]] +# Baseline: modules with existing type errors (error count at baseline time). +# Remove modules from this list one small PR at a time. Never add new ones. +module = [ + "gns3server.agent", # 2 + "gns3server.agent.gns3_copilot.agent.context_manager", # 5 + "gns3server.agent.gns3_copilot.agent.model_factory", # 4 + "gns3server.agent.gns3_copilot.agent_service", # 8 + "gns3server.agent.gns3_copilot.chat_sessions_repository", # 8 + "gns3server.agent.gns3_copilot.gns3_client.api_handlers", # 10 + "gns3server.agent.gns3_copilot.gns3_client.connector_factory", # 1 + "gns3server.agent.gns3_copilot.gns3_client.project_inventory", # 2 + "gns3server.agent.gns3_copilot.skills.loader", # 1 + "gns3server.agent.gns3_copilot.skills.manager", # 4 + "gns3server.agent.gns3_copilot.tools_v2.config_tools_nornir", # 6 + "gns3server.agent.gns3_copilot.tools_v2.display_tools_nornir", # 6 + "gns3server.agent.gns3_copilot.tools_v2.gns3_packet_filter", # 1 + "gns3server.agent.gns3_copilot.tools_v2.gns3_start_node", # 3 + "gns3server.agent.gns3_copilot.tools_v2.gns3_stop_node", # 3 + "gns3server.agent.gns3_copilot.tools_v2.gns3_suspend_node", # 3 + "gns3server.agent.gns3_copilot.tools_v2.packet_analysis_tool", # 1 + "gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko", # 9 + "gns3server.agent.gns3_copilot.utils.custom_netmiko.scripts.list_netmiko_telnet_devices", # 1 + "gns3server.agent.gns3_copilot.utils.message_converters", # 2 + "gns3server.agent.gns3_copilot.utils.parse_tool_content", # 1 + "gns3server.agent.mcp", # 34 + "gns3server.agent.mcp.appliances", # 1 + "gns3server.agent.web_wireshark.docker_client", # 5 + "gns3server.agent.web_wireshark.manage_wireshark", # 1 + "gns3server.agent.web_wireshark.manager", # 9 + "gns3server.agent.web_wireshark.stats", # 4 + "gns3server.api.routes.compute.atm_switch_nodes", # 9 + "gns3server.api.routes.compute.capabilities", # 1 + "gns3server.api.routes.compute.cloud_nodes", # 14 + "gns3server.api.routes.compute.compute", # 1 + "gns3server.api.routes.compute.dependencies.authentication", # 3 + "gns3server.api.routes.compute.docker_nodes", # 26 + "gns3server.api.routes.compute.dynamips_nodes", # 9 + "gns3server.api.routes.compute.ethernet_hub_nodes", # 9 + "gns3server.api.routes.compute.ethernet_switch_nodes", # 16 + "gns3server.api.routes.compute.frame_relay_switch_nodes", # 9 + "gns3server.api.routes.compute.iou_nodes", # 15 + "gns3server.api.routes.compute.nat_nodes", # 10 + "gns3server.api.routes.compute.notifications", # 4 + "gns3server.api.routes.compute.projects", # 7 + "gns3server.api.routes.compute.qemu_nodes", # 18 + "gns3server.api.routes.compute.virtualbox_nodes", # 20 + "gns3server.api.routes.compute.vmware_nodes", # 10 + "gns3server.api.routes.compute.vpcs_nodes", # 9 + "gns3server.api.routes.controller.acl", # 5 + "gns3server.api.routes.controller.api_keys", # 1 + "gns3server.api.routes.controller.chat", # 1 + "gns3server.api.routes.controller.computes", # 6 + "gns3server.api.routes.controller.controller", # 10 + "gns3server.api.routes.controller.dependencies.authentication", # 11 + "gns3server.api.routes.controller.dependencies.database", # 2 + "gns3server.api.routes.controller.drawings", # 1 + "gns3server.api.routes.controller.groups", # 6 + "gns3server.api.routes.controller.images", # 9 + "gns3server.api.routes.controller.links", # 27 + "gns3server.api.routes.controller.llm_model_configs", # 32 + "gns3server.api.routes.controller.nodes", # 13 + "gns3server.api.routes.controller.pools", # 8 + "gns3server.api.routes.controller.privileges", # 1 + "gns3server.api.routes.controller.projects", # 16 + "gns3server.api.routes.controller.roles", # 5 + "gns3server.api.routes.controller.snapshots", # 1 + "gns3server.api.routes.controller.templates", # 10 + "gns3server.api.routes.controller.users", # 14 + "gns3server.api.routes.index", # 1 + "gns3server.api.server", # 2 + "gns3server.compute.docker.docker_vm", # 3 + "gns3server.compute.docker.iol_docker_vm", # 3 + "gns3server.compute.dynamips.nodes.router", # 2 + "gns3server.compute.iou.iou_vm", # 4 + "gns3server.compute.iou.utils.iou_export", # 2 + "gns3server.compute.iou.utils.iou_import", # 2 + "gns3server.compute.project", # 1 + "gns3server.compute.qemu.qemu_vm", # 2 + "gns3server.compute.virtualbox.virtualbox_vm", # 1 + "gns3server.compute.vmware.vmware_vm", # 1 + "gns3server.compute.vpcs.vpcs_vm", # 2 + "gns3server.config", # 1 + "gns3server.controller.appliance", # 2 + "gns3server.controller.appliance_manager", # 5 + "gns3server.controller.compute", # 2 + "gns3server.controller.project", # 2 + "gns3server.controller.topology", # 1 + "gns3server.db.models.acl", # 1 + "gns3server.db.models.images", # 1 + "gns3server.db.models.pools", # 1 + "gns3server.db.models.privileges", # 2 + "gns3server.db.models.roles", # 1 + "gns3server.db.models.templates", # 22 + "gns3server.db.models.users", # 3 + "gns3server.db.repositories.api_keys", # 8 + "gns3server.db.repositories.computes", # 10 + "gns3server.db.repositories.images", # 14 + "gns3server.db.repositories.llm_model_configs", # 32 + "gns3server.db.repositories.pools", # 11 + "gns3server.db.repositories.rbac", # 14 + "gns3server.db.repositories.templates", # 6 + "gns3server.db.repositories.users", # 12 + "gns3server.db.tasks", # 12 + "gns3server.db_migrations.env", # 1 + "gns3server.schemas.compute.atm_switch_nodes", # 5 + "gns3server.schemas.compute.cloud_nodes", # 1 + "gns3server.schemas.compute.docker_nodes", # 2 + "gns3server.schemas.compute.dynamips_nodes", # 1 + "gns3server.schemas.compute.frame_relay_switch_nodes", # 5 + "gns3server.schemas.compute.iou_nodes", # 3 + "gns3server.schemas.compute.nat_nodes", # 1 + "gns3server.schemas.compute.qemu_nodes", # 1 + "gns3server.schemas.compute.virtualbox_nodes", # 2 + "gns3server.schemas.compute.vmware_nodes", # 3 + "gns3server.schemas.compute.vpcs_nodes", # 1 + "gns3server.schemas.controller.appliances", # 1 + "gns3server.schemas.controller.computes", # 6 + "gns3server.schemas.controller.nodes", # 3 + "gns3server.schemas.controller.projects", # 2 + "gns3server.schemas.controller.settings", # 3 + "gns3server.schemas.controller.templates.docker_templates", # 1 + "gns3server.schemas.controller.templates.dynamips_templates", # 17 + "gns3server.schemas.controller.templates.iou_templates", # 1 + "gns3server.schemas.controller.templates.virtualbox_templates", # 1 + "gns3server.schemas.controller.templates.vmware_templates", # 1 + "gns3server.schemas.qemu_disk_image", # 2 + "gns3server.services.authentication", # 5 + "gns3server.services.computes", # 4 + "gns3server.services.templates", # 18 + "gns3server.utils.asyncio.aiozipstream", # 8 + "gns3server.utils.asyncio.embed_shell", # 1 + "gns3server.utils.encryption", # 2 + "gns3server.utils.http_client", # 4 + "gns3server.utils.images", # 4 + "gns3server.utils.packet_filter_validation", # 16 + "gns3server.utils.websocket_to_websocket", # 12 + "gns3server.utils.zipfile_zstd", # 2 + "gns3server.utils.zipfile_zstd._patcher", # 1 + "gns3server.utils.zipfile_zstd._zipfile", # 5 +] +ignore_errors = true From 2ac637c662c371f4b2360e58394ec4f2fbb472cc Mon Sep 17 00:00:00 2001 From: Mark Paronyan Date: Sun, 27 Sep 2026 15:43:49 +0300 Subject: [PATCH 50/60] ci: run just test --- .github/workflows/testing.yml | 13 +++++-------- justfile | 2 ++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d0bfa4f1a..42916ecf5 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -21,26 +21,23 @@ jobs: steps: - uses: actions/checkout@v7 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: pip + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install .[ai-features,dev] + - name: Install sharkd (marker replay engine) run: | sudo apt-get update sudo apt-get install -y wireshark-common - - name: Lint with flake8 + - name: Run all checks run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - python -m pytest -vv + just test diff --git a/justfile b/justfile index b07fefc81..43528fd1c 100644 --- a/justfile +++ b/justfile @@ -20,6 +20,8 @@ format: lint: python -m ruff check --exit-non-zero-on-fix python -m ruff format --check --diff + python -m flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + python -m flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics [group('dev')] type-check: From a0450b6bcee8dc6b58286fedde2b005f2de6c775 Mon Sep 17 00:00:00 2001 From: Mark Paronyan Date: Sun, 27 Sep 2026 15:55:10 +0300 Subject: [PATCH 51/60] refactor: ruff autofixes --- .claude/skills/gns3-api-test-writing/SKILL.md | 2 +- PROJECT_CONTEXT.md | 10 +- ...telnet-server-connection-race-condition.md | 2 +- docs/features/mcp-service.md | 30 +- docs/features/project-open-performance.md | 4 +- docs/gns3-copilot/implemented/chat-api.md | 8 +- .../multi-vendor-device-support.md | 15 +- .../implemented/node-control-tools.md | 231 +-- .../implemented/skills-repository.md | 6 +- .../roadmap/rbac-user-isolation-roadmap.md | 16 +- .../template-based-configuration-roadmap.md | 125 +- .../roadmap/user-node-limit-roadmap.md | 14 +- gns3server/agent/__init__.py | 8 +- .../gns3_copilot/agent/context_manager.py | 79 +- .../agent/gns3_copilot/agent/gns3_copilot.py | 64 +- .../agent/gns3_copilot/agent/model_factory.py | 6 +- .../agent/gns3_copilot/agent_service.py | 136 +- .../gns3_copilot/chat_sessions_repository.py | 53 +- .../gns3_copilot/configs/skills_config.py | 3 - .../gns3_copilot/gns3_client/api_handlers.py | 134 +- .../gns3_copilot/gns3_client/connector.py | 52 +- .../gns3_client/connector_factory.py | 51 +- .../gns3_client/context_helpers.py | 8 +- .../gns3_client/gns3_project_info.py | 22 +- .../gns3_client/gns3_topology_reader.py | 26 +- .../gns3_client/project_inventory.py | 42 +- .../gns3_copilot/project_agent_manager.py | 4 +- .../gns3_copilot/prompts/prompt_loader.py | 14 +- .../agent/gns3_copilot/skills/loader.py | 4 +- .../agent/gns3_copilot/skills/manager.py | 59 +- .../agent/gns3_copilot/skills/registry.py | 212 ++- .../tools_v2/config_tools_nornir.py | 128 +- .../tools_v2/display_tools_nornir.py | 107 +- .../gns3_copilot/tools_v2/gns3_create_link.py | 47 +- .../gns3_copilot/tools_v2/gns3_create_node.py | 33 +- .../tools_v2/gns3_get_node_temp.py | 37 +- .../tools_v2/gns3_packet_filter.py | 58 +- .../gns3_copilot/tools_v2/gns3_start_node.py | 39 +- .../gns3_copilot/tools_v2/gns3_stop_node.py | 40 +- .../tools_v2/gns3_suspend_node.py | 41 +- .../tools_v2/gns3_update_node_name.py | 17 +- .../agent/gns3_copilot/tools_v2/gns3_wait.py | 4 +- .../tools_v2/packet_analysis_tool.py | 71 +- .../tools_v2/vpcs_tools_netmiko.py | 125 +- .../gns3_copilot/utils/command_filter.py | 16 +- .../utils/custom_netmiko/README.md | 11 +- .../utils/custom_netmiko/huawei_ce.py | 16 +- .../utils/custom_netmiko/ruijie_telnet.py | 63 +- .../scripts/list_netmiko_telnet_devices.py | 103 +- .../custom_netmiko/tests/test_huawei_ce.py | 29 +- .../tests/test_ruijie_telnet.py | 78 +- .../custom_netmiko/tests/test_vpcs_telnet.py | 42 +- .../utils/custom_netmiko/vpcs_telnet.py | 20 +- .../agent/gns3_copilot/utils/error_handler.py | 5 +- .../utils/get_gns3_device_port.py | 18 +- .../gns3_copilot/utils/gns3_drawing_utils.py | 78 +- .../gns3_copilot/utils/llm_config_helper.py | 4 +- .../gns3_copilot/utils/parse_tool_content.py | 84 +- .../gns3_copilot/utils/tool_call_stream.py | 22 +- gns3server/agent/mcp/__init__.py | 831 ++++++--- gns3server/agent/mcp/appliances.py | 36 +- gns3server/agent/mcp/computes.py | 1 + gns3server/agent/mcp/device_config.py | 38 +- gns3server/agent/mcp/drawings.py | 3 + gns3server/agent/mcp/images.py | 3 + gns3server/agent/mcp/projects.py | 15 +- gns3server/agent/mcp/server.py | 3 + gns3server/agent/mcp/snapshots.py | 3 + gns3server/agent/mcp/symbols.py | 13 +- gns3server/agent/mcp/templates.py | 20 +- .../agent/web_wireshark/docker_client.py | 34 +- .../agent/web_wireshark/manage_wireshark.py | 58 +- gns3server/agent/web_wireshark/manager.py | 238 ++- .../web_wireshark/setup_wireshark_image.py | 46 +- gns3server/agent/web_wireshark/stats.py | 28 +- gns3server/api/routes/compute/__init__.py | 78 +- .../api/routes/compute/atm_switch_nodes.py | 47 +- gns3server/api/routes/compute/cloud_nodes.py | 87 +- .../compute/dependencies/authentication.py | 6 +- gns3server/api/routes/compute/docker_nodes.py | 165 +- .../api/routes/compute/dynamips_nodes.py | 141 +- .../api/routes/compute/ethernet_hub_nodes.py | 47 +- .../routes/compute/ethernet_switch_nodes.py | 91 +- .../compute/frame_relay_switch_nodes.py | 53 +- gns3server/api/routes/compute/iou_nodes.py | 123 +- gns3server/api/routes/compute/nat_nodes.py | 55 +- .../api/routes/compute/notifications.py | 2 - gns3server/api/routes/compute/projects.py | 37 +- gns3server/api/routes/compute/qemu_nodes.py | 180 +- .../api/routes/compute/virtualbox_nodes.py | 113 +- gns3server/api/routes/compute/vmware_nodes.py | 109 +- gns3server/api/routes/compute/vpcs_nodes.py | 157 +- gns3server/api/routes/controller/__init__.py | 181 +- gns3server/api/routes/controller/acl.py | 72 +- .../api/routes/controller/appliances.py | 42 +- gns3server/api/routes/controller/chat.py | 77 +- gns3server/api/routes/controller/computes.py | 27 +- .../api/routes/controller/controller.py | 30 +- .../controller/dependencies/authentication.py | 32 +- .../controller/dependencies/database.py | 2 - .../routes/controller/dependencies/rbac.py | 31 +- gns3server/api/routes/controller/drawings.py | 16 +- gns3server/api/routes/controller/groups.py | 60 +- gns3server/api/routes/controller/images.py | 108 +- gns3server/api/routes/controller/links.py | 113 +- .../routes/controller/llm_model_configs.py | 234 +-- gns3server/api/routes/controller/netmiko.py | 7 +- gns3server/api/routes/controller/nodes.py | 124 +- gns3server/api/routes/controller/pools.py | 63 +- .../api/routes/controller/privileges.py | 2 +- gns3server/api/routes/controller/projects.py | 175 +- gns3server/api/routes/controller/roles.py | 54 +- gns3server/api/routes/controller/settings.py | 97 +- gns3server/api/routes/controller/snapshots.py | 17 +- gns3server/api/routes/controller/symbols.py | 15 +- gns3server/api/routes/controller/templates.py | 69 +- gns3server/api/routes/controller/users.py | 64 +- gns3server/api/server.py | 28 +- gns3server/compute/__init__.py | 1 + gns3server/compute/adapters/adapter.py | 1 - .../compute/adapters/ethernet_adapter.py | 1 - gns3server/compute/adapters/serial_adapter.py | 1 - gns3server/compute/base_manager.py | 6 +- gns3server/compute/base_node.py | 63 +- gns3server/compute/builtin/__init__.py | 2 - .../compute/builtin/builtin_node_factory.py | 1 - gns3server/compute/builtin/nodes/cloud.py | 7 +- .../compute/builtin/nodes/ethernet_hub.py | 8 +- .../compute/builtin/nodes/ethernet_switch.py | 5 +- gns3server/compute/builtin/nodes/nat.py | 14 +- gns3server/compute/docker/__init__.py | 40 +- gns3server/compute/docker/docker_vm.py | 211 ++- gns3server/compute/docker/iol_docker_vm.py | 3 +- gns3server/compute/docker/vendor_docker_vm.py | 73 +- gns3server/compute/dynamips/__init__.py | 23 +- .../compute/dynamips/adapters/adapter.py | 1 - .../compute/dynamips/adapters/c1700_mb_1fe.py | 1 - .../dynamips/adapters/c1700_mb_wic1.py | 1 - .../compute/dynamips/adapters/c2600_mb_1e.py | 1 - .../compute/dynamips/adapters/c2600_mb_1fe.py | 1 - .../compute/dynamips/adapters/c2600_mb_2e.py | 1 - .../compute/dynamips/adapters/c2600_mb_2fe.py | 1 - .../compute/dynamips/adapters/c7200_io_2fe.py | 1 - .../compute/dynamips/adapters/c7200_io_fe.py | 1 - .../dynamips/adapters/c7200_io_ge_e.py | 1 - .../compute/dynamips/adapters/leopard_2fe.py | 1 - .../compute/dynamips/adapters/nm_16esw.py | 1 - gns3server/compute/dynamips/adapters/nm_1e.py | 1 - .../compute/dynamips/adapters/nm_1fe_tx.py | 1 - gns3server/compute/dynamips/adapters/nm_4e.py | 1 - gns3server/compute/dynamips/adapters/nm_4t.py | 1 - .../compute/dynamips/adapters/pa_2fe_tx.py | 1 - gns3server/compute/dynamips/adapters/pa_4e.py | 1 - gns3server/compute/dynamips/adapters/pa_4t.py | 1 - gns3server/compute/dynamips/adapters/pa_8e.py | 1 - gns3server/compute/dynamips/adapters/pa_8t.py | 1 - gns3server/compute/dynamips/adapters/pa_a1.py | 1 - .../compute/dynamips/adapters/pa_fe_tx.py | 1 - gns3server/compute/dynamips/adapters/pa_ge.py | 1 - .../compute/dynamips/adapters/pa_pos_oc3.py | 1 - .../compute/dynamips/adapters/wic_1enet.py | 1 - .../compute/dynamips/adapters/wic_1t.py | 1 - .../compute/dynamips/adapters/wic_2t.py | 1 - gns3server/compute/dynamips/dynamips_error.py | 1 - .../compute/dynamips/dynamips_factory.py | 1 - .../compute/dynamips/dynamips_hypervisor.py | 5 +- gns3server/compute/dynamips/hypervisor.py | 1 - gns3server/compute/dynamips/nios/nio.py | 1 - .../dynamips/nios/nio_generic_ethernet.py | 6 +- .../dynamips/nios/nio_linux_ethernet.py | 6 +- gns3server/compute/dynamips/nios/nio_null.py | 1 - gns3server/compute/dynamips/nios/nio_tap.py | 6 +- gns3server/compute/dynamips/nios/nio_udp.py | 8 +- gns3server/compute/dynamips/nios/nio_unix.py | 7 +- gns3server/compute/dynamips/nios/nio_vde.py | 7 +- .../compute/dynamips/nodes/atm_switch.py | 1 - gns3server/compute/dynamips/nodes/bridge.py | 1 - gns3server/compute/dynamips/nodes/c1700.py | 1 - gns3server/compute/dynamips/nodes/c2600.py | 1 - gns3server/compute/dynamips/nodes/c2691.py | 1 - gns3server/compute/dynamips/nodes/c3600.py | 1 - gns3server/compute/dynamips/nodes/c3725.py | 1 - gns3server/compute/dynamips/nodes/c3745.py | 1 - gns3server/compute/dynamips/nodes/c7200.py | 1 - gns3server/compute/dynamips/nodes/device.py | 1 - .../compute/dynamips/nodes/ethernet_hub.py | 1 - .../compute/dynamips/nodes/ethernet_switch.py | 1 - .../dynamips/nodes/frame_relay_switch.py | 1 - gns3server/compute/dynamips/nodes/router.py | 36 +- gns3server/compute/iou/__init__.py | 1 - gns3server/compute/iou/iou_vm.py | 35 +- gns3server/compute/marker/marker_listener.py | 4 +- gns3server/compute/marker/marker_manager.py | 16 +- gns3server/compute/nios/nio.py | 1 - gns3server/compute/nios/nio_ethernet.py | 6 +- gns3server/compute/nios/nio_tap.py | 6 +- gns3server/compute/nios/nio_udp.py | 3 +- gns3server/compute/port_manager.py | 12 +- gns3server/compute/project.py | 90 +- gns3server/compute/project_manager.py | 3 +- gns3server/compute/qemu/__init__.py | 11 +- gns3server/compute/qemu/qemu_error.py | 1 - gns3server/compute/qemu/qemu_vm.py | 126 +- gns3server/compute/qemu/utils/qcow2.py | 15 +- gns3server/compute/ubridge/hypervisor.py | 1 - .../compute/ubridge/ubridge_hypervisor.py | 1 - gns3server/compute/virtualbox/__init__.py | 6 +- .../compute/virtualbox/virtualbox_error.py | 1 - .../compute/virtualbox/virtualbox_vm.py | 12 +- gns3server/compute/vmware/__init__.py | 1 - gns3server/compute/vmware/vmware_error.py | 1 - gns3server/compute/vmware/vmware_vm.py | 7 +- gns3server/compute/vpcs/__init__.py | 1 - gns3server/compute/vpcs/vpcs_error.py | 1 - gns3server/config.py | 20 +- gns3server/controller/__init__.py | 38 +- gns3server/controller/appliance.py | 1 - gns3server/controller/appliance_manager.py | 132 +- .../controller/appliance_to_template.py | 3 +- gns3server/controller/compute.py | 21 +- gns3server/controller/export_project.py | 12 +- .../controller/gns3vm/hyperv_gns3_vm.py | 1 - .../controller/gns3vm/virtualbox_gns3_vm.py | 81 +- gns3server/controller/import_project.py | 33 +- gns3server/controller/link.py | 37 +- gns3server/controller/marker_replay.py | 131 +- gns3server/controller/node.py | 128 +- gns3server/controller/node_types.py | 18 +- gns3server/controller/ports/ethernet_port.py | 2 - gns3server/controller/ports/port.py | 2 +- gns3server/controller/ports/port_factory.py | 1 - gns3server/controller/project.py | 140 +- gns3server/controller/snapshot.py | 15 +- gns3server/controller/topology.py | 9 +- gns3server/controller/udp_link.py | 95 +- gns3server/core/tasks.py | 4 +- gns3server/crash_report.py | 10 +- gns3server/db/models/acl.py | 7 +- gns3server/db/models/api_keys.py | 1 - gns3server/db/models/base.py | 6 +- gns3server/db/models/computes.py | 1 - gns3server/db/models/images.py | 3 +- gns3server/db/models/llm_model_configs.py | 23 +- gns3server/db/models/pools.py | 4 +- gns3server/db/models/privileges.py | 276 +-- gns3server/db/models/roles.py | 5 +- gns3server/db/models/templates.py | 11 - gns3server/db/models/users.py | 15 +- gns3server/db/repositories/api_keys.py | 25 +- gns3server/db/repositories/computes.py | 5 +- gns3server/db/repositories/images.py | 36 +- .../db/repositories/llm_model_configs.py | 192 +- gns3server/db/repositories/pools.py | 57 +- gns3server/db/repositories/rbac.py | 135 +- gns3server/db/repositories/templates.py | 68 +- gns3server/db/repositories/users.py | 65 +- gns3server/db/tasks.py | 42 +- gns3server/db_migrations/env.py | 4 +- ...20260303_create_llm_model_configs_table.py | 64 +- .../versions/7ceeddd9c9a8_init.py | 5 +- ...b_add_extra_configs_to_docker_templates.py | 9 +- ...8083573d011_add_tags_field_to_templates.py | 9 +- ...aa4389_add_mac_address_field_in_docker_.py | 10 +- .../a8829e6c069b_add_llm_config_privileges.py | 47 +- ...fc119a_add_token_version_to_users_table.py | 10 +- ...4f_add_netmiko_device_type_to_templates.py | 9 +- ...2b6_add_appliance_metadata_to_templates.py | 9 +- ...98555_convert_pickletype_fields_to_json.py | 74 +- .../versions/f0b0de2a9_add_api_keys_table.py | 39 +- gns3server/logger.py | 9 +- gns3server/main.py | 15 +- gns3server/schemas/__init__.py | 49 +- .../schemas/compute/atm_switch_nodes.py | 1 - gns3server/schemas/compute/cloud_nodes.py | 5 - gns3server/schemas/compute/docker_nodes.py | 16 +- gns3server/schemas/compute/dynamips_nodes.py | 1 - .../schemas/compute/ethernet_hub_nodes.py | 2 - .../schemas/compute/ethernet_switch_nodes.py | 8 +- .../compute/frame_relay_switch_nodes.py | 1 - gns3server/schemas/compute/iou_nodes.py | 2 - gns3server/schemas/compute/nat_nodes.py | 2 - gns3server/schemas/compute/nios.py | 3 - gns3server/schemas/compute/qemu_nodes.py | 4 - .../schemas/compute/virtualbox_nodes.py | 2 - gns3server/schemas/compute/vmware_nodes.py | 1 - gns3server/schemas/compute/vpcs_nodes.py | 1 - gns3server/schemas/config.py | 180 +- gns3server/schemas/controller/appliances.py | 839 ++++----- gns3server/schemas/controller/base.py | 1 - gns3server/schemas/controller/chat.py | 18 +- gns3server/schemas/controller/computes.py | 46 +- gns3server/schemas/controller/images.py | 1 - gns3server/schemas/controller/iou_license.py | 1 - gns3server/schemas/controller/links.py | 38 +- .../schemas/controller/llm_model_configs.py | 16 +- gns3server/schemas/controller/nodes.py | 10 +- gns3server/schemas/controller/pools.py | 3 - gns3server/schemas/controller/projects.py | 4 - gns3server/schemas/controller/rbac.py | 4 - gns3server/schemas/controller/settings.py | 31 +- gns3server/schemas/controller/snapshots.py | 2 +- .../schemas/controller/templates/__init__.py | 12 +- .../controller/templates/cloud_templates.py | 9 +- .../controller/templates/docker_templates.py | 12 +- .../templates/dynamips_templates.py | 18 - .../templates/ethernet_hub_templates.py | 2 - .../templates/ethernet_switch_templates.py | 2 - .../controller/templates/iou_templates.py | 2 - .../controller/templates/qemu_templates.py | 26 +- .../templates/virtualbox_templates.py | 7 +- .../controller/templates/vmware_templates.py | 8 +- .../controller/templates/vpcs_templates.py | 2 - gns3server/schemas/controller/tokens.py | 2 - gns3server/schemas/controller/topology.py | 3 - gns3server/schemas/controller/users.py | 3 - gns3server/schemas/qemu_disk_image.py | 3 - gns3server/schemas/version.py | 1 - gns3server/server.py | 16 +- gns3server/services/authentication.py | 11 +- gns3server/services/templates.py | 16 +- gns3server/utils/__init__.py | 1 + gns3server/utils/asyncio/__init__.py | 4 +- gns3server/utils/asyncio/aiozipstream.py | 28 +- gns3server/utils/asyncio/embed_shell.py | 12 +- gns3server/utils/asyncio/input_stream.py | 3 +- .../utils/asyncio/raw_command_server.py | 2 +- gns3server/utils/asyncio/telnet_server.py | 6 +- gns3server/utils/encryption.py | 31 +- gns3server/utils/get_resource.py | 1 + gns3server/utils/hostname.py | 22 +- gns3server/utils/images.py | 55 +- gns3server/utils/interfaces.py | 15 +- gns3server/utils/packet_filter_validation.py | 42 +- gns3server/utils/request_utils.py | 2 +- gns3server/utils/uninstall_ai_features.py | 15 +- gns3server/utils/uuid_validator.py | 5 +- gns3server/utils/websocket_to_websocket.py | 48 +- gns3server/utils/zipfile_zstd/__init__.py | 2 - gns3server/utils/zipfile_zstd/_patcher.py | 2 - gns3server/utils/zipfile_zstd/_zipfile.py | 28 +- gns3server/version.py | 1 + scripts/extract_mermaid.py | 65 +- scripts/publish_api_documentation.py | 12 +- scripts/tag.py | 8 +- scripts/welcome.py | 225 ++- tests/agent/mcp/conftest.py | 1 + tests/agent/mcp/test_device_tools.py | 114 +- tests/agent/mcp/test_handlers.py | 373 ++-- tests/agent/mcp/test_tool_params.py | 73 +- tests/api/routes/compute/test_capabilities.py | 60 +- tests/api/routes/compute/test_cloud_nodes.py | 224 +-- tests/api/routes/compute/test_compute.py | 33 +- tests/api/routes/compute/test_docker_nodes.py | 207 +-- .../api/routes/compute/test_dynamips_nodes.py | 48 +- .../compute/test_ethernet_switch_nodes.py | 296 ++- tests/api/routes/compute/test_images.py | 14 +- tests/api/routes/compute/test_iou_nodes.py | 364 ++-- tests/api/routes/compute/test_nat_nodes.py | 169 +- .../api/routes/compute/test_notifications.py | 24 +- tests/api/routes/compute/test_projects.py | 123 +- tests/api/routes/compute/test_qemu_nodes.py | 572 +++--- .../routes/compute/test_virtualbox_nodes.py | 248 ++- tests/api/routes/compute/test_vmware_nodes.py | 247 ++- tests/api/routes/compute/test_vpcs_nodes.py | 351 ++-- .../routes/controller/test_access_tickets.py | 73 +- tests/api/routes/controller/test_acl.py | 88 +- .../api/routes/controller/test_appliances.py | 31 +- tests/api/routes/controller/test_computes.py | 57 +- .../api/routes/controller/test_controller.py | 14 +- tests/api/routes/controller/test_drawings.py | 63 +- tests/api/routes/controller/test_gns3vm.py | 26 +- tests/api/routes/controller/test_groups.py | 72 +- tests/api/routes/controller/test_images.py | 181 +- tests/api/routes/controller/test_links.py | 413 ++--- .../routes/controller/test_marker_replay.py | 205 ++- tests/api/routes/controller/test_markers.py | 53 +- tests/api/routes/controller/test_netmiko.py | 1 - tests/api/routes/controller/test_nodes.py | 660 +++---- tests/api/routes/controller/test_pools.py | 55 +- .../api/routes/controller/test_privileges.py | 1 - tests/api/routes/controller/test_projects.py | 508 +++--- tests/api/routes/controller/test_roles.py | 62 +- tests/api/routes/controller/test_settings.py | 88 +- tests/api/routes/controller/test_snapshots.py | 48 +- tests/api/routes/controller/test_symbols.py | 37 +- tests/api/routes/controller/test_templates.py | 1582 ++++++++--------- tests/api/routes/controller/test_users.py | 347 ++-- tests/api/routes/controller/test_version.py | 36 +- tests/api/routes/test_index.py | 10 +- tests/api/routes/test_routes.py | 39 +- tests/compute/builtin/nodes/test_cloud.py | 178 +- tests/compute/builtin/nodes/test_nat.py | 39 +- tests/compute/docker/test_docker.py | 134 +- tests/compute/docker/test_docker_vm.py | 1403 +++++++-------- tests/compute/docker/test_iol_docker_vm.py | 169 +- tests/compute/docker/test_vendor_docker_vm.py | 197 +- .../compute/dynamips/test_dynamips_manager.py | 26 +- .../compute/dynamips/test_ethernet_switch.py | 8 +- tests/compute/iou/test_iou_vm.py | 43 +- tests/compute/marker/test_marker_manager.py | 22 +- tests/compute/qemu/test_qcow2.py | 9 +- tests/compute/qemu/test_qemu_manager.py | 14 +- tests/compute/qemu/test_qemu_vm.py | 127 +- tests/compute/test_base_node.py | 88 +- tests/compute/test_manager.py | 34 +- tests/compute/test_notification_manager.py | 2 +- tests/compute/test_project.py | 34 +- tests/compute/test_project_manager.py | 6 +- tests/compute/test_statistics.py | 16 +- tests/compute/ubridge/test_hypervisor.py | 17 +- .../virtualbox/test_virtualbox_manager.py | 19 +- .../compute/virtualbox/test_virtualbox_vm.py | 4 +- tests/compute/vmware/test_vmware_vm.py | 8 +- tests/compute/vpcs/test_vpcs_vm.py | 109 +- tests/conftest.py | 73 +- .../controller/gns3vm/test_remote_gns3_vm.py | 37 +- .../gns3vm/test_virtualbox_gns3_vm.py | 9 +- .../controller/gns3vm/test_vmware_gns3_vm.py | 9 +- tests/controller/test_appliance.py | 31 +- tests/controller/test_appliance_manager.py | 19 +- .../controller/test_appliance_to_template.py | 70 +- tests/controller/test_compute.py | 237 ++- tests/controller/test_controller.py | 78 +- tests/controller/test_drawing.py | 12 +- tests/controller/test_export_project.py | 181 +- tests/controller/test_gns3vm.py | 15 +- tests/controller/test_import_project.py | 222 ++- tests/controller/test_link.py | 81 +- tests/controller/test_marker.py | 79 +- tests/controller/test_marker_replay.py | 336 ++-- tests/controller/test_node.py | 381 ++-- tests/controller/test_node_port_name.py | 232 +-- tests/controller/test_notification.py | 36 +- tests/controller/test_project.py | 120 +- tests/controller/test_project_open.py | 71 +- tests/controller/test_rbac.py | 166 +- tests/controller/test_snapshot.py | 5 +- tests/controller/test_statistics.py | 6 +- tests/controller/test_symbols.py | 20 +- tests/controller/test_topology.py | 71 +- tests/controller/test_udp_link.py | 252 +-- tests/stress/benchmark_running_server.py | 111 +- tests/stress/telnet_race_condition_test.py | 119 +- tests/test_config.py | 100 +- tests/test_server.py | 65 +- tests/test_topologies.py | 24 +- tests/test_utils.py | 24 +- tests/utils.py | 3 +- tests/utils/asyncio/test_embed_shell.py | 4 +- tests/utils/asyncio/test_ssh_server.py | 6 +- tests/utils/asyncio/test_telnet_server.py | 4 +- tests/utils/test_asyncio.py | 9 +- tests/utils/test_file_watcher.py | 6 +- tests/utils/test_images.py | 107 +- tests/utils/test_packet_filter_validation.py | 24 +- 455 files changed, 12322 insertions(+), 14762 deletions(-) diff --git a/.claude/skills/gns3-api-test-writing/SKILL.md b/.claude/skills/gns3-api-test-writing/SKILL.md index 991df03f2..a43bab40c 100644 --- a/.claude/skills/gns3-api-test-writing/SKILL.md +++ b/.claude/skills/gns3-api-test-writing/SKILL.md @@ -33,7 +33,7 @@ from gns3server.services import auth_service from gns3server.services.authentication import DEFAULT_JWT_SECRET_KEY token = auth_service.create_access_token(test_user.username, secret_key=DEFAULT_JWT_SECRET_KEY) -response = await client.get(url, headers={"Authorization": f"Bearer {token}"}) # specific user +response = await client.get(url, headers={"Authorization": f"Bearer {token}"}) # specific user response = await client.get(url, headers={"Authorization": "Bearer invalid_token"}) # 401 ``` diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md index b09064e9a..46747d1dd 100644 --- a/PROJECT_CONTEXT.md +++ b/PROJECT_CONTEXT.md @@ -126,34 +126,37 @@ flake8 gns3server/utils/ import os # Never used # Good: Delete this line + # F841: Mark as intentionally unused # Bad def foo(): x = 1 # Never used + # Good: Use or mark as intentionally unused def foo(): x = 1 _ = x # Mark as intentionally unused + # F824: Remove unnecessary global declaration # Bad def foo(): global _fernet # Never assigned in this scope + # Good: Remove global statement (if only reading) def foo(): # Just read the global variable, no global declaration needed pass + # E501: Break long lines # Bad raise RuntimeError("Encryption not initialized. Call init_encryption() first.") # Good -raise RuntimeError( - "Encryption not initialized. Call init_encryption() first." -) +raise RuntimeError("Encryption not initialized. Call init_encryption() first.") ``` #### Ruff (Alternative) @@ -177,6 +180,7 @@ ruff format gns3server/ # Format code def process_data(input_data: dict[str, Any]) -> list[dict[str, Any]]: pass + # 2. Error handling try: result = tool.invoke(args) diff --git a/docs/bugs/telnet-server-connection-race-condition.md b/docs/bugs/telnet-server-connection-race-condition.md index e9fdf5e75..979afcb0c 100644 --- a/docs/bugs/telnet-server-connection-race-condition.md +++ b/docs/bugs/telnet-server-connection-race-condition.md @@ -279,7 +279,7 @@ Fix the WebSocket error handler: ```python @app.exception_handler(ControllerNotFoundError) async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError): - method = getattr(request, 'method', 'WebSocket') + method = getattr(request, "method", "WebSocket") log.error(f"Controller not found error in {request.url.path} ({method}): {exc}") return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, diff --git a/docs/features/mcp-service.md b/docs/features/mcp-service.md index a6d5eb837..5390211a3 100644 --- a/docs/features/mcp-service.md +++ b/docs/features/mcp-service.md @@ -238,25 +238,32 @@ Both `device_config_send` and `device_show_run` support an optional `template` p ```python # Direct commands (single/batch) -device_config_send(project_id, device_configs=[ - {"device_name": "R1", "config_commands": ["int lo0", "ip add 1.1.1.1 255.255.255.255"]}, -]) +device_config_send( + project_id, + device_configs=[ + {"device_name": "R1", "config_commands": ["int lo0", "ip add 1.1.1.1 255.255.255.255"]}, + ], +) # Jinja2 template (reduces token usage for batch) -device_config_send(project_id, +device_config_send( + project_id, template="interface lo{{ n }}\nip address {{ ip }} 255.255.255.255", device_configs=[ {"device_name": "R1", "vars": {"n": 0, "ip": "1.1.1.1"}}, {"device_name": "R2", "vars": {"n": 0, "ip": "2.2.2.2"}}, - ]) + ], +) # Show commands with template -device_show_run(project_id, +device_show_run( + project_id, template="show ip route {{ protocol }}", device_configs=[ {"device_name": "R1", "vars": {"protocol": "ospf"}}, {"device_name": "R2", "vars": {"protocol": "bgp"}}, - ]) + ], +) ``` ### Best Practices @@ -273,9 +280,12 @@ device_show_run(project_id, ```python # Save config on device -device_show_run(project_id, device_configs=[ - {"device_name": "R1", "commands": ["write memory"]}, -]) +device_show_run( + project_id, + device_configs=[ + {"device_name": "R1", "commands": ["write memory"]}, + ], +) # Backup config = node_file_get(project_id, node_id, "startup-config.cfg") # Restore if config breaks diff --git a/docs/features/project-open-performance.md b/docs/features/project-open-performance.md index 38cd4334c..e3be54315 100644 --- a/docs/features/project-open-performance.md +++ b/docs/features/project-open-performance.md @@ -51,8 +51,8 @@ Each IOU VM creation spawned `ld-linux --verify` and `iou-image -h` subprocesses ```python # Class-level caches shared across all instances -IOUVM._loader_cache = {} # image path → loader command -IOUVM._default_values_cache = {} # image path → (ram, nvram) +IOUVM._loader_cache = {} # image path → loader command +IOUVM._default_values_cache = {} # image path → (ram, nvram) ``` Only the first node with a given image runs the subprocesses; subsequent nodes reuse cached values. diff --git a/docs/gns3-copilot/implemented/chat-api.md b/docs/gns3-copilot/implemented/chat-api.md index f11b7f98c..22cc9341b 100644 --- a/docs/gns3-copilot/implemented/chat-api.md +++ b/docs/gns3-copilot/implemented/chat-api.md @@ -798,7 +798,7 @@ OpenAI-compatible message model. content=observation, # Always JSON string format tool_call_id=tool_call["id"], name=tool_call["name"], - metadata={"created_at": datetime.utcnow().isoformat()} + metadata={"created_at": datetime.utcnow().isoformat()}, ) ``` @@ -924,11 +924,7 @@ ToolMessage(content=JSON_string) **Route Registration**: ```python -router.include_router( - chat.router, - prefix="/{project_id}/chat", - tags=["Chat"] -) +router.include_router(chat.router, prefix="/{project_id}/chat", tags=["Chat"]) ``` **Main Endpoint Implementation**: diff --git a/docs/gns3-copilot/implemented/multi-vendor-device-support.md b/docs/gns3-copilot/implemented/multi-vendor-device-support.md index eaaa63801..8dde4759b 100644 --- a/docs/gns3-copilot/implemented/multi-vendor-device-support.md +++ b/docs/gns3-copilot/implemented/multi-vendor-device-support.md @@ -79,9 +79,11 @@ PC1> ping 10.10.0.254 def check_config_mode(self) -> bool: return False # VPCS has no config mode + def config_mode(self) -> str: return "" # No config mode to enter + def exit_config_mode(self) -> str: return "" # No config mode to exit ``` @@ -354,6 +356,7 @@ The custom driver must be registered with Netmiko's global mappings: ```python def register_custom_device_type() -> None: import importlib + sd = importlib.import_module("netmiko.ssh_dispatcher") # Register in CLASS_MAPPER (for ConnectHandler) @@ -430,12 +433,12 @@ gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py **1. Preprocessing - Known Interactive Commands** ```python INTERACTIVE_PATTERNS = [ - re.compile(r'^router-id\s+', re.IGNORECASE), # OSPF/EIGRP/BGP router-id - re.compile(r'^erase\s+', re.IGNORECASE), # erase startup-config - re.compile(r'^delete\s+', re.IGNORECASE), # delete files - re.compile(r'^format\s+', re.IGNORECASE), # format filesystem - re.compile(r'^reload\b', re.IGNORECASE), # reload/reboot - re.compile(r'^boot\s+system\s+', re.IGNORECASE), # change boot image + re.compile(r"^router-id\s+", re.IGNORECASE), # OSPF/EIGRP/BGP router-id + re.compile(r"^erase\s+", re.IGNORECASE), # erase startup-config + re.compile(r"^delete\s+", re.IGNORECASE), # delete files + re.compile(r"^format\s+", re.IGNORECASE), # format filesystem + re.compile(r"^reload\b", re.IGNORECASE), # reload/reboot + re.compile(r"^boot\s+system\s+", re.IGNORECASE), # change boot image ] ``` diff --git a/docs/gns3-copilot/implemented/node-control-tools.md b/docs/gns3-copilot/implemented/node-control-tools.md index b09083f08..4937d4d2a 100644 --- a/docs/gns3-copilot/implemented/node-control-tools.md +++ b/docs/gns3-copilot/implemented/node-control-tools.md @@ -520,9 +520,14 @@ The tools call the shared REST handler layer (`gns3_copilot.gns3_client.api_hand ```python from gns3server.agent.gns3_copilot.gns3_client.api_handlers import ( - build_gns3_ctx, create_node_handler, create_link_handler, - get_nodes_handler, start_node_handler, stop_node_handler, - suspend_node_handler, update_node_handler, + build_gns3_ctx, + create_node_handler, + create_link_handler, + get_nodes_handler, + start_node_handler, + stop_node_handler, + suspend_node_handler, + update_node_handler, ) gns3_ctx = build_gns3_ctx() # JWT + server URL from the request context @@ -650,10 +655,7 @@ Note: These special node types are filtered out by GNS3TemplateTool and won't ap from gns3server.agent.gns3_copilot.tools_v2 import GNS3StartNodeTool tool = GNS3StartNodeTool() -result = tool._run(json.dumps({ - "project_id": "abc-123-def", - "node_ids": ["node-1", "node-2", "node-3"] -})) +result = tool._run(json.dumps({"project_id": "abc-123-def", "node_ids": ["node-1", "node-2", "node-3"]})) # Output includes progress bar and final status ``` @@ -664,10 +666,7 @@ result = tool._run(json.dumps({ from gns3server.agent.gns3_copilot.tools_v2 import GNS3StopNodeTool tool = GNS3StopNodeTool() -result = tool._run(json.dumps({ - "project_id": "abc-123-def", - "node_ids": ["node-1", "node-2"] -})) +result = tool._run(json.dumps({"project_id": "abc-123-def", "node_ids": ["node-1", "node-2"]})) # Immediate return with stop status ``` @@ -677,19 +676,13 @@ result = tool._run(json.dumps({ ```python # Lab deployment start_tool = GNS3StartNodeTool() -start_result = start_tool._run(json.dumps({ - "project_id": project_id, - "node_ids": all_node_ids -})) +start_result = start_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids})) # ... Run tests ... # Lab shutdown stop_tool = GNS3StopNodeTool() -stop_result = stop_tool._run(json.dumps({ - "project_id": project_id, - "node_ids": all_node_ids -})) +stop_result = stop_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids})) ``` ### Example 4: Lab Pause and Resume @@ -699,56 +692,45 @@ from gns3server.agent.gns3_copilot.tools_v2 import GNS3SuspendNodeTool # Start lab start_tool = GNS3StartNodeTool() -start_result = start_tool._run(json.dumps({ - "project_id": project_id, - "node_ids": all_node_ids -})) +start_result = start_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids})) # ... Configure devices ... # ... Run some tests ... # Suspend lab (preserves all state) suspend_tool = GNS3SuspendNodeTool() -suspend_result = suspend_tool._run(json.dumps({ - "project_id": project_id, - "node_ids": all_node_ids -})) +suspend_result = suspend_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids})) # Nodes suspended - state preserved in memory # ... Take a break, work on something else ... # Resume lab (quick recovery) -start_result = start_tool._run(json.dumps({ - "project_id": project_id, - "node_ids": all_node_ids -})) +start_result = start_tool._run(json.dumps({"project_id": project_id, "node_ids": all_node_ids})) # Back to previous state in seconds! ``` ### Example 5: Suspend While Renaming Nodes ```python -from gns3server.agent.gns3_copilot.tools_v2 import ( - GNS3SuspendNodeTool, - GNS3UpdateNodeNameTool -) +from gns3server.agent.gns3_copilot.tools_v2 import GNS3SuspendNodeTool, GNS3UpdateNodeNameTool # Suspend nodes first (allows renaming) suspend_tool = GNS3SuspendNodeTool() -suspend_result = suspend_tool._run(json.dumps({ - "project_id": project_id, - "node_ids": node_ids -})) +suspend_result = suspend_tool._run(json.dumps({"project_id": project_id, "node_ids": node_ids})) # Now rename nodes (possible while suspended!) rename_tool = GNS3UpdateNodeNameTool() -rename_result = rename_tool._run(json.dumps({ - "project_id": project_id, - "nodes": [ - {"node_id": "node-1", "new_name": "Router-Primary"}, - {"node_id": "node-2", "new_name": "Router-Backup"} - ] -})) +rename_result = rename_tool._run( + json.dumps( + { + "project_id": project_id, + "nodes": [ + {"node_id": "node-1", "new_name": "Router-Primary"}, + {"node_id": "node-2", "new_name": "Router-Backup"}, + ], + } + ) +) # Resume when ready # Note: Cannot rename while started, but CAN rename while suspended! @@ -777,21 +759,17 @@ result = tool._run("") from gns3server.agent.gns3_copilot.tools_v2 import GNS3CreateNodeTool tool = GNS3CreateNodeTool() -result = tool._run(json.dumps({ - "project_id": "abc-123-def", - "nodes": [ +result = tool._run( + json.dumps( { - "template_id": "uuid-of-router-template", - "x": 100, - "y": -200 - }, - { - "template_id": "uuid-of-switch-template", - "x": -200, - "y": 300 + "project_id": "abc-123-def", + "nodes": [ + {"template_id": "uuid-of-router-template", "x": 100, "y": -200}, + {"template_id": "uuid-of-switch-template", "x": -200, "y": 300}, + ], } - ] -})) + ) +) # Creates two nodes with specified templates and positions ``` @@ -802,23 +780,27 @@ result = tool._run(json.dumps({ from gns3server.agent.gns3_copilot.tools_v2 import GNS3LinkTool tool = GNS3LinkTool() -result = tool._run(json.dumps({ - "project_id": "abc-123-def", - "links": [ +result = tool._run( + json.dumps( { - "node_id1": "uuid-of-node1", - "port1": "Ethernet0/0", - "node_id2": "uuid-of-node2", - "port2": "Ethernet0/0" - }, - { - "node_id1": "uuid-of-node1", - "port1": "Ethernet0/1", - "node_id2": "uuid-of-node3", - "port2": "Ethernet0/0" + "project_id": "abc-123-def", + "links": [ + { + "node_id1": "uuid-of-node1", + "port1": "Ethernet0/0", + "node_id2": "uuid-of-node2", + "port2": "Ethernet0/0", + }, + { + "node_id1": "uuid-of-node1", + "port1": "Ethernet0/1", + "node_id2": "uuid-of-node3", + "port2": "Ethernet0/0", + }, + ], } - ] -})) + ) +) # Creates two links connecting the nodes ``` @@ -829,15 +811,19 @@ result = tool._run(json.dumps({ from gns3server.agent.gns3_copilot.tools_v2 import GNS3UpdateNodeNameTool tool = GNS3UpdateNodeNameTool() -result = tool._run(json.dumps({ - "project_id": "abc-123-def", - "nodes": [ - {"node_id": "node-1", "new_name": "R1-Core"}, - {"node_id": "node-2", "new_name": "R2-Core"}, - {"node_id": "node-3", "new_name": "S1-Access"}, - {"node_id": "node-4", "new_name": "S2-Access"} - ] -})) +result = tool._run( + json.dumps( + { + "project_id": "abc-123-def", + "nodes": [ + {"node_id": "node-1", "new_name": "R1-Core"}, + {"node_id": "node-2", "new_name": "R2-Core"}, + {"node_id": "node-3", "new_name": "S1-Access"}, + {"node_id": "node-4", "new_name": "S2-Access"}, + ], + } + ) +) # Applies consistent naming to all nodes ``` @@ -850,7 +836,7 @@ from gns3server.agent.gns3_copilot.tools_v2 import ( GNS3CreateNodeTool, GNS3LinkTool, GNS3UpdateNodeNameTool, - GNS3StartNodeTool + GNS3StartNodeTool, ) # Step 1: Get available templates @@ -860,44 +846,63 @@ templates = template_tool._run("") # Step 2: Create nodes create_tool = GNS3CreateNodeTool() -nodes = create_tool._run(json.dumps({ - "project_id": project_id, - "nodes": [ - {"template_id": router_template_id, "x": 0, "y": -200}, - {"template_id": router_template_id, "x": 200, "y": -200}, - {"template_id": switch_template_id, "x": 100, "y": 0} - ] -})) +nodes = create_tool._run( + json.dumps( + { + "project_id": project_id, + "nodes": [ + {"template_id": router_template_id, "x": 0, "y": -200}, + {"template_id": router_template_id, "x": 200, "y": -200}, + {"template_id": switch_template_id, "x": 100, "y": 0}, + ], + } + ) +) # Step 3: Connect nodes link_tool = GNS3LinkTool() -links = link_tool._run(json.dumps({ - "project_id": project_id, - "links": [ - {"node_id1": nodes["created_nodes"][0]["node_id"], "port1": "Ethernet0/0", - "node_id2": nodes["created_nodes"][2]["node_id"], "port2": "Ethernet0/0"}, - {"node_id1": nodes["created_nodes"][1]["node_id"], "port1": "Ethernet0/0", - "node_id2": nodes["created_nodes"][2]["node_id"], "port2": "Ethernet0/1"} - ] -})) +links = link_tool._run( + json.dumps( + { + "project_id": project_id, + "links": [ + { + "node_id1": nodes["created_nodes"][0]["node_id"], + "port1": "Ethernet0/0", + "node_id2": nodes["created_nodes"][2]["node_id"], + "port2": "Ethernet0/0", + }, + { + "node_id1": nodes["created_nodes"][1]["node_id"], + "port1": "Ethernet0/0", + "node_id2": nodes["created_nodes"][2]["node_id"], + "port2": "Ethernet0/1", + }, + ], + } + ) +) # Step 4: Apply naming name_tool = GNS3UpdateNodeNameTool() -names = name_tool._run(json.dumps({ - "project_id": project_id, - "nodes": [ - {"node_id": nodes["created_nodes"][0]["node_id"], "new_name": "R1"}, - {"node_id": nodes["created_nodes"][1]["node_id"], "new_name": "R2"}, - {"node_id": nodes["created_nodes"][2]["node_id"], "new_name": "SW1"} - ] -})) +names = name_tool._run( + json.dumps( + { + "project_id": project_id, + "nodes": [ + {"node_id": nodes["created_nodes"][0]["node_id"], "new_name": "R1"}, + {"node_id": nodes["created_nodes"][1]["node_id"], "new_name": "R2"}, + {"node_id": nodes["created_nodes"][2]["node_id"], "new_name": "SW1"}, + ], + } + ) +) # Step 5: Start nodes start_tool = GNS3StartNodeTool() -start_result = start_tool._run(json.dumps({ - "project_id": project_id, - "node_ids": [n["node_id"] for n in nodes["created_nodes"]] -})) +start_result = start_tool._run( + json.dumps({"project_id": project_id, "node_ids": [n["node_id"] for n in nodes["created_nodes"]]}) +) ``` ## Error Handling diff --git a/docs/gns3-copilot/implemented/skills-repository.md b/docs/gns3-copilot/implemented/skills-repository.md index 167941465..76f9e3c96 100644 --- a/docs/gns3-copilot/implemented/skills-repository.md +++ b/docs/gns3-copilot/implemented/skills-repository.md @@ -150,9 +150,9 @@ Git operations use per-command environment variables to prevent hanging: ```python _GIT_TIMEOUT_ENV = { - 'GIT_HTTP_TIMEOUT': '10', # Connection timeout (default: 120s) - 'GIT_HTTP_LOW_SPEED_TIME': '5', # Slow speed threshold window - 'GIT_HTTP_LOW_SPEED_LIMIT': '1000', # < 1 KB/s = slow → abort + "GIT_HTTP_TIMEOUT": "10", # Connection timeout (default: 120s) + "GIT_HTTP_LOW_SPEED_TIME": "5", # Slow speed threshold window + "GIT_HTTP_LOW_SPEED_LIMIT": "1000", # < 1 KB/s = slow → abort } ``` diff --git a/docs/gns3-copilot/roadmap/rbac-user-isolation-roadmap.md b/docs/gns3-copilot/roadmap/rbac-user-isolation-roadmap.md index 447279d07..0712c90fa 100644 --- a/docs/gns3-copilot/roadmap/rbac-user-isolation-roadmap.md +++ b/docs/gns3-copilot/roadmap/rbac-user-isolation-roadmap.md @@ -556,20 +556,20 @@ Server: **API Endpoints:** ```python # Configuration (admin only) -POST /v3/access/config/email -GET /v3/access/config/email -PUT /v3/access/config/email +POST / v3 / access / config / email +GET / v3 / access / config / email +PUT / v3 / access / config / email # Test email (admin only) -POST /v3/access/config/email/test +POST / v3 / access / config / email / test # Password reset (public) -POST /v3/access/users/password/reset/request -POST /v3/access/users/password/reset/confirm +POST / v3 / access / users / password / reset / request +POST / v3 / access / users / password / reset / confirm # Email verification (public) -POST /v3/access/users/verify/email -POST /v3/access/users/verify/confirm +POST / v3 / access / users / verify / email +POST / v3 / access / users / verify / confirm ``` ### Database Schema diff --git a/docs/gns3-copilot/roadmap/template-based-configuration-roadmap.md b/docs/gns3-copilot/roadmap/template-based-configuration-roadmap.md index 88cb3f542..637da5d79 100644 --- a/docs/gns3-copilot/roadmap/template-based-configuration-roadmap.md +++ b/docs/gns3-copilot/roadmap/template-based-configuration-roadmap.md @@ -389,18 +389,13 @@ router ospf {{ process_id }} @interrupt def template_review_checkpoint(state): """Pause and wait for user confirmation.""" - return { - "type": "template_review", - "data": state["generated_template"] - } + return {"type": "template_review", "data": state["generated_template"]} + @interrupt def params_review_checkpoint(state): """Pause and wait for user confirmation.""" - return { - "type": "params_review", - "data": state["generated_params"] - } + return {"type": "params_review", "data": state["generated_params"]} ``` **State Management:** @@ -939,24 +934,9 @@ For 1000+ devices, some failures are inevitable. The system provides: ```python { "total_devices": 1000, - "summary": { - "success": 987, - "failed": 13, - "skipped": 0 - }, - "failed_devices": [ - { - "device_name": "R456", - "error": "Connection timeout", - "retry_available": true - }, - ... - ], - "retry_suggestions": { - "auto_retry": True, - "retry_batch_size": 10, - "exponential_backoff": True - } + "summary": {"success": 987, "failed": 13, "skipped": 0}, + "failed_devices": [{"device_name": "R456", "error": "Connection timeout", "retry_available": true}, ...], + "retry_suggestions": {"auto_retry": True, "retry_batch_size": 10, "exponential_backoff": True}, } ``` @@ -1277,54 +1257,38 @@ All servers connect to access switches in pairs. "node_type": "cisco_iosv", "count": 4, "name_pattern": "Spine-R{{ id }}", - "properties": { - "ram": 4096, - "cpus": 2, - "adapters": 8 - }, - "position": { - "strategy": "hierarchical", - "y": 100, - "x_spacing": 600 - } + "properties": {"ram": 4096, "cpus": 2, "adapters": 8}, + "position": {"strategy": "hierarchical", "y": 100, "x_spacing": 600}, }, { "name": "leaf", "node_type": "cisco_iosv_l2", "count": 20, "name_pattern": "Leaf-SW{{ id }}", - "properties": { - "ram": 2048, - "cpus": 1, - "adapters": 16 - }, + "properties": {"ram": 2048, "cpus": 1, "adapters": 16}, "position": { "strategy": "grid", "grid_rows": 4, "grid_cols": 5, "y": 400, "x_spacing": 300, - "y_spacing": 200 - } + "y_spacing": 200, + }, }, { "name": "access", "node_type": "cisco_iosv_l2", "count": 200, "name_pattern": "Acc-SW{{ id }}", - "properties": { - "ram": 1024, - "cpus": 1, - "adapters": 4 - }, + "properties": {"ram": 1024, "cpus": 1, "adapters": 4}, "position": { "strategy": "grid", "grid_rows": 10, "grid_cols": 20, "y": 800, "x_spacing": 120, - "y_spacing": 100 - } + "y_spacing": 100, + }, }, { "name": "server", @@ -1338,36 +1302,19 @@ All servers connect to access switches in pairs. "grid_cols": 25, "y": 1200, "x_spacing": 60, - "y_spacing": 60 - } - } + "y_spacing": 60, + }, + }, ], "auto_link": { "links": [ - { - "from_group": "spine", - "to_group": "leaf", - "strategy": "mesh" - }, - { - "from_group": "leaf", - "to_group": "access", - "strategy": "paired", - "count": 10 - }, - { - "from_group": "access", - "to_group": "server", - "strategy": "paired", - "count": 2 - } + {"from_group": "spine", "to_group": "leaf", "strategy": "mesh"}, + {"from_group": "leaf", "to_group": "access", "strategy": "paired", "count": 10}, + {"from_group": "access", "to_group": "server", "strategy": "paired", "count": 2}, ] }, "layout": "auto_spine_leaf", - "resource_limits": { - "max_ram_mb": 750000, - "max_vcpus": 724 - } + "resource_limits": {"max_ram_mb": 750000, "max_vcpus": 724}, } # Execution Result @@ -1381,8 +1328,8 @@ All servers connect to access switches in pairs. {"name": "spine", "created": 4, "failed": 0}, {"name": "leaf", "created": 20, "failed": 0}, {"name": "access", "created": 200, "failed": 0}, - {"name": "server", "created": 500, "failed": 0} - ] + {"name": "server", "created": 500, "failed": 0}, + ], } ``` @@ -1522,14 +1469,14 @@ class LinkPattern(BaseModel): """Pattern for creating links between node groups.""" from_nodes: NodeSelector # Source nodes - to_nodes: NodeSelector # Destination nodes + to_nodes: NodeSelector # Destination nodes strategy: Literal[ - "one_to_one", # 1:1 pairing - "one_to_many", # Star topology - "many_to_many", # Full mesh - "sequential", # Linear chain - "ring" # Ring topology + "one_to_one", # 1:1 pairing + "one_to_many", # Star topology + "many_to_many", # Full mesh + "sequential", # Linear chain + "ring", # Ring topology ] port_allocation: PortAllocationStrategy @@ -1548,16 +1495,16 @@ class PortAllocationStrategy(BaseModel): """How to allocate ports for links.""" strategy: Literal[ - "round_robin", # Distribute evenly - "sequential", # Use in order - "optimized", # Smart allocation - "auto" # Automatic selection + "round_robin", # Distribute evenly + "sequential", # Use in order + "optimized", # Smart allocation + "auto", # Automatic selection ] on_conflict: Literal[ - "skip", # Skip if port unavailable - "use_next", # Use next available port - "fail" # Fail on conflict + "skip", # Skip if port unavailable + "use_next", # Use next available port + "fail", # Fail on conflict ] = "use_next" ``` diff --git a/docs/gns3-copilot/roadmap/user-node-limit-roadmap.md b/docs/gns3-copilot/roadmap/user-node-limit-roadmap.md index 1d59ad006..324e7b07f 100644 --- a/docs/gns3-copilot/roadmap/user-node-limit-roadmap.md +++ b/docs/gns3-copilot/roadmap/user-node-limit-roadmap.md @@ -35,7 +35,7 @@ Add `max_nodes` field to both `User` and `UserGroup` models: # In User model max_nodes = Column(Integer, nullable=True) # NULL = no limit -# In UserGroup model +# In UserGroup model max_nodes = Column(Integer, nullable=True) # NULL = no limit ``` @@ -49,9 +49,7 @@ Add node limit configuration to `ControllerSettings`: class NodeLimitSettings(BaseModel): enabled: bool = False # Feature toggle (default disabled) default_max_nodes: int = 5 # Default limit when enabled - excluded_node_types: List[str] = Field(default_factory=lambda: [ - "ethernet_switch", "ethernet_hub", "cloud", "nat" - ]) + excluded_node_types: List[str] = Field(default_factory=lambda: ["ethernet_switch", "ethernet_hub", "cloud", "nat"]) ``` #### 3. Core Service Implementation @@ -83,15 +81,13 @@ Add limit checking to node startup endpoint: async def start_node( node: Node = Depends(dep_node), current_user: User = Depends(get_current_active_user), - node_limit_service: NodeLimitService = Depends(get_node_limit_service) + node_limit_service: NodeLimitService = Depends(get_node_limit_service), ): # Node limit check - can_start, error_msg = await node_limit_service.check_user_node_limit( - current_user, node.project - ) + can_start, error_msg = await node_limit_service.check_user_node_limit(current_user, node.project) if not can_start: raise HTTPException(status_code=403, detail=error_msg) - + # Original startup logic await node.start() ``` diff --git a/gns3server/agent/__init__.py b/gns3server/agent/__init__.py index d914a912c..dee2aaed1 100644 --- a/gns3server/agent/__init__.py +++ b/gns3server/agent/__init__.py @@ -36,6 +36,7 @@ AI_COPILOT_AVAILABLE = False try: from .gns3_copilot.project_agent_manager import get_project_agent_manager from .gns3_copilot.project_agent_manager import ProjectAgentManager + AI_COPILOT_AVAILABLE = True # Start skills repository initialization in background. @@ -64,8 +65,7 @@ except ImportError as e: RuntimeError: If AI Copilot dependencies are not installed """ raise RuntimeError( - "AI Copilot is not available. " - "Install AI dependencies with: pip install gns3-server[ai-features]" + "AI Copilot is not available. Install AI dependencies with: pip install gns3-server[ai-features]" ) class ProjectAgentManager: @@ -75,8 +75,7 @@ except ImportError as e: def __init__(self): raise RuntimeError( - "AI Copilot is not available. " - "Install AI dependencies with: pip install gns3-server[ai-features]" + "AI Copilot is not available. Install AI dependencies with: pip install gns3-server[ai-features]" ) @@ -88,6 +87,7 @@ try: # Use importlib so the top-level SDK name "mcp" is not bound in this # namespace — it would shadow the gns3server.agent.mcp subpackage. import importlib + importlib.import_module("mcp.server.fastmcp") MCP_AVAILABLE = True except ImportError: diff --git a/gns3server/agent/gns3_copilot/agent/context_manager.py b/gns3server/agent/gns3_copilot/agent/context_manager.py index e9427c4bc..79c526dbf 100644 --- a/gns3server/agent/gns3_copilot/agent/context_manager.py +++ b/gns3server/agent/gns3_copilot/agent/context_manager.py @@ -67,9 +67,12 @@ logger = logging.getLogger(__name__) # Initialize tiktoken encoding (required dependency) import time + logger.debug("Initializing tiktoken encoding (cl100k_base)...") logger.debug(f"Cache directory: {_cache_dir}") -logger.debug("This may take a moment on first run (downloading ~1.6MB encoding file from openaipublic.blob.core.windows.net)") +logger.debug( + "This may take a moment on first run (downloading ~1.6MB encoding file from openaipublic.blob.core.windows.net)" +) start_time = time.time() _tiktoken_encoding = tiktoken.get_encoding("cl100k_base") elapsed = time.time() - start_time @@ -149,21 +152,16 @@ def estimate_tool_tokens(tools: list[Any]) -> int: if hasattr(tool, "args_schema") and tool.args_schema: try: # Try Pydantic v2 method (model_json_schema) - tool_schema["function"]["parameters"] = ( - tool.args_schema.model_json_schema() - ) + tool_schema["function"]["parameters"] = tool.args_schema.model_json_schema() except AttributeError: # Fallback to Pydantic v1 method (schema) try: - tool_schema["function"]["parameters"] = ( - tool.args_schema.schema() - ) + tool_schema["function"]["parameters"] = tool.args_schema.schema() except Exception: # Both methods failed, use empty schema tool_name = getattr(tool, "name", "unknown") logger.debug( - "Failed to get schema for tool %s, using empty " - "parameters", + "Failed to get schema for tool %s, using empty parameters", tool_name, ) tool_schema["function"]["parameters"] = {} @@ -171,15 +169,12 @@ def estimate_tool_tokens(tools: list[Any]) -> int: # model_json_schema() raised an exception tool_name = getattr(tool, "name", "unknown") logger.debug( - "model_json_schema() failed for tool %s: %s, " - "trying v1 fallback", + "model_json_schema() failed for tool %s: %s, trying v1 fallback", tool_name, e, ) try: - tool_schema["function"]["parameters"] = ( - tool.args_schema.schema() - ) + tool_schema["function"]["parameters"] = tool.args_schema.schema() except Exception: tool_schema["function"]["parameters"] = {} @@ -331,14 +326,11 @@ def create_pre_model_hook( if not llm_config: logger.error("LLM config not found. context_limit is required.") - raise ValueError( - "LLM config not found. context_limit is required." - ) + raise ValueError("LLM config not found. context_limit is required.") if "context_limit" not in llm_config: logger.error( - "context_limit not found in LLM config. " - "This is a required field. Please configure context_limit." + "context_limit not found in LLM config. This is a required field. Please configure context_limit." ) raise ValueError("context_limit is required in LLM config") @@ -385,9 +377,7 @@ def create_pre_model_hook( system_tokens = _count_tokens_for_message(system_message) # Calculate tokens for messages_with_system (including system) - messages_with_system_tokens = sum( - _count_tokens_for_message(m) for m in messages_with_system - ) + messages_with_system_tokens = sum(_count_tokens_for_message(m) for m in messages_with_system) # Calculate available budget model_limit_tokens = context_limit_k * TOKENS_PER_K @@ -449,14 +439,11 @@ def create_pre_model_hook( # Calculate final token counts final_total = sum(_count_tokens_for_message(m) for m in trimmed) - usage_percent = ( - (final_total + tool_tokens) / model_limit_tokens * 100 - ) + usage_percent = (final_total + tool_tokens) / model_limit_tokens * 100 if len(trimmed) < len(messages_with_system): logger.info( - "Messages trimmed: %d → %d msgs. Total: ~%d tokens + %d " - "tools = %d / %dK (%.1f%%), strategy=%s", + "Messages trimmed: %d → %d msgs. Total: ~%d tokens + %d tools = %d / %dK (%.1f%%), strategy=%s", len(messages_with_system), len(trimmed), final_total, @@ -468,8 +455,7 @@ def create_pre_model_hook( ) else: logger.info( - "Context ready: %d msgs, ~%d tokens + %d tools = %d / %dK " - "(%.1f%%), strategy=%s", + "Context ready: %d msgs, ~%d tokens + %d tools = %d / %dK (%.1f%%), strategy=%s", len(trimmed), final_total, tool_tokens, @@ -520,33 +506,23 @@ def _inject_topology_into_system( if topology_data: topology_str = str(topology_data) - formatted_prompt = system_prompt.replace( - "{{topology_info}}", f"\n\n## Current Topology\n{topology_str}" - ) + formatted_prompt = system_prompt.replace("{{topology_info}}", f"\n\n## Current Topology\n{topology_str}") logger.info( "✓ Topology injected: %d chars, nodes: %s", len(topology_str), list(topology_data.get("nodes", {}).keys())[:5], ) # Show first 5 node names - logger.debug( - "Full topology data: %s", topology_str[:500] - ) # First 500 chars + logger.debug("Full topology data: %s", topology_str[:500]) # First 500 chars else: - formatted_prompt = system_prompt.replace( - "{{topology_info}}", "(No topology information available)" - ) + formatted_prompt = system_prompt.replace("{{topology_info}}", "(No topology information available)") logger.warning("✗ Topology data is None, injecting placeholder") # Filter out existing SystemMessage instances - non_system_messages = [ - m for m in messages if not isinstance(m, SystemMessage) - ] + non_system_messages = [m for m in messages if not isinstance(m, SystemMessage)] filtered_count = len(messages) - len(non_system_messages) if filtered_count > 0: - logger.debug( - "Filtered out %d existing SystemMessage(s)", filtered_count - ) + logger.debug("Filtered out %d existing SystemMessage(s)", filtered_count) return [SystemMessage(content=formatted_prompt)] + non_system_messages @@ -568,8 +544,7 @@ def prepare_context_messages( **DEPRECATED**: Use create_pre_model_hook() instead. """ warnings.warn( - "prepare_context_messages() is deprecated. Use " - "create_pre_model_hook() instead.", + "prepare_context_messages() is deprecated. Use create_pre_model_hook() instead.", DeprecationWarning, stacklevel=2, ) @@ -577,13 +552,9 @@ def prepare_context_messages( if "{{topology_info}}" not in system_prompt: formatted_prompt = system_prompt elif topology_context: - formatted_prompt = system_prompt.replace( - "{{topology_info}}", f"\n\n## Current Topology\n{topology_context}" - ) + formatted_prompt = system_prompt.replace("{{topology_info}}", f"\n\n## Current Topology\n{topology_context}") else: - formatted_prompt = system_prompt.replace( - "{{topology_info}}", "(No topology information available)" - ) + formatted_prompt = system_prompt.replace("{{topology_info}}", "(No topology information available)") return [SystemMessage(content=formatted_prompt)] + state_messages @@ -629,9 +600,7 @@ if __name__ == "__main__": # Test invocation print("\nTest 3: Invoke pre_model_hook") test_state = { - "messages": [ - HumanMessage(f"Message {i}: {'x' * 50}") for i in range(5) - ], + "messages": [HumanMessage(f"Message {i}: {'x' * 50}") for i in range(5)], "topology_info": {"project_id": "test123", "nodes": 3}, } diff --git a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py index 7478301f8..a9616eb8b 100644 --- a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py +++ b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py @@ -311,8 +311,7 @@ def llm_call(state: dict, config: RunnableConfig | None = None): if topology and "error" not in topology: topology_info = topology logger.info( - "Successfully retrieved topology for project_id: %s, " - "name: %s", + "Successfully retrieved topology for project_id: %s, name: %s", project_id, topology.get("name"), ) @@ -320,9 +319,7 @@ def llm_call(state: dict, config: RunnableConfig | None = None): logger.warning( "Failed to retrieve topology for project_id %s: %s", project_id, - topology.get("error", "Unknown error") - if topology - else "No result", + topology.get("error", "Unknown error") if topology else "No result", ) except Exception as e: logger.warning( @@ -338,20 +335,13 @@ def llm_call(state: dict, config: RunnableConfig | None = None): copilot_mode = llm_config.get("copilot_mode", "teaching_assistant").lower() if copilot_mode == "lab_automation_assistant": mode_tools = LAB_AUTOMATION_ASSISTANT_MODE_TOOLS - logger.info( - "Using LAB_AUTOMATION_ASSISTANT mode tools (includes " - "configuration tools)" - ) + logger.info("Using LAB_AUTOMATION_ASSISTANT mode tools (includes configuration tools)") elif copilot_mode == "troubleshooting_injection": mode_tools = TROUBLESHOOTING_INJECTION_MODE_TOOLS - logger.info( - "Using TROUBLESHOOTING_INJECTION mode tools (fault injection)" - ) + logger.info("Using TROUBLESHOOTING_INJECTION mode tools (fault injection)") else: # teaching_assistant mode (default) mode_tools = TEACHING_ASSISTANT_MODE_TOOLS - logger.info( - "Using TEACHING_ASSISTANT mode tools (diagnostic tools only)" - ) + logger.info("Using TEACHING_ASSISTANT mode tools (diagnostic tools only)") # Create pre_model_hook for automatic topology injection and trimming # Load system prompt based on copilot_mode configuration @@ -372,22 +362,16 @@ def llm_call(state: dict, config: RunnableConfig | None = None): copilot_mode, len(mode_tools), ) - model_with_tools = create_base_model_with_tools( - mode_tools, llm_config=llm_config - ) + model_with_tools = create_base_model_with_tools(mode_tools, llm_config=llm_config) # Call pre_hook directly to prepare messages (topology injection + # trimming) # Note: LangGraph's pre_model_hook only works with prebuilt agents, not # custom StateGraph logger.debug("Calling pre_hook to prepare %d messages", len(messages)) - prepared_state = pre_hook( - {"messages": messages, "topology_info": topology_info} - ) + prepared_state = pre_hook({"messages": messages, "topology_info": topology_info}) prepared_messages = prepared_state["messages"] - logger.info( - "Messages prepared: %d → %d", len(messages), len(prepared_messages) - ) + logger.info("Messages prepared: %d → %d", len(messages), len(prepared_messages)) # Invoke model with prepared messages response = model_with_tools.invoke(prepared_messages) @@ -421,9 +405,7 @@ def llm_call(state: dict, config: RunnableConfig | None = None): # Define generate title node -def generate_title( - state: MessagesState, config: RunnableConfig | None = None -) -> dict: +def generate_title(state: MessagesState, config: RunnableConfig | None = None) -> dict: """ Generate a conversation title using a lightweight assistant LLM (title_model). This node is only executed when no title has been set yet @@ -469,27 +451,21 @@ def generate_title( # Validate the generated title if not new_title or len(new_title) < 3: - raise ValueError( - f"Generated title too short or empty: '{new_title}'" - ) + raise ValueError(f"Generated title too short or empty: '{new_title}'") if new_title in [ "New Conversation", "Untitled Session", "GNS3 Session", ]: - raise ValueError( - f"Generated title is a default value: '{new_title}'" - ) + raise ValueError(f"Generated title is a default value: '{new_title}'") # Safety: truncate long titles and avoid line breaks if len(new_title) > TITLE_MAX_LENGTH: new_title = new_title[: TITLE_MAX_LENGTH - 2] + "..." # Remove unwanted characters - new_title = ( - new_title.replace("\n", " ").replace('"', "").replace("'", "") - ) + new_title = new_title.replace("\n", " ").replace('"', "").replace("'", "") logger.debug("Generated new title: %s", new_title) return {"conversation_title": new_title, "session_id": state.get("session_id")} @@ -540,9 +516,7 @@ def tool_node(state: dict, config: RunnableConfig | None = None): result = [] for tool_call in tool_calls: tool_name = tool_call["name"] - logger.debug( - "Executing tool: %s with args: %s", tool_name, tool_call["args"] - ) + logger.debug("Executing tool: %s with args: %s", tool_name, tool_call["args"]) tool = tools_by_name[tool_name] try: observation = tool.invoke(tool_call["args"]) @@ -590,11 +564,13 @@ def abort_handler_node(state: dict) -> dict: result = [] for tool_call in last_message.tool_calls: tool_msg = ToolMessage( - content=json.dumps({ - "status": "aborted", - "message": "Tool execution was aborted by user", - "tool_call_id": tool_call["id"], - }), + content=json.dumps( + { + "status": "aborted", + "message": "Tool execution was aborted by user", + "tool_call_id": tool_call["id"], + } + ), tool_call_id=tool_call["id"], name=tool_call["name"], metadata={"created_at": datetime.utcnow().isoformat(), "aborted": True}, diff --git a/gns3server/agent/gns3_copilot/agent/model_factory.py b/gns3server/agent/gns3_copilot/agent/model_factory.py index 86a087654..cfdc26854 100644 --- a/gns3server/agent/gns3_copilot/agent/model_factory.py +++ b/gns3server/agent/gns3_copilot/agent/model_factory.py @@ -94,8 +94,7 @@ def create_base_model( # Log the loaded configuration (mask sensitive data) logger.info( - "Creating base model: name=%s, provider=%s, base_url=%s, " - "temperature=%s", + "Creating base model: name=%s, provider=%s, base_url=%s, temperature=%s", config_vars["model_name"], config_vars["model_provider"], config_vars["base_url"] if config_vars["base_url"] else "default", @@ -163,8 +162,7 @@ def create_title_model( config_vars = _load_llm_config(llm_config) logger.info( - "Creating title model: name=%s, provider=%s, base_url=%s, " - "temperature=1.0", + "Creating title model: name=%s, provider=%s, base_url=%s, temperature=1.0", config_vars["model_name"], config_vars["model_provider"], config_vars["base_url"] if config_vars["base_url"] else "default", diff --git a/gns3server/agent/gns3_copilot/agent_service.py b/gns3server/agent/gns3_copilot/agent_service.py index 627ea2f69..bf916b043 100644 --- a/gns3server/agent/gns3_copilot/agent_service.py +++ b/gns3server/agent/gns3_copilot/agent_service.py @@ -123,9 +123,7 @@ class AgentService: return self._checkpointer checkpoint_dir = self._get_checkpoint_dir() - checkpointer_path = os.path.join( - checkpoint_dir, "copilot_checkpoints.db" - ) + checkpointer_path = os.path.join(checkpoint_dir, "copilot_checkpoints.db") log.debug("Creating checkpointer at: %s", checkpointer_path) @@ -135,17 +133,13 @@ class AgentService: await self._checkpointer_conn.close() log.debug("Closed previous checkpointer connection") except Exception as e: - log.warning( - "Error closing old checkpointer connection: %s", e - ) + log.warning("Error closing old checkpointer connection: %s", e) # Create new connection conn = await aiosqlite.connect(checkpointer_path) # Enable WAL mode for better concurrent performance await conn.execute("PRAGMA journal_mode=WAL;") - self._checkpointer_conn = ( - conn # Save connection reference to prevent GC - ) + self._checkpointer_conn = conn # Save connection reference to prevent GC self._checkpointer = AsyncSqliteSaver(conn) # CRITICAL: Initialize database schema @@ -197,14 +191,8 @@ class AgentService: """) # Create indexes - await conn.execute( - "CREATE INDEX IF NOT EXISTS idx_thread_id ON " - "chat_sessions(thread_id)" - ) - await conn.execute( - "CREATE INDEX IF NOT EXISTS idx_user_project ON " - "chat_sessions(user_id, project_id)" - ) + await conn.execute("CREATE INDEX IF NOT EXISTS idx_thread_id ON chat_sessions(thread_id)") + await conn.execute("CREATE INDEX IF NOT EXISTS idx_user_project ON chat_sessions(user_id, project_id)") # Check if pinned column exists, add it if not (migration for existing # databases) @@ -214,16 +202,12 @@ class AgentService: if "pinned" not in column_names: log.debug("Adding pinned column to existing chat_sessions table") - await conn.execute( - "ALTER TABLE chat_sessions ADD COLUMN pinned BOOLEAN DEFAULT " - "FALSE" - ) + await conn.execute("ALTER TABLE chat_sessions ADD COLUMN pinned BOOLEAN DEFAULT FALSE") await conn.commit() # Create pinned index (after column is guaranteed to exist) await conn.execute( - "CREATE INDEX IF NOT EXISTS idx_pinned_updated ON " - "chat_sessions(pinned DESC, updated_at DESC)" + "CREATE INDEX IF NOT EXISTS idx_pinned_updated ON chat_sessions(pinned DESC, updated_at DESC)" ) await conn.commit() @@ -234,9 +218,7 @@ class AgentService: if self._graph is None: checkpointer = await self._get_checkpointer() self._graph = agent_builder.compile(checkpointer=checkpointer) - log.info( - "LangGraph agent compiled for project: %s", self.project_path - ) + log.info("LangGraph agent compiled for project: %s", self.project_path) return self._graph async def stream_chat( @@ -266,8 +248,7 @@ class AgentService: Dict containing SSE-compatible response chunks """ log.info( - "Stream chat started: project_id=%s, user_id=%s, session_id=%s, " - "mode=%s", + "Stream chat started: project_id=%s, user_id=%s, session_id=%s, mode=%s", project_id, user_id, session_id, @@ -293,11 +274,7 @@ class AgentService: title="New Conversation", copilot_mode=copilot_mode, ) - log.debug( - "Created new chat session: thread_id=%s, copilot_mode=%s", - session_id, - copilot_mode - ) + log.debug("Created new chat session: thread_id=%s, copilot_mode=%s", session_id, copilot_mode) # Set request-scoped context variables (memory-only, not persisted) if jwt_token: @@ -387,39 +364,25 @@ class AgentService: # Stream events try: - async for event in graph.astream_events( - inputs, config=config, version="v2" - ): + async for event in graph.astream_events(inputs, config=config, version="v2"): event_type = event.get("event", "") data = event.get("data", {}) # Track LLM calls and tokens if event_type == "on_chat_model_start": # Filter out title_generator_node from statistics - langgraph_node = event.get("metadata", {}).get( - "langgraph_node", "" - ) + langgraph_node = event.get("metadata", {}).get("langgraph_node", "") if langgraph_node != "title_generator_node": llm_calls_count += 1 - log.debug( - "LLM call started, count=%d", llm_calls_count - ) + log.debug("LLM call started, count=%d", llm_calls_count) else: - log.debug( - "Skipping LLM call count for internal node: " - "title_generator_node" - ) + log.debug("Skipping LLM call count for internal node: title_generator_node") elif event_type == "on_chat_model_end": # Filter out title_generator_node from token counting - langgraph_node = event.get("metadata", {}).get( - "langgraph_node", "" - ) + langgraph_node = event.get("metadata", {}).get("langgraph_node", "") if langgraph_node == "title_generator_node": - log.debug( - "Skipping token counting for internal node: " - "title_generator_node" - ) + log.debug("Skipping token counting for internal node: title_generator_node") else: # Extract token usage from response metadata # Try multiple possible locations where token usage @@ -441,12 +404,8 @@ class AgentService: if hasattr(output_msg, "usage_metadata"): usage = output_msg.usage_metadata if usage: - input_tokens += usage.get( - "input_tokens", 0 - ) - output_tokens += usage.get( - "output_tokens", 0 - ) + input_tokens += usage.get("input_tokens", 0) + output_tokens += usage.get("output_tokens", 0) token_info_found = True # Method 3: Check data directly for token usage fields @@ -455,10 +414,7 @@ class AgentService: input_tokens += data.get("input_tokens", 0) if "output_tokens" in data: output_tokens += data.get("output_tokens", 0) - if ( - "input_tokens" in data - or "output_tokens" in data - ): + if "input_tokens" in data or "output_tokens" in data: token_info_found = True # Count AI response as one message (only once per turn) @@ -470,9 +426,7 @@ class AgentService: elif event_type == "on_tool_end": message_count += 1 # Tool result message tool_messages_counted += 1 - log.debug( - "Tool message counted, message_count=%d", message_count - ) + log.debug("Tool message counted, message_count=%d", message_count) # Convert event to chunk for SSE streaming # Use accumulator for on_chat_model_stream events to handle @@ -480,16 +434,11 @@ class AgentService: # Filter out internal nodes (title_generator_node) from # streaming to frontend - langgraph_node = event.get("metadata", {}).get( - "langgraph_node", "" - ) + langgraph_node = event.get("metadata", {}).get("langgraph_node", "") if langgraph_node == "title_generator_node": # Skip all events from the title_generator_node (internal # use only) - log.debug( - "Skipping event from internal node: " - "title_generator_node" - ) + log.debug("Skipping event from internal node: title_generator_node") continue if event_type == "on_chat_model_stream": @@ -540,8 +489,7 @@ class AgentService: last_message_at=last_message_at, ) log.info( - "Session statistics updated: thread_id=%s, messages=%d, " - "llm_calls=%d, tokens=%d+%d=%d", + "Session statistics updated: thread_id=%s, messages=%d, llm_calls=%d, tokens=%d+%d=%d", session_id, message_count, llm_calls_count, @@ -555,13 +503,8 @@ class AgentService: if final_state and "conversation_title" in final_state.values: generated_title = final_state.values["conversation_title"] current_session = await repo.get_session_by_thread(session_id) - if ( - current_session - and current_session.title != generated_title - ): - await repo.update_session( - thread_id=session_id, title=generated_title - ) + if current_session and current_session.title != generated_title: + await repo.update_session(thread_id=session_id, title=generated_title) log.info( "Auto-generated title synced: thread_id=%s, title=%s", session_id, @@ -576,9 +519,7 @@ class AgentService: "session_id": session_id, } - def _convert_event_to_chunk( - self, event: Dict[str, Any], session_id: str - ) -> Optional[Dict[str, Any]]: + def _convert_event_to_chunk(self, event: Dict[str, Any], session_id: str) -> Optional[Dict[str, Any]]: """ Convert LangGraph event to API response chunk. @@ -627,9 +568,7 @@ class AgentService: return None - async def get_history( - self, session_id: str, limit: int = 100 - ) -> Dict[str, Any]: + async def get_history(self, session_id: str, limit: int = 100) -> Dict[str, Any]: """ Get conversation history for a session. @@ -651,9 +590,7 @@ class AgentService: for msg in state.values["messages"][-limit:]: messages.append(self._convert_message_to_dict(msg)) - title = state.values.get( - "conversation_title", "New Conversation" - ) + title = state.values.get("conversation_title", "New Conversation") return { "thread_id": session_id, @@ -710,9 +647,7 @@ class AgentService: repo = ChatSessionsRepository(self._checkpointer_conn) return await repo.delete_session(session_id) - async def rename_session( - self, session_id: str, new_title: str - ) -> Optional[Dict[str, Any]]: + async def rename_session(self, session_id: str, new_title: str) -> Optional[Dict[str, Any]]: """ Rename a chat session. @@ -727,14 +662,10 @@ class AgentService: await self._get_checkpointer() repo = ChatSessionsRepository(self._checkpointer_conn) - session = await repo.update_session( - thread_id=session_id, title=new_title - ) + session = await repo.update_session(thread_id=session_id, title=new_title) return session.to_dict() if session else None - async def pin_session( - self, session_id: str, pinned: bool = True - ) -> Optional[Dict[str, Any]]: + async def pin_session(self, session_id: str, pinned: bool = True) -> Optional[Dict[str, Any]]: """ Pin or unpin a chat session. @@ -760,10 +691,7 @@ class AgentService: if self._checkpointer_conn: try: # Add timeout to prevent blocking on database close - await asyncio.wait_for( - self._checkpointer_conn.close(), - timeout=5.0 - ) + await asyncio.wait_for(self._checkpointer_conn.close(), timeout=5.0) log.debug( "Checkpointer connection closed for: %s", self.project_path, diff --git a/gns3server/agent/gns3_copilot/chat_sessions_repository.py b/gns3server/agent/gns3_copilot/chat_sessions_repository.py index 691971052..4d18d8852 100644 --- a/gns3server/agent/gns3_copilot/chat_sessions_repository.py +++ b/gns3server/agent/gns3_copilot/chat_sessions_repository.py @@ -156,16 +156,11 @@ class ChatSessionsRepository: await self.conn.commit() session_id = cursor.lastrowid - log.info( - "Created chat session: id=%s, thread_id=%s, copilot_mode=%s", - session_id, thread_id, copilot_mode - ) + log.info("Created chat session: id=%s, thread_id=%s, copilot_mode=%s", session_id, thread_id, copilot_mode) return await self.get_session_by_id(session_id) - async def get_session_by_id( - self, session_id: int - ) -> Optional[ChatSession]: + async def get_session_by_id(self, session_id: int) -> Optional[ChatSession]: """ Get a session by its database ID. @@ -175,18 +170,14 @@ class ChatSessionsRepository: Returns: ChatSession or None """ - cursor = await self.conn.execute( - "SELECT * FROM chat_sessions WHERE id = ?", (session_id,) - ) + cursor = await self.conn.execute("SELECT * FROM chat_sessions WHERE id = ?", (session_id,)) row = await cursor.fetchone() if row: return self._row_to_session(row) return None - async def get_session_by_thread( - self, thread_id: str - ) -> Optional[ChatSession]: + async def get_session_by_thread(self, thread_id: str) -> Optional[ChatSession]: """ Get a session by thread_id. @@ -196,9 +187,7 @@ class ChatSessionsRepository: Returns: ChatSession or None """ - cursor = await self.conn.execute( - "SELECT * FROM chat_sessions WHERE thread_id = ?", (thread_id,) - ) + cursor = await self.conn.execute("SELECT * FROM chat_sessions WHERE thread_id = ?", (thread_id,)) row = await cursor.fetchone() if row: @@ -318,10 +307,7 @@ class ChatSessionsRepository: params.append(now) params.append(thread_id) - query = ( - f"UPDATE chat_sessions SET {', '.join(updates)} WHERE thread_id " - f"= ?" - ) + query = f"UPDATE chat_sessions SET {', '.join(updates)} WHERE thread_id = ?" await self.conn.execute(query, params) await self.conn.commit() @@ -340,21 +326,15 @@ class ChatSessionsRepository: True if deleted, False if not found """ # First, delete the checkpoint data - await self.conn.execute( - "DELETE FROM checkpoints WHERE thread_id = ?", (thread_id,) - ) + await self.conn.execute("DELETE FROM checkpoints WHERE thread_id = ?", (thread_id,)) # Then delete the session - cursor = await self.conn.execute( - "DELETE FROM chat_sessions WHERE thread_id = ?", (thread_id,) - ) + cursor = await self.conn.execute("DELETE FROM chat_sessions WHERE thread_id = ?", (thread_id,)) await self.conn.commit() deleted = cursor.rowcount > 0 if deleted: - log.info( - "Deleted chat session and checkpoints: thread_id=%s", thread_id - ) + log.info("Deleted chat session and checkpoints: thread_id=%s", thread_id) return deleted @@ -378,13 +358,9 @@ class ChatSessionsRepository: # Delete checkpoints and sessions for thread_id in thread_ids: - await self.conn.execute( - "DELETE FROM checkpoints WHERE thread_id = ?", (thread_id,) - ) + await self.conn.execute("DELETE FROM checkpoints WHERE thread_id = ?", (thread_id,)) - cursor = await self.conn.execute( - "DELETE FROM chat_sessions WHERE project_id = ?", (project_id,) - ) + cursor = await self.conn.execute("DELETE FROM chat_sessions WHERE project_id = ?", (project_id,)) await self.conn.commit() deleted_count = cursor.rowcount @@ -397,9 +373,7 @@ class ChatSessionsRepository: return deleted_count - async def pin_session( - self, thread_id: str, pinned: bool = True - ) -> Optional[ChatSession]: + async def pin_session(self, thread_id: str, pinned: bool = True) -> Optional[ChatSession]: """ Pin or unpin a session. @@ -412,8 +386,7 @@ class ChatSessionsRepository: """ now = datetime.utcnow().isoformat() await self.conn.execute( - "UPDATE chat_sessions SET pinned = ?, updated_at = ? WHERE " - "thread_id = ?", + "UPDATE chat_sessions SET pinned = ?, updated_at = ? WHERE thread_id = ?", (1 if pinned else 0, now, thread_id), ) await self.conn.commit() diff --git a/gns3server/agent/gns3_copilot/configs/skills_config.py b/gns3server/agent/gns3_copilot/configs/skills_config.py index 3a10332d9..d08429042 100644 --- a/gns3server/agent/gns3_copilot/configs/skills_config.py +++ b/gns3server/agent/gns3_copilot/configs/skills_config.py @@ -35,13 +35,10 @@ from gns3server.config import Config SKILLS_CONFIG = { # Git repository URL for skills "repo_url": "https://github.com/gns3/gns3-skills.git", - # Git branch to use "branch": "main", - # Automatically pull updates on reload "auto_update": True, - # Enable external skills loading # If False, use the built-in hardcoded skills "enabled": True, diff --git a/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py b/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py index 831dace7b..b0934c201 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py +++ b/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py @@ -70,22 +70,53 @@ MAX_NODE_FILE_BYTES = 50 * 1024 # 50 KiB VALID_NODE_FIELDS = { # NodeBase - "compute_id", "name", "node_type", "node_id", - "console", "console_type", "console_auto_start", - "aux", "aux_type", "properties", "label", "symbol", - "x", "y", "z", "locked", - "port_name_format", "port_segment_size", "first_port_name", - "custom_adapters", "tags", + "compute_id", + "name", + "node_type", + "node_id", + "console", + "console_type", + "console_auto_start", + "aux", + "aux_type", + "properties", + "label", + "symbol", + "x", + "y", + "z", + "locked", + "port_name_format", + "port_segment_size", + "first_port_name", + "custom_adapters", + "tags", # Node - "template_id", "project_id", "node_directory", "status", - "command_line", "width", "height", "ports", "console_host", + "template_id", + "project_id", + "node_directory", + "status", + "command_line", + "width", + "height", + "ports", + "console_host", } VALID_LINK_FIELDS = { - "link_id", "project_id", "link_type", "nodes", "suspend", - "link_style", "filters", "show_filters_icon", - "capturing", "capture_file_name", "capture_file_path", - "capture_compute_id", "wireshark", + "link_id", + "project_id", + "link_type", + "nodes", + "suspend", + "link_style", + "filters", + "show_filters_icon", + "capturing", + "capture_file_name", + "capture_file_path", + "capture_compute_id", + "wireshark", } LINK_DEFAULT_FIELDS = ["link_id", "link_type", "nodes"] @@ -93,6 +124,7 @@ LINK_DEFAULT_FIELDS = ["link_id", "link_type", "nodes"] # ── Helpers ──────────────────────────────────────────────────────────────── + def _get_connector(gns3_ctx: dict[str, Any]): return Gns3Connector( url=gns3_ctx["server_url"], @@ -102,9 +134,7 @@ def _get_connector(gns3_ctx: dict[str, Any]): ) -def build_gns3_ctx( - jwt_token: str | None = None, url: str | None = None -) -> dict[str, Any] | None: +def build_gns3_ctx(jwt_token: str | None = None, url: str | None = None) -> dict[str, Any] | None: """ Build a handler ``gns3_ctx`` for in-process copilot callers. @@ -183,13 +213,14 @@ def _normalize_link_nodes(nodes) -> list[dict[str, Any]]: ] raise ValueError( f"Unrecognized link nodes format. " - f"Use standard [{{\"node_id\":\"..\",\"adapter_number\":0,\"port_number\":0}},...] " - f"or compact [\"id\",0,0,\"id\",0,0], got: {nodes}" + f'Use standard [{{"node_id":"..","adapter_number":0,"port_number":0}},...] ' + f'or compact ["id",0,0,"id",0,0], got: {nodes}' ) # ── Node handlers ────────────────────────────────────────────────────────── + def get_nodes_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: project_id = params.get("project_id") if not project_id: @@ -199,7 +230,7 @@ def get_nodes_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[ fields = params.get("fields") if fields: if not isinstance(fields, list): - return {"error": "fields must be a list of field names, e.g. [\"name\", \"status\"]"} + return {"error": 'fields must be a list of field names, e.g. ["name", "status"]'} invalid = [f for f in fields if f not in VALID_NODE_FIELDS] if invalid: return { @@ -221,7 +252,7 @@ def get_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[s fields = params.get("fields") if fields: if not isinstance(fields, list): - return {"error": "fields must be a list of field names, e.g. [\"name\", \"status\"]"} + return {"error": 'fields must be a list of field names, e.g. ["name", "status"]'} invalid = [f for f in fields if f not in VALID_NODE_FIELDS] if invalid: return { @@ -235,12 +266,14 @@ def get_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[s def _batch_lifecycle(project_id, node_ids, action, conn, action_label): """Helper to run a lifecycle action on multiple nodes in parallel.""" + def _act(nid): try: conn.http_call("post", f"{conn.base_url}/projects/{project_id}/nodes/{nid}/{action}") return {"node_id": nid, "status": "success", "message": f"Node {nid} {action_label}"} except Exception as e: return {"node_id": nid, "status": "error", "error": str(e)} + with ThreadPoolExecutor(max_workers=min(len(node_ids), BATCH_MAX_WORKERS)) as pool: return list(pool.map(_act, node_ids)) @@ -307,7 +340,7 @@ def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic fields = params.get("fields") if fields is not None and not isinstance(fields, list): - return {"error": "fields must be a list, e.g. [\"node_id\", \"name\"]"} + return {"error": 'fields must be a list, e.g. ["node_id", "name"]'} nodes = params.get("nodes") # Batch mode: nodes=[{template_id?, x, y, name?, compute_id?}] @@ -317,6 +350,7 @@ def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic return {"error": "nodes must be a non-empty array"} default_tid = params.get("template_id") conn = _get_connector(gns3_ctx) + def _create_one(node_data): tid = node_data.get("template_id", default_tid) if not tid: @@ -335,6 +369,7 @@ def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic return {"template_id": tid, "status": "success", "node": _filter_node_response(resp, fields)} except Exception as e: return {"template_id": tid, "status": "error", "error": str(e)} + if any(not node.get("name") for node in nodes): # The controller assigns default names (R-1, R-2, ...) and console # ports in request arrival order, and a parallel fan-out makes the @@ -375,12 +410,14 @@ def delete_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic if not isinstance(node_ids, list): return {"error": "node_ids must be a list"} conn = _get_connector(gns3_ctx) + def _del(nid): try: conn.http_call("delete", f"{conn.base_url}/projects/{project_id}/nodes/{nid}") return {"node_id": nid, "status": "success", "message": f"Node {nid} deleted"} except Exception as e: return {"node_id": nid, "status": "error", "error": str(e)} + with ThreadPoolExecutor(max_workers=min(len(node_ids), BATCH_MAX_WORKERS)) as pool: return list(pool.map(_del, node_ids)) node_id = params.get("node_id") @@ -423,12 +460,16 @@ def get_node_console_info_handler(params: dict[str, Any], gns3_ctx: dict[str, An # the ~200-char JWT previously embedded here (dropped header segment → # "Missing 'alg' value in header" on the server). username = gns3_ctx.get("jwt_username") - ticket = access_ticket_service.mint( - username, - token_version=gns3_ctx.get("jwt_token_version", 0), - project_id=project_id, - node_id=node_id, - ) if username else None + ticket = ( + access_ticket_service.mint( + username, + token_version=gns3_ctx.get("jwt_token_version", 0), + project_id=project_id, + node_id=node_id, + ) + if username + else None + ) raw_url = f"{gns3_ctx['server_url']}/v3/projects/{project_id}/nodes/{node_id}/console/ws" if ticket: raw_url += f"?token={ticket}" @@ -499,7 +540,7 @@ def get_node_file_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> d total_lines = len(lines) # Apply offset/limit - selected = lines[offset: offset + limit] if offset < total_lines else [] + selected = lines[offset : offset + limit] if offset < total_lines else [] has_more = (offset + limit) < total_lines or truncated content = "".join(selected) @@ -581,7 +622,9 @@ def duplicate_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> return {"error": "project_id and node_id are required"} conn = _get_connector(gns3_ctx) data = {k: v for k, v in params.items() if k not in ("project_id", "node_id") and v is not None} - result = conn.http_call("post", f"{conn.base_url}/projects/{project_id}/nodes/{node_id}/duplicate", json_data=data).json() + result = conn.http_call( + "post", f"{conn.base_url}/projects/{project_id}/nodes/{node_id}/duplicate", json_data=data + ).json() return {"message": f"Node {node_id} duplicated", "node": result} @@ -617,6 +660,7 @@ def get_node_links_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> # ── Link handlers ────────────────────────────────────────────────────────── + def get_links_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: project_id = params.get("project_id") if not project_id: @@ -626,7 +670,7 @@ def get_links_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[ fields = params.get("fields") if fields: if not isinstance(fields, list): - return {"error": "fields must be a list, e.g. [\"link_id\", \"nodes\"]"} + return {"error": 'fields must be a list, e.g. ["link_id", "nodes"]'} invalid = [f for f in fields if f not in VALID_LINK_FIELDS] if invalid: return { @@ -669,7 +713,7 @@ def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic fields = params.get("fields") if fields is not None and not isinstance(fields, list): - return {"error": "fields must be a list, e.g. [\"link_id\", \"nodes\"]"} + return {"error": 'fields must be a list, e.g. ["link_id", "nodes"]'} links = params.get("links") # Batch mode: links=[{nodes, link_type?, filters?, suspend?}] @@ -677,6 +721,7 @@ def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic if not isinstance(links, list) or not links: return {"error": "links must be a non-empty array"} conn = _get_connector(gns3_ctx) + def _create_one(link_data): raw_nodes = link_data.get("nodes") if not raw_nodes: @@ -694,6 +739,7 @@ def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic return {"status": "success", "link": _filter_link_response(resp, fields)} except Exception as e: return {"status": "error", "error": str(e)} + with ThreadPoolExecutor(max_workers=min(len(links), BATCH_MAX_WORKERS)) as pool: # pool.map keeps the submission order, so callers can correlate # results with the links they sent regardless of completion order @@ -725,12 +771,14 @@ def delete_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic if not isinstance(link_ids, list): return {"error": "link_ids must be a list"} conn = _get_connector(gns3_ctx) + def _del(lid): try: conn.http_call("delete", f"{conn.base_url}/projects/{project_id}/links/{lid}") return {"link_id": lid, "status": "success", "message": f"Link {lid} deleted"} except Exception as e: return {"link_id": lid, "status": "error", "error": str(e)} + with ThreadPoolExecutor(max_workers=min(len(link_ids), BATCH_MAX_WORKERS)) as pool: return list(pool.map(_del, link_ids)) link_id = params.get("link_id") @@ -770,6 +818,7 @@ def reset_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict if not isinstance(link_ids, list): return {"error": "link_ids must be a list"} conn = _get_connector(gns3_ctx) + def _rst(lid): try: url = f"{conn.base_url}/projects/{project_id}/links/{lid}/reset" @@ -777,6 +826,7 @@ def reset_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict return {"link_id": lid, "status": "reset", "link": r} except Exception as e: return {"link_id": lid, "status": "error", "error": str(e)} + with ThreadPoolExecutor(max_workers=min(len(link_ids), BATCH_MAX_WORKERS)) as pool: return list(pool.map(_rst, link_ids)) link_id = params.get("link_id") @@ -790,6 +840,7 @@ def reset_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict def _batch_capture(project_id, link_ids, action, data_builder, conn): """Helper for batch capture start/stop.""" + def _act(lid): try: url = f"{conn.base_url}/projects/{project_id}/links/{lid}/capture/{action}" @@ -798,6 +849,7 @@ def _batch_capture(project_id, link_ids, action, data_builder, conn): return {"link_id": lid, "status": "success"} except Exception as e: return {"link_id": lid, "status": "error", "error": str(e)} + with ThreadPoolExecutor(max_workers=min(len(link_ids), BATCH_MAX_WORKERS)) as pool: return list(pool.map(_act, link_ids)) @@ -814,11 +866,13 @@ def start_capture_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> d dlt = params.get("data_link_type", "DLT_EN10MB") ws = params.get("wireshark", False) fname = params.get("capture_file_name") + def _build(lid): data = {"data_link_type": dlt, "wireshark": ws} if fname: data["capture_file_name"] = fname return {"json_data": data} + return _batch_capture(project_id, link_ids, "start", _build, conn) link_id = params.get("link_id") if not link_id: @@ -879,7 +933,11 @@ def download_capture_file_handler(params: dict[str, Any], gns3_ctx: dict[str, An if not isinstance(link_ids, list): return {"error": "link_ids must be a list"} results = [_download(lid) for lid in link_ids] - return {"downloads": results, "count": len(results), "note": "Files are in pcap format. URLs include a 10-minute ticket."} + return { + "downloads": results, + "count": len(results), + "note": "Files are in pcap format. URLs include a 10-minute ticket.", + } link_id = params.get("link_id") if not link_id: @@ -946,12 +1004,18 @@ def link_marker_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic elif direction in ("tx", "rx"): body["direction"] = direction if not body: - return {"error": "At least one update field is required (bpf, tag, enabled, direction, color, highlight_duration)"} + return { + "error": "At least one update field is required (bpf, tag, enabled, direction, color, highlight_duration)" + } return conn.http_call("put", url, json_data=body).json() # action == "delete" conn.http_call("delete", url) - return {"message": f"Marker '{marker_name}' deleted from link {link_id}", "link_id": link_id, "marker_name": marker_name} + return { + "message": f"Marker '{marker_name}' deleted from link {link_id}", + "link_id": link_id, + "marker_name": marker_name, + } def marker_definition_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: @@ -1002,7 +1066,9 @@ def marker_definition_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) if params.get(opt) is not None: body[opt] = params[opt] if not body: - return {"error": "At least one update field is required (bpf, tag, color, highlight_duration, data_link_type)"} + return { + "error": "At least one update field is required (bpf, tag, color, highlight_duration, data_link_type)" + } return conn.http_call("put", url, json_data=body).json() # action == "delete" diff --git a/gns3server/agent/gns3_copilot/gns3_client/connector.py b/gns3server/agent/gns3_copilot/gns3_client/connector.py index bd5fbcf9c..40ce1c5e5 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/connector.py +++ b/gns3server/agent/gns3_copilot/gns3_client/connector.py @@ -139,17 +139,11 @@ class Gns3Connector: self.session.headers["Accept"] = "application/json" # pragma: no cover # Set authentication based on API version - if ( - self.auth_type == "basic" - and self.user is not None - and self.cred is not None - ): + if self.auth_type == "basic" and self.user is not None and self.cred is not None: self.session.auth = (self.user, self.cred) # pragma: no cover elif self.auth_type == "jwt" and self.access_token: - self.session.headers["Authorization"] = ( - f"Bearer {self.access_token}" - ) + self.session.headers["Authorization"] = f"Bearer {self.access_token}" def _authenticate_v3(self) -> None: """ @@ -161,15 +155,10 @@ class Gns3Connector: return if not self.user or not self.cred: - raise ValueError( - "Username and password are required for v3 authentication " - "when no JWT token is provided" - ) + raise ValueError("Username and password are required for v3 authentication when no JWT token is provided") # Construct authentication URL (v3 API uses different base URL) - auth_url = ( - f"{self.base_url.replace('/v3', '')}/v3/access/users/authenticate" - ) + auth_url = f"{self.base_url.replace('/v3', '')}/v3/access/users/authenticate" auth_data = {"username": self.user, "password": self.cred} # Use temporary session for authentication @@ -177,21 +166,14 @@ class Gns3Connector: temp_session.headers["Content-Type"] = "application/json" try: - response = temp_session.post( - auth_url, json=auth_data, verify=self.verify, timeout=10.0 - ) + response = temp_session.post(auth_url, json=auth_data, verify=self.verify, timeout=10.0) if response.status_code == 200: auth_result = response.json() self.access_token = auth_result["access_token"] # Update session with new token - self.session.headers["Authorization"] = ( - f"Bearer {self.access_token}" - ) + self.session.headers["Authorization"] = f"Bearer {self.access_token}" else: - raise HTTPError( - f"v3 API authentication failed: {response.status_code} - " - f"{response.text}" - ) + raise HTTPError(f"v3 API authentication failed: {response.status_code} - {response.text}") except Exception as e: raise HTTPError(f"v3 API authentication error: {str(e)}") from e @@ -205,9 +187,7 @@ class Gns3Connector: try: # Decode token without verification to check expiry - decoded: dict[str, Any] = jwt.decode( - token, options={"verify_signature": False} - ) + decoded: dict[str, Any] = jwt.decode(token, options={"verify_signature": False}) exp = decoded.get("exp") if exp is not None: return time.time() > float(exp) @@ -237,12 +217,7 @@ class Gns3Connector: """ # Handle JWT authentication - if ( - self.auth_type == "jwt" - and not self.access_token - and self.user - and self.cred - ): + if self.auth_type == "jwt" and not self.access_token and self.user and self.cred: self._authenticate_v3() # Get request function (e.g., session.get, session.post) @@ -285,15 +260,10 @@ class Gns3Connector: try: # Only attempt parsing when Content-Type is JSON - if ( - "application/json" - in response.headers.get("Content-Type", "").lower() - ): + if "application/json" in response.headers.get("Content-Type", "").lower(): error_json = response.json() status = error_json.get("status", "Unknown Status") - message = error_json.get( - "message", "No message provided in JSON." - ) + message = error_json.get("message", "No message provided in JSON.") # Construct a more descriptive new error new_err = HTTPError( f"{status}: {message} (Original {response.status_code} Error)", diff --git a/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py b/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py index 724291782..c57dc8fee 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py +++ b/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py @@ -76,10 +76,7 @@ def _get_url_from_controller() -> Optional[str]: controller = Controller.instance() local_compute = controller.get_compute("local") - url = ( - f"{local_compute.protocol}://{local_compute.host}:" - f"{local_compute.port}" - ) + url = f"{local_compute.protocol}://{local_compute.host}:{local_compute.port}" logger.debug( "Got GNS3 URL from Controller: %s (protocol=%s, host=%s, port=%s)", url, @@ -98,9 +95,7 @@ def _get_url_from_controller() -> Optional[str]: logger.debug("Local compute not found in Controller: %s", str(e)) return None except Exception as e: - logger.warning( - "Unexpected error getting URL from Controller: %s", str(e) - ) + logger.warning("Unexpected error getting URL from Controller: %s", str(e)) return None @@ -114,10 +109,7 @@ def _get_url_from_config() -> Optional[str]: from gns3server.config import Config server_config = Config.instance().settings.Server - url = ( - f"{server_config.protocol.value}://{server_config.host}:" - f"{server_config.port}" - ) + url = f"{server_config.protocol.value}://{server_config.host}:{server_config.port}" logger.debug( "Got GNS3 URL from Config: %s (protocol=%s, host=%s, port=%s)", url, @@ -137,9 +129,7 @@ def _get_url_from_config() -> Optional[str]: return None -def get_gns3_connector( - jwt_token: Optional[str] = None, url: Optional[str] = None -) -> Optional[Gns3Connector]: +def get_gns3_connector(jwt_token: Optional[str] = None, url: Optional[str] = None) -> Optional[Gns3Connector]: """Create and return a Gns3Connector instance with JWT authentication. URL Resolution Strategy (in order): @@ -223,9 +213,7 @@ def get_gns3_connector( return connector except Exception as e: - logger.error( - "Failed to create Gns3Connector: %s", str(e), exc_info=True - ) + logger.error("Failed to create Gns3Connector: %s", str(e), exc_info=True) return None @@ -284,9 +272,7 @@ async def get_gns3_connector_with_llm_config( url = _detect_url_for_api() # Step 3: Get LLM config - llm_config = get_llm_config( - user_id=user_id, jwt_token=jwt_token, app=app - ) + llm_config = get_llm_config(user_id=user_id, jwt_token=jwt_token, app=app) if not llm_config: logger.warning(f"No LLM config found for user {user_id}") @@ -303,9 +289,7 @@ async def get_gns3_connector_with_llm_config( return {"connector": connector, "llm_config": llm_config} except Exception as e: - logger.error( - f"Failed to get GNS3 connector with LLM config: {e}", exc_info=True - ) + logger.error(f"Failed to get GNS3 connector with LLM config: {e}", exc_info=True) return None @@ -375,9 +359,7 @@ def get_gns3_server_host() -> str: logger.debug("Extracted GNS3 server host: %s from URL: %s", host, url) return host except Exception as e: - logger.warning( - "Failed to extract host from URL %s: %s, using fallback", url, e - ) + logger.warning("Failed to extract host from URL %s: %s, using fallback", url, e) return DEFAULT_GNS3_URL.split("://")[1].split(":")[0] @@ -428,9 +410,7 @@ def get_llm_config(user_id, jwt_token: str, app=None) -> Optional[dict]: # We're in an async context with a running loop # This shouldn't happen since this is a sync function with concurrent.futures.ThreadPoolExecutor() as executor: - future = executor.submit( - asyncio.run, get_user_llm_config_with_app(user_id, app) - ) + future = executor.submit(asyncio.run, get_user_llm_config_with_app(user_id, app)) return future.result(timeout=10) except RuntimeError: # No running event loop - we're in a sync context @@ -449,26 +429,19 @@ def get_llm_config(user_id, jwt_token: str, app=None) -> Optional[dict]: return future.result(timeout=10) else: # Loop exists but not running - use it - return loop.run_until_complete( - get_user_llm_config_with_app(user_id, app) - ) + return loop.run_until_complete(get_user_llm_config_with_app(user_id, app)) except RuntimeError: # No event loop exists - create a new one loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: - return loop.run_until_complete( - get_user_llm_config_with_app(user_id, app) - ) + return loop.run_until_complete(get_user_llm_config_with_app(user_id, app)) finally: loop.close() # Fallback: No app provided, try API call (will mask group config API # keys) - logger.warning( - "No app provided for get_llm_config, group config API keys may be " - "masked" - ) + logger.warning("No app provided for get_llm_config, group config API keys may be masked") return None except Exception as e: diff --git a/gns3server/agent/gns3_copilot/gns3_client/context_helpers.py b/gns3server/agent/gns3_copilot/gns3_client/context_helpers.py index 73e275767..5070dcb9a 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/context_helpers.py +++ b/gns3server/agent/gns3_copilot/gns3_client/context_helpers.py @@ -62,12 +62,8 @@ logger = logging.getLogger(__name__) # Context variables for request-scoped data # Automatically cleaned up when request context ends -_jwt_token_context: ContextVar[Optional[str]] = ContextVar( - "_jwt_token_context", default=None -) -_llm_config_context: ContextVar[Optional[dict]] = ContextVar( - "_llm_config_context", default=None -) +_jwt_token_context: ContextVar[Optional[str]] = ContextVar("_jwt_token_context", default=None) +_llm_config_context: ContextVar[Optional[dict]] = ContextVar("_llm_config_context", default=None) def set_current_jwt_token(token: str) -> None: diff --git a/gns3server/agent/gns3_copilot/gns3_client/gns3_project_info.py b/gns3server/agent/gns3_copilot/gns3_client/gns3_project_info.py index eedb72a00..9b699e51f 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/gns3_project_info.py +++ b/gns3server/agent/gns3_copilot/gns3_client/gns3_project_info.py @@ -90,20 +90,13 @@ class GNS3ProjectInfoTool(BaseTool): """ # Log received input - logger.info( - "Received tool_input: %s, project_id: %s", tool_input, project_id - ) + logger.info("Received tool_input: %s, project_id: %s", tool_input, project_id) try: # Validate project_id parameter if not project_id: logger.error("project_id parameter is required.") - return { - "error": ( - "project_id parameter is required. Please provide a valid " - "project UUID." - ) - } + return {"error": ("project_id parameter is required. Please provide a valid project UUID.")} # Build handler context (JWT + server URL from request context) logger.debug("Connecting to GNS3 server...") @@ -111,17 +104,10 @@ class GNS3ProjectInfoTool(BaseTool): if gns3_ctx is None: logger.error("Failed to create GNS3 connector") - return { - "error": ( - "Failed to connect to GNS3 server. Please check your " - "configuration." - ) - } + return {"error": ("Failed to connect to GNS3 server. Please check your configuration.")} # Use the provided project_id directly - logger.info( - f"Retrieving project info for project_id: {project_id}" - ) + logger.info(f"Retrieving project info for project_id: {project_id}") inventory = fetch_project_inventory(gns3_ctx, project_id) # Get node and link counts diff --git a/gns3server/agent/gns3_copilot/gns3_client/gns3_topology_reader.py b/gns3server/agent/gns3_copilot/gns3_client/gns3_topology_reader.py index 59bd33507..ce5449079 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/gns3_topology_reader.py +++ b/gns3server/agent/gns3_copilot/gns3_client/gns3_topology_reader.py @@ -100,18 +100,13 @@ class GNS3TopologyTool(BaseTool): """ # Log received input - logger.info( - "Received tool_input: %s, project_id: %s", tool_input, project_id - ) + logger.info("Received tool_input: %s, project_id: %s", tool_input, project_id) try: # Validate project_id parameter if not project_id: logger.error("project_id parameter is required.") - return { - "error": "project_id parameter is required. " - "Please provide a valid project UUID." - } + return {"error": "project_id parameter is required. Please provide a valid project UUID."} # Build handler context (JWT + server URL) # jwt_token/url can be passed explicitly (e.g. from MCP handlers) @@ -121,10 +116,7 @@ class GNS3TopologyTool(BaseTool): if gns3_ctx is None: logger.error("Failed to create GNS3 connector") - return { - "error": "Failed to connect to GNS3 server. Please check " - "your configuration." - } + return {"error": "Failed to connect to GNS3 server. Please check your configuration."} # Use the provided project_id directly logger.info(f"Retrieving topology for project_id: {project_id}") @@ -135,16 +127,13 @@ class GNS3TopologyTool(BaseTool): "project_id": inventory["project_id"], "name": inventory["name"], "status": inventory["status"], - "nodes": self._clean_nodes_ports( - copy.deepcopy(inventory["nodes_inventory"]) - ), + "nodes": self._clean_nodes_ports(copy.deepcopy(inventory["nodes_inventory"])), "links": inventory["links_summary"], } # Log topology result logger.info( - "Topology retrieved: project_id=%s, name=%s, nodes=%d, " - "links=%d", + "Topology retrieved: project_id=%s, name=%s, nodes=%d, links=%d", topology.get("project_id"), topology.get("name"), len(topology.get("nodes", {})), @@ -167,10 +156,7 @@ class GNS3TopologyTool(BaseTool): """ for node in data.values(): # Iterate through R-1, R-2, R-3, R-4 if "ports" in node and isinstance(node["ports"], list): - node["ports"] = [ - {"name": port["name"], "short_name": port["short_name"]} - for port in node["ports"] - ] + node["ports"] = [{"name": port["name"], "short_name": port["short_name"]} for port in node["ports"]] return data diff --git a/gns3server/agent/gns3_copilot/gns3_client/project_inventory.py b/gns3server/agent/gns3_copilot/gns3_client/project_inventory.py index a4b6782da..b1f958767 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/project_inventory.py +++ b/gns3server/agent/gns3_copilot/gns3_client/project_inventory.py @@ -38,9 +38,7 @@ from urllib.parse import urlparse from gns3server.agent.gns3_copilot.gns3_client.api_handlers import _get_connector -def build_nodes_inventory( - nodes: list[dict[str, Any]], server_host: str | None -) -> dict[str, Any]: +def build_nodes_inventory(nodes: list[dict[str, Any]], server_host: str | None) -> dict[str, Any]: """ Build an inventory-style dict keyed by node name. @@ -70,9 +68,7 @@ def build_nodes_inventory( return inventory -def build_links_summary( - nodes: list[dict[str, Any]], links: list[dict[str, Any]] -) -> list[dict[str, str]]: +def build_links_summary(nodes: list[dict[str, Any]], links: list[dict[str, Any]]) -> list[dict[str, str]]: """ Build a human/LLM-friendly link list resolving node and port names. @@ -87,46 +83,40 @@ def build_links_summary( side_a = link["nodes"][0] side_b = link["nodes"][1] try: - node_a = next( - x for x in nodes if x.get("node_id") == side_a["node_id"] - ) + node_a = next(x for x in nodes if x.get("node_id") == side_a["node_id"]) port_a = str( next( p["name"] for p in (node_a.get("ports") or []) - if p["port_number"] == side_a["port_number"] - and p["adapter_number"] == side_a["adapter_number"] + if p["port_number"] == side_a["port_number"] and p["adapter_number"] == side_a["adapter_number"] ) ) - node_b = next( - x for x in nodes if x.get("node_id") == side_b["node_id"] - ) + node_b = next(x for x in nodes if x.get("node_id") == side_b["node_id"]) port_b = str( next( p["name"] for p in (node_b.get("ports") or []) - if p["port_number"] == side_b["port_number"] - and p["adapter_number"] == side_b["adapter_number"] + if p["port_number"] == side_b["port_number"] and p["adapter_number"] == side_b["adapter_number"] ) ) name_a = str(node_a["name"]) if node_a.get("name") else "Unknown" name_b = str(node_b["name"]) if node_b.get("name") else "Unknown" - summary.append({ - "link_id": link.get("link_id"), - "node_a": name_a, - "port_a": port_a, - "node_b": name_b, - "port_b": port_b, - }) + summary.append( + { + "link_id": link.get("link_id"), + "node_a": name_a, + "port_a": port_a, + "node_b": name_b, + "port_b": port_b, + } + ) except (StopIteration, KeyError, AttributeError): # Prevent errors when lookups can't match data continue return summary -def fetch_project_inventory( - gns3_ctx: dict[str, Any], project_id: str -) -> dict[str, Any]: +def fetch_project_inventory(gns3_ctx: dict[str, Any], project_id: str) -> dict[str, Any]: """ Fetch a project's metadata, nodes and links and return the aggregated inventory — the equivalent of the old ``Project.get()`` + diff --git a/gns3server/agent/gns3_copilot/project_agent_manager.py b/gns3server/agent/gns3_copilot/project_agent_manager.py index edc452f32..b9ade018a 100644 --- a/gns3server/agent/gns3_copilot/project_agent_manager.py +++ b/gns3server/agent/gns3_copilot/project_agent_manager.py @@ -61,9 +61,7 @@ class ProjectAgentManager: cls._instance._lock = asyncio.Lock() return cls._instance - async def get_agent( - self, project_id: str, project_path: str - ) -> AgentService: + async def get_agent(self, project_id: str, project_path: str) -> AgentService: """ Get or create an AgentService for a project. diff --git a/gns3server/agent/gns3_copilot/prompts/prompt_loader.py b/gns3server/agent/gns3_copilot/prompts/prompt_loader.py index 110f76dae..1b664b85c 100644 --- a/gns3server/agent/gns3_copilot/prompts/prompt_loader.py +++ b/gns3server/agent/gns3_copilot/prompts/prompt_loader.py @@ -70,10 +70,7 @@ def load_system_prompt(llm_config: dict | None = None) -> str: str: The system prompt string, or empty string if not found. """ if not llm_config: - logger.debug( - "No LLM config provided, using default TEACHING_ASSISTANT " - "prompt mode" - ) + logger.debug("No LLM config provided, using default TEACHING_ASSISTANT prompt mode") return _load_prompt("teaching_assistant") # llm_config is a flattened dict with copilot_mode at the top level @@ -81,15 +78,10 @@ def load_system_prompt(llm_config: dict | None = None) -> str: mode = llm_config.get("copilot_mode", "teaching_assistant").lower() if mode == "lab_automation_assistant": - logger.debug( - "Using LAB_AUTOMATION_ASSISTANT prompt mode (diagnostics + " - "configuration)" - ) + logger.debug("Using LAB_AUTOMATION_ASSISTANT prompt mode (diagnostics + configuration)") return _load_prompt("lab_automation_assistant") elif mode == "troubleshooting_injection": - logger.debug( - "Using TROUBLESHOOTING_INJECTION prompt mode (fault injection)" - ) + logger.debug("Using TROUBLESHOOTING_INJECTION prompt mode (fault injection)") return _load_prompt("troubleshooting_injection") else: logger.debug("Using TEACHING_ASSISTANT prompt mode (diagnostics only)") diff --git a/gns3server/agent/gns3_copilot/skills/loader.py b/gns3server/agent/gns3_copilot/skills/loader.py index fee183ef6..655d76263 100644 --- a/gns3server/agent/gns3_copilot/skills/loader.py +++ b/gns3server/agent/gns3_copilot/skills/loader.py @@ -201,7 +201,9 @@ class SkillsLoader: topic_key = yaml_file.stem logger.warning(f"No 'topic' field in {yaml_file}, using filename '{topic_key}'") if topic_key in topics: - logger.warning(f"Duplicate topic '{topic_key}' in {device_path.name} (from {yaml_file}), overwriting") + logger.warning( + f"Duplicate topic '{topic_key}' in {device_path.name} (from {yaml_file}), overwriting" + ) # category/topics belong to the base skill only topic_data.pop("category", None) diff --git a/gns3server/agent/gns3_copilot/skills/manager.py b/gns3server/agent/gns3_copilot/skills/manager.py index e14b8a8dc..473fb2b09 100644 --- a/gns3server/agent/gns3_copilot/skills/manager.py +++ b/gns3server/agent/gns3_copilot/skills/manager.py @@ -49,9 +49,9 @@ logger = logging.getLogger(__name__) # Applied per-command via the `env` parameter to avoid polluting # the global process environment. _GIT_TIMEOUT_ENV = { - 'GIT_HTTP_TIMEOUT': '10', # Connection timeout (default: 120s) - 'GIT_HTTP_LOW_SPEED_TIME': '5', # Slow speed threshold window - 'GIT_HTTP_LOW_SPEED_LIMIT': '10240', # < 10 KB/s = slow → abort + "GIT_HTTP_TIMEOUT": "10", # Connection timeout (default: 120s) + "GIT_HTTP_LOW_SPEED_TIME": "5", # Slow speed threshold window + "GIT_HTTP_LOW_SPEED_LIMIT": "10240", # < 10 KB/s = slow → abort } @@ -66,12 +66,7 @@ class SkillsManager: - Version tracking """ - def __init__( - self, - repo_url: str = None, - branch: str = "main", - auto_update: bool = False - ): + def __init__(self, repo_url: str = None, branch: str = "main", auto_update: bool = False): """ Initialize the skills manager. @@ -147,7 +142,7 @@ class SkillsManager: logger.warning( "Skills repository has uncommitted changes, skipping pull. " "Commit or stash changes in %s to enable automatic updates.", - self.local_path + self.local_path, ) return @@ -161,14 +156,9 @@ class SkillsManager: # Check if behind and pull try: - behind_commits = list(self._repo.iter_commits( - f'{self.branch}..origin/{self.branch}' - )) + behind_commits = list(self._repo.iter_commits(f"{self.branch}..origin/{self.branch}")) if behind_commits: - logger.info( - "Skills repository is behind by %d commit(s), pulling...", - len(behind_commits) - ) + logger.info("Skills repository is behind by %d commit(s), pulling...", len(behind_commits)) origin.pull(self.branch, env=_GIT_TIMEOUT_ENV) logger.info(f"Updated to commit {self.get_current_version()}") else: @@ -188,12 +178,7 @@ class SkillsManager: return False try: - self._repo = git.Repo.clone_from( - self.repo_url, - self.local_path, - branch=self.branch, - env=_GIT_TIMEOUT_ENV - ) + self._repo = git.Repo.clone_from(self.repo_url, self.local_path, branch=self.branch, env=_GIT_TIMEOUT_ENV) logger.info(f"Successfully cloned skills repository to {self.local_path}") return True except git.GitCommandError as e: @@ -256,7 +241,9 @@ class SkillsManager: del SKILLS_REGISTRY[k] SKILLS_REGISTRY.update(all_skills) - logger.info(f"Loaded {len(new_injection_skills)} injection skills, {len(new_device_skills)} device skills, and {len(new_feature_skills)} feature skills") + logger.info( + f"Loaded {len(new_injection_skills)} injection skills, {len(new_device_skills)} device skills, and {len(new_feature_skills)} feature skills" + ) return True except Exception as e: logger.error(f"Failed to reload skills: {e}") @@ -302,12 +289,7 @@ class SkillsManager: """ try: # Available prompt names - prompt_names = [ - "lab_automation_assistant", - "teaching_assistant", - "troubleshooting_injection", - "title" - ] + prompt_names = ["lab_automation_assistant", "teaching_assistant", "troubleshooting_injection", "title"] # Load all prompts loaded_count = 0 @@ -385,6 +367,7 @@ class SkillsManager: """ try: from .registry import INJECTION_SKILLS_REGISTRY + return len(INJECTION_SKILLS_REGISTRY) except Exception: return 0 @@ -413,7 +396,7 @@ class SkillsManager: "skill_count": self.get_skill_count(), "prompt_count": self.get_prompt_count(), "auto_update": self.auto_update, - "is_initialized": (self.local_path / ".git").exists() + "is_initialized": (self.local_path / ".git").exists(), } def rollback(self, commit_hash: str) -> bool: @@ -462,12 +445,14 @@ class SkillsManager: commits = [] for commit in self._repo.iter_commits(max_count=limit): - commits.append({ - "hash": commit.hexsha, - "message": commit.message.strip(), - "author": str(commit.author), - "date": commit.committed_datetime.isoformat() - }) + commits.append( + { + "hash": commit.hexsha, + "message": commit.message.strip(), + "author": str(commit.author), + "date": commit.committed_datetime.isoformat(), + } + ) return commits except Exception as e: logger.error(f"Failed to get commit history: {e}") diff --git a/gns3server/agent/gns3_copilot/skills/registry.py b/gns3server/agent/gns3_copilot/skills/registry.py index a003413be..d9e2b5083 100644 --- a/gns3server/agent/gns3_copilot/skills/registry.py +++ b/gns3server/agent/gns3_copilot/skills/registry.py @@ -115,7 +115,7 @@ def _ensure_skills_manager(): manager = SkillsManager( repo_url=config.get("repo_url"), branch=config.get("branch", "main"), - auto_update=config.get("auto_update", False) + auto_update=config.get("auto_update", False), ) if not manager.initialize(): @@ -224,7 +224,7 @@ def reload_injection_skills() -> dict[str, Any]: "success": False, "message": "Skills manager not initialized", "skill_count": len(INJECTION_SKILLS_REGISTRY), - "version": "" + "version": "", } try: @@ -233,7 +233,7 @@ def reload_injection_skills() -> dict[str, Any]: "success": success, "message": "Skills reloaded successfully" if success else "Failed to reload skills", "skill_count": manager.get_skill_count(), - "version": manager.get_current_version() + "version": manager.get_current_version(), } except Exception as e: logger.error(f"Error during skills reload: {e}") @@ -241,7 +241,7 @@ def reload_injection_skills() -> dict[str, Any]: "success": False, "message": f"Error: {str(e)}", "skill_count": len(INJECTION_SKILLS_REGISTRY), - "version": "" + "version": "", } @@ -263,12 +263,7 @@ def reload_prompts() -> dict[str, Any]: """ manager = get_skills_manager() if manager is None: - return { - "success": False, - "message": "Skills manager not initialized", - "prompt_count": 0, - "version": "" - } + return {"success": False, "message": "Skills manager not initialized", "prompt_count": 0, "version": ""} try: success = manager.reload_prompts() @@ -276,7 +271,7 @@ def reload_prompts() -> dict[str, Any]: "success": success, "message": "Prompts reloaded successfully" if success else "Failed to reload prompts", "prompt_count": manager.get_prompt_count(), - "version": manager.get_current_version() + "version": manager.get_current_version(), } except Exception as e: logger.error(f"Error during prompts reload: {e}") @@ -285,7 +280,7 @@ def reload_prompts() -> dict[str, Any]: "success": False, "message": f"Error: {str(e)}", "prompt_count": manager.get_prompt_count() if manager else 0, - "version": "" + "version": "", } @@ -315,16 +310,11 @@ def reload_forbidden_commands() -> dict[str, Any]: "success": True, "message": "Forbidden commands reloaded", "command_count": len(commands), - "version": manager.get_current_version() if manager else "" + "version": manager.get_current_version() if manager else "", } except Exception as e: logger.error(f"Error during forbidden commands reload: {e}") - return { - "success": False, - "message": f"Error: {str(e)}", - "command_count": 0, - "version": "" - } + return {"success": False, "message": f"Error: {str(e)}", "command_count": 0, "version": ""} def get_prompt(prompt_name: str) -> str: @@ -360,10 +350,7 @@ def get_skills_repository_info() -> dict[str, Any]: """ manager = get_skills_manager() if manager is None: - return { - "initialized": False, - "message": "Skills manager not initialized" - } + return {"initialized": False, "message": "Skills manager not initialized"} return manager.get_repository_info() @@ -411,8 +398,7 @@ def get_skill( "error": f"device_type '{device_type}' is not in category '{category}'", "device_category": skill.get("category"), "available_in_category": [ - did for did, s in SKILLS_REGISTRY.items() - if s.get("category", "").lower() == category.lower() + did for did, s in SKILLS_REGISTRY.items() if s.get("category", "").lower() == category.lower() ], } @@ -484,8 +470,7 @@ def get_skill( } if topics: result["topics"] = { - k: {"name": v.get("name", k), "description": v.get("description", "")} - for k, v in topics.items() + k: {"name": v.get("name", k), "description": v.get("description", "")} for k, v in topics.items() } return result @@ -503,19 +488,23 @@ def list_available_skills(category: str | None = None) -> list[dict[str, Any]]: for did, skill in SKILLS_REGISTRY.items(): if category: if skill.get("category", "").lower() == category.lower(): - skills.append({ + skills.append( + { + "device_type": did, + "name": skill.get("name", did), + "category": skill.get("category"), + "topic_count": len(skill.get("topics", {})), + } + ) + else: + skills.append( + { "device_type": did, "name": skill.get("name", did), "category": skill.get("category"), "topic_count": len(skill.get("topics", {})), - }) - else: - skills.append({ - "device_type": did, - "name": skill.get("name", did), - "category": skill.get("category"), - "topic_count": len(skill.get("topics", {})), - }) + } + ) return skills @@ -541,7 +530,7 @@ def get_injection_skill( return { "error": f"Unknown injection fault type: {device_type}", "available_fault_types": list(INJECTION_SKILLS_REGISTRY.keys()), - "hint": "Use {'action': 'list'} to see all available fault types" + "hint": "Use {'action': 'list'} to see all available fault types", } issues = skill.get("issues", {}) @@ -611,11 +600,13 @@ def list_available_injection_skills(context: list[str] | None = None) -> list[di category_lower = category.lower() if not any(kw.lower() in category_lower or kw.lower() in did.lower() for kw in context): continue - skills.append({ - "device_type": did, - "name": skill.get("name", did), - "category": category, - }) + skills.append( + { + "device_type": did, + "name": skill.get("name", did), + "category": category, + } + ) return skills @@ -658,11 +649,13 @@ def list_available_packet_analysis_protocols() -> list[dict[str, str]]: """ protocols = [] for key, data in PACKET_ANALYSIS_REGISTRY.items(): - protocols.append({ - "protocol": key, - "name": data.get("name", key), - "description": data.get("description", ""), - }) + protocols.append( + { + "protocol": key, + "name": data.get("name", key), + "description": data.get("description", ""), + } + ) return protocols @@ -711,10 +704,14 @@ class DeviceSkillsTool(BaseTool): try: params = json.loads(tool_input) except json.JSONDecodeError as e: - return json.dumps({ - "error": f"Invalid JSON input: {e}", - "hint": 'Expected format: {"device_type": "xxx"} or {"action": "list"}' - }, ensure_ascii=False, indent=2) + return json.dumps( + { + "error": f"Invalid JSON input: {e}", + "hint": 'Expected format: {"device_type": "xxx"} or {"action": "list"}', + }, + ensure_ascii=False, + indent=2, + ) else: params = tool_input @@ -722,18 +719,19 @@ class DeviceSkillsTool(BaseTool): if action == "list": skills = list_available_skills() - return json.dumps({ - "count": len(skills), - "skills": skills - }, ensure_ascii=False, indent=2) + return json.dumps({"count": len(skills), "skills": skills}, ensure_ascii=False, indent=2) device_type = params.get("device_type") if not device_type: - return json.dumps({ - "error": "Missing required field: device_type", - "available_device_types": list(SKILLS_REGISTRY.keys()), - "hint": 'Use {"action": "list"} to see all available device types' - }, ensure_ascii=False, indent=2) + return json.dumps( + { + "error": "Missing required field: device_type", + "available_device_types": list(SKILLS_REGISTRY.keys()), + "hint": 'Use {"action": "list"} to see all available device types', + }, + ensure_ascii=False, + indent=2, + ) category = params.get("category") detail = params.get("detail", "full") @@ -788,10 +786,14 @@ class InjectionSkillsTool(BaseTool): try: params = json.loads(tool_input) except json.JSONDecodeError as e: - return json.dumps({ - "error": f"Invalid JSON input: {e}", - "hint": 'Expected format: {"device_type": "xxx"} or {"action": "list"}' - }, ensure_ascii=False, indent=2) + return json.dumps( + { + "error": f"Invalid JSON input: {e}", + "hint": 'Expected format: {"device_type": "xxx"} or {"action": "list"}', + }, + ensure_ascii=False, + indent=2, + ) else: params = tool_input @@ -800,33 +802,44 @@ class InjectionSkillsTool(BaseTool): if action == "list": context = params.get("context") if not context or not isinstance(context, list) or len(context) == 0: - return json.dumps({ - "error": "context parameter is required when action='list'", - "hint": "Analyze the topology and device configurations first, " - "then pass the protocols/services you found as context. " - 'Example: {"action": "list", "context": ["ospf", "bgp", "vlan"]}', - "available_categories": sorted(set( - skill.get("category", "") - for skill in INJECTION_SKILLS_REGISTRY.values() - )) - }, ensure_ascii=False, indent=2) + return json.dumps( + { + "error": "context parameter is required when action='list'", + "hint": "Analyze the topology and device configurations first, " + "then pass the protocols/services you found as context. " + 'Example: {"action": "list", "context": ["ospf", "bgp", "vlan"]}', + "available_categories": sorted( + set(skill.get("category", "") for skill in INJECTION_SKILLS_REGISTRY.values()) + ), + }, + ensure_ascii=False, + indent=2, + ) skills = list_available_injection_skills(context=context) logger.debug(f"Injection skills filtered by context={context}: {len(skills)} matching") - return json.dumps({ - "count": len(skills), - "total_available": len(INJECTION_SKILLS_REGISTRY), - "context": context, - "fault_types": skills - }, ensure_ascii=False, indent=2) + return json.dumps( + { + "count": len(skills), + "total_available": len(INJECTION_SKILLS_REGISTRY), + "context": context, + "fault_types": skills, + }, + ensure_ascii=False, + indent=2, + ) device_type = params.get("device_type") if not device_type: - return json.dumps({ - "error": "Missing required field: device_type", - "available_fault_types": list(INJECTION_SKILLS_REGISTRY.keys()), - "hint": 'Use {"action": "list"} to see all available fault types' - }, ensure_ascii=False, indent=2) + return json.dumps( + { + "error": "Missing required field: device_type", + "available_fault_types": list(INJECTION_SKILLS_REGISTRY.keys()), + "hint": 'Use {"action": "list"} to see all available fault types', + }, + ensure_ascii=False, + indent=2, + ) detail = params.get("detail", "full") issue = params.get("issue") @@ -876,10 +889,14 @@ class PacketAnalysisSkillsTool(BaseTool): try: params = json.loads(tool_input) except json.JSONDecodeError as e: - return json.dumps({ - "error": f"Invalid JSON input: {e}", - "hint": 'Expected format: {"action": "get", "protocol": "ospf"}' - }, ensure_ascii=False, indent=2) + return json.dumps( + { + "error": f"Invalid JSON input: {e}", + "hint": 'Expected format: {"action": "get", "protocol": "ospf"}', + }, + ensure_ascii=False, + indent=2, + ) else: params = tool_input @@ -887,17 +904,18 @@ class PacketAnalysisSkillsTool(BaseTool): if action == "list": protocols = list_available_packet_analysis_protocols() - return json.dumps({ - "count": len(protocols), - "protocols": protocols - }, ensure_ascii=False, indent=2) + return json.dumps({"count": len(protocols), "protocols": protocols}, ensure_ascii=False, indent=2) protocol = params.get("protocol") if not protocol: - return json.dumps({ - "error": "Missing required field: protocol", - "available_protocols": list(PACKET_ANALYSIS_REGISTRY.keys()), - }, ensure_ascii=False, indent=2) + return json.dumps( + { + "error": "Missing required field: protocol", + "available_protocols": list(PACKET_ANALYSIS_REGISTRY.keys()), + }, + ensure_ascii=False, + indent=2, + ) result = get_packet_analysis_protocol(protocol) return json.dumps(result, ensure_ascii=False, indent=2) diff --git a/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py b/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py index 285e221dc..db215c6bf 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py @@ -196,56 +196,35 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): # Validate input device_configs_list, project_id = self._validate_tool_input(tool_input) - if ( - isinstance(device_configs_list, list) - and len(device_configs_list) > 0 - and "error" in device_configs_list[0] - ): + if isinstance(device_configs_list, list) and len(device_configs_list) > 0 and "error" in device_configs_list[0]: return device_configs_list # Filter forbidden commands and store blocked commands info - device_configs_list, blocked_commands_map = ( - self._filter_forbidden_commands_from_device_configs( - device_configs_list - ) + device_configs_list, blocked_commands_map = self._filter_forbidden_commands_from_device_configs( + device_configs_list ) # Expand multiline commands (e.g., banner commands with embedded newlines) # This converts commands like "banner motd #\nline1\nline2\n#" into # separate commands: ["banner motd #", "line1", "line2", "#"] - device_configs_list = self._expand_multiline_commands( - device_configs_list - ) + device_configs_list = self._expand_multiline_commands(device_configs_list) # Create a mapping of device names to their configuration commands device_configs_map = self._configs_map(device_configs_list) # Prepare device hosts data try: - hosts_data = self._prepare_device_hosts_data( - device_configs_list, project_id, jwt_token=jwt_token, url=url - ) + hosts_data = self._prepare_device_hosts_data(device_configs_list, project_id, jwt_token=jwt_token, url=url) except ValueError as e: logger.error("Failed to prepare device hosts data: %s", e) return [{"status": "failed", "error": str(e)}] # Check if any devices have errors (e.g., missing device_type tag) - error_devices = { - name: data - for name, data in hosts_data.items() - if "error" in data - } + error_devices = {name: data for name, data in hosts_data.items() if "error" in data} if error_devices: - logger.error( - "Devices with configuration errors: %s", - list(error_devices.keys()) - ) + logger.error("Devices with configuration errors: %s", list(error_devices.keys())) return [ - { - "device_name": name, - "status": "failed", - "error": data["error"] - } + {"device_name": name, "status": "failed", "error": data["error"]} for name, data in error_devices.items() ] @@ -275,9 +254,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): except Exception as e: # Overall execution failed - logger.error( - "Error executing configurations on all devices: %s", e - ) + logger.error("Error executing configurations on all devices: %s", e) return [{"status": "failed", "error": f"Execution error: {str(e)}"}] logger.info( @@ -287,22 +264,16 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): return results - def _run_all_device_configs_with_single_retry( - self, task: Task, device_configs_map: dict[str, list[str]] - ) -> Result: + def _run_all_device_configs_with_single_retry(self, task: Task, device_configs_map: dict[str, list[str]]) -> Result: """Execute configuration commands with single retry mechanism.""" device_name = task.host.name config_commands = device_configs_map.get(device_name, []) if not config_commands: - return Result( - host=task.host, result="No configuration commands to execute" - ) + return Result(host=task.host, result="No configuration commands to execute") try: - _result = task.run( - task=netmiko_send_config, config_commands=config_commands - ) + _result = task.run(task=netmiko_send_config, config_commands=config_commands) return Result(host=task.host, result=_result.result) except ReadTimeout as e: @@ -322,9 +293,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): # Handle Cisco IOSv L2 where '#' prompt char may be delayed, # causing Netmiko failures. Implements retry logic. if "netmiko_send_config (failed)" in str(e): - _result = task.run( - task=netmiko_send_config, config_commands=config_commands - ) + _result = task.run(task=netmiko_send_config, config_commands=config_commands) return Result(host=task.host, result=_result.result) # Log any other exceptions with full details @@ -364,9 +333,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): parsed_input = json.loads(tool_input) logger.debug("Successfully parsed tool input from JSON string.") except json.JSONDecodeError as e: - logger.error( - "Invalid JSON string received as tool input: %s", e - ) + logger.error("Invalid JSON string received as tool input: %s", e) return ( [{"status": "failed", "error": f"Invalid JSON string input from model: {e}"}], None, @@ -392,10 +359,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): return ([{"status": "failed", "error": error_msg}], None) if not self._validate_project_id(project_id): - error_msg = ( - f"Invalid project_id format: {project_id}. " - "Expected UUID format." - ) + error_msg = f"Invalid project_id format: {project_id}. Expected UUID format." logger.error(error_msg) return ([{"status": "failed", "error": error_msg}], None) @@ -413,10 +377,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): # Handle legacy format: [...] elif isinstance(parsed_input, list): - logger.warning( - "Using legacy format without project_id. " - "Please use new format with project_id." - ) + logger.warning("Using legacy format without project_id. Please use new format with project_id.") return parsed_input, None else: @@ -438,9 +399,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): Returns: True if valid UUID format, False otherwise """ - uuid_pattern = ( - r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" - ) + uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" return bool(re.match(uuid_pattern, project_id, re.IGNORECASE)) def _filter_forbidden_commands_from_device_configs( @@ -465,9 +424,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): commands = device_config["config_commands"] # Filter commands - allowed_commands, blocked_info = filter_forbidden_commands( - commands - ) + allowed_commands, blocked_info = filter_forbidden_commands(commands) # Update device config with allowed commands only filtered_config = device_config.copy() @@ -486,9 +443,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): return filtered_list, blocked_commands_map - def _expand_multiline_commands( - self, device_configs_list: list[dict[str, Any]] - ) -> list[dict[str, Any]]: + def _expand_multiline_commands(self, device_configs_list: list[dict[str, Any]]) -> list[dict[str, Any]]: """ Expand commands that contain embedded newline characters. @@ -540,9 +495,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): return expanded_list - def _configs_map( - self, device_config_list: list[dict[str, Any]] - ) -> dict[str, list[str]]: + def _configs_map(self, device_config_list: list[dict[str, Any]]) -> dict[str, list[str]]: """Create a mapping of device names to their configuration commands.""" device_configs_map = {} for device_config in device_config_list: @@ -561,15 +514,10 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): ) -> dict[str, dict[str, Any]]: """Prepare device hosts data from topology information.""" # Extract device names list - device_names = [ - device_config["device_name"] - for device_config in device_config_list - ] + device_names = [device_config["device_name"] for device_config in device_config_list] # Get device port information with project_id - hosts_data = get_device_ports_from_topology( - device_names, project_id, jwt_token=jwt_token, url=url - ) + hosts_data = get_device_ports_from_topology(device_names, project_id, jwt_token=jwt_token, url=url) if not hosts_data: error_msg = ( @@ -589,9 +537,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): return hosts_data - def _initialize_nornir( - self, hosts_data: dict[str, dict[str, Any]] - ) -> Nornir: + def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir: """ Initialize Nornir with the provided hosts data. @@ -616,8 +562,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): # Log device types being configured device_types = [ - host["connection_options"]["netmiko"]["extras"]["device_type"] - for host in hosts_data.values() + host["connection_options"]["netmiko"]["extras"]["device_type"] for host in hosts_data.values() ] logger.info( "Initializing Nornir: host=%s, device_types=%s, hosts=%d", @@ -665,16 +610,11 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): device_result = { "device_name": device_name, "status": "failed", - "error": ( - f"Device '{device_name}' not found in topology or " - "missing console_port" - ), + "error": (f"Device '{device_name}' not found in topology or missing console_port"), } # Add blocked commands info if any if blocked_commands_info: - device_result["blocked_commands"] = list( - blocked_commands_info.keys() - ) + device_result["blocked_commands"] = list(blocked_commands_info.keys()) device_result["blocked_info"] = blocked_commands_info results.append(device_result) continue @@ -684,15 +624,11 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): device_result = { "device_name": device_name, "status": "failed", - "error": ( - f"Device '{device_name}' not found in task results" - ), + "error": (f"Device '{device_name}' not found in task results"), } # Add blocked commands info if any if blocked_commands_info: - device_result["blocked_commands"] = list( - blocked_commands_info.keys() - ) + device_result["blocked_commands"] = list(blocked_commands_info.keys()) device_result["blocked_info"] = blocked_commands_info results.append(device_result) continue @@ -704,9 +640,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): if multi_result[0].failed: # Execution failed device_result["status"] = "failed" - device_result["error"] = ( - f"Configuration execution failed: {multi_result[0].result}" - ) + device_result["error"] = f"Configuration execution failed: {multi_result[0].result}" device_result["output"] = multi_result[0].result else: # Execution successful @@ -716,9 +650,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool): # Add blocked commands info if any if blocked_commands_info: - device_result["blocked_commands"] = list( - blocked_commands_info.keys() - ) + device_result["blocked_commands"] = list(blocked_commands_info.keys()) device_result["blocked_info"] = blocked_commands_info # Update status if some commands were blocked but succeeded if device_result["status"] == "success": diff --git a/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py b/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py index 7c4f45622..8c4a5281d 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py @@ -199,18 +199,12 @@ class ExecuteMultipleDeviceCommands(BaseTool): # Validate input device_configs_list, project_id = self._validate_tool_input(tool_input) - if ( - isinstance(device_configs_list, list) - and len(device_configs_list) > 0 - and "error" in device_configs_list[0] - ): + if isinstance(device_configs_list, list) and len(device_configs_list) > 0 and "error" in device_configs_list[0]: return device_configs_list # Filter forbidden commands and store blocked commands info - device_configs_list, blocked_commands_map = ( - self._filter_forbidden_commands_from_device_configs( - device_configs_list - ) + device_configs_list, blocked_commands_map = self._filter_forbidden_commands_from_device_configs( + device_configs_list ) # Create a mapping of device names to their display commands @@ -218,30 +212,17 @@ class ExecuteMultipleDeviceCommands(BaseTool): # Prepare device hosts data try: - hosts_data = self._prepare_device_hosts_data( - device_configs_list, project_id, jwt_token=jwt_token, url=url - ) + hosts_data = self._prepare_device_hosts_data(device_configs_list, project_id, jwt_token=jwt_token, url=url) except ValueError as e: logger.error("Failed to prepare device hosts data: %s", e) return [{"status": "failed", "error": str(e)}] # Check if any devices have errors (e.g., missing device_type tag) - error_devices = { - name: data - for name, data in hosts_data.items() - if "error" in data - } + error_devices = {name: data for name, data in hosts_data.items() if "error" in data} if error_devices: - logger.error( - "Devices with configuration errors: %s", - list(error_devices.keys()) - ) + logger.error("Devices with configuration errors: %s", list(error_devices.keys())) return [ - { - "device_name": name, - "status": "failed", - "error": data["error"] - } + {"device_name": name, "status": "failed", "error": data["error"]} for name, data in error_devices.items() ] @@ -281,17 +262,13 @@ class ExecuteMultipleDeviceCommands(BaseTool): return results - def _run_all_device_configs_with_single_retry( - self, task: Task, device_configs_map: dict[str, list[str]] - ) -> Result: + def _run_all_device_configs_with_single_retry(self, task: Task, device_configs_map: dict[str, list[str]]) -> Result: """Execute READ-ONLY diagnostic commands with single retry.""" device_name = task.host.name diagnostic_commands = device_configs_map.get(device_name, []) if not diagnostic_commands: - return Result( - host=task.host, result="No diagnostic commands to execute" - ) + return Result(host=task.host, result="No diagnostic commands to execute") try: _result = task.run( @@ -366,9 +343,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): parsed_input = json.loads(tool_input) logger.info("Successfully parsed tool input from JSON string.") except json.JSONDecodeError as e: - logger.error( - "Invalid JSON string received as tool input: %s", e - ) + logger.error("Invalid JSON string received as tool input: %s", e) return ( [{"status": "failed", "error": f"Invalid JSON string input from model: {e}"}], None, @@ -412,9 +387,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): # Handle legacy format: [...] elif isinstance(parsed_input, list): - logger.warning( - "Legacy input format without project_id. Use new format." - ) + logger.warning("Legacy input format without project_id. Use new format.") return parsed_input, None else: @@ -435,9 +408,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): Returns: True if valid UUID format, False otherwise """ - uuid_pattern = ( - r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" - ) + uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" return bool(re.match(uuid_pattern, project_id, re.IGNORECASE)) def _filter_forbidden_commands_from_device_configs( @@ -462,9 +433,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): commands = device_config["commands"] # Filter commands - allowed_commands, blocked_info = filter_forbidden_commands( - commands - ) + allowed_commands, blocked_info = filter_forbidden_commands(commands) # Update device config with allowed commands only filtered_config = device_config.copy() @@ -483,9 +452,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): return filtered_list, blocked_commands_map - def _configs_map( - self, device_config_list: list[dict[str, Any]] - ) -> dict[str, list[str]]: + def _configs_map(self, device_config_list: list[dict[str, Any]]) -> dict[str, list[str]]: """Create a mapping of device names to their diagnostic commands.""" device_diagnostic_map = {} for device_config in device_config_list: @@ -504,21 +471,13 @@ class ExecuteMultipleDeviceCommands(BaseTool): ) -> dict[str, dict[str, Any]]: """Prepare device hosts data from topology information.""" # Extract device names list - device_names = [ - device_config["device_name"] - for device_config in device_config_list - ] + device_names = [device_config["device_name"] for device_config in device_config_list] # Get device port information with project_id - hosts_data = get_device_ports_from_topology( - device_names, project_id, jwt_token=jwt_token, url=url - ) + hosts_data = get_device_ports_from_topology(device_names, project_id, jwt_token=jwt_token, url=url) if not hosts_data: - error_msg = ( - f"Failed to get device info from topology. " - f"Project: {project_id}, Devices: {device_names}" - ) + error_msg = f"Failed to get device info from topology. Project: {project_id}, Devices: {device_names}" raise ValueError(error_msg) # Check for missing devices @@ -532,9 +491,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): return hosts_data - def _initialize_nornir( - self, hosts_data: dict[str, dict[str, Any]] - ) -> Nornir: + def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir: """ Initialize Nornir with the provided hosts data. @@ -559,8 +516,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): # Log device types being configured device_types = [ - host["connection_options"]["netmiko"]["extras"]["device_type"] - for host in hosts_data.values() + host["connection_options"]["netmiko"]["extras"]["device_type"] for host in hosts_data.values() ] logger.info( "Initializing Nornir: host=%s, device_types=%s, hosts=%d", @@ -608,16 +564,11 @@ class ExecuteMultipleDeviceCommands(BaseTool): device_result = { "device_name": device_name, "status": "failed", - "error": ( - f"Device '{device_name}' not found in topology " - "or missing console_port" - ), + "error": (f"Device '{device_name}' not found in topology or missing console_port"), } # Add blocked commands info if any if blocked_commands_info: - device_result["blocked_commands"] = list( - blocked_commands_info.keys() - ) + device_result["blocked_commands"] = list(blocked_commands_info.keys()) device_result["blocked_info"] = blocked_commands_info results.append(device_result) continue @@ -627,15 +578,11 @@ class ExecuteMultipleDeviceCommands(BaseTool): device_result = { "device_name": device_name, "status": "failed", - "error": ( - f"Device '{device_name}' not found in task results" - ), + "error": (f"Device '{device_name}' not found in task results"), } # Add blocked commands info if any if blocked_commands_info: - device_result["blocked_commands"] = list( - blocked_commands_info.keys() - ) + device_result["blocked_commands"] = list(blocked_commands_info.keys()) device_result["blocked_info"] = blocked_commands_info results.append(device_result) continue @@ -647,9 +594,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): if multi_result[0].failed: # Execution failed device_result["status"] = "failed" - device_result["error"] = ( - f"Diagnostic command failed: {multi_result[0].result}" - ) + device_result["error"] = f"Diagnostic command failed: {multi_result[0].result}" device_result["output"] = multi_result[0].result else: # Execution successful @@ -659,9 +604,7 @@ class ExecuteMultipleDeviceCommands(BaseTool): # Add blocked commands info if any if blocked_commands_info: - device_result["blocked_commands"] = list( - blocked_commands_info.keys() - ) + device_result["blocked_commands"] = list(blocked_commands_info.keys()) device_result["blocked_info"] = blocked_commands_info # Update status if some commands were blocked but succeeded if device_result["status"] == "success": diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_create_link.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_link.py index 86d28769f..8f926586b 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_create_link.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_link.py @@ -132,9 +132,7 @@ class GNS3LinkTool(BaseTool): if not isinstance(links_data, list) or len(links_data) == 0: logger.error("Invalid links data: must be a non-empty array") - return [ - {"error": "Invalid links data: must be a non-empty array"} - ] + return [{"error": "Invalid links data: must be a non-empty array"}] # Build handler context (JWT + server URL from request context) logger.info("Connecting to GNS3 server...") @@ -142,14 +140,7 @@ class GNS3LinkTool(BaseTool): if gns3_ctx is None: logger.error("Failed to create GNS3 connector") - return [ - { - "error": ( - "Failed to connect to GNS3 server. " - "Please check your configuration." - ) - } - ] + return [{"error": ("Failed to connect to GNS3 server. Please check your configuration.")}] # Fetch all nodes once for port resolution listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) @@ -172,9 +163,7 @@ class GNS3LinkTool(BaseTool): # Validate link parameters if not all([node_id1, port1, node_id2, port2]): - error_msg = ( - f"Missing required fields in link definition {i}" - ) + error_msg = f"Missing required fields in link definition {i}" logger.error(error_msg) created_links.append({"error": error_msg}) continue @@ -218,29 +207,15 @@ class GNS3LinkTool(BaseTool): "nodes": [ { "node_id": node_id1, - "adapter_number": port1_info.get( - "adapter_number", 0 - ), - "port_number": port1_info.get( - "port_number", 0 - ), - "label": { - "text": port1_info.get("short_name") - or port1 - }, + "adapter_number": port1_info.get("adapter_number", 0), + "port_number": port1_info.get("port_number", 0), + "label": {"text": port1_info.get("short_name") or port1}, }, { "node_id": node_id2, - "adapter_number": port2_info.get( - "adapter_number", 0 - ), - "port_number": port2_info.get( - "port_number", 0 - ), - "label": { - "text": port2_info.get("short_name") - or port2 - }, + "adapter_number": port2_info.get("adapter_number", 0), + "port_number": port2_info.get("port_number", 0), + "label": {"text": port2_info.get("short_name") or port2}, }, ], "fields": ["link_id"], @@ -266,9 +241,7 @@ class GNS3LinkTool(BaseTool): created_links.append({"error": error_msg}) # Log final results - success_count = len( - [link for link in created_links if "error" not in link] - ) + success_count = len([link for link in created_links if "error" not in link]) logger.info( "Link creation completed: %d successful, %d failed", success_count, diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_create_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_node.py index e03e42ec4..a511de21f 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_create_node.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_node.py @@ -169,9 +169,7 @@ class GNS3CreateNodeTool(BaseTool): # Validate each node in the array for i, node_data in enumerate(nodes): if not isinstance(node_data, dict): - logger.error( - "Invalid input: Node %d must be a dictionary.", i + 1 - ) + logger.error("Invalid input: Node %d must be a dictionary.", i + 1) return {"error": f"Node {i + 1} must be a dictionary."} template_id = node_data.get("template_id") @@ -187,14 +185,10 @@ class GNS3CreateNodeTool(BaseTool): ] ): logger.error( - "Invalid input: Node %d missing or invalid " - "template_id, x, or y.", + "Invalid input: Node %d missing or invalid template_id, x, or y.", i + 1, ) - return { - "error": f"Node {i + 1} missing or invalid " - f"template_id, x, or y." - } + return {"error": f"Node {i + 1} missing or invalid template_id, x, or y."} # Build handler context (JWT + server URL from request context) logger.info("Connecting to GNS3 server...") @@ -202,15 +196,10 @@ class GNS3CreateNodeTool(BaseTool): if gns3_ctx is None: logger.error("Failed to create GNS3 connector") - return { - "error": "Failed to connect to GNS3 server. " - "Please check your configuration." - } + return {"error": "Failed to connect to GNS3 server. Please check your configuration."} # Create nodes - logger.info( - "Creating %d nodes in project %s...", len(nodes), project_id - ) + logger.info("Creating %d nodes in project %s...", len(nodes), project_id) results: list[dict[str, Any]] = [] for i, node_data in enumerate(nodes): @@ -261,12 +250,8 @@ class GNS3CreateNodeTool(BaseTool): # Continue with next node even if one fails # Calculate summary statistics - successful_nodes = len( - [r for r in results if r.get("status") == "success"] - ) - failed_nodes = len( - [r for r in results if r.get("status") == "failed"] - ) + successful_nodes = len([r for r in results if r.get("status") == "success"]) + failed_nodes = len([r for r in results if r.get("status") == "failed"]) # Prepare final result final_result = { @@ -293,9 +278,7 @@ class GNS3CreateNodeTool(BaseTool): return {"error": f"Invalid JSON input: {e}"} except Exception as e: logger.error("Failed to process node creation request: %s", e) - return { - "error": f"Failed to process node creation request: {str(e)}" - } + return {"error": f"Failed to process node creation request: {str(e)}"} if __name__ == "__main__": diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_get_node_temp.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_get_node_temp.py index e10e6390f..af685ea4b 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_get_node_temp.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_get_node_temp.py @@ -46,12 +46,12 @@ logger = logging.getLogger(__name__) # Built-in templates to filter out (utility templates, not actual network devices) FILTERED_TEMPLATES = { - "atm_switch", # ATM switch - "cloud", # Cloud - "ethernet_hub", # Ethernet hub - "ethernet_switch", # Ethernet switch (built-in) + "atm_switch", # ATM switch + "cloud", # Cloud + "ethernet_hub", # Ethernet hub + "ethernet_switch", # Ethernet switch (built-in) "frame_relay_switch", # Frame Relay switch - "nat", # NAT device + "nat", # NAT device } @@ -131,17 +131,10 @@ class GNS3TemplateTool(BaseTool): if gns3_server is None: logger.error("Failed to create GNS3 connector") - return { - "error": ( - "Failed to connect to GNS3 server. " - "Please check your configuration." - ) - } + return {"error": ("Failed to connect to GNS3 server. Please check your configuration.")} # Retrieve all available templates - templates = gns3_server.http_call( - "get", f"{gns3_server.base_url}/templates" - ).json() + templates = gns3_server.http_call("get", f"{gns3_server.base_url}/templates").json() # Filter out utility templates and extract relevant info template_info = [] @@ -154,11 +147,13 @@ class GNS3TemplateTool(BaseTool): continue # Extract name, template_id, and template_type - template_info.append({ - "name": template.get("name", "N/A"), - "template_id": template.get("template_id", "N/A"), - "template_type": template.get("template_type", "N/A"), - }) + template_info.append( + { + "name": template.get("name", "N/A"), + "template_id": template.get("template_id", "N/A"), + "template_type": template.get("template_type", "N/A"), + } + ) # Return JSON-formatted result with full logging result = {"templates": template_info} @@ -172,9 +167,7 @@ class GNS3TemplateTool(BaseTool): return result except Exception as e: - logger.error( - "Failed to connect to GNS3 server or retrieve templates: %s", e - ) + logger.error("Failed to connect to GNS3 server or retrieve templates: %s", e) return {"error": f"Failed to retrieve templates: {str(e)}"} diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_packet_filter.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_packet_filter.py index 562a80f1b..d94a0fc1d 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_packet_filter.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_packet_filter.py @@ -198,18 +198,14 @@ class GNS3PacketFilterTool(BaseTool): valid_actions = ["get_available", "set", "get", "clear"] if action not in valid_actions: logger.error("Invalid action: %s. Must be one of %s", action, valid_actions) - return { - "error": f"Invalid action: {action}. Must be one of {valid_actions}" - } + return {"error": f"Invalid action: {action}. Must be one of {valid_actions}"} # Validate filters for "set" action if action == "set": filters = input_data.get("filters") if not filters or not isinstance(filters, dict): logger.error("Invalid input: 'set' action requires 'filters' dict.") - return { - "error": "'set' action requires 'filters' dict with filter configuration." - } + return {"error": "'set' action requires 'filters' dict with filter configuration."} # Build handler context (JWT + server URL from request context) logger.info("Connecting to GNS3 server...") @@ -217,28 +213,19 @@ class GNS3PacketFilterTool(BaseTool): if gns3_ctx is None: logger.error("Failed to create GNS3 connector") - return { - "error": "Failed to connect to GNS3 server. " - "Please check your configuration." - } + return {"error": "Failed to connect to GNS3 server. Please check your configuration."} # Execute action - logger.info( - "Processing packet filter action '%s' for link %s...", action, link_id - ) + logger.info("Processing packet filter action '%s' for link %s...", action, link_id) if action == "get_available": result = self._get_available_filters(gns3_ctx, project_id, link_id) elif action == "set": filters = input_data.get("filters", {}) - result = self._set_filters( - gns3_ctx, project_id, link_id, filters, show_filters_icon - ) + result = self._set_filters(gns3_ctx, project_id, link_id, filters, show_filters_icon) elif action == "get": result = self._get_filters(gns3_ctx, project_id, link_id) elif action == "clear": - result = self._clear_filters( - gns3_ctx, project_id, link_id, show_filters_icon - ) + result = self._clear_filters(gns3_ctx, project_id, link_id, show_filters_icon) else: result = {"error": f"Unknown action: {action}"} @@ -252,18 +239,12 @@ class GNS3PacketFilterTool(BaseTool): return {"error": f"Invalid JSON input: {e}"} except Exception as e: logger.error("Failed to process packet filter request: %s", e) - return { - "error": f"Failed to process packet filter request: {str(e)}" - } + return {"error": f"Failed to process packet filter request: {str(e)}"} - def _get_available_filters( - self, gns3_ctx: dict, project_id: str, link_id: str - ) -> dict[str, Any]: + def _get_available_filters(self, gns3_ctx: dict, project_id: str, link_id: str) -> dict[str, Any]: """Get available filter types for the link.""" try: - filters = available_filters_handler( - {"project_id": project_id, "link_id": link_id}, gns3_ctx - ) + filters = available_filters_handler({"project_id": project_id, "link_id": link_id}, gns3_ctx) if "error" in filters: raise RuntimeError(filters["error"]) logger.info("Retrieved %d available filter types.", len(filters)) @@ -307,13 +288,9 @@ class GNS3PacketFilterTool(BaseTool): if "Invalid" in result.stdout or "Invalid" in result.stderr: error_lines = [] if "Invalid" in result.stderr: - error_lines.extend( - line for line in result.stderr.split("\n") if "Invalid" in line - ) + error_lines.extend(line for line in result.stderr.split("\n") if "Invalid" in line) if "Invalid" in result.stdout: - error_lines.extend( - line for line in result.stdout.split("\n") if "Invalid" in line - ) + error_lines.extend(line for line in result.stdout.split("\n") if "Invalid" in line) # Strip interface suffix (e.g., "for interface 'lo'") for cleaner error error_msg_parts = [] @@ -336,10 +313,7 @@ class GNS3PacketFilterTool(BaseTool): except FileNotFoundError: # tshark not installed - skip validation - logger.warning( - "tshark not found, skipping BPF syntax validation. " - "Install tshark to enable BPF validation." - ) + logger.warning("tshark not found, skipping BPF syntax validation. Install tshark to enable BPF validation.") return {"valid": True, "error": None} except Exception as e: @@ -414,14 +388,10 @@ class GNS3PacketFilterTool(BaseTool): "status": "failed", } - def _get_filters( - self, gns3_ctx: dict, project_id: str, link_id: str - ) -> dict[str, Any]: + def _get_filters(self, gns3_ctx: dict, project_id: str, link_id: str) -> dict[str, Any]: """Get current filters configured on the link.""" try: - link = get_link_handler( - {"project_id": project_id, "link_id": link_id}, gns3_ctx - ) + link = get_link_handler({"project_id": project_id, "link_id": link_id}, gns3_ctx) if "error" in link: raise RuntimeError(link["error"]) diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py index e7ae2dd82..8e98c7480 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py @@ -106,13 +106,8 @@ class GNS3StartNodeTool(BaseTool): # Validate input if not project_id or not node_ids: - logger.error( - "Missing required fields: project_id or node_ids." - ) - return { - "error": "Missing required fields: " - "project_id and node_ids." - } + logger.error("Missing required fields: project_id or node_ids.") + return {"error": "Missing required fields: project_id and node_ids."} if not isinstance(node_ids, list): logger.error("node_ids must be a list.") @@ -124,10 +119,7 @@ class GNS3StartNodeTool(BaseTool): if gns3_ctx is None: logger.error("Failed to create GNS3 connector") - return { - "error": "Failed to connect to GNS3 server. " - "Please check your configuration." - } + return {"error": "Failed to connect to GNS3 server. Please check your configuration."} # Verify nodes exist and capture pre-start info (one call) listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) @@ -143,14 +135,8 @@ class GNS3StartNodeTool(BaseTool): ) results = [] known_ids = [nid for nid in node_ids if nid in nodes_by_id] - start_results = start_node_handler( - {"project_id": project_id, "node_ids": known_ids}, gns3_ctx - ) - start_errors = { - r["node_id"]: r.get("error") - for r in start_results - if r.get("status") == "error" - } + start_results = start_node_handler({"project_id": project_id, "node_ids": known_ids}, gns3_ctx) + start_errors = {r["node_id"]: r.get("error") for r in start_results if r.get("status") == "error"} # Get immediate status (likely 'starting' or 'stopped') — one call listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) @@ -160,9 +146,7 @@ class GNS3StartNodeTool(BaseTool): for node_id in node_ids: if node_id not in nodes_by_id: - logger.error( - "Node %s not found in project %s", node_id, project_id - ) + logger.error("Node %s not found in project %s", node_id, project_id) results.append( { "node_id": node_id, @@ -204,9 +188,7 @@ class GNS3StartNodeTool(BaseTool): ) # Analyze results (count based on successful command sending) - successful_nodes = [ - r for r in results if r.get("status") != "error" - ] + successful_nodes = [r for r in results if r.get("status") != "error"] failed_nodes = [r for r in results if r.get("status") == "error"] # Construct final response @@ -217,8 +199,7 @@ class GNS3StartNodeTool(BaseTool): "failed": len(failed_nodes), "nodes": results, "note": ( - "Start commands sent. Nodes are booting in background. " - "Use wait_seconds, then check node status." + "Start commands sent. Nodes are booting in background. Use wait_seconds, then check node status." ), } @@ -249,9 +230,7 @@ if __name__ == "__main__": test_input_single = json.dumps( { "project_id": "", # Replace with actual project UUID - "node_ids": [ - "fbeda109-9a74-4d8c-a749-cc3847911a90" - ], # Replace with actual node UUID + "node_ids": ["fbeda109-9a74-4d8c-a749-cc3847911a90"], # Replace with actual node UUID } ) tool = GNS3StartNodeTool() diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py index a6cdfc2a2..3c0d8ef2c 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py @@ -94,13 +94,8 @@ class GNS3StopNodeTool(BaseTool): # Validate input if not project_id or not node_ids: - logger.error( - "Missing required fields: project_id or node_ids." - ) - return { - "error": "Missing required fields: " - "project_id and node_ids." - } + logger.error("Missing required fields: project_id or node_ids.") + return {"error": "Missing required fields: project_id and node_ids."} if not isinstance(node_ids, list): logger.error("node_ids must be a list.") @@ -112,10 +107,7 @@ class GNS3StopNodeTool(BaseTool): if gns3_ctx is None: logger.error("Failed to create GNS3 connector") - return { - "error": "Failed to connect to GNS3 server. " - "Please check your configuration." - } + return {"error": "Failed to connect to GNS3 server. Please check your configuration."} # Verify nodes exist and capture names (one call) listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) @@ -131,14 +123,8 @@ class GNS3StopNodeTool(BaseTool): ) results = [] known_ids = [nid for nid in node_ids if nid in nodes_by_id] - stop_results = stop_node_handler( - {"project_id": project_id, "node_ids": known_ids}, gns3_ctx - ) - stop_errors = { - r["node_id"]: r.get("error") - for r in stop_results - if r.get("status") == "error" - } + stop_results = stop_node_handler({"project_id": project_id, "node_ids": known_ids}, gns3_ctx) + stop_errors = {r["node_id"]: r.get("error") for r in stop_results if r.get("status") == "error"} # Get updated status — one call listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) @@ -148,9 +134,7 @@ class GNS3StopNodeTool(BaseTool): for node_id in node_ids: if node_id not in nodes_by_id: - logger.error( - "Node %s not found in project %s", node_id, project_id - ) + logger.error("Node %s not found in project %s", node_id, project_id) results.append( { "node_id": node_id, @@ -160,9 +144,7 @@ class GNS3StopNodeTool(BaseTool): } ) elif node_id in stop_errors: - logger.error( - "Failed to stop node %s: %s", node_id, stop_errors[node_id] - ) + logger.error("Failed to stop node %s: %s", node_id, stop_errors[node_id]) results.append( { "node_id": node_id, @@ -187,9 +169,7 @@ class GNS3StopNodeTool(BaseTool): ) # Analyze results - successful_nodes = [ - r for r in results if r.get("status") != "error" - ] + successful_nodes = [r for r in results if r.get("status") != "error"] failed_nodes = [r for r in results if r.get("status") == "error"] # Construct final response @@ -223,9 +203,7 @@ if __name__ == "__main__": test_input_single = json.dumps( { "project_id": "", # Replace with actual project UUID - "node_ids": [ - "fbeda109-9a74-4d8c-a749-cc3847911a90" - ], # Replace with actual node UUID + "node_ids": ["fbeda109-9a74-4d8c-a749-cc3847911a90"], # Replace with actual node UUID } ) tool = GNS3StopNodeTool() diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py index c7d28025e..74f9ba883 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py @@ -100,13 +100,8 @@ class GNS3SuspendNodeTool(BaseTool): # Validate input if not project_id or not node_ids: - logger.error( - "Missing required fields: project_id or node_ids." - ) - return { - "error": "Missing required fields: " - "project_id and node_ids." - } + logger.error("Missing required fields: project_id or node_ids.") + return {"error": "Missing required fields: project_id and node_ids."} if not isinstance(node_ids, list): logger.error("node_ids must be a list.") @@ -118,10 +113,7 @@ class GNS3SuspendNodeTool(BaseTool): if gns3_ctx is None: logger.error("Failed to create GNS3 connector") - return { - "error": "Failed to connect to GNS3 server. " - "Please check your configuration." - } + return {"error": "Failed to connect to GNS3 server. Please check your configuration."} # Verify nodes exist and capture names (one call) listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) @@ -137,14 +129,8 @@ class GNS3SuspendNodeTool(BaseTool): ) results = [] known_ids = [nid for nid in node_ids if nid in nodes_by_id] - suspend_results = suspend_node_handler( - {"project_id": project_id, "node_ids": known_ids}, gns3_ctx - ) - suspend_errors = { - r["node_id"]: r.get("error") - for r in suspend_results - if r.get("status") == "error" - } + suspend_results = suspend_node_handler({"project_id": project_id, "node_ids": known_ids}, gns3_ctx) + suspend_errors = {r["node_id"]: r.get("error") for r in suspend_results if r.get("status") == "error"} # Get updated status — one call listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) @@ -154,9 +140,7 @@ class GNS3SuspendNodeTool(BaseTool): for node_id in node_ids: if node_id not in nodes_by_id: - logger.error( - "Node %s not found in project %s", node_id, project_id - ) + logger.error("Node %s not found in project %s", node_id, project_id) results.append( { "node_id": node_id, @@ -195,9 +179,7 @@ class GNS3SuspendNodeTool(BaseTool): ) # Analyze results - successful_nodes = [ - r for r in results if r.get("status") != "error" - ] + successful_nodes = [r for r in results if r.get("status") != "error"] failed_nodes = [r for r in results if r.get("status") == "error"] # Construct final response @@ -207,10 +189,7 @@ class GNS3SuspendNodeTool(BaseTool): "successful": len(successful_nodes), "failed": len(failed_nodes), "nodes": results, - "note": ( - "Suspended nodes preserve their state in memory. " - "Use resume to continue where you left off." - ), + "note": ("Suspended nodes preserve their state in memory. Use resume to continue where you left off."), } logger.info( @@ -235,9 +214,7 @@ if __name__ == "__main__": test_input_single = json.dumps( { "project_id": "", # Replace with actual project UUID - "node_ids": [ - "fbeda109-9a74-4d8c-a749-cc3847911a90" - ], # Replace with actual node UUID + "node_ids": ["fbeda109-9a74-4d8c-a749-cc3847911a90"], # Replace with actual node UUID } ) tool = GNS3SuspendNodeTool() diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_update_node_name.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_update_node_name.py index f6b00171b..7756b33a2 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_update_node_name.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_update_node_name.py @@ -125,9 +125,7 @@ class GNS3UpdateNodeNameTool(BaseTool): return {"error": f"Node {i + 1} must be a dictionary."} if "node_id" not in node_data or "new_name" not in node_data: logger.error("Node %d missing node_id or new_name.", i + 1) - return { - "error": f"Node {i + 1} missing node_id or new_name." - } + return {"error": f"Node {i + 1} missing node_id or new_name."} # Build handler context (JWT + server URL from request context) logger.info("Connecting to GNS3 server...") @@ -135,10 +133,7 @@ class GNS3UpdateNodeNameTool(BaseTool): if gns3_ctx is None: logger.error("Failed to create GNS3 connector") - return { - "error": "Failed to connect to GNS3 server. " - "Please check your configuration." - } + return {"error": "Failed to connect to GNS3 server. Please check your configuration."} # Fetch current node names in one call (old names + existence) listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) @@ -208,9 +203,7 @@ class GNS3UpdateNodeNameTool(BaseTool): "error": "Name verification failed", } results.append(error_info) - logger.error( - "Failed to update node name for %s", node_id - ) + logger.error("Failed to update node name for %s", node_id) except Exception as e: error_info = { @@ -223,9 +216,7 @@ class GNS3UpdateNodeNameTool(BaseTool): logger.error("Failed to update node %d: %s", i + 1, e) # Analyze results - successful_nodes = [ - r for r in results if r.get("status") == "success" - ] + successful_nodes = [r for r in results if r.get("status") == "success"] failed_nodes = [r for r in results if r.get("status") == "failed"] # Construct final response diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_wait.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_wait.py index d31e9e6d2..15a9ed24e 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_wait.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_wait.py @@ -88,9 +88,7 @@ class GNS3WaitTool(BaseTool): if not isinstance(seconds, int) or isinstance(seconds, bool): return {"error": "seconds must be an integer (1-600)."} if not 1 <= seconds <= MAX_WAIT_SECONDS: - return { - "error": f"seconds must be between 1 and {MAX_WAIT_SECONDS}." - } + return {"error": f"seconds must be between 1 and {MAX_WAIT_SECONDS}."} logger.info("Waiting %d seconds...", seconds) waited = 0 diff --git a/gns3server/agent/gns3_copilot/tools_v2/packet_analysis_tool.py b/gns3server/agent/gns3_copilot/tools_v2/packet_analysis_tool.py index 233b24e56..b9c35d141 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/packet_analysis_tool.py +++ b/gns3server/agent/gns3_copilot/tools_v2/packet_analysis_tool.py @@ -123,19 +123,25 @@ class PacketAnalysisTool(BaseTool): query = query.strip() if len(query.split()) > 1: - return json.dumps({ - "error": "Only one keyword allowed", - "hint": 'Use a single keyword like "ospf.lsa" or "bgp", not multiple words', - }) + return json.dumps( + { + "error": "Only one keyword allowed", + "hint": 'Use a single keyword like "ospf.lsa" or "bgp", not multiple words', + } + ) tshark = subprocess.Popen( ["tshark", "-G", "fields"], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, ) grep = subprocess.Popen( ["grep", "-iF", query, "-"], - stdin=tshark.stdout, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, text=True, + stdin=tshark.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, ) tshark.stdout.close() stdout, _ = grep.communicate(timeout=30) @@ -151,21 +157,27 @@ class PacketAnalysisTool(BaseTool): name = parts[2] if name not in seen: seen.add(name) - results.append({ - "field": name, - "type": parts[3] if len(parts) > 3 else "", - "protocol": parts[4] if len(parts) > 4 else "", - "description": parts[7] if len(parts) > 7 else "", - }) + results.append( + { + "field": name, + "type": parts[3] if len(parts) > 3 else "", + "protocol": parts[4] if len(parts) > 4 else "", + "description": parts[7] if len(parts) > 7 else "", + } + ) if len(results) >= 40: break - return json.dumps({ - "query": query, - "count": len(results), - "fields": results, - "hint": "Use the 'field' values in tshark_args with -e", - }, ensure_ascii=False, indent=2) + return json.dumps( + { + "query": query, + "count": len(results), + "fields": results, + "hint": "Use the 'field' values in tshark_args with -e", + }, + ensure_ascii=False, + indent=2, + ) @classmethod def _load_valid_tshark_fields(cls) -> set: @@ -235,11 +247,13 @@ class PacketAnalysisTool(BaseTool): i += 1 if invalid_fields: - return json.dumps({ - "error": f"Invalid tshark field names: {', '.join(invalid_fields)}", - "hint": 'Use {"action": "search_fields", "query": ""} to look up valid field names', - "invalid_fields": invalid_fields, - }) + return json.dumps( + { + "error": f"Invalid tshark field names: {', '.join(invalid_fields)}", + "hint": 'Use {"action": "search_fields", "query": ""} to look up valid field names', + "invalid_fields": invalid_fields, + } + ) return None @@ -271,8 +285,7 @@ class PacketAnalysisTool(BaseTool): return self._search_fields(query or "") logger.info( - f"PacketAnalysisTool invoked: project_id={project_id}, " - f"link_id={link_id}, tshark_args={tshark_args}" + f"PacketAnalysisTool invoked: project_id={project_id}, link_id={link_id}, tshark_args={tshark_args}" ) # Validate inputs @@ -429,6 +442,7 @@ class PacketAnalysisTool(BaseTool): """ # Build command: tshark -r import shlex + cmd = ["tshark", "-r", pcap_file] + shlex.split(tshark_args) logger.info(f"Running tshark: {' '.join(cmd)}") @@ -451,10 +465,7 @@ class PacketAnalysisTool(BaseTool): f"tshark reported: {stderr_clean}", ] if "is not a valid protocol" in stderr.lower() or "aren't valid" in stderr.lower(): - hints.append( - 'Use {"action": "search_fields", "query": ""} ' - "to find correct field names." - ) + hints.append('Use {"action": "search_fields", "query": ""} to find correct field names.') logger.warning(f"tshark stderr: {stderr}") return json.dumps({"error": "tshark argument error", "hints": hints}) diff --git a/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py b/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py index 8339baefc..3a8789cda 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py +++ b/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py @@ -179,11 +179,7 @@ class VPCSCommands(BaseTool): # Validate input device_configs_list, project_id = self._validate_tool_input(tool_input) - if ( - isinstance(device_configs_list, list) - and len(device_configs_list) > 0 - and "error" in device_configs_list[0] - ): + if isinstance(device_configs_list, list) and len(device_configs_list) > 0 and "error" in device_configs_list[0]: return device_configs_list # Create a mapping of device names to their commands @@ -191,30 +187,17 @@ class VPCSCommands(BaseTool): # Prepare device hosts data try: - hosts_data = self._prepare_device_hosts_data( - device_configs_list, project_id, jwt_token=jwt_token, url=url - ) + hosts_data = self._prepare_device_hosts_data(device_configs_list, project_id, jwt_token=jwt_token, url=url) except ValueError as e: logger.error("Failed to prepare device hosts data: %s", e) return [{"status": "failed", "error": str(e)}] # Check if any devices have errors (e.g., missing device) - error_devices = { - name: data - for name, data in hosts_data.items() - if "error" in data - } + error_devices = {name: data for name, data in hosts_data.items() if "error" in data} if error_devices: - logger.error( - "Devices with configuration errors: %s", - list(error_devices.keys()) - ) + logger.error("Devices with configuration errors: %s", list(error_devices.keys())) return [ - { - "device_name": name, - "status": "failed", - "error": data["error"] - } + {"device_name": name, "status": "failed", "error": data["error"]} for name, data in error_devices.items() ] @@ -253,17 +236,13 @@ class VPCSCommands(BaseTool): return results - def _run_vpcs_commands( - self, task: Task, device_configs_map: dict[str, list[str]] - ) -> Result: + def _run_vpcs_commands(self, task: Task, device_configs_map: dict[str, list[str]]) -> Result: """Execute VPCS commands with single retry.""" device_name = task.host.name commands = device_configs_map.get(device_name, []) if not commands: - return Result( - host=task.host, result="No commands to execute" - ) + return Result(host=task.host, result="No commands to execute") try: # Use netmiko_multiline for VPCS commands @@ -334,9 +313,7 @@ class VPCSCommands(BaseTool): parsed_input = json.loads(tool_input) logger.info("Successfully parsed tool input from JSON string.") except json.JSONDecodeError as e: - logger.error( - "Invalid JSON string received as tool input: %s", e - ) + logger.error("Invalid JSON string received as tool input: %s", e) return ( [{"status": "failed", "error": f"Invalid JSON string input from model: {e}"}], None, @@ -377,10 +354,7 @@ class VPCSCommands(BaseTool): return device_configs, project_id else: - error_msg = ( - "Tool input must be JSON with project_id and device_configs, " - f"got {type(parsed_input).__name__}" - ) + error_msg = f"Tool input must be JSON with project_id and device_configs, got {type(parsed_input).__name__}" logger.error(error_msg) return ([{"status": "failed", "error": error_msg}], None) @@ -394,14 +368,10 @@ class VPCSCommands(BaseTool): Returns: True if valid UUID format, False otherwise """ - uuid_pattern = ( - r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" - ) + uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" return bool(re.match(uuid_pattern, project_id, re.IGNORECASE)) - def _configs_map( - self, device_config_list: list[dict[str, Any]] - ) -> dict[str, list[str]]: + def _configs_map(self, device_config_list: list[dict[str, Any]]) -> dict[str, list[str]]: """ Create a mapping of device names to their command lists. @@ -411,10 +381,7 @@ class VPCSCommands(BaseTool): Returns: Dictionary mapping device names to command lists """ - return { - config["device_name"]: config["commands"] - for config in device_config_list - } + return {config["device_name"]: config["commands"] for config in device_config_list} def _prepare_device_hosts_data( self, @@ -445,18 +412,14 @@ class VPCSCommands(BaseTool): device_names = [config["device_name"] for config in device_configs_list] # Get device port mappings from topology - device_ports = get_device_ports_from_topology( - device_names, project_id=project_id, jwt_token=jwt_token, url=url - ) + device_ports = get_device_ports_from_topology(device_names, project_id=project_id, jwt_token=jwt_token, url=url) # Build Nornir inventory hosts data hosts_data = {} for device_name in device_names: if device_name not in device_ports: logger.error("Device '%s' not found in topology", device_name) - hosts_data[device_name] = { - "error": f"Device '{device_name}' not found in topology" - } + hosts_data[device_name] = {"error": f"Device '{device_name}' not found in topology"} continue port = device_ports[device_name]["port"] @@ -498,9 +461,7 @@ class VPCSCommands(BaseTool): return hosts_data - def _initialize_nornir( - self, hosts_data: dict[str, dict[str, Any]] - ) -> "Nornir": + def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> "Nornir": """ Initialize Nornir with VPCS device inventory. @@ -576,12 +537,14 @@ class VPCSCommands(BaseTool): # Check if device had an error during preparation if device_name in hosts_data and "error" in hosts_data[device_name]: - results.append({ - "device_name": device_name, - "status": "failed", - "error": hosts_data[device_name]["error"], - "commands": device_config["commands"], - }) + results.append( + { + "device_name": device_name, + "status": "failed", + "error": hosts_data[device_name]["error"], + "commands": device_config["commands"], + } + ) continue # Get result from Nornir task @@ -591,28 +554,34 @@ class VPCSCommands(BaseTool): if host_result.failed: # Task failed error_msg = str(host_result.result) if host_result.result else "Unknown error" - results.append({ - "device_name": device_name, - "status": "failed", - "error": error_msg, - "commands": device_config["commands"], - }) + results.append( + { + "device_name": device_name, + "status": "failed", + "error": error_msg, + "commands": device_config["commands"], + } + ) else: # Task succeeded - results.append({ - "device_name": device_name, - "status": "success", - "output": host_result.result, - "commands": device_config["commands"], - }) + results.append( + { + "device_name": device_name, + "status": "success", + "output": host_result.result, + "commands": device_config["commands"], + } + ) else: # Device not in task result (shouldn't happen) - results.append({ - "device_name": device_name, - "status": "failed", - "error": f"Device '{device_name}' not in task results", - "commands": device_config["commands"], - }) + results.append( + { + "device_name": device_name, + "status": "failed", + "error": f"Device '{device_name}' not in task results", + "commands": device_config["commands"], + } + ) return results diff --git a/gns3server/agent/gns3_copilot/utils/command_filter.py b/gns3server/agent/gns3_copilot/utils/command_filter.py index deb5e7c5f..78ed176bd 100644 --- a/gns3server/agent/gns3_copilot/utils/command_filter.py +++ b/gns3server/agent/gns3_copilot/utils/command_filter.py @@ -73,14 +73,14 @@ def _load_forbidden_commands() -> list[str]: # Try to load from skills repository manager = get_skills_manager() if manager is not None: - commands = manager.load_forbidden_commands() - if commands: - logger.info( - "Loaded %d forbidden command patterns from skills repository", - len(commands), - ) - _forbidden_commands_cache = commands - return _forbidden_commands_cache + commands = manager.load_forbidden_commands() + if commands: + logger.info( + "Loaded %d forbidden command patterns from skills repository", + len(commands), + ) + _forbidden_commands_cache = commands + return _forbidden_commands_cache # Fallback to defaults logger.warning("Using default forbidden commands list") diff --git a/gns3server/agent/gns3_copilot/utils/custom_netmiko/README.md b/gns3server/agent/gns3_copilot/utils/custom_netmiko/README.md index 2eea98e5c..f36ed12e9 100644 --- a/gns3server/agent/gns3_copilot/utils/custom_netmiko/README.md +++ b/gns3server/agent/gns3_copilot/utils/custom_netmiko/README.md @@ -69,11 +69,9 @@ The drivers are auto-registered when the tools are imported: from gns3server.agent.gns3_copilot.tools_v2 import DisplayToolNornir tool = DisplayToolNornir() -result = tool._run(json.dumps({ - "device_names": ["huawei-sw1"], - "commands": ["display version"], - "project_id": "project-uuid" -})) +result = tool._run( + json.dumps({"device_names": ["huawei-sw1"], "commands": ["display version"], "project_id": "project-uuid"}) +) ``` ## Running Tests @@ -145,11 +143,14 @@ Create `tests/test_cisco.py`: import unittest from gns3server.agent.gns3_copilot.utils.custom_netmiko import cisco + class TestCustomCiscoDriver(unittest.TestCase): def test_device_type_registered(self): from netmiko.ssh_dispatcher import CLASS_MAPPER + self.assertIn("cisco_custom", CLASS_MAPPER) + if __name__ == "__main__": unittest.main() ``` diff --git a/gns3server/agent/gns3_copilot/utils/custom_netmiko/huawei_ce.py b/gns3server/agent/gns3_copilot/utils/custom_netmiko/huawei_ce.py index 99f2f4220..68080117e 100644 --- a/gns3server/agent/gns3_copilot/utils/custom_netmiko/huawei_ce.py +++ b/gns3server/agent/gns3_copilot/utils/custom_netmiko/huawei_ce.py @@ -209,7 +209,7 @@ class GNS3HuaweiTelnetCE(HuaweiBase): # Ensure we're in a clean state try: - if hasattr(self, 'base_prompt') and self.base_prompt: + if hasattr(self, "base_prompt") and self.base_prompt: self._test_channel_read(pattern=self.base_prompt) else: # If base_prompt is not set yet, just read to clear buffer @@ -288,9 +288,7 @@ class GNS3HuaweiTelnetCE(HuaweiBase): # This method keeps reading until there is no new data for # 'last_read' seconds. This is the proper Netmiko way to handle # command output - output += self.read_channel_timing( - read_timeout=read_timeout, last_read=2.0 - ) + output += self.read_channel_timing(read_timeout=read_timeout, last_read=2.0) # Exit config mode if requested if exit_config_mode: @@ -318,9 +316,7 @@ class GNS3HuaweiTelnetCE(HuaweiBase): return output - def exit_config_mode( - self, exit_config: str = "return", pattern: str = r"<\S+>|>\s*$" - ) -> str: + def exit_config_mode(self, exit_config: str = "return", pattern: str = r"<\S+>|>\s*$") -> str: r""" Exit configuration mode for Huawei devices. @@ -377,6 +373,7 @@ class GNS3HuaweiTelnetCE(HuaweiBase): # Register the custom device type with Netmiko _registered = False # Flag to prevent duplicate registration + def register_custom_device_type() -> None: """ Register the custom GNS3HuaweiTelnetCE device type with Netmiko. @@ -451,7 +448,4 @@ try: except Exception as e: # Log but don't fail on import logger = logging.getLogger(__name__) - logger.warning( - f"Failed to register custom device type: {e}", - exc_info=True - ) + logger.warning(f"Failed to register custom device type: {e}", exc_info=True) diff --git a/gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py b/gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py index 108eb02b9..00a70eec6 100644 --- a/gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py +++ b/gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py @@ -60,12 +60,12 @@ class RuijieTelnetEnhanced(RuijieOSBase): # Interactive command patterns that trigger [yes/no] prompts # These are commands that commonly require confirmation INTERACTIVE_PATTERNS = [ - re.compile(r'^router-id\s+', re.IGNORECASE), # OSPF router-id - re.compile(r'^erase\s+', re.IGNORECASE), # erase startup-config - re.compile(r'^delete\s+', re.IGNORECASE), # delete files - re.compile(r'^format\s+', re.IGNORECASE), # format filesystem - re.compile(r'^reload\b', re.IGNORECASE), # reload/reboot - re.compile(r'^boot\s+system\s+', re.IGNORECASE), # change boot image + re.compile(r"^router-id\s+", re.IGNORECASE), # OSPF router-id + re.compile(r"^erase\s+", re.IGNORECASE), # erase startup-config + re.compile(r"^delete\s+", re.IGNORECASE), # delete files + re.compile(r"^format\s+", re.IGNORECASE), # format filesystem + re.compile(r"^reload\b", re.IGNORECASE), # reload/reboot + re.compile(r"^boot\s+system\s+", re.IGNORECASE), # change boot image ] def __init__( @@ -80,9 +80,7 @@ class RuijieTelnetEnhanced(RuijieOSBase): kwargs["default_enter"] = "\r\n" super().__init__(*args, **kwargs) - def _preprocess_interactive_commands( - self, config_commands: list[str] - ) -> list[str]: + def _preprocess_interactive_commands(self, config_commands: list[str]) -> list[str]: """ Preprocess commands to insert 'yes' after interactive commands. @@ -105,8 +103,7 @@ class RuijieTelnetEnhanced(RuijieOSBase): for pattern in self.INTERACTIVE_PATTERNS: if pattern.match(cmd.strip()): logging.info( - "Ruijie device: Detected interactive command '%s', " - "inserting 'yes'", + "Ruijie device: Detected interactive command '%s', inserting 'yes'", cmd.strip(), ) # Insert 'yes' after this command @@ -153,25 +150,18 @@ class RuijieTelnetEnhanced(RuijieOSBase): output += self.config_mode() # Preprocess: Insert 'yes' after known interactive commands - processed_commands = self._preprocess_interactive_commands( - config_commands - ) + processed_commands = self._preprocess_interactive_commands(config_commands) # Try batch send first (fast path) try: - output += self._send_config_batch( - processed_commands, read_timeout, delay_factor - ) + output += self._send_config_batch(processed_commands, read_timeout, delay_factor) except Exception as batch_error: logging.warning( - "Ruijie device: Batch send failed, " - "falling back to one-by-one: %s", + "Ruijie device: Batch send failed, falling back to one-by-one: %s", batch_error, ) # Fallback to one-by-one send with real-time detection - output += self._send_config_one_by_one( - config_commands, read_timeout, delay_factor - ) + output += self._send_config_one_by_one(config_commands, read_timeout, delay_factor) # Exit config mode if requested if exit_config_mode: @@ -179,9 +169,7 @@ class RuijieTelnetEnhanced(RuijieOSBase): return output - def _send_config_batch( - self, commands: list[str], read_timeout: int, delay_factor: float - ) -> str: + def _send_config_batch(self, commands: list[str], read_timeout: int, delay_factor: float) -> str: """ Send configuration commands in batch (fast). @@ -197,15 +185,11 @@ class RuijieTelnetEnhanced(RuijieOSBase): # Read all output at once # Use same default as Netmiko (2.0 seconds) to ensure complete output - output += self.read_channel_timing( - read_timeout=read_timeout, last_read=2.0 - ) + output += self.read_channel_timing(read_timeout=read_timeout, last_read=2.0) return output - def _send_config_one_by_one( - self, commands: list[str], read_timeout: int, delay_factor: float - ) -> str: + def _send_config_one_by_one(self, commands: list[str], read_timeout: int, delay_factor: float) -> str: """ Send commands one-by-one with real-time prompt detection. @@ -225,26 +209,21 @@ class RuijieTelnetEnhanced(RuijieOSBase): time.sleep(0.05) # Read output after this command - new_output = self.read_channel_timing( - read_timeout=10, last_read=0.5 - ) + new_output = self.read_channel_timing(read_timeout=10, last_read=0.5) output += new_output # Check if interactive prompt appeared after this command for pattern in interactive_patterns: if re.search(pattern, new_output, re.IGNORECASE): logging.info( - "Ruijie device: Detected interactive prompt " - "after '%s', sending 'yes'", + "Ruijie device: Detected interactive prompt after '%s', sending 'yes'", cmd, ) # Send 'yes' to confirm self.write_channel(f"yes{self.RETURN}") time.sleep(0.3) # Read the confirmation response - output += self.read_channel_timing( - read_timeout=30, last_read=0.5 - ) + output += self.read_channel_timing(read_timeout=30, last_read=0.5) break return output @@ -253,6 +232,7 @@ class RuijieTelnetEnhanced(RuijieOSBase): # Register the custom device type with Netmiko _registered = False # Flag to prevent duplicate registration + def register_custom_device_type() -> None: """ Register the custom RuijieTelnetEnhanced device type with Netmiko. @@ -295,7 +275,4 @@ try: register_custom_device_type() except Exception as e: logger = logging.getLogger(__name__) - logger.warning( - f"Failed to register Ruijie device type: {e}", - exc_info=True - ) + logger.warning(f"Failed to register Ruijie device type: {e}", exc_info=True) diff --git a/gns3server/agent/gns3_copilot/utils/custom_netmiko/scripts/list_netmiko_telnet_devices.py b/gns3server/agent/gns3_copilot/utils/custom_netmiko/scripts/list_netmiko_telnet_devices.py index d9edd8c34..3e951f10a 100644 --- a/gns3server/agent/gns3_copilot/utils/custom_netmiko/scripts/list_netmiko_telnet_devices.py +++ b/gns3server/agent/gns3_copilot/utils/custom_netmiko/scripts/list_netmiko_telnet_devices.py @@ -39,6 +39,7 @@ def get_netmiko_version() -> str: """Get Netmiko library version.""" try: import netmiko + return netmiko.__version__ except Exception: return "Unknown" @@ -68,47 +69,39 @@ def register_custom_drivers() -> set: try: # Import and register Huawei CE driver - from gns3server.agent.gns3_copilot.utils.custom_netmiko \ - import huawei_ce + from gns3server.agent.gns3_copilot.utils.custom_netmiko import huawei_ce # Store custom device types before registration custom_devices.add("gns3_huawei_telnet_ce") # The driver auto-registers on import, but we can call it # explicitly - if hasattr(huawei_ce, 'register_custom_device_type'): + if hasattr(huawei_ce, "register_custom_device_type"): huawei_ce.register_custom_device_type() except ImportError: # Silently skip if custom drivers are not available pass except Exception as e: # Log but don't fail - print( - f"Warning: Failed to register Huawei CE driver: {e}", - file=sys.stderr - ) + print(f"Warning: Failed to register Huawei CE driver: {e}", file=sys.stderr) try: # Import and register Ruijie Telnet driver - from gns3server.agent.gns3_copilot.utils.custom_netmiko \ - import ruijie_telnet + from gns3server.agent.gns3_copilot.utils.custom_netmiko import ruijie_telnet # Store custom device types before registration custom_devices.add("gns3_ruijie_telnet") # The driver auto-registers on import, but we can call it # explicitly - if hasattr(ruijie_telnet, 'register_custom_device_type'): + if hasattr(ruijie_telnet, "register_custom_device_type"): ruijie_telnet.register_custom_device_type() except ImportError: # Silently skip if custom drivers are not available pass except Exception as e: # Log but don't fail - print( - f"Warning: Failed to register Ruijie driver: {e}", - file=sys.stderr - ) + print(f"Warning: Failed to register Ruijie driver: {e}", file=sys.stderr) return custom_devices @@ -124,32 +117,32 @@ def extract_brand_name(device_type: str) -> str: Brand name in a standardized format """ # Remove common suffixes - name = device_type.replace('_telnet', '').replace('_ssh', '') - name = name.replace('_serial', '') + name = device_type.replace("_telnet", "").replace("_ssh", "") + name = name.replace("_serial", "") # Map to standard brand names brand_mapping = { - 'cisco': 'Cisco', - 'huawei': 'Huawei', - 'juniper': 'Juniper', - 'arista': 'Arista', - 'hp': 'HP', - 'aruba': 'Aruba', - 'dell': 'Dell', - 'brocade': 'Brocade', - 'extreme': 'Extreme', - 'ruckus': 'Ruckus', - 'ruijie': 'Ruijie (锐捷)', - 'zte': 'ZTE (中兴)', - 'maipu': 'Maipu (迈普)', - 'h3c': 'H3C (华三)', - 'nokia': 'Nokia', - 'paloalto': 'Palo Alto', - 'f5': 'F5', - 'checkpoint': 'Check Point', - 'generic': 'Generic', - 'huaweiyt': 'HuaweiYT', - 'vsz': 'Ruckus', + "cisco": "Cisco", + "huawei": "Huawei", + "juniper": "Juniper", + "arista": "Arista", + "hp": "HP", + "aruba": "Aruba", + "dell": "Dell", + "brocade": "Brocade", + "extreme": "Extreme", + "ruckus": "Ruckus", + "ruijie": "Ruijie (锐捷)", + "zte": "ZTE (中兴)", + "maipu": "Maipu (迈普)", + "h3c": "H3C (华三)", + "nokia": "Nokia", + "paloalto": "Palo Alto", + "f5": "F5", + "checkpoint": "Check Point", + "generic": "Generic", + "huaweiyt": "HuaweiYT", + "vsz": "Ruckus", } # Check for exact matches first @@ -163,7 +156,7 @@ def extract_brand_name(device_type: str) -> str: return value # Return capitalized version of the first part - return name.split('_')[0].capitalize() + return name.split("_")[0].capitalize() def group_devices_by_brand(device_types: List[str]) -> Dict[str, List[str]]: @@ -205,8 +198,8 @@ def get_devices_by_protocol() -> Tuple[List[str], List[str]]: for device_type in CLASS_MAPPER.keys(): device_lower = device_type.lower() - has_telnet = 'telnet' in device_lower - has_ssh = 'ssh' in device_lower + has_telnet = "telnet" in device_lower + has_ssh = "ssh" in device_lower if has_telnet and has_ssh: both_protocols.append(device_type) @@ -230,17 +223,14 @@ def get_total_device_count() -> int: """Get total number of device types in Netmiko.""" try: from netmiko.ssh_dispatcher import CLASS_MAPPER + return len(CLASS_MAPPER) except ImportError: return 0 def generate_markdown( - version: str, - ssh_devices: List[str], - telnet_devices: List[str], - total_count: int, - custom_devices: set + version: str, ssh_devices: List[str], telnet_devices: List[str], total_count: int, custom_devices: set ) -> str: """ Generate Markdown documentation with table format. @@ -261,7 +251,7 @@ def generate_markdown( md_lines.append("# Netmiko Supported Devices") md_lines.append("") md_lines.append(f"**Netmiko Version:** {version}") - timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") md_lines.append(f"**Generated:** {timestamp}") md_lines.append("") md_lines.append("---") @@ -313,8 +303,7 @@ def generate_markdown( # Footer md_lines.append("---") md_lines.append("") - md_lines.append("*This document was generated automatically by the " - "Netmiko device list script.*") + md_lines.append("*This document was generated automatically by the Netmiko device list script.*") return "\n".join(md_lines) @@ -332,10 +321,8 @@ def save_markdown(content: str, filename: str = "netmiko_devices.md") -> None: # Go up from: .../gns3server/agent/gns3_copilot/utils/ # custom_netmiko/scripts to project root # That's 6 levels up - project_root = os.path.dirname(os.path.dirname( - os.path.dirname(os.path.dirname( - os.path.dirname(os.path.dirname(script_dir)))) - ) + project_root = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))) ) # Target directory: docs/gns3-copilot @@ -348,17 +335,15 @@ def save_markdown(content: str, filename: str = "netmiko_devices.md") -> None: output_path = os.path.join(target_dir, filename) try: - with open(output_path, 'w', encoding='utf-8') as f: + with open(output_path, "w", encoding="utf-8") as f: f.write(content) print(f"Markdown document saved to: {output_path}") except IOError as e: - print(f"Error: Failed to save Markdown file: {e}", - file=sys.stderr) + print(f"Error: Failed to save Markdown file: {e}", file=sys.stderr) sys.exit(1) -def print_summary(version: str, ssh_count: int, telnet_count: int, - total_count: int, output_file: str) -> None: +def print_summary(version: str, ssh_count: int, telnet_count: int, total_count: int, output_file: str) -> None: """Print summary to console.""" print("=" * 80) print("Netmiko Device List Generator") @@ -402,7 +387,7 @@ def main() -> int: ssh_devices=ssh_devices, telnet_devices=telnet_devices, total_count=total_count, - custom_devices=custom_devices + custom_devices=custom_devices, ) # Save to file @@ -415,7 +400,7 @@ def main() -> int: ssh_count=len(ssh_devices), telnet_count=len(telnet_devices), total_count=total_count, - output_file=output_file + output_file=output_file, ) return 0 diff --git a/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_huawei_ce.py b/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_huawei_ce.py index db18bada4..c6b6db344 100644 --- a/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_huawei_ce.py +++ b/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_huawei_ce.py @@ -25,9 +25,7 @@ import os # Add project root to path using relative path test_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname( - os.path.dirname(os.path.dirname( - os.path.dirname(os.path.dirname(test_dir))))) +project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(test_dir))))) sys.path.insert(0, project_root) @@ -103,13 +101,8 @@ class TestHuaweiTelnetCEDriver(unittest.TestCase): def test_initialization_parameters(self): """Test that initialization sets correct parameters.""" # Create a mock instance (without actual connection) - with patch.object( - self.HuaweiTelnetCE, "__init__", - lambda self, *args, **kwargs: None - ): - instance = self.HuaweiTelnetCE.__new__( - self.HuaweiTelnetCE - ) + with patch.object(self.HuaweiTelnetCE, "__init__", lambda self, *args, **kwargs: None): + instance = self.HuaweiTelnetCE.__new__(self.HuaweiTelnetCE) # Mock the necessary attributes instance.protocol = "telnet" @@ -169,9 +162,7 @@ class TestHuaweiTelnetCEIntegration(unittest.TestCase): ) # Create a mock instance - instance = GNS3HuaweiTelnetCE.__new__( - GNS3HuaweiTelnetCE - ) + instance = GNS3HuaweiTelnetCE.__new__(GNS3HuaweiTelnetCE) # Mock the necessary attributes and methods instance.host = "127.0.0.1" @@ -203,12 +194,8 @@ def run_tests(): suite = unittest.TestSuite() # Add test cases - suite.addTests( - loader.loadTestsFromTestCase(TestHuaweiTelnetCEDriver) - ) - suite.addTests( - loader.loadTestsFromTestCase(TestHuaweiTelnetCEIntegration) - ) + suite.addTests(loader.loadTestsFromTestCase(TestHuaweiTelnetCEDriver)) + suite.addTests(loader.loadTestsFromTestCase(TestHuaweiTelnetCEIntegration)) # Run tests runner = unittest.TextTestRunner(verbosity=2) @@ -218,9 +205,7 @@ def run_tests(): print("\n" + "=" * 100) print("Test Summary:") print(f" Run: {result.testsRun}") - success_count = ( - result.testsRun - len(result.failures) - len(result.errors) - ) + success_count = result.testsRun - len(result.failures) - len(result.errors) print(f" Success: {success_count}") print(f" Failed: {len(result.failures)}") print(f" Errors: {len(result.errors)}") diff --git a/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_ruijie_telnet.py b/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_ruijie_telnet.py index b913f83ee..dfef81823 100644 --- a/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_ruijie_telnet.py +++ b/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_ruijie_telnet.py @@ -25,9 +25,7 @@ import os # Add project root to path using relative path test_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname( - os.path.dirname(os.path.dirname( - os.path.dirname(os.path.dirname(test_dir))))) +project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(test_dir))))) sys.path.insert(0, project_root) @@ -92,9 +90,7 @@ class TestRuijieTelnetEnhancedDriver(unittest.TestCase): import re # Should have INTERACTIVE_PATTERNS as a class attribute - self.assertTrue( - hasattr(self.RuijieTelnetEnhanced, "INTERACTIVE_PATTERNS") - ) + self.assertTrue(hasattr(self.RuijieTelnetEnhanced, "INTERACTIVE_PATTERNS")) # Check it's a list patterns = self.RuijieTelnetEnhanced.INTERACTIVE_PATTERNS @@ -108,18 +104,11 @@ class TestRuijieTelnetEnhancedDriver(unittest.TestCase): def test_preprocess_interactive_commands(self): """Test that interactive commands are preprocessed correctly.""" # Create a mock instance (without actual connection) - with patch.object( - self.RuijieTelnetEnhanced, "__init__", - lambda self, *args, **kwargs: None - ): - instance = self.RuijieTelnetEnhanced.__new__( - self.RuijieTelnetEnhanced - ) + with patch.object(self.RuijieTelnetEnhanced, "__init__", lambda self, *args, **kwargs: None): + instance = self.RuijieTelnetEnhanced.__new__(self.RuijieTelnetEnhanced) # Set INTERACTIVE_PATTERNS from class - instance.INTERACTIVE_PATTERNS = ( - self.RuijieTelnetEnhanced.INTERACTIVE_PATTERNS - ) + instance.INTERACTIVE_PATTERNS = self.RuijieTelnetEnhanced.INTERACTIVE_PATTERNS # Test commands with router-id (should trigger 'yes') commands = [ @@ -128,34 +117,23 @@ class TestRuijieTelnetEnhancedDriver(unittest.TestCase): "network 10.0.0.0 0.0.0.255 area 0", ] - processed = instance._preprocess_interactive_commands( - commands - ) + processed = instance._preprocess_interactive_commands(commands) # Should have inserted 'yes' after router-id command self.assertEqual(len(processed), 4) self.assertEqual(processed[0], "router ospf 1") self.assertEqual(processed[1], "router-id 1.1.1.1") self.assertEqual(processed[2], "yes") - self.assertEqual( - processed[3], "network 10.0.0.0 0.0.0.255 area 0" - ) + self.assertEqual(processed[3], "network 10.0.0.0 0.0.0.255 area 0") def test_preprocess_non_interactive_commands(self): """Test that non-interactive commands are not modified.""" # Create a mock instance - with patch.object( - self.RuijieTelnetEnhanced, "__init__", - lambda self, *args, **kwargs: None - ): - instance = self.RuijieTelnetEnhanced.__new__( - self.RuijieTelnetEnhanced - ) + with patch.object(self.RuijieTelnetEnhanced, "__init__", lambda self, *args, **kwargs: None): + instance = self.RuijieTelnetEnhanced.__new__(self.RuijieTelnetEnhanced) # Set INTERACTIVE_PATTERNS from class - instance.INTERACTIVE_PATTERNS = ( - self.RuijieTelnetEnhanced.INTERACTIVE_PATTERNS - ) + instance.INTERACTIVE_PATTERNS = self.RuijieTelnetEnhanced.INTERACTIVE_PATTERNS # Test commands without interactive prompts commands = [ @@ -190,9 +168,7 @@ class TestRuijieTelnetEnhancedDriver(unittest.TestCase): # Check that default_enter is handled in __init__ # (The actual logic is in the __init__ method body) - init_source = inspect.getsource( - self.RuijieTelnetEnhanced.__init__ - ) + init_source = inspect.getsource(self.RuijieTelnetEnhanced.__init__) self.assertIn("default_enter", init_source) self.assertIn("\\r\\n", init_source) @@ -207,9 +183,7 @@ class TestRuijieTelnetEnhancedIntegration(unittest.TestCase): ) # Create a mock instance - instance = RuijieTelnetEnhanced.__new__( - RuijieTelnetEnhanced - ) + instance = RuijieTelnetEnhanced.__new__(RuijieTelnetEnhanced) # Mock the necessary attributes and methods instance.RETURN = "\r\n" @@ -224,9 +198,7 @@ class TestRuijieTelnetEnhancedIntegration(unittest.TestCase): commands = ["interface GigabitEthernet 0/1", "description Test"] # Call send_config_set - instance.send_config_set( - commands, enter_config_mode=False, exit_config_mode=False - ) + instance.send_config_set(commands, enter_config_mode=False, exit_config_mode=False) # Verify _send_config_batch was called instance._send_config_batch.assert_called_once() @@ -238,17 +210,13 @@ class TestRuijieTelnetEnhancedIntegration(unittest.TestCase): ) # Create a mock instance - instance = RuijieTelnetEnhanced.__new__( - RuijieTelnetEnhanced - ) - instance.INTERACTIVE_PATTERNS = ( - RuijieTelnetEnhanced.INTERACTIVE_PATTERNS - ) + instance = RuijieTelnetEnhanced.__new__(RuijieTelnetEnhanced) + instance.INTERACTIVE_PATTERNS = RuijieTelnetEnhanced.INTERACTIVE_PATTERNS # Test commands with multiple interactive prompts commands = [ - "router-id 1.1.1.1", # Should trigger 'yes' - "erase startup-config", # Should trigger 'yes' + "router-id 1.1.1.1", # Should trigger 'yes' + "erase startup-config", # Should trigger 'yes' "interface GigabitEthernet 0/1", # Should NOT trigger ] @@ -273,12 +241,8 @@ def run_tests(): suite = unittest.TestSuite() # Add test cases - suite.addTests( - loader.loadTestsFromTestCase(TestRuijieTelnetEnhancedDriver) - ) - suite.addTests( - loader.loadTestsFromTestCase(TestRuijieTelnetEnhancedIntegration) - ) + suite.addTests(loader.loadTestsFromTestCase(TestRuijieTelnetEnhancedDriver)) + suite.addTests(loader.loadTestsFromTestCase(TestRuijieTelnetEnhancedIntegration)) # Run tests runner = unittest.TextTestRunner(verbosity=2) @@ -288,9 +252,7 @@ def run_tests(): print("\n" + "=" * 100) print("Test Summary:") print(f" Run: {result.testsRun}") - success_count = ( - result.testsRun - len(result.failures) - len(result.errors) - ) + success_count = result.testsRun - len(result.failures) - len(result.errors) print(f" Success: {success_count}") print(f" Failed: {len(result.failures)}") print(f" Errors: {len(result.errors)}") diff --git a/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_vpcs_telnet.py b/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_vpcs_telnet.py index 95fa06b8c..4659465cd 100644 --- a/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_vpcs_telnet.py +++ b/gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/test_vpcs_telnet.py @@ -27,9 +27,7 @@ import os # Add project root to path using relative path test_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname( - os.path.dirname(os.path.dirname( - os.path.dirname(os.path.dirname(test_dir))))) +project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(test_dir))))) sys.path.insert(0, project_root) @@ -121,6 +119,7 @@ class TestVPCSTelnetInit(unittest.TestCase): from gns3server.agent.gns3_copilot.utils.custom_netmiko.vpcs_telnet import ( # noqa: E501 VPCSTelnet, ) + cls.VPCSTelnet = VPCSTelnet def test_default_enter_parameter(self): @@ -134,6 +133,7 @@ class TestVPCSTelnetInit(unittest.TestCase): def test_default_parameters_when_none(self): """Test default parameters when not provided.""" + # Mock the BaseConnection.__init__ to avoid actual connection # But also set device_type since VPCSTelnet.__init__ sets it def mock_init(self, *args, **kwargs): @@ -141,23 +141,20 @@ class TestVPCSTelnetInit(unittest.TestCase): kwargs.setdefault("device_type", "gns3_vpcs_telnet") self.device_type = kwargs.get("device_type") - with patch.object( - self.VPCSTelnet, "__init__", mock_init - ): + with patch.object(self.VPCSTelnet, "__init__", mock_init): instance = self.VPCSTelnet(host="127.0.0.1") self.assertEqual(instance.device_type, "gns3_vpcs_telnet") def test_device_type_set(self): """Test that device_type is set to gns3_vpcs_telnet.""" + # Mock the __init__ to set device_type def mock_init(self, *args, **kwargs): # Simulate what VPCSTelnet.__init__ does kwargs.setdefault("device_type", "gns3_vpcs_telnet") self.device_type = kwargs.get("device_type") - with patch.object( - self.VPCSTelnet, "__init__", mock_init - ): + with patch.object(self.VPCSTelnet, "__init__", mock_init): instance = self.VPCSTelnet(host="127.0.0.1") self.assertEqual(instance.device_type, "gns3_vpcs_telnet") @@ -171,6 +168,7 @@ class TestVPCSTelnetMethods(unittest.TestCase): from gns3server.agent.gns3_copilot.utils.custom_netmiko.vpcs_telnet import ( # noqa: E501 VPCSTelnet, ) + cls.VPCSTelnet = VPCSTelnet def test_check_config_mode_always_false(self): @@ -213,6 +211,7 @@ class TestVPCSTelnetSendCommand(unittest.TestCase): from gns3server.agent.gns3_copilot.utils.custom_netmiko.vpcs_telnet import ( # noqa: E501 VPCSTelnet, ) + cls.VPCSTelnet = VPCSTelnet def test_send_command_writes_bytes(self): @@ -243,7 +242,7 @@ class TestVPCSTelnetSendCommand(unittest.TestCase): # Check that expect_string defaults to r"PC\d+>" source = inspect.getsource(self.VPCSTelnet.send_command) - self.assertIn("expect_string = r\"PC\\d+>\"", source) + self.assertIn('expect_string = r"PC\\d+>"', source) def test_send_command_timing_calls_send_command(self): """Test that send_command_timing delegates to send_command.""" @@ -277,6 +276,7 @@ class TestVPCSTelnetTelnetLogin(unittest.TestCase): from gns3server.agent.gns3_copilot.utils.custom_netmiko.vpcs_telnet import ( # noqa: E501 VPCSTelnet, ) + cls.VPCSTelnet = VPCSTelnet def test_telnet_login_default_prompt_pattern(self): @@ -335,19 +335,16 @@ class TestVPCSTelnetRegistration(unittest.TestCase): from gns3server.agent.gns3_copilot.utils.custom_netmiko import ( vpcs_telnet, ) + cls.vpcs_telnet = vpcs_telnet def test_register_function_exists(self): """Test that register_custom_device_type function exists.""" - self.assertTrue( - hasattr(self.vpcs_telnet, "register_custom_device_type") - ) + self.assertTrue(hasattr(self.vpcs_telnet, "register_custom_device_type")) def test_register_function_is_callable(self): """Test that register_custom_device_type is callable.""" - self.assertTrue( - callable(self.vpcs_telnet.register_custom_device_type) - ) + self.assertTrue(callable(self.vpcs_telnet.register_custom_device_type)) class TestVPCSTelnetAnsiStripping(unittest.TestCase): @@ -359,6 +356,7 @@ class TestVPCSTelnetAnsiStripping(unittest.TestCase): from gns3server.agent.gns3_copilot.utils.custom_netmiko.vpcs_telnet import ( # noqa: E501 VPCSTelnet, ) + cls.VPCSTelnet = VPCSTelnet def test_strip_ansi_codes_method_exists(self): @@ -435,14 +433,10 @@ IP/MASK : 192.168.1.20/24""" # Mock necessary attributes instance.remote_conn = MagicMock() instance.remote_conn.write = MagicMock() - instance.read_until_pattern = MagicMock( - return_value="\x1b[1mNAME\x1b[0m : PC2\nPC2>" - ) + instance.read_until_pattern = MagicMock(return_value="\x1b[1mNAME\x1b[0m : PC2\nPC2>") # Mock _strip_ansi_codes to verify it's called - instance._strip_ansi_codes = Mock( - return_value="NAME : PC2\nPC2>" - ) + instance._strip_ansi_codes = Mock(return_value="NAME : PC2\nPC2>") # Call send_command instance.send_command("show ip") @@ -476,9 +470,7 @@ def run_tests(): print("\n" + "=" * 100) print("Test Summary:") print(f" Run: {result.testsRun}") - success_count = ( - result.testsRun - len(result.failures) - len(result.errors) - ) + success_count = result.testsRun - len(result.failures) - len(result.errors) print(f" Success: {success_count}") print(f" Failed: {len(result.failures)}") print(f" Errors: {len(result.errors)}") diff --git a/gns3server/agent/gns3_copilot/utils/custom_netmiko/vpcs_telnet.py b/gns3server/agent/gns3_copilot/utils/custom_netmiko/vpcs_telnet.py index 348e123e1..0711f9c5d 100644 --- a/gns3server/agent/gns3_copilot/utils/custom_netmiko/vpcs_telnet.py +++ b/gns3server/agent/gns3_copilot/utils/custom_netmiko/vpcs_telnet.py @@ -149,10 +149,7 @@ class VPCSTelnet(BaseConnection): # Step 3: Wait for VPCS prompt pattern try: # Read until we see the prompt - output = self.read_until_pattern( - pattern=pri_prompt_terminator, - read_timeout=10 - ) + output = self.read_until_pattern(pattern=pri_prompt_terminator, read_timeout=10) return_msg += output if re.search(pri_prompt_terminator, return_msg, flags=re.M): @@ -291,9 +288,7 @@ class VPCSTelnet(BaseConnection): strip_command=strip_command, ) - def check_config_mode( - self, check_string: str = "", pattern: str = "", force_regex: bool = False - ) -> bool: + def check_config_mode(self, check_string: str = "", pattern: str = "", force_regex: bool = False) -> bool: """ VPCS has no configuration mode. @@ -326,9 +321,7 @@ class VPCSTelnet(BaseConnection): """ return ANSI_ESCAPE_PATTERN.sub("", text) - def config_mode( - self, config_command: str = "", pattern: str = "", re_flags: int = 0 - ) -> str: + def config_mode(self, config_command: str = "", pattern: str = "", re_flags: int = 0) -> str: """ VPCS has no configuration mode. @@ -370,6 +363,7 @@ class VPCSTelnet(BaseConnection): # Register the custom device type with Netmiko _registered = False # Flag to prevent duplicate registration + def register_custom_device_type() -> None: """ Register the custom VPCS Telnet device type with Netmiko. @@ -431,8 +425,4 @@ try: register_custom_device_type() except Exception as e: # Log but don't fail on import - logger.warning( - "Failed to register VPCS device type: %s", - e, - exc_info=True - ) + logger.warning("Failed to register VPCS device type: %s", e, exc_info=True) diff --git a/gns3server/agent/gns3_copilot/utils/error_handler.py b/gns3server/agent/gns3_copilot/utils/error_handler.py index a919d0a41..560454eb3 100644 --- a/gns3server/agent/gns3_copilot/utils/error_handler.py +++ b/gns3server/agent/gns3_copilot/utils/error_handler.py @@ -61,10 +61,7 @@ def format_error_message(error: Exception) -> str: # Check if error message contains HTML if _contains_html(error_str): - logger.warning( - "Detected HTML in error message, likely due to incorrect API " - "configuration or base URL" - ) + logger.warning("Detected HTML in error message, likely due to incorrect API configuration or base URL") return ( "API request failed. The error response indicates an issue with " "your API configuration. Please check:\n" diff --git a/gns3server/agent/gns3_copilot/utils/get_gns3_device_port.py b/gns3server/agent/gns3_copilot/utils/get_gns3_device_port.py index b3127574e..6c75f5b12 100644 --- a/gns3server/agent/gns3_copilot/utils/get_gns3_device_port.py +++ b/gns3server/agent/gns3_copilot/utils/get_gns3_device_port.py @@ -71,9 +71,7 @@ def get_device_ports_from_topology( Devices that don't exist or missing console_port will not be included """ # Log received parameters - logger.info( - "Called with device_names=%s, project_id=%s", device_names, project_id - ) + logger.info("Called with device_names=%s, project_id=%s", device_names, project_id) try: # Lazy import to avoid circular dependency @@ -93,9 +91,7 @@ def get_device_ports_from_topology( for device_name in device_names: # Check if device exists in topology if device_name not in topology.get("nodes", {}): - logger.warning( - "Device '%s' not found in topology", device_name - ) + logger.warning("Device '%s' not found in topology", device_name) continue node_info = topology["nodes"][device_name] @@ -133,9 +129,7 @@ def get_device_ports_from_topology( f"Current tags: {tags}" ) logger.error(error_msg) - hosts_data[device_name] = { - "error": error_msg - } + hosts_data[device_name] = {"error": error_msg} continue logger.debug( @@ -170,11 +164,7 @@ def get_device_ports_from_topology( "platform": platform, "node_type": node_info.get("type"), "groups": ["network_devices"], # For inheriting hostname, timeout, etc. - "connection_options": { - "netmiko": { - "extras": {"device_type": device_type} - } - }, + "connection_options": {"netmiko": {"extras": {"device_type": device_type}}}, } # Per-node default credentials (seeded from the template appliance diff --git a/gns3server/agent/gns3_copilot/utils/gns3_drawing_utils.py b/gns3server/agent/gns3_copilot/utils/gns3_drawing_utils.py index 8dc21a516..a28278744 100644 --- a/gns3server/agent/gns3_copilot/utils/gns3_drawing_utils.py +++ b/gns3server/agent/gns3_copilot/utils/gns3_drawing_utils.py @@ -135,14 +135,9 @@ def calculate_two_node_shape( node2_center_x = node2["x"] + (node2_width / 2) node2_center_y = node2["y"] + (node2_height / 2) - distance = math.sqrt( - (node2_center_x - node1_center_x) ** 2 - + (node2_center_y - node1_center_y) ** 2 - ) + distance = math.sqrt((node2_center_x - node1_center_x) ** 2 + (node2_center_y - node1_center_y) ** 2) - angle_rad = math.atan2( - node2_center_y - node1_center_y, node2_center_x - node1_center_x - ) + angle_rad = math.atan2(node2_center_y - node1_center_y, node2_center_x - node1_center_x) angle_deg = round(math.degrees(angle_rad)) angle_rad = math.radians(angle_deg) @@ -162,16 +157,10 @@ def calculate_two_node_shape( shape_width = rx * 2 shape_height = ry * 2 - svg_x = center_x - ( - rx * math.cos(angle_rad) - ry * math.sin(angle_rad) - ) - svg_y = center_y - ( - rx * math.sin(angle_rad) + ry * math.cos(angle_rad) - ) + svg_x = center_x - (rx * math.cos(angle_rad) - ry * math.sin(angle_rad)) + svg_y = center_y - (rx * math.sin(angle_rad) + ry * math.cos(angle_rad)) - shape_svg = generate_ellipse_svg( - int(rx), int(ry), color_scheme, int(shape_width), int(shape_height) - ) + shape_svg = generate_ellipse_svg(int(rx), int(ry), color_scheme, int(shape_width), int(shape_height)) offset_distance = ry * text_offset_ratio @@ -188,22 +177,12 @@ def calculate_two_node_shape( else: # rectangle shape_width = distance - shape_height = max( - node1_width, node1_height, node2_width, node2_height - ) + shape_height = max(node1_width, node1_height, node2_width, node2_height) - svg_x = center_x - ( - (shape_width / 2) * math.cos(angle_rad) - - (shape_height / 2) * math.sin(angle_rad) - ) - svg_y = center_y - ( - (shape_width / 2) * math.sin(angle_rad) - + (shape_height / 2) * math.cos(angle_rad) - ) + svg_x = center_x - ((shape_width / 2) * math.cos(angle_rad) - (shape_height / 2) * math.sin(angle_rad)) + svg_y = center_y - ((shape_width / 2) * math.sin(angle_rad) + (shape_height / 2) * math.cos(angle_rad)) - shape_svg = generate_rectangle_svg( - int(shape_width), int(shape_height), color_scheme - ) + shape_svg = generate_rectangle_svg(int(shape_width), int(shape_height), color_scheme) offset_distance = (shape_height / 2) * text_offset_ratio @@ -354,33 +333,15 @@ def _get_color_scheme(area_name: str) -> dict[str, Any]: return COLOR_SCHEMES["NORMAL_AREA"] # 3. Logical Isolation - if ( - "VRF" in label - or "VLAN" in label - or "MSTP" in label - or "VXLAN" in label - or "MPLS" in label - ): + if "VRF" in label or "VLAN" in label or "MSTP" in label or "VXLAN" in label or "MPLS" in label: return COLOR_SCHEMES["ISOLATION"] # 4. High Availability - if ( - "VRRP" in label - or "HSRP" in label - or "HA" in label - or "STACK" in label - or "M-LAG" in label - ): + if "VRRP" in label or "HSRP" in label or "HA" in label or "STACK" in label or "M-LAG" in label: return COLOR_SCHEMES["HIGH_AVAILABILITY"] # 5. External/Internet - if ( - "INET" in label - or "OUT" in label - or "EXTERNAL" in label - or "INTERNET" in label - or "DMZ" in label - ): + if "INET" in label or "OUT" in label or "EXTERNAL" in label or "INTERNET" in label or "DMZ" in label: return COLOR_SCHEMES["EXTERNAL"] # 6. Management @@ -392,12 +353,7 @@ def _get_color_scheme(area_name: str) -> dict[str, Any]: return COLOR_SCHEMES["SECURITY_TRUSTED"] # 8. Cloud/Tunnel - if ( - "TUNNEL" in label - or "CLOUD" in label - or "GRE" in label - or "IPSEC" in label - ): + if "TUNNEL" in label or "CLOUD" in label or "GRE" in label or "IPSEC" in label: return COLOR_SCHEMES["CLOUD_TUNNEL"] # Legacy keyword matching for backward compatibility @@ -436,9 +392,7 @@ def calculate_two_node_ellipse( Wrapper around calculate_two_node_shape with shape_type="ellipse". """ - result = calculate_two_node_shape( - node1, node2, area_name, "ellipse", text_offset_ratio - ) + result = calculate_two_node_shape(node1, node2, area_name, "ellipse", text_offset_ratio) return { "ellipse": result["shape"], "text": result["text"], @@ -457,9 +411,7 @@ def calculate_two_node_rectangle( Wrapper around calculate_two_node_shape with shape_type="rectangle". """ - result = calculate_two_node_shape( - node1, node2, area_name, "rectangle", text_offset_ratio - ) + result = calculate_two_node_shape(node1, node2, area_name, "rectangle", text_offset_ratio) metadata = result["metadata"] return { "rectangle": result["shape"], diff --git a/gns3server/agent/gns3_copilot/utils/llm_config_helper.py b/gns3server/agent/gns3_copilot/utils/llm_config_helper.py index 568a062cb..f0118c9d2 100644 --- a/gns3server/agent/gns3_copilot/utils/llm_config_helper.py +++ b/gns3server/agent/gns3_copilot/utils/llm_config_helper.py @@ -53,9 +53,7 @@ from fastapi import FastAPI logger = logging.getLogger(__name__) -async def get_user_llm_config_with_app( - user_id: UUID, app: FastAPI -) -> Optional[Dict[str, Any]]: +async def get_user_llm_config_with_app(user_id: UUID, app: FastAPI) -> Optional[Dict[str, Any]]: """ Get user's default LLM model configuration with decrypted API key. diff --git a/gns3server/agent/gns3_copilot/utils/parse_tool_content.py b/gns3server/agent/gns3_copilot/utils/parse_tool_content.py index 92e60f216..4ea17912d 100644 --- a/gns3server/agent/gns3_copilot/utils/parse_tool_content.py +++ b/gns3server/agent/gns3_copilot/utils/parse_tool_content.py @@ -160,9 +160,7 @@ def parse_tool_content( # Empty string handling if not content.strip(): result = {} - logger.info( - "Content is empty or whitespace, returning: %s", result - ) + logger.info("Content is empty or whitespace, returning: %s", result) return result s = content.strip() @@ -170,9 +168,7 @@ def parse_tool_content( # Handle empty dictionary case if s == "{}": result = {} - logger.info( - "Content is empty dictionary, returning: %s", result - ) + logger.info("Content is empty dictionary, returning: %s", result) return result # Try to parse as Python literal @@ -190,9 +186,7 @@ def parse_tool_content( # Try to parse as JSON try: result = json.loads(s) - logger.info( - "Successfully parsed as JSON, returning: %s", result - ) + logger.info("Successfully parsed as JSON, returning: %s", result) return result except json.JSONDecodeError: pass @@ -221,8 +215,7 @@ def parse_tool_content( # Handle unsupported types error_msg = ( # type: ignore[unreachable] - "Content must be str, dict, list, int, float, bool, or None, got " - f"{type(content).__name__}" + f"Content must be str, dict, list, int, float, bool, or None, got {type(content).__name__}" ) logger.error(error_msg) @@ -238,9 +231,7 @@ def parse_tool_content( return result -def format_tool_response( - content: str | dict | list | int | float | bool | None, indent: int = 2 -) -> str: +def format_tool_response(content: str | dict | list | int | float | bool | None, indent: int = 2) -> str: """ Format tool response as a beautiful JSON string for UI display. @@ -259,9 +250,7 @@ def format_tool_response( logger.info("format_tool_response parameter indent: %s", indent) try: - parsed = parse_tool_content( - content, fallback_to_raw=True, strict_mode=False - ) + parsed = parse_tool_content(content, fallback_to_raw=True, strict_mode=False) # Ensure the result can be serialized to JSON result = json.dumps(parsed, ensure_ascii=False, indent=indent) logger.info("format_tool_response returning: %s", result) @@ -270,9 +259,7 @@ def format_tool_response( # If the parsed result cannot be serialized, convert to string and wrap logger.error("Cannot serialize parsed result to JSON: %s", e) try: - result = json.dumps( - {"raw": str(content)}, ensure_ascii=False, indent=indent - ) + result = json.dumps({"raw": str(content)}, ensure_ascii=False, indent=indent) logger.info("format_tool_response returning fallback: %s", result) return result except Exception: @@ -286,16 +273,12 @@ def format_tool_response( return result except Exception as e: logger.error("Error formatting tool response: %s", e) - result = json.dumps( - {"error": str(e)}, ensure_ascii=False, indent=indent - ) + result = json.dumps({"error": str(e)}, ensure_ascii=False, indent=indent) logger.info("format_tool_response returning error: %s", result) return result -def normalize_tool_response( - response: dict | list | str, tool_name: str = "unknown" -) -> dict: +def normalize_tool_response(response: dict | list | str, tool_name: str = "unknown") -> dict: """ Normalize tool response to standard format for consistent frontend display. @@ -334,11 +317,7 @@ def normalize_tool_response( } # Handle error responses - if ( - isinstance(response, dict) - and "error" in response - and len(response) == 1 - ): + if isinstance(response, dict) and "error" in response and len(response) == 1: return { "success": False, "total": 0, @@ -362,34 +341,21 @@ def normalize_tool_response( # Handle list responses (most tools return list of device results) if isinstance(response, list): - successful = sum( - 1 - for item in response - if isinstance(item, dict) and item.get("status") == "success" - ) + successful = sum(1 for item in response if isinstance(item, dict) and item.get("status") == "success") failed = len(response) - successful normalized_data = [] for item in response: if isinstance(item, dict): normalized_item = { - "id": item.get("device_id") - or item.get("node_id") - or item.get("id") - or "", + "id": item.get("device_id") or item.get("node_id") or item.get("id") or "", "name": item.get("device_name") or item.get("name") or "", "status": item.get("status", "unknown"), } if normalized_item["status"] == "success": - normalized_item["result"] = ( - item.get("output") or item.get("result") or "" - ) + normalized_item["result"] = item.get("output") or item.get("result") or "" else: - normalized_item["error"] = ( - item.get("error") - or item.get("output") - or "Unknown error" - ) + normalized_item["error"] = item.get("error") or item.get("output") or "Unknown error" normalized_data.append(normalized_item) else: # Non-dict items in list @@ -426,14 +392,8 @@ def normalize_tool_response( } # Legacy format: extract common fields - total = ( - response.get("total_nodes") - or response.get("total") - or response.get("count", 1) - ) - successful = ( - response.get("successful_nodes") or response.get("successful") or 0 - ) + total = response.get("total_nodes") or response.get("total") or response.get("count", 1) + successful = response.get("successful_nodes") or response.get("successful") or 0 failed = response.get("failed_nodes") or response.get("failed") or 0 # Extract data from various possible locations @@ -458,13 +418,9 @@ def normalize_tool_response( if not data and "status" in response: data = [ { - "name": response.get("device_name") - or response.get("name") - or "", + "name": response.get("device_name") or response.get("name") or "", "status": response["status"], - "result": response.get("output") - or response.get("result") - or "", + "result": response.get("output") or response.get("result") or "", "error": response.get("error") or "", } ] @@ -564,9 +520,7 @@ def _test_parse_tool_content() -> None: valid = "✓" except Exception: valid = "✗" - print( - f"Format Test {i + 1}: {valid} Input: {repr(input_data)} -> {result}" - ) + print(f"Format Test {i + 1}: {valid} Input: {repr(input_data)} -> {result}") if __name__ == "__main__": diff --git a/gns3server/agent/gns3_copilot/utils/tool_call_stream.py b/gns3server/agent/gns3_copilot/utils/tool_call_stream.py index 8cd89a93e..6747db5bc 100644 --- a/gns3server/agent/gns3_copilot/utils/tool_call_stream.py +++ b/gns3server/agent/gns3_copilot/utils/tool_call_stream.py @@ -98,9 +98,7 @@ class ToolCallStreamAccumulator: "id": tc_id, "type": "function", "function": { - "name": self._current_tool_call[ - "name" - ], + "name": self._current_tool_call["name"], "arguments": "", }, }, @@ -122,9 +120,7 @@ class ToolCallStreamAccumulator: # Core: string concatenation if isinstance(args_chunk, str): - tool_data[ - "args_string" - ] += args_chunk + tool_data["args_string"] += args_chunk # Send updated tool_call event with accumulated args chunks.append( @@ -135,9 +131,7 @@ class ToolCallStreamAccumulator: "type": "function", "function": { "name": tool_data["name"], - "arguments": tool_data[ - "args_string" - ], + "arguments": tool_data["args_string"], }, }, } @@ -147,15 +141,9 @@ class ToolCallStreamAccumulator: # complete ========== # Check finish_reason == "tool_calls" or "STOP" response_metadata = getattr(chunk, "response_metadata", {}) - finish_reason = ( - response_metadata.get("finish_reason") - if isinstance(response_metadata, dict) - else None - ) + finish_reason = response_metadata.get("finish_reason") if isinstance(response_metadata, dict) else None - if (finish_reason == "tool_calls") or ( - finish_reason == "stop" and self._current_tool_call is not None - ): + if (finish_reason == "tool_calls") or (finish_reason == "stop" and self._current_tool_call is not None): if self._current_tool_call: tool_data = self._current_tool_call diff --git a/gns3server/agent/mcp/__init__.py b/gns3server/agent/mcp/__init__.py index d6179d24b..9b9026253 100644 --- a/gns3server/agent/mcp/__init__.py +++ b/gns3server/agent/mcp/__init__.py @@ -55,16 +55,26 @@ from gns3server.utils.request_utils import extract_client_info from gns3server.db.repositories.api_keys import ApiKeysRepository from gns3server.db.repositories.users import UsersRepository from .projects import ( - list_projects_handler, get_project_handler, create_project_handler, - delete_project_handler, open_project_handler, close_project_handler, - get_project_stats_handler, update_project_handler, duplicate_project_handler, - get_project_readme_handler, update_project_readme_handler, - lock_project_handler, unlock_project_handler, + list_projects_handler, + get_project_handler, + create_project_handler, + delete_project_handler, + open_project_handler, + close_project_handler, + get_project_stats_handler, + update_project_handler, + duplicate_project_handler, + get_project_readme_handler, + update_project_readme_handler, + lock_project_handler, + unlock_project_handler, get_locked_project_handler, ) from .server import ( - get_version_handler, get_statistics_handler, + get_version_handler, + get_statistics_handler, ) + # Symbol tools are disabled for now: they require a vision-capable model to # be genuinely useful (the tools shuttle SVG content, which a text-only LLM # cannot inspect or produce). Revisit later. @@ -74,50 +84,80 @@ from .server import ( # upload_symbol_handler, delete_symbol_handler, # ) from .appliances import ( - get_appliances_handler, get_appliance_handler, + get_appliances_handler, + get_appliance_handler, install_appliance_handler, ) from .images import ( - get_images_handler, get_image_handler, - delete_image_handler, prune_images_handler, + get_images_handler, + get_image_handler, + delete_image_handler, + prune_images_handler, install_images_handler, ) from .device_config import ( - device_config_send_handler, device_show_run_handler, + device_config_send_handler, + device_show_run_handler, vpcs_config_set_handler, ) from gns3server.agent.gns3_copilot.gns3_client.api_handlers import ( - get_nodes_handler, get_node_handler, start_node_handler, - stop_node_handler, suspend_node_handler, - create_node_handler, delete_node_handler, update_node_handler, + get_nodes_handler, + get_node_handler, + start_node_handler, + stop_node_handler, + suspend_node_handler, + create_node_handler, + delete_node_handler, + update_node_handler, get_node_console_info_handler, - list_node_files_handler, get_node_file_handler, - write_node_file_handler, delete_node_file_handler, - start_all_nodes_handler, stop_all_nodes_handler, + list_node_files_handler, + get_node_file_handler, + write_node_file_handler, + delete_node_file_handler, + start_all_nodes_handler, + stop_all_nodes_handler, suspend_all_nodes_handler, - duplicate_node_handler, isolate_node_handler, - unisolate_node_handler, get_node_links_handler, - get_links_handler, get_link_handler, available_filters_handler, + duplicate_node_handler, + isolate_node_handler, + unisolate_node_handler, + get_node_links_handler, + get_links_handler, + get_link_handler, + available_filters_handler, create_link_handler, - delete_link_handler, update_link_handler, - reset_link_handler, start_capture_handler, stop_capture_handler, + delete_link_handler, + update_link_handler, + reset_link_handler, + start_capture_handler, + stop_capture_handler, download_capture_file_handler, - link_marker_handler, marker_definition_handler, + link_marker_handler, + marker_definition_handler, ) from .templates import ( - list_templates_handler, get_template_handler, create_template_handler, - update_template_handler, delete_template_handler, + list_templates_handler, + get_template_handler, + create_template_handler, + update_template_handler, + delete_template_handler, ) from .computes import ( - list_computes_handler, get_compute_handler, get_compute_images_handler, + list_computes_handler, + get_compute_handler, + get_compute_images_handler, ) from .snapshots import ( - get_snapshots_handler, create_snapshot_handler, - delete_snapshot_handler, restore_snapshot_handler, + get_snapshots_handler, + create_snapshot_handler, + delete_snapshot_handler, + restore_snapshot_handler, ) from .drawings import ( - get_drawings_handler, create_drawing_handler, - get_drawing_handler, update_drawing_handler, delete_drawing_handler, + get_drawings_handler, + create_drawing_handler, + get_drawing_handler, + update_drawing_handler, + delete_drawing_handler, ) log = logging.getLogger(__name__) @@ -177,10 +217,7 @@ async def wait_for_mcp_ready() -> bool: log.debug("MCP server is now ready, proceeding with connection") return True except asyncio.TimeoutError: - log.warning( - "MCP server ready check timed out after 5 seconds - " - "GNS3 server initialization may have issues" - ) + log.warning("MCP server ready check timed out after 5 seconds - GNS3 server initialization may have issues") return False @@ -188,24 +225,19 @@ async def wait_for_mcp_ready() -> bool: # Set during SSE authentication, read by tool handlers running in the # same asyncio task (contextvars propagate through asyncio.to_thread). -_jwt_token_var: contextvars.ContextVar[str | None] = contextvars.ContextVar( - "mcp_jwt_token", default=None -) +_jwt_token_var: contextvars.ContextVar[str | None] = contextvars.ContextVar("mcp_jwt_token", default=None) # Username extracted during token validation — used by handlers to generate # short-lived JWTs for download/console URLs without exposing the raw key. -_jwt_username_var: contextvars.ContextVar[str | None] = contextvars.ContextVar( - "mcp_jwt_username", default=None -) +_jwt_username_var: contextvars.ContextVar[str | None] = contextvars.ContextVar("mcp_jwt_username", default=None) # token_version extracted during token validation — short-lived JWTs minted for # download/console URLs must carry the same version, or the revocation check # (token_data.token_version != user.token_version) rejects them as "revoked". -_jwt_token_version_var: contextvars.ContextVar[int] = contextvars.ContextVar( - "mcp_jwt_token_version", default=0 -) +_jwt_token_version_var: contextvars.ContextVar[int] = contextvars.ContextVar("mcp_jwt_token_version", default=0) # ── Token validation ────────────────────────────────────────────────── + async def _resolve_token(token: str) -> str | None: """Validate a token (JWT or API key) and return the effective JWT to use. @@ -245,7 +277,9 @@ async def _resolve_token(token: str) -> str | None: if user: _jwt_username_var.set(user.username) _jwt_token_version_var.set(user.token_version) - fresh_token = auth_service.create_access_token(user.username, token_version=user.token_version) + fresh_token = auth_service.create_access_token( + user.username, token_version=user.token_version + ) return fresh_token except Exception: pass @@ -255,6 +289,7 @@ async def _resolve_token(token: str) -> str | None: # ── Server URL helper ───────────────────────────────────────────────── + def _server_url() -> str: cfg = Config.instance().settings host = cfg.Server.host @@ -272,6 +307,7 @@ def _server_url() -> str: # ── FastMCP Server ──────────────────────────────────────────────────── + def _create_mcp_server() -> FastMCP: """Create MCP server with security settings from configuration.""" cfg = Config.instance().settings.Server @@ -298,6 +334,7 @@ mcp = _create_mcp_server() # ── Tool handlers ───────────────────────────────────────────────────── + def _run_handler_sync(handler, params: dict[str, Any]) -> list[dict[str, Any]]: """Run a synchronous Gns3Connector handler in a thread.""" ctx = { @@ -348,6 +385,7 @@ async def project_open( """Open a closed GNS3 project.""" return await asyncio.to_thread(_run_handler_sync, open_project_handler, {"project_id": project_id}) + @mcp.tool() async def project_close( project_id: Annotated[str, Field(description="UUID of the project to close")], @@ -355,6 +393,7 @@ async def project_close( """Close an open GNS3 project.""" return await asyncio.to_thread(_run_handler_sync, close_project_handler, {"project_id": project_id}) + @mcp.tool() async def project_stats( project_id: Annotated[str, Field(description="UUID of the project to get statistics for")], @@ -383,10 +422,19 @@ async def project_update( """Update a project's properties (name, auto_close, auto_open, etc.).""" params = {"project_id": project_id} local_vars = { - "name": name, "auto_close": auto_close, "auto_open": auto_open, "auto_start": auto_start, - "scene_width": scene_width, "scene_height": scene_height, "zoom": zoom, - "show_layers": show_layers, "snap_to_grid": snap_to_grid, "show_grid": show_grid, - "grid_size": grid_size, "drawing_grid_size": drawing_grid_size, "show_interface_labels": show_interface_labels, + "name": name, + "auto_close": auto_close, + "auto_open": auto_open, + "auto_start": auto_start, + "scene_width": scene_width, + "scene_height": scene_height, + "zoom": zoom, + "show_layers": show_layers, + "snap_to_grid": snap_to_grid, + "show_grid": show_grid, + "grid_size": grid_size, + "drawing_grid_size": drawing_grid_size, + "show_interface_labels": show_interface_labels, } for key, val in local_vars.items(): if val is not None: @@ -421,15 +469,23 @@ async def project_readme_update( content: Annotated[str, Field(description="Content to write to README.md (Markdown format)")], ) -> list[dict[str, Any]]: """Update or create a project's README.md file — the project documentation (Markdown format).""" - return await asyncio.to_thread(_run_handler_sync, update_project_readme_handler, {"project_id": project_id, "content": content}) + return await asyncio.to_thread( + _run_handler_sync, update_project_readme_handler, {"project_id": project_id, "content": content} + ) # ── Node tools ──────────────────────────────────────────────────────── + @mcp.tool() async def node_list( project_id: Annotated[str, Field(description="UUID of the project")], - fields: Annotated[list[str] | None, Field(description="Optional: return only these fields per node. e.g. [\"name\",\"status\"]. Available: name, status, node_type, console, console_type, console_host, node_id, project_id, compute_id, symbol, x, y, z, locked, ports, properties, command_line, node_directory, label, tags, template_id, width, height, aux, aux_type")] = None, + fields: Annotated[ + list[str] | None, + Field( + description='Optional: return only these fields per node. e.g. ["name","status"]. Available: name, status, node_type, console, console_type, console_host, node_id, project_id, compute_id, symbol, x, y, z, locked, ports, properties, command_line, node_directory, label, tags, template_id, width, height, aux, aux_type' + ), + ] = None, ) -> list[dict[str, Any]]: """List all nodes in a project. Use fields=[] to return only what you need.""" return await asyncio.to_thread(_run_handler_sync, get_nodes_handler, {"project_id": project_id, "fields": fields}) @@ -439,18 +495,32 @@ async def node_list( async def node_get( project_id: Annotated[str, Field(description="UUID of the project")], node_id: Annotated[str, Field(description="UUID of the node")], - fields: Annotated[list[str] | None, Field(description="Optional: return only these fields. e.g. [\"name\",\"status\"]. Available: name, status, node_type, console, console_type, console_host, node_id, project_id, compute_id, symbol, x, y, z, locked, ports, properties, command_line, node_directory, label, tags, template_id, width, height, aux, aux_type")] = None, + fields: Annotated[ + list[str] | None, + Field( + description='Optional: return only these fields. e.g. ["name","status"]. Available: name, status, node_type, console, console_type, console_host, node_id, project_id, compute_id, symbol, x, y, z, locked, ports, properties, command_line, node_directory, label, tags, template_id, width, height, aux, aux_type' + ), + ] = None, ) -> list[dict[str, Any]]: """Get detailed information about a specific node. Use fields=[] to return only what you need.""" - return await asyncio.to_thread(_run_handler_sync, get_node_handler, { - "project_id": project_id, "node_id": node_id, "fields": fields, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_node_handler, + { + "project_id": project_id, + "node_id": node_id, + "fields": fields, + }, + ) + @mcp.tool() async def node_start( project_id: Annotated[str, Field(description="UUID of the project")], node_id: Annotated[str | None, Field(description="Node UUID (single mode)")] = None, - node_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — start multiple nodes in parallel")] = None, + node_ids: Annotated[ + list[str] | None, Field(description='Batch mode: ["uuid1","uuid2"] — start multiple nodes in parallel') + ] = None, ) -> list[dict[str, Any]]: """Start one or more nodes. Provide node_id for single, or node_ids for batch.""" params = {"project_id": project_id} @@ -460,11 +530,14 @@ async def node_start( params["node_id"] = node_id return await asyncio.to_thread(_run_handler_sync, start_node_handler, params) + @mcp.tool() async def node_stop( project_id: Annotated[str, Field(description="UUID of the project")], node_id: Annotated[str | None, Field(description="Node UUID (single mode)")] = None, - node_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — stop multiple nodes in parallel")] = None, + node_ids: Annotated[ + list[str] | None, Field(description='Batch mode: ["uuid1","uuid2"] — stop multiple nodes in parallel') + ] = None, ) -> list[dict[str, Any]]: """Stop one or more nodes. Provide node_id for single, or node_ids for batch.""" params = {"project_id": project_id} @@ -474,11 +547,14 @@ async def node_stop( params["node_id"] = node_id return await asyncio.to_thread(_run_handler_sync, stop_node_handler, params) + @mcp.tool() async def node_suspend( project_id: Annotated[str, Field(description="UUID of the project")], node_id: Annotated[str | None, Field(description="Node UUID (single mode)")] = None, - node_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — suspend multiple nodes in parallel")] = None, + node_ids: Annotated[ + list[str] | None, Field(description='Batch mode: ["uuid1","uuid2"] — suspend multiple nodes in parallel') + ] = None, ) -> list[dict[str, Any]]: """Suspend one or more nodes. Provide node_id for single, or node_ids for batch.""" params = {"project_id": project_id} @@ -492,17 +568,29 @@ async def node_suspend( @mcp.tool() async def node_create( project_id: Annotated[str, Field(description="UUID of the project")], - template_id: Annotated[str | None, Field(description="Template UUID (required for single mode; used as default in batch mode)")] = None, + template_id: Annotated[ + str | None, Field(description="Template UUID (required for single mode; used as default in batch mode)") + ] = None, x: Annotated[int, Field(description="X coordinate (canvas center origin, right positive)")] = 0, y: Annotated[int, Field(description="Y coordinate (canvas center origin, down positive)")] = 0, compute_id: Annotated[str, Field(description="Compute ID (default: local)")] = "local", - nodes: Annotated[list | None, Field(description="Batch mode: [{name, template_id?, x?, y?, compute_id?}] — top-level template_id applies as default")] = None, - fields: Annotated[list[str] | None, Field(description="Response fields to include (default: [node_id, name, node_type, status, console]). " - "Available: compute_id, name, node_type, node_id, console, console_type, " - "console_auto_start, aux, aux_type, properties, label, symbol, x, y, z, " - "locked, port_name_format, port_segment_size, first_port_name, " - "custom_adapters, tags, template_id, project_id, node_directory, " - "status, command_line, width, height, ports, console_host")] = None, + nodes: Annotated[ + list | None, + Field( + description="Batch mode: [{name, template_id?, x?, y?, compute_id?}] — top-level template_id applies as default" + ), + ] = None, + fields: Annotated[ + list[str] | None, + Field( + description="Response fields to include (default: [node_id, name, node_type, status, console]). " + "Available: compute_id, name, node_type, node_id, console, console_type, " + "console_auto_start, aux, aux_type, properties, label, symbol, x, y, z, " + "locked, port_name_format, port_segment_size, first_port_name, " + "custom_adapters, tags, template_id, project_id, node_directory, " + "status, command_line, width, height, ports, console_host" + ), + ] = None, ) -> list[dict[str, Any]]: """Create one or more nodes from templates. @@ -514,21 +602,37 @@ async def node_create( port — such batches are created sequentially so those assignments follow submission order. """ if nodes is not None: - return await asyncio.to_thread(_run_handler_sync, create_node_handler, { - "project_id": project_id, "nodes": nodes, "fields": fields, + return await asyncio.to_thread( + _run_handler_sync, + create_node_handler, + { + "project_id": project_id, + "nodes": nodes, + "fields": fields, + "template_id": template_id, + }, + ) + return await asyncio.to_thread( + _run_handler_sync, + create_node_handler, + { + "project_id": project_id, "template_id": template_id, - }) - return await asyncio.to_thread(_run_handler_sync, create_node_handler, { - "project_id": project_id, "template_id": template_id, - "x": x, "y": y, "compute_id": compute_id, "fields": fields, - }) + "x": x, + "y": y, + "compute_id": compute_id, + "fields": fields, + }, + ) @mcp.tool() async def node_delete( project_id: Annotated[str, Field(description="UUID of the project")], node_id: Annotated[str | None, Field(description="Node UUID (single mode)")] = None, - node_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — delete multiple nodes in parallel")] = None, + node_ids: Annotated[ + list[str] | None, Field(description='Batch mode: ["uuid1","uuid2"] — delete multiple nodes in parallel') + ] = None, ) -> list[dict[str, Any]]: """Delete one or more nodes from a project. Provide node_id for single, or node_ids for batch.""" params = {"project_id": project_id} @@ -576,17 +680,28 @@ async def node_console( device output is not cut off before it arrives - Set a timeout to prevent hanging connections """ - return await asyncio.to_thread(_run_handler_sync, get_node_console_info_handler, { - "project_id": project_id, "node_id": node_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_node_console_info_handler, + { + "project_id": project_id, + "node_id": node_id, + }, + ) # ── Link tools ──────────────────────────────────────────────────────── + @mcp.tool() async def link_list( project_id: Annotated[str, Field(description="UUID of the project")], - fields: Annotated[list[str] | None, Field(description="Optional: return only these fields. e.g. [\"link_id\",\"nodes\"]. Available: link_id, project_id, link_type, nodes, suspend, filters, capturing, capture_file_name, link_style")] = None, + fields: Annotated[ + list[str] | None, + Field( + description='Optional: return only these fields. e.g. ["link_id","nodes"]. Available: link_id, project_id, link_type, nodes, suspend, filters, capturing, capture_file_name, link_style' + ), + ] = None, ) -> list[dict[str, Any]]: """List all links in a project. Use fields=[] to return only what you need.""" return await asyncio.to_thread(_run_handler_sync, get_links_handler, {"project_id": project_id, "fields": fields}) @@ -604,14 +719,27 @@ async def link_get( @mcp.tool() async def link_create( project_id: Annotated[str, Field(description="UUID of the project")], - nodes: Annotated[list | None, Field(description="Single mode: [{node_id, adapter_number, port_number}] or compact [id, ad, pt, id, ad, pt]")] = None, + nodes: Annotated[ + list | None, + Field(description="Single mode: [{node_id, adapter_number, port_number}] or compact [id, ad, pt, id, ad, pt]"), + ] = None, link_type: Annotated[str, Field(description="Link type - ethernet or serial")] = "ethernet", filters: Annotated[dict | None, Field(description="Optional packet filters")] = None, - links: Annotated[list | None, Field(description="Batch mode: [{nodes, link_type?, filters?}] — nodes supports compact [id, ad, pt, id, ad, pt] format")] = None, - fields: Annotated[list[str] | None, Field(description="Response fields to include (default: [link_id, link_type, nodes]). " - "Available: link_id, project_id, link_type, nodes, suspend, " - "link_style, filters, show_filters_icon, capturing, " - "capture_file_name, capture_file_path, capture_compute_id, wireshark")] = None, + links: Annotated[ + list | None, + Field( + description="Batch mode: [{nodes, link_type?, filters?}] — nodes supports compact [id, ad, pt, id, ad, pt] format" + ), + ] = None, + fields: Annotated[ + list[str] | None, + Field( + description="Response fields to include (default: [link_id, link_type, nodes]). " + "Available: link_id, project_id, link_type, nodes, suspend, " + "link_style, filters, show_filters_icon, capturing, " + "capture_file_name, capture_file_path, capture_compute_id, wireshark" + ), + ] = None, ) -> list[dict[str, Any]]: """Create one or more links between nodes. @@ -619,9 +747,15 @@ async def link_create( Batch mode: provide links=[{nodes, link_type?, filters?}] — up to 100 in parallel """ if links: - return await asyncio.to_thread(_run_handler_sync, create_link_handler, { - "project_id": project_id, "links": links, "fields": fields, - }) + return await asyncio.to_thread( + _run_handler_sync, + create_link_handler, + { + "project_id": project_id, + "links": links, + "fields": fields, + }, + ) params = {"project_id": project_id, "nodes": nodes, "link_type": link_type, "fields": fields} if filters: params["filters"] = filters @@ -632,7 +766,9 @@ async def link_create( async def link_delete( project_id: Annotated[str, Field(description="UUID of the project")], link_id: Annotated[str | None, Field(description="Link UUID (single mode)")] = None, - link_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — delete multiple links in parallel")] = None, + link_ids: Annotated[ + list[str] | None, Field(description='Batch mode: ["uuid1","uuid2"] — delete multiple links in parallel') + ] = None, ) -> list[dict[str, Any]]: """Delete one or more links from a project.""" params = {"project_id": project_id} @@ -685,18 +821,29 @@ async def link_available_filters( ) -> list[dict[str, Any]]: """List the packet filter types available for a link (frequency_drop, packet_loss, delay, corrupt, bpf) with their parameters. Use before setting filters with link_update.""" - return await asyncio.to_thread(_run_handler_sync, available_filters_handler, { - "project_id": project_id, "link_id": link_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + available_filters_handler, + { + "project_id": project_id, + "link_id": link_id, + }, + ) # ── Template tools ──────────────────────────────────────────── + @mcp.tool() async def template_list( - fields: Annotated[list[str] | None, Field(description="Response fields to include (default: [template_id, name, template_type, category, default_name_format]). " - "Available: template_id, name, version, category, default_name_format, symbol, " - "template_type, compute_id, usage, tags, builtin, created_at, updated_at")] = None, + fields: Annotated[ + list[str] | None, + Field( + description="Response fields to include (default: [template_id, name, template_type, category, default_name_format]). " + "Available: template_id, name, version, category, default_name_format, symbol, " + "template_type, compute_id, usage, tags, builtin, created_at, updated_at" + ), + ] = None, ) -> list[dict[str, Any]]: """List all available templates on the server.""" return await asyncio.to_thread(_run_handler_sync, list_templates_handler, {"fields": fields}) @@ -708,9 +855,14 @@ async def template_get( name: Annotated[str | None, Field(description="Template name (optional if template_id is provided)")] = None, ) -> list[dict[str, Any]]: """Get detailed information about a specific template.""" - return await asyncio.to_thread(_run_handler_sync, get_template_handler, { - "template_id": template_id, "name": name, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_template_handler, + { + "template_id": template_id, + "name": name, + }, + ) @mcp.tool() @@ -718,7 +870,9 @@ async def template_create( name: Annotated[str, Field(description="Template name")], template_type: Annotated[str, Field(description="Template type (e.g. qemu, docker, dynamips)")], compute_id: Annotated[str, Field(description="Compute ID (default: local)")] = "local", - image: Annotated[str | None, Field(description="Docker image name or Dynamips IOS image path (required for docker/dynamips)")] = None, + image: Annotated[ + str | None, Field(description="Docker image name or Dynamips IOS image path (required for docker/dynamips)") + ] = None, ) -> list[dict[str, Any]]: """Create a new template. @@ -751,13 +905,19 @@ async def template_delete( name: Annotated[str | None, Field(description="Template name (optional if template_id is provided)")] = None, ) -> list[dict[str, Any]]: """Delete a template.""" - return await asyncio.to_thread(_run_handler_sync, delete_template_handler, { - "template_id": template_id, "name": name, - }) + return await asyncio.to_thread( + _run_handler_sync, + delete_template_handler, + { + "template_id": template_id, + "name": name, + }, + ) # ── Compute tools ───────────────────────────────────────────────────── + @mcp.tool() async def compute_list() -> list[dict[str, Any]]: """List all remotely registered compute nodes (returns only database entries, does NOT include the built-in local compute). @@ -769,7 +929,12 @@ async def compute_list() -> list[dict[str, Any]]: @mcp.tool() async def compute_get( - compute_id: Annotated[str, Field(description="Compute ID: 'local' (default) for the built-in local compute, or a compute UUID from compute_list")] = "local", + compute_id: Annotated[ + str, + Field( + description="Compute ID: 'local' (default) for the built-in local compute, or a compute UUID from compute_list" + ), + ] = "local", ) -> list[dict[str, Any]]: """Get detailed information about a compute node. @@ -782,16 +947,26 @@ async def compute_get( @mcp.tool() async def compute_images( emulator: Annotated[str, Field(description="Emulator type (e.g. qemu, iou, docker)")], - compute_id: Annotated[str, Field(description="Compute ID: 'local' (default) for the built-in local compute, or a compute UUID from compute_list")] = "local", + compute_id: Annotated[ + str, + Field( + description="Compute ID: 'local' (default) for the built-in local compute, or a compute UUID from compute_list" + ), + ] = "local", ) -> list[dict[str, Any]]: """List available images for an emulator on a compute node. Accepts 'local' for the built-in local compute or a UUID from compute_list for a registered remote compute. """ - return await asyncio.to_thread(_run_handler_sync, get_compute_images_handler, { - "emulator": emulator, "compute_id": compute_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_compute_images_handler, + { + "emulator": emulator, + "compute_id": compute_id, + }, + ) # ── Node file tools ──────────────────────────────────────────────────── @@ -809,9 +984,16 @@ async def node_file_list( Use this first to check file sizes before reading files with get_node_file. Large config files should be read in chunks using offset/limit. """ - return await asyncio.to_thread(_run_handler_sync, list_node_files_handler, { - "project_id": project_id, "node_id": node_id, "path": path, "recursive": recursive, - }) + return await asyncio.to_thread( + _run_handler_sync, + list_node_files_handler, + { + "project_id": project_id, + "node_id": node_id, + "path": path, + "recursive": recursive, + }, + ) @mcp.tool() @@ -831,10 +1013,17 @@ async def node_file_get( Large files (>50KB) are auto-truncated; check the metadata.truncated flag. For binary files, check the file type via list_node_files first. """ - return await asyncio.to_thread(_run_handler_sync, get_node_file_handler, { - "project_id": project_id, "node_id": node_id, "file_path": file_path, - "offset": offset, "limit": limit, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_node_file_handler, + { + "project_id": project_id, + "node_id": node_id, + "file_path": file_path, + "offset": offset, + "limit": limit, + }, + ) @mcp.tool() @@ -845,9 +1034,16 @@ async def node_file_write( content: Annotated[str, Field(description="Content to write to the file")], ) -> list[dict[str, Any]]: """Write content to a file in a node directory. Creates the file if it doesn't exist. Overwrites existing content.""" - return await asyncio.to_thread(_run_handler_sync, write_node_file_handler, { - "project_id": project_id, "node_id": node_id, "file_path": file_path, "content": content, - }) + return await asyncio.to_thread( + _run_handler_sync, + write_node_file_handler, + { + "project_id": project_id, + "node_id": node_id, + "file_path": file_path, + "content": content, + }, + ) @mcp.tool() @@ -857,9 +1053,15 @@ async def node_file_delete( file_path: Annotated[str, Field(description="Path to the file within the node directory")], ) -> list[dict[str, Any]]: """Delete a file from a node directory. Cannot be undone.""" - return await asyncio.to_thread(_run_handler_sync, delete_node_file_handler, { - "project_id": project_id, "node_id": node_id, "file_path": file_path, - }) + return await asyncio.to_thread( + _run_handler_sync, + delete_node_file_handler, + { + "project_id": project_id, + "node_id": node_id, + "file_path": file_path, + }, + ) # ── Node bulk / advanced tools ───────────────────────────────────────── @@ -870,9 +1072,13 @@ async def node_start_all( project_id: Annotated[str, Field(description="UUID of the project")], ) -> list[dict[str, Any]]: """Start all nodes in a project.""" - return await asyncio.to_thread(_run_handler_sync, start_all_nodes_handler, { - "project_id": project_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + start_all_nodes_handler, + { + "project_id": project_id, + }, + ) @mcp.tool() @@ -880,9 +1086,13 @@ async def node_stop_all( project_id: Annotated[str, Field(description="UUID of the project")], ) -> list[dict[str, Any]]: """Stop all nodes in a project.""" - return await asyncio.to_thread(_run_handler_sync, stop_all_nodes_handler, { - "project_id": project_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + stop_all_nodes_handler, + { + "project_id": project_id, + }, + ) @mcp.tool() @@ -890,9 +1100,13 @@ async def node_suspend_all( project_id: Annotated[str, Field(description="UUID of the project")], ) -> list[dict[str, Any]]: """Suspend all nodes in a project.""" - return await asyncio.to_thread(_run_handler_sync, suspend_all_nodes_handler, { - "project_id": project_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + suspend_all_nodes_handler, + { + "project_id": project_id, + }, + ) @mcp.tool() @@ -904,9 +1118,17 @@ async def node_duplicate( z: Annotated[int, Field(description="Z layer for the new node")] = 0, ) -> list[dict[str, Any]]: """Duplicate a node in a project, creating a copy at a new position.""" - return await asyncio.to_thread(_run_handler_sync, duplicate_node_handler, { - "project_id": project_id, "node_id": node_id, "x": x, "y": y, "z": z, - }) + return await asyncio.to_thread( + _run_handler_sync, + duplicate_node_handler, + { + "project_id": project_id, + "node_id": node_id, + "x": x, + "y": y, + "z": z, + }, + ) @mcp.tool() @@ -915,9 +1137,14 @@ async def node_isolate( node_id: Annotated[str, Field(description="UUID of the node to isolate")], ) -> list[dict[str, Any]]: """Isolate a node by suspending all its attached links (network isolation).""" - return await asyncio.to_thread(_run_handler_sync, isolate_node_handler, { - "project_id": project_id, "node_id": node_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + isolate_node_handler, + { + "project_id": project_id, + "node_id": node_id, + }, + ) @mcp.tool() @@ -926,9 +1153,14 @@ async def node_unisolate( node_id: Annotated[str, Field(description="UUID of the node to unisolate")], ) -> list[dict[str, Any]]: """Un-isolate a node by resuming all its suspended links.""" - return await asyncio.to_thread(_run_handler_sync, unisolate_node_handler, { - "project_id": project_id, "node_id": node_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + unisolate_node_handler, + { + "project_id": project_id, + "node_id": node_id, + }, + ) @mcp.tool() @@ -937,9 +1169,14 @@ async def node_links( node_id: Annotated[str, Field(description="UUID of the node")], ) -> list[dict[str, Any]]: """List all links connected to a specific node.""" - return await asyncio.to_thread(_run_handler_sync, get_node_links_handler, { - "project_id": project_id, "node_id": node_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_node_links_handler, + { + "project_id": project_id, + "node_id": node_id, + }, + ) # ── Link capture / reset tools ──────────────────────────────────────── @@ -949,7 +1186,9 @@ async def node_links( async def link_reset( project_id: Annotated[str, Field(description="UUID of the project")], link_id: Annotated[str | None, Field(description="Link UUID (single mode)")] = None, - link_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — reset multiple links in parallel")] = None, + link_ids: Annotated[ + list[str] | None, Field(description='Batch mode: ["uuid1","uuid2"] — reset multiple links in parallel') + ] = None, ) -> list[dict[str, Any]]: """Reset one or more links by tearing down and recreating the UDP connection. @@ -977,10 +1216,18 @@ async def link_capture_start( data_link_type: Annotated[str, Field(description="Data link type (default: DLT_EN10MB)")] = "DLT_EN10MB", capture_file_name: Annotated[str | None, Field(description="Capture file name (optional)")] = None, wireshark: Annotated[bool, Field(description="Open Wireshark automatically (default: false)")] = False, - link_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — start capture on multiple links in parallel")] = None, + link_ids: Annotated[ + list[str] | None, + Field(description='Batch mode: ["uuid1","uuid2"] — start capture on multiple links in parallel'), + ] = None, ) -> list[dict[str, Any]]: """Start packet capture on one or more links.""" - params = {"project_id": project_id, "data_link_type": data_link_type, "capture_file_name": capture_file_name, "wireshark": wireshark} + params = { + "project_id": project_id, + "data_link_type": data_link_type, + "capture_file_name": capture_file_name, + "wireshark": wireshark, + } if link_ids: params["link_ids"] = link_ids else: @@ -992,7 +1239,10 @@ async def link_capture_start( async def link_capture_stop( project_id: Annotated[str, Field(description="UUID of the project")], link_id: Annotated[str | None, Field(description="Link UUID (single mode)")] = None, - link_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — stop capture on multiple links in parallel")] = None, + link_ids: Annotated[ + list[str] | None, + Field(description='Batch mode: ["uuid1","uuid2"] — stop capture on multiple links in parallel'), + ] = None, ) -> list[dict[str, Any]]: """Stop packet capture on one or more links.""" params = {"project_id": project_id} @@ -1007,7 +1257,9 @@ async def link_capture_stop( async def link_capture_download( project_id: Annotated[str, Field(description="UUID of the project")], link_id: Annotated[str | None, Field(description="Link UUID (single mode)")] = None, - link_ids: Annotated[list[str] | None, Field(description="Batch mode: [\"uuid1\",\"uuid2\"] — get download URLs for multiple captures")] = None, + link_ids: Annotated[ + list[str] | None, Field(description='Batch mode: ["uuid1","uuid2"] — get download URLs for multiple captures') + ] = None, ) -> list[dict[str, Any]]: """Get download command(s) for PCAP capture file(s). @@ -1037,16 +1289,35 @@ async def link_marker( project_id: Annotated[str, Field(description="UUID of the project")], link_id: Annotated[str, Field(description="UUID of the link")], action: Annotated[str, Field(description="Action: create, update, or delete")], - bpf: Annotated[str | None, Field(description="BPF expression, e.g. 'arp', 'icmp', 'tcp port 80' (required for create)")] = None, + bpf: Annotated[ + str | None, Field(description="BPF expression, e.g. 'arp', 'icmp', 'tcp port 80' (required for create)") + ] = None, marker_name: Annotated[str | None, Field(description="Marker name (required for update/delete actions)")] = None, - name: Annotated[str | None, Field(description="Custom marker name for create action (auto-generated if omitted)")] = None, + name: Annotated[ + str | None, Field(description="Custom marker name for create action (auto-generated if omitted)") + ] = None, tag: Annotated[int | None, Field(description="Numeric tag for packet correlation")] = None, enabled: Annotated[bool | None, Field(description="Enable or disable the marker (for update action)")] = None, - direction: Annotated[str | None, Field(description="Direction filter: 'tx' (capture node sending only), 'rx' (receiving only), or 'both' (no filter — on update this clears a previously set direction). Omit to leave unchanged on update.")] = None, - capture_node_id: Annotated[str | None, Field(description="UUID of the endpoint whose uBridge hosts the marker (the observer; tx/rx are from its perspective). Must be a link endpoint and marker-capable. Omit to auto-pick.")] = None, + direction: Annotated[ + str | None, + Field( + description="Direction filter: 'tx' (capture node sending only), 'rx' (receiving only), or 'both' (no filter — on update this clears a previously set direction). Omit to leave unchanged on update." + ), + ] = None, + capture_node_id: Annotated[ + str | None, + Field( + description="UUID of the endpoint whose uBridge hosts the marker (the observer; tx/rx are from its perspective). Must be a link endpoint and marker-capable. Omit to auto-pick." + ), + ] = None, color: Annotated[str | None, Field(description="Hex color for UI highlight, e.g. '#ff5722'")] = None, highlight_duration: Annotated[int | None, Field(description="UI highlight duration in milliseconds")] = None, - data_link_type: Annotated[str | None, Field(description="pcap link-layer type for serial links (create-only): DLT_C_HDLC / DLT_PPP_SERIAL / DLT_FRELAY / DLT_ATM_RFC1483, matching the encapsulation on the serial link. Omit = DLT_EN10MB (Ethernet). Ignored on update — changing it would invalidate the capture file.")] = None, + data_link_type: Annotated[ + str | None, + Field( + description="pcap link-layer type for serial links (create-only): DLT_C_HDLC / DLT_PPP_SERIAL / DLT_FRELAY / DLT_ATM_RFC1483, matching the encapsulation on the serial link. Omit = DLT_EN10MB (Ethernet). Ignored on update — changing it would invalidate the capture file." + ), + ] = None, ) -> list[dict[str, Any]]: """Manage traffic-insight markers on a link. @@ -1063,7 +1334,18 @@ async def link_marker( and cannot be modified or deleted via this tool. """ params = {"project_id": project_id, "link_id": link_id, "action": action} - for opt in ("bpf", "marker_name", "name", "tag", "enabled", "direction", "capture_node_id", "color", "highlight_duration", "data_link_type"): + for opt in ( + "bpf", + "marker_name", + "name", + "tag", + "enabled", + "direction", + "capture_node_id", + "color", + "highlight_duration", + "data_link_type", + ): val = locals().get(opt) if val is not None: params[opt] = val @@ -1074,13 +1356,22 @@ async def link_marker( async def marker_definition( project_id: Annotated[str, Field(description="UUID of the project")], action: Annotated[str, Field(description="Action: create, update, delete, or list")], - bpf: Annotated[str | None, Field(description="BPF expression, e.g. 'arp', 'ospf', 'tcp port 22' (required for create)")] = None, + bpf: Annotated[ + str | None, Field(description="BPF expression, e.g. 'arp', 'ospf', 'tcp port 22' (required for create)") + ] = None, def_name: Annotated[str | None, Field(description="Definition name (required for update/delete actions)")] = None, - name: Annotated[str | None, Field(description="Custom definition name for create action (auto-generated if omitted)")] = None, + name: Annotated[ + str | None, Field(description="Custom definition name for create action (auto-generated if omitted)") + ] = None, tag: Annotated[int | None, Field(description="Numeric tag for packet correlation")] = None, color: Annotated[str | None, Field(description="Hex color for UI highlight, e.g. '#ff5722'")] = None, highlight_duration: Annotated[int | None, Field(description="UI highlight duration in milliseconds")] = None, - data_link_type: Annotated[str | None, Field(description="pcap link-layer type for serial links (DLT_C_HDLC / DLT_PPP_SERIAL / DLT_FRELAY / DLT_ATM_RFC1483). Omit = Ethernet-only (serial links skipped); setting it also covers serial links with that encapsulation")] = None, + data_link_type: Annotated[ + str | None, + Field( + description="pcap link-layer type for serial links (DLT_C_HDLC / DLT_PPP_SERIAL / DLT_FRELAY / DLT_ATM_RFC1483). Omit = Ethernet-only (serial links skipped); setting it also covers serial links with that encapsulation" + ), + ] = None, ) -> list[dict[str, Any]]: """Manage project-level marker definitions — traffic-insight rules that apply to ALL links. @@ -1117,9 +1408,13 @@ async def snapshot_list( project_id: Annotated[str, Field(description="UUID of the project")], ) -> list[dict[str, Any]]: """List all snapshots of a project.""" - return await asyncio.to_thread(_run_handler_sync, get_snapshots_handler, { - "project_id": project_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_snapshots_handler, + { + "project_id": project_id, + }, + ) @mcp.tool() @@ -1133,9 +1428,14 @@ async def snapshot_create( must be stopped first. Use node_stop_all before creating a snapshot. Cloud, NAT, and switch nodes are always-running and can be ignored. """ - return await asyncio.to_thread(_run_handler_sync, create_snapshot_handler, { - "project_id": project_id, "name": name, - }) + return await asyncio.to_thread( + _run_handler_sync, + create_snapshot_handler, + { + "project_id": project_id, + "name": name, + }, + ) @mcp.tool() @@ -1144,9 +1444,14 @@ async def snapshot_delete( snapshot_id: Annotated[str, Field(description="UUID of the snapshot to delete")], ) -> list[dict[str, Any]]: """Delete a snapshot from a project. Cannot be undone.""" - return await asyncio.to_thread(_run_handler_sync, delete_snapshot_handler, { - "project_id": project_id, "snapshot_id": snapshot_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + delete_snapshot_handler, + { + "project_id": project_id, + "snapshot_id": snapshot_id, + }, + ) @mcp.tool() @@ -1155,9 +1460,14 @@ async def snapshot_restore( snapshot_id: Annotated[str, Field(description="UUID of the snapshot to restore")], ) -> list[dict[str, Any]]: """Restore a project to a previous snapshot state. The project may be closed and reopened.""" - return await asyncio.to_thread(_run_handler_sync, restore_snapshot_handler, { - "project_id": project_id, "snapshot_id": snapshot_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + restore_snapshot_handler, + { + "project_id": project_id, + "snapshot_id": snapshot_id, + }, + ) # ── Drawing tools ────────────────────────────────────────────────────── @@ -1168,9 +1478,13 @@ async def drawing_list( project_id: Annotated[str, Field(description="UUID of the project")], ) -> list[dict[str, Any]]: """List all drawings (labels, shapes, images) on a project canvas.""" - return await asyncio.to_thread(_run_handler_sync, get_drawings_handler, { - "project_id": project_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_drawings_handler, + { + "project_id": project_id, + }, + ) @mcp.tool() @@ -1198,10 +1512,19 @@ async def drawing_create( Line: Dashed line: """ - return await asyncio.to_thread(_run_handler_sync, create_drawing_handler, { - "project_id": project_id, "svg": svg, "x": x, "y": y, "z": z, - "locked": locked, "rotation": rotation, - }) + return await asyncio.to_thread( + _run_handler_sync, + create_drawing_handler, + { + "project_id": project_id, + "svg": svg, + "x": x, + "y": y, + "z": z, + "locked": locked, + "rotation": rotation, + }, + ) @mcp.tool() @@ -1210,9 +1533,14 @@ async def drawing_get( drawing_id: Annotated[str, Field(description="UUID of the drawing")], ) -> list[dict[str, Any]]: """Get detailed information about a specific drawing.""" - return await asyncio.to_thread(_run_handler_sync, get_drawing_handler, { - "project_id": project_id, "drawing_id": drawing_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_drawing_handler, + { + "project_id": project_id, + "drawing_id": drawing_id, + }, + ) @mcp.tool() @@ -1241,9 +1569,14 @@ async def drawing_delete( drawing_id: Annotated[str, Field(description="UUID of the drawing to delete")], ) -> list[dict[str, Any]]: """Delete a drawing from a project canvas. Cannot be undone.""" - return await asyncio.to_thread(_run_handler_sync, delete_drawing_handler, { - "project_id": project_id, "drawing_id": drawing_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + delete_drawing_handler, + { + "project_id": project_id, + "drawing_id": drawing_id, + }, + ) # ── Project lock tools ──────────────────────────────────────────────── @@ -1254,9 +1587,13 @@ async def project_lock( project_id: Annotated[str, Field(description="UUID of the project")], ) -> list[dict[str, Any]]: """Lock all drawings and nodes in a project to prevent accidental changes.""" - return await asyncio.to_thread(_run_handler_sync, lock_project_handler, { - "project_id": project_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + lock_project_handler, + { + "project_id": project_id, + }, + ) @mcp.tool() @@ -1264,9 +1601,13 @@ async def project_unlock( project_id: Annotated[str, Field(description="UUID of the project")], ) -> list[dict[str, Any]]: """Unlock a project to allow editing of drawings and nodes.""" - return await asyncio.to_thread(_run_handler_sync, unlock_project_handler, { - "project_id": project_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + unlock_project_handler, + { + "project_id": project_id, + }, + ) @mcp.tool() @@ -1274,9 +1615,13 @@ async def project_locked( project_id: Annotated[str, Field(description="UUID of the project")], ) -> list[dict[str, Any]]: """Check whether a project is locked (preventing edits to drawings and nodes).""" - return await asyncio.to_thread(_run_handler_sync, get_locked_project_handler, { - "project_id": project_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_locked_project_handler, + { + "project_id": project_id, + }, + ) # ── Server info tools ───────────────────────────────────────────────── @@ -1369,7 +1714,12 @@ async def server_statistics() -> list[dict[str, Any]]: @mcp.tool() async def appliance_list( - fields: Annotated[list[str] | None, Field(description="Optional: return only these fields. e.g. [\"name\",\"category\"]. Available: name, category, description, vendor_name, product_name, status, availability, images, versions, tags, symbol, usage, builtin")] = None, + fields: Annotated[ + list[str] | None, + Field( + description='Optional: return only these fields. e.g. ["name","category"]. Available: name, category, description, vendor_name, product_name, status, availability, images, versions, tags, symbol, usage, builtin' + ), + ] = None, ) -> list[dict[str, Any]]: """List all available appliances (template library). Use fields=[] to return only what you need.""" return await asyncio.to_thread(_run_handler_sync, get_appliances_handler, {"fields": fields} if fields else {}) @@ -1380,15 +1730,24 @@ async def appliance_get( appliance_id: Annotated[str, Field(description="UUID of the appliance")], ) -> list[dict[str, Any]]: """Get detailed information about a specific appliance.""" - return await asyncio.to_thread(_run_handler_sync, get_appliance_handler, { - "appliance_id": appliance_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_appliance_handler, + { + "appliance_id": appliance_id, + }, + ) @mcp.tool() async def appliance_install( appliance_id: Annotated[str, Field(description="UUID of the appliance to install")], - version: Annotated[str | None, Field(description="Version to install (e.g. '2.7.0.356'). Required if the appliance has multiple versions. Use appliance_get to see available versions.")] = None, + version: Annotated[ + str | None, + Field( + description="Version to install (e.g. '2.7.0.356'). Required if the appliance has multiple versions. Use appliance_get to see available versions." + ), + ] = None, ) -> list[dict[str, Any]]: """Create a template from a GNS3 appliance definition and return the created template. @@ -1397,10 +1756,14 @@ async def appliance_install( The appliance definition is read from local .gns3a files bundled with the server. Use get_appliance first to see what images are required. """ - return await asyncio.to_thread(_run_handler_sync, install_appliance_handler, { - "appliance_id": appliance_id, - "version": version, - }) + return await asyncio.to_thread( + _run_handler_sync, + install_appliance_handler, + { + "appliance_id": appliance_id, + "version": version, + }, + ) # ── Image tools ─────────────────────────────────────────────────────── @@ -1417,9 +1780,13 @@ async def image_get( image_id: Annotated[str, Field(description="ID or filename of the image")], ) -> list[dict[str, Any]]: """Get detailed information about a specific image.""" - return await asyncio.to_thread(_run_handler_sync, get_image_handler, { - "image_id": image_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + get_image_handler, + { + "image_id": image_id, + }, + ) @mcp.tool() @@ -1427,9 +1794,13 @@ async def image_delete( image_id: Annotated[str, Field(description="ID or filename of the image to delete")], ) -> list[dict[str, Any]]: """Delete an image from the server. Cannot be undone.""" - return await asyncio.to_thread(_run_handler_sync, delete_image_handler, { - "image_id": image_id, - }) + return await asyncio.to_thread( + _run_handler_sync, + delete_image_handler, + { + "image_id": image_id, + }, + ) @mcp.tool() @@ -1470,10 +1841,18 @@ async def image_install() -> list[dict[str, Any]]: @mcp.tool() async def device_config_send( project_id: Annotated[str, Field(description="UUID of the project")], - device_configs: Annotated[list, Field( - description="List of device configs. Each entry: {\"device_name\": \"R1\", \"config_commands\": [\"int lo0\", \"ip add 1.1.1.1 255.255.255.255\"]}" - )], - template: Annotated[str | None, Field(description="Optional Jinja2 template. Use with vars in each device to reduce token usage for batch config. Example: \"interface lo{{ n }}\\nip address {{ ip }} 255.255.255.255\"")] = None, + device_configs: Annotated[ + list, + Field( + description='List of device configs. Each entry: {"device_name": "R1", "config_commands": ["int lo0", "ip add 1.1.1.1 255.255.255.255"]}' + ), + ], + template: Annotated[ + str | None, + Field( + description='Optional Jinja2 template. Use with vars in each device to reduce token usage for batch config. Example: "interface lo{{ n }}\\nip address {{ ip }} 255.255.255.255"' + ), + ] = None, ) -> list[dict[str, Any]]: """Send configuration commands to network devices via console (telnet/SSH). @@ -1499,10 +1878,18 @@ async def device_config_send( @mcp.tool() async def device_show_run( project_id: Annotated[str, Field(description="UUID of the project")], - device_configs: Annotated[list, Field( - description="List of device commands. Each entry: {\"device_name\": \"R1\", \"commands\": [\"show ip int brief\", \"show running-config\"]}" - )], - template: Annotated[str | None, Field(description="Optional Jinja2 template. Use with vars per device. Example: \"show ip route {{ protocol }}\"")] = None, + device_configs: Annotated[ + list, + Field( + description='List of device commands. Each entry: {"device_name": "R1", "commands": ["show ip int brief", "show running-config"]}' + ), + ], + template: Annotated[ + str | None, + Field( + description='Optional Jinja2 template. Use with vars per device. Example: "show ip route {{ protocol }}"' + ), + ] = None, ) -> list[dict[str, Any]]: """Run read-only diagnostic (show) commands on network devices via console. @@ -1533,9 +1920,12 @@ async def device_show_run( @mcp.tool() async def vpcs_config_set( project_id: Annotated[str, Field(description="UUID of the project")], - device_configs: Annotated[list, Field( - description="List of VPCS configs. Each entry: {\"device_name\": \"PC1\", \"commands\": [\"ip 10.0.0.1/24 10.0.0.254\", \"save\"]}" - )], + device_configs: Annotated[ + list, + Field( + description='List of VPCS configs. Each entry: {"device_name": "PC1", "commands": ["ip 10.0.0.1/24 10.0.0.254", "save"]}' + ), + ], ) -> list[dict[str, Any]]: """Configure VPCS devices (set IP addresses, gateway, etc.). @@ -1549,13 +1939,19 @@ async def vpcs_config_set( - save Save config to startup.vpc - ping Test connectivity """ - return await asyncio.to_thread(_run_handler_sync, vpcs_config_set_handler, { - "project_id": project_id, "device_configs": device_configs, - }) + return await asyncio.to_thread( + _run_handler_sync, + vpcs_config_set_handler, + { + "project_id": project_id, + "device_configs": device_configs, + }, + ) # ── Auth‑wrapped SSE app ────────────────────────────────────────────── + def _make_auth_wrapper(inner_app): """Wrap the SSE app with JWT validation. @@ -1576,10 +1972,7 @@ def _make_auth_wrapper(inner_app): f"Rejecting MCP connection - GNS3 server initialization not complete. " f"Client: {client_info['host']}:{client_info['port']} ({client_info['user_info']}, Path: {client_info['path']})" ) - response = Response( - "GNS3 server initialization not complete - please retry later", - status_code=503 - ) + response = Response("GNS3 server initialization not complete - please retry later", status_code=503) await response(scope, receive, send) return diff --git a/gns3server/agent/mcp/appliances.py b/gns3server/agent/mcp/appliances.py index ab867fe57..45a9b472a 100644 --- a/gns3server/agent/mcp/appliances.py +++ b/gns3server/agent/mcp/appliances.py @@ -28,8 +28,10 @@ log = logging.getLogger(__name__) # ── Helper ───────────────────────────────────────────────────────────────── + def _get_connector(gns3_ctx: dict[str, Any]): from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector + return Gns3Connector( url=gns3_ctx["server_url"], jwt_token=gns3_ctx["jwt_token"], @@ -41,12 +43,32 @@ def _get_connector(gns3_ctx: dict[str, Any]): # ── Tool handlers ────────────────────────────────────────────────────────── VALID_APPLIANCE_FIELDS = { - "appliance_id", "name", "category", "description", "vendor_name", - "vendor_url", "product_name", "product_url", "documentation_url", - "status", "availability", "maintainer", "usage", "symbol", - "images", "versions", "tags", "builtin", - "first_port_name", "port_name_format", "port_segment_size", - "linked_clone", "docker", "iou", "dynamips", "qemu", + "appliance_id", + "name", + "category", + "description", + "vendor_name", + "vendor_url", + "product_name", + "product_url", + "documentation_url", + "status", + "availability", + "maintainer", + "usage", + "symbol", + "images", + "versions", + "tags", + "builtin", + "first_port_name", + "port_name_format", + "port_segment_size", + "linked_clone", + "docker", + "iou", + "dynamips", + "qemu", } @@ -56,7 +78,7 @@ def get_appliances_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> fields = params.get("fields") if fields: if not isinstance(fields, list): - return {"error": "fields must be a list, e.g. [\"name\", \"category\"]"} + return {"error": 'fields must be a list, e.g. ["name", "category"]'} invalid = [f for f in fields if f not in VALID_APPLIANCE_FIELDS] if invalid: return { diff --git a/gns3server/agent/mcp/computes.py b/gns3server/agent/mcp/computes.py index 254dc53f1..1f744066c 100644 --- a/gns3server/agent/mcp/computes.py +++ b/gns3server/agent/mcp/computes.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) def _get_connector(gns3_ctx: dict[str, Any]): from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector + return Gns3Connector( url=gns3_ctx["server_url"], jwt_token=gns3_ctx["jwt_token"], diff --git a/gns3server/agent/mcp/device_config.py b/gns3server/agent/mcp/device_config.py index 990f4b097..278295071 100644 --- a/gns3server/agent/mcp/device_config.py +++ b/gns3server/agent/mcp/device_config.py @@ -81,6 +81,7 @@ def _render_template(template: str, device_configs: list[dict], commands_field: # ── Tool handlers ────────────────────────────────────────────────────────── + def device_config_send_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> list[dict[str, Any]]: """Send configuration commands to network devices via console.""" project_id = params.get("project_id") @@ -97,10 +98,12 @@ def device_config_send_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) from gns3server.agent.gns3_copilot.tools_v2.config_tools_nornir import ExecuteMultipleDeviceConfigCommands tool = ExecuteMultipleDeviceConfigCommands() - input_data = json.dumps({ - "project_id": project_id, - "device_configs": device_configs, - }) + input_data = json.dumps( + { + "project_id": project_id, + "device_configs": device_configs, + } + ) return tool._run( input_data, jwt_token=gns3_ctx["jwt_token"], @@ -114,7 +117,12 @@ def device_show_run_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> device_configs = params.get("device_configs") template = params.get("template") if not project_id or not device_configs: - return [{"status": "failed", "error": "project_id and device_configs (list of {device_name, commands}) are required"}] + return [ + { + "status": "failed", + "error": "project_id and device_configs (list of {device_name, commands}) are required", + } + ] if template: device_configs = _render_template(template, device_configs, commands_field="commands") @@ -124,10 +132,12 @@ def device_show_run_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> from gns3server.agent.gns3_copilot.tools_v2.display_tools_nornir import ExecuteMultipleDeviceCommands tool = ExecuteMultipleDeviceCommands() - input_data = json.dumps({ - "project_id": project_id, - "device_configs": device_configs, - }) + input_data = json.dumps( + { + "project_id": project_id, + "device_configs": device_configs, + } + ) return tool._run( input_data, jwt_token=gns3_ctx["jwt_token"], @@ -145,10 +155,12 @@ def vpcs_config_set_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> from gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko import VPCSCommands tool = VPCSCommands() - input_data = json.dumps({ - "project_id": project_id, - "device_configs": device_configs, - }) + input_data = json.dumps( + { + "project_id": project_id, + "device_configs": device_configs, + } + ) return tool._run( input_data, jwt_token=gns3_ctx["jwt_token"], diff --git a/gns3server/agent/mcp/drawings.py b/gns3server/agent/mcp/drawings.py index e9135b0f9..a3ca146f6 100644 --- a/gns3server/agent/mcp/drawings.py +++ b/gns3server/agent/mcp/drawings.py @@ -28,8 +28,10 @@ log = logging.getLogger(__name__) # ── Helper ───────────────────────────────────────────────────────────────── + def _get_connector(gns3_ctx: dict[str, Any]): from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector + return Gns3Connector( url=gns3_ctx["server_url"], jwt_token=gns3_ctx["jwt_token"], @@ -40,6 +42,7 @@ def _get_connector(gns3_ctx: dict[str, Any]): # ── Tool handlers ────────────────────────────────────────────────────────── + def get_drawings_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: project_id = params.get("project_id") if not project_id: diff --git a/gns3server/agent/mcp/images.py b/gns3server/agent/mcp/images.py index 3fcbfd17d..d2b7ed958 100644 --- a/gns3server/agent/mcp/images.py +++ b/gns3server/agent/mcp/images.py @@ -28,8 +28,10 @@ log = logging.getLogger(__name__) # ── Helper ───────────────────────────────────────────────────────────────── + def _get_connector(gns3_ctx: dict[str, Any]): from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector + return Gns3Connector( url=gns3_ctx["server_url"], jwt_token=gns3_ctx["jwt_token"], @@ -40,6 +42,7 @@ def _get_connector(gns3_ctx: dict[str, Any]): # ── Tool handlers ────────────────────────────────────────────────────────── + def get_images_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: conn = _get_connector(gns3_ctx) images = conn.http_call("get", f"{conn.base_url}/images").json() diff --git a/gns3server/agent/mcp/projects.py b/gns3server/agent/mcp/projects.py index e095354a1..ff6b5125c 100644 --- a/gns3server/agent/mcp/projects.py +++ b/gns3server/agent/mcp/projects.py @@ -31,9 +31,11 @@ log = logging.getLogger(__name__) # ── Helper ───────────────────────────────────────────────────────────────── + def _get_connector(gns3_ctx: dict[str, Any]): """Create a Gns3Connector from the GNS3 context dict.""" from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector + return Gns3Connector( url=gns3_ctx["server_url"], jwt_token=gns3_ctx["jwt_token"], @@ -44,6 +46,7 @@ def _get_connector(gns3_ctx: dict[str, Any]): # ── Tool handlers ────────────────────────────────────────────────────────── + def list_projects_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: conn = _get_connector(gns3_ctx) projects = conn.http_call("get", f"{conn.base_url}/projects").json() @@ -141,7 +144,12 @@ def get_project_readme_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) return {"project_id": project_id, "file": "README.txt", "content": content} except Exception as e: if "404" in str(e): - return {"project_id": project_id, "file": "README.txt", "content": None, "message": "README.txt does not exist yet"} + return { + "project_id": project_id, + "file": "README.txt", + "content": None, + "message": "README.txt does not exist yet", + } raise @@ -286,7 +294,10 @@ PROJECT_TOOLS = [ "show_grid": {"type": "boolean", "description": "Show the grid on the drawing area"}, "grid_size": {"type": "integer", "description": "Grid size for the drawing area for nodes"}, "drawing_grid_size": {"type": "integer", "description": "Grid size for the drawing area for drawings"}, - "show_interface_labels": {"type": "boolean", "description": "Show interface labels on the drawing area"}, + "show_interface_labels": { + "type": "boolean", + "description": "Show interface labels on the drawing area", + }, }, "required": ["project_id"], }, diff --git a/gns3server/agent/mcp/server.py b/gns3server/agent/mcp/server.py index 0f8ac4abb..fe5c21dc3 100644 --- a/gns3server/agent/mcp/server.py +++ b/gns3server/agent/mcp/server.py @@ -28,8 +28,10 @@ log = logging.getLogger(__name__) # ── Helper ───────────────────────────────────────────────────────────────── + def _get_connector(gns3_ctx: dict[str, Any]): from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector + return Gns3Connector( url=gns3_ctx["server_url"], jwt_token=gns3_ctx["jwt_token"], @@ -40,6 +42,7 @@ def _get_connector(gns3_ctx: dict[str, Any]): # ── Tool handlers ────────────────────────────────────────────────────────── + def get_version_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: conn = _get_connector(gns3_ctx) return conn.http_call("get", f"{conn.base_url}/version").json() diff --git a/gns3server/agent/mcp/snapshots.py b/gns3server/agent/mcp/snapshots.py index f428292b9..336c7a3e0 100644 --- a/gns3server/agent/mcp/snapshots.py +++ b/gns3server/agent/mcp/snapshots.py @@ -28,8 +28,10 @@ log = logging.getLogger(__name__) # ── Helper ───────────────────────────────────────────────────────────────── + def _get_connector(gns3_ctx: dict[str, Any]): from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector + return Gns3Connector( url=gns3_ctx["server_url"], jwt_token=gns3_ctx["jwt_token"], @@ -40,6 +42,7 @@ def _get_connector(gns3_ctx: dict[str, Any]): # ── Tool handlers ────────────────────────────────────────────────────────── + def get_snapshots_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: project_id = params.get("project_id") if not project_id: diff --git a/gns3server/agent/mcp/symbols.py b/gns3server/agent/mcp/symbols.py index 96f2841d6..d363e042c 100644 --- a/gns3server/agent/mcp/symbols.py +++ b/gns3server/agent/mcp/symbols.py @@ -30,8 +30,10 @@ log = logging.getLogger(__name__) # ── Helper ───────────────────────────────────────────────────────────────── + def _get_connector(gns3_ctx: dict[str, Any]): from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector + return Gns3Connector( url=gns3_ctx["server_url"], jwt_token=gns3_ctx["jwt_token"], @@ -42,6 +44,7 @@ def _get_connector(gns3_ctx: dict[str, Any]): # ── Tool handlers ────────────────────────────────────────────────────────── + def get_symbols_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: conn = _get_connector(gns3_ctx) symbols = conn.http_call("get", f"{conn.base_url}/symbols").json() @@ -57,9 +60,11 @@ def get_symbol_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict username = gns3_ctx.get("jwt_username") # short-lived ticket bound to this exact path — LLM clients retyping curl # commands corrupted the long Bearer JWT this used to embed - ticket = access_ticket_service.mint( - username, token_version=gns3_ctx.get("jwt_token_version", 0), path=path - ) if username else None + ticket = ( + access_ticket_service.mint(username, token_version=gns3_ctx.get("jwt_token_version", 0), path=path) + if username + else None + ) if ticket: download_url += f"?token={ticket}" result = { @@ -68,7 +73,7 @@ def get_symbol_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict "note": "Symbol files are SVG images.", } if ticket: - safe_name = symbol_id.replace(':', '').replace('/', '_') + safe_name = symbol_id.replace(":", "").replace("/", "_") result["curl_command"] = f"curl -L -o '{safe_name}.svg' '{download_url}'" result["note"] += " The download URL includes a 10-minute ticket." return result diff --git a/gns3server/agent/mcp/templates.py b/gns3server/agent/mcp/templates.py index 7c0f678bb..a80f08ff9 100644 --- a/gns3server/agent/mcp/templates.py +++ b/gns3server/agent/mcp/templates.py @@ -31,8 +31,10 @@ log = logging.getLogger(__name__) # ── Helper ───────────────────────────────────────────────────────────────── + def _get_connector(gns3_ctx: dict[str, Any]): from gns3server.agent.gns3_copilot.gns3_client.connector import Gns3Connector + return Gns3Connector( url=gns3_ctx["server_url"], jwt_token=gns3_ctx["jwt_token"], @@ -44,9 +46,19 @@ def _get_connector(gns3_ctx: dict[str, Any]): # ── Tool handlers ────────────────────────────────────────────────────────── VALID_TEMPLATE_FIELDS = { - "template_id", "name", "version", "category", "default_name_format", - "symbol", "template_type", "compute_id", "usage", "tags", "builtin", - "created_at", "updated_at", + "template_id", + "name", + "version", + "category", + "default_name_format", + "symbol", + "template_type", + "compute_id", + "usage", + "tags", + "builtin", + "created_at", + "updated_at", } TEMPLATE_DEFAULT_FIELDS = ["template_id", "name", "template_type", "category", "default_name_format"] @@ -67,7 +79,7 @@ def list_templates_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> fields = params.get("fields") if fields: if not isinstance(fields, list): - return {"error": "fields must be a list, e.g. [\"template_id\", \"name\"]"} + return {"error": 'fields must be a list, e.g. ["template_id", "name"]'} invalid = [f for f in fields if f not in VALID_TEMPLATE_FIELDS] if invalid: return { diff --git a/gns3server/agent/web_wireshark/docker_client.py b/gns3server/agent/web_wireshark/docker_client.py index ca9e86360..d27873fa5 100644 --- a/gns3server/agent/web_wireshark/docker_client.py +++ b/gns3server/agent/web_wireshark/docker_client.py @@ -83,7 +83,7 @@ class DockerHTTPClient: self._connected = True # Parse API version - api_version = parse_version(docker_info['ApiVersion']) + api_version = parse_version(docker_info["ApiVersion"]) docker_version = docker_info["Version"] logger.info(f"Connected to Docker {docker_version}, API {api_version}") @@ -102,7 +102,7 @@ class DockerHTTPClient: logger.info(f"Using Docker API version {self._api_version}") else: # Use Docker daemon's actual API version - self._api_version = docker_info['ApiVersion'] + self._api_version = docker_info["ApiVersion"] logger.info(f"Using Docker API version {self._api_version} (daemon native)") except (aiohttp.ClientError, FileNotFoundError) as e: @@ -124,7 +124,7 @@ class DockerHTTPClient: Response JSON data """ # Check connection and version on first request - check_connection = kwargs.pop('check_connection', True) + check_connection = kwargs.pop("check_connection", True) if check_connection and not self._connected: await self._check_connection() @@ -147,22 +147,21 @@ class DockerHTTPClient: except RuntimeError as e: # Retry with lower API version if Docker daemon doesn't support current version error_msg = str(e) - if ("400" in error_msg or "not found" in error_msg.lower()) and self._api_version == DOCKER_PREFERRED_API_VERSION: - logger.warning(f"Docker daemon doesn't support API version {self._api_version}, falling back to {DOCKER_MINIMUM_API_VERSION}") + if ( + "400" in error_msg or "not found" in error_msg.lower() + ) and self._api_version == DOCKER_PREFERRED_API_VERSION: + logger.warning( + f"Docker daemon doesn't support API version {self._api_version}, falling back to {DOCKER_MINIMUM_API_VERSION}" + ) self._api_version = DOCKER_MINIMUM_API_VERSION return await self._request(method, endpoint, check_connection=False, **kwargs) raise async def create_network(self, name: str, driver: str = "bridge", subnet: str = None): """Create Docker network.""" - data = { - "Name": name, - "Driver": driver - } + data = {"Name": name, "Driver": driver} if subnet: - data["IPAM"] = { - "Config": [{"Subnet": subnet}] - } + data["IPAM"] = {"Config": [{"Subnet": subnet}]} await self._request("POST", "networks/create", json=data) async def get_network(self, name: str): @@ -176,19 +175,12 @@ class DockerHTTPClient: async def create_container(self, name: str, image: str, **kwargs): """Create container.""" - data = { - "Image": image, - "name": name, - "HostConfig": {}, - "NetworkingConfig": {} - } + data = {"Image": image, "name": name, "HostConfig": {}, "NetworkingConfig": {}} # Handle network config if "network" in kwargs: network_name = kwargs.pop("network") - data["NetworkingConfig"]["EndpointsConfig"] = { - network_name: {} - } + data["NetworkingConfig"]["EndpointsConfig"] = {network_name: {}} # Handle environment variables if "environment" in kwargs: diff --git a/gns3server/agent/web_wireshark/manage_wireshark.py b/gns3server/agent/web_wireshark/manage_wireshark.py index 905e1c2da..559eaadd9 100755 --- a/gns3server/agent/web_wireshark/manage_wireshark.py +++ b/gns3server/agent/web_wireshark/manage_wireshark.py @@ -92,10 +92,7 @@ logger = logging.getLogger(__name__) def setup_logging(verbose: bool = False): """Setup logging configuration.""" level = logging.DEBUG if verbose else logging.INFO - logging.basicConfig( - level=level, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) + logging.basicConfig(level=level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") async def cmd_start(args) -> int: @@ -119,7 +116,7 @@ async def cmd_start(args) -> int: image=args.image, memory=args.memory, cpus=args.cpus, - pids_limit=args.pids_limit + pids_limit=args.pids_limit, ) print(json.dumps(result, indent=2)) @@ -143,10 +140,7 @@ async def cmd_stop(args) -> int: """ manager = WebWiresharkManager() try: - await manager.stop_wireshark_session( - project_id=args.project_id, - link_id=args.link_id - ) + await manager.stop_wireshark_session(project_id=args.project_id, link_id=args.link_id) print(json.dumps({"status": "stopped"})) return 0 except Exception as e: @@ -172,7 +166,7 @@ async def cmd_restart(args) -> int: project_id=args.project_id, link_id=args.link_id, jwt_token=args.jwt_token, - capture_stream_url=args.capture_url + capture_stream_url=args.capture_url, ) print(json.dumps(result, indent=2)) return 0 @@ -274,14 +268,8 @@ async def cmd_delete(args) -> int: def create_parser() -> argparse.ArgumentParser: """Create command line argument parser.""" - parser = argparse.ArgumentParser( - description="Web Wireshark container management" - ) - parser.add_argument( - "--verbose", "-v", - action="store_true", - help="Enable verbose logging" - ) + parser = argparse.ArgumentParser(description="Web Wireshark container management") + parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging") subparsers = parser.add_subparsers(dest="command", help="Commands") @@ -290,32 +278,13 @@ def create_parser() -> argparse.ArgumentParser: start_parser.add_argument("--project-id", required=True, type=validate_uuid, help="Project ID") start_parser.add_argument("--link-id", required=True, type=validate_uuid, help="Link ID") start_parser.add_argument("--jwt-token", required=True, help="JWT token") + start_parser.add_argument("--capture-url", help="Capture stream URL (auto-detected if not provided)") start_parser.add_argument( - "--capture-url", - help="Capture stream URL (auto-detected if not provided)" - ) - start_parser.add_argument( - "--image", - default="gns3/web-wireshark:latest", - help="Docker image (default: gns3/web-wireshark:latest)" - ) - start_parser.add_argument( - "--memory", - default="2g", - help="Memory limit (default: 2g)" - ) - start_parser.add_argument( - "--cpus", - type=float, - default=1.0, - help="CPU cores (default: 1.0)" - ) - start_parser.add_argument( - "--pids-limit", - type=int, - default=1000, - help="Process limit (default: 1000)" + "--image", default="gns3/web-wireshark:latest", help="Docker image (default: gns3/web-wireshark:latest)" ) + start_parser.add_argument("--memory", default="2g", help="Memory limit (default: 2g)") + start_parser.add_argument("--cpus", type=float, default=1.0, help="CPU cores (default: 1.0)") + start_parser.add_argument("--pids-limit", type=int, default=1000, help="Process limit (default: 1000)") # Stop command stop_parser = subparsers.add_parser("stop", help="Stop Web Wireshark session") @@ -327,10 +296,7 @@ def create_parser() -> argparse.ArgumentParser: restart_parser.add_argument("--project-id", required=True, type=validate_uuid, help="Project ID") restart_parser.add_argument("--link-id", required=True, type=validate_uuid, help="Link ID") restart_parser.add_argument("--jwt-token", required=True, help="JWT token") - restart_parser.add_argument( - "--capture-url", - help="Capture stream URL (auto-detected if not provided)" - ) + restart_parser.add_argument("--capture-url", help="Capture stream URL (auto-detected if not provided)") # Stop all command stop_all_parser = subparsers.add_parser("stop-all", help="Stop all sessions") diff --git a/gns3server/agent/web_wireshark/manager.py b/gns3server/agent/web_wireshark/manager.py index 19d23a5ab..e17cf65c0 100644 --- a/gns3server/agent/web_wireshark/manager.py +++ b/gns3server/agent/web_wireshark/manager.py @@ -63,15 +63,16 @@ class WebWiresharkManager: """ try: proc = await asyncio.create_subprocess_exec( - "docker", "exec", container_id, - "bash", "-c", "echo 'ping'", + "docker", + "exec", + container_id, + "bash", + "-c", + "echo 'ping'", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, _ = await asyncio.wait_for( - proc.communicate(), - timeout=self.CONTAINER_EXEC_TIMEOUT + stderr=asyncio.subprocess.PIPE, ) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=self.CONTAINER_EXEC_TIMEOUT) return proc.returncode == 0 and b"ping" in stdout except asyncio.TimeoutError: logger.warning(f"Container {container_id[:12]} health check timeout") @@ -99,16 +100,17 @@ class WebWiresharkManager: try: proc = await asyncio.create_subprocess_exec( - "docker", "exec", container_id, - "bash", "-c", command, + "docker", + "exec", + container_id, + "bash", + "-c", + command, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) try: - stdout, stderr = await asyncio.wait_for( - proc.communicate(), - timeout=timeout - ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) return (proc.returncode, stdout.decode().strip(), stderr.decode().strip()) except asyncio.TimeoutError: logger.error(f"Command timeout after {timeout}s: {command}") @@ -142,7 +144,7 @@ class WebWiresharkManager: logger.debug(f"No processes found matching pattern '{pattern}'") return - pids = pids_str.replace('\n', ' ') + pids = pids_str.replace("\n", " ") logger.info(f"Found matching processes for '{pattern}': PIDs={pids}") # Kill the processes using their container-local PIDs @@ -170,10 +172,13 @@ class WebWiresharkManager: try: # Get container init PID from host perspective proc = await asyncio.create_subprocess_exec( - "docker", "inspect", container_id, - "--format", "{{.State.Pid}}", + "docker", + "inspect", + container_id, + "--format", + "{{.State.Pid}}", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, _ = await proc.communicate() container_init_pid = stdout.decode().strip() @@ -187,15 +192,13 @@ class WebWiresharkManager: # Check for residual processes from host perspective proc = await asyncio.create_subprocess_exec( - "ps", "-eo", "pid,ppid,args", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + "ps", "-eo", "pid,ppid,args", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, _ = await proc.communicate() # Build parent->children mapping children_map = {} - for line in stdout.decode().strip().split('\n'): + for line in stdout.decode().strip().split("\n"): parts = line.split(None, 2) if len(parts) < 3: continue @@ -218,14 +221,15 @@ class WebWiresharkManager: # Check if any descendant processes match display patterns import re + patterns = [ - f'xpra.*:{display}', - f'Xvfb.*:{display}', - f'wireshark.*:{display}', - f'pulseaudio.*display=:{display}' + f"xpra.*:{display}", + f"Xvfb.*:{display}", + f"wireshark.*:{display}", + f"pulseaudio.*display=:{display}", ] - for line in stdout.decode().strip().split('\n'): + for line in stdout.decode().strip().split("\n"): parts = line.split(None, 2) if len(parts) < 3: continue @@ -248,13 +252,11 @@ class WebWiresharkManager: # /proc//root points to container filesystem lock_paths = [ f"/proc/{container_init_pid}/root/tmp/.X{display}-lock", - f"/proc/{container_init_pid}/root/tmp/.X11-unix/X{display}" + f"/proc/{container_init_pid}/root/tmp/.X11-unix/X{display}", ] for lock_path in lock_paths: proc = await asyncio.create_subprocess_exec( - "test", "-e", lock_path, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + "test", "-e", lock_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await proc.communicate() if proc.returncode == 0: @@ -264,9 +266,7 @@ class WebWiresharkManager: # Check for xpra socket files socket_path = f"/proc/{container_init_pid}/root/run/user/1000/xpra/{display}/socket" proc = await asyncio.create_subprocess_exec( - "test", "-e", socket_path, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + "test", "-e", socket_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await proc.communicate() if proc.returncode == 0: @@ -298,10 +298,13 @@ class WebWiresharkManager: try: # Get container init PID from host perspective proc = await asyncio.create_subprocess_exec( - "docker", "inspect", container_id, - "--format", "{{.State.Pid}}", + "docker", + "inspect", + container_id, + "--format", + "{{.State.Pid}}", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) stdout, _ = await proc.communicate() container_init_pid = stdout.decode().strip() @@ -314,9 +317,7 @@ class WebWiresharkManager: # List all processes from host perspective with PID, PPID, and command # This is faster than docker exec and allows us to walk the process tree proc = await asyncio.create_subprocess_exec( - "ps", "-eo", "pid,ppid,args", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + "ps", "-eo", "pid,ppid,args", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, _ = await proc.communicate() @@ -328,7 +329,7 @@ class WebWiresharkManager: children_map = {} # ppid -> [pid] process_info = {} # pid -> (ppid, command) - for line in stdout.decode().strip().split('\n'): + for line in stdout.decode().strip().split("\n"): parts = line.split(None, 2) if len(parts) < 3: continue @@ -371,9 +372,7 @@ class WebWiresharkManager: logger.info(f"Killing {len(matching_pids)} processes: {matching_pids}") # Kill all matching PIDs from host perspective proc = await asyncio.create_subprocess_exec( - "kill", "-9", *matching_pids, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + "kill", "-9", *matching_pids, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await proc.communicate() logger.debug("Kill completed from host perspective") @@ -396,16 +395,12 @@ class WebWiresharkManager: """ try: # Combine all patterns into a single regex using OR operator - combined_pattern = '|'.join(f'({pattern})' for pattern in patterns) + combined_pattern = "|".join(f"({pattern})" for pattern in patterns) # Single pgrep to find all matching processes pgrep_cmd = f'pids=$(pgrep -f "{combined_pattern}" 2>/dev/null || true); if [ -n "$pids" ]; then echo "Found processes: $pids"; kill -9 $pids 2>/dev/null || true; fi' - returncode, stdout, stderr = await self._exec_in_container( - container_id, - pgrep_cmd, - timeout=5 - ) + returncode, stdout, stderr = await self._exec_in_container(container_id, pgrep_cmd, timeout=5) if "Found processes:" in stdout: logger.info(stdout.strip()) @@ -422,10 +417,12 @@ class WebWiresharkManager: """ # Clean up X lock files and xpra socket files in a single docker exec # This reduces docker exec calls from 2 to 1, improving stop performance - cmd = (f"rm -f /tmp/.X{display}-lock /tmp/.X11-unix/X{display} " - f"/run/user/1000/xpra/{display}/socket " - f"/run/user/1000/xpra/*-{display} " - f"/home/gns3/.xpra/*-{display} 2>/dev/null || true") + cmd = ( + f"rm -f /tmp/.X{display}-lock /tmp/.X11-unix/X{display} " + f"/run/user/1000/xpra/{display}/socket " + f"/run/user/1000/xpra/*-{display} " + f"/home/gns3/.xpra/*-{display} 2>/dev/null || true" + ) returncode, stdout, stderr = await self._exec_in_container(container_id, cmd) logger.debug(f"Cleanup X locks and xpra sockets: returncode={returncode}") @@ -436,9 +433,11 @@ class WebWiresharkManager: container_id: Container ID display: Display number (e.g., 10210) """ - cmd = (f"rm -f /run/user/1000/xpra/{display}/socket " - f"/run/user/1000/xpra/*-{display} " - f"/home/gns3/.xpra/*-{display} 2>/dev/null || true") + cmd = ( + f"rm -f /run/user/1000/xpra/{display}/socket " + f"/run/user/1000/xpra/*-{display} " + f"/home/gns3/.xpra/*-{display} 2>/dev/null || true" + ) returncode, stdout, stderr = await self._exec_in_container(container_id, cmd) logger.debug(f"Cleanup socket files: returncode={returncode}") @@ -457,20 +456,15 @@ class WebWiresharkManager: return 0 # Extract number and unit - match = re.match(r'(\d+(?:\.\d+)?)\s*([kmg]b?)?', memory_str) + match = re.match(r"(\d+(?:\.\d+)?)\s*([kmg]b?)?", memory_str) if not match: raise ValueError(f"Invalid memory format: {memory_str}") value = float(match.group(1)) - unit = match.group(2) or 'b' + unit = match.group(2) or "b" # Convert to bytes - multipliers = { - 'b': 1, - 'k': 1024, - 'm': 1024 * 1024, - 'g': 1024 * 1024 * 1024 - } + multipliers = {"b": 1, "k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024} unit = unit[0] # Take first character return int(value * multipliers.get(unit, 1)) @@ -511,8 +505,7 @@ class WebWiresharkManager: if container_id: try: returncode, stdout, stderr = await self._exec_in_container( - container_id, - "cat /proc/net/route | grep -E '^eth0\\s+00000000' | awk '{print $3}' | head -1" + container_id, "cat /proc/net/route | grep -E '^eth0\\s+00000000' | awk '{print $3}' | head -1" ) logger.info(f"Gateway detection - fallback method: returncode={returncode}, stdout='{stdout}'") if returncode == 0 and stdout.strip(): @@ -557,8 +550,7 @@ class WebWiresharkManager: # Use 'hostname -I' to get all IP addresses and take the first one # This works on most Linux systems and is more portable than 'ip' command returncode, stdout, stderr = await self._exec_in_container( - container_id, - "hostname -I 2>/dev/null | awk '{print $1}'" + container_id, "hostname -I 2>/dev/null | awk '{print $1}'" ) if returncode == 0 and stdout.strip(): container_ip = stdout.strip() @@ -585,7 +577,7 @@ class WebWiresharkManager: """ try: parsed = urlparse(url) - if parsed.hostname in ('localhost', '127.0.0.1', '0.0.0.0'): + if parsed.hostname in ("localhost", "127.0.0.1", "0.0.0.0"): gateway = await self._get_container_gateway_ip(container_id) if gateway: fixed_url = f"{parsed.scheme}://{gateway}:{parsed.port or 3080}{parsed.path}" @@ -627,11 +619,7 @@ class WebWiresharkManager: config = Config.instance() subnet = getattr(config.settings.WebWireshark, "network_subnet", "172.31.0.0/22") logger.info(f"Creating network {self.network_name} with subnet {subnet}") - await self.docker.create_network( - self.network_name, - driver="bridge", - subnet=subnet - ) + await self.docker.create_network(self.network_name, driver="bridge", subnet=subnet) async def get_or_create_container( self, @@ -640,7 +628,7 @@ class WebWiresharkManager: memory: str = "2g", memory_swap: str = None, cpus: float = 1.0, - pids_limit: int = 1000 + pids_limit: int = 1000, ) -> str: """Get or create project's Web Wireshark container. @@ -716,21 +704,15 @@ class WebWiresharkManager: "PidsLimit": pids_limit, "RestartPolicy": {"Name": "unless-stopped"}, "Init": True, # Use init system (tini) as PID 1 to reap zombie processes - "LogConfig": { - "Type": "json-file", - "Config": { - "max-size": "10m", - "max-file": "3" - } - } + "LogConfig": {"Type": "json-file", "Config": {"max-size": "10m", "max-file": "3"}}, } # Health check configuration health_config = { "Test": ["CMD-SHELL", "xpra list"], "Interval": 30000000000, # 30 seconds (nanoseconds) - "Timeout": 10000000000, # 10 seconds - "Retries": 3 + "Timeout": 10000000000, # 10 seconds + "Retries": 3, } try: @@ -740,11 +722,7 @@ class WebWiresharkManager: network=self.network_name, host_config=host_config, health_config=health_config, - environment={ - "XDG_RUNTIME_DIR": "/run/user/1000", - "LANG": "C.UTF-8", - "LC_ALL": "C.UTF-8" - } + environment={"XDG_RUNTIME_DIR": "/run/user/1000", "LANG": "C.UTF-8", "LC_ALL": "C.UTF-8"}, ) except RuntimeError as e: error_msg = str(e) @@ -773,7 +751,7 @@ class WebWiresharkManager: memory: str = "2g", memory_swap: Optional[str] = None, cpus: float = 1.0, - pids_limit: int = 1000 + pids_limit: int = 1000, ) -> dict: """Start Web Wireshark session. @@ -799,20 +777,10 @@ class WebWiresharkManager: # Auto-detect capture_stream_url if not provided if not capture_stream_url: gns3_url = self.detect_gns3_url() - capture_stream_url = ( - f"{gns3_url}/v3/projects/{project_id}/links/" - f"{link_id}/capture/stream" - ) + capture_stream_url = f"{gns3_url}/v3/projects/{project_id}/links/{link_id}/capture/stream" logger.info(f"Auto-detected capture stream URL: {capture_stream_url}") - container_id = await self.get_or_create_container( - project_id, - image, - memory, - memory_swap, - cpus, - pids_limit - ) + container_id = await self.get_or_create_container(project_id, image, memory, memory_swap, cpus, pids_limit) container_name = f"gns3-wireshark-{project_id}" # Fix localhost URL after we have container_id @@ -828,13 +796,15 @@ class WebWiresharkManager: has_process_residuals, has_socket_residuals = await self._check_residuals_exist(container_id, display) if has_process_residuals or has_socket_residuals: - logger.info(f"Found residual processes={has_process_residuals} sockets={has_socket_residuals} on display :{display}, cleaning up...") + logger.info( + f"Found residual processes={has_process_residuals} sockets={has_socket_residuals} on display :{display}, cleaning up..." + ) if has_process_residuals: patterns = [ - f'xpra.*:{display}', - f'Xvfb.*:{display}', - f'wireshark.*:{display}', - f'pulseaudio.*display=:{display}' + f"xpra.*:{display}", + f"Xvfb.*:{display}", + f"wireshark.*:{display}", + f"pulseaudio.*display=:{display}", ] await self._kill_process_tree_batch(container_id, patterns) if has_socket_residuals: @@ -848,29 +818,24 @@ class WebWiresharkManager: xpra_cmd = [ "XPRA_CLIENT_CAN_SHUTDOWN=false", - "xpra", "start", f":{display}", + "xpra", + "start", + f":{display}", '--xvfb="Xvfb -screen 0 1920x1080x24 +extension RANDR"', "--html=off", f"--bind-ws=0.0.0.0:{port}", f"--session-name={session_name}", "--daemon=yes", "--dbus-launch=no", - "--resize-display=yes" + "--resize-display=yes", ] # Parallel execution: get container info + start xpra - container_info_task = asyncio.create_task( - self.docker.get_container(container_name) - ) - xpra_start_task = asyncio.create_task( - self._exec_in_container(container_id, " ".join(xpra_cmd)) - ) + container_info_task = asyncio.create_task(self.docker.get_container(container_name)) + xpra_start_task = asyncio.create_task(self._exec_in_container(container_id, " ".join(xpra_cmd))) # Wait for both tasks to complete - container, (returncode, stdout, stderr) = await asyncio.gather( - container_info_task, - xpra_start_task - ) + container, (returncode, stdout, stderr) = await asyncio.gather(container_info_task, xpra_start_task) # Check xpra start result if returncode != 0: @@ -901,10 +866,14 @@ class WebWiresharkManager: # Execute Wireshark command without waiting for completion await asyncio.create_subprocess_exec( - "docker", "exec", container_id, - "bash", "-c", wireshark_cmd, + "docker", + "exec", + container_id, + "bash", + "-c", + wireshark_cmd, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) # Don't wait - Wireshark runs in background @@ -918,7 +887,7 @@ class WebWiresharkManager: "container_name": container_name, "container_id": container_id, "session_name": session_name, - "capture_stream_url": capture_stream_url + "capture_stream_url": capture_stream_url, } logger.info(f"Web Wireshark session started successfully: {result['ws_url']}") @@ -947,11 +916,11 @@ class WebWiresharkManager: # This ensures child processes are properly terminated, not left as zombies logger.info(f"Stopping all processes on display :{display}") patterns = [ - f'xpra.*:{display}', - f'Xvfb.*:{display}', - f'Xvfb-for-Xpra-{display}', - f'wireshark.*:{display}', - f'pulseaudio.*display=:{display}' + f"xpra.*:{display}", + f"Xvfb.*:{display}", + f"Xvfb-for-Xpra-{display}", + f"wireshark.*:{display}", + f"pulseaudio.*display=:{display}", ] await self._kill_process_tree_batch(container["Id"], patterns) await self._cleanup_x_lock(container["Id"], display) @@ -971,7 +940,7 @@ class WebWiresharkManager: memory: str = "2g", memory_swap: str = None, cpus: float = 1.0, - pids_limit: int = 1000 + pids_limit: int = 1000, ): """Restart Web Wireshark session. @@ -1001,7 +970,7 @@ class WebWiresharkManager: memory=memory, memory_swap=memory_swap, cpus=cpus, - pids_limit=pids_limit + pids_limit=pids_limit, ) async def stop_all_sessions(self, project_id: str): @@ -1021,12 +990,7 @@ class WebWiresharkManager: # Kill all wireshark, xpra and Xvfb processes for link sessions # This ensures clean removal of all session processes - patterns = [ - "xpra.*--session-name=link-", - "Xvfb-for-Xpra-", - "wireshark.*display :", - "pulseaudio.*display :" - ] + patterns = ["xpra.*--session-name=link-", "Xvfb-for-Xpra-", "wireshark.*display :", "pulseaudio.*display :"] await self._kill_process_tree_batch(container["Id"], patterns) logger.info(f"All Web Wireshark sessions stopped for project {project_id}") diff --git a/gns3server/agent/web_wireshark/setup_wireshark_image.py b/gns3server/agent/web_wireshark/setup_wireshark_image.py index 65afc892d..92949a923 100644 --- a/gns3server/agent/web_wireshark/setup_wireshark_image.py +++ b/gns3server/agent/web_wireshark/setup_wireshark_image.py @@ -62,11 +62,7 @@ def check_docker(): # Check if Docker daemon is running try: - result = subprocess.run( - ["docker", "info"], - capture_output=True, - text=True - ) + result = subprocess.run(["docker", "info"], capture_output=True, text=True) if result.returncode != 0: print("Error: Docker daemon is not running", file=sys.stderr) print("Please start Docker and try again", file=sys.stderr) @@ -82,11 +78,7 @@ def image_exists(): """Check if the Docker image already exists.""" try: - result = subprocess.run( - ["docker", "image", "inspect", DOCKER_IMAGE], - capture_output=True, - text=True - ) + result = subprocess.run(["docker", "image", "inspect", DOCKER_IMAGE], capture_output=True, text=True) return result.returncode == 0 except Exception: return False @@ -115,11 +107,7 @@ def pull_image(): print(f"Pulling Docker image: {DOCKER_IMAGE}") print("-" * 60) - result = subprocess.run( - ["docker", "pull", DOCKER_IMAGE], - capture_output=True, - text=True - ) + result = subprocess.run(["docker", "pull", DOCKER_IMAGE], capture_output=True, text=True) # Store output for network error detection pull_output = result.stdout + result.stderr @@ -140,33 +128,17 @@ def build_image(dockerfile_path): print("-" * 60) result = subprocess.run( - ["docker", "build", "-t", DOCKER_IMAGE, "-f", dockerfile_path, "."], - cwd=dockerfile_dir, - pass_fds=(1, 2) + ["docker", "build", "-t", DOCKER_IMAGE, "-f", dockerfile_path, "."], cwd=dockerfile_dir, pass_fds=(1, 2) ) return result.returncode == 0 def main(): - parser = argparse.ArgumentParser( - description="Setup Web Wireshark Docker image for GNS3" - ) - parser.add_argument( - "--force", - action="store_true", - help="Force rebuild even if image exists" - ) - parser.add_argument( - "--build-only", - action="store_true", - help="Only build locally, skip pull" - ) - parser.add_argument( - "--pull-only", - action="store_true", - help="Only pull from registry, skip build" - ) + parser = argparse.ArgumentParser(description="Setup Web Wireshark Docker image for GNS3") + parser.add_argument("--force", action="store_true", help="Force rebuild even if image exists") + parser.add_argument("--build-only", action="store_true", help="Only build locally, skip pull") + parser.add_argument("--pull-only", action="store_true", help="Only pull from registry, skip build") args = parser.parse_args() @@ -183,7 +155,7 @@ def main(): if image_exists() and not args.force: print(f"Image {DOCKER_IMAGE} already exists.") response = input("Do you want to rebuild it? [y/N]: ").strip().lower() - if response != 'y': + if response != "y": print("Setup cancelled.") sys.exit(0) print() diff --git a/gns3server/agent/web_wireshark/stats.py b/gns3server/agent/web_wireshark/stats.py index e5611a997..78dc7b0c0 100644 --- a/gns3server/agent/web_wireshark/stats.py +++ b/gns3server/agent/web_wireshark/stats.py @@ -30,12 +30,7 @@ async def collect_webwireshark_stats(projects: List) -> Dict: """ from .manager import WebWiresharkManager - stats = { - "total_containers": 0, - "running_containers": 0, - "active_sessions": 0, - "containers": [] - } + stats = {"total_containers": 0, "running_containers": 0, "active_sessions": 0, "containers": []} # Create a single manager instance and reuse it manager = WebWiresharkManager() @@ -70,15 +65,14 @@ async def collect_webwireshark_stats(projects: List) -> Dict: pids_limit = host_config.get("PidsLimit", 0) # Count active capture sessions - active_sessions = sum( - 1 for link in project.links.values() - if getattr(link, "capturing", False) - ) + active_sessions = sum(1 for link in project.links.values() if getattr(link, "capturing", False)) stats["active_sessions"] += active_sessions container_info["active_sessions"] = active_sessions # Add resource limits - container_info["memory_limit"] = f"{memory_limit / (1024**3):.1f} GB" if memory_limit > 0 else "unlimited" + container_info["memory_limit"] = ( + f"{memory_limit / (1024**3):.1f} GB" if memory_limit > 0 else "unlimited" + ) container_info["cpu_limit"] = f"{cpu_quota / 1000000000:.1f}" if cpu_quota > 0 else "unlimited" container_info["pids_limit"] = pids_limit if pids_limit > 0 else "unlimited" @@ -111,21 +105,15 @@ async def _get_container_resource_stats(container_id: str) -> Optional[Dict]: """ try: result = subprocess.run( - ["docker", "stats", "--no-stream", "--format", - "{{.MemUsage}}\t{{.CPUPerc}}\t{{.PIDs}}", - container_id], + ["docker", "stats", "--no-stream", "--format", "{{.MemUsage}}\t{{.CPUPerc}}\t{{.PIDs}}", container_id], capture_output=True, text=True, - timeout=2 + timeout=2, ) if result.returncode == 0: parts = result.stdout.strip().split("\t") if len(parts) >= 3: - return { - "memory": parts[0], - "cpu": parts[1], - "pids": int(parts[2]) - } + return {"memory": parts[0], "cpu": parts[1], "pids": int(parts[2])} except subprocess.TimeoutExpired: logger.debug(f"Docker stats timeout for container {container_id[:12]}") except Exception as e: diff --git a/gns3server/api/routes/compute/__init__.py b/gns3server/api/routes/compute/__init__.py index 017853b85..7606e6ddf 100644 --- a/gns3server/api/routes/compute/__init__.py +++ b/gns3server/api/routes/compute/__init__.py @@ -155,113 +155,71 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException): ) -compute_api.include_router( - capabilities.router, - dependencies=[Depends(compute_authentication)], - tags=["Capabilities"] -) +compute_api.include_router(capabilities.router, dependencies=[Depends(compute_authentication)], tags=["Capabilities"]) -compute_api.include_router( - compute.router, - dependencies=[Depends(compute_authentication)], - tags=["Compute"] -) +compute_api.include_router(compute.router, dependencies=[Depends(compute_authentication)], tags=["Compute"]) -compute_api.include_router( - notifications.router, - tags=["Notifications"] -) +compute_api.include_router(notifications.router, tags=["Notifications"]) -compute_api.include_router( - projects.router, - dependencies=[Depends(compute_authentication)], - tags=["Projects"] -) +compute_api.include_router(projects.router, dependencies=[Depends(compute_authentication)], tags=["Projects"]) -compute_api.include_router( - images.router, - dependencies=[Depends(compute_authentication)], - tags=["Images"] -) +compute_api.include_router(images.router, dependencies=[Depends(compute_authentication)], tags=["Images"]) compute_api.include_router( atm_switch_nodes.router, dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/atm_switch/nodes", - tags=["ATM switch"] + tags=["ATM switch"], ) compute_api.include_router( cloud_nodes.router, dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/cloud/nodes", - tags=["Cloud nodes"] + tags=["Cloud nodes"], ) -compute_api.include_router( - docker_nodes.router, - prefix="/projects/{project_id}/docker/nodes", - tags=["Docker nodes"] -) +compute_api.include_router(docker_nodes.router, prefix="/projects/{project_id}/docker/nodes", tags=["Docker nodes"]) compute_api.include_router( - dynamips_nodes.router, - prefix="/projects/{project_id}/dynamips/nodes", - tags=["Dynamips nodes"] + dynamips_nodes.router, prefix="/projects/{project_id}/dynamips/nodes", tags=["Dynamips nodes"] ) compute_api.include_router( ethernet_hub_nodes.router, dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/ethernet_hub/nodes", - tags=["Ethernet hub nodes"] + tags=["Ethernet hub nodes"], ) compute_api.include_router( ethernet_switch_nodes.router, dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/ethernet_switch/nodes", - tags=["Ethernet switch nodes"] + tags=["Ethernet switch nodes"], ) compute_api.include_router( frame_relay_switch_nodes.router, dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/frame_relay_switch/nodes", - tags=["Frame Relay switch nodes"] + tags=["Frame Relay switch nodes"], ) -compute_api.include_router( - iou_nodes.router, - prefix="/projects/{project_id}/iou/nodes", - tags=["IOU nodes"]) +compute_api.include_router(iou_nodes.router, prefix="/projects/{project_id}/iou/nodes", tags=["IOU nodes"]) compute_api.include_router( nat_nodes.router, dependencies=[Depends(compute_authentication)], prefix="/projects/{project_id}/nat/nodes", - tags=["NAT nodes"] + tags=["NAT nodes"], ) -compute_api.include_router( - qemu_nodes.router, - prefix="/projects/{project_id}/qemu/nodes", - tags=["Qemu nodes"] -) +compute_api.include_router(qemu_nodes.router, prefix="/projects/{project_id}/qemu/nodes", tags=["Qemu nodes"]) compute_api.include_router( - virtualbox_nodes.router, - prefix="/projects/{project_id}/virtualbox/nodes", - tags=["VirtualBox nodes"] + virtualbox_nodes.router, prefix="/projects/{project_id}/virtualbox/nodes", tags=["VirtualBox nodes"] ) -compute_api.include_router( - vmware_nodes.router, - prefix="/projects/{project_id}/vmware/nodes", - tags=["VMware nodes"] -) +compute_api.include_router(vmware_nodes.router, prefix="/projects/{project_id}/vmware/nodes", tags=["VMware nodes"]) -compute_api.include_router( - vpcs_nodes.router, - prefix="/projects/{project_id}/vpcs/nodes", - tags=["VPCS nodes"] -) +compute_api.include_router(vpcs_nodes.router, prefix="/projects/{project_id}/vpcs/nodes", tags=["VPCS nodes"]) diff --git a/gns3server/api/routes/compute/atm_switch_nodes.py b/gns3server/api/routes/compute/atm_switch_nodes.py index 2dbe10a1d..65bc1157d 100644 --- a/gns3server/api/routes/compute/atm_switch_nodes.py +++ b/gns3server/api/routes/compute/atm_switch_nodes.py @@ -79,8 +79,7 @@ def get_atm_switch(node: ATMSwitch = Depends(dep_node)) -> schemas.ATMSwitch: @router.post("/{node_id}/duplicate", response_model=schemas.ATMSwitch, status_code=status.HTTP_201_CREATED) async def duplicate_atm_switch( - destination_node_id: UUID = Body(..., embed=True), - node: ATMSwitch = Depends(dep_node) + destination_node_id: UUID = Body(..., embed=True), node: ATMSwitch = Depends(dep_node) ) -> schemas.ATMSwitch: """ Duplicate an ATM switch node. @@ -92,8 +91,7 @@ async def duplicate_atm_switch( @router.put("/{node_id}", response_model=schemas.ATMSwitch) async def update_atm_switch( - node_data: schemas.ATMSwitchUpdate, - node: ATMSwitch = Depends(dep_node) + node_data: schemas.ATMSwitchUpdate, node: ATMSwitch = Depends(dep_node) ) -> schemas.ATMSwitch: """ Update an ATM switch node. @@ -124,8 +122,7 @@ def start_atm_switch(node: ATMSwitch = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Start is not supported for ATM switches" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Start is not supported for ATM switches" ) @@ -135,10 +132,7 @@ def stop_atm_switch(node: ATMSwitch = Depends(dep_node)) -> None: Stop an ATM switch node. """ - raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Stop is not supported for ATM switches" - ) + raise HTTPException(status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Stop is not supported for ATM switches") @router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) @@ -149,8 +143,7 @@ def suspend_atm_switch(node: ATMSwitch = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Suspend is not supported for ATM switches" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Suspend is not supported for ATM switches" ) @@ -160,11 +153,11 @@ def suspend_atm_switch(node: ATMSwitch = Depends(dep_node)) -> None: response_model=schemas.UDPNIO, ) async def create_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: schemas.UDPNIO, - node: ATMSwitch = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: schemas.UDPNIO, + node: ATMSwitch = Depends(dep_node), ) -> schemas.UDPNIO: """ Add a NIO (Network Input/Output) to the node. @@ -189,11 +182,11 @@ async def delete_nio(adapter_number: int, port_number: int, node: ATMSwitch = De @router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") async def start_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node_capture_data: schemas.NodeCapture, - node: ATMSwitch = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + node_capture_data: schemas.NodeCapture, + node: ATMSwitch = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -210,10 +203,7 @@ async def start_capture( status_code=status.HTTP_204_NO_CONTENT, ) async def stop_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: ATMSwitch = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: ATMSwitch = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -225,10 +215,7 @@ async def stop_capture( @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") async def stream_pcap_file( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: ATMSwitch = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: ATMSwitch = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. diff --git a/gns3server/api/routes/compute/cloud_nodes.py b/gns3server/api/routes/compute/cloud_nodes.py index a5f2bb6a9..7baf8ca17 100644 --- a/gns3server/api/routes/compute/cloud_nodes.py +++ b/gns3server/api/routes/compute/cloud_nodes.py @@ -122,10 +122,7 @@ async def stop_cloud(node: Cloud = Depends(dep_node)) -> None: Stop a cloud node. """ - raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Stop is not supported for cloud nodes" - ) + raise HTTPException(status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Stop is not supported for cloud nodes") @router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) @@ -136,8 +133,7 @@ async def suspend_cloud(node: Cloud = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Suspend is not supported for cloud nodes" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Suspend is not supported for cloud nodes" ) @@ -147,11 +143,11 @@ async def suspend_cloud(node: Cloud = Depends(dep_node)) -> None: response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], ) async def create_cloud_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], - node: Cloud = Depends(dep_node), + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + node: Cloud = Depends(dep_node), ) -> Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]: """ Add a NIO (Network Input/Output) to the node. @@ -169,11 +165,11 @@ async def create_cloud_nio( response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], ) async def update_cloud_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], - node: Cloud = Depends(dep_node), + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + node: Cloud = Depends(dep_node), ) -> Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]: """ Update a NIO (Network Input/Output) to the node. @@ -192,10 +188,7 @@ async def update_cloud_nio( @router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) async def delete_cloud_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: Cloud = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: Cloud = Depends(dep_node) ) -> None: """ Remove a NIO (Network Input/Output) from the node. @@ -207,11 +200,11 @@ async def delete_cloud_nio( @router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") async def start_cloud_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node_capture_data: schemas.NodeCapture, - node: Cloud = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + node_capture_data: schemas.NodeCapture, + node: Cloud = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -227,10 +220,7 @@ async def start_cloud_capture( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT ) async def stop_cloud_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: Cloud = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: Cloud = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -242,10 +232,7 @@ async def stop_cloud_capture( @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") async def stream_pcap_file( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: Cloud = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: Cloud = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. @@ -257,13 +244,9 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.put( - "/{node_id}/markers/{marker_name}" -) +@router.put("/{node_id}/markers/{marker_name}") async def toggle_cloud_marker( - marker_name: str, - toggle_data: schemas.MarkerToggle, - node: Cloud = Depends(dep_node) + marker_name: str, toggle_data: schemas.MarkerToggle, node: Cloud = Depends(dep_node) ) -> dict: """ Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). @@ -278,19 +261,13 @@ async def toggle_cloud_marker( return {"marker_name": marker_name, "enabled": toggle_data.enabled} -@router.post( - "/{node_id}/markers/pause", - status_code=status.HTTP_204_NO_CONTENT -) +@router.post("/{node_id}/markers/pause", status_code=status.HTTP_204_NO_CONTENT) async def pause_cloud_markers(node: Cloud = Depends(dep_node)) -> None: await node._ubridge_marker_pause() -@router.post( - "/{node_id}/markers/resume", - status_code=status.HTTP_204_NO_CONTENT -) +@router.post("/{node_id}/markers/resume", status_code=status.HTTP_204_NO_CONTENT) async def resume_cloud_markers(node: Cloud = Depends(dep_node)) -> None: await node._ubridge_marker_resume() @@ -298,7 +275,7 @@ async def resume_cloud_markers(node: Cloud = Depends(dep_node)) -> None: @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/markers/{marker_name}", - status_code=status.HTTP_204_NO_CONTENT + status_code=status.HTTP_204_NO_CONTENT, ) async def delete_cloud_marker_capture( *, @@ -306,7 +283,7 @@ async def delete_cloud_marker_capture( adapter_number: int = Path(..., ge=0, le=0), port_number: int, link_id: str = "", - node: Cloud = Depends(dep_node) + node: Cloud = Depends(dep_node), ) -> None: """ Delete a marker's capture pcap (called by the controller when the marker is @@ -321,9 +298,7 @@ async def delete_cloud_marker_capture( @router.put("/{node_id}/markers/{marker_name}/rebuild") async def rebuild_cloud_marker( - marker_name: str, - rebuild_data: schemas.MarkerRebuild, - node: Cloud = Depends(dep_node) + marker_name: str, rebuild_data: schemas.MarkerRebuild, node: Cloud = Depends(dep_node) ) -> dict: """ Re-install a single marker filter with new BPF/tag/direction (delete + add, @@ -331,7 +306,11 @@ async def rebuild_cloud_marker( """ await node.rebuild_marker_filter( - marker_name, rebuild_data.link_id, rebuild_data.bpf, - rebuild_data.tag, rebuild_data.direction, rebuild_data.enabled, + marker_name, + rebuild_data.link_id, + rebuild_data.bpf, + rebuild_data.tag, + rebuild_data.direction, + rebuild_data.enabled, ) return {"marker_name": marker_name} diff --git a/gns3server/api/routes/compute/dependencies/authentication.py b/gns3server/api/routes/compute/dependencies/authentication.py index a5ad44b10..14ba07f91 100644 --- a/gns3server/api/routes/compute/dependencies/authentication.py +++ b/gns3server/api/routes/compute/dependencies/authentication.py @@ -32,7 +32,7 @@ security = HTTPBasic(auto_error=False) def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends(security)) -> None: """ Authenticate compute requests. - + Returns None if authentication is disabled or if authentication succeeds Raises HTTPException if authentication is required but credentials are invalid """ @@ -58,9 +58,9 @@ def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends headers={"WWW-Authenticate": "Basic"}, ) + async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]: - """ - """ + """ """ server_settings = Config.instance().settings.Server diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index a5e48c57d..6091cf37b 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -51,7 +51,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> DockerVM: response_model=schemas.Docker, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}}, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) -> schemas.Docker: """ @@ -87,10 +87,23 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) # fallback loop below only applies truly extra keys and does not # re-trigger console/aux port setter logging. for key in ( - "console", "console_type", "console_resolution", "console_http_port", - "console_http_path", "aux", "aux_type", "start_command", "environment", - "adapters", "mac_address", "extra_hosts", "extra_volumes", "extra_configs", - "memory", "cpus", "image_digest", + "console", + "console_type", + "console_resolution", + "console_http_port", + "console_http_path", + "aux", + "aux_type", + "start_command", + "environment", + "adapters", + "mac_address", + "extra_hosts", + "extra_volumes", + "extra_configs", + "memory", + "cpus", + "image_digest", ): node_data.pop(key, None) for name, value in node_data.items(): @@ -101,11 +114,7 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) return container.asdict() -@router.get( - "/{node_id}", - response_model=schemas.Docker, - dependencies=[Depends(compute_authentication)] -) +@router.get("/{node_id}", response_model=schemas.Docker, dependencies=[Depends(compute_authentication)]) def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker: """ Return a Docker node. @@ -114,11 +123,7 @@ def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker: return node.asdict() -@router.put( - "/{node_id}", - response_model=schemas.Docker, - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}", response_model=schemas.Docker, dependencies=[Depends(compute_authentication)]) async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker: """ Update a Docker node. @@ -160,11 +165,7 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D return node.asdict() -@router.post( - "/{node_id}/start", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Start a Docker node. @@ -173,11 +174,7 @@ async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.start() -@router.post( - "/{node_id}/stop", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Stop a Docker node. This is the explicit user stop — the only path that @@ -189,9 +186,7 @@ async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/suspend", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ @@ -202,9 +197,7 @@ async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/reload", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ @@ -214,11 +207,7 @@ async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.restart() -@router.post( - "/{node_id}/pause", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/pause", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Pause a Docker node. @@ -228,9 +217,7 @@ async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/unpause", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/unpause", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ @@ -240,11 +227,7 @@ async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None: await node.unpause() -@router.delete( - "/{node_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None: """ Delete a Docker node. @@ -258,11 +241,10 @@ async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None: "/{node_id}/duplicate", response_model=schemas.Docker, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def duplicate_docker_node( - destination_node_id: UUID = Body(..., embed=True), - node: DockerVM = Depends(dep_node) + destination_node_id: UUID = Body(..., embed=True), node: DockerVM = Depends(dep_node) ) -> schemas.Docker: """ Duplicate a Docker node. @@ -276,7 +258,7 @@ async def duplicate_docker_node( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_docker_node_nio( adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node) @@ -295,7 +277,7 @@ async def create_docker_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def update_docker_node_nio( adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node) @@ -318,13 +300,9 @@ async def update_docker_node_nio( @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) -async def delete_docker_node_nio( - adapter_number: int, - port_number: int, - node: DockerVM = Depends(dep_node) -) -> None: +async def delete_docker_node_nio(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)) -> None: """ Delete a NIO (Network Input/Output) from the node. The port number on the Docker node is always 0. @@ -335,13 +313,10 @@ async def delete_docker_node_nio( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def start_docker_node_capture( - adapter_number: int, - port_number: int, - node_capture_data: schemas.NodeCapture, - node: DockerVM = Depends(dep_node) + adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: DockerVM = Depends(dep_node) ) -> dict: """ Start a packet capture on the node. @@ -356,13 +331,9 @@ async def start_docker_node_capture( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) -async def stop_docker_node_capture( - adapter_number: int, - port_number: int, - node: DockerVM = Depends(dep_node) -) -> None: +async def stop_docker_node_capture(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)) -> None: """ Stop a packet capture on the node. The port number on the Docker node is always 0. @@ -373,12 +344,10 @@ async def stop_docker_node_capture( @router.get( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stream_pcap_file( - adapter_number: int, - port_number: int, - node: DockerVM = Depends(dep_node) + adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. @@ -392,8 +361,7 @@ async def stream_pcap_file( @router.websocket("/{node_id}/console/ws") async def console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: DockerVM = Depends(dep_node) + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), node: DockerVM = Depends(dep_node) ) -> None: """ Console WebSocket. @@ -403,12 +371,9 @@ async def console_ws( await node.start_websocket_console(websocket) -@router.websocket( - "/{node_id}/console/vnc" -) +@router.websocket("/{node_id}/console/vnc") async def vnc_console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: DockerVM = Depends(dep_node) + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), node: DockerVM = Depends(dep_node) ) -> None: """ VNC Console WebSocket. @@ -419,23 +384,16 @@ async def vnc_console_ws( @router.post( - "/{node_id}/console/reset", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reset_console(node: DockerVM = Depends(dep_node)) -> None: await node.reset_console() -@router.put( - "/{node_id}/markers/{marker_name}", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}", dependencies=[Depends(compute_authentication)]) async def toggle_docker_marker( - marker_name: str, - toggle_data: schemas.MarkerToggle, - node: DockerVM = Depends(dep_node) + marker_name: str, toggle_data: schemas.MarkerToggle, node: DockerVM = Depends(dep_node) ) -> dict: """ Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). @@ -451,9 +409,7 @@ async def toggle_docker_marker( @router.post( - "/{node_id}/markers/pause", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/pause", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def pause_docker_markers(node: DockerVM = Depends(dep_node)) -> None: @@ -461,9 +417,7 @@ async def pause_docker_markers(node: DockerVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/markers/resume", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/resume", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def resume_docker_markers(node: DockerVM = Depends(dep_node)) -> None: @@ -473,14 +427,10 @@ async def resume_docker_markers(node: DockerVM = Depends(dep_node)) -> None: @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/markers/{marker_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_docker_marker_capture( - marker_name: str, - adapter_number: int, - port_number: int, - link_id: str = "", - node: DockerVM = Depends(dep_node) + marker_name: str, adapter_number: int, port_number: int, link_id: str = "", node: DockerVM = Depends(dep_node) ) -> None: """ Delete a marker's capture pcap (called by the controller when the marker is @@ -493,14 +443,9 @@ async def delete_docker_marker_capture( await node.delete_marker_capture(marker_name, link_id, nio) -@router.put( - "/{node_id}/markers/{marker_name}/rebuild", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}/rebuild", dependencies=[Depends(compute_authentication)]) async def rebuild_docker_marker( - marker_name: str, - rebuild_data: schemas.MarkerRebuild, - node: DockerVM = Depends(dep_node) + marker_name: str, rebuild_data: schemas.MarkerRebuild, node: DockerVM = Depends(dep_node) ) -> dict: """ Re-install a single marker filter with new BPF/tag/direction (delete + add, @@ -508,7 +453,11 @@ async def rebuild_docker_marker( """ await node.rebuild_marker_filter( - marker_name, rebuild_data.link_id, rebuild_data.bpf, - rebuild_data.tag, rebuild_data.direction, rebuild_data.enabled, + marker_name, + rebuild_data.link_id, + rebuild_data.bpf, + rebuild_data.tag, + rebuild_data.direction, + rebuild_data.enabled, ) return {"marker_name": marker_name} diff --git a/gns3server/api/routes/compute/dynamips_nodes.py b/gns3server/api/routes/compute/dynamips_nodes.py index 41314f8a1..ce1c1b42b 100644 --- a/gns3server/api/routes/compute/dynamips_nodes.py +++ b/gns3server/api/routes/compute/dynamips_nodes.py @@ -55,7 +55,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> Router: response_model=schemas.Dynamips, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}}, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) -> schemas.Dynamips: """ @@ -86,11 +86,7 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) -> return vm.asdict() -@router.get( - "/{node_id}", - response_model=schemas.Dynamips, - dependencies=[Depends(compute_authentication)] -) +@router.get("/{node_id}", response_model=schemas.Dynamips, dependencies=[Depends(compute_authentication)]) def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips: """ Return Dynamips router. @@ -99,11 +95,7 @@ def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips: return node.asdict() -@router.put( - "/{node_id}", - response_model=schemas.Dynamips, - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}", response_model=schemas.Dynamips, dependencies=[Depends(compute_authentication)]) async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depends(dep_node)) -> schemas.Dynamips: """ Update a Dynamips router. @@ -114,11 +106,7 @@ async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depend return node.asdict() -@router.delete( - "/{node_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def delete_router(node: Router = Depends(dep_node)) -> None: """ Delete a Dynamips router. @@ -127,11 +115,7 @@ async def delete_router(node: Router = Depends(dep_node)) -> None: await Dynamips.instance().delete_node(node.id) -@router.post( - "/{node_id}/start", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def start_router(node: Router = Depends(dep_node)) -> None: """ Start a Dynamips router. @@ -144,11 +128,7 @@ async def start_router(node: Router = Depends(dep_node)) -> None: await node.start() -@router.post( - "/{node_id}/stop", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def stop_router(node: Router = Depends(dep_node)) -> None: """ Stop a Dynamips router. @@ -158,9 +138,7 @@ async def stop_router(node: Router = Depends(dep_node)) -> None: @router.post( - "/{node_id}/suspend", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def suspend_router(node: Router = Depends(dep_node)) -> None: @@ -168,9 +146,7 @@ async def suspend_router(node: Router = Depends(dep_node)) -> None: @router.post( - "/{node_id}/resume", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def resume_router(node: Router = Depends(dep_node)) -> None: """ @@ -181,9 +157,7 @@ async def resume_router(node: Router = Depends(dep_node)) -> None: @router.post( - "/{node_id}/reload", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reload_router(node: Router = Depends(dep_node)) -> None: """ @@ -197,13 +171,10 @@ async def reload_router(node: Router = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_nio( - adapter_number: int, - port_number: int, - nio_data: schemas.UDPNIO, - node: Router = Depends(dep_node) + adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: Router = Depends(dep_node) ) -> schemas.UDPNIO: """ Add a NIO (Network Input/Output) to the node. @@ -218,13 +189,10 @@ async def create_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def update_nio( - adapter_number: int, - port_number: int, - nio_data: schemas.UDPNIO, - node: Router = Depends(dep_node) + adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: Router = Depends(dep_node) ) -> schemas.UDPNIO: """ Update a NIO (Network Input/Output) on the node. @@ -242,7 +210,7 @@ async def update_nio( @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_nio(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None: """ @@ -255,13 +223,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: Router = Depen @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def start_capture( - adapter_number: int, - port_number: int, - node_capture_data: schemas.NodeCapture, - node: Router = Depends(dep_node) + adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: Router = Depends(dep_node) ) -> dict: """ Start a packet capture on the node. @@ -275,7 +240,7 @@ async def start_capture( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stop_capture(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None: """ @@ -287,12 +252,10 @@ async def stop_capture(adapter_number: int, port_number: int, node: Router = Dep @router.get( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stream_pcap_file( - adapter_number: int, - port_number: int, - node: Router = Depends(dep_node) + adapter_number: int, port_number: int, node: Router = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. @@ -303,10 +266,7 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.get( - "/{node_id}/idlepc_proposals", - dependencies=[Depends(compute_authentication)] -) +@router.get("/{node_id}/idlepc_proposals", dependencies=[Depends(compute_authentication)]) async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]: """ Retrieve Dynamips idle-pc proposals @@ -316,10 +276,7 @@ async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]: return await node.get_idle_pc_prop() -@router.get( - "/{node_id}/auto_idlepc", - dependencies=[Depends(compute_authentication)] -) +@router.get("/{node_id}/auto_idlepc", dependencies=[Depends(compute_authentication)]) async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict: """ Get an automatically guessed best idle-pc value. @@ -333,9 +290,11 @@ async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict: "/{node_id}/duplicate", response_model=schemas.Dynamips, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) -async def duplicate_router(destination_node_id: UUID = Body(..., embed=True), node: Router = Depends(dep_node)) -> schemas.Dynamips: +async def duplicate_router( + destination_node_id: UUID = Body(..., embed=True), node: Router = Depends(dep_node) +) -> schemas.Dynamips: """ Duplicate a router. """ @@ -346,9 +305,7 @@ async def duplicate_router(destination_node_id: UUID = Body(..., embed=True), no @router.websocket("/{node_id}/console/ws") async def console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: Router = Depends(dep_node) - + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), node: Router = Depends(dep_node) ) -> None: """ Console WebSocket. @@ -359,23 +316,16 @@ async def console_ws( @router.post( - "/{node_id}/console/reset", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reset_console(node: Router = Depends(dep_node)) -> None: await node.reset_console() -@router.put( - "/{node_id}/markers/{marker_name}", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}", dependencies=[Depends(compute_authentication)]) async def toggle_dynamips_marker( - marker_name: str, - toggle_data: schemas.MarkerToggle, - node: Router = Depends(dep_node) + marker_name: str, toggle_data: schemas.MarkerToggle, node: Router = Depends(dep_node) ) -> dict: """ Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). @@ -391,9 +341,7 @@ async def toggle_dynamips_marker( @router.post( - "/{node_id}/markers/pause", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/pause", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def pause_dynamips_markers(node: Router = Depends(dep_node)) -> None: @@ -401,9 +349,7 @@ async def pause_dynamips_markers(node: Router = Depends(dep_node)) -> None: @router.post( - "/{node_id}/markers/resume", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/resume", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def resume_dynamips_markers(node: Router = Depends(dep_node)) -> None: @@ -413,14 +359,10 @@ async def resume_dynamips_markers(node: Router = Depends(dep_node)) -> None: @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/markers/{marker_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_dynamips_marker_capture( - marker_name: str, - adapter_number: int, - port_number: int, - link_id: str = "", - node: Router = Depends(dep_node) + marker_name: str, adapter_number: int, port_number: int, link_id: str = "", node: Router = Depends(dep_node) ) -> None: """ Delete a marker's capture pcap (called by the controller when the marker is @@ -433,14 +375,9 @@ async def delete_dynamips_marker_capture( await node.delete_marker_capture(marker_name, link_id, nio) -@router.put( - "/{node_id}/markers/{marker_name}/rebuild", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}/rebuild", dependencies=[Depends(compute_authentication)]) async def rebuild_dynamips_marker( - marker_name: str, - rebuild_data: schemas.MarkerRebuild, - node: Router = Depends(dep_node) + marker_name: str, rebuild_data: schemas.MarkerRebuild, node: Router = Depends(dep_node) ) -> dict: """ Re-install a single marker filter with new BPF/tag/direction (delete + add, @@ -448,7 +385,11 @@ async def rebuild_dynamips_marker( """ await node.rebuild_marker_filter( - marker_name, rebuild_data.link_id, rebuild_data.bpf, - rebuild_data.tag, rebuild_data.direction, rebuild_data.enabled, + marker_name, + rebuild_data.link_id, + rebuild_data.bpf, + rebuild_data.tag, + rebuild_data.direction, + rebuild_data.enabled, ) return {"marker_name": marker_name} diff --git a/gns3server/api/routes/compute/ethernet_hub_nodes.py b/gns3server/api/routes/compute/ethernet_hub_nodes.py index 22f7a3b6d..350105789 100644 --- a/gns3server/api/routes/compute/ethernet_hub_nodes.py +++ b/gns3server/api/routes/compute/ethernet_hub_nodes.py @@ -91,8 +91,7 @@ async def duplicate_ethernet_hub( @router.put("/{node_id}", response_model=schemas.EthernetHub) async def update_ethernet_hub( - node_data: schemas.EthernetHubUpdate, - node: EthernetHub = Depends(dep_node) + node_data: schemas.EthernetHubUpdate, node: EthernetHub = Depends(dep_node) ) -> schemas.EthernetHub: """ Update an Ethernet hub. @@ -124,8 +123,7 @@ def start_ethernet_hub(node: EthernetHub = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Start is not supported for Ethernet hubs" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Start is not supported for Ethernet hubs" ) @@ -136,8 +134,7 @@ def stop_ethernet_hub(node: EthernetHub = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Stop is not supported for Ethernet hubs" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Stop is not supported for Ethernet hubs" ) @@ -148,8 +145,7 @@ def suspend_ethernet_hub(node: EthernetHub = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Suspend is not supported for Ethernet hubs" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Suspend is not supported for Ethernet hubs" ) @@ -159,11 +155,11 @@ def suspend_ethernet_hub(node: EthernetHub = Depends(dep_node)) -> None: response_model=schemas.UDPNIO, ) async def create_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: schemas.UDPNIO, - node: EthernetHub = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: schemas.UDPNIO, + node: EthernetHub = Depends(dep_node), ) -> schemas.UDPNIO: """ Add a NIO (Network Input/Output) to the node. @@ -177,10 +173,7 @@ async def create_nio( @router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) async def delete_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: EthernetHub = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: EthernetHub = Depends(dep_node) ) -> None: """ Delete a NIO (Network Input/Output) from the node. @@ -193,11 +186,11 @@ async def delete_nio( @router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") async def start_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node_capture_data: schemas.NodeCapture, - node: EthernetHub = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + node_capture_data: schemas.NodeCapture, + node: EthernetHub = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -213,10 +206,7 @@ async def start_capture( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT ) async def stop_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: EthernetHub = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: EthernetHub = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -228,10 +218,7 @@ async def stop_capture( @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") async def stream_pcap_file( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: EthernetHub = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: EthernetHub = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. diff --git a/gns3server/api/routes/compute/ethernet_switch_nodes.py b/gns3server/api/routes/compute/ethernet_switch_nodes.py index d6d253ca9..bedfac0b6 100644 --- a/gns3server/api/routes/compute/ethernet_switch_nodes.py +++ b/gns3server/api/routes/compute/ethernet_switch_nodes.py @@ -82,8 +82,7 @@ def get_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> schemas.Eth @router.post("/{node_id}/duplicate", response_model=schemas.EthernetSwitch, status_code=status.HTTP_201_CREATED) async def duplicate_ethernet_switch( - destination_node_id: UUID = Body(..., embed=True), - node: EthernetSwitch = Depends(dep_node) + destination_node_id: UUID = Body(..., embed=True), node: EthernetSwitch = Depends(dep_node) ) -> schemas.EthernetSwitch: """ Duplicate an Ethernet switch. @@ -95,8 +94,7 @@ async def duplicate_ethernet_switch( @router.put("/{node_id}", response_model=schemas.EthernetSwitch) async def update_ethernet_switch( - node_data: schemas.EthernetSwitchUpdate, - node: EthernetSwitch = Depends(dep_node) + node_data: schemas.EthernetSwitchUpdate, node: EthernetSwitch = Depends(dep_node) ) -> schemas.EthernetSwitch: """ Update an Ethernet switch. @@ -132,8 +130,7 @@ def start_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Start is not supported for Ethernet switches" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Start is not supported for Ethernet switches" ) @@ -144,8 +141,7 @@ def stop_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Stop is not supported for Ethernet switches" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Stop is not supported for Ethernet switches" ) @@ -156,8 +152,7 @@ def suspend_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Suspend is not supported for Ethernet switches" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Suspend is not supported for Ethernet switches" ) @@ -169,8 +164,7 @@ def reload_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Reload is not supported for Ethernet switches" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Reload is not supported for Ethernet switches" ) @@ -180,11 +174,11 @@ def reload_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None: response_model=schemas.UDPNIO, ) async def create_ethernet_switch_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: schemas.UDPNIO, - node: EthernetSwitch = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: schemas.UDPNIO, + node: EthernetSwitch = Depends(dep_node), ) -> schemas.UDPNIO: nio = Builtin.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True)) @@ -198,11 +192,11 @@ async def create_ethernet_switch_nio( response_model=schemas.UDPNIO, ) async def update_ethernet_switch_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: schemas.UDPNIO, - node: EthernetSwitch = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: schemas.UDPNIO, + node: EthernetSwitch = Depends(dep_node), ) -> schemas.UDPNIO: """ Update a NIO (Network Input/Output) on the node: re-apply the packet @@ -221,10 +215,7 @@ async def update_ethernet_switch_nio( @router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) async def delete_ethernet_switch_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: EthernetSwitch = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: EthernetSwitch = Depends(dep_node) ) -> None: """ Delete a NIO (Network Input/Output) from the node. @@ -236,11 +227,11 @@ async def delete_ethernet_switch_nio( @router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") async def start_ethernet_switch_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node_capture_data: schemas.NodeCapture, - node: EthernetSwitch = Depends(dep_node), + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + node_capture_data: schemas.NodeCapture, + node: EthernetSwitch = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -256,10 +247,7 @@ async def start_ethernet_switch_capture( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT ) async def stop_ethernet_switch_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: EthernetSwitch = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: EthernetSwitch = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -271,10 +259,7 @@ async def stop_ethernet_switch_capture( @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") async def stream_pcap_file( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: EthernetSwitch = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: EthernetSwitch = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. @@ -288,9 +273,7 @@ async def stream_pcap_file( @router.put("/{node_id}/markers/{marker_name}") async def toggle_ethernet_switch_marker( - marker_name: str, - toggle_data: schemas.MarkerToggle, - node: EthernetSwitch = Depends(dep_node) + marker_name: str, toggle_data: schemas.MarkerToggle, node: EthernetSwitch = Depends(dep_node) ) -> dict: """ Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). @@ -322,12 +305,12 @@ async def resume_ethernet_switch_markers(node: EthernetSwitch = Depends(dep_node status_code=status.HTTP_204_NO_CONTENT, ) async def delete_ethernet_switch_marker_capture( - *, - marker_name: str, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - link_id: str = "", - node: EthernetSwitch = Depends(dep_node) + *, + marker_name: str, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + link_id: str = "", + node: EthernetSwitch = Depends(dep_node), ) -> None: """ Delete a marker's capture pcap (called by the controller when the marker is @@ -342,9 +325,7 @@ async def delete_ethernet_switch_marker_capture( @router.put("/{node_id}/markers/{marker_name}/rebuild") async def rebuild_ethernet_switch_marker( - marker_name: str, - rebuild_data: schemas.MarkerRebuild, - node: EthernetSwitch = Depends(dep_node) + marker_name: str, rebuild_data: schemas.MarkerRebuild, node: EthernetSwitch = Depends(dep_node) ) -> dict: """ Re-install a single marker filter with new BPF/tag/direction (delete + add, @@ -352,7 +333,11 @@ async def rebuild_ethernet_switch_marker( """ await node.rebuild_marker_filter( - marker_name, rebuild_data.link_id, rebuild_data.bpf, - rebuild_data.tag, rebuild_data.direction, rebuild_data.enabled, + marker_name, + rebuild_data.link_id, + rebuild_data.bpf, + rebuild_data.tag, + rebuild_data.direction, + rebuild_data.enabled, ) return {"marker_name": marker_name} diff --git a/gns3server/api/routes/compute/frame_relay_switch_nodes.py b/gns3server/api/routes/compute/frame_relay_switch_nodes.py index aa81896e3..f425b3200 100644 --- a/gns3server/api/routes/compute/frame_relay_switch_nodes.py +++ b/gns3server/api/routes/compute/frame_relay_switch_nodes.py @@ -51,8 +51,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> FrameRelaySwitch: responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Frame Relay switch node"}}, ) async def create_frame_relay_switch( - project_id: UUID, - node_data: schemas.FrameRelaySwitchCreate + project_id: UUID, node_data: schemas.FrameRelaySwitchCreate ) -> schemas.FrameRelaySwitch: """ Create a new Frame Relay switch node. @@ -82,8 +81,7 @@ def get_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)) -> schema @router.post("/{node_id}/duplicate", response_model=schemas.FrameRelaySwitch, status_code=status.HTTP_201_CREATED) async def duplicate_frame_relay_switch( - destination_node_id: UUID = Body(..., embed=True), - node: FrameRelaySwitch = Depends(dep_node) + destination_node_id: UUID = Body(..., embed=True), node: FrameRelaySwitch = Depends(dep_node) ) -> schemas.FrameRelaySwitch: """ Duplicate a Frame Relay switch node. @@ -95,8 +93,7 @@ async def duplicate_frame_relay_switch( @router.put("/{node_id}", response_model=schemas.FrameRelaySwitch) async def update_frame_relay_switch( - node_data: schemas.FrameRelaySwitchUpdate, - node: FrameRelaySwitch = Depends(dep_node) + node_data: schemas.FrameRelaySwitchUpdate, node: FrameRelaySwitch = Depends(dep_node) ) -> schemas.FrameRelaySwitch: """ Update an Frame Relay switch node. @@ -127,8 +124,7 @@ def start_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)) -> None """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Start is not supported for Frame Relay switches" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Start is not supported for Frame Relay switches" ) @@ -139,8 +135,7 @@ def stop_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Stop is not supported for Frame Relay switches" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Stop is not supported for Frame Relay switches" ) @@ -151,8 +146,7 @@ def suspend_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)) -> No """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Suspend is not supported for Frame Relay switches" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Suspend is not supported for Frame Relay switches" ) @@ -162,11 +156,11 @@ def suspend_frame_relay_switch(node: FrameRelaySwitch = Depends(dep_node)) -> No response_model=schemas.UDPNIO, ) async def create_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: schemas.UDPNIO, - node: FrameRelaySwitch = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: schemas.UDPNIO, + node: FrameRelaySwitch = Depends(dep_node), ) -> schemas.UDPNIO: """ Add a NIO (Network Input/Output) to the node. @@ -180,10 +174,7 @@ async def create_nio( @router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) async def delete_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: FrameRelaySwitch = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: FrameRelaySwitch = Depends(dep_node) ) -> None: """ Remove a NIO (Network Input/Output) from the node. @@ -196,11 +187,11 @@ async def delete_nio( @router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") async def start_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node_capture_data: schemas.NodeCapture, - node: FrameRelaySwitch = Depends(dep_node), + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + node_capture_data: schemas.NodeCapture, + node: FrameRelaySwitch = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -216,10 +207,7 @@ async def start_capture( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT ) async def stop_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: FrameRelaySwitch = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: FrameRelaySwitch = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -231,10 +219,7 @@ async def stop_capture( @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") async def stream_pcap_file( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: FrameRelaySwitch = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: FrameRelaySwitch = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. diff --git a/gns3server/api/routes/compute/iou_nodes.py b/gns3server/api/routes/compute/iou_nodes.py index b826b21d9..0282d0f0f 100644 --- a/gns3server/api/routes/compute/iou_nodes.py +++ b/gns3server/api/routes/compute/iou_nodes.py @@ -52,7 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> IOUVM: response_model=schemas.IOU, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}}, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> schemas.IOU: """ @@ -85,11 +85,7 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch return vm.asdict() -@router.get( - "/{node_id}", - response_model=schemas.IOU, - dependencies=[Depends(compute_authentication)] -) +@router.get("/{node_id}", response_model=schemas.IOU, dependencies=[Depends(compute_authentication)]) def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU: """ Return an IOU node. @@ -98,11 +94,7 @@ def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU: return node.asdict() -@router.put( - "/{node_id}", - response_model=schemas.IOU, - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}", response_model=schemas.IOU, dependencies=[Depends(compute_authentication)]) async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(dep_node)) -> schemas.IOU: """ Update an IOU node. @@ -123,11 +115,7 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de return node.asdict() -@router.delete( - "/{node_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None: """ Delete an IOU node. @@ -140,11 +128,10 @@ async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None: "/{node_id}/duplicate", response_model=schemas.IOU, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def duplicate_iou_node( - destination_node_id: UUID = Body(..., embed=True), - node: IOUVM = Depends(dep_node) + destination_node_id: UUID = Body(..., embed=True), node: IOUVM = Depends(dep_node) ) -> schemas.IOU: """ Duplicate an IOU node. @@ -154,11 +141,7 @@ async def duplicate_iou_node( return new_node.asdict() -@router.post( - "/{node_id}/start", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep_node)) -> None: """ Start an IOU node. @@ -172,11 +155,7 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep await node.start() -@router.post( - "/{node_id}/stop", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None: """ Stop an IOU node. @@ -185,25 +164,17 @@ async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None: await node.stop() -@router.post( - "/{node_id}/suspend", - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/suspend", dependencies=[Depends(compute_authentication)]) def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None: """ Suspend an IOU node. """ - raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Suspend is not supported for IOU nodes" - ) + raise HTTPException(status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Suspend is not supported for IOU nodes") @router.post( - "/{node_id}/reload", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None: """ @@ -217,7 +188,7 @@ async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_iou_node_nio( adapter_number: int, @@ -238,7 +209,7 @@ async def create_iou_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def update_iou_node_nio( adapter_number: int, @@ -263,7 +234,7 @@ async def update_iou_node_nio( @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None: """ @@ -275,13 +246,10 @@ async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def start_iou_node_capture( - adapter_number: int, - port_number: int, - node_capture_data: schemas.NodeCapture, - node: IOUVM = Depends(dep_node) + adapter_number: int, port_number: int, node_capture_data: schemas.NodeCapture, node: IOUVM = Depends(dep_node) ) -> dict: """ Start a packet capture on the node. @@ -295,7 +263,7 @@ async def start_iou_node_capture( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None: """ @@ -307,13 +275,9 @@ async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOU @router.get( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) -async def stream_pcap_file( - adapter_number: int, - port_number: int, - node: IOUVM = Depends(dep_node) -) -> StreamingResponse: +async def stream_pcap_file(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> StreamingResponse: """ Stream the pcap capture file. """ @@ -327,8 +291,7 @@ async def stream_pcap_file( "/{node_id}/console/ws", ) async def console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: IOUVM = Depends(dep_node) + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), node: IOUVM = Depends(dep_node) ) -> None: """ Console WebSocket. @@ -339,23 +302,16 @@ async def console_ws( @router.post( - "/{node_id}/console/reset", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reset_console(node: IOUVM = Depends(dep_node)) -> None: await node.reset_console() -@router.put( - "/{node_id}/markers/{marker_name}", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}", dependencies=[Depends(compute_authentication)]) async def toggle_iou_marker( - marker_name: str, - toggle_data: schemas.MarkerToggle, - node: IOUVM = Depends(dep_node) + marker_name: str, toggle_data: schemas.MarkerToggle, node: IOUVM = Depends(dep_node) ) -> dict: """ Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). @@ -371,9 +327,7 @@ async def toggle_iou_marker( @router.post( - "/{node_id}/markers/pause", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/pause", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def pause_iou_markers(node: IOUVM = Depends(dep_node)) -> None: @@ -381,9 +335,7 @@ async def pause_iou_markers(node: IOUVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/markers/resume", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/resume", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def resume_iou_markers(node: IOUVM = Depends(dep_node)) -> None: @@ -393,14 +345,10 @@ async def resume_iou_markers(node: IOUVM = Depends(dep_node)) -> None: @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/markers/{marker_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_iou_marker_capture( - marker_name: str, - adapter_number: int, - port_number: int, - link_id: str = "", - node: IOUVM = Depends(dep_node) + marker_name: str, adapter_number: int, port_number: int, link_id: str = "", node: IOUVM = Depends(dep_node) ) -> None: """ Delete a marker's capture pcap (called by the controller when the marker is @@ -413,14 +361,9 @@ async def delete_iou_marker_capture( await node.delete_marker_capture(marker_name, link_id, nio) -@router.put( - "/{node_id}/markers/{marker_name}/rebuild", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}/rebuild", dependencies=[Depends(compute_authentication)]) async def rebuild_iou_marker( - marker_name: str, - rebuild_data: schemas.MarkerRebuild, - node: IOUVM = Depends(dep_node) + marker_name: str, rebuild_data: schemas.MarkerRebuild, node: IOUVM = Depends(dep_node) ) -> dict: """ Re-install a single marker filter with new BPF/tag/direction (delete + add, @@ -428,7 +371,11 @@ async def rebuild_iou_marker( """ await node.rebuild_marker_filter( - marker_name, rebuild_data.link_id, rebuild_data.bpf, - rebuild_data.tag, rebuild_data.direction, rebuild_data.enabled, + marker_name, + rebuild_data.link_id, + rebuild_data.bpf, + rebuild_data.tag, + rebuild_data.direction, + rebuild_data.enabled, ) return {"marker_name": marker_name} diff --git a/gns3server/api/routes/compute/nat_nodes.py b/gns3server/api/routes/compute/nat_nodes.py index d11b7975d..60654b603 100644 --- a/gns3server/api/routes/compute/nat_nodes.py +++ b/gns3server/api/routes/compute/nat_nodes.py @@ -117,10 +117,7 @@ async def stop_nat_node(node: Nat = Depends(dep_node)) -> None: Stop a NAT node. """ - raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Stop is not supported for NAT nodes" - ) + raise HTTPException(status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Stop is not supported for NAT nodes") @router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT) @@ -129,10 +126,7 @@ async def suspend_nat_node(node: Nat = Depends(dep_node)) -> None: Suspend a NAT node. """ - raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Suspend is not supported for NAT nodes" - ) + raise HTTPException(status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Suspend is not supported for NAT nodes") @router.post( @@ -141,11 +135,11 @@ async def suspend_nat_node(node: Nat = Depends(dep_node)) -> None: response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], ) async def create_nat_node_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], - node: Nat = Depends(dep_node), + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + node: Nat = Depends(dep_node), ) -> Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]: """ Add a NIO (Network Input/Output) to the node. @@ -163,11 +157,11 @@ async def create_nat_node_nio( response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], ) async def update_nat_node_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], - node: Nat = Depends(dep_node), + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO], + node: Nat = Depends(dep_node), ) -> Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO]: """ Update a NIO (Network Input/Output) to the node. @@ -184,10 +178,7 @@ async def update_nat_node_nio( @router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT) async def delete_nat_node_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: Nat = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: Nat = Depends(dep_node) ) -> None: """ Remove a NIO (Network Input/Output) from the node. @@ -199,11 +190,11 @@ async def delete_nat_node_nio( @router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start") async def start_nat_node_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node_capture_data: schemas.NodeCapture, - node: Nat = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + node_capture_data: schemas.NodeCapture, + node: Nat = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -219,10 +210,7 @@ async def start_nat_node_capture( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT ) async def stop_nat_node_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: Nat = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: Nat = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -234,10 +222,7 @@ async def stop_nat_node_capture( @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream") async def stream_pcap_file( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: Nat = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: Nat = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. diff --git a/gns3server/api/routes/compute/notifications.py b/gns3server/api/routes/compute/notifications.py index f784e3c34..2c6ca4ad5 100644 --- a/gns3server/api/routes/compute/notifications.py +++ b/gns3server/api/routes/compute/notifications.py @@ -18,7 +18,6 @@ API routes for compute notifications. """ - from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect from typing import Union from websockets.exceptions import ConnectionClosed, WebSocketException @@ -53,7 +52,6 @@ async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(w if __name__ == "__main__": - import uvicorn from fastapi import FastAPI from starlette.responses import HTMLResponse diff --git a/gns3server/api/routes/compute/projects.py b/gns3server/api/routes/compute/projects.py index 54d7e460e..7c1248cc5 100644 --- a/gns3server/api/routes/compute/projects.py +++ b/gns3server/api/routes/compute/projects.py @@ -86,8 +86,7 @@ def create_compute_project(project_data: schemas.ProjectCreate) -> schemas.Proje @router.put("/projects/{project_id}", response_model=schemas.Project) async def update_compute_project( - project_data: schemas.ProjectUpdate, - project: Project = Depends(dep_project) + project_data: schemas.ProjectUpdate, project: Project = Depends(dep_project) ) -> schemas.Project: """ Update project on the compute. @@ -195,6 +194,7 @@ def _get_existing_nio(node, adapter_number, port_number): # via get_nio(port). if hasattr(node, "get_nio"): import inspect as _inspect + if len(_inspect.signature(node.get_nio).parameters) >= 2: return node.get_nio(adapter_number, port_number) return node.get_nio(port_number) @@ -242,9 +242,9 @@ async def _update_nio_binding(node, adapter_number, port_number, nio): status_code=status.HTTP_201_CREATED, ) async def create_batch_nios( - project_id: UUID, - batch: schemas.BatchNIOCreate, - project: Project = Depends(dep_project), + project_id: UUID, + batch: schemas.BatchNIOCreate, + project: Project = Depends(dep_project), ) -> dict: """ Create many NIO bindings across nodes in a single request. @@ -281,9 +281,7 @@ async def create_batch_nios( nio = node.manager.create_nio(nio_settings) await _add_nio_binding(node, entry.adapter_number, entry.port_number, nio) - await asyncio.gather( - *[_create_one_node(nid, ents) for nid, ents in per_node.items()] - ) + await asyncio.gather(*[_create_one_node(nid, ents) for nid, ents in per_node.items()]) return {"added": len(batch.nios)} @@ -292,9 +290,9 @@ async def create_batch_nios( status_code=status.HTTP_200_OK, ) async def update_batch_nios( - project_id: UUID, - batch: schemas.BatchNIOCreate, - project: Project = Depends(dep_project), + project_id: UUID, + batch: schemas.BatchNIOCreate, + project: Project = Depends(dep_project), ) -> dict: """ Update many NIO bindings (filters + markers) across nodes in a single @@ -322,9 +320,7 @@ async def update_batch_nios( nio.markers = e.nio.markers or {} await _update_nio_binding(node, e.adapter_number, e.port_number, nio) - await asyncio.gather( - *[_update_one_node(nid, ents) for nid, ents in per_node.items()] - ) + await asyncio.gather(*[_update_one_node(nid, ents) for nid, ents in per_node.items()]) return {"updated": len(batch.nios)} @@ -343,7 +339,7 @@ async def get_compute_node_files( node_id: str, project: Project = Depends(dep_project), path: str = Query("", description="Subdirectory path within node directory"), - recursive: bool = Query(False, description="Recursively list all files") + recursive: bool = Query(False, description="Recursively list all files"), ) -> List[schemas.NodeFile]: """ Return files belonging to a specific node with detailed metadata. @@ -374,11 +370,7 @@ async def get_compute_project_file(file_path: str, project: Project = Depends(de @router.post("/projects/{project_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT) -async def write_compute_project_file( - file_path: str, - request: Request, - project: Project = Depends(dep_project) -) -> None: +async def write_compute_project_file(file_path: str, request: Request, project: Project = Depends(dep_project)) -> None: file_path = urllib.parse.unquote(file_path) path = os.path.normpath(file_path) @@ -405,10 +397,7 @@ async def write_compute_project_file( @router.delete("/projects/{project_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_compute_project_file( - file_path: str, - project: Project = Depends(dep_project) -) -> None: +async def delete_compute_project_file(file_path: str, project: Project = Depends(dep_project)) -> None: file_path = urllib.parse.unquote(file_path) path = os.path.normpath(file_path) diff --git a/gns3server/api/routes/compute/qemu_nodes.py b/gns3server/api/routes/compute/qemu_nodes.py index ed9345308..4f8498847 100644 --- a/gns3server/api/routes/compute/qemu_nodes.py +++ b/gns3server/api/routes/compute/qemu_nodes.py @@ -33,6 +33,7 @@ from gns3server.compute.qemu.qemu_vm import QemuVM from .dependencies.authentication import compute_authentication, ws_compute_authentication import logging + log = logging.getLogger(__name__) responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}} @@ -55,7 +56,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> QemuVM: response_model=schemas.Qemu, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}}, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> schemas.Qemu: """ @@ -117,11 +118,7 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s return vm.asdict() -@router.get( - "/{node_id}", - response_model=schemas.Qemu, - dependencies=[Depends(compute_authentication)] -) +@router.get("/{node_id}", response_model=schemas.Qemu, dependencies=[Depends(compute_authentication)]) def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu: """ Return a Qemu node. @@ -130,11 +127,7 @@ def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu: return node.asdict() -@router.put( - "/{node_id}", - response_model=schemas.Qemu, - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}", response_model=schemas.Qemu, dependencies=[Depends(compute_authentication)]) async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends(dep_node)) -> schemas.Qemu: """ Update a Qemu node. @@ -150,11 +143,7 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends return node.asdict() -@router.delete( - "/{node_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ Delete a Qemu node. @@ -167,11 +156,10 @@ async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None: "/{node_id}/duplicate", response_model=schemas.Qemu, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def duplicate_qemu_node( - destination_node_id: UUID = Body(..., embed=True), - node: QemuVM = Depends(dep_node) + destination_node_id: UUID = Body(..., embed=True), node: QemuVM = Depends(dep_node) ) -> schemas.Qemu: """ Duplicate a Qemu node. @@ -184,12 +172,10 @@ async def duplicate_qemu_node( @router.post( "/{node_id}/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_qemu_disk_image( - disk_name: str, - disk_data: schemas.QemuDiskImageCreate, - node: QemuVM = Depends(dep_node) + disk_name: str, disk_data: schemas.QemuDiskImageCreate, node: QemuVM = Depends(dep_node) ) -> None: """ Create a Qemu disk image. @@ -202,12 +188,10 @@ async def create_qemu_disk_image( @router.put( "/{node_id}/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def update_qemu_disk_image( - disk_name: str, - disk_data: schemas.QemuDiskImageUpdate, - node: QemuVM = Depends(dep_node) + disk_name: str, disk_data: schemas.QemuDiskImageUpdate, node: QemuVM = Depends(dep_node) ) -> None: """ Update a Qemu disk image. @@ -220,12 +204,9 @@ async def update_qemu_disk_image( @router.delete( "/{node_id}/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) -async def delete_qemu_disk_image( - disk_name: str, - node: QemuVM = Depends(dep_node) -) -> None: +async def delete_qemu_disk_image(disk_name: str, node: QemuVM = Depends(dep_node)) -> None: """ Delete a Qemu disk image. """ @@ -233,11 +214,7 @@ async def delete_qemu_disk_image( node.delete_disk_image(disk_name) -@router.post( - "/{node_id}/start", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ Start a Qemu node. @@ -246,11 +223,7 @@ async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None: await node.start() -@router.post( - "/{node_id}/stop", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ Stop a Qemu node. @@ -260,9 +233,7 @@ async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/reload", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ @@ -273,9 +244,7 @@ async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/suspend", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ @@ -286,9 +255,7 @@ async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/resume", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None: """ @@ -302,14 +269,14 @@ async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_qemu_node_nio( - *, - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - nio_data: schemas.UDPNIO, - node: QemuVM = Depends(dep_node) + *, + adapter_number: int, + port_number: int = Path(..., ge=0, le=0), + nio_data: schemas.UDPNIO, + node: QemuVM = Depends(dep_node), ) -> schemas.UDPNIO: """ Add a NIO (Network Input/Output) to the node. @@ -325,14 +292,14 @@ async def create_qemu_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def update_qemu_node_nio( - *, - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - nio_data: schemas.UDPNIO, - node: QemuVM = Depends(dep_node) + *, + adapter_number: int, + port_number: int = Path(..., ge=0, le=0), + nio_data: schemas.UDPNIO, + node: QemuVM = Depends(dep_node), ) -> schemas.UDPNIO: """ Update a NIO (Network Input/Output) on the node. @@ -352,12 +319,10 @@ async def update_qemu_node_nio( @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_qemu_node_nio( - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node: QemuVM = Depends(dep_node) + adapter_number: int, port_number: int = Path(..., ge=0, le=0), node: QemuVM = Depends(dep_node) ) -> None: """ Delete a NIO (Network Input/Output) from the node. @@ -369,14 +334,14 @@ async def delete_qemu_node_nio( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def start_qemu_node_capture( - *, - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node_capture_data: schemas.NodeCapture, - node: QemuVM = Depends(dep_node) + *, + adapter_number: int, + port_number: int = Path(..., ge=0, le=0), + node_capture_data: schemas.NodeCapture, + node: QemuVM = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -391,12 +356,10 @@ async def start_qemu_node_capture( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stop_qemu_node_capture( - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node: QemuVM = Depends(dep_node) + adapter_number: int, port_number: int = Path(..., ge=0, le=0), node: QemuVM = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -408,12 +371,11 @@ async def stop_qemu_node_capture( @router.get( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stream_pcap_file( - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node: QemuVM = Depends(dep_node)) -> StreamingResponse: + adapter_number: int, port_number: int = Path(..., ge=0, le=0), node: QemuVM = Depends(dep_node) +) -> StreamingResponse: """ Stream the pcap capture file. The port number on the Qemu node is always 0. @@ -424,12 +386,9 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.websocket( - "/{node_id}/console/ws" -) +@router.websocket("/{node_id}/console/ws") async def console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: QemuVM = Depends(dep_node) + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), node: QemuVM = Depends(dep_node) ) -> None: """ Console WebSocket. @@ -439,12 +398,9 @@ async def console_ws( await node.start_websocket_console(websocket) -@router.websocket( - "/{node_id}/console/vnc" -) +@router.websocket("/{node_id}/console/vnc") async def vnc_console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: QemuVM = Depends(dep_node) + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), node: QemuVM = Depends(dep_node) ) -> None: """ VNC Console WebSocket. @@ -455,23 +411,16 @@ async def vnc_console_ws( @router.post( - "/{node_id}/console/reset", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reset_console(node: QemuVM = Depends(dep_node)) -> None: await node.reset_console() -@router.put( - "/{node_id}/markers/{marker_name}", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}", dependencies=[Depends(compute_authentication)]) async def toggle_qemu_marker( - marker_name: str, - toggle_data: schemas.MarkerToggle, - node: QemuVM = Depends(dep_node) + marker_name: str, toggle_data: schemas.MarkerToggle, node: QemuVM = Depends(dep_node) ) -> dict: """ Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). @@ -487,9 +436,7 @@ async def toggle_qemu_marker( @router.post( - "/{node_id}/markers/pause", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/pause", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def pause_qemu_markers(node: QemuVM = Depends(dep_node)) -> None: @@ -497,9 +444,7 @@ async def pause_qemu_markers(node: QemuVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/markers/resume", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/resume", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def resume_qemu_markers(node: QemuVM = Depends(dep_node)) -> None: @@ -509,14 +454,14 @@ async def resume_qemu_markers(node: QemuVM = Depends(dep_node)) -> None: @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/markers/{marker_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_qemu_marker_capture( marker_name: str, adapter_number: int, port_number: int = Path(..., ge=0, le=0), link_id: str = "", - node: QemuVM = Depends(dep_node) + node: QemuVM = Depends(dep_node), ) -> None: """ Delete a marker's capture pcap (called by the controller when the marker is @@ -529,14 +474,9 @@ async def delete_qemu_marker_capture( await node.delete_marker_capture(marker_name, link_id, nio) -@router.put( - "/{node_id}/markers/{marker_name}/rebuild", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}/rebuild", dependencies=[Depends(compute_authentication)]) async def rebuild_qemu_marker( - marker_name: str, - rebuild_data: schemas.MarkerRebuild, - node: QemuVM = Depends(dep_node) + marker_name: str, rebuild_data: schemas.MarkerRebuild, node: QemuVM = Depends(dep_node) ) -> dict: """ Re-install a single marker filter with new BPF/tag/direction (delete + add, @@ -544,7 +484,11 @@ async def rebuild_qemu_marker( """ await node.rebuild_marker_filter( - marker_name, rebuild_data.link_id, rebuild_data.bpf, - rebuild_data.tag, rebuild_data.direction, rebuild_data.enabled, + marker_name, + rebuild_data.link_id, + rebuild_data.bpf, + rebuild_data.tag, + rebuild_data.direction, + rebuild_data.enabled, ) return {"marker_name": marker_name} diff --git a/gns3server/api/routes/compute/virtualbox_nodes.py b/gns3server/api/routes/compute/virtualbox_nodes.py index 957f1635f..ec040d3e4 100644 --- a/gns3server/api/routes/compute/virtualbox_nodes.py +++ b/gns3server/api/routes/compute/virtualbox_nodes.py @@ -53,7 +53,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VirtualBoxVM: response_model=schemas.VirtualBox, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}}, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBoxCreate) -> schemas.VirtualBox: """ @@ -86,11 +86,7 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox return vm.asdict() -@router.get( - "/{node_id}", - response_model=schemas.VirtualBox, - dependencies=[Depends(compute_authentication)] -) +@router.get("/{node_id}", response_model=schemas.VirtualBox, dependencies=[Depends(compute_authentication)]) def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.VirtualBox: """ Return a VirtualBox node. @@ -99,14 +95,9 @@ def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.Virtu return node.asdict() -@router.put( - "/{node_id}", - response_model=schemas.VirtualBox, - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}", response_model=schemas.VirtualBox, dependencies=[Depends(compute_authentication)]) async def update_virtualbox_node( - node_data: schemas.VirtualBoxUpdate, - node: VirtualBoxVM = Depends(dep_node) + node_data: schemas.VirtualBoxUpdate, node: VirtualBoxVM = Depends(dep_node) ) -> schemas.VirtualBox: """ Update a VirtualBox node. @@ -148,11 +139,7 @@ async def update_virtualbox_node( return node.asdict() -@router.delete( - "/{node_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ Delete a VirtualBox node. @@ -161,11 +148,7 @@ async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None await VirtualBox.instance().delete_node(node.id) -@router.post( - "/{node_id}/start", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ Start a VirtualBox node. @@ -174,11 +157,7 @@ async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: await node.start() -@router.post( - "/{node_id}/stop", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ Stop a VirtualBox node. @@ -188,9 +167,7 @@ async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/suspend", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ @@ -201,9 +178,7 @@ async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> Non @router.post( - "/{node_id}/resume", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ @@ -214,9 +189,7 @@ async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None @router.post( - "/{node_id}/reload", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None: """ @@ -230,14 +203,14 @@ async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_virtualbox_node_nio( - *, - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - nio_data: schemas.UDPNIO, - node: VirtualBoxVM = Depends(dep_node) + *, + adapter_number: int, + port_number: int = Path(..., ge=0, le=0), + nio_data: schemas.UDPNIO, + node: VirtualBoxVM = Depends(dep_node), ) -> schemas.UDPNIO: """ Add a NIO (Network Input/Output) to the node. @@ -253,14 +226,14 @@ async def create_virtualbox_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def update_virtualbox_node_nio( - *, - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - nio_data: schemas.UDPNIO, - node: VirtualBoxVM = Depends(dep_node) + *, + adapter_number: int, + port_number: int = Path(..., ge=0, le=0), + nio_data: schemas.UDPNIO, + node: VirtualBoxVM = Depends(dep_node), ) -> schemas.UDPNIO: """ Update a NIO (Network Input/Output) on the node. @@ -279,12 +252,10 @@ async def update_virtualbox_node_nio( @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_virtualbox_node_nio( - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node: VirtualBoxVM = Depends(dep_node) + adapter_number: int, port_number: int = Path(..., ge=0, le=0), node: VirtualBoxVM = Depends(dep_node) ) -> None: """ Delete a NIO (Network Input/Output) from the node. @@ -296,14 +267,14 @@ async def delete_virtualbox_node_nio( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def start_virtualbox_node_capture( - *, - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node_capture_data: schemas.NodeCapture, - node: VirtualBoxVM = Depends(dep_node), + *, + adapter_number: int, + port_number: int = Path(..., ge=0, le=0), + node_capture_data: schemas.NodeCapture, + node: VirtualBoxVM = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -318,12 +289,10 @@ async def start_virtualbox_node_capture( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stop_virtualbox_node_capture( - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node: VirtualBoxVM = Depends(dep_node) + adapter_number: int, port_number: int = Path(..., ge=0, le=0), node: VirtualBoxVM = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -335,12 +304,10 @@ async def stop_virtualbox_node_capture( @router.get( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stream_pcap_file( - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node: VirtualBoxVM = Depends(dep_node) + adapter_number: int, port_number: int = Path(..., ge=0, le=0), node: VirtualBoxVM = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. @@ -352,12 +319,9 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.websocket( - "/{node_id}/console/ws" -) +@router.websocket("/{node_id}/console/ws") async def console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: VirtualBoxVM = Depends(dep_node) + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), node: VirtualBoxVM = Depends(dep_node) ) -> None: """ Console WebSocket. @@ -367,11 +331,8 @@ async def console_ws( @router.post( - "/{node_id}/console/reset", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reset_console(node: VirtualBoxVM = Depends(dep_node)) -> None: await node.reset_console() - diff --git a/gns3server/api/routes/compute/vmware_nodes.py b/gns3server/api/routes/compute/vmware_nodes.py index aecce0289..0f339883e 100644 --- a/gns3server/api/routes/compute/vmware_nodes.py +++ b/gns3server/api/routes/compute/vmware_nodes.py @@ -52,7 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VMwareVM: response_model=schemas.VMware, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}}, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate) -> schemas.VMware: """ @@ -79,11 +79,7 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate) return vm.asdict() -@router.get( - "/{node_id}", - response_model=schemas.VMware, - dependencies=[Depends(compute_authentication)] -) +@router.get("/{node_id}", response_model=schemas.VMware, dependencies=[Depends(compute_authentication)]) def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware: """ Return a VMware node. @@ -92,11 +88,7 @@ def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware: return node.asdict() -@router.put( - "/{node_id}", - response_model=schemas.VMware, - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}", response_model=schemas.VMware, dependencies=[Depends(compute_authentication)]) async def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends(dep_node)) -> schemas.VMware: """ Update a VMware node. @@ -113,11 +105,7 @@ async def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = D return node.asdict() -@router.delete( - "/{node_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ Delete a VMware node. @@ -126,11 +114,7 @@ async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: await VMware.instance().delete_node(node.id) -@router.post( - "/{node_id}/start", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ Start a VMware node. @@ -139,11 +123,7 @@ async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: await node.start() -@router.post( - "/{node_id}/stop", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ Stop a VMware node. @@ -153,9 +133,7 @@ async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/suspend", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ @@ -166,9 +144,7 @@ async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/resume", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ @@ -179,9 +155,7 @@ async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/reload", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: """ @@ -195,14 +169,14 @@ async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_vmware_node_nio( - *, - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - nio_data: schemas.UDPNIO, - node: VMwareVM = Depends(dep_node) + *, + adapter_number: int, + port_number: int = Path(..., ge=0, le=0), + nio_data: schemas.UDPNIO, + node: VMwareVM = Depends(dep_node), ) -> schemas.UDPNIO: """ Add a NIO (Network Input/Output) to the node. @@ -218,14 +192,14 @@ async def create_vmware_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def update_vmware_node_nio( - *, - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - nio_data: schemas.UDPNIO, - node: VMwareVM = Depends(dep_node) + *, + adapter_number: int, + port_number: int = Path(..., ge=0, le=0), + nio_data: schemas.UDPNIO, + node: VMwareVM = Depends(dep_node), ) -> schemas.UDPNIO: """ Update a NIO (Network Input/Output) on the node. @@ -243,12 +217,10 @@ async def update_vmware_node_nio( @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_vmware_node_nio( - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node: VMwareVM = Depends(dep_node) + adapter_number: int, port_number: int = Path(..., ge=0, le=0), node: VMwareVM = Depends(dep_node) ) -> None: """ Delete a NIO (Network Input/Output) from the node. @@ -260,14 +232,14 @@ async def delete_vmware_node_nio( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def start_vmware_node_capture( - *, - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node_capture_data: schemas.NodeCapture, - node: VMwareVM = Depends(dep_node) + *, + adapter_number: int, + port_number: int = Path(..., ge=0, le=0), + node_capture_data: schemas.NodeCapture, + node: VMwareVM = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -282,12 +254,10 @@ async def start_vmware_node_capture( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stop_vmware_node_capture( - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node: VMwareVM = Depends(dep_node) + adapter_number: int, port_number: int = Path(..., ge=0, le=0), node: VMwareVM = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -299,12 +269,10 @@ async def stop_vmware_node_capture( @router.get( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stream_pcap_file( - adapter_number: int, - port_number: int = Path(..., ge=0, le=0), - node: VMwareVM = Depends(dep_node) + adapter_number: int, port_number: int = Path(..., ge=0, le=0), node: VMwareVM = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. @@ -317,9 +285,7 @@ async def stream_pcap_file( @router.post( - "/{node_id}/interfaces/vmnet", - status_code=status.HTTP_201_CREATED, - dependencies=[Depends(compute_authentication)] + "/{node_id}/interfaces/vmnet", status_code=status.HTTP_201_CREATED, dependencies=[Depends(compute_authentication)] ) def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict: """ @@ -335,8 +301,7 @@ def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict: @router.websocket("/{node_id}/console/ws") async def console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: VMwareVM = Depends(dep_node) + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), node: VMwareVM = Depends(dep_node) ) -> None: """ Console WebSocket. @@ -347,9 +312,7 @@ async def console_ws( @router.post( - "/{node_id}/console/reset", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reset_console(node: VMwareVM = Depends(dep_node)) -> None: diff --git a/gns3server/api/routes/compute/vpcs_nodes.py b/gns3server/api/routes/compute/vpcs_nodes.py index 546fabdb0..826571dec 100644 --- a/gns3server/api/routes/compute/vpcs_nodes.py +++ b/gns3server/api/routes/compute/vpcs_nodes.py @@ -51,7 +51,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VPCSVM: response_model=schemas.VPCS, status_code=status.HTTP_201_CREATED, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}}, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> schemas.VPCS: """ @@ -72,11 +72,7 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s return vm.asdict() -@router.get( - "/{node_id}", - response_model=schemas.VPCS, - dependencies=[Depends(compute_authentication)] -) +@router.get("/{node_id}", response_model=schemas.VPCS, dependencies=[Depends(compute_authentication)]) def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS: """ Return a VPCS node. @@ -85,11 +81,7 @@ def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS: return node.asdict() -@router.put( - "/{node_id}", - response_model=schemas.VPCS, - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}", response_model=schemas.VPCS, dependencies=[Depends(compute_authentication)]) async def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_node)) -> schemas.VPCS: """ Update a VPCS node. @@ -109,11 +101,7 @@ async def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends return node.asdict() -@router.delete( - "/{node_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ Delete a VPCS node. @@ -126,11 +114,11 @@ async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: "/{node_id}/duplicate", response_model=schemas.VPCS, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def duplicate_vpcs_node( - destination_node_id: UUID = Body(..., embed=True), - node: VPCSVM = Depends(dep_node)) -> None: + destination_node_id: UUID = Body(..., embed=True), node: VPCSVM = Depends(dep_node) +) -> None: """ Duplicate a VPCS node. """ @@ -139,11 +127,7 @@ async def duplicate_vpcs_node( return new_node.asdict() -@router.post( - "/{node_id}/start", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ Start a VPCS node. @@ -152,11 +136,7 @@ async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: await node.start() -@router.post( - "/{node_id}/stop", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] -) +@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)]) async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ Stop a VPCS node. @@ -166,9 +146,7 @@ async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/suspend", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ @@ -176,15 +154,12 @@ async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ raise HTTPException( - status_code=status.HTTP_405_METHOD_NOT_ALLOWED, - detail="Suspend is not supported for VPCS nodes" + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, detail="Suspend is not supported for VPCS nodes" ) @router.post( - "/{node_id}/reload", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: """ @@ -198,14 +173,14 @@ async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None: "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def create_vpcs_node_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: schemas.UDPNIO, - node: VPCSVM = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: schemas.UDPNIO, + node: VPCSVM = Depends(dep_node), ) -> schemas.UDPNIO: """ Add a NIO (Network Input/Output) to the node. @@ -221,14 +196,14 @@ async def create_vpcs_node_nio( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_201_CREATED, response_model=schemas.UDPNIO, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def update_vpcs_node_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - nio_data: schemas.UDPNIO, - node: VPCSVM = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + nio_data: schemas.UDPNIO, + node: VPCSVM = Depends(dep_node), ) -> schemas.UDPNIO: """ Update a NIO (Network Input/Output) on the node. @@ -247,13 +222,10 @@ async def update_vpcs_node_nio( @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_vpcs_node_nio( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: VPCSVM = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: VPCSVM = Depends(dep_node) ) -> None: """ Delete a NIO (Network Input/Output) from the node. @@ -265,14 +237,14 @@ async def delete_vpcs_node_nio( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def start_vpcs_node_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node_capture_data: schemas.NodeCapture, - node: VPCSVM = Depends(dep_node) + *, + adapter_number: int = Path(..., ge=0, le=0), + port_number: int, + node_capture_data: schemas.NodeCapture, + node: VPCSVM = Depends(dep_node), ) -> dict: """ Start a packet capture on the node. @@ -287,13 +259,10 @@ async def start_vpcs_node_capture( @router.post( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stop_vpcs_node_capture( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: VPCSVM = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: VPCSVM = Depends(dep_node) ) -> None: """ Stop a packet capture on the node. @@ -303,16 +272,12 @@ async def stop_vpcs_node_capture( await node.stop_capture(port_number) - @router.get( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream", - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def stream_pcap_file( - *, - adapter_number: int = Path(..., ge=0, le=0), - port_number: int, - node: VPCSVM = Depends(dep_node) + *, adapter_number: int = Path(..., ge=0, le=0), port_number: int, node: VPCSVM = Depends(dep_node) ) -> StreamingResponse: """ Stream the pcap capture file. @@ -324,12 +289,10 @@ async def stream_pcap_file( return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") -@router.websocket( - "/{node_id}/console/ws" -) +@router.websocket("/{node_id}/console/ws") async def console_ws( - websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), - node: VPCSVM = Depends(dep_node)) -> None: + websocket: Union[None, WebSocket] = Depends(ws_compute_authentication), node: VPCSVM = Depends(dep_node) +) -> None: """ Console WebSocket. """ @@ -338,23 +301,16 @@ async def console_ws( @router.post( - "/{node_id}/console/reset", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def reset_console(node: VPCSVM = Depends(dep_node)) -> None: await node.reset_console() -@router.put( - "/{node_id}/markers/{marker_name}", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}", dependencies=[Depends(compute_authentication)]) async def toggle_vpcs_marker( - marker_name: str, - toggle_data: schemas.MarkerToggle, - node: VPCSVM = Depends(dep_node) + marker_name: str, toggle_data: schemas.MarkerToggle, node: VPCSVM = Depends(dep_node) ) -> dict: """ Toggle a marker filter on/off without an NIO rebuild (ubridge contract §3.2). @@ -370,9 +326,7 @@ async def toggle_vpcs_marker( @router.post( - "/{node_id}/markers/pause", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/pause", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def pause_vpcs_markers(node: VPCSVM = Depends(dep_node)) -> None: @@ -380,9 +334,7 @@ async def pause_vpcs_markers(node: VPCSVM = Depends(dep_node)) -> None: @router.post( - "/{node_id}/markers/resume", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + "/{node_id}/markers/resume", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(compute_authentication)] ) async def resume_vpcs_markers(node: VPCSVM = Depends(dep_node)) -> None: @@ -392,7 +344,7 @@ async def resume_vpcs_markers(node: VPCSVM = Depends(dep_node)) -> None: @router.delete( "/{node_id}/adapters/{adapter_number}/ports/{port_number}/markers/{marker_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(compute_authentication)] + dependencies=[Depends(compute_authentication)], ) async def delete_vpcs_marker_capture( *, @@ -400,7 +352,7 @@ async def delete_vpcs_marker_capture( adapter_number: int = Path(..., ge=0, le=0), port_number: int, link_id: str = "", - node: VPCSVM = Depends(dep_node) + node: VPCSVM = Depends(dep_node), ) -> None: """ Delete a marker's capture pcap (called by the controller when the marker is @@ -413,14 +365,9 @@ async def delete_vpcs_marker_capture( await node.delete_marker_capture(marker_name, link_id, nio) -@router.put( - "/{node_id}/markers/{marker_name}/rebuild", - dependencies=[Depends(compute_authentication)] -) +@router.put("/{node_id}/markers/{marker_name}/rebuild", dependencies=[Depends(compute_authentication)]) async def rebuild_vpcs_marker( - marker_name: str, - rebuild_data: schemas.MarkerRebuild, - node: VPCSVM = Depends(dep_node) + marker_name: str, rebuild_data: schemas.MarkerRebuild, node: VPCSVM = Depends(dep_node) ) -> dict: """ Re-install a single marker filter with new BPF/tag/direction (delete + add, @@ -428,7 +375,11 @@ async def rebuild_vpcs_marker( """ await node.rebuild_marker_filter( - marker_name, rebuild_data.link_id, rebuild_data.bpf, - rebuild_data.tag, rebuild_data.direction, rebuild_data.enabled, + marker_name, + rebuild_data.link_id, + rebuild_data.bpf, + rebuild_data.tag, + rebuild_data.direction, + rebuild_data.enabled, ) return {"marker_name": marker_name} diff --git a/gns3server/api/routes/controller/__init__.py b/gns3server/api/routes/controller/__init__.py index 25334f81b..9236e17c0 100644 --- a/gns3server/api/routes/controller/__init__.py +++ b/gns3server/api/routes/controller/__init__.py @@ -24,6 +24,7 @@ if AI_COPILOT_AVAILABLE: from . import chat from . import copilot from . import llm_model_configs + _chat_router = chat.router _copilot_router = copilot.router _llm_router = llm_model_configs.router @@ -39,9 +40,10 @@ else: async def ai_not_available(path: str = ""): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, - detail="AI Copilot is not available. Install AI dependencies with: pip install gns3-server[ai-features]" + detail="AI Copilot is not available. Install AI dependencies with: pip install gns3-server[ai-features]", ) + from . import controller from . import appliances from . import computes @@ -68,148 +70,65 @@ from .dependencies.authentication import get_current_active_user router = APIRouter() +router.include_router(controller.router, tags=["Controller"]) + +router.include_router(settings.router, prefix="/settings", tags=["Server settings"]) + +router.include_router(users.router, prefix="/access/users", tags=["Users"]) + +router.include_router(groups.router, prefix="/access/groups", tags=["Users groups"]) + +router.include_router(roles.router, prefix="/access/roles", tags=["Roles"]) + router.include_router( - controller.router, - tags=["Controller"] + privileges.router, dependencies=[Depends(get_current_active_user)], prefix="/access/privileges", tags=["Privileges"] +) + +router.include_router(acl.router, prefix="/access/acl", tags=["ACL"]) + +router.include_router(images.router, prefix="/images", tags=["Images"]) + +router.include_router(templates.router, prefix="/templates", tags=["Templates"]) + +router.include_router(projects.router, prefix="/projects", tags=["Projects"]) + +router.include_router(nodes.router, prefix="/projects/{project_id}/nodes", tags=["Nodes"]) + +router.include_router(links.router, prefix="/projects/{project_id}/links", tags=["Links"]) + +router.include_router(drawings.router, prefix="/projects/{project_id}/drawings", tags=["Drawings"]) + +router.include_router(symbols.router, prefix="/symbols", tags=["Symbols"]) + +router.include_router(snapshots.router, prefix="/projects/{project_id}/snapshots", tags=["Snapshots"]) + +router.include_router( + computes.router, dependencies=[Depends(get_current_active_user)], prefix="/computes", tags=["Computes"] +) + +router.include_router(appliances.router, prefix="/appliances", tags=["Appliances"]) + +router.include_router(netmiko.router, prefix="/netmiko", tags=["Netmiko"]) + +router.include_router(pools.router, prefix="/pools", tags=["Resource pools"]) + +router.include_router( + gns3vm.router, dependencies=[Depends(get_current_active_user)], deprecated=True, prefix="/gns3vm", tags=["GNS3 VM"] ) router.include_router( - settings.router, - prefix="/settings", - tags=["Server settings"] + _llm_router, prefix="/access", dependencies=[Depends(get_current_active_user)], tags=["LLM Model Configurations"] ) router.include_router( - users.router, - prefix="/access/users", - tags=["Users"] -) - -router.include_router( - groups.router, - prefix="/access/groups", - tags=["Users groups"] -) - -router.include_router( - roles.router, - prefix="/access/roles", - tags=["Roles"] -) - -router.include_router( - privileges.router, - dependencies=[Depends(get_current_active_user)], - prefix="/access/privileges", - tags=["Privileges"] -) - -router.include_router( - acl.router, - prefix="/access/acl", - tags=["ACL"] -) - -router.include_router( - images.router, - prefix="/images", - tags=["Images"] -) - -router.include_router( - templates.router, - prefix="/templates", - tags=["Templates"] -) - -router.include_router( - projects.router, - prefix="/projects", - tags=["Projects"]) - -router.include_router( - nodes.router, - prefix="/projects/{project_id}/nodes", - tags=["Nodes"] -) - -router.include_router( - links.router, - prefix="/projects/{project_id}/links", - tags=["Links"] -) - -router.include_router( - drawings.router, - prefix="/projects/{project_id}/drawings", - tags=["Drawings"]) - -router.include_router( - symbols.router, - prefix="/symbols", tags=["Symbols"] -) - -router.include_router( - snapshots.router, - prefix="/projects/{project_id}/snapshots", - tags=["Snapshots"]) - -router.include_router( - computes.router, - dependencies=[Depends(get_current_active_user)], - prefix="/computes", - tags=["Computes"] -) - -router.include_router( - appliances.router, - prefix="/appliances", - tags=["Appliances"] -) - -router.include_router( - netmiko.router, - prefix="/netmiko", - tags=["Netmiko"] -) - -router.include_router( - pools.router, - prefix="/pools", - tags=["Resource pools"] -) - -router.include_router( - gns3vm.router, - dependencies=[Depends(get_current_active_user)], - deprecated=True, - prefix="/gns3vm", - tags=["GNS3 VM"] -) - -router.include_router( - _llm_router, - prefix="/access", - dependencies=[Depends(get_current_active_user)], - tags=["LLM Model Configurations"] -) - -router.include_router( - _copilot_router, - prefix="/copilot", - dependencies=[Depends(get_current_active_user)], - tags=["GNS3 Copilot"] + _copilot_router, prefix="/copilot", dependencies=[Depends(get_current_active_user)], tags=["GNS3 Copilot"] ) router.include_router( _chat_router, prefix="/copilot/projects/{project_id}/chat", dependencies=[Depends(get_current_active_user)], - tags=["GNS3 Copilot"] + tags=["GNS3 Copilot"], ) -router.include_router( - api_keys.router, - dependencies=[Depends(get_current_active_user)], - tags=["API Keys"] -) +router.include_router(api_keys.router, dependencies=[Depends(get_current_active_user)], tags=["API Keys"]) diff --git a/gns3server/api/routes/controller/acl.py b/gns3server/api/routes/controller/acl.py index 3ec65e874..beb7bea07 100644 --- a/gns3server/api/routes/controller/acl.py +++ b/gns3server/api/routes/controller/acl.py @@ -29,10 +29,7 @@ from typing import Iterator, List, Sequence from gns3server import schemas -from gns3server.controller.controller_error import ( - ControllerBadRequestError, - ControllerNotFoundError -) +from gns3server.controller.controller_error import ControllerBadRequestError, ControllerNotFoundError from gns3server.controller import Controller from gns3server.db.repositories.users import UsersRepository @@ -69,7 +66,9 @@ def _iter_route_paths(routes: Sequence[BaseRoute], prefix: str = "", include_mou for route in routes: if isinstance(route, _IncludedRouter): include_prefix = route.include_context.prefix or "" - yield from _iter_route_paths(route.original_router.routes, _join_paths(prefix, include_prefix), include_mounted_routes) + yield from _iter_route_paths( + route.original_router.routes, _join_paths(prefix, include_prefix), include_mounted_routes + ) continue if isinstance(route, APIRoute): @@ -82,16 +81,12 @@ def _iter_route_paths(routes: Sequence[BaseRoute], prefix: str = "", include_mou yield from _iter_route_paths(mounted_routes, _join_paths(prefix, route.path), include_mounted_routes) -@router.get( - "/endpoints", - status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("ACE.Audit"))] -) +@router.get("/endpoints", status_code=status.HTTP_201_CREATED, dependencies=[Depends(has_privilege("ACE.Audit"))]) async def endpoints( - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), - images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), - templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), ) -> List[dict]: """ List all endpoints to be used in ACL entries. @@ -123,7 +118,7 @@ async def endpoints( add_to_endpoints( f"/projects/{project.id}/nodes/{node['node_id']}", f'Node "{node["name"]}" in project "{project.name}"', - endpoint_type="node" + endpoint_type="node", ) # links @@ -140,7 +135,7 @@ async def endpoints( add_to_endpoints( f"/projects/{project.id}/links/{link['link_id']}", f'Link from "{node_name_1}" to "{node_name_2}" in project "{project.name}"', - endpoint_type="link" + endpoint_type="link", ) # users @@ -179,14 +174,8 @@ async def endpoints( return endpoints -@router.get( - "", - response_model=List[schemas.ACE], - dependencies=[Depends(has_privilege("ACE.Audit"))] -) -async def get_aces( - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) -) -> List[schemas.ACE]: +@router.get("", response_model=List[schemas.ACE], dependencies=[Depends(has_privilege("ACE.Audit"))]) +async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[schemas.ACE]: """ Get all ACL entries. @@ -200,12 +189,10 @@ async def get_aces( "", response_model=schemas.ACE, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("ACE.Allocate"))] + dependencies=[Depends(has_privilege("ACE.Allocate"))], ) async def create_ace( - request: Request, - ace_create: schemas.ACECreate, - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + request: Request, ace_create: schemas.ACECreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> schemas.ACE: """ Create a new ACL entry. @@ -217,11 +204,12 @@ async def create_ace( print(route_path) for route_path in _iter_route_paths(request.app.routes, include_mounted_routes=True): - # remove the prefix (e.g. "/v3") from the route path normalized_path = re.sub(r"^/v[0-9]", "", route_path) # replace route path ID parameters by a UUID regex - normalized_path = re.sub(r"{\w+_id}", "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", normalized_path) + normalized_path = re.sub( + r"{\w+_id}", "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", normalized_path + ) # replace remaining route path parameters by a word matching regex normalized_path = re.sub(r"/{[\w:]+}", r"/\\w+", normalized_path) @@ -232,14 +220,10 @@ async def create_ace( raise ControllerBadRequestError(f"Path '{ace_create.path}' doesn't match any existing endpoint") -@router.get( - "/{ace_id}", - response_model=schemas.ACE, - dependencies=[Depends(has_privilege("ACE.Audit"))] -) +@router.get("/{ace_id}", response_model=schemas.ACE, dependencies=[Depends(has_privilege("ACE.Audit"))]) async def get_ace( - ace_id: UUID, - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + ace_id: UUID, + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> schemas.ACE: """ Get an ACL entry. @@ -253,15 +237,9 @@ async def get_ace( return ace -@router.put( - "/{ace_id}", - response_model=schemas.ACE, - dependencies=[Depends(has_privilege("ACE.Modify"))] -) +@router.put("/{ace_id}", response_model=schemas.ACE, dependencies=[Depends(has_privilege("ACE.Modify"))]) async def update_ace( - ace_id: UUID, - ace_update: schemas.ACEUpdate, - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + ace_id: UUID, ace_update: schemas.ACEUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> schemas.ACE: """ Update an ACL entry. @@ -277,9 +255,7 @@ async def update_ace( @router.delete( - "/{ace_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("ACE.Allocate"))] + "/{ace_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("ACE.Allocate"))] ) async def delete_ace( ace_id: UUID, diff --git a/gns3server/api/routes/controller/appliances.py b/gns3server/api/routes/controller/appliances.py index 142818f66..46c187940 100644 --- a/gns3server/api/routes/controller/appliances.py +++ b/gns3server/api/routes/controller/appliances.py @@ -26,11 +26,7 @@ from uuid import UUID from gns3server import schemas from gns3server.controller import Controller -from gns3server.controller.controller_error import ( - ControllerError, - ControllerBadRequestError, - ControllerNotFoundError -) +from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError, ControllerNotFoundError from gns3server.db.repositories.images import ImagesRepository from gns3server.db.repositories.templates import TemplatesRepository @@ -50,12 +46,9 @@ router = APIRouter() "", response_model=List[schemas.Appliance], response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Appliance.Audit"))] + dependencies=[Depends(has_privilege("Appliance.Audit"))], ) -async def get_appliances( - update: Optional[bool] = False, - symbol_theme: Optional[str] = None -) -> List[schemas.Appliance]: +async def get_appliances(update: Optional[bool] = False, symbol_theme: Optional[str] = None) -> List[schemas.Appliance]: """ Return all appliances known by the controller. @@ -73,7 +66,7 @@ async def get_appliances( "/{appliance_id}", response_model=schemas.Appliance, response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Appliance.Audit"))] + dependencies=[Depends(has_privilege("Appliance.Audit"))], ) def get_appliance(appliance_id: UUID) -> schemas.Appliance: """ @@ -92,9 +85,11 @@ def get_appliance(appliance_id: UUID) -> schemas.Appliance: @router.post( "/{appliance_id}/version", status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Appliance.Allocate"))] + dependencies=[Depends(has_privilege("Appliance.Allocate"))], ) -def add_appliance_version(appliance_id: UUID, appliance_version: Union[schemas.ApplianceVersion, schemas.ApplianceVersionV8]) -> dict: +def add_appliance_version( + appliance_id: UUID, appliance_version: Union[schemas.ApplianceVersion, schemas.ApplianceVersionV8] +) -> dict: """ Add a version to an appliance. @@ -124,15 +119,15 @@ def add_appliance_version(appliance_id: UUID, appliance_version: Union[schemas.A "/{appliance_id}/install", response_model=schemas.Template, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Appliance.Allocate"))] + dependencies=[Depends(has_privilege("Appliance.Allocate"))], ) async def install_appliance( - appliance_id: UUID, - version: Optional[str] = None, - images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), - templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), - current_user: schemas.User = Depends(get_current_active_user), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + appliance_id: UUID, + version: Optional[str] = None, + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), + current_user: schemas.User = Depends(get_current_active_user), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> schemas.Template: """ Install an appliance and return the created template. @@ -142,10 +137,5 @@ async def install_appliance( controller = Controller.instance() return await controller.appliance_manager.install_appliance( - appliance_id, - version, - images_repo, - templates_repo, - rbac_repo, - current_user + appliance_id, version, images_repo, templates_repo, rbac_repo, current_user ) diff --git a/gns3server/api/routes/controller/chat.py b/gns3server/api/routes/controller/chat.py index 61f56e0d7..cc796fe26 100644 --- a/gns3server/api/routes/controller/chat.py +++ b/gns3server/api/routes/controller/chat.py @@ -69,7 +69,7 @@ def dep_project(project_id: UUID) -> Project: "/stream", response_model=None, summary="Stream chat responses from GNS3 Copilot", - description="Send a message to GNS3 Copilot and stream the response via Server-Sent Events (SSE)." + description="Send a message to GNS3 Copilot and stream the response via Server-Sent Events (SSE).", ) async def stream_chat( request: schemas.ChatRequest, @@ -96,11 +96,11 @@ async def stream_chat( "Chat rejected: project not opened. user_id=%s, project_id=%s, status=%s", user_id, project.id, - project.status + project.status, ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Project must be opened to use chat. Current status: {project.status}" + detail=f"Project must be opened to use chat. Current status: {project.status}", ) log.info( @@ -124,7 +124,7 @@ async def stream_chat( log.warning("LLM config not found for user: %s", user_id) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="LLM configuration not found. Please configure your LLM settings first." + detail="LLM configuration not found. Please configure your LLM settings first.", ) log.debug( @@ -161,7 +161,7 @@ async def stream_chat( user_id=user_id, jwt_token=jwt_token, mode=request.mode, - llm_config=llm_config + llm_config=llm_config, ): try: # Validate and serialize chunk @@ -187,7 +187,7 @@ async def stream_chat( "Cache-Control": "no-cache", "X-Accel-Buffering": "no", "Connection": "keep-alive", - } + }, ) @@ -195,7 +195,7 @@ async def stream_chat( "/sessions", response_model=List[schemas.ChatSession], summary="List chat sessions", - description="List all chat sessions for a project, optionally filtered by copilot_mode." + description="List all chat sessions for a project, optionally filtered by copilot_mode.", ) async def list_sessions( project: Project = Depends(dep_project), @@ -213,7 +213,7 @@ async def list_sessions( if project.status != "opened": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Project must be opened to access chat sessions. Current status: {project.status}" + detail=f"Project must be opened to access chat sessions. Current status: {project.status}", ) # Get AgentService for this project @@ -221,10 +221,7 @@ async def list_sessions( agent_service = await agent_manager.get_agent(str(project.id), project.path) # List sessions with optional copilot_mode filter - sessions = await agent_service.list_sessions( - user_id=str(current_user.user_id), - copilot_mode=copilot_mode - ) + sessions = await agent_service.list_sessions(user_id=str(current_user.user_id), copilot_mode=copilot_mode) # Convert to schemas return [schemas.ChatSession(**s) for s in sessions] @@ -234,7 +231,7 @@ async def list_sessions( "/sessions/{session_id}/history", response_model=schemas.ConversationHistory, summary="Get conversation history", - description="Retrieve the conversation history for a specific session/thread." + description="Retrieve the conversation history for a specific session/thread.", ) async def get_history( session_id: str, @@ -250,7 +247,7 @@ async def get_history( if project.status != "opened": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Project must be opened to access chat history. Current status: {project.status}" + detail=f"Project must be opened to access chat history. Current status: {project.status}", ) # Get AgentService for this project @@ -267,7 +264,7 @@ async def get_history( "/sessions/{session_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete a chat session", - description="Delete a specific chat session and its checkpoints." + description="Delete a specific chat session and its checkpoints.", ) async def delete_session( session_id: str, @@ -282,7 +279,7 @@ async def delete_session( if project.status != "opened": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Project must be opened to delete chat sessions. Current status: {project.status}" + detail=f"Project must be opened to delete chat sessions. Current status: {project.status}", ) # Get AgentService for this project @@ -293,17 +290,14 @@ async def delete_session( deleted = await agent_service.delete_session(session_id) if not deleted: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Session '{session_id}' not found" - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Session '{session_id}' not found") @router.post( "/sessions/{session_id}/abort", status_code=status.HTTP_200_OK, summary="Abort a streaming session", - description="Abort an ongoing streaming session for a specific session." + description="Abort an ongoing streaming session for a specific session.", ) async def abort_session( session_id: str, @@ -321,7 +315,7 @@ async def abort_session( if project.status != "opened": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Project must be opened to abort chat session. Current status: {project.status}" + detail=f"Project must be opened to abort chat session. Current status: {project.status}", ) # Get AgentService for this project @@ -338,7 +332,7 @@ async def abort_session( "/sessions/{session_id}", response_model=schemas.ChatSession, summary="Rename a chat session", - description="Rename a specific chat session." + description="Rename a specific chat session.", ) async def rename_session( session_id: str, @@ -354,7 +348,7 @@ async def rename_session( if project.status != "opened": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Project must be opened to rename chat sessions. Current status: {project.status}" + detail=f"Project must be opened to rename chat sessions. Current status: {project.status}", ) # Get AgentService for this project @@ -365,10 +359,7 @@ async def rename_session( session = await agent_service.rename_session(session_id, request.title) if not session: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Session '{session_id}' not found" - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Session '{session_id}' not found") return schemas.ChatSession(**session) @@ -377,7 +368,7 @@ async def rename_session( "/sessions/{session_id}/pin", response_model=schemas.ChatSession, summary="Pin a chat session", - description="Pin a chat session to the top of the list." + description="Pin a chat session to the top of the list.", ) async def pin_session( session_id: str, @@ -392,7 +383,7 @@ async def pin_session( if project.status != "opened": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Project must be opened to pin chat sessions. Current status: {project.status}" + detail=f"Project must be opened to pin chat sessions. Current status: {project.status}", ) # Get AgentService for this project @@ -403,10 +394,7 @@ async def pin_session( session = await agent_service.pin_session(session_id, pinned=True) if not session: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Session '{session_id}' not found" - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Session '{session_id}' not found") return schemas.ChatSession(**session) @@ -415,7 +403,7 @@ async def pin_session( "/sessions/{session_id}/pin", response_model=schemas.ChatSession, summary="Unpin a chat session", - description="Unpin a chat session from the top of the list." + description="Unpin a chat session from the top of the list.", ) async def unpin_session( session_id: str, @@ -430,7 +418,7 @@ async def unpin_session( if project.status != "opened": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Project must be opened to unpin chat sessions. Current status: {project.status}" + detail=f"Project must be opened to unpin chat sessions. Current status: {project.status}", ) # Get AgentService for this project @@ -441,10 +429,7 @@ async def unpin_session( session = await agent_service.pin_session(session_id, pinned=False) if not session: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Session '{session_id}' not found" - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Session '{session_id}' not found") return schemas.ChatSession(**session) @@ -453,7 +438,7 @@ async def unpin_session( "/inject", response_model=None, summary="Inject a network fault for troubleshooting practice", - description="Inject a realistic network fault into the GNS3 lab for troubleshooting training." + description="Inject a realistic network fault into the GNS3 lab for troubleshooting training.", ) async def inject_issue( request: schemas.ChatRequest, @@ -482,11 +467,11 @@ async def inject_issue( "Fault injection rejected: project not opened. user_id=%s, project_id=%s, status=%s", user_id, project.id, - project.status + project.status, ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Project must be opened to use fault injection. Current status: {project.status}" + detail=f"Project must be opened to use fault injection. Current status: {project.status}", ) log.info( @@ -510,7 +495,7 @@ async def inject_issue( log.warning("LLM config not found for user: %s", user_id) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="LLM configuration not found. Please configure your LLM settings first." + detail="LLM configuration not found. Please configure your LLM settings first.", ) # Override copilot_mode to troubleshooting_injection @@ -546,7 +531,7 @@ async def inject_issue( user_id=user_id, jwt_token=jwt_token, mode=request.mode, - llm_config=llm_config + llm_config=llm_config, ): try: # Validate and serialize chunk @@ -572,5 +557,5 @@ async def inject_issue( "Cache-Control": "no-cache", "X-Accel-Buffering": "no", "Connection": "keep-alive", - } + }, ) diff --git a/gns3server/api/routes/controller/computes.py b/gns3server/api/routes/controller/computes.py index 59c347ef7..b1173eba7 100644 --- a/gns3server/api/routes/controller/computes.py +++ b/gns3server/api/routes/controller/computes.py @@ -45,12 +45,12 @@ router = APIRouter(responses=responses) 409: {"model": schemas.ErrorMessage, "description": "Could not create compute"}, 401: {"model": schemas.ErrorMessage, "description": "Invalid authentication for compute"}, }, - dependencies=[Depends(has_privilege("Compute.Allocate"))] + dependencies=[Depends(has_privilege("Compute.Allocate"))], ) async def create_compute( compute_create: schemas.ComputeCreate, computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), - connect: Optional[bool] = False + connect: Optional[bool] = False, ) -> schemas.Compute: """ Create a new compute on the controller. @@ -64,7 +64,7 @@ async def create_compute( @router.post( "/{compute_id}/connect", status_code=status.HTTP_204_NO_CONTENT, - #dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui + # dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui ) async def connect_compute(compute_id: Union[str, UUID]) -> None: """ @@ -82,7 +82,7 @@ async def connect_compute(compute_id: Union[str, UUID]) -> None: "/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True, - #dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui + # dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui ) async def get_compute( compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)) @@ -100,7 +100,7 @@ async def get_compute( "", response_model=List[schemas.Compute], response_model_exclude_unset=True, - #dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui + # dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui ) async def get_computes( computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), @@ -118,7 +118,7 @@ async def get_computes( "/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Compute.Modify"))] + dependencies=[Depends(has_privilege("Compute.Modify"))], ) async def update_compute( compute_id: Union[str, UUID], @@ -135,14 +135,12 @@ async def update_compute( @router.delete( - "/{compute_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Compute.Allocate"))] + "/{compute_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Compute.Allocate"))] ) async def delete_compute( - compute_id: Union[str, UUID], - computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + compute_id: Union[str, UUID], + computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a compute from the controller. @@ -168,11 +166,10 @@ async def docker_get_images(compute_id: Union[str, UUID]) -> List[schemas.Comput @router.post( "/{compute_id}/docker/images/pull", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Compute.Modify"))] + dependencies=[Depends(has_privilege("Compute.Modify"))], ) async def docker_pull_image( - compute_id: Union[str, UUID], - image: str = Body(..., embed=True, min_length=1, pattern=r"^\S+$") + compute_id: Union[str, UUID], image: str = Body(..., embed=True, min_length=1, pattern=r"^\S+$") ) -> None: """ Pull or update a Docker image on a compute. diff --git a/gns3server/api/routes/controller/controller.py b/gns3server/api/routes/controller/controller.py index da00957a4..843cad739 100644 --- a/gns3server/api/routes/controller/controller.py +++ b/gns3server/api/routes/controller/controller.py @@ -68,11 +68,7 @@ def get_version(request: Request) -> dict: controller_host = route.app.state.controller_host local_server = Config.instance().settings.Server.local - return { - "controller_host": controller_host, - "version": __version__, - "local": local_server - } + return {"controller_host": controller_host, "version": __version__, "local": local_server} @router.post( @@ -140,11 +136,7 @@ async def shutdown() -> None: os.kill(os.getpid(), signal.SIGTERM) -@router.get( - "/iou_license", - dependencies=[Depends(get_current_active_user)], - response_model=schemas.IOULicense -) +@router.get("/iou_license", dependencies=[Depends(get_current_active_user)], response_model=schemas.IOULicense) def get_iou_license() -> schemas.IOULicense: """ Return the IOU license settings @@ -157,7 +149,7 @@ def get_iou_license() -> schemas.IOULicense: "/iou_license", dependencies=[Depends(get_current_active_user)], status_code=status.HTTP_201_CREATED, - response_model=schemas.IOULicense + response_model=schemas.IOULicense, ) async def update_iou_license(iou_license: schemas.IOULicense) -> schemas.IOULicense: """ @@ -271,8 +263,10 @@ async def controller_http_notifications(request: Request) -> StreamingResponse: """ from gns3server.api.server import app - log.info(f"New client {request.client.host}:{request.client.port} has connected to controller HTTP " - f"notification stream") + + log.info( + f"New client {request.client.host}:{request.client.port} has connected to controller HTTP notification stream" + ) async def event_stream(): try: @@ -281,15 +275,17 @@ async def controller_http_notifications(request: Request) -> StreamingResponse: msg = await queue.get_json(5) yield f"{msg}\n".encode("utf-8") finally: - log.info(f"Client {request.client.host}:{request.client.port} has disconnected from controller HTTP " - f"notification stream") + log.info( + f"Client {request.client.host}:{request.client.port} has disconnected from controller HTTP " + f"notification stream" + ) + return StreamingResponse(event_stream(), media_type="application/json") @router.websocket("/notifications/ws") async def controller_ws_notifications( - websocket: WebSocket, - current_user: schemas.User = Depends(get_current_active_user_from_websocket) + websocket: WebSocket, current_user: schemas.User = Depends(get_current_active_user_from_websocket) ) -> None: """ Receive project notifications about the controller from WebSocket. diff --git a/gns3server/api/routes/controller/dependencies/authentication.py b/gns3server/api/routes/controller/dependencies/authentication.py index d363c996e..1043db717 100644 --- a/gns3server/api/routes/controller/dependencies/authentication.py +++ b/gns3server/api/routes/controller/dependencies/authentication.py @@ -50,14 +50,13 @@ def _reject_refresh_token(token_data) -> None: async def get_user_from_token( - request: Request, - bearer_token: str = Depends(oauth2_scheme), - user_repo: UsersRepository = Depends(get_repository(UsersRepository)), - api_keys_repo: ApiKeysRepository = Depends(get_repository(ApiKeysRepository)), - token: Optional[str] = Query(None, include_in_schema=False) + request: Request, + bearer_token: str = Depends(oauth2_scheme), + user_repo: UsersRepository = Depends(get_repository(UsersRepository)), + api_keys_repo: ApiKeysRepository = Depends(get_repository(ApiKeysRepository)), + token: Optional[str] = Query(None, include_in_schema=False), ) -> schemas.User: - if bearer_token: # bearer token is used first, then any token passed as a URL parameter token = bearer_token @@ -147,9 +146,9 @@ async def get_user_from_token( async def get_current_active_user( - request: Request, - current_user: schemas.User = Depends(get_user_from_token), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + request: Request, + current_user: schemas.User = Depends(get_user_from_token), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> schemas.User: # Super admin is always authorized @@ -167,9 +166,9 @@ async def get_current_active_user( async def get_current_active_user_from_websocket( - websocket: WebSocket, - token: str = Query(...), - user_repo: UsersRepository = Depends(get_repository(UsersRepository)), + websocket: WebSocket, + token: str = Query(...), + user_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> Optional[schemas.User]: # Extract requested subprotocols from headers for proper WebSocket negotiation @@ -209,12 +208,11 @@ async def get_current_active_user_from_websocket( if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Could not validate credentials for '{token_data.username}'" + detail=f"Could not validate credentials for '{token_data.username}'", ) if token_data.token_version != user.token_version: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Token has been revoked for '{token_data.username}'" + status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Token has been revoked for '{token_data.username}'" ) # Super admin is always authorized @@ -223,8 +221,7 @@ async def get_current_active_user_from_websocket( if not user.is_active: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"'{token_data.username}' is not an active user" + status_code=status.HTTP_401_UNAUTHORIZED, detail=f"'{token_data.username}' is not an active user" ) return user @@ -242,4 +239,3 @@ async def get_current_active_user_from_websocket( await websocket.send_json(websocket_error) log.error(err_msg) return await websocket.close(code=1008) - diff --git a/gns3server/api/routes/controller/dependencies/database.py b/gns3server/api/routes/controller/dependencies/database.py index e5338ad75..2e859106b 100644 --- a/gns3server/api/routes/controller/dependencies/database.py +++ b/gns3server/api/routes/controller/dependencies/database.py @@ -22,8 +22,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from gns3server.db.repositories.base import BaseRepository - - async def get_db_session(request: HTTPConnection) -> AsyncSession: async with AsyncSession(request.app.state._db_engine, expire_on_commit=False) as session: diff --git a/gns3server/api/routes/controller/dependencies/rbac.py b/gns3server/api/routes/controller/dependencies/rbac.py index 41f4cbd0d..995055e1d 100644 --- a/gns3server/api/routes/controller/dependencies/rbac.py +++ b/gns3server/api/routes/controller/dependencies/rbac.py @@ -27,30 +27,29 @@ import logging log = logging.getLogger() -def has_privilege( - privilege_name: str -): +def has_privilege(privilege_name: str): async def get_user_and_check_privilege( - request: Request, - current_user: schemas.User = Depends(get_current_active_user), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + request: Request, + current_user: schemas.User = Depends(get_current_active_user), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ): if not current_user.is_superadmin: path = re.sub(r"^/v[0-9]", "", request.url.path) # remove the prefix (e.g. "/v3") from URL path log.debug(f"Checking user {current_user.username} has privilege {privilege_name} on '{path}'") if not await rbac_repo.check_user_has_privilege(current_user.user_id, path, privilege_name): - raise HTTPException(status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)") + raise HTTPException( + status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)" + ) return current_user + return get_user_and_check_privilege -def has_privilege_on_websocket( - privilege_name: str -): +def has_privilege_on_websocket(privilege_name: str): async def get_user_and_check_privilege( - websocket: WebSocket, - current_user: schemas.User = Depends(get_current_active_user_from_websocket), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + websocket: WebSocket, + current_user: schemas.User = Depends(get_current_active_user_from_websocket), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ): # Authentication may have failed and closed the socket inside the auth # dependency, returning None — bail out before touching the user object. @@ -60,10 +59,14 @@ def has_privilege_on_websocket( path = re.sub(r"^/v[0-9]", "", websocket.url.path) # remove the prefix (e.g. "/v3") from URL path log.debug(f"Checking user {current_user.username} has privilege {privilege_name} on '{path}'") if not await rbac_repo.check_user_has_privilege(current_user.user_id, path, privilege_name): - raise HTTPException(status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)") + raise HTTPException( + status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)" + ) return current_user + return get_user_and_check_privilege + # class PrivilegeChecker: # # def __init__(self, required_privilege: str) -> None: diff --git a/gns3server/api/routes/controller/drawings.py b/gns3server/api/routes/controller/drawings.py index 1d8c47f17..42e989f0f 100644 --- a/gns3server/api/routes/controller/drawings.py +++ b/gns3server/api/routes/controller/drawings.py @@ -39,7 +39,7 @@ router = APIRouter(responses=responses) "", response_model=List[schemas.Drawing], response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Drawing.Audit"))] + dependencies=[Depends(has_privilege("Drawing.Audit"))], ) async def get_drawings(project_id: UUID) -> List[schemas.Drawing]: """ @@ -59,7 +59,7 @@ async def get_drawings(project_id: UUID) -> List[schemas.Drawing]: "", status_code=status.HTTP_201_CREATED, response_model=schemas.Drawing, - dependencies=[Depends(has_privilege("Drawing.Allocate"))] + dependencies=[Depends(has_privilege("Drawing.Allocate"))], ) async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing: """ @@ -77,7 +77,7 @@ async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing) -> sch "/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Drawing.Audit"))] + dependencies=[Depends(has_privilege("Drawing.Audit"))], ) async def get_drawing(project_id: UUID, drawing_id: UUID) -> schemas.Drawing: """ @@ -95,7 +95,7 @@ async def get_drawing(project_id: UUID, drawing_id: UUID) -> schemas.Drawing: "/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Drawing.Modify"))] + dependencies=[Depends(has_privilege("Drawing.Modify"))], ) async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing: """ @@ -111,14 +111,10 @@ async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schem @router.delete( - "/{drawing_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Drawing.Allocate"))] + "/{drawing_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Drawing.Allocate"))] ) async def delete_drawing( - project_id: UUID, - drawing_id: UUID, - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + project_id: UUID, drawing_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> None: """ Delete a drawing. diff --git a/gns3server/api/routes/controller/groups.py b/gns3server/api/routes/controller/groups.py index c74f2b8c1..606835419 100644 --- a/gns3server/api/routes/controller/groups.py +++ b/gns3server/api/routes/controller/groups.py @@ -44,13 +44,9 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get( - "", - response_model=List[schemas.UserGroup], - dependencies=[Depends(has_privilege("Group.Audit"))] -) +@router.get("", response_model=List[schemas.UserGroup], dependencies=[Depends(has_privilege("Group.Audit"))]) async def get_user_groups( - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> List[schemas.UserGroup]: """ Get all user groups. @@ -65,11 +61,10 @@ async def get_user_groups( "", response_model=schemas.UserGroup, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Group.Allocate"))] + dependencies=[Depends(has_privilege("Group.Allocate"))], ) async def create_user_group( - user_group_create: schemas.UserGroupCreate, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) + user_group_create: schemas.UserGroupCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) ) -> schemas.UserGroup: """ Create a new user group. @@ -83,14 +78,10 @@ async def create_user_group( return await users_repo.create_user_group(user_group_create) -@router.get( - "/{user_group_id}", - response_model=schemas.UserGroup, - dependencies=[Depends(has_privilege("Group.Audit"))] -) +@router.get("/{user_group_id}", response_model=schemas.UserGroup, dependencies=[Depends(has_privilege("Group.Audit"))]) async def get_user_group( - user_group_id: UUID, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + user_group_id: UUID, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> schemas.UserGroup: """ Get a user group. @@ -104,15 +95,11 @@ async def get_user_group( return user_group -@router.put( - "/{user_group_id}", - response_model=schemas.UserGroup, - dependencies=[Depends(has_privilege("Group.Modify"))] -) +@router.put("/{user_group_id}", response_model=schemas.UserGroup, dependencies=[Depends(has_privilege("Group.Modify"))]) async def update_user_group( - user_group_id: UUID, - user_group_update: schemas.UserGroupUpdate, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) + user_group_id: UUID, + user_group_update: schemas.UserGroupUpdate, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> schemas.UserGroup: """ Update a user group. @@ -130,14 +117,12 @@ async def update_user_group( @router.delete( - "/{user_group_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Group.Allocate"))] + "/{user_group_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Group.Allocate"))] ) async def delete_user_group( - user_group_id: UUID, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + user_group_id: UUID, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a user group. @@ -159,13 +144,10 @@ async def delete_user_group( @router.get( - "/{user_group_id}/members", - response_model=List[schemas.User], - dependencies=[Depends(has_privilege("Group.Audit"))] + "/{user_group_id}/members", response_model=List[schemas.User], dependencies=[Depends(has_privilege("Group.Audit"))] ) async def get_user_group_members( - user_group_id: UUID, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) + user_group_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) ) -> List[schemas.User]: """ Get all user group members. @@ -179,12 +161,10 @@ async def get_user_group_members( @router.put( "/{user_group_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Group.Modify"))] + dependencies=[Depends(has_privilege("Group.Modify"))], ) async def add_member_to_group( - user_group_id: UUID, - user_id: UUID, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) + user_group_id: UUID, user_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) ) -> None: """ Add member to a user group. @@ -209,7 +189,7 @@ async def add_member_to_group( @router.delete( "/{user_group_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Group.Modify"))] + dependencies=[Depends(has_privilege("Group.Modify"))], ) async def remove_member_from_group( user_group_id: UUID, diff --git a/gns3server/api/routes/controller/images.py b/gns3server/api/routes/controller/images.py index fe27c20d8..a67c68351 100644 --- a/gns3server/api/routes/controller/images.py +++ b/gns3server/api/routes/controller/images.py @@ -31,7 +31,13 @@ from typing import List, Optional from gns3server import schemas from gns3server.config import Config from gns3server.compute.qemu import Qemu -from gns3server.utils.images import InvalidImageError, write_image, read_image_info, default_images_directory, get_builtin_disks +from gns3server.utils.images import ( + InvalidImageError, + write_image, + read_image_info, + default_images_directory, + get_builtin_disks, +) from gns3server.db.repositories.images import ImagesRepository from gns3server.db.repositories.templates import TemplatesRepository from gns3server.db.repositories.rbac import RbacRepository @@ -40,7 +46,7 @@ from gns3server.controller.controller_error import ( ControllerError, ControllerNotFoundError, ControllerForbiddenError, - ControllerBadRequestError + ControllerBadRequestError, ) from .dependencies.authentication import get_current_active_user @@ -51,17 +57,17 @@ log = logging.getLogger(__name__) router = APIRouter() + @router.post( "/qemu/{image_path:path}", response_model=schemas.Image, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Image.Allocate"))] + dependencies=[Depends(has_privilege("Image.Allocate"))], ) async def create_qemu_image( - image_path: str, - image_data: schemas.QemuDiskImageCreate, - images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), - + image_path: str, + image_data: schemas.QemuDiskImageCreate, + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), ) -> schemas.Image: """ Create a new blank Qemu image. @@ -104,14 +110,11 @@ async def create_qemu_image( else: return await images_repo.add_image(**image_info) -@router.get( - "", - response_model=List[schemas.Image], - dependencies=[Depends(has_privilege("Image.Audit"))] -) + +@router.get("", response_model=List[schemas.Image], dependencies=[Depends(has_privilege("Image.Audit"))]) async def get_images( - images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), - image_type: Optional[schemas.ImageType] = None + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + image_type: Optional[schemas.ImageType] = None, ) -> List[schemas.Image]: """ Return all images. @@ -126,16 +129,16 @@ async def get_images( "/upload/{image_path:path}", response_model=schemas.Image, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Image.Allocate"))] + dependencies=[Depends(has_privilege("Image.Allocate"))], ) async def upload_image( - image_path: str, - request: Request, - images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), - templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), - current_user: schemas.User = Depends(get_current_active_user), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), - install_appliances: Optional[bool] = False, + image_path: str, + request: Request, + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), + current_user: schemas.User = Depends(get_current_active_user), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + install_appliances: Optional[bool] = False, ) -> schemas.Image: """ Upload an image. @@ -161,9 +164,7 @@ async def upload_image( duplicate = await images_repo.get_image_by_checksum(checksum_header, check_dir) if duplicate: location = f" in '{check_dir}'" if check_dir else "" - raise ControllerError( - f"Image '{duplicate.filename}' with the same checksum already exists{location}" - ) + raise ControllerError(f"Image '{duplicate.filename}' with the same checksum already exists{location}") try: allow_raw_image = Config.instance().settings.Server.allow_raw_images @@ -180,19 +181,17 @@ async def upload_image( templates_repo, rbac_repo, current_user, - os.path.dirname(image.path) + os.path.dirname(image.path), ) return image @router.delete( - "/prune", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Image.Allocate"))] + "/prune", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Image.Allocate"))] ) async def prune_images( - images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), ) -> None: """ Prune images not attached to any template. @@ -209,14 +208,10 @@ async def prune_images( await images_repo.prune_images(list(skip_images) + list(referenced_filenames)) -@router.post( - "/install", - status_code=status.HTTP_200_OK, - dependencies=[Depends(has_privilege("Image.Allocate"))] -) +@router.post("/install", status_code=status.HTTP_200_OK, dependencies=[Depends(has_privilege("Image.Allocate"))]) async def install_images( - images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), - templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), ) -> dict: """ Attempt to automatically create templates based on image checksums. @@ -239,19 +234,15 @@ async def install_images( if templates: # the image is already used by a template log.warning(f"Image '{image.path}' is used by one or more templates") - skipped.append({ - "name": image.filename, - "reason": "image is already used by one or more templates", - }) + skipped.append( + { + "name": image.filename, + "reason": "image is already used by one or more templates", + } + ) continue results = await Controller.instance().appliance_manager.install_appliances_from_image( - image.path, - image.checksum, - images_repo, - templates_repo, - None, - None, - os.path.dirname(image.path) + image.path, image.checksum, images_repo, templates_repo, None, None, os.path.dirname(image.path) ) for result in results: if result.get("status") == "created": @@ -261,14 +252,10 @@ async def install_images( return {"created": created, "skipped": skipped} -@router.get( - "/{image_path:path}", - response_model=schemas.Image, - dependencies=[Depends(has_privilege("Image.Audit"))] -) +@router.get("/{image_path:path}", response_model=schemas.Image, dependencies=[Depends(has_privilege("Image.Audit"))]) async def get_image( - image_path: str, - images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + image_path: str, + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), ) -> schemas.Image: """ Return an image. @@ -286,11 +273,11 @@ async def get_image( @router.delete( "/{image_path:path}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Image.Allocate"))] + dependencies=[Depends(has_privilege("Image.Allocate"))], ) async def delete_image( - image_path: str, - images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), + image_path: str, + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), ) -> None: """ Delete an image. @@ -303,8 +290,9 @@ async def delete_image( try: image = await images_repo.get_image(image_path) except MultipleResultsFound: - raise ControllerBadRequestError(f"Image '{image_path}' matches multiple images. " - f"Please include the absolute path of the image") + raise ControllerBadRequestError( + f"Image '{image_path}' matches multiple images. Please include the absolute path of the image" + ) if not image: raise ControllerNotFoundError(f"Image '{image_path}' not found") diff --git a/gns3server/api/routes/controller/links.py b/gns3server/api/routes/controller/links.py index bf6c5467f..1ee60a891 100644 --- a/gns3server/api/routes/controller/links.py +++ b/gns3server/api/routes/controller/links.py @@ -65,7 +65,7 @@ async def dep_link(project_id: UUID, link_id: UUID) -> Link: "", response_model=List[schemas.Link], response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Link.Audit"))] + dependencies=[Depends(has_privilege("Link.Audit"))], ) async def get_links(project_id: UUID) -> List[schemas.Link]: """ @@ -89,7 +89,7 @@ async def get_links(project_id: UUID) -> List[schemas.Link]: 404: {"model": schemas.ErrorMessage, "description": "Could not find project"}, 409: {"model": schemas.ErrorMessage, "description": "Could not create link"}, }, - dependencies=[Depends(has_privilege("Link.Allocate"))] + dependencies=[Depends(has_privilege("Link.Allocate"))], ) async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schemas.Link: """ @@ -123,10 +123,7 @@ async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schema return link.asdict() -@router.get( - "/{link_id}/available_filters", - dependencies=[Depends(has_privilege("Link.Audit"))] -) +@router.get("/{link_id}/available_filters", dependencies=[Depends(has_privilege("Link.Audit"))]) async def get_filters(link: Link = Depends(dep_link)) -> List[dict]: """ Return all filters available for a given link. @@ -141,7 +138,7 @@ async def get_filters(link: Link = Depends(dep_link)) -> List[dict]: "/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Link.Audit"))] + dependencies=[Depends(has_privilege("Link.Audit"))], ) async def get_link(link: Link = Depends(dep_link)) -> schemas.Link: """ @@ -157,7 +154,7 @@ async def get_link(link: Link = Depends(dep_link)) -> schemas.Link: "/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Link.Modify"))] + dependencies=[Depends(has_privilege("Link.Modify"))], ) async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link: """ @@ -181,14 +178,12 @@ async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_li @router.delete( - "/{link_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Link.Allocate"))] + "/{link_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Link.Allocate"))] ) async def delete_link( - project_id: UUID, - link: Link = Depends(dep_link), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + project_id: UUID, + link: Link = Depends(dep_link), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a link. @@ -201,11 +196,7 @@ async def delete_link( await rbac_repo.delete_all_ace_starting_with_path(f"/links/{link.id}") -@router.post( - "/{link_id}/reset", - response_model=schemas.Link, - dependencies=[Depends(has_privilege("Link.Modify"))] -) +@router.post("/{link_id}/reset", response_model=schemas.Link, dependencies=[Depends(has_privilege("Link.Modify"))]) async def reset_link(link: Link = Depends(dep_link)) -> schemas.Link: """ Reset a link. @@ -221,12 +212,10 @@ async def reset_link(link: Link = Depends(dep_link)) -> schemas.Link: "/{link_id}/capture/start", status_code=status.HTTP_201_CREATED, response_model=schemas.Link, - dependencies=[Depends(has_privilege("Link.Capture"))] + dependencies=[Depends(has_privilege("Link.Capture"))], ) async def start_capture( - capture_data: schemas.LinkCapture, - http_request: Request, - link: Link = Depends(dep_link) + capture_data: schemas.LinkCapture, http_request: Request, link: Link = Depends(dep_link) ) -> schemas.Link: """ Start packet capture on the link. @@ -242,7 +231,7 @@ async def start_capture( data_link_type=capture_data.data_link_type, capture_file_name=capture_data.capture_file_name, wireshark=capture_data.wireshark, - jwt_token=jwt_token + jwt_token=jwt_token, ) return link.asdict() @@ -250,7 +239,7 @@ async def start_capture( @router.post( "/{link_id}/capture/stop", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Link.Capture"))] + dependencies=[Depends(has_privilege("Link.Capture"))], ) async def stop_capture(link: Link = Depends(dep_link)) -> None: """ @@ -265,12 +254,9 @@ async def stop_capture(link: Link = Depends(dep_link)) -> None: @router.post( "/{link_id}/capture/wireshark/restart", status_code=status.HTTP_200_OK, - dependencies=[Depends(has_privilege("Link.Capture"))] + dependencies=[Depends(has_privilege("Link.Capture"))], ) -async def restart_wireshark( - http_request: Request, - link: Link = Depends(dep_link) -) -> dict: +async def restart_wireshark(http_request: Request, link: Link = Depends(dep_link)) -> dict: """ Restart Wireshark window without stopping the capture. @@ -289,10 +275,7 @@ async def restart_wireshark( return {"status": "restarted"} -@router.get( - "/{link_id}/capture/stream", - dependencies=[Depends(has_privilege("Link.Capture"))] -) +@router.get("/{link_id}/capture/stream", dependencies=[Depends(has_privilege("Link.Capture"))]) async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> StreamingResponse: """ Stream the PCAP capture file from compute. @@ -318,13 +301,13 @@ async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> Strea try: ssl_context = Controller.instance().ssl_context() async with HTTPClient.request( - request.method, - pcap_streaming_url, - user=compute.user, - password=compute.password, - ssl_context=ssl_context, - timeout=None, - data=body + request.method, + pcap_streaming_url, + user=compute.user, + password=compute.password, + ssl_context=ssl_context, + timeout=None, + data=body, ) as response: async for data in response.content.iter_any(): if not data: @@ -337,9 +320,7 @@ async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> Strea @router.get( - "/{link_id}/capture/file", - dependencies=[Depends(has_privilege("Link.Capture"))], - response_class=FileResponse + "/{link_id}/capture/file", dependencies=[Depends(has_privilege("Link.Capture"))], response_class=FileResponse ) async def download_capture_file(link: Link = Depends(dep_link)): """ @@ -359,7 +340,7 @@ async def download_capture_file(link: Link = Depends(dep_link)): return FileResponse( path=link.capture_file_path, filename=os.path.basename(link.capture_file_path), - media_type="application/vnd.tcpdump.pcap" + media_type="application/vnd.tcpdump.pcap", ) @@ -368,7 +349,7 @@ async def web_wireshark_websocket( websocket: WebSocket, link_id: str, project_id: str, - current_user: schemas.User = Depends(has_privilege_on_websocket("Link.Capture")) + current_user: schemas.User = Depends(has_privilege_on_websocket("Link.Capture")), ): """ WebSocket proxy endpoint for xpra container (Web Wireshark). @@ -424,10 +405,7 @@ async def web_wireshark_websocket( pass -@router.get( - "/{link_id}/markers", - dependencies=[Depends(has_privilege("Link.Audit"))] -) +@router.get("/{link_id}/markers", dependencies=[Depends(has_privilege("Link.Audit"))]) async def get_markers(link: Link = Depends(dep_link)) -> dict: """ Return all traffic-insight markers configured on this link. @@ -439,14 +417,9 @@ async def get_markers(link: Link = Depends(dep_link)) -> dict: @router.post( - "/{link_id}/markers", - status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Link.Modify"))] + "/{link_id}/markers", status_code=status.HTTP_201_CREATED, dependencies=[Depends(has_privilege("Link.Modify"))] ) -async def create_marker( - marker_data: schemas.MarkerCreate, - link: Link = Depends(dep_link) -) -> dict: +async def create_marker(marker_data: schemas.MarkerCreate, link: Link = Depends(dep_link)) -> dict: """ Attach a traffic-insight marker to the link. On BPF match uBridge emits MARK signals and appends packets to a pcap. @@ -477,12 +450,9 @@ async def create_marker( @router.delete( "/{link_id}/markers/{marker_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Link.Modify"))] + dependencies=[Depends(has_privilege("Link.Modify"))], ) -async def delete_marker( - marker_name: str, - link: Link = Depends(dep_link) -) -> None: +async def delete_marker(marker_name: str, link: Link = Depends(dep_link)) -> None: """ Remove a traffic-insight marker from the link. @@ -492,15 +462,8 @@ async def delete_marker( await link.stop_marker(marker_name) -@router.put( - "/{link_id}/markers/{marker_name}", - dependencies=[Depends(has_privilege("Link.Modify"))] -) -async def update_marker( - marker_name: str, - marker_data: schemas.MarkerUpdate, - link: Link = Depends(dep_link) -) -> dict: +@router.put("/{link_id}/markers/{marker_name}", dependencies=[Depends(has_privilege("Link.Modify"))]) +async def update_marker(marker_name: str, marker_data: schemas.MarkerUpdate, link: Link = Depends(dep_link)) -> dict: """ Update a traffic-insight marker (change BPF, tag, or enabled). @@ -522,7 +485,7 @@ async def update_marker( @router.get( "/{link_id}/iface", response_model=Union[schemas.UDPPortInfo, schemas.EthernetPortInfo], - dependencies=[Depends(has_privilege("Link.Audit"))] + dependencies=[Depends(has_privilege("Link.Audit"))], ) async def get_iface(link: Link = Depends(dep_link)) -> Union[schemas.UDPPortInfo, schemas.EthernetPortInfo]: """ @@ -544,7 +507,7 @@ async def get_iface(link: Link = Depends(dep_link)) -> Union[schemas.UDPPortInfo if "ports_mapping" not in response.json: continue ports_mapping = response.json["ports_mapping"] - + for port in ports_mapping: port_num = port.get("port_number") @@ -556,7 +519,7 @@ async def get_iface(link: Link = Depends(dep_link)) -> Union[schemas.UDPPortInfo "type": f"{port_type}", "lport": port["lport"], "rhost": port["rhost"], - "rport": port["rport"] + "rport": port["rport"], } else: ifaces_info = { @@ -564,7 +527,7 @@ async def get_iface(link: Link = Depends(dep_link)) -> Union[schemas.UDPPortInfo "type": f"{port_type}", "interface": port["interface"], } - + if not ifaces_info: raise ControllerError("Link not connected to Cloud/NAT") return ifaces_info diff --git a/gns3server/api/routes/controller/llm_model_configs.py b/gns3server/api/routes/controller/llm_model_configs.py index e6c5b4338..620f75c26 100644 --- a/gns3server/api/routes/controller/llm_model_configs.py +++ b/gns3server/api/routes/controller/llm_model_configs.py @@ -60,14 +60,12 @@ def _filter_api_key_from_config(config: dict) -> dict: # User LLM Model Configuration Endpoints # ============================================================================ -@router.get( - "/users/{user_id}/llm-model-configs", - response_model=schemas.LLMModelConfigInheritedResponse -) + +@router.get("/users/{user_id}/llm-model-configs", response_model=schemas.LLMModelConfigInheritedResponse) async def get_user_llm_model_configs( - user_id: UUID, - current_user: schemas.User = Depends(has_privilege("LLMConfig.Audit")), - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + user_id: UUID, + current_user: schemas.User = Depends(has_privilege("LLMConfig.Audit")), + llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)), ) -> schemas.LLMModelConfigInheritedResponse: """ Get user's effective LLM model configurations (own + inherited from groups). @@ -77,31 +75,25 @@ async def get_user_llm_model_configs( try: result = await llm_repo.get_user_effective_configs( - user_id, - current_user_id=current_user.user_id, - current_user_is_superadmin=current_user.is_superadmin + user_id, current_user_id=current_user.user_id, current_user_is_superadmin=current_user.is_superadmin ) return schemas.LLMModelConfigInheritedResponse( - configs=result["configs"], - default_config=result.get("default_config"), - total=len(result["configs"]) + configs=result["configs"], default_config=result.get("default_config"), total=len(result["configs"]) ) except Exception as e: log.error(f"Failed to retrieve user LLM model configs: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to retrieve LLM model configurations" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve LLM model configurations" ) @router.get( "/users/{user_id}/llm-model-configs/own", response_model=List[schemas.LLMModelConfigResponse], - dependencies=[Depends(has_privilege("LLMConfig.Audit"))] + dependencies=[Depends(has_privilege("LLMConfig.Audit"))], ) async def get_user_own_llm_model_configs( - user_id: UUID, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + user_id: UUID, llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) ) -> List[schemas.LLMModelConfigResponse]: """ Get user's own LLM model configurations (excluding inherited ones). @@ -122,26 +114,24 @@ async def get_user_own_llm_model_configs( is_default=config.is_default, version=config.version, created_at=config.created_at, - updated_at=config.updated_at + updated_at=config.updated_at, ) for config in configs ] except Exception as e: log.error(f"Failed to retrieve user's own LLM model configs: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to retrieve LLM model configurations" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve LLM model configurations" ) @router.get( "/users/{user_id}/llm-model-configs/default", response_model=schemas.LLMModelConfigResponse, - dependencies=[Depends(has_privilege("LLMConfig.Audit"))] + dependencies=[Depends(has_privilege("LLMConfig.Audit"))], ) async def get_user_default_llm_model_config( - user_id: UUID, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + user_id: UUID, llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) ) -> schemas.LLMModelConfigResponse: """ Get user's default LLM model configuration. @@ -154,7 +144,7 @@ async def get_user_default_llm_model_config( if not config: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"No default LLM model configuration found for user '{user_id}'" + detail=f"No default LLM model configuration found for user '{user_id}'", ) return schemas.LLMModelConfigResponse( @@ -167,15 +157,14 @@ async def get_user_default_llm_model_config( is_default=config.is_default, version=config.version, created_at=config.created_at, - updated_at=config.updated_at + updated_at=config.updated_at, ) except HTTPException: raise except Exception as e: log.error(f"Failed to retrieve user's default LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to retrieve LLM model configuration" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve LLM model configuration" ) @@ -183,13 +172,13 @@ async def get_user_default_llm_model_config( "/users/{user_id}/llm-model-configs", response_model=schemas.LLMModelConfigResponse, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("LLMConfig.Modify"))] + dependencies=[Depends(has_privilege("LLMConfig.Modify"))], ) async def create_user_llm_model_config( - user_id: UUID, - config_create: schemas.LLMModelConfigCreate, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + user_id: UUID, + config_create: schemas.LLMModelConfigCreate, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)), ) -> schemas.LLMModelConfigResponse: """ Create a new LLM model configuration for a user. @@ -206,7 +195,7 @@ async def create_user_llm_model_config( raise ControllerNotFoundError(f"User '{user_id}' not found") # Validate context_limit is provided - if not hasattr(config_create, 'context_limit') or config_create.context_limit is None: + if not hasattr(config_create, "context_limit") or config_create.context_limit is None: raise ControllerBadRequestError( "context_limit is required (unit: K tokens, e.g., 128 = 128K = 128,000 tokens). " "Please check your model provider's documentation for the current context window size " @@ -217,11 +206,7 @@ async def create_user_llm_model_config( # Extract config fields (excluding table-level fields) config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"}) new_config = await llm_repo.create_user_config( - user_id, - config_create.name, - config_create.model_type, - config_fields, - is_default=config_create.is_default + user_id, config_create.name, config_create.model_type, config_fields, is_default=config_create.is_default ) return schemas.LLMModelConfigResponse( @@ -234,28 +219,27 @@ async def create_user_llm_model_config( is_default=new_config.is_default, version=new_config.version, created_at=new_config.created_at, - updated_at=new_config.updated_at + updated_at=new_config.updated_at, ) except ValueError as e: raise ControllerBadRequestError(str(e)) except Exception as e: log.error(f"Failed to create LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to create LLM model configuration: {e}" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create LLM model configuration: {e}" ) @router.put( "/users/{user_id}/llm-model-configs/{config_id}", response_model=schemas.LLMModelConfigResponse, - dependencies=[Depends(has_privilege("LLMConfig.Modify"))] + dependencies=[Depends(has_privilege("LLMConfig.Modify"))], ) async def update_user_llm_model_config( - user_id: UUID, - config_id: UUID, - config_update: schemas.LLMModelConfigUpdate, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + user_id: UUID, + config_id: UUID, + config_update: schemas.LLMModelConfigUpdate, + llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)), ) -> schemas.LLMModelConfigResponse: """ Update a user's LLM model configuration. @@ -272,16 +256,12 @@ async def update_user_llm_model_config( expected_version = updates.pop("expected_version", None) updated_config = await llm_repo.update_user_config( - config_id, - user_id, - updates, - expected_version=expected_version + config_id, user_id, updates, expected_version=expected_version ) if not updated_config: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"LLM model configuration '{config_id}' not found" + status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found" ) return schemas.LLMModelConfigResponse( @@ -294,35 +274,31 @@ async def update_user_llm_model_config( is_default=updated_config.is_default, version=updated_config.version, created_at=updated_config.created_at, - updated_at=updated_config.updated_at + updated_at=updated_config.updated_at, ) except HTTPException: raise except ValueError as e: # Handle optimistic lock errors if "Concurrent modification" in str(e): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=str(e) - ) + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e)) raise ControllerBadRequestError(str(e)) except Exception as e: log.error(f"Failed to update LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to update LLM model configuration" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update LLM model configuration" ) @router.delete( "/users/{user_id}/llm-model-configs/{config_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("LLMConfig.Modify"))] + dependencies=[Depends(has_privilege("LLMConfig.Modify"))], ) async def delete_user_llm_model_config( - user_id: UUID, - config_id: UUID, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + user_id: UUID, + config_id: UUID, + llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)), ) -> None: """ Delete a user's LLM model configuration. @@ -334,28 +310,26 @@ async def delete_user_llm_model_config( success = await llm_repo.delete_user_config(config_id, user_id) if not success: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"LLM model configuration '{config_id}' not found" + status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found" ) except HTTPException: raise except Exception as e: log.error(f"Failed to delete LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to delete LLM model configuration" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete LLM model configuration" ) @router.put( "/users/{user_id}/llm-model-configs/default/{config_id}", response_model=schemas.LLMModelConfigResponse, - dependencies=[Depends(has_privilege("LLMConfig.Modify"))] + dependencies=[Depends(has_privilege("LLMConfig.Modify"))], ) async def set_user_default_llm_model_config( - user_id: UUID, - config_id: UUID, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + user_id: UUID, + config_id: UUID, + llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)), ) -> schemas.LLMModelConfigResponse: """ Set a user's default LLM model configuration. @@ -367,8 +341,7 @@ async def set_user_default_llm_model_config( success = await llm_repo.set_user_default_config(user_id, config_id) if not success: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"LLM model configuration '{config_id}' not found" + status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found" ) # Get the updated config @@ -383,15 +356,14 @@ async def set_user_default_llm_model_config( is_default=config.is_default, version=config.version, created_at=config.created_at, - updated_at=config.updated_at + updated_at=config.updated_at, ) except HTTPException: raise except Exception as e: log.error(f"Failed to set default LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to set default LLM model configuration" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to set default LLM model configuration" ) @@ -399,14 +371,14 @@ async def set_user_default_llm_model_config( # Group LLM Model Configuration Endpoints # ============================================================================ + @router.get( "/groups/{group_id}/llm-model-configs", response_model=schemas.LLMModelConfigListResponse, - dependencies=[Depends(has_privilege("Group.Audit"))] + dependencies=[Depends(has_privilege("Group.Audit"))], ) async def get_group_llm_model_configs( - group_id: UUID, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + group_id: UUID, llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) ) -> schemas.LLMModelConfigListResponse: """ Get all LLM model configurations for a user group. @@ -427,7 +399,7 @@ async def get_group_llm_model_configs( is_default=config.is_default, version=config.version, created_at=config.created_at, - updated_at=config.updated_at + updated_at=config.updated_at, ) for config in configs ] @@ -444,26 +416,22 @@ async def get_group_llm_model_configs( default_config = config_responses[0] return schemas.LLMModelConfigListResponse( - configs=config_responses, - default_config=default_config, - total=len(config_responses) + configs=config_responses, default_config=default_config, total=len(config_responses) ) except Exception as e: log.error(f"Failed to retrieve group LLM model configs: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to retrieve LLM model configurations" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve LLM model configurations" ) @router.get( "/groups/{group_id}/llm-model-configs/default", response_model=schemas.LLMModelConfigResponse, - dependencies=[Depends(has_privilege("Group.Audit"))] + dependencies=[Depends(has_privilege("Group.Audit"))], ) async def get_group_default_llm_model_config( - group_id: UUID, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + group_id: UUID, llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) ) -> schemas.LLMModelConfigResponse: """ Get group's default LLM model configuration. @@ -476,7 +444,7 @@ async def get_group_default_llm_model_config( if not config: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"No default LLM model configuration found for group '{group_id}'" + detail=f"No default LLM model configuration found for group '{group_id}'", ) return schemas.LLMModelConfigResponse( @@ -489,15 +457,14 @@ async def get_group_default_llm_model_config( is_default=config.is_default, version=config.version, created_at=config.created_at, - updated_at=config.updated_at + updated_at=config.updated_at, ) except HTTPException: raise except Exception as e: log.error(f"Failed to retrieve group's default LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to retrieve LLM model configuration" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve LLM model configuration" ) @@ -505,13 +472,13 @@ async def get_group_default_llm_model_config( "/groups/{group_id}/llm-model-configs", response_model=schemas.LLMModelConfigResponse, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Group.Modify"))] + dependencies=[Depends(has_privilege("Group.Modify"))], ) async def create_group_llm_model_config( - group_id: UUID, - config_create: schemas.LLMModelConfigCreate, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + group_id: UUID, + config_create: schemas.LLMModelConfigCreate, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)), ) -> schemas.LLMModelConfigResponse: """ Create a new LLM model configuration for a user group. @@ -528,7 +495,7 @@ async def create_group_llm_model_config( raise ControllerNotFoundError(f"User group '{group_id}' not found") # Validate context_limit is provided - if not hasattr(config_create, 'context_limit') or config_create.context_limit is None: + if not hasattr(config_create, "context_limit") or config_create.context_limit is None: raise ControllerBadRequestError( "context_limit is required (unit: K tokens, e.g., 128 = 128K = 128,000 tokens). " "Please check your model provider's documentation for the current context window size " @@ -539,11 +506,7 @@ async def create_group_llm_model_config( # Extract config fields (excluding table-level fields) config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"}) new_config = await llm_repo.create_group_config( - group_id, - config_create.name, - config_create.model_type, - config_fields, - is_default=config_create.is_default + group_id, config_create.name, config_create.model_type, config_fields, is_default=config_create.is_default ) return schemas.LLMModelConfigResponse( @@ -556,28 +519,27 @@ async def create_group_llm_model_config( is_default=new_config.is_default, version=new_config.version, created_at=new_config.created_at, - updated_at=new_config.updated_at + updated_at=new_config.updated_at, ) except ValueError as e: raise ControllerBadRequestError(str(e)) except Exception as e: log.error(f"Failed to create LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to create LLM model configuration: {e}" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create LLM model configuration: {e}" ) @router.put( "/groups/{group_id}/llm-model-configs/{config_id}", response_model=schemas.LLMModelConfigResponse, - dependencies=[Depends(has_privilege("Group.Modify"))] + dependencies=[Depends(has_privilege("Group.Modify"))], ) async def update_group_llm_model_config( - group_id: UUID, - config_id: UUID, - config_update: schemas.LLMModelConfigUpdate, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + group_id: UUID, + config_id: UUID, + config_update: schemas.LLMModelConfigUpdate, + llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)), ) -> schemas.LLMModelConfigResponse: """ Update a group's LLM model configuration. @@ -594,16 +556,12 @@ async def update_group_llm_model_config( expected_version = updates.pop("expected_version", None) updated_config = await llm_repo.update_group_config( - config_id, - group_id, - updates, - expected_version=expected_version + config_id, group_id, updates, expected_version=expected_version ) if not updated_config: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"LLM model configuration '{config_id}' not found" + status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found" ) return schemas.LLMModelConfigResponse( @@ -616,35 +574,31 @@ async def update_group_llm_model_config( is_default=updated_config.is_default, version=updated_config.version, created_at=updated_config.created_at, - updated_at=updated_config.updated_at + updated_at=updated_config.updated_at, ) except HTTPException: raise except ValueError as e: # Handle optimistic lock errors if "Concurrent modification" in str(e): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=str(e) - ) + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e)) raise ControllerBadRequestError(str(e)) except Exception as e: log.error(f"Failed to update LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to update LLM model configuration" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update LLM model configuration" ) @router.delete( "/groups/{group_id}/llm-model-configs/{config_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Group.Modify"))] + dependencies=[Depends(has_privilege("Group.Modify"))], ) async def delete_group_llm_model_config( - group_id: UUID, - config_id: UUID, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + group_id: UUID, + config_id: UUID, + llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)), ) -> None: """ Delete a group's LLM model configuration. @@ -656,28 +610,26 @@ async def delete_group_llm_model_config( success = await llm_repo.delete_group_config(config_id, group_id) if not success: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"LLM model configuration '{config_id}' not found" + status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found" ) except HTTPException: raise except Exception as e: log.error(f"Failed to delete LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to delete LLM model configuration" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete LLM model configuration" ) @router.put( "/groups/{group_id}/llm-model-configs/default/{config_id}", response_model=schemas.LLMModelConfigResponse, - dependencies=[Depends(has_privilege("Group.Modify"))] + dependencies=[Depends(has_privilege("Group.Modify"))], ) async def set_group_default_llm_model_config( - group_id: UUID, - config_id: UUID, - llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)) + group_id: UUID, + config_id: UUID, + llm_repo: LLMModelConfigsRepository = Depends(get_repository(LLMModelConfigsRepository)), ) -> schemas.LLMModelConfigResponse: """ Set a group's default LLM model configuration. @@ -689,8 +641,7 @@ async def set_group_default_llm_model_config( success = await llm_repo.set_group_default_config(group_id, config_id) if not success: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"LLM model configuration '{config_id}' not found" + status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found" ) # Get the updated config @@ -705,13 +656,12 @@ async def set_group_default_llm_model_config( is_default=config.is_default, version=config.version, created_at=config.created_at, - updated_at=config.updated_at + updated_at=config.updated_at, ) except HTTPException: raise except Exception as e: log.error(f"Failed to set default LLM model config: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to set default LLM model configuration" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to set default LLM model configuration" ) diff --git a/gns3server/api/routes/controller/netmiko.py b/gns3server/api/routes/controller/netmiko.py index 1fd2b907d..827ac751c 100644 --- a/gns3server/api/routes/controller/netmiko.py +++ b/gns3server/api/routes/controller/netmiko.py @@ -55,6 +55,7 @@ def _load_netmiko_device_types() -> schemas.NetmikoDeviceTypeList: import importlib import netmiko + # "from netmiko import ssh_dispatcher" is shadowed by a function of the same # name in netmiko's __init__, so import the module through importlib sd = importlib.import_module("netmiko.ssh_dispatcher") @@ -79,9 +80,7 @@ def _load_netmiko_device_types() -> schemas.NetmikoDeviceTypeList: @router.get( - "/device_types", - response_model=schemas.NetmikoDeviceTypeList, - dependencies=[Depends(get_current_active_user)] + "/device_types", response_model=schemas.NetmikoDeviceTypeList, dependencies=[Depends(get_current_active_user)] ) def get_netmiko_device_types() -> schemas.NetmikoDeviceTypeList: """ @@ -97,6 +96,6 @@ def get_netmiko_device_types() -> schemas.NetmikoDeviceTypeList: except ImportError: raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, - detail="Netmiko is not available. Install AI dependencies with: pip install gns3-server[ai-features]" + detail="Netmiko is not available. Install AI dependencies with: pip install gns3-server[ai-features]", ) return _device_types_cache diff --git a/gns3server/api/routes/controller/nodes.py b/gns3server/api/routes/controller/nodes.py index 482c56146..0938ab087 100644 --- a/gns3server/api/routes/controller/nodes.py +++ b/gns3server/api/routes/controller/nodes.py @@ -125,7 +125,7 @@ def _check_node_type(node: Node, *required_types: str) -> None: 404: {"model": schemas.ErrorMessage, "description": "Could not find project"}, 409: {"model": schemas.ErrorMessage, "description": "Could not create node"}, }, - dependencies=[Depends(has_privilege("Node.Allocate"))] + dependencies=[Depends(has_privilege("Node.Allocate"))], ) async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node: """ @@ -145,11 +145,11 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends( "", response_model=List[schemas.Node], response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Node.Audit"))] + dependencies=[Depends(has_privilege("Node.Audit"))], ) def get_nodes( project: Project = Depends(dep_project), - tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)") + tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)"), ) -> List[schemas.Node]: """ Return all nodes belonging to a given project. @@ -213,7 +213,9 @@ async def stop_all_nodes(project: Project = Depends(dep_project)) -> None: raise -@router.post("/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))]) +@router.post( + "/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))] +) async def suspend_all_nodes(project: Project = Depends(dep_project)) -> None: """ Suspend all nodes belonging to a given project. @@ -270,7 +272,7 @@ async def get_node(node: Node = Depends(dep_node)) -> schemas.Node: "/{node_id}", response_model=schemas.Node, response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Node.Modify"))] + dependencies=[Depends(has_privilege("Node.Modify"))], ) async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node: """ @@ -294,11 +296,12 @@ async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_no "/{node_id}", status_code=status.HTTP_204_NO_CONTENT, responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Cannot delete node"}}, - dependencies=[Depends(has_privilege("Node.Allocate"))] + dependencies=[Depends(has_privilege("Node.Allocate"))], ) async def delete_node( - node_id: UUID, project: Project = Depends(dep_project), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + node_id: UUID, + project: Project = Depends(dep_project), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a node from a project. @@ -314,7 +317,7 @@ async def delete_node( "/{node_id}/duplicate", response_model=schemas.Node, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Node.Allocate"))] + dependencies=[Depends(has_privilege("Node.Allocate"))], ) async def duplicate_node(duplicate_data: schemas.NodeDuplicate, node: Node = Depends(dep_node)) -> schemas.Node: """ @@ -328,9 +331,7 @@ async def duplicate_node(duplicate_data: schemas.NodeDuplicate, node: Node = Dep @router.post( - "/{node_id}/start", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.PowerMgmt"))] + "/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))] ) async def start_node(start_data: Optional[dict] = None, node: Node = Depends(dep_node)) -> None: """ @@ -345,10 +346,9 @@ async def start_node(start_data: Optional[dict] = None, node: Node = Depends(dep if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED: raise + @router.post( - "/{node_id}/stop", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.PowerMgmt"))] + "/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))] ) async def stop_node(node: Node = Depends(dep_node)) -> None: """ @@ -367,7 +367,7 @@ async def stop_node(node: Node = Depends(dep_node)) -> None: @router.post( "/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.PowerMgmt"))] + dependencies=[Depends(has_privilege("Node.PowerMgmt"))], ) async def suspend_node(node: Node = Depends(dep_node)) -> None: """ @@ -383,9 +383,7 @@ async def suspend_node(node: Node = Depends(dep_node)) -> None: @router.post( - "/{node_id}/reload", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.PowerMgmt"))] + "/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))] ) async def reload_node(node: Node = Depends(dep_node)) -> None: """ @@ -400,10 +398,9 @@ async def reload_node(node: Node = Depends(dep_node)) -> None: if not e.status_code == status.HTTP_405_METHOD_NOT_ALLOWED: raise + @router.post( - "/{node_id}/isolate", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Link.Modify"))] + "/{node_id}/isolate", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Link.Modify"))] ) async def isolate_node(node: Node = Depends(dep_node)) -> None: """ @@ -417,9 +414,7 @@ async def isolate_node(node: Node = Depends(dep_node)) -> None: @router.post( - "/{node_id}/unisolate", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Link.Modify"))] + "/{node_id}/unisolate", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Link.Modify"))] ) async def unisolate_node(node: Node = Depends(dep_node)) -> None: """ @@ -436,7 +431,7 @@ async def unisolate_node(node: Node = Depends(dep_node)) -> None: "/{node_id}/links", response_model=List[schemas.Link], response_model_exclude_unset=True, - dependencies = [Depends(has_privilege("Link.Audit"))] + dependencies=[Depends(has_privilege("Link.Audit"))], ) async def get_node_links(node: Node = Depends(dep_node)) -> List[schemas.Link]: """ @@ -478,12 +473,10 @@ async def idlepc_proposals(node: Node = Depends(dep_node)) -> List[str]: @router.post( "/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.Allocate"))] + dependencies=[Depends(has_privilege("Node.Allocate"))], ) async def create_disk_image( - disk_name: str, - disk_data: schemas.QemuDiskImageCreate, - node: Node = Depends(dep_node) + disk_name: str, disk_data: schemas.QemuDiskImageCreate, node: Node = Depends(dep_node) ) -> None: """ Create a Qemu disk image. @@ -498,12 +491,10 @@ async def create_disk_image( @router.put( "/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.Allocate"))] + dependencies=[Depends(has_privilege("Node.Allocate"))], ) async def update_disk_image( - disk_name: str, - disk_data: schemas.QemuDiskImageUpdate, - node: Node = Depends(dep_node) + disk_name: str, disk_data: schemas.QemuDiskImageUpdate, node: Node = Depends(dep_node) ) -> None: """ Update a Qemu disk image. @@ -518,12 +509,9 @@ async def update_disk_image( @router.delete( "/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.Allocate"))] + dependencies=[Depends(has_privilege("Node.Allocate"))], ) -async def delete_disk_image( - disk_name: str, - node: Node = Depends(dep_node) -) -> None: +async def delete_disk_image(disk_name: str, node: Node = Depends(dep_node)) -> None: """ Delete a Qemu disk image. @@ -534,11 +522,13 @@ async def delete_disk_image( await node.delete(f"/disk_image/{disk_name}") -@router.get("/{node_id}/files", response_model=List[schemas.NodeFile], dependencies=[Depends(has_privilege("Node.Audit"))]) +@router.get( + "/{node_id}/files", response_model=List[schemas.NodeFile], dependencies=[Depends(has_privilege("Node.Audit"))] +) async def list_node_files( node: Node = Depends(dep_node), path: str = Query("", description="Subdirectory path within node directory"), - recursive: bool = Query(False, description="Recursively list all files") + recursive: bool = Query(False, description="Recursively list all files"), ) -> List[schemas.NodeFile]: """ List files in a node directory with detailed metadata. @@ -556,11 +546,7 @@ async def list_node_files( params["path"] = path if recursive: params["recursive"] = "true" - res = await node.compute.http_query( - "GET", url, - params=params if params else None, - timeout=None - ) + res = await node.compute.http_query("GET", url, params=params if params else None, timeout=None) return res.json @@ -582,8 +568,7 @@ async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response: path = f"/project-files/{node_type}/{node.id}/{path}" compute_resp = await node.compute.http_query( - "GET", f"/projects/{node.project.id}/files{path}", - timeout=None, stream=True + "GET", f"/projects/{node.project.id}/files{path}", timeout=None, stream=True ) async def streamer(): @@ -606,7 +591,7 @@ async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response: @router.post( "/{node_id}/files/{file_path:path}", status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Node.Modify"))] + dependencies=[Depends(has_privilege("Node.Modify"))], ) async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)): """ @@ -626,15 +611,14 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n # Stream request body directly to compute node await node.compute.http_query( - "POST", f"/projects/{node.project.id}/files{path}", - data=request.stream(), timeout=None + "POST", f"/projects/{node.project.id}/files{path}", data=request.stream(), timeout=None ) @router.delete( "/{node_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.Modify"))] + dependencies=[Depends(has_privilege("Node.Modify"))], ) async def delete_node_file(file_path: str, node: Node = Depends(dep_node)) -> None: """ @@ -651,17 +635,14 @@ async def delete_node_file(file_path: str, node: Node = Depends(dep_node)) -> No node_type = node.node_type path = f"/project-files/{node_type}/{node.id}/{path}" - await node.compute.http_query( - "DELETE", f"/projects/{node.project.id}/files{path}", - timeout=None - ) + await node.compute.http_query("DELETE", f"/projects/{node.project.id}/files{path}", timeout=None) @router.websocket("/{node_id}/console/ws") async def ws_console( - websocket: WebSocket, - current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")), - node: Node = Depends(dep_node) + websocket: WebSocket, + current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")), + node: Node = Depends(dep_node), ) -> None: """ WebSocket console. @@ -682,7 +663,7 @@ async def ws_console( # handle IPv6 address ip = ipaddress.ip_address(compute_host) if isinstance(ip, ipaddress.IPv6Address): - compute_host = '[' + compute_host + ']' + compute_host = "[" + compute_host + "]" except ValueError: pass @@ -710,8 +691,7 @@ async def ws_console( except WebSocketDisconnect: pass log.info( - f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" - f" console WebSocket" + f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller console WebSocket" ) async def ws_send(ws_console_compute): @@ -766,11 +746,12 @@ async def ws_console( except aiohttp.ClientError as e: log.error(f"Client error received when forwarding to compute console WebSocket: {e}") + @router.websocket("/{node_id}/console/vnc") async def vnc_console( - websocket: WebSocket, - current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")), - node: Node = Depends(dep_node) + websocket: WebSocket, + current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")), + node: Node = Depends(dep_node), ) -> None: """ VNC WebSocket console. @@ -791,7 +772,7 @@ async def vnc_console( # handle IPv6 address ip = ipaddress.ip_address(compute_host) if isinstance(ip, ipaddress.IPv6Address): - compute_host = '[' + compute_host + ']' + compute_host = "[" + compute_host + "]" except ValueError: pass @@ -852,7 +833,9 @@ async def vnc_console( else: auth = aiohttp.BasicAuth(user, "") ssl_context = Controller.instance().ssl_context() - async with HTTPClient.get_client().ws_connect(vnc_console_compute_url, auth=auth, ssl_context=ssl_context) as ws: + async with HTTPClient.get_client().ws_connect( + vnc_console_compute_url, auth=auth, ssl_context=ssl_context + ) as ws: tasks = [ asyncio.ensure_future(vnc_receive(ws)), asyncio.ensure_future(vnc_send(ws)), @@ -870,10 +853,9 @@ async def vnc_console( except aiohttp.ClientError as e: log.error(f"Client error received when forwarding to compute VNC console WebSocket: {e}") + @router.post( - "/console/reset", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.Console"))] + "/console/reset", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.Console"))] ) async def reset_console_all_nodes(project: Project = Depends(dep_project)) -> None: """ @@ -888,7 +870,7 @@ async def reset_console_all_nodes(project: Project = Depends(dep_project)) -> No @router.post( "/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Node.Console"))] + dependencies=[Depends(has_privilege("Node.Console"))], ) async def console_reset(node: Node = Depends(dep_node)) -> None: """ diff --git a/gns3server/api/routes/controller/pools.py b/gns3server/api/routes/controller/pools.py index 0d79e39bc..a5ef8e8b5 100644 --- a/gns3server/api/routes/controller/pools.py +++ b/gns3server/api/routes/controller/pools.py @@ -24,11 +24,7 @@ from uuid import UUID from typing import List from gns3server import schemas -from gns3server.controller.controller_error import ( - ControllerError, - ControllerBadRequestError, - ControllerNotFoundError -) +from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError, ControllerNotFoundError from gns3server.controller import Controller from gns3server.db.repositories.rbac import RbacRepository @@ -44,13 +40,9 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get( - "", - response_model=List[schemas.ResourcePool], - dependencies=[Depends(has_privilege("Pool.Audit"))] -) +@router.get("", response_model=List[schemas.ResourcePool], dependencies=[Depends(has_privilege("Pool.Audit"))]) async def get_resource_pools( - pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)) + pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), ) -> List[schemas.ResourcePool]: """ Get all resource pools. @@ -65,11 +57,11 @@ async def get_resource_pools( "", response_model=schemas.ResourcePool, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Pool.Allocate"))] + dependencies=[Depends(has_privilege("Pool.Allocate"))], ) async def create_resource_pool( - resource_pool_create: schemas.ResourcePoolCreate, - pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)) + resource_pool_create: schemas.ResourcePoolCreate, + pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), ) -> schemas.ResourcePool: """ Create a new resource pool @@ -78,19 +70,16 @@ async def create_resource_pool( """ if await pools_repo.get_resource_pool_by_name(resource_pool_create.name): - raise ControllerBadRequestError(f"Resource pool '{resource_pool_create.name}' already exists") + raise ControllerBadRequestError(f"Resource pool '{resource_pool_create.name}' already exists") return await pools_repo.create_resource_pool(resource_pool_create) @router.get( - "/{resource_pool_id}", - response_model=schemas.ResourcePool, - dependencies=[Depends(has_privilege("Pool.Audit"))] + "/{resource_pool_id}", response_model=schemas.ResourcePool, dependencies=[Depends(has_privilege("Pool.Audit"))] ) async def get_resource_pool( - resource_pool_id: UUID, - pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)) + resource_pool_id: UUID, pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)) ) -> schemas.ResourcePool: """ Get a resource pool. @@ -105,14 +94,12 @@ async def get_resource_pool( @router.put( - "/{resource_pool_id}", - response_model=schemas.ResourcePool, - dependencies=[Depends(has_privilege("Pool.Modify"))] + "/{resource_pool_id}", response_model=schemas.ResourcePool, dependencies=[Depends(has_privilege("Pool.Modify"))] ) async def update_resource_pool( - resource_pool_id: UUID, - resource_pool_update: schemas.ResourcePoolUpdate, - pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)) + resource_pool_id: UUID, + resource_pool_update: schemas.ResourcePoolUpdate, + pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), ) -> schemas.ResourcePool: """ Update a resource pool. @@ -130,12 +117,12 @@ async def update_resource_pool( @router.delete( "/{resource_pool_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Pool.Allocate"))] + dependencies=[Depends(has_privilege("Pool.Allocate"))], ) async def delete_resource_pool( - resource_pool_id: UUID, - pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + resource_pool_id: UUID, + pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a resource pool. @@ -184,11 +171,11 @@ async def delete_resource_pool( @router.get( "/{resource_pool_id}/resources", response_model=List[schemas.Resource], - dependencies=[Depends(has_privilege("Pool.Audit"))] + dependencies=[Depends(has_privilege("Pool.Audit"))], ) async def get_pool_resources( - resource_pool_id: UUID, - pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), + resource_pool_id: UUID, + pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), ) -> List[schemas.Resource]: """ Get all resource in a pool. @@ -202,12 +189,12 @@ async def get_pool_resources( @router.put( "/{resource_pool_id}/resources/{resource_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Pool.Modify"))] + dependencies=[Depends(has_privilege("Pool.Modify"))], ) async def add_resource_to_pool( - resource_pool_id: UUID, - resource_id: UUID, - pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), + resource_pool_id: UUID, + resource_id: UUID, + pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), ) -> None: """ Add resource to a resource pool. @@ -240,7 +227,7 @@ async def add_resource_to_pool( @router.delete( "/{resource_pool_id}/resources/{resource_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Pool.Modify"))] + dependencies=[Depends(has_privilege("Pool.Modify"))], ) async def remove_resource_from_pool( resource_pool_id: UUID, diff --git a/gns3server/api/routes/controller/privileges.py b/gns3server/api/routes/controller/privileges.py index 45d250e60..a070a6ee6 100644 --- a/gns3server/api/routes/controller/privileges.py +++ b/gns3server/api/routes/controller/privileges.py @@ -32,7 +32,7 @@ router = APIRouter() response_model=List[schemas.Privilege], ) async def get_privileges( - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> List[schemas.Privilege]: """ Get all privileges. diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index 26fb7d042..19c29eead 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -71,14 +71,10 @@ def dep_project(project_id: UUID) -> Project: return project -@router.get( - "", - response_model=List[schemas.Project], - response_model_exclude_unset=True -) +@router.get("", response_model=List[schemas.Project], response_model_exclude_unset=True) async def get_projects( - current_user: schemas.User = Depends(get_current_active_user), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + current_user: schemas.User = Depends(get_current_active_user), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> List[schemas.Project]: """ Return all projects. @@ -124,11 +120,11 @@ async def get_projects( response_model=schemas.Project, response_model_exclude_unset=True, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create project"}}, - dependencies=[Depends(has_privilege("Project.Allocate"))] + dependencies=[Depends(has_privilege("Project.Allocate"))], ) async def create_project( - project_data: schemas.ProjectCreate, - current_user: schemas.User = Depends(get_current_active_user), + project_data: schemas.ProjectCreate, + current_user: schemas.User = Depends(get_current_active_user), ) -> schemas.Project: """ Create a new project. @@ -158,11 +154,10 @@ def get_project(project: Project = Depends(dep_project)) -> schemas.Project: "/{project_id}", response_model=schemas.Project, response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Project.Modify"))] + dependencies=[Depends(has_privilege("Project.Modify"))], ) async def update_project( - project_data: schemas.ProjectUpdate, - project: Project = Depends(dep_project) + project_data: schemas.ProjectUpdate, project: Project = Depends(dep_project) ) -> schemas.Project: """ Update a project. @@ -175,13 +170,11 @@ async def update_project( @router.delete( - "/{project_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Project.Allocate"))] + "/{project_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Project.Allocate"))] ) async def delete_project( - project: Project = Depends(dep_project), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + project: Project = Depends(dep_project), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a project. @@ -272,9 +265,7 @@ async def replay_tag_range( Required privilege: Project.Audit """ - return await _replay_response( - marker_replay.build_timeline(project, tag, filter_expr=filter, link_id=link) - ) + return await _replay_response(marker_replay.build_timeline(project, tag, filter_expr=filter, link_id=link)) @router.get( @@ -306,8 +297,13 @@ async def replay_tag_frames( return await _replay_response( marker_replay.query_frames( - project, tag, ts, window_ms=window_ms, limit=limit, - filter_expr=filter, link_id=link, + project, + tag, + ts, + window_ms=window_ms, + limit=limit, + filter_expr=filter, + link_id=link, ) ) @@ -343,9 +339,7 @@ async def replay_tag_frame_detail( """ return await _replay_response( - marker_replay.decode_frame( - project, tag, ts, node_id, link_id, marker, frame_number=frame_number - ) + marker_replay.decode_frame(project, tag, ts, node_id, link_id, marker, frame_number=frame_number) ) @@ -353,10 +347,8 @@ async def replay_tag_frame_detail( # Project-level marker definitions (global rules inherited by every link) # --------------------------------------------------------------------------- -@router.get( - "/{project_id}/marker-definitions", - dependencies=[Depends(has_privilege("Project.Audit"))] -) + +@router.get("/{project_id}/marker-definitions", dependencies=[Depends(has_privilege("Project.Audit"))]) def get_marker_definitions(project: Project = Depends(dep_project)) -> dict: """ Return all project-level marker definitions with their bound link IDs. @@ -368,9 +360,9 @@ def get_marker_definitions(project: Project = Depends(dep_project)) -> dict: for name, d in project.marker_definitions.items(): # Collect which links currently carry an inherited copy. bound = [ - lid for lid, link in project.links.items() - if f"global-{name}" in link.markers - and link.markers[f"global-{name}"].get("inherited_from") == name + lid + for lid, link in project.links.items() + if f"global-{name}" in link.markers and link.markers[f"global-{name}"].get("inherited_from") == name ] result[name] = {**d, "link_ids": bound} return result @@ -379,11 +371,10 @@ def get_marker_definitions(project: Project = Depends(dep_project)) -> dict: @router.post( "/{project_id}/marker-definitions", status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Project.Modify"))] + dependencies=[Depends(has_privilege("Project.Modify"))], ) async def create_marker_definition( - def_data: schemas.MarkerDefinitionCreate, - project: Project = Depends(dep_project) + def_data: schemas.MarkerDefinitionCreate, project: Project = Depends(dep_project) ) -> dict: """ Create a project-level marker definition and fan out to every link. @@ -406,14 +397,9 @@ async def create_marker_definition( return project.marker_definitions.get(name, {}) -@router.put( - "/{project_id}/marker-definitions/{def_name}", - dependencies=[Depends(has_privilege("Project.Modify"))] -) +@router.put("/{project_id}/marker-definitions/{def_name}", dependencies=[Depends(has_privilege("Project.Modify"))]) async def update_marker_definition( - def_name: str, - def_data: schemas.MarkerDefinitionCreate, - project: Project = Depends(dep_project) + def_name: str, def_data: schemas.MarkerDefinitionCreate, project: Project = Depends(dep_project) ) -> dict: """ Update a marker definition and sync all inherited copies on every link. @@ -436,12 +422,9 @@ async def update_marker_definition( @router.post( "/{project_id}/marker-definitions/{def_name}/pause", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Project.Modify"))] + dependencies=[Depends(has_privilege("Project.Modify"))], ) -async def pause_marker_definition( - def_name: str, - project: Project = Depends(dep_project) -) -> None: +async def pause_marker_definition(def_name: str, project: Project = Depends(dep_project)) -> None: """ Pause a definition: toggle off every inherited ``global-{def_name}`` copy on every link (uBridge ``enable_packet_filter off``, instant — no NIO @@ -457,12 +440,9 @@ async def pause_marker_definition( @router.post( "/{project_id}/marker-definitions/{def_name}/resume", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Project.Modify"))] + dependencies=[Depends(has_privilege("Project.Modify"))], ) -async def resume_marker_definition( - def_name: str, - project: Project = Depends(dep_project) -) -> None: +async def resume_marker_definition(def_name: str, project: Project = Depends(dep_project)) -> None: """Resume a paused definition (toggle on every inherited copy). Required privilege: Project.Modify @@ -474,12 +454,9 @@ async def resume_marker_definition( @router.delete( "/{project_id}/marker-definitions/{def_name}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Project.Modify"))] + dependencies=[Depends(has_privilege("Project.Modify"))], ) -async def delete_marker_definition( - def_name: str, - project: Project = Depends(dep_project) -) -> None: +async def delete_marker_definition(def_name: str, project: Project = Depends(dep_project)) -> None: """ Delete a marker definition and remove all inherited copies from every link. @@ -493,7 +470,7 @@ async def delete_marker_definition( "/{project_id}/close", status_code=status.HTTP_204_NO_CONTENT, responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not close project"}}, - dependencies=[Depends(has_privilege("Project.Allocate"))] + dependencies=[Depends(has_privilege("Project.Allocate"))], ) async def close_project(project: Project = Depends(dep_project)) -> None: """ @@ -510,7 +487,7 @@ async def close_project(project: Project = Depends(dep_project)) -> None: status_code=status.HTTP_201_CREATED, response_model=schemas.Project, responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not open project"}}, - dependencies=[Depends(has_privilege("Project.Allocate"))] + dependencies=[Depends(has_privilege("Project.Allocate"))], ) async def open_project(project: Project = Depends(dep_project)) -> schemas.Project: """ @@ -528,7 +505,7 @@ async def open_project(project: Project = Depends(dep_project)) -> schemas.Proje status_code=status.HTTP_201_CREATED, response_model=schemas.Project, responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not load project"}}, - dependencies=[Depends(has_privilege("Project.Allocate"))] + dependencies=[Depends(has_privilege("Project.Allocate"))], ) async def load_project(path: str = Body(..., embed=True)) -> schemas.Project: """ @@ -552,6 +529,7 @@ async def project_http_notifications(project_id: UUID) -> StreamingResponse: """ from gns3server.api.server import app + controller = Controller.instance() project = controller.get_project(str(project_id)) @@ -579,9 +557,9 @@ async def project_http_notifications(project_id: UUID) -> StreamingResponse: @router.websocket("/{project_id}/notifications/ws") async def project_ws_notifications( - project_id: UUID, - websocket: WebSocket, - current_user: schemas.User = Depends(has_privilege_on_websocket("Project.Audit")) + project_id: UUID, + websocket: WebSocket, + current_user: schemas.User = Depends(has_privilege_on_websocket("Project.Audit")), ) -> None: """ Receive project notifications about the controller from WebSocket. @@ -617,9 +595,9 @@ async def project_ws_notifications( @router.websocket("/{project_id}/notifications/markers/ws") async def project_marker_ws_notifications( - project_id: UUID, - websocket: WebSocket, - current_user: schemas.User = Depends(has_privilege_on_websocket("Project.Audit")) + project_id: UUID, + websocket: WebSocket, + current_user: schemas.User = Depends(has_privilege_on_websocket("Project.Audit")), ) -> None: """ Receive marker notifications (e.g. marker.match) for a project on a @@ -635,14 +613,18 @@ async def project_marker_ws_notifications( controller = Controller.instance() project = controller.get_project(str(project_id)) - log.info(f"New client has connected to the marker notification stream for project ID '{project.id}' (WebSocket method)") + log.info( + f"New client has connected to the marker notification stream for project ID '{project.id}' (WebSocket method)" + ) try: with controller.notification.project_marker_queue(project.id) as queue: while True: notification = await queue.get_json(5) await websocket.send_text(notification) except (ConnectionClosed, WebSocketDisconnect): - log.info(f"Client has disconnected from the marker notification stream for project ID '{project.id}' (WebSocket method)") + log.info( + f"Client has disconnected from the marker notification stream for project ID '{project.id}' (WebSocket method)" + ) except WebSocketException as e: log.warning(f"Error while sending marker event to WebSocket client: {e}") @@ -685,7 +667,9 @@ async def export_project( raise ControllerBadRequestError("Compression level must be between 1 and 22 for Zstandard compression") if compression_level is not None and compression_query in ("none", "lzma"): - raise ControllerBadRequestError(f"Compression level is not supported for '{compression_query}' compression method") + raise ControllerBadRequestError( + f"Compression level is not supported for '{compression_query}' compression method" + ) try: begin = time.time() @@ -693,8 +677,9 @@ async def export_project( working_dir = os.path.abspath(os.path.join(project.path, os.pardir)) async def streamer(): - log.info(f"Exporting project '{project.name}' with '{compression_query}' compression " - f"(level {compression_level})") + log.info( + f"Exporting project '{project.name}' with '{compression_query}' compression (level {compression_level})" + ) with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir: with aiozipstream.ZipFile(compression=compression, compresslevel=compression_level) as zstream: await export_controller_project( @@ -720,7 +705,7 @@ async def export_project( encoded = urllib.parse.quote(project.name, safe="") headers = { "Content-Disposition": ( - f'attachment; filename="{fallback}.gns3project"; filename*=UTF-8\'\'{encoded}.gns3project' + f"attachment; filename=\"{fallback}.gns3project\"; filename*=UTF-8''{encoded}.gns3project" ) } return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers) @@ -730,13 +715,9 @@ async def export_project( "/{project_id}/import", status_code=status.HTTP_201_CREATED, response_model=schemas.Project, - dependencies=[Depends(has_privilege("Project.Allocate"))] + dependencies=[Depends(has_privilege("Project.Allocate"))], ) -async def import_project( - project_id: UUID, - request: Request, - name: Optional[str] = None -) -> schemas.Project: +async def import_project(project_id: UUID, request: Request, name: Optional[str] = None) -> schemas.Project: """ Import a project from a portable archive. @@ -768,14 +749,14 @@ async def import_project( status_code=status.HTTP_201_CREATED, response_model=schemas.Project, responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not duplicate project"}}, - dependencies=[Depends(has_privilege("Project.Audit"))] + dependencies=[Depends(has_privilege("Project.Audit"))], ) async def duplicate_project( - project_data: schemas.ProjectDuplicate, - project: Project = Depends(dep_project), - current_user: schemas.User = Depends(get_current_active_user), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), - pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)) + project_data: schemas.ProjectDuplicate, + project: Project = Depends(dep_project), + current_user: schemas.User = Depends(get_current_active_user), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), ) -> schemas.Project: """ Duplicate a project. @@ -790,22 +771,26 @@ async def duplicate_project( can_be_duplicated_somewhere = False if pool_memberships: for pool in pool_memberships: - if await rbac_repo.check_user_has_privilege(current_user.user_id, f"/pools/{pool.resource_pool_id}", "Project.Allocate"): + if await rbac_repo.check_user_has_privilege( + current_user.user_id, f"/pools/{pool.resource_pool_id}", "Project.Allocate" + ): can_be_duplicated_somewhere = True break - if not can_be_duplicated_somewhere and not await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", "Project.Allocate"): + if not can_be_duplicated_somewhere and not await rbac_repo.check_user_has_privilege( + current_user.user_id, "/projects", "Project.Allocate" + ): log.warning(f"Project {project.name} cannot be duplicated anywhere") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) reset_mac_addresses = project_data.reset_mac_addresses - new_project = await project.duplicate( - name=project_data.name, reset_mac_addresses=reset_mac_addresses - ) + new_project = await project.duplicate(name=project_data.name, reset_mac_addresses=reset_mac_addresses) # Add the new project in the same resource pools if the duplicated project belongs to any if pool_memberships: - resource_create = schemas.ResourceCreate(resource_id=new_project.id, resource_type="project", name=new_project.name) + resource_create = schemas.ResourceCreate( + resource_id=new_project.id, resource_type="project", name=new_project.name + ) resource = await pools_repo.create_resource(resource_create) for pool in pool_memberships: await pools_repo.add_resource_to_pool(pool.resource_pool_id, resource) @@ -827,7 +812,7 @@ async def locked_project(project: Project = Depends(dep_project)) -> bool: @router.post( "/{project_id}/lock", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Project.Modify"))] + dependencies=[Depends(has_privilege("Project.Modify"))], ) async def lock_project(project: Project = Depends(dep_project)) -> None: """ @@ -842,7 +827,7 @@ async def lock_project(project: Project = Depends(dep_project)) -> None: @router.post( "/{project_id}/unlock", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Project.Modify"))] + dependencies=[Depends(has_privilege("Project.Modify"))], ) async def unlock_project(project: Project = Depends(dep_project)) -> None: """ @@ -894,7 +879,7 @@ async def get_project_gns3_file(project: Project = Depends(dep_project)) -> File @router.post( "/{project_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Project.Modify"))] + dependencies=[Depends(has_privilege("Project.Modify"))], ) async def write_file(file_path: str, request: Request, project: Project = Depends(dep_project)) -> None: """ @@ -929,7 +914,7 @@ async def write_file(file_path: str, request: Request, project: Project = Depend response_model=schemas.Node, status_code=status.HTTP_201_CREATED, responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or template"}}, - dependencies=[Depends(has_privilege("Node.Allocate"))] + dependencies=[Depends(has_privilege("Node.Allocate"))], ) async def create_node_from_template( project_id: UUID, diff --git a/gns3server/api/routes/controller/roles.py b/gns3server/api/routes/controller/roles.py index f1a5434f1..aab6e5a00 100644 --- a/gns3server/api/routes/controller/roles.py +++ b/gns3server/api/routes/controller/roles.py @@ -42,14 +42,8 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get( - "", - response_model=List[schemas.Role], - dependencies=[Depends(has_privilege("Role.Audit"))] -) -async def get_roles( - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) -) -> List[schemas.Role]: +@router.get("", response_model=List[schemas.Role], dependencies=[Depends(has_privilege("Role.Audit"))]) +async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[schemas.Role]: """ Get all roles. @@ -63,11 +57,10 @@ async def get_roles( "", response_model=schemas.Role, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Role.Allocate"))] + dependencies=[Depends(has_privilege("Role.Allocate"))], ) async def create_role( - role_create: schemas.RoleCreate, - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + role_create: schemas.RoleCreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> schemas.Role: """ Create a new role. @@ -81,14 +74,10 @@ async def create_role( return await rbac_repo.create_role(role_create) -@router.get( - "/{role_id}", - response_model=schemas.Role, - dependencies=[Depends(has_privilege("Role.Audit"))] -) +@router.get("/{role_id}", response_model=schemas.Role, dependencies=[Depends(has_privilege("Role.Audit"))]) async def get_role( - role_id: UUID, - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), + role_id: UUID, + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> schemas.Role: """ Get a role. @@ -102,15 +91,9 @@ async def get_role( return role -@router.put( - "/{role_id}", - response_model=schemas.Role, - dependencies=[Depends(has_privilege("Role.Modify"))] -) +@router.put("/{role_id}", response_model=schemas.Role, dependencies=[Depends(has_privilege("Role.Modify"))]) async def update_role( - role_id: UUID, - role_update: schemas.RoleUpdate, - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + role_id: UUID, role_update: schemas.RoleUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> schemas.Role: """ Update a role. @@ -129,9 +112,7 @@ async def update_role( @router.delete( - "/{role_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Role.Allocate"))] + "/{role_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Role.Allocate"))] ) async def delete_role( role_id: UUID, @@ -157,13 +138,10 @@ async def delete_role( @router.get( - "/{role_id}/privileges", - response_model=List[schemas.Privilege], - dependencies=[Depends(has_privilege("Role.Audit"))] + "/{role_id}/privileges", response_model=List[schemas.Privilege], dependencies=[Depends(has_privilege("Role.Audit"))] ) async def get_role_privileges( - role_id: UUID, - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> List[schemas.Privilege]: """ Get all role privileges. @@ -177,12 +155,10 @@ async def get_role_privileges( @router.put( "/{role_id}/privileges/{privilege_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Role.Modify"))] + dependencies=[Depends(has_privilege("Role.Modify"))], ) async def add_privilege_to_role( - role_id: UUID, - privilege_id: UUID, - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + role_id: UUID, privilege_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ) -> None: """ Add a privilege to a role. @@ -202,7 +178,7 @@ async def add_privilege_to_role( @router.delete( "/{role_id}/privileges/{privilege_id}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Role.Modify"))] + dependencies=[Depends(has_privilege("Role.Modify"))], ) async def remove_privilege_from_role( role_id: UUID, diff --git a/gns3server/api/routes/controller/settings.py b/gns3server/api/routes/controller/settings.py index 9924435ac..466edc726 100644 --- a/gns3server/api/routes/controller/settings.py +++ b/gns3server/api/routes/controller/settings.py @@ -44,35 +44,37 @@ router = APIRouter() # - port ranges are read once by the PortManager singleton # - default admin credentials are only used to seed the users database # - builtin templates/appliances and the skills repository are installed at startup -RESTART_REQUIRED = frozenset({ - "Server.host", - "Server.port", - "Server.protocol", - "Server.enable_ssl", - "Server.certfile", - "Server.certkey", - "Server.secrets_dir", - "Server.images_path", - "Server.projects_path", - "Server.appliances_path", - "Server.symbols_path", - "Server.configs_path", - "Server.resources_path", - "Server.console_start_port_range", - "Server.console_end_port_range", - "Server.vnc_console_start_port_range", - "Server.vnc_console_end_port_range", - "Server.udp_start_port_range", - "Server.udp_end_port_range", - "Server.enable_builtin_templates", - "Server.install_builtin_appliances", - "Server.skills_repo_url", - "Server.skills_repo_branch", - "Server.skills_auto_update", - "Server.ubridge_path", - "Controller.default_admin_username", - "Controller.default_admin_password", -}) +RESTART_REQUIRED = frozenset( + { + "Server.host", + "Server.port", + "Server.protocol", + "Server.enable_ssl", + "Server.certfile", + "Server.certkey", + "Server.secrets_dir", + "Server.images_path", + "Server.projects_path", + "Server.appliances_path", + "Server.symbols_path", + "Server.configs_path", + "Server.resources_path", + "Server.console_start_port_range", + "Server.console_end_port_range", + "Server.vnc_console_start_port_range", + "Server.vnc_console_end_port_range", + "Server.udp_start_port_range", + "Server.udp_end_port_range", + "Server.enable_builtin_templates", + "Server.install_builtin_appliances", + "Server.skills_repo_url", + "Server.skills_repo_branch", + "Server.skills_auto_update", + "Server.ubridge_path", + "Controller.default_admin_username", + "Controller.default_admin_password", + } +) # never expose these sections (deprecated) nor the secret managed outside # the configuration file; must match the response model in schemas.controller.settings @@ -89,9 +91,12 @@ def _current_settings_response() -> dict: return settings.model_dump(mode="json", exclude=_DUMP_EXCLUDE) -@router.get("", response_model=schemas.SettingsResponse, - dependencies=[Depends(has_privilege("Server.Audit"))], - responses={401: {"model": schemas.ErrorMessage}, 403: {"model": schemas.ErrorMessage}}) +@router.get( + "", + response_model=schemas.SettingsResponse, + dependencies=[Depends(has_privilege("Server.Audit"))], + responses={401: {"model": schemas.ErrorMessage}, 403: {"model": schemas.ErrorMessage}}, +) async def get_server_settings() -> schemas.SettingsResponse: """ Return the server settings. @@ -103,15 +108,18 @@ async def get_server_settings() -> schemas.SettingsResponse: return schemas.SettingsResponse.model_validate(_current_settings_response()) -@router.put("", response_model=schemas.SettingsUpdateResponse, - dependencies=[Depends(has_privilege("Server.Modify"))], - responses={ - 400: {"model": schemas.ErrorMessage}, - 401: {"model": schemas.ErrorMessage}, - 403: {"model": schemas.ErrorMessage}, - 409: {"model": schemas.ErrorMessage}, - 422: {"model": schemas.ErrorMessage}, - }) +@router.put( + "", + response_model=schemas.SettingsUpdateResponse, + dependencies=[Depends(has_privilege("Server.Modify"))], + responses={ + 400: {"model": schemas.ErrorMessage}, + 401: {"model": schemas.ErrorMessage}, + 403: {"model": schemas.ErrorMessage}, + 409: {"model": schemas.ErrorMessage}, + 422: {"model": schemas.ErrorMessage}, + }, +) async def update_server_settings(settings_update: schemas.SettingsUpdate) -> schemas.SettingsUpdateResponse: """ Update the server settings and persist them to the configuration file. @@ -122,9 +130,7 @@ async def update_server_settings(settings_update: schemas.SettingsUpdate) -> sch """ changes = { - section: options - for section, options in settings_update.model_dump(exclude_unset=True).items() - if options + section: options for section, options in settings_update.model_dump(exclude_unset=True).items() if options } # masked or empty secrets mean "unchanged": never write them back @@ -153,8 +159,7 @@ async def update_server_settings(settings_update: schemas.SettingsUpdate) -> sch controller = Controller.instance() if controller is not None: controller.notification.controller_emit( - "settings.updated", - {"changed": changed, "restart_required": restart_required} + "settings.updated", {"changed": changed, "restart_required": restart_required} ) data = _current_settings_response() diff --git a/gns3server/api/routes/controller/snapshots.py b/gns3server/api/routes/controller/snapshots.py index 37d9f7771..f85b5d9ef 100644 --- a/gns3server/api/routes/controller/snapshots.py +++ b/gns3server/api/routes/controller/snapshots.py @@ -53,11 +53,10 @@ def dep_project(project_id: UUID) -> Project: "", status_code=status.HTTP_201_CREATED, response_model=schemas.Snapshot, - dependencies=[Depends(has_privilege("Snapshot.Allocate"))] + dependencies=[Depends(has_privilege("Snapshot.Allocate"))], ) async def create_snapshot( - snapshot_data: schemas.SnapshotCreate, - project: Project = Depends(dep_project) + snapshot_data: schemas.SnapshotCreate, project: Project = Depends(dep_project) ) -> schemas.Snapshot: """ Create a new snapshot of a project. @@ -73,7 +72,7 @@ async def create_snapshot( "", response_model=List[schemas.Snapshot], response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Snapshot.Audit"))] + dependencies=[Depends(has_privilege("Snapshot.Audit"))], ) def get_snapshots(project: Project = Depends(dep_project)) -> List[schemas.Snapshot]: """ @@ -87,14 +86,10 @@ def get_snapshots(project: Project = Depends(dep_project)) -> List[schemas.Snaps @router.delete( - "/{snapshot_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Snapshot.Allocate"))] + "/{snapshot_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Snapshot.Allocate"))] ) async def delete_snapshot( - snapshot_id: UUID, - project: Project = Depends(dep_project), - rbac_repo=Depends(get_repository(RbacRepository)) + snapshot_id: UUID, project: Project = Depends(dep_project), rbac_repo=Depends(get_repository(RbacRepository)) ) -> None: """ Delete a snapshot. @@ -110,7 +105,7 @@ async def delete_snapshot( "/{snapshot_id}/restore", status_code=status.HTTP_201_CREATED, response_model=schemas.Project, - dependencies=[Depends(has_privilege("Snapshot.Restore"))] + dependencies=[Depends(has_privilege("Snapshot.Restore"))], ) async def restore_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)) -> schemas.Project: """ diff --git a/gns3server/api/routes/controller/symbols.py b/gns3server/api/routes/controller/symbols.py index 12c36d00c..5f004ffa6 100644 --- a/gns3server/api/routes/controller/symbols.py +++ b/gns3server/api/routes/controller/symbols.py @@ -40,10 +40,7 @@ log = logging.getLogger(__name__) router = APIRouter() -@router.get( - "", - dependencies=[Depends(has_privilege("Symbol.Audit"))] -) +@router.get("", dependencies=[Depends(has_privilege("Symbol.Audit"))]) def get_symbols() -> List[dict]: """ Return all symbols. @@ -58,7 +55,7 @@ def get_symbols() -> List[dict]: @router.get( "/{symbol_id:path}/raw", responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}}, - dependencies=[Depends(has_privilege("Symbol.Audit"))] + dependencies=[Depends(has_privilege("Symbol.Audit"))], ) async def get_symbol(symbol_id: str, request: Request) -> Response: """ @@ -74,7 +71,7 @@ async def get_symbol(symbol_id: str, request: Request) -> Response: symbol, headers={ "Access-Control-Allow-Origin": request.headers.get("origin", "*"), - } + }, ) except (KeyError, OSError) as e: raise ControllerNotFoundError(f"Could not get symbol file: {e}") @@ -83,7 +80,7 @@ async def get_symbol(symbol_id: str, request: Request) -> Response: @router.get( "/{symbol_id:path}/dimensions", responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}}, - dependencies=[Depends(has_privilege("Symbol.Audit"))] + dependencies=[Depends(has_privilege("Symbol.Audit"))], ) async def get_symbol_dimensions(symbol_id: str) -> dict: """ @@ -116,7 +113,7 @@ def get_default_symbols() -> dict: @router.post( "/{symbol_id:path}/raw", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Symbol.Allocate"))] + dependencies=[Depends(has_privilege("Symbol.Allocate"))], ) async def upload_symbol(symbol_id: str, request: Request) -> None: """ @@ -141,7 +138,7 @@ async def upload_symbol(symbol_id: str, request: Request) -> None: @router.delete( "/{symbol_id:path}", status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Symbol.Allocate"))] + dependencies=[Depends(has_privilege("Symbol.Allocate"))], ) async def delete_symbol(symbol_id: str) -> None: """ diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py index 6d59cd913..84d55d693 100644 --- a/gns3server/api/routes/controller/templates.py +++ b/gns3server/api/routes/controller/templates.py @@ -52,11 +52,11 @@ router = APIRouter(responses=responses) "", response_model=schemas.Template, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Template.Allocate"))] + dependencies=[Depends(has_privilege("Template.Allocate"))], ) async def create_template( template_create: schemas.TemplateCreate, - templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), ) -> schemas.Template: """ Create a new template. @@ -73,7 +73,7 @@ async def create_template( response_model=schemas.Template, response_model_exclude_unset=True, dependencies=[Depends(get_current_active_user)], - #dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui + # dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui ) async def get_template( template_id: UUID, @@ -102,7 +102,7 @@ async def get_template( "/{template_id}", response_model=schemas.Template, response_model_exclude_unset=True, - dependencies=[Depends(has_privilege("Template.Modify"))] + dependencies=[Depends(has_privilege("Template.Modify"))], ) async def update_template( template_id: UUID, @@ -119,16 +119,14 @@ async def update_template( @router.delete( - "/{template_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("Template.Allocate"))] + "/{template_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Template.Allocate"))] ) async def delete_template( - template_id: UUID, - prune_images: Optional[bool] = False, - templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), - images_repo: RbacRepository = Depends(get_repository(ImagesRepository)), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + template_id: UUID, + prune_images: Optional[bool] = False, + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), + images_repo: RbacRepository = Depends(get_repository(ImagesRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a template. @@ -152,7 +150,8 @@ async def delete_template( # the template being deleted is still in the database at this # point, exclude it from the other-templates check other_templates = [ - template for template in await images_repo.get_image_templates(image.image_id) + template + for template in await images_repo.get_image_templates(image.image_id) if str(template.template_id) != str(template_id) ] if other_templates: @@ -163,7 +162,9 @@ async def delete_template( referenced_filenames = controller.collect_referenced_image_filenames() if image.filename in referenced_filenames: project_names = controller.find_projects_using_image(image.filename) - raise ControllerError(f"Image '{image.path}' is used by one or more projects: {', '.join(project_names)}") + raise ControllerError( + f"Image '{image.path}' is used by one or more projects: {', '.join(project_names)}" + ) images_to_prune.append(image) await TemplatesService(templates_repo).delete_template(template_id) @@ -186,12 +187,12 @@ async def delete_template( response_model=List[schemas.Template], response_model_exclude_unset=True, dependencies=[Depends(get_current_active_user)], - #dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui + # dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui ) async def get_templates( - templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), - current_user: schemas.User = Depends(get_current_active_user), - tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)") + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), + current_user: schemas.User = Depends(get_current_active_user), + tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)"), ) -> List[schemas.Template]: """ Return all templates. @@ -239,10 +240,10 @@ async def get_templates( "/{template_id}/duplicate", response_model=schemas.Template, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("Template.Allocate"))] + dependencies=[Depends(has_privilege("Template.Allocate"))], ) async def duplicate_template( - template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) + template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) ) -> schemas.Template: """ Duplicate a template. @@ -253,10 +254,8 @@ async def duplicate_template( template = await TemplatesService(templates_repo).duplicate_template(template_id) return template -@router.get( - "/{template_id}/base-config/{filename}", - dependencies=[Depends(has_privilege("Template.Audit"))] -) + +@router.get("/{template_id}/base-config/{filename}", dependencies=[Depends(has_privilege("Template.Audit"))]) async def get_base_config( template_id: UUID, filename: str, @@ -267,17 +266,10 @@ async def get_base_config( await service.get_template(template_id) content = service.get_file(str(template_id), filename) - return { - "template_id": str(template_id), - "filename": os.path.basename(filename), - "content": content - } + return {"template_id": str(template_id), "filename": os.path.basename(filename), "content": content} -@router.put( - "/{template_id}/base-config/{filename}", - dependencies=[Depends(has_privilege("Template.Modify"))] -) +@router.put("/{template_id}/base-config/{filename}", dependencies=[Depends(has_privilege("Template.Modify"))]) async def update_base_config( template_id: UUID, filename: str, @@ -291,17 +283,10 @@ async def update_base_config( await service.get_template(template_id) service.update_file(str(template_id), filename, body["content"]) - return { - "template_id": str(template_id), - "filename": os.path.basename(filename), - "content": body["content"] - } + return {"template_id": str(template_id), "filename": os.path.basename(filename), "content": body["content"]} -@router.get( - "/{template_id}/base-configs", - dependencies=[Depends(has_privilege("Template.Audit"))] -) +@router.get("/{template_id}/base-configs", dependencies=[Depends(has_privilege("Template.Audit"))]) async def list_base_configs( template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), diff --git a/gns3server/api/routes/controller/users.py b/gns3server/api/routes/controller/users.py index 97fedd47f..1f834d712 100644 --- a/gns3server/api/routes/controller/users.py +++ b/gns3server/api/routes/controller/users.py @@ -144,8 +144,8 @@ async def refresh_access_token( @router.post("/logout", status_code=status.HTTP_204_NO_CONTENT) async def logout( - current_user: schemas.User = Depends(get_current_active_user), - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + current_user: schemas.User = Depends(get_current_active_user), + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> None: """ Logout the current user by revoking all existing tokens. @@ -165,9 +165,9 @@ async def get_logged_in_user(current_user: schemas.User = Depends(get_current_ac @router.put("/me", response_model=schemas.User) async def update_logged_in_user( - user_update: schemas.LoggedInUserUpdate, - current_user: schemas.User = Depends(get_current_active_user), - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) + user_update: schemas.LoggedInUserUpdate, + current_user: schemas.User = Depends(get_current_active_user), + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> schemas.User: """ Update the current active user. @@ -179,14 +179,8 @@ async def update_logged_in_user( return await users_repo.update_user(current_user.user_id, user_update) -@router.get( - "", - response_model=List[schemas.User], - dependencies=[Depends(has_privilege("User.Audit"))] -) -async def get_users( - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) -) -> List[schemas.User]: +@router.get("", response_model=List[schemas.User], dependencies=[Depends(has_privilege("User.Audit"))]) +async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRepository))) -> List[schemas.User]: """ Get all users. @@ -200,11 +194,10 @@ async def get_users( "", response_model=schemas.User, status_code=status.HTTP_201_CREATED, - dependencies=[Depends(has_privilege("User.Allocate"))] + dependencies=[Depends(has_privilege("User.Allocate"))], ) async def create_user( - user_create: schemas.UserCreate, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) + user_create: schemas.UserCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) ) -> schemas.User: """ Create a new user. @@ -221,14 +214,10 @@ async def create_user( return await users_repo.create_user(user_create) -@router.get( - "/{user_id}", - response_model=schemas.User, - dependencies=[Depends(has_privilege("User.Audit"))] -) +@router.get("/{user_id}", response_model=schemas.User, dependencies=[Depends(has_privilege("User.Audit"))]) async def get_user( - user_id: UUID, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + user_id: UUID, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> schemas.User: """ Get a user. @@ -242,15 +231,11 @@ async def get_user( return user -@router.put( - "/{user_id}", - response_model=schemas.User, - dependencies=[Depends(has_privilege("User.Modify"))] -) +@router.put("/{user_id}", response_model=schemas.User, dependencies=[Depends(has_privilege("User.Modify"))]) async def update_user( - user_id: UUID, - user_update: schemas.UserUpdate, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) + user_id: UUID, + user_update: schemas.UserUpdate, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), ) -> schemas.User: """ Update a user. @@ -271,14 +256,12 @@ async def update_user( @router.delete( - "/{user_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(has_privilege("User.Allocate"))] + "/{user_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("User.Allocate"))] ) async def delete_user( - user_id: UUID, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)), - rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) + user_id: UUID, + users_repo: UsersRepository = Depends(get_repository(UsersRepository)), + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ Delete a user. @@ -300,13 +283,10 @@ async def delete_user( @router.get( - "/{user_id}/groups", - response_model=List[schemas.UserGroup], - dependencies=[Depends(has_privilege("Group.Audit"))] + "/{user_id}/groups", response_model=List[schemas.UserGroup], dependencies=[Depends(has_privilege("Group.Audit"))] ) async def get_user_memberships( - user_id: UUID, - users_repo: UsersRepository = Depends(get_repository(UsersRepository)) + user_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) ) -> List[schemas.UserGroup]: """ Get user memberships. diff --git a/gns3server/api/server.py b/gns3server/api/server.py index 3b3218380..3eaade6e8 100644 --- a/gns3server/api/server.py +++ b/gns3server/api/server.py @@ -40,7 +40,7 @@ from gns3server.controller.controller_error import ( ControllerTimeoutError, ControllerForbiddenError, ControllerUnauthorizedError, - ComputeConflictError + ComputeConflictError, ) from gns3server.api.routes import controller, index @@ -52,6 +52,7 @@ from gns3server.agent import MCP_AVAILABLE if MCP_AVAILABLE: from gns3server.agent import mcp + _mcp_router = mcp.router else: from fastapi import APIRouter @@ -62,9 +63,10 @@ else: async def mcp_not_available(path: str = ""): raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, - detail="MCP is not available. Install AI dependencies with: pip install gns3-server[ai-features]" + detail="MCP is not available. Install AI dependencies with: pip install gns3-server[ai-features]", ) + import logging log = logging.getLogger(__name__) @@ -78,7 +80,7 @@ def get_application() -> FastAPI: description="This page describes the public controller API for GNS3", version="3.0.0", docs_url=None, - redoc_url=None + redoc_url=None, ) application.add_middleware( @@ -91,7 +93,7 @@ def get_application() -> FastAPI: application.include_router(index.router, tags=["Index"]) application.include_router(controller.router, prefix="/v3") - application.mount("/static", StaticFiles(packages=[('gns3server', 'static')], html=True), name="static") + application.mount("/static", StaticFiles(packages=[("gns3server", "static")], html=True), name="static") application.mount("/v3/compute", compute_api, name="compute") # Register MCP routes (stub returns 501 if MCP dependencies are not installed) @@ -118,6 +120,7 @@ def handle_exit(*args, **kwargs): UvicornServer.handle_exit = handle_exit + # Configure self-hosting JavaScript and CSS for docs @app.get("/docs", include_in_schema=False) async def custom_swagger_ui_html(): @@ -127,9 +130,10 @@ async def custom_swagger_ui_html(): oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, swagger_js_url="/static/swagger-ui-bundle.js", swagger_css_url="/static/swagger-ui.css", - swagger_favicon_url="/static/favicon.ico" + swagger_favicon_url="/static/favicon.ico", ) + @app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False) async def swagger_ui_redirect(): return get_swagger_ui_oauth2_redirect_html() @@ -141,9 +145,10 @@ async def redoc_html(): openapi_url=app.openapi_url, title=app.title + " - ReDoc", redoc_js_url="/static/redoc.standalone.js", - redoc_favicon_url="/static/favicon.ico" + redoc_favicon_url="/static/favicon.ico", ) + @app.exception_handler(ControllerError) async def controller_error_handler(request: Request, exc: ControllerError): log.error(f"Controller error in {request.url.path} ({request.method}): {exc}") @@ -210,11 +215,7 @@ async def compute_conflict_error_handler(request: Request, exc: ComputeConflictE # make sure the content key is "message", not "detail" per default @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): - return JSONResponse( - status_code=exc.status_code, - content={"message": exc.detail}, - headers=exc.headers - ) + return JSONResponse(status_code=exc.status_code, content={"message": exc.detail}, headers=exc.headers) @app.exception_handler(SQLAlchemyError) @@ -229,7 +230,4 @@ async def sqlalchemy_error_handler(request: Request, exc: SQLAlchemyError): @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): log.error(f"Request validation error in {request.url.path} ({request.method}): {exc}") - return JSONResponse( - status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, - content={"message": str(exc)} - ) + return JSONResponse(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, content={"message": str(exc)}) diff --git a/gns3server/compute/__init__.py b/gns3server/compute/__init__.py index 30cf3c493..9b7a6cc18 100644 --- a/gns3server/compute/__init__.py +++ b/gns3server/compute/__init__.py @@ -34,5 +34,6 @@ if ( # IOU & Docker only runs on Linux but test suite works on UNIX platform from .docker import Docker from .iou import IOU + MODULES.append(Docker) MODULES.append(IOU) diff --git a/gns3server/compute/adapters/adapter.py b/gns3server/compute/adapters/adapter.py index 8294a036d..6b34d352f 100644 --- a/gns3server/compute/adapters/adapter.py +++ b/gns3server/compute/adapters/adapter.py @@ -16,7 +16,6 @@ class Adapter: - """ Base class for adapters. diff --git a/gns3server/compute/adapters/ethernet_adapter.py b/gns3server/compute/adapters/ethernet_adapter.py index 1a09c25c1..d0116e9fd 100644 --- a/gns3server/compute/adapters/ethernet_adapter.py +++ b/gns3server/compute/adapters/ethernet_adapter.py @@ -18,7 +18,6 @@ from .adapter import Adapter class EthernetAdapter(Adapter): - """ Ethernet adapter. """ diff --git a/gns3server/compute/adapters/serial_adapter.py b/gns3server/compute/adapters/serial_adapter.py index 989d5be46..265398eee 100644 --- a/gns3server/compute/adapters/serial_adapter.py +++ b/gns3server/compute/adapters/serial_adapter.py @@ -18,7 +18,6 @@ from .adapter import Adapter class SerialAdapter(Adapter): - """ Serial adapter. """ diff --git a/gns3server/compute/base_manager.py b/gns3server/compute/base_manager.py index 8ef27265c..6ee63f427 100644 --- a/gns3server/compute/base_manager.py +++ b/gns3server/compute/base_manager.py @@ -50,7 +50,6 @@ CHUNK_SIZE = 1024 * 8 # 8KB class BaseManager: - """ Base class for all Manager classes. Responsible of management of a node pool of the same type. @@ -222,7 +221,7 @@ class BaseManager: if hasattr(source_node, "status") and source_node.status != "stopped": raise ComputeError("Cannot duplicate node data while the node is running") - + destination_dir = destination_node.working_dir try: shutil.rmtree(destination_dir) @@ -431,7 +430,6 @@ class BaseManager: ) if not os.path.isabs(orig_path): - for directory in valid_directory_prefices: log.debug(f"Searching for image '{orig_path}' in '{directory}'") path = self._recursive_search_file_in_directory(directory, orig_path) @@ -466,7 +464,7 @@ class BaseManager: for root, dirs, files in os.walk(directory): for file in files: - if s[1] == file and (s[0] == '' or root == os.path.join(directory, s[0])): + if s[1] == file and (s[0] == "" or root == os.path.join(directory, s[0])): path = os.path.normpath(os.path.join(root, s[1])) if os.path.exists(path): return path diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 30fa856c5..0f56bdd04 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -46,7 +46,6 @@ log = logging.getLogger(__name__) class BaseNode: - """ Base node implementation. @@ -460,8 +459,7 @@ class BaseNode: while True: try: (self._wrap_console_reader, self._wrap_console_writer) = await asyncio.open_connection( - host="127.0.0.1", - port=internal_port + host="127.0.0.1", port=internal_port ) break except (OSError, ConnectionRefusedError) as e: @@ -472,10 +470,7 @@ class BaseNode: if console_type == "telnet": await AsyncioTelnetServer.write_client_intro(self._wrap_console_writer, echo=True) server = AsyncioTelnetServer( - reader=self._wrap_console_reader, - writer=self._wrap_console_writer, - binary=True, - echo=True + reader=self._wrap_console_reader, writer=self._wrap_console_writer, binary=True, echo=True ) elif console_type == "ssh": server = AsyncioSSHServer(reader=self._wrap_console_reader, writer=self._wrap_console_writer) @@ -543,8 +538,7 @@ class BaseNode: """ log.info( - f"New client {websocket.client.host}:{websocket.client.port} has connected to compute" - f" console WebSocket" + f"New client {websocket.client.host}:{websocket.client.port} has connected to compute console WebSocket" ) if self.status != "started": @@ -555,8 +549,7 @@ class BaseNode: if self._console_type not in ("telnet", "ssh", "docker_exec"): await websocket.close(code=1000) log.warning( - f"Cannot open console WebSocket: node {self.name} console type '{self._console_type}' " - f"is not supported" + f"Cannot open console WebSocket: node {self.name} console type '{self._console_type}' is not supported" ) return @@ -609,8 +602,10 @@ class BaseNode: return None cols, rows = message.get("cols"), message.get("rows") if ( - isinstance(cols, int) and not isinstance(cols, bool) - and isinstance(rows, int) and not isinstance(rows, bool) + isinstance(cols, int) + and not isinstance(cols, bool) + and isinstance(rows, int) + and not isinstance(rows, bool) and 2 <= cols <= 5000 and 2 <= rows <= 100000 ): @@ -720,8 +715,7 @@ class BaseNode: """ log.info( - f"New client {websocket.client.host}:{websocket.client.port} has connected to compute " - f"VNC console WebSocket" + f"New client {websocket.client.host}:{websocket.client.port} has connected to compute VNC console WebSocket" ) if self.status != "started": @@ -730,15 +724,13 @@ class BaseNode: return if self._console_type != "vnc": await websocket.close(code=1000) - log.warning( - f"Cannot open VNC WebSocket: node {self.name} console type '{self._console_type}' is not vnc" - ) + log.warning(f"Cannot open VNC WebSocket: node {self.name} console type '{self._console_type}' is not vnc") return try: vnc_reader, vnc_writer = await asyncio.open_connection( self._manager.port_manager.console_host, - self.console # VNC port + self.console, # VNC port ) log.info(f"Connected to VNC server {self._manager.port_manager.console_host}:{self.console}") except ConnectionError as e: @@ -909,9 +901,7 @@ class BaseNode: elif console_type == "vnc": vnc_console_start_port_range, vnc_console_end_port_range = self._get_vnc_console_port_range() self._console = self._manager.port_manager.get_free_tcp_port( - self._project, - vnc_console_start_port_range, - vnc_console_end_port_range + self._project, vnc_console_start_port_range, vnc_console_end_port_range ) else: self._console = self._manager.port_manager.get_free_tcp_port(self._project) @@ -1044,9 +1034,7 @@ class BaseNode: log.debug(f"Starting new uBridge hypervisor at {self._ubridge_hypervisor.endpoint}") await self._ubridge_hypervisor.start() if self._ubridge_hypervisor: - log.info( - f"Hypervisor at {self._ubridge_hypervisor.endpoint} has successfully started" - ) + log.info(f"Hypervisor at {self._ubridge_hypervisor.endpoint} has successfully started") await self._ubridge_hypervisor.connect() # Tell this uBridge where to send MARK signals and which node id to # tag them with. Marker is opt-in and inert until a `mark` filter is @@ -1181,7 +1169,7 @@ class BaseNode: """ i = 0 - for (filter_type, values) in filters.items(): + for filter_type, values in filters.items(): if isinstance(values[0], str): for line in values[0].split("\n"): line = line.strip() @@ -1217,7 +1205,9 @@ class BaseNode: dlt = dlt[4:] return None if dlt == "EN10MB" else dlt - async def _ubridge_add_marker_filter(self, bridge_name, name, bpf, pcap_path, tag=None, link_id=None, direction=None, data_link_type=None): + async def _ubridge_add_marker_filter( + self, bridge_name, name, bpf, pcap_path, tag=None, link_id=None, direction=None, data_link_type=None + ): """ Attach a `mark` packet filter to a uBridge bridge for traffic insight. @@ -1246,9 +1236,7 @@ class BaseNode: # so allow up to 48 here. if not _MARKER_NAME_RE.match(name) or len(name) > 48: raise UbridgeError(f"Invalid marker name: {name!r}") - cmd = 'bridge add_packet_filter {bridge} {name} mark "{bpf}"'.format( - bridge=bridge_name, name=name, bpf=bpf - ) + cmd = 'bridge add_packet_filter {bridge} {name} mark "{bpf}"'.format(bridge=bridge_name, name=name, bpf=bpf) if tag is not None: cmd += f" tag {tag}" # Per-link attribution (contract §3.2): when one ubridge bridge serves @@ -1353,7 +1341,7 @@ class BaseNode: """ from gns3server.compute.marker.marker_manager import MarkerManager - markers = nio.markers if hasattr(nio, 'markers') else {} + markers = nio.markers if hasattr(nio, "markers") else {} manager = MarkerManager.instance() markers_dir = self.project.markers_working_directory() desired = {(name, spec.get("link_id", "")): spec for name, spec in markers.items()} @@ -1404,9 +1392,16 @@ class BaseNode: continue # unchanged pcap_path = os.path.join(markers_dir, f"{self._id}_{link_id}_{name}.pcap") try: - await self._ubridge_add_marker_filter(bridge_name, name, bpf, pcap_path, tag, link_id, - direction=spec.get("direction"), - data_link_type=spec.get("data_link_type")) + await self._ubridge_add_marker_filter( + bridge_name, + name, + bpf, + pcap_path, + tag, + link_id, + direction=spec.get("direction"), + data_link_type=spec.get("data_link_type"), + ) except UbridgeError as e: # Swallow BPF compile errors (warn + skip) so a single bad # expression can't break link creation / node restart — mirrors diff --git a/gns3server/compute/builtin/__init__.py b/gns3server/compute/builtin/__init__.py index 84e093951..94b20748b 100644 --- a/gns3server/compute/builtin/__init__.py +++ b/gns3server/compute/builtin/__init__.py @@ -18,7 +18,6 @@ Builtin nodes server module. """ - from ..base_manager import BaseManager from .builtin_node_factory import BuiltinNodeFactory, BUILTIN_NODES @@ -28,7 +27,6 @@ log = logging.getLogger(__name__) class Builtin(BaseManager): - _NODE_CLASS = BuiltinNodeFactory def __init__(self): diff --git a/gns3server/compute/builtin/builtin_node_factory.py b/gns3server/compute/builtin/builtin_node_factory.py index a078274c2..eafa1d544 100644 --- a/gns3server/compute/builtin/builtin_node_factory.py +++ b/gns3server/compute/builtin/builtin_node_factory.py @@ -29,7 +29,6 @@ BUILTIN_NODES = {"cloud": Cloud, "nat": Nat, "ethernet_hub": EthernetHub, "ether class BuiltinNodeFactory: - """ Factory to create an builtin object based on the node type. """ diff --git a/gns3server/compute/builtin/nodes/cloud.py b/gns3server/compute/builtin/nodes/cloud.py index b01f2b03d..173604582 100644 --- a/gns3server/compute/builtin/nodes/cloud.py +++ b/gns3server/compute/builtin/nodes/cloud.py @@ -31,7 +31,6 @@ log = logging.getLogger(__name__) class Cloud(BaseNode): - """ Cloud. @@ -317,7 +316,6 @@ class Cloud(BaseNode): await self._ubridge_apply_filters(bridge_name, nio.filters) await self._ubridge_apply_markers(bridge_name, nio) if port_info["type"] in ("ethernet", "tap"): - if not self.manager.has_privileged_access(self.ubridge_path): raise NodeError( "uBridge requires root access or the capability to interact with Ethernet and TAP adapters" @@ -339,9 +337,7 @@ class Cloud(BaseNode): elif port_info["type"] == "tap": await self._ubridge_send( - 'bridge add_nio_tap {name} "{interface}"'.format( - name=bridge_name, interface=port_info["interface"] - ) + 'bridge add_nio_tap {name} "{interface}"'.format(name=bridge_name, interface=port_info["interface"]) ) elif port_info["type"] == "udp": @@ -367,7 +363,6 @@ class Cloud(BaseNode): interface = port_info["interface"] if gns3server.utils.interfaces.is_interface_bridge(interface): - network_interfaces = [interface["name"] for interface in self._interfaces()] i = 0 while True: diff --git a/gns3server/compute/builtin/nodes/ethernet_hub.py b/gns3server/compute/builtin/nodes/ethernet_hub.py index 4ef0e27e9..0edc82ae5 100644 --- a/gns3server/compute/builtin/nodes/ethernet_hub.py +++ b/gns3server/compute/builtin/nodes/ethernet_hub.py @@ -24,7 +24,6 @@ log = logging.getLogger(__name__) class EthernetHub(BaseNode): - """ Ethernet hub @@ -40,12 +39,7 @@ class EthernetHub(BaseNode): def asdict(self): - return { - "name": self.name, - "usage": self.usage, - "node_id": self.id, - "project_id": self.project.id - } + return {"name": self.name, "usage": self.usage, "node_id": self.id, "project_id": self.project.id} async def create(self): """ diff --git a/gns3server/compute/builtin/nodes/ethernet_switch.py b/gns3server/compute/builtin/nodes/ethernet_switch.py index 5c66cbad5..153cb0af6 100644 --- a/gns3server/compute/builtin/nodes/ethernet_switch.py +++ b/gns3server/compute/builtin/nodes/ethernet_switch.py @@ -55,7 +55,6 @@ _QINQ_ETHERTYPE = "0x88a8" class EthernetSwitch(BaseNode): - """ Ethernet switch. @@ -74,8 +73,8 @@ class EthernetSwitch(BaseNode): self._ubridge_require_privileged_access = True self._nios = {} - self._tap_by_port = {} # port_number -> kernel TAP enslaved to the bridge - self._bridge_name = None # kernel bridge interface name (allocated on start) + self._tap_by_port = {} # port_number -> kernel TAP enslaved to the bridge + self._bridge_name = None # kernel bridge interface name (allocated on start) self._bridge_created = False self._bridge_proto_set = False # whether ``brctl setvlanproto`` has been applied # Idempotency flag for start(). Decoupled from ``status`` so the node can diff --git a/gns3server/compute/builtin/nodes/nat.py b/gns3server/compute/builtin/nodes/nat.py index a3b9b69d4..4a75d9a48 100644 --- a/gns3server/compute/builtin/nodes/nat.py +++ b/gns3server/compute/builtin/nodes/nat.py @@ -38,14 +38,16 @@ class Nat(Cloud): allowed_interfaces = Config.instance().settings.Server.allowed_interfaces if allowed_interfaces and isinstance(allowed_interfaces, str): - allowed_interfaces = allowed_interfaces.split(',') + allowed_interfaces = allowed_interfaces.split(",") if sys.platform.startswith("linux"): nat_interface = Config.instance().settings.Server.default_nat_interface if not nat_interface: nat_interface = "virbr0" if allowed_interfaces and nat_interface not in allowed_interfaces: - raise NodeError("NAT interface {} is not allowed be used on this server. " - "Please check the server configuration file.".format(nat_interface)) + raise NodeError( + "NAT interface {} is not allowed be used on this server. " + "Please check the server configuration file.".format(nat_interface) + ) if nat_interface not in [interface["name"] for interface in gns3server.utils.interfaces.interfaces()]: raise NodeError(f"NAT interface {nat_interface} is missing, please install libvirt") interface = nat_interface @@ -54,8 +56,10 @@ class Nat(Cloud): if not nat_interface: nat_interface = "vmnet8" if allowed_interfaces and nat_interface not in allowed_interfaces: - raise NodeError("NAT interface {} is not allowed be used on this server. " - "Please check the server configuration file.".format(nat_interface)) + raise NodeError( + "NAT interface {} is not allowed be used on this server. " + "Please check the server configuration file.".format(nat_interface) + ) interfaces = list( filter( lambda x: nat_interface in x.lower(), diff --git a/gns3server/compute/docker/__init__.py b/gns3server/compute/docker/__init__.py index 39ca9f20a..a6b1d4dae 100644 --- a/gns3server/compute/docker/__init__.py +++ b/gns3server/compute/docker/__init__.py @@ -34,7 +34,12 @@ from gns3server.compute.base_manager import BaseManager from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM from gns3server.compute.docker.iol_docker_vm import IOLDockerVM -from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error, DockerHttp409Error +from gns3server.compute.docker.docker_error import ( + DockerError, + DockerHttp304Error, + DockerHttp404Error, + DockerHttp409Error, +) log = logging.getLogger(__name__) @@ -47,7 +52,6 @@ CHUNK_SIZE = 1024 * 8 # 8KB class Docker(BaseManager): - _NODE_CLASS = DockerVM def __init__(self): @@ -97,10 +101,7 @@ class Docker(BaseManager): # check that busybox is statically linked # (dynamically linked busybox will fail to run in a container) proc = await asyncio.create_subprocess_exec( - "ldd", - busybox_path, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.DEVNULL + "ldd", busybox_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL ) stdout, _ = await proc.communicate() if proc.returncode == 1 or "static" in busybox_exec: @@ -111,11 +112,15 @@ class Docker(BaseManager): shutil.copy2(busybox_path, dst_busybox, follow_symlinks=True) return else: - log.warning(f"Busybox '{busybox_path}' is dynamically linked\n" - f"{stdout.decode('utf-8', errors='ignore').strip()}") + log.warning( + f"Busybox '{busybox_path}' is dynamically linked\n" + f"{stdout.decode('utf-8', errors='ignore').strip()}" + ) except OSError as e: raise DockerError(f"Could not install busybox: {e}") - raise DockerError("No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH") + raise DockerError( + "No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH" + ) @staticmethod def resources_path(): @@ -142,6 +147,7 @@ class Docker(BaseManager): dst_path = self.resources_path() log.info(f"Installing Docker resources in '{dst_path}'") from gns3server.controller import Controller + await Controller.instance().install_resource_files(dst_path, "compute/docker/resources") await self.install_busybox(dst_path) except OSError as e: @@ -157,12 +163,12 @@ class Docker(BaseManager): self._connected = False raise DockerError("Can't connect to Docker daemon") - api_version = parse_version(docker_info['ApiVersion']) + api_version = parse_version(docker_info["ApiVersion"]) version = docker_info["Version"] if api_version < parse_version(DOCKER_MINIMUM_API_VERSION): - raise DockerError(f"Docker version is {version}. " - f"GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}" + raise DockerError( + f"Docker version is {version}. GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}" ) preferred_api_version = parse_version(DOCKER_PREFERRED_API_VERSION) @@ -170,7 +176,7 @@ class Docker(BaseManager): self._api_version = DOCKER_PREFERRED_API_VERSION else: # use the Min API version supported by the daemon - self._api_version = docker_info['MinAPIVersion'] + self._api_version = docker_info["MinAPIVersion"] log.warning("Using Docker client with the minimum API version {}".format(self._api_version)) log.info("Connected to Docker daemon version {} using API version {}".format(version, self._api_version)) @@ -264,7 +270,7 @@ class Docker(BaseManager): response = await self.http_query(method, path, data=data, params=params) body = await response.read() response.close() - if response.headers.get('CONTENT-TYPE') == 'application/json': + if response.headers.get("CONTENT-TYPE") == "application/json": body = json.loads(body.decode("utf-8", errors="ignore")) else: body = body.decode("utf-8", errors="ignore") @@ -292,7 +298,7 @@ class Docker(BaseManager): if timeout is None: timeout = 60 * 60 * 24 * 31 # One month timeout - if path == 'version': + if path == "version": url = "http://docker/" + path else: await self._check_connection() # version is use by check connection @@ -380,9 +386,7 @@ class Docker(BaseManager): f"Disconnected while pulling Docker image '{image}' from Docker repository" ) from e except asyncio.TimeoutError as e: - raise DockerError( - f"Timeout while pulling Docker image '{image}' from Docker repository" - ) from e + raise DockerError(f"Timeout while pulling Docker image '{image}' from Docker repository") from e if not chunk: break content += chunk.decode("utf-8") diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index bfcc53a7e..7d557dff2 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -236,7 +236,9 @@ class DockerVM(BaseNode): """ if not is_rfc1123_hostname_valid(new_name): - raise DockerError(f"'{new_name}' is an invalid name to rename Docker container '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name cannot start or end with a hyphen.") + raise DockerError( + f"'{new_name}' is an invalid name to rename Docker container '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name cannot start or end with a hyphen." + ) super(DockerVM, DockerVM).name.__set__(self, new_name) @property @@ -275,10 +277,10 @@ class DockerVM(BaseNode): else: self._mac_address = mac_address - log.debug('Docker container "{name}" [{id}]: MAC address changed to {mac_addr}'.format( - name=self._name, - id=self._id, - mac_addr=self._mac_address) + log.debug( + 'Docker container "{name}" [{id}]: MAC address changed to {mac_addr}'.format( + name=self._name, id=self._id, mac_addr=self._mac_address + ) ) @property @@ -453,12 +455,7 @@ class DockerVM(BaseNode): raise DockerError(f"Cannot access resources: {e}") log.debug(f'Mount resources from "{resources_path}"') - binds = [{ - "Type": "bind", - "Source": resources_path, - "Target": "/gns3", - "ReadOnly": True - }] + binds = [{"Type": "bind", "Source": resources_path, "Target": "/gns3", "ReadOnly": True}] self._image_id = image_info.get("Id") or self._image_id # We mount our own etc/network @@ -471,11 +468,7 @@ class DockerVM(BaseNode): for volume in self._volumes: source = os.path.join(self.working_dir, os.path.relpath(volume, "/")) os.makedirs(source, exist_ok=True) - binds.append({ - "Type": "bind", - "Source": source, - "Target": "/gns3volumes{}".format(volume) - }) + binds.append({"Type": "bind", "Source": source, "Target": "/gns3volumes{}".format(volume)}) # Inject extra config files: write each to the node working directory and # bind-mount it read-only at its target path. Single-file binds are applied @@ -485,9 +478,7 @@ class DockerVM(BaseNode): target = cfg["target"] if isinstance(cfg, dict) else cfg.target content = cfg["content"] if isinstance(cfg, dict) else cfg.content if not target.startswith("/") or target.endswith("/") or ".." in target.split("/"): - raise DockerError( - f"Extra config target '{target}' must be an absolute file path and not contain '..'." - ) + raise DockerError(f"Extra config target '{target}' must be an absolute file path and not contain '..'.") for volume in self._volumes: # A single-file bind gets covered by the volume's bind mount at # start (init.sh or the vendor volume bridge), so the injected @@ -497,18 +488,22 @@ class DockerVM(BaseNode): log.warning( "Extra config target '%s' on container '%s' is shadowed by persisted volume '%s' " "and will not take effect; pick a target outside persisted volumes.", - target, self._name, volume, + target, + self._name, + volume, ) host_path = os.path.join(self.working_dir, "configs", target.lstrip("/")) os.makedirs(os.path.dirname(host_path), exist_ok=True) with open(host_path, "w") as f: f.write(content) - binds.append({ - "Type": "bind", - "Source": host_path, - "Target": target, - "ReadOnly": True, - }) + binds.append( + { + "Type": "bind", + "Source": host_path, + "Target": target, + "ReadOnly": True, + } + ) return binds @@ -554,7 +549,8 @@ class DockerVM(BaseNode): #auto eth{adapter} #iface eth{adapter} inet dhcp #\thostname {hostname} -""".format(adapter=adapter, hostname=self._name)) +""".format(adapter=adapter, hostname=self._name) + ) return path def _prepare_init_and_interface_env(self, params): @@ -573,9 +569,11 @@ class DockerVM(BaseNode): """ if ":" in os.path.splitdrive(self.working_dir)[1]: - raise DockerError("Cannot create a Docker container with a project directory containing a colon character (':')") + raise DockerError( + "Cannot create a Docker container with a project directory containing a colon character (':')" + ) - #await self.manager.install_resources() + # await self.manager.install_resources() try: image_infos = await self._get_image_information() @@ -601,8 +599,7 @@ class DockerVM(BaseNode): available_cpus = psutil.cpu_count(logical=True) if self._cpus > available_cpus: raise DockerError( - f"You have allocated too many CPUs for the Docker container " - f"(max available is {available_cpus} CPUs)" + f"You have allocated too many CPUs for the Docker container (max available is {available_cpus} CPUs)" ) # Prepare persistent volume content before the container and its @@ -622,7 +619,7 @@ class DockerVM(BaseNode): "Mounts": self._mount_binds(image_infos), "Memory": self._memory * (1024 * 1024), # convert memory to bytes "NanoCpus": int(self._cpus * 1e9), # convert cpus to nano cpus - "UsernsMode": "host" + "UsernsMode": "host", }, "Volumes": {}, "Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573 @@ -651,8 +648,11 @@ class DockerVM(BaseNode): devices = self._format_devices(line.split("=", 1)[1]) if devices: params["HostConfig"]["Devices"] = devices - elif line.startswith("GNS3_MASK_UDEV=") and \ - line.split("=", 1)[1].strip().lower() in ("1", "true", "yes"): + elif line.startswith("GNS3_MASK_UDEV=") and line.split("=", 1)[1].strip().lower() in ( + "1", + "true", + "yes", + ): # A privileged systemd-based NOS container (e.g. Cisco XRd) # runs systemd-udevd, which coldplugs every device it can see # -- and in privileged mode that includes the HOST's USB/input/ @@ -660,24 +660,28 @@ class DockerVM(BaseNode): # XRd doesn't need udev (interfaces are pre-created by GNS3), so # bind /dev/null over the udev units to keep it from running. for target in [f"/etc/systemd/system/{u}" for u in self._UDEV_UNITS] + list(self._UDEVADM_PATHS): - params["HostConfig"]["Mounts"].append({ - "Type": "bind", - "Source": "/dev/null", - "Target": target, - "ReadOnly": True, - }) + params["HostConfig"]["Mounts"].append( + { + "Type": "bind", + "Source": "/dev/null", + "Target": target, + "ReadOnly": True, + } + ) elif line.startswith("GNS3_MASK_SYSTEMD="): # Generic form: comma/semicolon-separated unit names to mask # the same way (bind /dev/null over /etc/systemd/system/). for unit in line.split("=", 1)[1].replace(";", ",").split(","): unit = unit.strip() if unit and "/" not in unit and ".." not in unit: - params["HostConfig"]["Mounts"].append({ - "Type": "bind", - "Source": "/dev/null", - "Target": f"/etc/systemd/system/{unit}", - "ReadOnly": True, - }) + params["HostConfig"]["Mounts"].append( + { + "Type": "bind", + "Source": "/dev/null", + "Target": f"/etc/systemd/system/{unit}", + "ReadOnly": True, + } + ) # Overlapping bind targets (GNS3_MASK_UDEV together with a # GNS3_MASK_SYSTEMD entry for the same unit, an extra_configs target @@ -754,12 +758,14 @@ class DockerVM(BaseNode): "QT_GRAPHICSSYSTEM=native" ) # To fix a Qt issue: https://github.com/GNS3/gns3-server/issues/556 params["Env"].append(f"DISPLAY=:{self._display}") - params["HostConfig"]["Mounts"].append({ - "Type": "bind", - "Source": f"/tmp/.X11-unix/X{self._display}", - "Target": f"/tmp/.X11-unix/X{self._display}", - "ReadOnly": True - }) + params["HostConfig"]["Mounts"].append( + { + "Type": "bind", + "Source": f"/tmp/.X11-unix/X{self._display}", + "Target": f"/tmp/.X11-unix/X{self._display}", + "ReadOnly": True, + } + ) if self._extra_hosts: extra_hosts = self._format_extra_hosts(self._extra_hosts) @@ -772,7 +778,9 @@ class DockerVM(BaseNode): except DockerHttp409Error: # Container name already exists. This can happen when the server crashes # and leaves containers behind. Try to remove the conflicting container. - log.warning(f"Container name '{self.docker_name}' is already in use, attempting to clean up the stale container...") + log.warning( + f"Container name '{self.docker_name}' is already in use, attempting to clean up the stale container..." + ) try: # Try to get and remove the conflicting container try: @@ -850,11 +858,13 @@ class DockerVM(BaseNode): on_host, in_container, permissions = parts else: continue - formatted.append({ - "PathOnHost": on_host, - "PathInContainer": in_container, - "CgroupPermissions": permissions, - }) + formatted.append( + { + "PathOnHost": on_host, + "PathInContainer": in_container, + "CgroupPermissions": permissions, + } + ) return formatted async def update(self): @@ -897,7 +907,6 @@ class DockerVM(BaseNode): await self._start_interface_monitor() return else: - if self._console_type == "vnc" and not self._vnc_process: # restart the vnc process in case it had previously crashed await self._start_vnc_process(restart=True) @@ -1042,7 +1051,9 @@ class DockerVM(BaseNode): stderr = (await process.stderr.read()).decode(errors="replace").strip() log.error( "Failed to fix permissions on '%s' for container '%s': %s", - volume, self._name, stderr or f"exit code {process.returncode}" + volume, + self._name, + stderr or f"exit code {process.returncode}", ) else: self._permissions_fixed = True @@ -1102,7 +1113,9 @@ class DockerVM(BaseNode): log.warning(f"Docker container '{self._name}': cannot access resources to reclaim '{directory}': {e}") return False - log.info(f"Docker container '{self._name}': reclaiming root-owned files under '{directory}' via a one-shot container") + log.info( + f"Docker container '{self._name}': reclaiming root-owned files under '{directory}' via a one-shot container" + ) # Prefer the image's own chown over the static busybox one: busybox's # chown dlopens NSS modules from the image, which mismatch the static # glibc and abort on NOS images whose glibc differs (same reasoning @@ -1116,12 +1129,24 @@ class DockerVM(BaseNode): image_ref = self._image_id or self._image try: process = await asyncio.subprocess.create_subprocess_exec( - "docker", "run", "--rm", "--network", "none", "--pull", "never", "--user", "0:0", - "--entrypoint", "/gns3/bin/busybox", - "-v", f"{resources_path}:/gns3:ro", - "-v", f"{directory}:/target", + "docker", + "run", + "--rm", + "--network", + "none", + "--pull", + "never", + "--user", + "0:0", + "--entrypoint", + "/gns3/bin/busybox", + "-v", + f"{resources_path}:/gns3:ro", + "-v", + f"{directory}:/target", image_ref, - "sh", "-c", + "sh", + "-c", "/gns3/bin/busybox chmod -R u+rwX /target" f" && ( command -v chown >/dev/null 2>&1 && chown {uid}:{gid} -R /target" f" || /gns3/bin/busybox chown {uid}:{gid} -R /target )", @@ -1159,17 +1184,27 @@ class DockerVM(BaseNode): if tigervnc_path: with open(os.path.join(self.working_dir, "vnc.log"), "w") as fd: - self._vnc_process = await asyncio.create_subprocess_exec(tigervnc_path, - "-extension", "MIT-SHM", - "-geometry", self._console_resolution, - "-depth", "16", - "-interface", self._manager.port_manager.console_host, - "-rfbport", str(self.console), - "-AlwaysShared", - "-SecurityTypes", "None", - "-desktop", self.name, - ":{}".format(self._display), - stdout=fd, stderr=subprocess.STDOUT) + self._vnc_process = await asyncio.create_subprocess_exec( + tigervnc_path, + "-extension", + "MIT-SHM", + "-geometry", + self._console_resolution, + "-depth", + "16", + "-interface", + self._manager.port_manager.console_host, + "-rfbport", + str(self.console), + "-AlwaysShared", + "-SecurityTypes", + "None", + "-desktop", + self.name, + ":{}".format(self._display), + stdout=fd, + stderr=subprocess.STDOUT, + ) async def _start_vnc(self): """ @@ -1576,8 +1611,8 @@ class DockerVM(BaseNode): script = ( "while :; do " "for ifname do " - "flags=$(/gns3/bin/busybox cat \"/sys/class/net/$ifname/flags\" 2>/dev/null) || continue; " - "/gns3/bin/busybox printf '%s=%s\\n' \"$ifname\" \"$flags\"; " + 'flags=$(/gns3/bin/busybox cat "/sys/class/net/$ifname/flags" 2>/dev/null) || continue; ' + '/gns3/bin/busybox printf \'%s=%s\\n\' "$ifname" "$flags"; ' "done; " "/gns3/bin/busybox sleep 1; " "done" @@ -1621,7 +1656,7 @@ class DockerVM(BaseNode): asyncio.IncompleteReadError, asyncio.TimeoutError, ) as e: - if 'writer' in locals(): + if "writer" in locals(): await self._close_interface_monitor_writer(writer) log.warning("Could not monitor interfaces for Docker container '%s': %s", self.name, e) return @@ -1658,8 +1693,7 @@ class DockerVM(BaseNode): """Read ``ifname=flags`` records and emit changed adapter states.""" interfaces = { - self._get_container_ifname(adapter_number): adapter_number - for adapter_number in range(self.adapters) + self._get_container_ifname(adapter_number): adapter_number for adapter_number in range(self.adapters) } try: while True: @@ -1747,9 +1781,7 @@ class DockerVM(BaseNode): await self._ubridge_send(f"bridge create {bridge_name}") self._bridges.add(bridge_name) await self._ubridge_send( - "bridge add_nio_tap {bridge_name} {hostif} off".format( - bridge_name=bridge_name, hostif=adapter.host_ifc - ) + "bridge add_nio_tap {bridge_name} {hostif} off".format(bridge_name=bridge_name, hostif=adapter.host_ifc) ) mac_address = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number) @@ -1759,17 +1791,14 @@ class DockerVM(BaseNode): mac_address = custom_mac_address try: - await self._ubridge_send('docker set_mac_addr {ifc} {mac}'.format(ifc=adapter.host_ifc, mac=mac_address)) + await self._ubridge_send("docker set_mac_addr {ifc} {mac}".format(ifc=adapter.host_ifc, mac=mac_address)) except UbridgeError: log.warning(f"Could not set MAC address {mac_address} on interface {adapter.host_ifc}") - ifname = self._get_container_ifname(adapter_number) log.debug(f"Move container {self.name} adapter {adapter.host_ifc} -> {ifname} in ns {self._namespace}") try: - await self._ubridge_send( - f"docker move_to_ns {adapter.host_ifc} {self._namespace} {ifname}" - ) + await self._ubridge_send(f"docker move_to_ns {adapter.host_ifc} {self._namespace} {ifname}") except UbridgeError as e: raise UbridgeNamespaceError(e) else: @@ -2061,6 +2090,6 @@ class DockerVM(BaseNode): raise ComputeError( f"Could not delete the node directory '{self.working_dir}': files left owned by " f"another user could not be reclaimed ({e}). Reclaim them manually with: " - f"docker run --rm --user 0:0 -v \"{self.working_dir}\":/target --entrypoint /bin/sh " + f'docker run --rm --user 0:0 -v "{self.working_dir}":/target --entrypoint /bin/sh ' f"{self._image} -c 'chown -R {os.getuid()}:{os.getgid()} /target'" ) diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index 38042de6d..ff0ab2b00 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -247,7 +247,8 @@ class IOLDockerVM(VendorDockerVM): log.debug( "IOL container '%s': number of 4-port Ethernet adapters set to %d", - self._name, adapters, + self._name, + adapters, ) def _persistent_volume_list(self, image_info, include_network_config=True): diff --git a/gns3server/compute/docker/vendor_docker_vm.py b/gns3server/compute/docker/vendor_docker_vm.py index b156209ae..1adb5a951 100644 --- a/gns3server/compute/docker/vendor_docker_vm.py +++ b/gns3server/compute/docker/vendor_docker_vm.py @@ -113,9 +113,7 @@ class VendorDockerVM(DockerVM): if _line.startswith("GNS3_SKIP_INIT="): self._gns3_init = _line.split("=", 1)[1].strip().lower() not in ("1", "true", "yes") elif _line.startswith("GNS3_INTERFACE_NAMES="): - self._interface_names = [ - n.strip() for n in _line.split("=", 1)[1].split(",") if n.strip() - ] + self._interface_names = [n.strip() for n in _line.split("=", 1)[1].split(",") if n.strip()] elif _line.startswith("GNS3_CONSOLE_CMD="): self._console_cmd = _line.split("=", 1)[1].strip() elif _line.startswith("GNS3_CONSOLE_RESIZE="): @@ -172,19 +170,20 @@ class VendorDockerVM(DockerVM): if self._unix_socket_nio: socket_dir = self._unix_socket_dir.rstrip("/") if not any( - v.rstrip("/") == socket_dir or socket_dir.startswith(v.rstrip("/") + "/") - for v in self._volumes + v.rstrip("/") == socket_dir or socket_dir.startswith(v.rstrip("/") + "/") for v in self._volumes ): # The image's network agent drops privileges before using the # socket directory, so it must be writable by the server user: # bind a per-node directory from the runtime directory (see # _unix_socket_host_dir). - binds.append({ - "Type": "bind", - "Source": self._unix_socket_host_dir(), - "Target": socket_dir, - "BindOptions": {"Propagation": "rprivate"}, - }) + binds.append( + { + "Type": "bind", + "Source": self._unix_socket_host_dir(), + "Target": socket_dir, + "BindOptions": {"Propagation": "rprivate"}, + } + ) if self._gns3_init: return binds binds = [b for b in binds if b.get("Target") != "/gns3volumes/etc/network"] @@ -198,7 +197,7 @@ class VendorDockerVM(DockerVM): for bind in binds: target = bind.get("Target", "") if target.startswith("/gns3volumes"): - volume = target[len("/gns3volumes"):] + volume = target[len("/gns3volumes") :] if volume in self._volumes: bind = {**bind, "Target": volume} retargeted.append(bind) @@ -249,8 +248,11 @@ class VendorDockerVM(DockerVM): try: process = await asyncio.subprocess.create_subprocess_exec( - "docker", "create", self._image, - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + "docker", + "create", + self._image, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, ) except OSError as e: raise DockerError(f"Could not seed persistent volumes for '{self._name}': {e}") @@ -271,8 +273,13 @@ class VendorDockerVM(DockerVM): try: process = await asyncio.subprocess.create_subprocess_exec( - "docker", "cp", "-a", f"{seed_cid}:{volume}/.", host_dir + "/", - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + "docker", + "cp", + "-a", + f"{seed_cid}:{volume}/.", + host_dir + "/", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, ) except OSError as e: raise DockerError(f"Could not seed persistent volume '{volume}' for '{self._name}': {e}") @@ -283,7 +290,10 @@ class VendorDockerVM(DockerVM): # init.sh's first copy (cp -a ... 2>/dev/null). log.info( "Persistent volume '%s' on '%s' not seedable from image '%s' (%s); starting empty", - volume, self._name, self._image, stderr.decode(errors="replace").strip(), + volume, + self._name, + self._image, + stderr.decode(errors="replace").strip(), ) return log.info("Seeded persistent volume '%s' for '%s' from image '%s'", volume, self._name, self._image) @@ -295,8 +305,12 @@ class VendorDockerVM(DockerVM): try: process = await asyncio.subprocess.create_subprocess_exec( - "docker", "rm", "-f", seed_cid, - stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + "docker", + "rm", + "-f", + seed_cid, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, ) except OSError: return @@ -348,9 +362,7 @@ class VendorDockerVM(DockerVM): try: os.makedirs(host_dir, mode=0o700, exist_ok=True) except OSError as e: - raise DockerError( - f"Could not create unix-socket directory '{host_dir}' for container '{self._name}': {e}" - ) + raise DockerError(f"Could not create unix-socket directory '{host_dir}' for container '{self._name}': {e}") return host_dir def _remove_unix_socket_host_dir(self): @@ -448,7 +460,11 @@ class VendorDockerVM(DockerVM): adapter.host_ifc = local_sock # bookkeeping / removal logging only log.debug( "Adapter %d port %d of container '%s' wired via unix sockets %s <-> %s", - adapter_number, port_number, self._name, local_sock, remote_sock, + adapter_number, + port_number, + self._name, + local_sock, + remote_sock, ) if nio: @@ -564,7 +580,8 @@ class VendorDockerVM(DockerVM): if state == "stopped" or state == "exited": log.info( "Container '%s' is %s, skipping permission fix (next start will fix)", - self._name, state, + self._name, + state, ) return @@ -601,7 +618,9 @@ class VendorDockerVM(DockerVM): stderr = (await process.stderr.read()).decode(errors="replace").strip() log.error( "Failed to fix permissions on '%s' for container '%s': %s", - volume, self._name, stderr or f"exit code {process.returncode}", + volume, + self._name, + stderr or f"exit code {process.returncode}", ) else: self._permissions_fixed = True @@ -640,9 +659,7 @@ class VendorDockerVM(DockerVM): allow_resize=self._console_resize, ) try: - self._telnet_servers.append( - await telnet.start(self._manager.port_manager.console_host, self.console) - ) + self._telnet_servers.append(await telnet.start(self._manager.port_manager.console_host, self.console)) except OSError as e: raise DockerError( f"Could not start console server on socket {self._manager.port_manager.console_host}:{self.console}: {e}" diff --git a/gns3server/compute/dynamips/__init__.py b/gns3server/compute/dynamips/__init__.py index 134ce83e0..cbedaa6ad 100644 --- a/gns3server/compute/dynamips/__init__.py +++ b/gns3server/compute/dynamips/__init__.py @@ -110,7 +110,6 @@ PLATFORMS_DEFAULT_RAM = { class Dynamips(BaseManager): - _NODE_CLASS = DynamipsFactory _NODE_TYPE = "dynamips" _ghost_ios_lock = None @@ -253,7 +252,9 @@ class Dynamips(BaseManager): dynamips_path = self.config.settings.Dynamips.dynamips_path if not os.path.isabs(dynamips_path): if sys.platform.startswith("win") and hasattr(sys, "frozen"): - dynamips_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "dynamips")) + dynamips_dir = os.path.normpath( + os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "dynamips") + ) os.environ["PATH"] = os.pathsep.join(dynamips_dir) + os.pathsep + os.environ.get("PATH", "") dynamips_path = shutil.which(dynamips_path) @@ -305,14 +306,14 @@ class Dynamips(BaseManager): bind_console_host = False dynamips_version = await self.dynamips_version(self.dynamips_path) - if parse_version(dynamips_version) < parse_version('0.2.11'): + if parse_version(dynamips_version) < parse_version("0.2.11"): raise DynamipsError("Dynamips version must be >= 0.2.11, detected version is {}".format(dynamips_version)) if not sys.platform.startswith("win"): # Hypervisor should always listen to 127.0.0.1 # See https://github.com/GNS3/dynamips/issues/62 # This was fixed in Dynamips v0.2.23 which hasn't been built for Windows - if parse_version(dynamips_version) >= parse_version('0.2.23'): + if parse_version(dynamips_version) >= parse_version("0.2.23"): server_host = "127.0.0.1" bind_console_host = True @@ -331,7 +332,9 @@ class Dynamips(BaseManager): raise DynamipsError(f"Could not find free port for the Dynamips hypervisor: {e}") port_manager = PortManager.instance() - hypervisor = Hypervisor(self._dynamips_path, working_dir, server_host, port, port_manager.console_host, bind_console_host) + hypervisor = Hypervisor( + self._dynamips_path, working_dir, server_host, port, port_manager.console_host, bind_console_host + ) log.debug(f"Creating new hypervisor {hypervisor.host}:{hypervisor.port} with working directory {working_dir}") await hypervisor.start() @@ -482,7 +485,9 @@ class Dynamips(BaseManager): if vm.slots[slot_id].removable(): await vm.slot_remove_binding(slot_id) else: - log.warning(f"Slot {slot_id} on router '{vm.name}' has a non-removable adapter, skipping replacement") + log.warning( + f"Slot {slot_id} on router '{vm.name}' has a non-removable adapter, skipping replacement" + ) continue if not isinstance(vm.slots[slot_id], type(adapter)): await vm.slot_add_binding(slot_id, adapter) @@ -500,7 +505,11 @@ class Dynamips(BaseManager): wic_name = value wic = WIC_MATRIX[wic_name]() try: - if vm.slots[0] and vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)): + if ( + vm.slots[0] + and vm.slots[0].wics[wic_slot_id] + and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)) + ): await vm.uninstall_wic(wic_slot_id) if vm.slots[0] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)): await vm.install_wic(wic_slot_id, wic) diff --git a/gns3server/compute/dynamips/adapters/adapter.py b/gns3server/compute/dynamips/adapters/adapter.py index 5126f0bb0..afa2a3e52 100644 --- a/gns3server/compute/dynamips/adapters/adapter.py +++ b/gns3server/compute/dynamips/adapters/adapter.py @@ -16,7 +16,6 @@ class Adapter: - """ Base class for adapters. diff --git a/gns3server/compute/dynamips/adapters/c1700_mb_1fe.py b/gns3server/compute/dynamips/adapters/c1700_mb_1fe.py index ef7d88c48..14048fe02 100644 --- a/gns3server/compute/dynamips/adapters/c1700_mb_1fe.py +++ b/gns3server/compute/dynamips/adapters/c1700_mb_1fe.py @@ -18,7 +18,6 @@ from .adapter import Adapter class C1700_MB_1FE(Adapter): - """ Integrated 1 port FastEthernet adapter for c1700 platform. """ diff --git a/gns3server/compute/dynamips/adapters/c1700_mb_wic1.py b/gns3server/compute/dynamips/adapters/c1700_mb_wic1.py index 170a44898..d5bd6443f 100644 --- a/gns3server/compute/dynamips/adapters/c1700_mb_wic1.py +++ b/gns3server/compute/dynamips/adapters/c1700_mb_wic1.py @@ -18,7 +18,6 @@ from .adapter import Adapter class C1700_MB_WIC1(Adapter): - """ Fake module to provide a placeholder for slot 1 interfaces when WICs are inserted into WIC slot 1. diff --git a/gns3server/compute/dynamips/adapters/c2600_mb_1e.py b/gns3server/compute/dynamips/adapters/c2600_mb_1e.py index be92cf3f5..25f6b690e 100644 --- a/gns3server/compute/dynamips/adapters/c2600_mb_1e.py +++ b/gns3server/compute/dynamips/adapters/c2600_mb_1e.py @@ -18,7 +18,6 @@ from .adapter import Adapter class C2600_MB_1E(Adapter): - """ Integrated 1 port Ethernet adapter for the c2600 platform. """ diff --git a/gns3server/compute/dynamips/adapters/c2600_mb_1fe.py b/gns3server/compute/dynamips/adapters/c2600_mb_1fe.py index 2c06dbdde..2974d6c0f 100644 --- a/gns3server/compute/dynamips/adapters/c2600_mb_1fe.py +++ b/gns3server/compute/dynamips/adapters/c2600_mb_1fe.py @@ -18,7 +18,6 @@ from .adapter import Adapter class C2600_MB_1FE(Adapter): - """ Integrated 1 port FastEthernet adapter for the c2600 platform. """ diff --git a/gns3server/compute/dynamips/adapters/c2600_mb_2e.py b/gns3server/compute/dynamips/adapters/c2600_mb_2e.py index 7798b07a8..858f952c1 100644 --- a/gns3server/compute/dynamips/adapters/c2600_mb_2e.py +++ b/gns3server/compute/dynamips/adapters/c2600_mb_2e.py @@ -18,7 +18,6 @@ from .adapter import Adapter class C2600_MB_2E(Adapter): - """ Integrated 2 port Ethernet adapter for the c2600 platform. """ diff --git a/gns3server/compute/dynamips/adapters/c2600_mb_2fe.py b/gns3server/compute/dynamips/adapters/c2600_mb_2fe.py index a3016f01d..724c2ff2f 100644 --- a/gns3server/compute/dynamips/adapters/c2600_mb_2fe.py +++ b/gns3server/compute/dynamips/adapters/c2600_mb_2fe.py @@ -18,7 +18,6 @@ from .adapter import Adapter class C2600_MB_2FE(Adapter): - """ Integrated 2 port FastEthernet adapter for the c2600 platform. """ diff --git a/gns3server/compute/dynamips/adapters/c7200_io_2fe.py b/gns3server/compute/dynamips/adapters/c7200_io_2fe.py index addde38cd..2715c1c2b 100644 --- a/gns3server/compute/dynamips/adapters/c7200_io_2fe.py +++ b/gns3server/compute/dynamips/adapters/c7200_io_2fe.py @@ -18,7 +18,6 @@ from .adapter import Adapter class C7200_IO_2FE(Adapter): - """ C7200-IO-2FE FastEthernet Input/Ouput controller. """ diff --git a/gns3server/compute/dynamips/adapters/c7200_io_fe.py b/gns3server/compute/dynamips/adapters/c7200_io_fe.py index 4d2c1b1ba..a570ab7e5 100644 --- a/gns3server/compute/dynamips/adapters/c7200_io_fe.py +++ b/gns3server/compute/dynamips/adapters/c7200_io_fe.py @@ -18,7 +18,6 @@ from .adapter import Adapter class C7200_IO_FE(Adapter): - """ C7200-IO-FE FastEthernet Input/Ouput controller. """ diff --git a/gns3server/compute/dynamips/adapters/c7200_io_ge_e.py b/gns3server/compute/dynamips/adapters/c7200_io_ge_e.py index 09f982ed6..50913f1c7 100644 --- a/gns3server/compute/dynamips/adapters/c7200_io_ge_e.py +++ b/gns3server/compute/dynamips/adapters/c7200_io_ge_e.py @@ -18,7 +18,6 @@ from .adapter import Adapter class C7200_IO_GE_E(Adapter): - """ C7200-IO-GE-E GigabitEthernet Input/Ouput controller. """ diff --git a/gns3server/compute/dynamips/adapters/leopard_2fe.py b/gns3server/compute/dynamips/adapters/leopard_2fe.py index 55d75c19f..83752b14e 100644 --- a/gns3server/compute/dynamips/adapters/leopard_2fe.py +++ b/gns3server/compute/dynamips/adapters/leopard_2fe.py @@ -18,7 +18,6 @@ from .adapter import Adapter class Leopard_2FE(Adapter): - """ Integrated 2 port FastEthernet adapter for c3660 router. """ diff --git a/gns3server/compute/dynamips/adapters/nm_16esw.py b/gns3server/compute/dynamips/adapters/nm_16esw.py index f346da8bd..d95b9a339 100644 --- a/gns3server/compute/dynamips/adapters/nm_16esw.py +++ b/gns3server/compute/dynamips/adapters/nm_16esw.py @@ -18,7 +18,6 @@ from .adapter import Adapter class NM_16ESW(Adapter): - """ NM-16ESW FastEthernet network module. """ diff --git a/gns3server/compute/dynamips/adapters/nm_1e.py b/gns3server/compute/dynamips/adapters/nm_1e.py index 919324743..f0b27c5a4 100644 --- a/gns3server/compute/dynamips/adapters/nm_1e.py +++ b/gns3server/compute/dynamips/adapters/nm_1e.py @@ -18,7 +18,6 @@ from .adapter import Adapter class NM_1E(Adapter): - """ NM-1E Ethernet network module. """ diff --git a/gns3server/compute/dynamips/adapters/nm_1fe_tx.py b/gns3server/compute/dynamips/adapters/nm_1fe_tx.py index bb03d3f30..f693b019f 100644 --- a/gns3server/compute/dynamips/adapters/nm_1fe_tx.py +++ b/gns3server/compute/dynamips/adapters/nm_1fe_tx.py @@ -18,7 +18,6 @@ from .adapter import Adapter class NM_1FE_TX(Adapter): - """ NM-1FE-TX FastEthernet network module. """ diff --git a/gns3server/compute/dynamips/adapters/nm_4e.py b/gns3server/compute/dynamips/adapters/nm_4e.py index 6a4db9f7c..2db29130b 100644 --- a/gns3server/compute/dynamips/adapters/nm_4e.py +++ b/gns3server/compute/dynamips/adapters/nm_4e.py @@ -18,7 +18,6 @@ from .adapter import Adapter class NM_4E(Adapter): - """ NM-4E Ethernet network module. """ diff --git a/gns3server/compute/dynamips/adapters/nm_4t.py b/gns3server/compute/dynamips/adapters/nm_4t.py index fa527c2ff..ddda0659d 100644 --- a/gns3server/compute/dynamips/adapters/nm_4t.py +++ b/gns3server/compute/dynamips/adapters/nm_4t.py @@ -18,7 +18,6 @@ from .adapter import Adapter class NM_4T(Adapter): - """ NM-4T Serial network module. """ diff --git a/gns3server/compute/dynamips/adapters/pa_2fe_tx.py b/gns3server/compute/dynamips/adapters/pa_2fe_tx.py index 361199996..c191172ac 100644 --- a/gns3server/compute/dynamips/adapters/pa_2fe_tx.py +++ b/gns3server/compute/dynamips/adapters/pa_2fe_tx.py @@ -18,7 +18,6 @@ from .adapter import Adapter class PA_2FE_TX(Adapter): - """ PA-2FE-TX FastEthernet port adapter. """ diff --git a/gns3server/compute/dynamips/adapters/pa_4e.py b/gns3server/compute/dynamips/adapters/pa_4e.py index 5f3288b76..551e4177e 100644 --- a/gns3server/compute/dynamips/adapters/pa_4e.py +++ b/gns3server/compute/dynamips/adapters/pa_4e.py @@ -18,7 +18,6 @@ from .adapter import Adapter class PA_4E(Adapter): - """ PA-4E Ethernet port adapter. """ diff --git a/gns3server/compute/dynamips/adapters/pa_4t.py b/gns3server/compute/dynamips/adapters/pa_4t.py index beae79654..68aee25a0 100644 --- a/gns3server/compute/dynamips/adapters/pa_4t.py +++ b/gns3server/compute/dynamips/adapters/pa_4t.py @@ -18,7 +18,6 @@ from .adapter import Adapter class PA_4T(Adapter): - """ PA-4T+ Serial port adapter. """ diff --git a/gns3server/compute/dynamips/adapters/pa_8e.py b/gns3server/compute/dynamips/adapters/pa_8e.py index b36173a23..f0db7c00b 100644 --- a/gns3server/compute/dynamips/adapters/pa_8e.py +++ b/gns3server/compute/dynamips/adapters/pa_8e.py @@ -18,7 +18,6 @@ from .adapter import Adapter class PA_8E(Adapter): - """ PA-8E Ethernet port adapter. """ diff --git a/gns3server/compute/dynamips/adapters/pa_8t.py b/gns3server/compute/dynamips/adapters/pa_8t.py index 81d307c87..1b4dddfd8 100644 --- a/gns3server/compute/dynamips/adapters/pa_8t.py +++ b/gns3server/compute/dynamips/adapters/pa_8t.py @@ -18,7 +18,6 @@ from .adapter import Adapter class PA_8T(Adapter): - """ PA-8T Serial port adapter. """ diff --git a/gns3server/compute/dynamips/adapters/pa_a1.py b/gns3server/compute/dynamips/adapters/pa_a1.py index b20efb700..9dcbf47a4 100644 --- a/gns3server/compute/dynamips/adapters/pa_a1.py +++ b/gns3server/compute/dynamips/adapters/pa_a1.py @@ -18,7 +18,6 @@ from .adapter import Adapter class PA_A1(Adapter): - """ PA-A1 ATM port adapter. """ diff --git a/gns3server/compute/dynamips/adapters/pa_fe_tx.py b/gns3server/compute/dynamips/adapters/pa_fe_tx.py index 3a5f3fdb6..0ce6d94cb 100644 --- a/gns3server/compute/dynamips/adapters/pa_fe_tx.py +++ b/gns3server/compute/dynamips/adapters/pa_fe_tx.py @@ -18,7 +18,6 @@ from .adapter import Adapter class PA_FE_TX(Adapter): - """ PA-FE-TX FastEthernet port adapter. """ diff --git a/gns3server/compute/dynamips/adapters/pa_ge.py b/gns3server/compute/dynamips/adapters/pa_ge.py index dde8d7ed0..e64007b83 100644 --- a/gns3server/compute/dynamips/adapters/pa_ge.py +++ b/gns3server/compute/dynamips/adapters/pa_ge.py @@ -18,7 +18,6 @@ from .adapter import Adapter class PA_GE(Adapter): - """ PA-GE GigabitEthernet port adapter. """ diff --git a/gns3server/compute/dynamips/adapters/pa_pos_oc3.py b/gns3server/compute/dynamips/adapters/pa_pos_oc3.py index d6b8487ca..9646d9250 100644 --- a/gns3server/compute/dynamips/adapters/pa_pos_oc3.py +++ b/gns3server/compute/dynamips/adapters/pa_pos_oc3.py @@ -18,7 +18,6 @@ from .adapter import Adapter class PA_POS_OC3(Adapter): - """ PA-POS-OC3 port adapter. """ diff --git a/gns3server/compute/dynamips/adapters/wic_1enet.py b/gns3server/compute/dynamips/adapters/wic_1enet.py index 1c8c98055..df099f674 100644 --- a/gns3server/compute/dynamips/adapters/wic_1enet.py +++ b/gns3server/compute/dynamips/adapters/wic_1enet.py @@ -16,7 +16,6 @@ class WIC_1ENET: - """ WIC-1ENET Ethernet """ diff --git a/gns3server/compute/dynamips/adapters/wic_1t.py b/gns3server/compute/dynamips/adapters/wic_1t.py index 95bc57d4b..9738525c9 100644 --- a/gns3server/compute/dynamips/adapters/wic_1t.py +++ b/gns3server/compute/dynamips/adapters/wic_1t.py @@ -16,7 +16,6 @@ class WIC_1T: - """ WIC-1T Serial """ diff --git a/gns3server/compute/dynamips/adapters/wic_2t.py b/gns3server/compute/dynamips/adapters/wic_2t.py index 2f32db654..cb88bc1f0 100644 --- a/gns3server/compute/dynamips/adapters/wic_2t.py +++ b/gns3server/compute/dynamips/adapters/wic_2t.py @@ -16,7 +16,6 @@ class WIC_2T: - """ WIC-2T Serial """ diff --git a/gns3server/compute/dynamips/dynamips_error.py b/gns3server/compute/dynamips/dynamips_error.py index 8f9140e09..cf7f82f2d 100644 --- a/gns3server/compute/dynamips/dynamips_error.py +++ b/gns3server/compute/dynamips/dynamips_error.py @@ -22,5 +22,4 @@ from ..error import NodeError class DynamipsError(NodeError): - pass diff --git a/gns3server/compute/dynamips/dynamips_factory.py b/gns3server/compute/dynamips/dynamips_factory.py index 35dbd2e86..cbafc4934 100644 --- a/gns3server/compute/dynamips/dynamips_factory.py +++ b/gns3server/compute/dynamips/dynamips_factory.py @@ -52,7 +52,6 @@ DEVICES = { class DynamipsFactory: - """ Factory to create an Router object based on the correct platform. """ diff --git a/gns3server/compute/dynamips/dynamips_hypervisor.py b/gns3server/compute/dynamips/dynamips_hypervisor.py index 997cfce60..514e75360 100644 --- a/gns3server/compute/dynamips/dynamips_hypervisor.py +++ b/gns3server/compute/dynamips/dynamips_hypervisor.py @@ -30,7 +30,6 @@ log = logging.getLogger(__name__) class DynamipsHypervisor: - """ Creates a new connection to a Dynamips server (also called hypervisor) @@ -90,7 +89,9 @@ class DynamipsHypervisor: if not connection_success: raise DynamipsError(f"Couldn't connect to hypervisor on {host}:{self._port} :{last_exception}") else: - log.debug(f"Connected to Dynamips hypervisor on {host}:{self._port} after {time.time() - begin:.4f} seconds") + log.debug( + f"Connected to Dynamips hypervisor on {host}:{self._port} after {time.time() - begin:.4f} seconds" + ) try: version = await self.send("hypervisor version") diff --git a/gns3server/compute/dynamips/hypervisor.py b/gns3server/compute/dynamips/hypervisor.py index 30d9a268b..403c38c7e 100644 --- a/gns3server/compute/dynamips/hypervisor.py +++ b/gns3server/compute/dynamips/hypervisor.py @@ -33,7 +33,6 @@ log = logging.getLogger(__name__) class Hypervisor(DynamipsHypervisor): - """ Hypervisor. diff --git a/gns3server/compute/dynamips/nios/nio.py b/gns3server/compute/dynamips/nios/nio.py index 5c5c9ec6d..512a10699 100644 --- a/gns3server/compute/dynamips/nios/nio.py +++ b/gns3server/compute/dynamips/nios/nio.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class NIO: - """ Base NIO class diff --git a/gns3server/compute/dynamips/nios/nio_generic_ethernet.py b/gns3server/compute/dynamips/nios/nio_generic_ethernet.py index cf2cc7509..1407c6694 100644 --- a/gns3server/compute/dynamips/nios/nio_generic_ethernet.py +++ b/gns3server/compute/dynamips/nios/nio_generic_ethernet.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class NIOGenericEthernet(NIO): - """ Dynamips generic Ethernet NIO. @@ -67,7 +66,4 @@ class NIOGenericEthernet(NIO): def asdict(self): - return { - "type": "nio_generic_ethernet", - "ethernet_device": self._ethernet_device - } + return {"type": "nio_generic_ethernet", "ethernet_device": self._ethernet_device} diff --git a/gns3server/compute/dynamips/nios/nio_linux_ethernet.py b/gns3server/compute/dynamips/nios/nio_linux_ethernet.py index 64f7e9385..c11039a5c 100644 --- a/gns3server/compute/dynamips/nios/nio_linux_ethernet.py +++ b/gns3server/compute/dynamips/nios/nio_linux_ethernet.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class NIOLinuxEthernet(NIO): - """ Dynamips Linux Ethernet NIO. @@ -66,7 +65,4 @@ class NIOLinuxEthernet(NIO): def asdict(self): - return { - "type": "nio_linux_ethernet", - "ethernet_device": self._ethernet_device - } + return {"type": "nio_linux_ethernet", "ethernet_device": self._ethernet_device} diff --git a/gns3server/compute/dynamips/nios/nio_null.py b/gns3server/compute/dynamips/nios/nio_null.py index 08144646c..1f56a12e8 100644 --- a/gns3server/compute/dynamips/nios/nio_null.py +++ b/gns3server/compute/dynamips/nios/nio_null.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class NIONull(NIO): - """ Dynamips NULL NIO. diff --git a/gns3server/compute/dynamips/nios/nio_tap.py b/gns3server/compute/dynamips/nios/nio_tap.py index 50efd5534..acd3a864e 100644 --- a/gns3server/compute/dynamips/nios/nio_tap.py +++ b/gns3server/compute/dynamips/nios/nio_tap.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class NIOTAP(NIO): - """ Dynamips TAP NIO. @@ -60,7 +59,4 @@ class NIOTAP(NIO): def asdict(self): - return { - "type": "nio_tap", - "tap_device": self._tap_device - } + return {"type": "nio_tap", "tap_device": self._tap_device} diff --git a/gns3server/compute/dynamips/nios/nio_udp.py b/gns3server/compute/dynamips/nios/nio_udp.py index 6a7590cbd..c811858d9 100644 --- a/gns3server/compute/dynamips/nios/nio_udp.py +++ b/gns3server/compute/dynamips/nios/nio_udp.py @@ -31,7 +31,6 @@ log = logging.getLogger(__name__) class NIOUDP(NIO): - """ Dynamips UDP NIO. @@ -130,9 +129,4 @@ class NIOUDP(NIO): def asdict(self): - return { - "type": "nio_udp", - "lport": self._lport, - "rport": self._rport, - "rhost": self._rhost - } + return {"type": "nio_udp", "lport": self._lport, "rport": self._rport, "rhost": self._rhost} diff --git a/gns3server/compute/dynamips/nios/nio_unix.py b/gns3server/compute/dynamips/nios/nio_unix.py index e6edaa4ba..bab9ee06e 100644 --- a/gns3server/compute/dynamips/nios/nio_unix.py +++ b/gns3server/compute/dynamips/nios/nio_unix.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class NIOUNIX(NIO): - """ Dynamips UNIX NIO. @@ -81,8 +80,4 @@ class NIOUNIX(NIO): def asdict(self): - return { - "type": "nio_unix", - "local_file": self._local_file, - "remote_file": self._remote_file - } + return {"type": "nio_unix", "local_file": self._local_file, "remote_file": self._remote_file} diff --git a/gns3server/compute/dynamips/nios/nio_vde.py b/gns3server/compute/dynamips/nios/nio_vde.py index d65687739..7218179ce 100644 --- a/gns3server/compute/dynamips/nios/nio_vde.py +++ b/gns3server/compute/dynamips/nios/nio_vde.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class NIOVDE(NIO): - """ Dynamips VDE NIO. @@ -81,8 +80,4 @@ class NIOVDE(NIO): def asdict(self): - return { - "type": "nio_vde", - "local_file": self._local_file, - "control_file": self._control_file - } + return {"type": "nio_vde", "local_file": self._local_file, "control_file": self._control_file} diff --git a/gns3server/compute/dynamips/nodes/atm_switch.py b/gns3server/compute/dynamips/nodes/atm_switch.py index c97b3ad35..38e9d4ecb 100644 --- a/gns3server/compute/dynamips/nodes/atm_switch.py +++ b/gns3server/compute/dynamips/nodes/atm_switch.py @@ -32,7 +32,6 @@ log = logging.getLogger(__name__) class ATMSwitch(Device): - """ Dynamips ATM switch. diff --git a/gns3server/compute/dynamips/nodes/bridge.py b/gns3server/compute/dynamips/nodes/bridge.py index 4b8bfcaf4..04c1651f8 100644 --- a/gns3server/compute/dynamips/nodes/bridge.py +++ b/gns3server/compute/dynamips/nodes/bridge.py @@ -24,7 +24,6 @@ from .device import Device class Bridge(Device): - """ Dynamips bridge. diff --git a/gns3server/compute/dynamips/nodes/c1700.py b/gns3server/compute/dynamips/nodes/c1700.py index 55b8dc4dc..b2d24b25b 100644 --- a/gns3server/compute/dynamips/nodes/c1700.py +++ b/gns3server/compute/dynamips/nodes/c1700.py @@ -30,7 +30,6 @@ log = logging.getLogger(__name__) class C1700(Router): - """ Dynamips c1700 router. diff --git a/gns3server/compute/dynamips/nodes/c2600.py b/gns3server/compute/dynamips/nodes/c2600.py index 8c39f43b2..774d67e54 100644 --- a/gns3server/compute/dynamips/nodes/c2600.py +++ b/gns3server/compute/dynamips/nodes/c2600.py @@ -32,7 +32,6 @@ log = logging.getLogger(__name__) class C2600(Router): - """ Dynamips c2600 router. diff --git a/gns3server/compute/dynamips/nodes/c2691.py b/gns3server/compute/dynamips/nodes/c2691.py index 3ef3f73ff..c28d1c161 100644 --- a/gns3server/compute/dynamips/nodes/c2691.py +++ b/gns3server/compute/dynamips/nodes/c2691.py @@ -30,7 +30,6 @@ log = logging.getLogger(__name__) class C2691(Router): - """ Dynamips c2691 router. diff --git a/gns3server/compute/dynamips/nodes/c3600.py b/gns3server/compute/dynamips/nodes/c3600.py index fc10a1629..d4d5f7235 100644 --- a/gns3server/compute/dynamips/nodes/c3600.py +++ b/gns3server/compute/dynamips/nodes/c3600.py @@ -29,7 +29,6 @@ log = logging.getLogger(__name__) class C3600(Router): - """ Dynamips c3600 router. diff --git a/gns3server/compute/dynamips/nodes/c3725.py b/gns3server/compute/dynamips/nodes/c3725.py index c20cdbecf..3f9c22e1d 100644 --- a/gns3server/compute/dynamips/nodes/c3725.py +++ b/gns3server/compute/dynamips/nodes/c3725.py @@ -30,7 +30,6 @@ log = logging.getLogger(__name__) class C3725(Router): - """ Dynamips c3725 router. diff --git a/gns3server/compute/dynamips/nodes/c3745.py b/gns3server/compute/dynamips/nodes/c3745.py index 9da5de8d5..36eac087d 100644 --- a/gns3server/compute/dynamips/nodes/c3745.py +++ b/gns3server/compute/dynamips/nodes/c3745.py @@ -30,7 +30,6 @@ log = logging.getLogger(__name__) class C3745(Router): - """ Dynamips c3745 router. diff --git a/gns3server/compute/dynamips/nodes/c7200.py b/gns3server/compute/dynamips/nodes/c7200.py index fe2f2b039..98541b87f 100644 --- a/gns3server/compute/dynamips/nodes/c7200.py +++ b/gns3server/compute/dynamips/nodes/c7200.py @@ -32,7 +32,6 @@ log = logging.getLogger(__name__) class C7200(Router): - """ Dynamips c7200 router (model is 7206). diff --git a/gns3server/compute/dynamips/nodes/device.py b/gns3server/compute/dynamips/nodes/device.py index 898f0ef9b..ed44e04d5 100644 --- a/gns3server/compute/dynamips/nodes/device.py +++ b/gns3server/compute/dynamips/nodes/device.py @@ -16,7 +16,6 @@ class Device: - """ Base device for switches and hubs diff --git a/gns3server/compute/dynamips/nodes/ethernet_hub.py b/gns3server/compute/dynamips/nodes/ethernet_hub.py index 46f8cc5a4..e8ea75062 100644 --- a/gns3server/compute/dynamips/nodes/ethernet_hub.py +++ b/gns3server/compute/dynamips/nodes/ethernet_hub.py @@ -29,7 +29,6 @@ log = logging.getLogger(__name__) class EthernetHub(Bridge): - """ Dynamips Ethernet hub (based on Bridge) diff --git a/gns3server/compute/dynamips/nodes/ethernet_switch.py b/gns3server/compute/dynamips/nodes/ethernet_switch.py index 016322141..df3adf349 100644 --- a/gns3server/compute/dynamips/nodes/ethernet_switch.py +++ b/gns3server/compute/dynamips/nodes/ethernet_switch.py @@ -68,7 +68,6 @@ log = logging.getLogger(__name__) class EthernetSwitch(Device): - """ Dynamips Ethernet switch. diff --git a/gns3server/compute/dynamips/nodes/frame_relay_switch.py b/gns3server/compute/dynamips/nodes/frame_relay_switch.py index 32c920898..5839e1cdf 100644 --- a/gns3server/compute/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/compute/dynamips/nodes/frame_relay_switch.py @@ -31,7 +31,6 @@ log = logging.getLogger(__name__) class FrameRelaySwitch(Device): - """ Dynamips Frame Relay switch. diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index 6a09cae5f..4bc1f77b5 100644 --- a/gns3server/compute/dynamips/nodes/router.py +++ b/gns3server/compute/dynamips/nodes/router.py @@ -42,7 +42,6 @@ from gns3server.utils.images import md5sum class Router(BaseNode): - """ Dynamips router implementation. @@ -80,7 +79,14 @@ class Router(BaseNode): raise DynamipsError(f"{name} is an invalid name to create a Dynamips node") super().__init__( - name, node_id, project, manager, console=console, console_type=console_type, aux=aux, aux_type=aux_type, + name, + node_id, + project, + manager, + console=console, + console_type=console_type, + aux=aux, + aux_type=aux_type, wrap_console=(console_type == "ssh"), wrap_aux=(aux_type == "ssh"), ) @@ -242,7 +248,6 @@ class Router(BaseNode): ) if not self._ghost_flag: - log.debug( 'Router {platform} "{name}" [{id}] has been created'.format( name=self._name, platform=self._platform, id=self._id @@ -252,7 +257,9 @@ class Router(BaseNode): if self._console is not None: # For SSH console, tell Dynamips to listen on the internal port so that # the AsyncioSSHServer proxy can wrap it on the external console port. - con_port = self._internal_console_port if self._wrap_console and self._internal_console_port else self._console + con_port = ( + self._internal_console_port if self._wrap_console and self._internal_console_port else self._console + ) await self._hypervisor.send(f'vm set_con_tcp_port "{self._name}" {con_port}') if self.aux is not None: @@ -287,7 +294,6 @@ class Router(BaseNode): if status == "suspended": await self.resume() elif status == "inactive": - if not os.path.isfile(self._image) or not os.path.exists(self._image): if os.path.islink(self._image): raise DynamipsError( @@ -1026,8 +1032,11 @@ class Router(BaseNode): if self.console_type != console_type: status = await self.get_status() if status == "running": - raise DynamipsError('"{name}" must be stopped to change the console type to {console_type}'.format(name=self._name, - console_type=console_type)) + raise DynamipsError( + '"{name}" must be stopped to change the console type to {console_type}'.format( + name=self._name, console_type=console_type + ) + ) self.console_type = console_type @@ -1068,9 +1077,10 @@ class Router(BaseNode): if self.aux_type != aux_type: status = await self.get_status() if status == "running": - raise DynamipsError('"{name}" must be stopped to change the auxiliary console type to {aux_type}'.format( - name=self._name, - aux_type=aux_type) + raise DynamipsError( + '"{name}" must be stopped to change the auxiliary console type to {aux_type}'.format( + name=self._name, aux_type=aux_type + ) ) self.aux_type = aux_type @@ -1228,7 +1238,6 @@ class Router(BaseNode): # Generate an OIR event if the router is running if is_running: - await self._hypervisor.send( 'vm slot_oir_start "{name}" {slot_number} 0'.format(name=self._name, slot_number=slot_number) ) @@ -1274,7 +1283,6 @@ class Router(BaseNode): # Generate an OIR event if the router is running if is_running: - await self._hypervisor.send( 'vm slot_oir_stop "{name}" {slot_number} 0'.format(name=self._name, slot_number=slot_number) ) @@ -1722,7 +1730,9 @@ class Router(BaseNode): """ if not is_ios_hostname_valid(new_name): - raise DynamipsError(f"{new_name} is an invalid name to rename router '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name must start with a letter, end with a letter or digit, and be 63 characters or fewer.") + raise DynamipsError( + f"{new_name} is an invalid name to rename router '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name must start with a letter, end with a letter or digit, and be 63 characters or fewer." + ) await self._hypervisor.send(f'vm rename "{self._name}" "{new_name}"') diff --git a/gns3server/compute/iou/__init__.py b/gns3server/compute/iou/__init__.py index 31e7878db..afe2766a2 100644 --- a/gns3server/compute/iou/__init__.py +++ b/gns3server/compute/iou/__init__.py @@ -31,7 +31,6 @@ log = logging.getLogger(__name__) class IOU(BaseManager): - _NODE_CLASS = IOUVM _NODE_TYPE = "iou" diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index ff46bb4fb..4e329aad5 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -338,13 +338,11 @@ class IOUVM(BaseNode): if elf_header_start[4] == 1: # 32-bit loader loader = os.path.join(self._lib_base, "lib", "ld-linux.so.2") - lib_path = (os.path.join(self._lib_base, "lib"), - os.path.join(self._lib_base, "lib", "i386-linux-gnu")) + lib_path = (os.path.join(self._lib_base, "lib"), os.path.join(self._lib_base, "lib", "i386-linux-gnu")) else: # 64-bit loader loader = os.path.join(self._lib_base, "lib64", "ld-linux-x86-64.so.2") - lib_path = (os.path.join(self._lib_base, "lib64"), - os.path.join(self._lib_base, "lib", "x86_64-linux-gnu")) + lib_path = (os.path.join(self._lib_base, "lib64"), os.path.join(self._lib_base, "lib", "x86_64-linux-gnu")) self._loader = [] if os.path.isfile(loader): try: @@ -475,7 +473,9 @@ class IOUVM(BaseNode): """ if not is_ios_hostname_valid(new_name): - raise IOUError(f"'{new_name}' is an invalid name to rename IOU node '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name must start with a letter, end with a letter or digit, and be 63 characters or fewer.") + raise IOUError( + f"'{new_name}' is an invalid name to rename IOU node '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name must start with a letter, end with a letter or digit, and be 63 characters or fewer." + ) if self.startup_config_file: content = self.startup_config_content content = re.sub(r"hostname .+$", "hostname " + new_name, content, flags=re.MULTILINE) @@ -613,7 +613,7 @@ class IOUVM(BaseNode): raise IOUError(f"Invalid hostid detected: {hostid}") for x in hostname: ioukey += ord(x) - pad1 = b"\x4B\x58\x21\x81\x56\x7B\x0D\xF3\x21\x43\x9B\x7E\xAC\x1D\xE6\x8A" + pad1 = b"\x4b\x58\x21\x81\x56\x7b\x0d\xf3\x21\x43\x9b\x7e\xac\x1d\xe6\x8a" pad2 = b"\x80" + 39 * b"\0" ioukey = hashlib.md5(pad1 + pad2 + struct.pack("!I", ioukey) + pad1).hexdigest()[:16] if ioukey != user_ioukey: @@ -668,7 +668,6 @@ class IOUVM(BaseNode): await self._check_requirements() if not self.is_running(): - await self._library_check() try: @@ -709,7 +708,7 @@ class IOUVM(BaseNode): if len(iou_image_path) > 63: # IOU file basename length must be <= 63 chars iou_file_name, iou_file_ext = os.path.splitext(iou_image_path) - iou_image_path = iou_file_name[:63 - len(iou_file_ext)] + iou_file_ext + iou_image_path = iou_file_name[: 63 - len(iou_file_ext)] + iou_file_ext symlink = os.path.join(self.working_dir, iou_image_path) if os.path.islink(symlink): os.unlink(symlink) @@ -729,7 +728,8 @@ class IOUVM(BaseNode): log.debug(f"Starting IOU: {command}") self.command_line = " ".join(command) self._iou_process = await asyncio.create_subprocess_exec( - *self._loader, *command, + *self._loader, + *command, stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE, stderr=subprocess.STDOUT, @@ -743,7 +743,9 @@ class IOUVM(BaseNode): gns3server.utils.asyncio.monitor_process(self._iou_process, callback) except FileNotFoundError as e: self._stop_l1_keepalive_responder() - raise IOUError(f"Could not start IOU: {e}: 32-bit binary support is probably not installed, it is recommended to use a 64-bit image instead") + raise IOUError( + f"Could not start IOU: {e}: 32-bit binary support is probably not installed, it is recommended to use a 64-bit image instead" + ) except (OSError, subprocess.SubprocessError) as e: self._stop_l1_keepalive_responder() iou_stdout = self.read_iou_stdout() @@ -775,8 +777,7 @@ class IOUVM(BaseNode): await self.stop() raise IOUError( "Could not start {} server on socket {}:{}: {}".format( - error_prefix, - self._manager.port_manager.console_host, self.console, e + error_prefix, self._manager.port_manager.console_host, self.console, e ) ) @@ -1281,13 +1282,11 @@ class IOUVM(BaseNode): """ from gns3server.compute.marker.marker_manager import MarkerManager - markers = nio.markers if hasattr(nio, 'markers') else {} + markers = nio.markers if hasattr(nio, "markers") else {} manager = MarkerManager.instance() markers_dir = self.project.markers_working_directory() bridge_name = f"IOL-BRIDGE-{self.application_id + 512}" - location = "{bridge_name} {bay} {unit}".format( - bridge_name=bridge_name, bay=adapter_number, unit=port_number - ) + location = "{bridge_name} {bay} {unit}".format(bridge_name=bridge_name, bay=adapter_number, unit=port_number) desired = {(name, spec.get("link_id", "")): spec for name, spec in markers.items()} # 1. Remove installed markers that are no longer desired. @@ -1331,9 +1330,7 @@ class IOUVM(BaseNode): continue pcap_path = os.path.join(markers_dir, f"{self._id}_{link_id}_{name}.pcap") # iol_bridge add_packet_filter {br} {bay} {unit} {name} mark "{bpf}" [tag {id}] pcap "{path}" - cmd = 'iol_bridge add_packet_filter {loc} {name} mark "{bpf}"'.format( - loc=location, name=name, bpf=bpf - ) + cmd = 'iol_bridge add_packet_filter {loc} {name} mark "{bpf}"'.format(loc=location, name=name, bpf=bpf) if tag is not None: cmd += f" tag {tag}" if link_id: diff --git a/gns3server/compute/marker/marker_listener.py b/gns3server/compute/marker/marker_listener.py index a30769c1e..4dff2ab18 100644 --- a/gns3server/compute/marker/marker_listener.py +++ b/gns3server/compute/marker/marker_listener.py @@ -103,9 +103,7 @@ class MarkerListener(asyncio.DatagramProtocol): project_id, link_id, registered_tag = self._manager.lookup(node_id, filter_name) if project_id is None: - log.warning( - "MARK signal for unregistered node=%s filter=%s, dropping", node_id, filter_name - ) + log.warning("MARK signal for unregistered node=%s filter=%s, dropping", node_id, filter_name) return # `link=` is the authoritative per-link id (opaque, set by gns3server at diff --git a/gns3server/compute/marker/marker_manager.py b/gns3server/compute/marker/marker_manager.py index 132a19812..7c5fe8b36 100644 --- a/gns3server/compute/marker/marker_manager.py +++ b/gns3server/compute/marker/marker_manager.py @@ -86,24 +86,18 @@ class MarkerManager: sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8 * 1024 * 1024) try: - self._transport, _ = await loop.create_datagram_endpoint( - lambda: self._listener, local_addr=(host, port) - ) + self._transport, _ = await loop.create_datagram_endpoint(lambda: self._listener, local_addr=(host, port)) _configure_transport(self._transport) except OSError: if port != 0: - log.warning( - "Marker listener: port %s unavailable, falling back to OS-assigned port", port - ) + log.warning("Marker listener: port %s unavailable, falling back to OS-assigned port", port) try: self._transport, _ = await loop.create_datagram_endpoint( lambda: self._listener, local_addr=(host, 0) ) _configure_transport(self._transport) except OSError as e: - log.error( - "Marker listener startup failed: %s. Traffic insight signals are unavailable.", e - ) + log.error("Marker listener startup failed: %s. Traffic insight signals are unavailable.", e) self._listener = None return else: @@ -132,7 +126,9 @@ class MarkerManager: if received: log.info( "marker sink: %d matches (%.0f/s), %d errors in last 10s", - received, received / 10.0, errors, + received, + received / 10.0, + errors, ) async def stop(self): diff --git a/gns3server/compute/nios/nio.py b/gns3server/compute/nios/nio.py index 6fe57a130..0de5e4cfc 100644 --- a/gns3server/compute/nios/nio.py +++ b/gns3server/compute/nios/nio.py @@ -20,7 +20,6 @@ Base interface for NIOs. class NIO: - """ IOU NIO. """ diff --git a/gns3server/compute/nios/nio_ethernet.py b/gns3server/compute/nios/nio_ethernet.py index 0f7b48d31..0dc153c54 100644 --- a/gns3server/compute/nios/nio_ethernet.py +++ b/gns3server/compute/nios/nio_ethernet.py @@ -22,7 +22,6 @@ from .nio import NIO class NIOEthernet(NIO): - """ Generic Ethernet NIO. @@ -50,7 +49,4 @@ class NIOEthernet(NIO): def asdict(self): - return { - "type": "nio_ethernet", - "ethernet_device": self._ethernet_device - } + return {"type": "nio_ethernet", "ethernet_device": self._ethernet_device} diff --git a/gns3server/compute/nios/nio_tap.py b/gns3server/compute/nios/nio_tap.py index a81b0906f..ac26bb662 100644 --- a/gns3server/compute/nios/nio_tap.py +++ b/gns3server/compute/nios/nio_tap.py @@ -22,7 +22,6 @@ from .nio import NIO class NIOTAP(NIO): - """ TAP NIO. @@ -50,7 +49,4 @@ class NIOTAP(NIO): def asdict(self): - return { - "type": "nio_tap", - "tap_device": self._tap_device - } + return {"type": "nio_tap", "tap_device": self._tap_device} diff --git a/gns3server/compute/nios/nio_udp.py b/gns3server/compute/nios/nio_udp.py index e6f1bd8bc..0003e0744 100644 --- a/gns3server/compute/nios/nio_udp.py +++ b/gns3server/compute/nios/nio_udp.py @@ -22,7 +22,6 @@ from .nio import NIO class NIOUDP(NIO): - """ UDP NIO. @@ -81,5 +80,5 @@ class NIOUDP(NIO): "rhost": self._rhost, "suspend": self._suspended, "filters": self._filters, - "markers": self._markers + "markers": self._markers, } diff --git a/gns3server/compute/port_manager.py b/gns3server/compute/port_manager.py index dbb18fc0e..ece70c3ab 100644 --- a/gns3server/compute/port_manager.py +++ b/gns3server/compute/port_manager.py @@ -95,7 +95,6 @@ BANNED_PORTS = { class PortManager: - """ :param host: IP address to bind for console connections """ @@ -318,7 +317,9 @@ class PortManager: if port in self._used_tcp_ports: old_port = port port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end) - msg = f"TCP port {old_port} already in use on host {self._console_host}. Port has been replaced by {port}" + msg = ( + f"TCP port {old_port} already in use on host {self._console_host}. Port has been replaced by {port}" + ) log.debug(msg) return port if port < port_range_start or port > port_range_end: @@ -335,7 +336,9 @@ class PortManager: except OSError: old_port = port port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end) - msg = f"TCP port {old_port} already in use on host {self._console_host}. Port has been replaced by {port}" + msg = ( + f"TCP port {old_port} already in use on host {self._console_host}. Port has been replaced by {port}" + ) log.debug(msg) return port @@ -395,8 +398,7 @@ class PortManager: if port < self._udp_port_range[0] or port > self._udp_port_range[1]: raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail=f"UDP port {port} is outside the range " - f"{self._udp_port_range[0]}-{self._udp_port_range[1]}", + detail=f"UDP port {port} is outside the range {self._udp_port_range[0]}-{self._udp_port_range[1]}", ) self._used_udp_ports.add(port) project.record_udp_port(port) diff --git a/gns3server/compute/project.py b/gns3server/compute/project.py index ae3b5c8d1..6f683d5bc 100644 --- a/gns3server/compute/project.py +++ b/gns3server/compute/project.py @@ -37,7 +37,6 @@ log = logging.getLogger(__name__) class Project: - """ A project contains a list of nodes. In theory nodes are isolated project/project. @@ -82,11 +81,7 @@ class Project: def asdict(self): - return { - "name": self._name, - "project_id": self._id, - "variables": self._variables - } + return {"name": self._name, "project_id": self._id, "variables": self._variables} @property def id(self): @@ -461,15 +456,14 @@ class Project: # Security check: ensure the path is within the node directory if not os.path.commonpath([target_path, node_full_path]) == node_full_path: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, - detail="Path is outside the node directory") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Path is outside the node directory") if not os.path.exists(target_path): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, - detail="Path not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Path not found") if recursive: - return await self._list_node_files_recursive(node_full_path, target_path, - node_full_path if subpath else None) + return await self._list_node_files_recursive( + node_full_path, target_path, node_full_path if subpath else None + ) # Non-recursive: list only the current directory level files = [] @@ -492,20 +486,20 @@ class Project: modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat() except (OSError, OverflowError, ValueError): created_at = modified_at = "" - files.append({ - "path": rel_path, - "size": stat_info.st_size, - "created_at": created_at, - "modified_at": modified_at, - "file_type": "directory" - }) + files.append( + { + "path": rel_path, + "size": stat_info.st_size, + "created_at": created_at, + "modified_at": modified_at, + "file_type": "directory", + } + ) else: if name.endswith(".ghost"): continue try: - file_type = await wait_run_in_executor( - lambda e=entry: magic.from_file(e.path, mime=False) - ) + file_type = await wait_run_in_executor(lambda e=entry: magic.from_file(e.path, mime=False)) except Exception as e: log.warning(f"Error getting file type for '{rel_path}': {e}") file_type = "" @@ -514,13 +508,15 @@ class Project: modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat() except (OSError, OverflowError, ValueError): created_at = modified_at = "" - files.append({ - "path": rel_path, - "size": stat_info.st_size, - "created_at": created_at, - "modified_at": modified_at, - "file_type": file_type - }) + files.append( + { + "path": rel_path, + "size": stat_info.st_size, + "created_at": created_at, + "modified_at": modified_at, + "file_type": file_type, + } + ) except OSError: continue return files @@ -543,13 +539,15 @@ class Project: modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat() except (OSError, OverflowError, ValueError): created_at = modified_at = "" - files.append({ - "path": os.path.relpath(dir_full_path, base_path), - "size": stat_info.st_size, - "created_at": created_at, - "modified_at": modified_at, - "file_type": "directory" - }) + files.append( + { + "path": os.path.relpath(dir_full_path, base_path), + "size": stat_info.st_size, + "created_at": created_at, + "modified_at": modified_at, + "file_type": "directory", + } + ) except OSError: continue @@ -561,9 +559,7 @@ class Project: try: stat_info = await wait_run_in_executor(os.stat, file_path) try: - file_type = await wait_run_in_executor( - lambda fp=file_path: magic.from_file(fp, mime=False) - ) + file_type = await wait_run_in_executor(lambda fp=file_path: magic.from_file(fp, mime=False)) except Exception as e: log.warning(f"Error getting file type for '{rel_path}': {e}") file_type = "" @@ -573,13 +569,15 @@ class Project: except (OSError, OverflowError, ValueError) as e: log.warning(f"Invalid timestamp for '{rel_path}': {e}") created_at = modified_at = "" - files.append({ - "path": rel_path, - "size": stat_info.st_size, - "created_at": created_at, - "modified_at": modified_at, - "file_type": file_type - }) + files.append( + { + "path": rel_path, + "size": stat_info.st_size, + "created_at": created_at, + "modified_at": modified_at, + "file_type": file_type, + } + ) except OSError as e: log.warning(f"Error getting metadata for file '{rel_path}': {e}") continue diff --git a/gns3server/compute/project_manager.py b/gns3server/compute/project_manager.py index a2ab578d2..91f4757b4 100644 --- a/gns3server/compute/project_manager.py +++ b/gns3server/compute/project_manager.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class ProjectManager: - """ This singleton keeps track of available projects. """ @@ -106,7 +105,7 @@ class ProjectManager: if project_id is not None and project_id in self._projects: return self._projects[project_id] project = Project(name=name, project_id=project_id, path=path, variables=variables) - #self._check_available_disk_space(project) # FIXME: disabled for now + # self._check_available_disk_space(project) # FIXME: disabled for now self._projects[project.id] = project return project diff --git a/gns3server/compute/qemu/__init__.py b/gns3server/compute/qemu/__init__.py index 1bb0f06eb..f01fdb68e 100644 --- a/gns3server/compute/qemu/__init__.py +++ b/gns3server/compute/qemu/__init__.py @@ -42,7 +42,6 @@ log = logging.getLogger(__name__) class Qemu(BaseManager): - _NODE_CLASS = QemuVM _NODE_TYPE = "qemu" @@ -252,17 +251,21 @@ class Qemu(BaseManager): :returns: HAXM version number. Returns None if HAXM is not installed. """ - assert(sys.platform.startswith("win")) + assert sys.platform.startswith("win") import winreg - hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products") + hkey = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products" + ) version = None for index in range(winreg.QueryInfoKey(hkey)[0]): product_id = winreg.EnumKey(hkey, index) try: product_key = winreg.OpenKey(hkey, r"{}\InstallProperties".format(product_id)) try: - if winreg.QueryValueEx(product_key, "DisplayName")[0].endswith("Hardware Accelerated Execution Manager"): + if winreg.QueryValueEx(product_key, "DisplayName")[0].endswith( + "Hardware Accelerated Execution Manager" + ): version = winreg.QueryValueEx(product_key, "DisplayVersion")[0] break finally: diff --git a/gns3server/compute/qemu/qemu_error.py b/gns3server/compute/qemu/qemu_error.py index 90659215d..2d4a9df7b 100644 --- a/gns3server/compute/qemu/qemu_error.py +++ b/gns3server/compute/qemu/qemu_error.py @@ -22,5 +22,4 @@ from ..error import NodeError class QemuError(NodeError): - pass diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index d6b0318b5..25721b309 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -57,10 +57,8 @@ import logging log = logging.getLogger(__name__) # forbidden additional options -FORBIDDEN_OPTIONS = {"-blockdev", "-drive", "-hda", "-hdb", "-hdc", "-hdd", - "-fsdev", "-virtfs", "-nic", "-netdev"} -FORBIDDEN_OPTIONS |= {"-" + opt for opt in FORBIDDEN_OPTIONS - if opt.startswith("-") and not opt.startswith("--")} +FORBIDDEN_OPTIONS = {"-blockdev", "-drive", "-hda", "-hdb", "-hdc", "-hdd", "-fsdev", "-virtfs", "-nic", "-netdev"} +FORBIDDEN_OPTIONS |= {"-" + opt for opt in FORBIDDEN_OPTIONS if opt.startswith("-") and not opt.startswith("--")} class QemuVM(BaseNode): @@ -132,8 +130,10 @@ class QemuVM(BaseNode): except QemuError: # If the binary is not found for topologies 1.4 and later # search via the platform otherwise use the binary name - log.warning(f"Could not find the QEMU binary {qemu_path} on this system, " - f"trying to find one using platform {platform}") + log.warning( + f"Could not find the QEMU binary {qemu_path} on this system, " + f"trying to find one using platform {platform}" + ) if platform: self.platform = platform else: @@ -198,7 +198,9 @@ class QemuVM(BaseNode): """ if not is_rfc1123_hostname_valid(new_name): - raise QemuError(f"'{new_name}' is an invalid name to rename Qemu node '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name cannot start or end with a hyphen.") + raise QemuError( + f"'{new_name}' is an invalid name to rename Qemu node '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name cannot start or end with a hyphen." + ) super(QemuVM, QemuVM).name.__set__(self, new_name) @property @@ -267,7 +269,7 @@ class QemuVM(BaseNode): if qemu_bin == "qemu": self._platform = "i386" else: - self._platform = re.sub(r'^qemu-system-(\w+).*$', r'\1', qemu_bin, flags=re.IGNORECASE) + self._platform = re.sub(r"^qemu-system-(\w+).*$", r"\1", qemu_bin, flags=re.IGNORECASE) if self._platform.split(".")[0] not in list(QemuPlatform): raise QemuError(f"Platform {self._platform} is unknown") log.debug(f'QEMU VM "{self._name}" [{self._name}] has set the QEMU path to {qemu_path}') @@ -1082,9 +1084,7 @@ class QemuVM(BaseNode): else: priority = 0 try: - process = await asyncio.create_subprocess_exec( - "renice", "-n", str(priority), "-p", str(self._process.pid) - ) + process = await asyncio.create_subprocess_exec("renice", "-n", str(priority), "-p", str(self._process.pid)) await process.wait() except (OSError, subprocess.SubprocessError) as e: log.error(f'Could not change process priority for QEMU VM "{self._name}": {e}') @@ -1108,11 +1108,18 @@ class QemuVM(BaseNode): try: if sys.platform.startswith("win") and hasattr(sys, "frozen"): - cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe") + cpulimit_exec = os.path.join( + os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe" + ) else: cpulimit_exec = "cpulimit" - command = [cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)] + command = [ + cpulimit_exec, + "--lazy", + "--pid={}".format(self._process.pid), + "--limit={}".format(self._cpu_throttling), + ] self._cpulimit_process = subprocess.Popen(command, cwd=self.working_dir) log.debug(f"CPU throttled to {self._cpu_throttling}%") except FileNotFoundError: @@ -1251,10 +1258,12 @@ class QemuVM(BaseNode): if "TPM result for CMD_INIT" in qemu_stdout: partition = self._find_partition_for_path(self.project.path) if partition and partition.mountpoint != "/": - qemu_stdout += "\nTPM error: the project directory is not on the same partition as the root directory which can be a problem when using AppArmor.\n" \ - "Please try to execute the following commands on the server:\n\n" \ - "echo 'owner {}/** rwk,' | sudo tee /etc/apparmor.d/local/usr.bin.swtpm > /dev/null\n" \ - "sudo service apparmor restart".format(os.path.dirname(self.project.path)) + qemu_stdout += ( + "\nTPM error: the project directory is not on the same partition as the root directory which can be a problem when using AppArmor.\n" + "Please try to execute the following commands on the server:\n\n" + "echo 'owner {}/** rwk,' | sudo tee /etc/apparmor.d/local/usr.bin.swtpm > /dev/null\n" + "sudo service apparmor restart".format(os.path.dirname(self.project.path)) + ) self.project.emit( "log.error", {"message": f"QEMU process has stopped, return code: {returncode}\n{qemu_stdout}"}, @@ -1272,7 +1281,6 @@ class QemuVM(BaseNode): if self.is_running(): log.debug(f'Stopping QEMU VM "{self._name}" PID={self._process.pid}') try: - if self.on_close == "save_vm_state": await self._control_vm("stop") await self._control_vm("savevm GNS3_SAVED_STATE") @@ -1399,7 +1407,6 @@ class QemuVM(BaseNode): """ if self.is_running() and self._monitor: - reader, writer = await self._open_qemu_monitor_connection_vm() if reader is None and writer is None: return @@ -1729,7 +1736,9 @@ class QemuVM(BaseNode): retcode = await self._qemu_img_exec(command) if retcode: stdout = self.read_qemu_img_stdout() - raise QemuError(f"Could not create '{disk_name}' disk image: qemu-img returned with {retcode}\n{stdout}") + raise QemuError( + f"Could not create '{disk_name}' disk image: qemu-img returned with {retcode}\n{stdout}" + ) else: log.debug(f"QEMU VM '{self.name}' [{self.id}]: Qemu disk image'{disk_name}' created") except (OSError, subprocess.SubprocessError) as e: @@ -1757,7 +1766,9 @@ class QemuVM(BaseNode): retcode = await self._qemu_img_exec(command) if retcode: stdout = self.read_qemu_img_stdout() - raise QemuError(f"Could not update '{disk_name}' disk image: qemu-img returned with {retcode}\n{stdout}") + raise QemuError( + f"Could not update '{disk_name}' disk image: qemu-img returned with {retcode}\n{stdout}" + ) else: log.debug(f"QEMU VM '{self.name}' [{self.id}]: Qemu disk image '{disk_name}' extended by {extend} MB") except (OSError, subprocess.SubprocessError) as e: @@ -1894,9 +1905,7 @@ class QemuVM(BaseNode): console_host = "::" except OSError as e: raise QemuError("Could not check if IPv6 is enabled: {}".format(e)) - return ["-spice", - f"addr={console_host},port={port},disable-ticketing", - "-vga", "qxl"] + return ["-spice", f"addr={console_host},port={port},disable-ticketing", "-vga", "qxl"] else: return [] @@ -2015,11 +2024,31 @@ class QemuVM(BaseNode): if base_qcow2 and base_qcow2.crypt_method: # Workaround for https://gitlab.com/qemu-project/qemu/-/issues/441 # (we have to pass -u and the size). Also embed secret name. - command = [qemu_img_path, "create", "-b", backing_options, - "-F", backing_file_format, "-f", "qcow2", "-u", disk, str(base_qcow2.size)] + command = [ + qemu_img_path, + "create", + "-b", + backing_options, + "-F", + backing_file_format, + "-f", + "qcow2", + "-u", + disk, + str(base_qcow2.size), + ] else: - command = [qemu_img_path, "create", "-o", "backing_file={}".format(disk_image), - "-F", backing_file_format, "-f", "qcow2", disk] + command = [ + qemu_img_path, + "create", + "-o", + "backing_file={}".format(disk_image), + "-F", + backing_file_format, + "-f", + "qcow2", + disk, + ] retcode = await self._qemu_img_exec(command) if retcode: @@ -2186,8 +2215,7 @@ class QemuVM(BaseNode): if not os.path.isfile(disk_image) or not os.path.exists(disk_image): if os.path.islink(disk_image): raise QemuError( - f"'{disk_name}' disk image linked to " - f"'{os.path.realpath(disk_image)}' is not accessible" + f"'{disk_name}' disk image linked to '{os.path.realpath(disk_image)}' is not accessible" ) else: raise QemuError(f"'{disk_image}' is not accessible") @@ -2203,7 +2231,7 @@ class QemuVM(BaseNode): if await self._qemu_img_exec([qemu_img_path, "check", "-r", "leaks", f"{disk_image}"]) == 3: self.project.emit( "log.warning", - {"message": f"Disk image '{disk_image}' has leaked clusters and could not be fixed"} + {"message": f"Disk image '{disk_image}' has leaked clusters and could not be fixed"}, ) elif retcode == 2: # image is corrupted, let's try to fix it @@ -2211,15 +2239,14 @@ class QemuVM(BaseNode): if await self._qemu_img_exec([qemu_img_path, "check", "-r", "all", f"{disk_image}"]) == 2: self.project.emit( "log.warning", - {"message": f"Disk image '{disk_image}' is corrupted and could not be fixed"} + {"message": f"Disk image '{disk_image}' is corrupted and could not be fixed"}, ) except (OSError, subprocess.SubprocessError) as e: stdout = self.read_qemu_img_stdout() raise QemuError(f"Could not check '{disk_name}' disk image: {e}\n{stdout}") if self.linked_clone and os.path.dirname(disk_image) != self.working_dir: - - #cloned_disk_image = os.path.splitext(os.path.basename(disk_image)) + # cloned_disk_image = os.path.splitext(os.path.basename(disk_image)) disk = os.path.join(self.working_dir, f"{disk_name}_disk.qcow2") if not os.path.exists(disk): # create the disk @@ -2305,8 +2332,9 @@ class QemuVM(BaseNode): else: # Use a manual case-insensitive search instead try: - system_ovmf_firmware_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd") - if f.name.lower() == "ovmf_code_4m.fd"), None) + system_ovmf_firmware_path = next( + (f for f in system_ovmf_firmware_dir.glob("*.fd") if f.name.lower() == "ovmf_code_4m.fd"), None + ) except (FileNotFoundError, StopIteration): system_ovmf_firmware_path = None @@ -2321,8 +2349,9 @@ class QemuVM(BaseNode): # try to use the UEFI variables store from the system first try: - system_ovmf_vars_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd") - if f.name.lower() == "ovmf_vars_4m.fd"), None) + system_ovmf_vars_path = next( + (f for f in system_ovmf_firmware_dir.glob("*.fd") if f.name.lower() == "ovmf_vars_4m.fd"), None + ) except (FileNotFoundError, StopIteration): system_ovmf_vars_path = None if system_ovmf_vars_path: @@ -2389,15 +2418,18 @@ class QemuVM(BaseNode): swtpm_version = await self.manager.get_swtpm_version(swtpm) if swtpm_version and parse_version(swtpm_version) < parse_version("0.8.0"): # swtpm >= version 0.8.0 is required - raise QemuError("swtpm version 0.8.0 or above must be installed (detected version is {})".format(swtpm_version)) + raise QemuError( + "swtpm version 0.8.0 or above must be installed (detected version is {})".format(swtpm_version) + ) try: command = [ swtpm, "socket", "--tpm2", - '--tpmstate', "dir={}".format(tpm_dir), + "--tpmstate", + "dir={}".format(tpm_dir), "--ctrl", - "type=unixio,path={},terminate".format(tpm_sock) + "type=unixio,path={},terminate".format(tpm_sock), ] command_string = " ".join(shlex.quote(s) for s in command) log.debug("Starting swtpm (TPM emulator) with: {}".format(command_string)) @@ -2429,7 +2461,7 @@ class QemuVM(BaseNode): "-tpmdev", "emulator,id=tpm0,chardev=chrtpm", "-device", - "tpm-tis,tpmdev=tpm0" + "tpm-tis,tpmdev=tpm0", ] return options @@ -2506,7 +2538,7 @@ class QemuVM(BaseNode): Disable graphics depending of the QEMU version """ - if any(opt in self._options for opt in ["-display", "-nographic", "-curses", "-sdl" "-spice", "-vnc"]): + if any(opt in self._options for opt in ["-display", "-nographic", "-curses", "-sdl-spice", "-vnc"]): return [] if self._qemu_version and parse_version(self._qemu_version) >= parse_version("3.0"): return ["-display", "none"] @@ -2663,7 +2695,11 @@ class QemuVM(BaseNode): command.extend(["-enable-kvm"]) # Issue on some combo Intel CPU + KVM + Qemu 2.4.0 # https://github.com/GNS3/gns3-server/issues/685 - if self._qemu_version and parse_version(self._qemu_version) >= parse_version("2.4.0") and self.platform == "x86_64": + if ( + self._qemu_version + and parse_version(self._qemu_version) >= parse_version("2.4.0") + and self.platform == "x86_64" + ): command.extend(["-machine", "smm=off"]) elif sys.platform.startswith("darwin"): command.extend(["-enable-hax"]) @@ -2701,7 +2737,7 @@ class QemuVM(BaseNode): # avoiding mouse offset (see https://github.com/GNS3/gns3-server/issues/2335) if self._console_type == "vnc": - command.extend(['-machine', 'usb=on', '-device', 'usb-tablet']) + command.extend(["-machine", "usb=on", "-device", "usb-tablet"]) return command def asdict(self): diff --git a/gns3server/compute/qemu/utils/qcow2.py b/gns3server/compute/qemu/utils/qcow2.py index 60fdc9724..9ea5fb40a 100644 --- a/gns3server/compute/qemu/utils/qcow2.py +++ b/gns3server/compute/qemu/utils/qcow2.py @@ -60,11 +60,18 @@ class Qcow2: # } QCowHeader; struct_format = ">IIQiiQi" - with open(self._path, 'rb') as f: + with open(self._path, "rb") as f: content = f.read(struct.calcsize(struct_format)) try: - (self.magic, self.version, self.backing_file_offset, self.backing_file_size, - self.cluster_bits, self.size, self.crypt_method) = struct.unpack_from(struct_format, content) + ( + self.magic, + self.version, + self.backing_file_offset, + self.backing_file_size, + self.cluster_bits, + self.size, + self.crypt_method, + ) = struct.unpack_from(struct_format, content) except struct.error: raise Qcow2Error(f"Invalid file header for {self._path}") @@ -112,7 +119,7 @@ class Qcow2: "filename": base_image, }, } - return ("json:"+json.dumps(options, separators=(',', ':')), base_qcow2) + return ("json:" + json.dumps(options, separators=(",", ":")), base_qcow2) else: return (base_image, base_qcow2) except Qcow2Error: diff --git a/gns3server/compute/ubridge/hypervisor.py b/gns3server/compute/ubridge/hypervisor.py index faefcb872..addc5cc9f 100644 --- a/gns3server/compute/ubridge/hypervisor.py +++ b/gns3server/compute/ubridge/hypervisor.py @@ -37,7 +37,6 @@ log = logging.getLogger(__name__) class Hypervisor(UBridgeHypervisor): - """ Hypervisor. diff --git a/gns3server/compute/ubridge/ubridge_hypervisor.py b/gns3server/compute/ubridge/ubridge_hypervisor.py index c010e1e2d..b03752b69 100644 --- a/gns3server/compute/ubridge/ubridge_hypervisor.py +++ b/gns3server/compute/ubridge/ubridge_hypervisor.py @@ -26,7 +26,6 @@ log = logging.getLogger(__name__) class UBridgeHypervisor: - """ Creates a new connection to a uBridge hypervisor control channel. diff --git a/gns3server/compute/virtualbox/__init__.py b/gns3server/compute/virtualbox/__init__.py index ccc5a6952..3f3e92634 100644 --- a/gns3server/compute/virtualbox/__init__.py +++ b/gns3server/compute/virtualbox/__init__.py @@ -34,7 +34,6 @@ from .virtualbox_error import VirtualBoxError class VirtualBox(BaseManager): - _NODE_CLASS = VirtualBoxVM def __init__(self): @@ -106,10 +105,7 @@ class VirtualBox(BaseManager): env["LANG"] = "en" # force english output because we rely on it to parse the output try: process = await asyncio.create_subprocess_exec( - *command, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - env=env + *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env ) except (OSError, subprocess.SubprocessError) as e: raise VirtualBoxError(f"Could not execute VBoxManage: {e}") diff --git a/gns3server/compute/virtualbox/virtualbox_error.py b/gns3server/compute/virtualbox/virtualbox_error.py index 91f87e382..84597f68e 100644 --- a/gns3server/compute/virtualbox/virtualbox_error.py +++ b/gns3server/compute/virtualbox/virtualbox_error.py @@ -22,5 +22,4 @@ from ..error import NodeError class VirtualBoxError(NodeError): - pass diff --git a/gns3server/compute/virtualbox/virtualbox_vm.py b/gns3server/compute/virtualbox/virtualbox_vm.py index bcaa8b683..6c9fb6154 100644 --- a/gns3server/compute/virtualbox/virtualbox_vm.py +++ b/gns3server/compute/virtualbox/virtualbox_vm.py @@ -45,7 +45,6 @@ log = logging.getLogger(__name__) class VirtualBoxVM(BaseNode): - """ VirtualBox VM implementation. """ @@ -242,23 +241,23 @@ class VirtualBoxVM(BaseNode): try: tree = ET.parse(linked_vbox_file) except ET.ParseError: - raise VirtualBoxError(f"Cannot modify VirtualBox linked node file. " - "File {linked_vbox_file} is corrupted.") + raise VirtualBoxError(f"Cannot modify VirtualBox linked node file. File {{linked_vbox_file}} is corrupted.") except OSError as e: raise VirtualBoxError(f"Cannot modify VirtualBox linked nodes file '{self._linked_vbox_file()}': {e}") machine = tree.getroot().find("{http://www.virtualbox.org/}Machine") if machine is not None and machine.get("uuid") != "{" + self.id + "}": - for image in tree.getroot().findall("{http://www.virtualbox.org/}Image"): currentSnapshot = machine.get("currentSnapshot") if currentSnapshot: newSnapshot = re.sub(r"\{.*\}", "{" + str(uuid.uuid4()) + "}", currentSnapshot) shutil.move( os.path.join(self.working_dir, self._vmname, "Snapshots", currentSnapshot) + ".vdi", - os.path.join(self.working_dir, self._vmname, "Snapshots", newSnapshot) + ".vdi" + os.path.join(self.working_dir, self._vmname, "Snapshots", newSnapshot) + ".vdi", + ) + log.info( + f"VirtualBox VM '{self.name}' [{self.id}] snapshot file moved from '{currentSnapshot}' to '{newSnapshot}'" ) - log.info(f"VirtualBox VM '{self.name}' [{self.id}] snapshot file moved from '{currentSnapshot}' to '{newSnapshot}'") image.set("uuid", newSnapshot) log.info(f"VirtualBox VM '{self.name}' [{self.id}] '{linked_vbox_file}' has been patched") @@ -346,7 +345,6 @@ class VirtualBoxVM(BaseNode): vm_state = await self._get_vm_state() log.info(f"Stopping VirtualBox VM '{self.name}' [{self.id}] (current state is {vm_state})") if vm_state in ("running", "paused"): - if self.on_close == "save_vm_state": # add a guest property to know the VM has been saved await self.manager.execute("guestproperty", ["set", self._uuid, "SavedByGNS3", "yes"]) diff --git a/gns3server/compute/vmware/__init__.py b/gns3server/compute/vmware/__init__.py index f19e36613..03119d214 100644 --- a/gns3server/compute/vmware/__init__.py +++ b/gns3server/compute/vmware/__init__.py @@ -42,7 +42,6 @@ from gns3server.compute.vmware.vmware_error import VMwareError class VMware(BaseManager): - _NODE_CLASS = VMwareVM def __init__(self): diff --git a/gns3server/compute/vmware/vmware_error.py b/gns3server/compute/vmware/vmware_error.py index 1a4ec0cf3..88a3f4a94 100644 --- a/gns3server/compute/vmware/vmware_error.py +++ b/gns3server/compute/vmware/vmware_error.py @@ -22,5 +22,4 @@ from ..error import NodeError class VMwareError(NodeError): - pass diff --git a/gns3server/compute/vmware/vmware_vm.py b/gns3server/compute/vmware/vmware_vm.py index 1e0fa3b69..d36074f0b 100644 --- a/gns3server/compute/vmware/vmware_vm.py +++ b/gns3server/compute/vmware/vmware_vm.py @@ -42,7 +42,6 @@ log = logging.getLogger(__name__) class VMwareVM(BaseNode): - """ VMware VM implementation. """ @@ -257,7 +256,6 @@ class VMwareVM(BaseNode): # first some sanity checks for adapter_number in range(0, self._adapters): - # we want the vmnet interface to be connected when starting the VM connected = f"ethernet{adapter_number}.startConnected" if self._get_vmx_setting(connected): @@ -271,7 +269,6 @@ class VMwareVM(BaseNode): self.manager.refresh_vmnet_list(ubridge=use_ubridge) # then configure VMware network adapters for adapter_number in range(0, self._adapters): - custom_adapter = self._get_custom_adapter_settings(adapter_number) adapter_type = custom_adapter.get("adapter_type", self._adapter_type) @@ -365,7 +362,9 @@ class VMwareVM(BaseNode): await self._add_ubridge_ethernet_connection(vnet, vmnet_interface, block_host_traffic) else: # special case on macOS, we cannot bind VMnet interfaces using the libpcap - await self._ubridge_send('bridge add_nio_fusion_vmnet {name} "{interface}"'.format(name=vnet, interface=vmnet_interface)) + await self._ubridge_send( + 'bridge add_nio_fusion_vmnet {name} "{interface}"'.format(name=vnet, interface=vmnet_interface) + ) else: await self._add_ubridge_ethernet_connection(vnet, vmnet_interface, block_host_traffic) diff --git a/gns3server/compute/vpcs/__init__.py b/gns3server/compute/vpcs/__init__.py index 90953dd0a..d9073a193 100644 --- a/gns3server/compute/vpcs/__init__.py +++ b/gns3server/compute/vpcs/__init__.py @@ -27,7 +27,6 @@ from .vpcs_vm import VPCSVM class VPCS(BaseManager): - _NODE_CLASS = VPCSVM def __init__(self): diff --git a/gns3server/compute/vpcs/vpcs_error.py b/gns3server/compute/vpcs/vpcs_error.py index 0fb6f5a91..bde63c3ad 100644 --- a/gns3server/compute/vpcs/vpcs_error.py +++ b/gns3server/compute/vpcs/vpcs_error.py @@ -22,5 +22,4 @@ from ..error import NodeError class VPCSError(NodeError): - pass diff --git a/gns3server/config.py b/gns3server/config.py index 278432358..0baf2fe2f 100644 --- a/gns3server/config.py +++ b/gns3server/config.py @@ -110,20 +110,30 @@ class Config: self._files = [] if self._main_config_file is None: - if not os.path.exists(versioned_user_dir): # Try to migrate the configuration files and database from the previous version if it exists previous_version = f"{__version_info__[0]}.{int(__version_info__[1]) - 1}" if self._profile: - previous_versioned_user_dir = os.path.join(home, ".config", appname, previous_version, "profiles", self._profile) + previous_versioned_user_dir = os.path.join( + home, ".config", appname, previous_version, "profiles", self._profile + ) else: previous_versioned_user_dir = os.path.join(home, ".config", appname, previous_version) if os.path.exists(previous_versioned_user_dir): try: - shutil.copytree(previous_versioned_user_dir, versioned_user_dir, symlinks=True, ignore_dangling_symlinks=True) - log.info(f"Migrated configuration files and database from '{previous_versioned_user_dir}' to '{versioned_user_dir}'") + shutil.copytree( + previous_versioned_user_dir, + versioned_user_dir, + symlinks=True, + ignore_dangling_symlinks=True, + ) + log.info( + f"Migrated configuration files and database from '{previous_versioned_user_dir}' to '{versioned_user_dir}'" + ) except OSError as e: - log.error(f"Cannot migrate old config files and database from '{previous_versioned_user_dir}: {e}") + log.error( + f"Cannot migrate old config files and database from '{previous_versioned_user_dir}: {e}" + ) os.makedirs(versioned_user_dir, exist_ok=True) try: diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index b1e16c87b..debc6960a 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -202,12 +202,12 @@ class Controller: if computes: for c in computes: try: - #FIXME: Task exception was never retrieved + # FIXME: Task exception was never retrieved await self.add_compute( compute_id=str(c.compute_id), connect=False, **c.dict(exclude_unset=True, exclude={"compute_id", "created_at", "updated_at"}), - ssl_context=self._ssl_context + ssl_context=self._ssl_context, ) except (ControllerError, KeyError): pass # Skip not available servers at loading @@ -222,11 +222,9 @@ class Controller: # start to auto open projects (if configured) 5 seconds after the controller has started self._project_auto_open_task_handle = asyncio.get_event_loop().call_later( - 5, - lambda: asyncio.create_task(self._project_auto_open()) + 5, lambda: asyncio.create_task(self._project_auto_open()) ) - def _create_ssl_context(self, server_config): import ssl @@ -303,11 +301,10 @@ class Controller: controller_vars = { "appliances_etag": self._appliance_manager.appliances_etag, "iou_license_check": self._iou_license_settings["license_check"], - "version": __version__ + "version": __version__, } if self._iou_license_settings["iourc_content"]: - server_config = Config.instance().settings.Server os.makedirs(server_config.secrets_dir, exist_ok=True) iourc_path = os.path.join(server_config.secrets_dir, "iou_license") @@ -321,7 +318,7 @@ class Controller: try: os.makedirs(os.path.dirname(self._vars_file), exist_ok=True) - with open(self._vars_file, 'w+') as f: + with open(self._vars_file, "w+") as f: json.dump(controller_vars, f, indent=4, sort_keys=True) except OSError as e: log.error(f"Cannot write controller vars file '{self._vars_file}': {e}") @@ -357,15 +354,20 @@ class Controller: # IOU license check is disabled by default self._iou_license_settings["license_check"] = controller_vars.get("iou_license_check", False) - log.info("IOU license check is {} on the controller".format("enabled" if self._iou_license_settings["license_check"] else "disabled")) + log.info( + "IOU license check is {} on the controller".format( + "enabled" if self._iou_license_settings["license_check"] else "disabled" + ) + ) # install the built-in appliances if needed if Config.instance().settings.Server.install_builtin_appliances: previous_version = controller_vars.get("version") log.info("Comparing controller version {} with config version {}".format(__version__, previous_version)) builtin_appliances_path = self._appliance_manager.builtin_appliances_path() - if not previous_version or \ - parse_version(__version__.split("+")[0]) > parse_version(previous_version.split("+")[0]): + if not previous_version or parse_version(__version__.split("+")[0]) > parse_version( + previous_version.split("+")[0] + ): await self._appliance_manager.install_builtin_appliances() elif not os.listdir(builtin_appliances_path): await self._appliance_manager.install_builtin_appliances() @@ -467,7 +469,9 @@ class Controller: self._projects_scan_handle.cancel() loop = self._projects_monitor_loop or asyncio.get_running_loop() - self._projects_scan_handle = loop.call_later(delay, lambda: asyncio.create_task(self._scan_projects_directory())) + self._projects_scan_handle = loop.call_later( + delay, lambda: asyncio.create_task(self._scan_projects_directory()) + ) async def _scan_projects_directory(self): """ @@ -488,7 +492,6 @@ class Controller: except Exception as e: log.warning(f"Projects directory rescan failed: {e}") - @staticmethod async def install_resource_files(dst_path, resource_name, upgrade_resources=True): """ @@ -496,6 +499,7 @@ class Controller: """ installed_resources = [] + async def should_copy(src, dst, upgrade_resources): if not os.path.exists(dst): return True @@ -512,7 +516,7 @@ class Controller: if not os.path.exists(os.path.join(dst_path, filename)): shutil.copy(os.path.join(resource_path, filename), os.path.join(dst_path, filename)) else: - for entry in importlib_resources.files('gns3server').joinpath(resource_name).iterdir(): + for entry in importlib_resources.files("gns3server").joinpath(resource_name).iterdir(): full_path = os.path.join(dst_path, entry.name) if entry.is_file() and await should_copy(str(entry), full_path, upgrade_resources): log.debug(f'Installing {resource_name} resource file "{entry.name}" to "{full_path}"') @@ -604,7 +608,6 @@ class Controller: """ if compute_id not in self._computes: - # We disallow to create from the outside the local and VM server if (compute_id == "local" or compute_id == "vm") and not force: return None @@ -799,10 +802,7 @@ class Controller: project = self._projects[topo_data["project_id"]] else: project = await self.add_project( - path=os.path.dirname(path), - status="closed", - filename=os.path.basename(path), - **topo_data + path=os.path.dirname(path), status="closed", filename=os.path.basename(path), **topo_data ) if load or project.auto_open: await project.open() diff --git a/gns3server/controller/appliance.py b/gns3server/controller/appliance.py index fa964abfa..10d958983 100644 --- a/gns3server/controller/appliance.py +++ b/gns3server/controller/appliance.py @@ -22,7 +22,6 @@ log = logging.getLogger(__name__) class Appliance: - def __init__(self, path, data, builtin=True): self._data = data.copy() diff --git a/gns3server/controller/appliance_manager.py b/gns3server/controller/appliance_manager.py index e411b9852..3bf0a302c 100644 --- a/gns3server/controller/appliance_manager.py +++ b/gns3server/controller/appliance_manager.py @@ -116,6 +116,7 @@ class ApplianceManager: dst_path = self.builtin_appliances_path() log.info(f"Installing built-in appliances in '{dst_path}'") from . import Controller + try: await Controller.instance().install_resource_files(dst_path, "appliances") except OSError as e: @@ -135,12 +136,7 @@ class ApplianceManager: return appliances async def _download_image( - self, - image_dir: str, - image_name: str, - image_type: str, - image_url: str, - images_repo: ImagesRepository + self, image_dir: str, image_name: str, image_type: str, image_url: str, images_repo: ImagesRepository ) -> None: """ Download an image. @@ -152,7 +148,9 @@ class ApplianceManager: async with HTTPClient.get(image_url) as response: if response.status != 200: raise ControllerError(f"Could not download '{image_name}' due to HTTP error code {response.status}") - await write_image(image_name, image_path, response.content.iter_any(), images_repo, allow_raw_image=True) + await write_image( + image_name, image_path, response.content.iter_any(), images_repo, allow_raw_image=True + ) except (OSError, InvalidImageError) as e: raise ControllerError(f"Could not save {image_type} image '{image_path}': {e}") except ClientError as e: @@ -161,11 +159,7 @@ class ApplianceManager: raise ControllerError(f"Timeout while downloading '{image_name}' from '{image_url}'") async def _find_appliance_version_images( - self, - appliance: Appliance, - version: dict, - images_repo: ImagesRepository, - image_dir: str + self, appliance: Appliance, version: dict, images_repo: ImagesRepository, image_dir: str ) -> None: """ Find all the images belonging to a specific appliance version. @@ -196,11 +190,8 @@ class ApplianceManager: direct_download_url = image.get("direct_download_url") if direct_download_url: await self._download_image( - image_dir, - appliance_file, - appliance.type, - direct_download_url, - images_repo) + image_dir, appliance_file, appliance.type, direct_download_url, images_repo + ) else: raise ControllerError(f"Could not find '{appliance_file}'") @@ -214,8 +205,8 @@ class ApplianceManager: except ValidationError as e: raise ControllerError(message=f"Could not validate template data: {e}") template = await TemplatesService(templates_repo).create_template(template_create) - #template_id = template.get("template_id") - #await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/templates/{template_id}/*") + # template_id = template.get("template_id") + # await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/templates/{template_id}/*") log.info(f"Template '{template.get('name')}' has been created") return template @@ -237,14 +228,14 @@ class ApplianceManager: return template_data async def install_appliances_from_image( - self, - image_path: str, - image_checksum: str, - images_repo: ImagesRepository, - templates_repo: TemplatesRepository, - rbac_repo: RbacRepository, - current_user: schemas.User, - image_dir: str + self, + image_path: str, + image_checksum: str, + images_repo: ImagesRepository, + templates_repo: TemplatesRepository, + rbac_repo: RbacRepository, + current_user: schemas.User, + image_dir: str, ) -> List[dict]: """ Install appliances using an image checksum. @@ -262,11 +253,13 @@ class ApplianceManager: ApplianceModel.model_validate(appliance.asdict()) except ValidationError as e: log.warning(f"Could not validate appliance '{appliance.id}': {e}") - results.append({ - "status": "skipped", - "name": appliance.name, - "reason": f"could not validate appliance '{appliance.id}': {e}", - }) + results.append( + { + "status": "skipped", + "name": appliance.name, + "reason": f"could not validate appliance '{appliance.id}': {e}", + } + ) continue if appliance.versions: for version in appliance.versions: @@ -282,37 +275,45 @@ class ApplianceManager: # duplicates when the appliance version differs, but two # templates sharing a name is never what the user asked for here log.warning(f"Template '{name}' already exists, skipping automatic template creation") - results.append({ - "status": "skipped", - "name": name, - "reason": f"a template named '{name}' already exists", - }) + results.append( + { + "status": "skipped", + "name": name, + "reason": f"a template named '{name}' already exists", + } + ) continue - template = await self._create_template(template_data, templates_repo, rbac_repo, current_user) - results.append({ - "status": "created", - "template_id": str(template.get("template_id")), - "name": template.get("name"), - "version": template.get("version"), - "template_type": template.get("template_type"), - }) + template = await self._create_template( + template_data, templates_repo, rbac_repo, current_user + ) + results.append( + { + "status": "created", + "template_id": str(template.get("template_id")), + "name": template.get("name"), + "version": template.get("version"), + "template_type": template.get("template_type"), + } + ) except (ControllerError, InvalidImageError) as e: log.warning(f"Could not automatically create template using image '{image_path}': {e}") - results.append({ - "status": "skipped", - "name": appliance.name, - "reason": str(e), - }) + results.append( + { + "status": "skipped", + "name": appliance.name, + "reason": str(e), + } + ) return results async def install_appliance( - self, - appliance_id: UUID, - version: str, - images_repo: ImagesRepository, - templates_repo: TemplatesRepository, - rbac_repo: RbacRepository, - current_user: schemas.User + self, + appliance_id: UUID, + version: str, + images_repo: ImagesRepository, + templates_repo: TemplatesRepository, + rbac_repo: RbacRepository, + current_user: schemas.User, ) -> None: """ Install a new appliance @@ -335,11 +336,15 @@ class ApplianceManager: for appliance_version_info in appliance.versions: if appliance_version_info.get("name") == version: try: - template_type = ApplianceToTemplate().get_template_type(appliance.asdict(), appliance_version_info) + template_type = ApplianceToTemplate().get_template_type( + appliance.asdict(), appliance_version_info + ) if template_type != "docker": # docker appliances have no image files to find or download image_dir = default_images_directory(template_type) - await self._find_appliance_version_images(appliance, appliance_version_info, images_repo, image_dir) + await self._find_appliance_version_images( + appliance, appliance_version_info, images_repo, image_dir + ) except InvalidImageError as e: raise ControllerError(message=f"Image error: {e}") template_data = await self._appliance_to_template(appliance, appliance_version_info) @@ -350,8 +355,9 @@ class ApplianceManager: else: if appliance.versions: # TODO: install appliance versions based on available images - raise ControllerBadRequestError(message=f"Selecting a version is required to install " - f"appliance '{appliance_id}'") + raise ControllerBadRequestError( + message=f"Selecting a version is required to install appliance '{appliance_id}'" + ) template_data = await self._appliance_to_template(appliance) return await self._create_template(template_data, templates_repo, rbac_repo, current_user) @@ -383,7 +389,9 @@ class ApplianceManager: json_data = appliance.asdict() # Check if loaded without error if appliance.status != "broken": # Validate using discriminated union - automatically routes to correct version - log.debug(f"Validating appliance '{appliance.id}' with registry version {appliance.registry_version}") + log.debug( + f"Validating appliance '{appliance.id}' with registry version {appliance.registry_version}" + ) ApplianceModel.model_validate(json_data) self._appliances[appliance.id] = appliance if not appliance.symbol or appliance.symbol.startswith(":/symbols/"): diff --git a/gns3server/controller/appliance_to_template.py b/gns3server/controller/appliance_to_template.py index 3602a7a7e..b848e56d8 100644 --- a/gns3server/controller/appliance_to_template.py +++ b/gns3server/controller/appliance_to_template.py @@ -315,8 +315,7 @@ class ApplianceToTemplate: return settings_list[0] raise ControllerError( - f"Appliance '{appliance_config['name']}' has multiple settings " - f"but none is marked as default" + f"Appliance '{appliance_config['name']}' has multiple settings but none is marked as default" ) def _merge_v8_properties(self, settings, appliance_config): diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index a418f92a6..6c22d16b2 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -40,7 +40,7 @@ from ..controller.controller_error import ( ControllerTimeoutError, ControllerUnauthorizedError, ComputeError, - ComputeConflictError + ComputeConflictError, ) from ..version import __version__, __version_info__ @@ -483,10 +483,7 @@ class Compute: self._controller.notification.controller_emit("compute.updated", self.asdict()) else: await self._controller.notification.dispatch( - action, - event, - project_id=project_id, - compute_id=self.id + action, event, project_id=project_id, compute_id=self.id ) else: if response.type == aiohttp.WSMsgType.CLOSE: @@ -514,6 +511,7 @@ class Compute: self._controller.notification.controller_emit("compute.updated", self.asdict()) # Try to reconnect after 1 second if server unavailable only if not during tests (otherwise we create a resources usage bomb) from gns3server.api.server import app + if not app.state.exiting and not hasattr(sys, "_called_from_test"): log.info(f"Reconnecting to compute '{self._id}' WebSocket '{ws_url}'") asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(self.connect())) @@ -534,7 +532,7 @@ class Compute: return f"{self._protocol}://{host}:{self._port}/v3/compute{path}" def get_url(self, path): - """ Returns URL for specific path at Compute""" + """Returns URL for specific path at Compute""" return self._getUrl(path) async def _run_http_query(self, method, path, data=None, timeout=120, raw=False, stream=False, params=None): @@ -567,7 +565,14 @@ class Compute: try: log.debug(f"Attempting request to compute: {method} {url} {headers}") response = await self._session().request( - method, url, headers=headers, data=data, auth=self._auth, params=params, chunked=chunked, timeout=timeout + method, + url, + headers=headers, + data=data, + auth=self._auth, + params=params, + chunked=chunked, + timeout=timeout, ) except asyncio.TimeoutError: raise ComputeError(f"Timeout error for {method} call to {url} after {timeout}s") @@ -627,7 +632,7 @@ class Compute: raise HTTPException( status_code=response.status, detail=f"HTTP error {response.status} received from compute " - f"'{self.name}' for request {method} {path}: {msg}" + f"'{self.name}' for request {method} {path}: {msg}", ) if body and len(body): diff --git a/gns3server/controller/export_project.py b/gns3server/controller/export_project.py index 89b5a8140..080d3675c 100644 --- a/gns3server/controller/export_project.py +++ b/gns3server/controller/export_project.py @@ -81,7 +81,7 @@ async def export_project( keep_compute_ids, allow_all_nodes, temporary_dir, - reset_mac_addresses + reset_mac_addresses, ) # Export the local files @@ -198,14 +198,7 @@ def _is_exportable(path, include_snapshots=False): async def _patch_project_file( - project, - path, - zstream, - include_images, - keep_compute_ids, - allow_all_nodes, - temporary_dir, - reset_mac_addresses + project, path, zstream, include_images, keep_compute_ids, allow_all_nodes, temporary_dir, reset_mac_addresses ): """ Patch a project file (.gns3) to export a project. @@ -242,7 +235,6 @@ async def _patch_project_file( if "properties" in node: for prop, value in node["properties"].items(): - # reset the MAC address if reset_mac_addresses and prop in ("mac_addr", "mac_address"): node["properties"][prop] = None diff --git a/gns3server/controller/gns3vm/hyperv_gns3_vm.py b/gns3server/controller/gns3vm/hyperv_gns3_vm.py index 50b053f64..7ec969223 100644 --- a/gns3server/controller/gns3vm/hyperv_gns3_vm.py +++ b/gns3server/controller/gns3vm/hyperv_gns3_vm.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) class HyperVGNS3VM(BaseGNS3VM): - _HYPERV_VM_STATE_ENABLED = 2 _HYPERV_VM_STATE_DISABLED = 3 _HYPERV_VM_STATE_SHUTDOWN = 4 diff --git a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py index 97f309a15..f3a78f03d 100644 --- a/gns3server/controller/gns3vm/virtualbox_gns3_vm.py +++ b/gns3server/controller/gns3vm/virtualbox_gns3_vm.py @@ -135,8 +135,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM): result = await self._execute("showvminfo", [self._vmname, "--machinereadable"]) for info in result.splitlines(): - if '=' in info: - name, value = info.split('=', 1) + if "=" in info: + name, value = info.split("=", 1) if name == "{}{}".format(backend_type, interface_number): return value.strip('"') return None @@ -188,7 +188,9 @@ class VirtualBoxGNS3VM(BaseGNS3VM): lower_ip = str(interface.ip + 2) upper_ip = str(subnet.network_address + subnet.num_addresses - 2) except ValueError: - raise GNS3VMError("Invalid IP address and netmask for vboxnet {}: {}/{}".format(vboxnet, ip_address, netmask)) + raise GNS3VMError( + "Invalid IP address and netmask for vboxnet {}: {}/{}".format(vboxnet, ip_address, netmask) + ) dhcp_server_args = [ "add", @@ -197,7 +199,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM): "--netmask={}".format(netmask), "--lower-ip={}".format(lower_ip), "--upper-ip={}".format(upper_ip), - "--enable" + "--enable", ] await self._execute("dhcpserver", dhcp_server_args) @@ -249,7 +251,6 @@ class VirtualBoxGNS3VM(BaseGNS3VM): return True return False - async def list(self): """ List all VirtualBox VMs @@ -271,16 +272,24 @@ class VirtualBoxGNS3VM(BaseGNS3VM): # get a NAT interface number nat_interface_number = await self._look_for_interface("nat") if nat_interface_number < 0 and await self._look_for_interface("natnetwork") < 0: - raise GNS3VMError(f'VM "{self.vmname}" must have a NAT interface or NAT Network configured in order to start') + raise GNS3VMError( + f'VM "{self.vmname}" must have a NAT interface or NAT Network configured in order to start' + ) - if sys.platform.startswith("darwin") and parse_version(self._system_properties["API version"]) >= parse_version("7_0"): + if sys.platform.startswith("darwin") and parse_version(self._system_properties["API version"]) >= parse_version( + "7_0" + ): # VirtualBox 7.0+ on macOS requires a host-only network interface backend_type = "hostonly-network" backend_description = "host-only network" vboxnet_type = "hostonlynets" interface_number = await self._look_for_interface("hostonlynetwork") if interface_number < 0: - raise GNS3VMError('VM "{}" must have a network adapter attached to a host-only network in order to start'.format(self.vmname)) + raise GNS3VMError( + 'VM "{}" must have a network adapter attached to a host-only network in order to start'.format( + self.vmname + ) + ) else: backend_type = "hostonlyadapter" backend_description = "host-only adapter" @@ -288,11 +297,19 @@ class VirtualBoxGNS3VM(BaseGNS3VM): interface_number = await self._look_for_interface("hostonly") if interface_number < 0: - raise GNS3VMError('VM "{}" must have a network adapter attached to a {} in order to start'.format(self.vmname, backend_description)) + raise GNS3VMError( + 'VM "{}" must have a network adapter attached to a {} in order to start'.format( + self.vmname, backend_description + ) + ) vboxnet = await self._look_for_vboxnet(backend_type, interface_number) if vboxnet is None: - raise GNS3VMError('A VirtualBox host-only network could not be found on network adapter {} for "{}"'.format(interface_number, self._vmname)) + raise GNS3VMError( + 'A VirtualBox host-only network could not be found on network adapter {} for "{}"'.format( + interface_number, self._vmname + ) + ) if not (await self._check_vboxnet_exists(vboxnet, vboxnet_type)): if sys.platform.startswith("win") and vboxnet == "vboxnet0": @@ -300,24 +317,30 @@ class VirtualBoxGNS3VM(BaseGNS3VM): # on Windows. Try to patch this with the first available vboxnet we find. first_available_vboxnet = await self._find_first_available_vboxnet() if first_available_vboxnet is None: - raise GNS3VMError('Please add a VirtualBox host-only network with DHCP enabled and attached it to network adapter {} for "{}"'.format(interface_number, self._vmname)) + raise GNS3VMError( + 'Please add a VirtualBox host-only network with DHCP enabled and attached it to network adapter {} for "{}"'.format( + interface_number, self._vmname + ) + ) await self.set_hostonly_network(interface_number, first_available_vboxnet) vboxnet = first_available_vboxnet else: try: await self._execute("hostonlyif", ["create"]) except GNS3VMError: - raise GNS3VMError('VirtualBox host-only network "{}" does not exist and could not be automatically created, please make the sure the network adapter {} configuration is valid for "{}"'.format( - vboxnet, - interface_number, - self._vmname - )) + raise GNS3VMError( + 'VirtualBox host-only network "{}" does not exist and could not be automatically created, please make the sure the network adapter {} configuration is valid for "{}"'.format( + vboxnet, interface_number, self._vmname + ) + ) if backend_type == "hostonlyadapter" and not (await self._check_dhcp_server(vboxnet)): try: await self._add_dhcp_server(vboxnet) except GNS3VMError as e: - raise GNS3VMError("Could not add DHCP server for vboxnet {}: {}, please configure manually".format(vboxnet, e)) + raise GNS3VMError( + "Could not add DHCP server for vboxnet {}: {}, please configure manually".format(vboxnet, e) + ) vm_state = await self._get_state() log.info(f'"{self._vmname}" state is {vm_state}') @@ -363,12 +386,24 @@ class VirtualBoxGNS3VM(BaseGNS3VM): if await self._check_vbox_port_forwarding(): # delete the GNS3VM NAT port forwarding rule if it exists log.info("Removing GNS3VM NAT port forwarding rule from interface {}".format(nat_interface_number)) - await self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number), "delete", "GNS3VM"]) + await self._execute( + "controlvm", [self._vmname, "natpf{}".format(nat_interface_number), "delete", "GNS3VM"] + ) # add a GNS3VM NAT port forwarding rule to redirect 127.0.0.1 with random port to the port in the VM - log.info("Adding GNS3VM NAT port forwarding rule with port {} to interface {}".format(api_port, nat_interface_number)) - await self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number), - "GNS3VM,tcp,{},{},,{}".format(ip_address, api_port, self.port)]) + log.info( + "Adding GNS3VM NAT port forwarding rule with port {} to interface {}".format( + api_port, nat_interface_number + ) + ) + await self._execute( + "controlvm", + [ + self._vmname, + "natpf{}".format(nat_interface_number), + "GNS3VM,tcp,{},{},,{}".format(ip_address, api_port, self.port), + ], + ) self.ip_address = await self._get_ip_from_server(interface_number, api_port) @@ -384,8 +419,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM): while remaining_try > 0: result = await self._execute("guestproperty", ["get", self._vmname, "/VirtualBox/GuestInfo/Net/0/V4/IP"]) for info in result.splitlines(): - if ':' in info: - name, value = info.split(':', 1) + if ":" in info: + name, value = info.split(":", 1) if name == "Value": return value.strip() remaining_try -= 1 diff --git a/gns3server/controller/import_project.py b/gns3server/controller/import_project.py index e5da5460c..f26c747e2 100644 --- a/gns3server/controller/import_project.py +++ b/gns3server/controller/import_project.py @@ -41,16 +41,16 @@ Handle the import of project from a .gns3project async def import_project( - controller, - project_id, - stream, - location=None, - name=None, - reset_mac_addresses=False, - keep_compute_ids=False, - auto_start=False, - auto_open=False, - auto_close=True, + controller, + project_id, + stream, + location=None, + name=None, + reset_mac_addresses=False, + keep_compute_ids=False, + auto_start=False, + auto_open=False, + auto_close=True, ): """ Import a project contain in a zip file @@ -135,7 +135,9 @@ async def import_project( else: # Round-robin through available compute resources. # Only use computes that are connected to avoid import failures - available_computes = {compute_id: compute for compute_id, compute in controller.computes.items() if compute.connected} + available_computes = { + compute_id: compute for compute_id, compute in controller.computes.items() if compute.connected + } if available_computes: compute_nodes = itertools.cycle(available_computes) for node in topology["topology"]["nodes"]: @@ -258,6 +260,7 @@ def regenerate_topology_ids(topology, new_project_path, reset_mac_addresses=Fals for drawing in topology["topology"]["drawings"]: drawing["drawing_id"] = str(uuid.uuid4()) + def _move_node_file(path, old_id, new_id): """ Move a file from a node when changing its id @@ -284,7 +287,7 @@ async def _move_files_to_compute(compute, project_id, directory, files_path): location = os.path.join(directory, files_path) if os.path.exists(location): - for (dirpath, dirnames, filenames) in os.walk(location, followlinks=False): + for dirpath, dirnames, filenames in os.walk(location, followlinks=False): for filename in filenames: path = os.path.join(dirpath, filename) if os.path.islink(path): @@ -314,7 +317,7 @@ async def _import_images(controller, images_path): image_dir = controller.images_path() root = images_path - for (dirpath, dirnames, filenames) in os.walk(root, followlinks=False): + for dirpath, dirnames, filenames in os.walk(root, followlinks=False): for filename in filenames: path = os.path.join(dirpath, filename) dst = os.path.join(image_dir, os.path.relpath(path, root)) @@ -326,11 +329,12 @@ async def _import_images(controller, images_path): # read the first 7 bytes of the file. elf_header_start = f.read(7) # IOU images must start with the ELF magic number, be 32-bit or 64-bit, little endian and have an ELF version of 1 - if elf_header_start == b'\x7fELF\x01\x01\x01' or elf_header_start == b'\x7fELF\x02\x01\x01': + if elf_header_start == b"\x7fELF\x01\x01\x01" or elf_header_start == b"\x7fELF\x02\x01\x01": os.chmod(dst, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC) except OSError as e: continue + async def update_snapshots(snapshots_dir, project_path, project_name, project_id, reset_mac_addresses=True): """ Load the snapshots and update their project name and project ID to be the same as the main project. @@ -342,7 +346,6 @@ async def update_snapshots(snapshots_dir, project_path, project_name, project_id continue snapshot_path = os.path.join(snapshots_dir, snapshot) with tempfile.TemporaryDirectory(dir=snapshots_dir) as tmpdir: - # extract everything to a temporary directory try: with open(snapshot_path, "rb") as f: diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py index 5f68e484b..72c6552d5 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -23,7 +23,11 @@ import html from .controller_error import ControllerError, ControllerNotFoundError from gns3server.agent.web_wireshark.manager import WebWiresharkManager from gns3server.config import Config -from gns3server.utils.packet_filter_validation import validate_all_filters, filter_inactive_filters, FilterValidationError +from gns3server.utils.packet_filter_validation import ( + validate_all_filters, + filter_inactive_filters, + FilterValidationError, +) import logging @@ -164,7 +168,7 @@ class Link: """ Get whether to show filters icon in Web UI """ - return getattr(self, '_show_filters_icon', True) + return getattr(self, "_show_filters_icon", True) @property def project(self): @@ -438,14 +442,13 @@ class Link: jwt_token=jwt_token, memory=webwireshark_config.memory, cpus=webwireshark_config.cpus, - pids_limit=webwireshark_config.pids_limit + pids_limit=webwireshark_config.pids_limit, ) # Send notification - self._project.emit_notification("link.web_wireshark_started", { - "link_id": self.id, - "ws_url": result.get("ws_url", result.get("url")) - }) + self._project.emit_notification( + "link.web_wireshark_started", {"link_id": self.id, "ws_url": result.get("ws_url", result.get("url"))} + ) log.info(f"Web Wireshark started for link {self.id}: {result.get('ws_url')}") self._wireshark = True @@ -466,10 +469,7 @@ class Link: try: log.info(f"Stopping Web Wireshark for link {self.id}") - await manager.stop_wireshark_session( - project_id=self._project.id, - link_id=self.id - ) + await manager.stop_wireshark_session(project_id=self._project.id, link_id=self.id) log.info(f"Web Wireshark stopped for link {self.id}") self._wireshark = False @@ -494,15 +494,12 @@ class Link: log.info(f"Restarting Web Wireshark for link {self.id}") result = await manager.restart_wireshark_session( - project_id=self._project.id, - link_id=self.id, - jwt_token=jwt_token + project_id=self._project.id, link_id=self.id, jwt_token=jwt_token ) - self._project.emit_notification("link.web_wireshark_started", { - "link_id": self.id, - "ws_url": result.get("ws_url", result.get("url")) - }) + self._project.emit_notification( + "link.web_wireshark_started", {"link_id": self.id, "ws_url": result.get("ws_url", result.get("url"))} + ) log.info(f"Web Wireshark restarted for link {self.id}: {result.get('ws_url')}") @@ -660,7 +657,7 @@ class Link: "markers": self._persist_markers(), "link_style": self._link_style, "suspend": self._suspended, - "show_filters_icon": getattr(self, '_show_filters_icon', True), + "show_filters_icon": getattr(self, "_show_filters_icon", True), } result = { "nodes": res, @@ -676,6 +673,6 @@ class Link: "suspend": self._suspended, "link_style": self._link_style, "wireshark": self._wireshark, - "show_filters_icon": getattr(self, '_show_filters_icon', True), + "show_filters_icon": getattr(self, "_show_filters_icon", True), } return result diff --git a/gns3server/controller/marker_replay.py b/gns3server/controller/marker_replay.py index a3b9366e3..f5996c159 100644 --- a/gns3server/controller/marker_replay.py +++ b/gns3server/controller/marker_replay.py @@ -124,10 +124,10 @@ class _SharkdRpcError(Exception): # magic → (byte order, timestamp unit). Both pcap families uBridge can write # (libpcap default µs; ns variant accepted defensively) and both endiannesses. _PCAP_MAGICS = { - 0xA1B2C3D4: ("<", 1), # little-endian, microseconds - 0xD4C3B2A1: (">", 1), # big-endian, microseconds - 0xA1B23C4D: ("<", 1000), # little-endian, nanoseconds - 0x4D3CB2A1: (">", 1000), # big-endian, nanoseconds + 0xA1B2C3D4: ("<", 1), # little-endian, microseconds + 0xD4C3B2A1: (">", 1), # big-endian, microseconds + 0xA1B23C4D: ("<", 1000), # little-endian, nanoseconds + 0x4D3CB2A1: (">", 1000), # big-endian, nanoseconds } @@ -174,9 +174,7 @@ def scan_pcap_frames(path): pos = 24 while pos + 16 <= len(data): - ts_sec, ts_frac, incl_len, _orig_len = struct.unpack( - byteorder + "IIII", data[pos:pos + 16] - ) + ts_sec, ts_frac, incl_len, _orig_len = struct.unpack(byteorder + "IIII", data[pos : pos + 16]) if incl_len > 0xFFFF or pos + 16 + incl_len > len(data): break # truncated tail (snapshot mid-write / torn final record) # Normalize ns pcaps to µs by truncation — uBridge writes µs anyway. @@ -197,7 +195,7 @@ def read_frame_bytes(path, frame_number): return None # Offset arithmetic mirrors the header scan: global header + every full # record before the target + the target's own record header. - offset = 24 + sum(16 + incl for _s, _u, incl in frames[:frame_number - 1]) + 16 + offset = 24 + sum(16 + incl for _s, _u, incl in frames[: frame_number - 1]) + 16 incl_len = frames[frame_number - 1][2] with open(path, "rb") as f: f.seek(offset) @@ -213,10 +211,7 @@ def read_frame_bytes(path, frame_number): # (/etc/wireshark &c.), so the column indexes in _columns_for are a contract # we own rather than an environment default. Exactly the four columns the # frame entries consume — nothing else rides along in every page. -_PINNED_COLUMNS = ( - 'gui.column.format: "Source", "%s", "Destination", "%d", ' - '"Protocol", "%p", "Info", "%i"\n' -) +_PINNED_COLUMNS = 'gui.column.format: "Source", "%s", "Destination", "%d", "Protocol", "%p", "Info", "%i"\n' async def _prepare_scratch(pcap): @@ -262,13 +257,13 @@ def _engine_env(scratch_dir): # semantics live in VALUES (field names, labels, filter expressions), which # are never touched. _KEY_RENAME = { - "t": "element", # node type ("proto", …) - "l": "label", # display text - "fn": "name", # field name (e.g. "ip.ttl") - "f": "filter_expr", # ready-made display filter with the value baked in - "s": "expert", # expert severity name ("Chat", "Warn", …) - "g": "generated", # generated-by-wireshark flag - "n": "children", # nested fields + "t": "element", # node type ("proto", …) + "l": "label", # display text + "fn": "name", # field name (e.g. "ip.ttl") + "f": "filter_expr", # ready-made display filter with the value baked in + "s": "expert", # expert severity name ("Chat", "Warn", …) + "g": "generated", # generated-by-wireshark flag + "n": "children", # nested fields } # "h" → pos + size (byte range for hex highlighting) — handled specially. # "e" is sharkd's internal header-field registry id — unstable across @@ -308,6 +303,7 @@ def _count_tree_nodes(value): # sharkd sessions (one resident process per source pcap) # --------------------------------------------------------------------------- + class _SharkdSession: """A resident `sharkd -` process with one pcap loaded, addressed through line-oriented JSON-RPC. Serialized by an asyncio lock (sharkd serves one @@ -425,8 +421,7 @@ class _SharkdManager: async def _acquire(self, pcap): if shutil.which("sharkd") is None: raise SharkdMissingError( - "sharkd is not available on this server — marker replay requires sharkd " - "(part of the Wireshark package)" + "sharkd is not available on this server — marker replay requires sharkd (part of the Wireshark package)" ) try: stat = os.stat(pcap) @@ -492,9 +487,12 @@ class _SharkdManager: scratch_dir, scratch = await _prepare_scratch(pcap) try: proc = await asyncio.create_subprocess_exec( - "sharkd", "-", - stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.DEVNULL, env=_engine_env(scratch_dir), + "sharkd", + "-", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL, + env=_engine_env(scratch_dir), limit=_STREAM_LIMIT_BYTES, ) except OSError as e: @@ -541,6 +539,7 @@ async def close_sessions(): # Columns + display filter (sharkd `frames` RPC) # --------------------------------------------------------------------------- + async def _columns_for(pcap, filter_expr): """ One resident `frames` pass over the source pcap. Returns @@ -596,6 +595,7 @@ async def _columns_for(pcap, filter_expr): # Tag gate + timeline assembly # --------------------------------------------------------------------------- + def _tag_markers(project, tag): """ Every marker entry in the project carrying ``tag`` (flat @@ -606,13 +606,15 @@ def _tag_markers(project, tag): for key, info in project.markers.items(): if info.get("tag") == tag: link_id, _, name = key.partition("/") - entries.append({ - "node_id": info["node_id"], - "link_id": link_id, - "marker": name, - "enabled": info.get("enabled", True), - "data_link_type": info.get("data_link_type", "DLT_EN10MB"), - }) + entries.append( + { + "node_id": info["node_id"], + "link_id": link_id, + "marker": name, + "enabled": info.get("enabled", True), + "data_link_type": info.get("data_link_type", "DLT_EN10MB"), + } + ) return entries @@ -659,13 +661,12 @@ async def _merged_frames(project, entries, filter_expr=None, link_id=None): merged = [] sources = [] for entry in entries: - pcap = os.path.join( - markers_dir, f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}.pcap" - ) + pcap = os.path.join(markers_dir, f"{entry['node_id']}_{entry['link_id']}_{entry['marker']}.pcap") frames = scan_pcap_frames(pcap) if os.path.exists(pcap) else [] # Inventory first: every source, engine-free totals. - sources.append({**{k: entry[k] for k in ("node_id", "link_id", "marker", "data_link_type")}, - "count": len(frames)}) + sources.append( + {**{k: entry[k] for k in ("node_id", "link_id", "marker", "data_link_type")}, "count": len(frames)} + ) if link_id and entry["link_id"] != link_id: continue # link narrows the stream before any engine work if frames: @@ -677,22 +678,24 @@ async def _merged_frames(project, entries, filter_expr=None, link_id=None): if filter_expr is not None and frame_number not in columns: continue cols = columns.get(frame_number, {}) - merged.append({ - "ts": _format_ts(sec, usec), - "ts_us": sec * 1_000_000 + usec, - "_source": source_key, - "len": incl_len, - "node_id": entry["node_id"], - "link_id": entry["link_id"], - "marker": entry["marker"], - "frame_number": frame_number, - "src": cols.get("src"), - "dst": cols.get("dst"), - "proto": cols.get("proto"), - "info": cols.get("info"), - "bg": cols.get("bg"), - "fg": cols.get("fg"), - }) + merged.append( + { + "ts": _format_ts(sec, usec), + "ts_us": sec * 1_000_000 + usec, + "_source": source_key, + "len": incl_len, + "node_id": entry["node_id"], + "link_id": entry["link_id"], + "marker": entry["marker"], + "frame_number": frame_number, + "src": cols.get("src"), + "dst": cols.get("dst"), + "proto": cols.get("proto"), + "info": cols.get("info"), + "bg": cols.get("bg"), + "fg": cols.get("fg"), + } + ) merged.sort(key=lambda f: (f["ts_us"], f["_source"], f["frame_number"])) for frame in merged: del frame["ts_us"] @@ -702,9 +705,7 @@ async def _merged_frames(project, entries, filter_expr=None, link_id=None): def _validate_filter(filter_expr): if filter_expr is not None and len(filter_expr) > FILTER_MAX_LENGTH: - raise ControllerBadRequestError( - f"Display filter too long (max {FILTER_MAX_LENGTH} characters)" - ) + raise ControllerBadRequestError(f"Display filter too long (max {FILTER_MAX_LENGTH} characters)") async def build_timeline(project, tag, filter_expr=None, link_id=None): @@ -746,13 +747,14 @@ async def query_frames(project, tag, ts, window_ms=100, limit=1000, filter_expr= start_us = _parse_ts(ts) end_us = start_us + max(window_ms, 0) * 1000 hits = [f for f in frames if start_us <= _parse_ts(f["ts"]) <= end_us] - return {"frames": hits[:max(limit, 0)]} + return {"frames": hits[: max(limit, 0)]} # --------------------------------------------------------------------------- # Frame detail (lazy — one frame per call, via the resident session) # --------------------------------------------------------------------------- + async def decode_frame(project, tag, ts, node_id, link_id, marker, frame_number=None): """ Decode exactly one frame: locate its pcap by source identity, verify the @@ -770,29 +772,23 @@ async def decode_frame(project, tag, ts, node_id, link_id, marker, frame_number= entries = gate_tag(project, tag) entry = next( - (e for e in entries - if e["node_id"] == node_id and e["link_id"] == link_id and e["marker"] == marker), + (e for e in entries if e["node_id"] == node_id and e["link_id"] == link_id and e["marker"] == marker), None, ) if entry is None: - raise ControllerNotFoundError( - f"No marker '{marker}' with tag {tag} on link {link_id} captured by {node_id}" - ) + raise ControllerNotFoundError(f"No marker '{marker}' with tag {tag} on link {link_id} captured by {node_id}") pcap = os.path.join(project.markers_directory, f"{node_id}_{link_id}_{marker}.pcap") if not os.path.exists(pcap): raise ControllerNotFoundError(f"No capture file for marker '{marker}' (nothing ever matched)") frames = scan_pcap_frames(pcap) - rebuilt_message = ( - f"No frame at ts {ts} in marker '{marker}' (the capture may have been rebuilt)" - ) + rebuilt_message = f"No frame at ts {ts} in marker '{marker}' (the capture may have been rebuilt)" if frame_number is None: # The ts must be the exact string the timeline returned; find the frame # it identifies rather than trusting any position hint from the client. frame_number = next( - (i for i, (sec, usec, _len) in enumerate(frames, start=1) - if _format_ts(sec, usec) == ts), + (i for i, (sec, usec, _len) in enumerate(frames, start=1) if _format_ts(sec, usec) == ts), None, ) if frame_number is None: @@ -820,8 +816,7 @@ async def decode_frame(project, tag, ts, node_id, link_id, marker, frame_number= tree = _rename_value(result.get("tree", [])) return { "ts": ts, - "source": {"node_id": node_id, "link_id": link_id, "marker": marker, - "frame_number": frame_number}, + "source": {"node_id": node_id, "link_id": link_id, "marker": marker, "frame_number": frame_number}, "field_count": _count_tree_nodes(tree), "hex": raw_hex, "tree": tree, diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 9cbfd9292..dac44f60d 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -21,12 +21,7 @@ import copy import uuid import os -from .controller_error import ( - ControllerError, - ControllerTimeoutError, - ComputeError, - ComputeConflictError -) +from .controller_error import ControllerError, ControllerTimeoutError, ComputeError, ComputeConflictError from .node_types import BUILTIN_NODE_TYPES from .ports.port_factory import PortFactory, StandardPortFactory, DynamipsPortFactory from ..utils.images import images_directories @@ -540,7 +535,10 @@ class Node: raise log.warning( "Could not provide missing image '%s' for node '%s' [%s]: %s", - last_missing_image, self._name, self._id, upload_error + last_missing_image, + self._name, + self._id, + upload_error, ) if not res: if allow_missing_image: @@ -554,7 +552,9 @@ class Node: self._missing_images = missing_images log.warning( "Node '%s' [%s] is kept in degraded state, missing image(s): %s", - self._name, self._id, ", ".join(m["image"] for m in self._missing_images) + self._name, + self._id, + ", ".join(m["image"] for m in self._missing_images), ) return False raise e @@ -571,7 +571,9 @@ class Node: self._missing_images = self._compute_missing_images(last_missing_image) log.warning( "Node '%s' [%s] could not be created, missing image(s): %s", - self._name, self._id, ", ".join(m["image"] for m in self._missing_images) + self._name, + self._id, + ", ".join(m["image"] for m in self._missing_images), ) elif last_missing_error is not None: # Uploading appeared to succeed, but the compute rejected every @@ -655,18 +657,13 @@ class Node: if prop is None: fallback_basename = os.path.basename(fallback_image) prop = next( - ( - p - for p in mapping - if properties.get(p) and os.path.basename(properties[p]) == fallback_basename - ), + (p for p in mapping if properties.get(p) and os.path.basename(properties[p]) == fallback_basename), None, ) if prop is None and mapping: prop = next(iter(mapping)) already_reported = any( - m["image"] == fallback_image or (prop is not None and m["property"] == prop) - for m in missing + m["image"] == fallback_image or (prop is not None and m["property"] == prop) for m in missing ) if not already_reported: image_type = mapping.get(prop, self._node_type) @@ -722,11 +719,7 @@ class Node: if prop == "properties": compute_properties = copy.deepcopy(kwargs[prop]) else: - if ( - prop == "name" - and self.status == "started" - and self._node_type not in BUILTIN_NODE_TYPES - ): + if prop == "name" and self.status == "started" and self._node_type not in BUILTIN_NODE_TYPES: raise ControllerError("Sorry, it is not possible to rename a node that is already powered on") setattr(self, prop, kwargs[prop]) @@ -808,7 +801,7 @@ class Node: "startup_config_content", "private_config_content", "startup_script", - "custom_adapters" + "custom_adapters", ]: if key in self._properties: del self._properties[key] @@ -866,10 +859,17 @@ class Node: # Image path fields for various node types image_path_fields = { # Common fields (IOU, Docker, etc.) - "path", "image", + "path", + "image", # QEMU-specific fields - "hda_disk_image", "hdb_disk_image", "hdc_disk_image", "hdd_disk_image", - "cdrom_image", "bios_image", "initrd", "kernel_image", + "hda_disk_image", + "hdb_disk_image", + "hdc_disk_image", + "hdd_disk_image", + "cdrom_image", + "bios_image", + "initrd", + "kernel_image", # VMware-specific fields "vmx_path", } @@ -931,7 +931,9 @@ class Node: if self.node_type == "iou": license_check = self._project.controller.iou_license.get("license_check", True) iourc_content = self._project.controller.iou_license.get("iourc_content", None) - await self.post("/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content}) + await self.post( + "/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content} + ) else: await self.post("/start", data=data, timeout=240) except asyncio.TimeoutError: @@ -1046,9 +1048,7 @@ class Node: self.project.emit_notification("log.info", {"message": f"Uploading missing image {img}"}) try: with open(image, "rb") as f: - await self._compute.post( - f"/{self._node_type}/images/{os.path.basename(img)}", data=f, timeout=None - ) + await self._compute.post(f"/{self._node_type}/images/{os.path.basename(img)}", data=f, timeout=None) except OSError as e: raise ControllerError(f"Can't upload {image}: {str(e)}") self.project.emit_notification("log.info", {"message": f"Upload finished for {img}"}) @@ -1094,23 +1094,23 @@ class Node: # compute to pull it from the Docker repository as a fallback self.project.emit_notification( "log.info", - {"message": f"Docker image '{image}' is not on the controller host, " - f"asking compute '{self._compute.name}' to pull it"} + { + "message": f"Docker image '{image}' is not on the controller host, " + f"asking compute '{self._compute.name}' to pull it" + }, ) await self._compute.post("/docker/images/pull", data={"image": image}, timeout=None) return True self.project.emit_notification( - "log.info", - {"message": f"Syncing Docker image '{image}' to compute '{self._compute.name}'"} + "log.info", {"message": f"Syncing Docker image '{image}' to compute '{self._compute.name}'"} ) try: await self._compute.post("/docker/images/load", data=response.content, timeout=None) finally: response.close() self.project.emit_notification( - "log.info", - {"message": f"Docker image '{image}' has been synced to compute '{self._compute.name}'"} + "log.info", {"message": f"Docker image '{image}' has been synced to compute '{self._compute.name}'"} ) return True @@ -1201,7 +1201,9 @@ class Node: port_name = custom_adapter_settings.get("port_name", port_name) mac_address = custom_adapter_settings.get("mac_address") if not mac_address and "mac_address" in self._properties: - mac_address = int_to_macaddress(macaddress_to_int(self._properties["mac_address"]) + adapter_number) + mac_address = int_to_macaddress( + macaddress_to_int(self._properties["mac_address"]) + adapter_number + ) port = PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name) port.mac_address = mac_address @@ -1245,33 +1247,33 @@ class Node: """ topology = { - "compute_id": str(self._compute.id), - "node_id": self._id, - "node_type": self._node_type, - "template_id": self._template_id, - "name": self._name, - "console": self._console, - "console_type": self._console_type, - "console_auto_start": self._console_auto_start, - "netmiko_device_type": self._netmiko_device_type, - "default_username": self._default_username, - "default_password": self._default_password, - "aux": self._aux, - "aux_type": self._aux_type, - "properties": self._properties, - "label": self._label, - "x": self._x, - "y": self._y, - "z": self._z, - "locked": self._locked, - "width": self._width, - "height": self._height, - "symbol": self._symbol, - "port_name_format": self._port_name_format, - "port_segment_size": self._port_segment_size, - "first_port_name": self._first_port_name, - "custom_adapters": self._custom_adapters, - "tags": self._tags, + "compute_id": str(self._compute.id), + "node_id": self._id, + "node_type": self._node_type, + "template_id": self._template_id, + "name": self._name, + "console": self._console, + "console_type": self._console_type, + "console_auto_start": self._console_auto_start, + "netmiko_device_type": self._netmiko_device_type, + "default_username": self._default_username, + "default_password": self._default_password, + "aux": self._aux, + "aux_type": self._aux_type, + "properties": self._properties, + "label": self._label, + "x": self._x, + "y": self._y, + "z": self._z, + "locked": self._locked, + "width": self._width, + "height": self._height, + "symbol": self._symbol, + "port_name_format": self._port_name_format, + "port_segment_size": self._port_segment_size, + "first_port_name": self._first_port_name, + "custom_adapters": self._custom_adapters, + "tags": self._tags, } if topology_dump: diff --git a/gns3server/controller/node_types.py b/gns3server/controller/node_types.py index 4c4683c62..98263ee54 100644 --- a/gns3server/controller/node_types.py +++ b/gns3server/controller/node_types.py @@ -21,11 +21,13 @@ Shared node type constants used across the controller. # Node types that are always running (builtin/virtual switch nodes). # These nodes do not require explicit start/stop and have limited feature support. -BUILTIN_NODE_TYPES = frozenset({ - "cloud", - "nat", - "ethernet_switch", - "ethernet_hub", - "frame_relay_switch", - "atm_switch", -}) \ No newline at end of file +BUILTIN_NODE_TYPES = frozenset( + { + "cloud", + "nat", + "ethernet_switch", + "ethernet_hub", + "frame_relay_switch", + "atm_switch", + } +) diff --git a/gns3server/controller/ports/ethernet_port.py b/gns3server/controller/ports/ethernet_port.py index 3ae256e23..6158685af 100644 --- a/gns3server/controller/ports/ethernet_port.py +++ b/gns3server/controller/ports/ethernet_port.py @@ -18,12 +18,10 @@ Ethernet port for Ethernet link end points. """ - from .port import Port class EthernetPort(Port): - """ Ethernet port. """ diff --git a/gns3server/controller/ports/port.py b/gns3server/controller/ports/port.py index 52e5fe4c0..9d45cbda1 100644 --- a/gns3server/controller/ports/port.py +++ b/gns3server/controller/ports/port.py @@ -83,7 +83,7 @@ class Port: # If port name format has changed we use the port name as the short name (1.X behavior) if self._short_name: return self._short_name - elif self._name and '/' in self._name: + elif self._name and "/" in self._name: return self._name.replace(self.long_name_type(), self.short_name_type()) elif self._name and self._name.startswith(f"{self.long_name_type()}{self._interface_number}"): return self.short_name_type() + f"{self._interface_number}" diff --git a/gns3server/controller/ports/port_factory.py b/gns3server/controller/ports/port_factory.py index 9aa0f98e1..c17434578 100644 --- a/gns3server/controller/ports/port_factory.py +++ b/gns3server/controller/ports/port_factory.py @@ -66,7 +66,6 @@ class StandardPortFactory: ethernet_adapters = properties.get("adapters", 1) for adapter_number in range(adapter_number, ethernet_adapters + adapter_number): - custom_adapter_settings = {} if custom_adapters: for custom_adapter in custom_adapters: diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 24511d40b..00906bc42 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -657,9 +657,7 @@ class Project: return node @open_required - async def add_node( - self, compute, name, node_id, dump=True, node_type=None, allow_missing_image=False, **kwargs - ): + async def add_node(self, compute, name, node_id, dump=True, node_type=None, allow_missing_image=False, **kwargs): """ Create a node or return an existing node @@ -846,10 +844,7 @@ class Project: try: await link.update_filters(link_data["filters"]) except ControllerError as e: - log.warning( - "Dropping invalid filters on link %s: %s", - link_data.get("link_id"), e - ) + log.warning("Dropping invalid filters on link %s: %s", link_data.get("link_id"), e) # Restore traffic-insight markers directly into link state (mirrors how # filters are restored via update_filters). The capture_node_id persisted # last time is reused for NIO routing; no side resolution is possible here @@ -865,7 +860,9 @@ class Project: if not result.get("valid"): log.warning( "Dropping marker %s on link %s: invalid BPF (%s)", - name, link_data.get("link_id"), result.get("error") + name, + link_data.get("link_id"), + result.get("error"), ) continue link._markers[name] = { @@ -935,7 +932,9 @@ class Project: if not result.get("valid"): log.warning( "Dropping marker %s on link %s: invalid BPF (%s)", - name, link_data.get("link_id"), result.get("error") + name, + link_data.get("link_id"), + result.get("error"), ) continue link._markers[name] = { @@ -996,7 +995,9 @@ class Project: link._deferred = True log.info( "Project '%s' [%s]: deferring link %s until missing image(s) are resolved", - self._name, self._id, link.id, + self._name, + self._id, + link.id, ) return None # Apply project-level marker definitions onto the link's memory @@ -1151,7 +1152,8 @@ class Project: self._marker_definitions[name]["paused"] = True marker_name = f"global-{name}" affected = [ - link for link in self._links.values() + link + for link in self._links.values() if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name ] await self._marker_apply_concurrently( @@ -1170,7 +1172,8 @@ class Project: self._marker_definitions[name]["paused"] = False marker_name = f"global-{name}" affected = [ - link for link in self._links.values() + link + for link in self._links.values() if marker_name in link.markers and link.markers[marker_name].get("inherited_from") == name ] await self._marker_apply_concurrently( @@ -1199,9 +1202,7 @@ class Project: """ result = validate_bpf_syntax(bpf) if not result.get("valid"): - raise ControllerError( - f"Marker definition '{name}': invalid BPF — {result.get('error', 'unknown error')}" - ) + raise ControllerError(f"Marker definition '{name}': invalid BPF — {result.get('error', 'unknown error')}") def _validate_marker_definition_direction(self, name, direction): """ @@ -1223,7 +1224,9 @@ class Project: "For a capture-node-relative direction on a single link, use a per-link marker." ) - async def create_marker_definition(self, name, bpf, tag=None, direction=None, color=None, highlight_duration=None, data_link_type="DLT_EN10MB"): + async def create_marker_definition( + self, name, bpf, tag=None, direction=None, color=None, highlight_duration=None, data_link_type="DLT_EN10MB" + ): """ Create a project-level marker definition and fan out to every existing link that has a capable node. Links without a capable node are silently @@ -1231,26 +1234,32 @@ class Project: """ if name in self._marker_definitions: - raise ControllerError( - f"Marker definition '{name}' already exists in this project" - ) + raise ControllerError(f"Marker definition '{name}' already exists in this project") self._validate_marker_definition_bpf(name, bpf) self._validate_marker_definition_direction(name, direction) - self._marker_definitions[name] = {"bpf": bpf, "tag": tag, "direction": direction, "color": color, "highlight_duration": highlight_duration, "data_link_type": data_link_type, "paused": False} + self._marker_definitions[name] = { + "bpf": bpf, + "tag": tag, + "direction": direction, + "color": color, + "highlight_duration": highlight_duration, + "data_link_type": data_link_type, + "paused": False, + } await self._apply_def_to_all_links(name) self.dump() self.emit_notification("project.updated", self.asdict()) - async def update_marker_definition(self, name, bpf=None, tag=None, direction=_UNSET, color=None, highlight_duration=None, data_link_type=_UNSET): + async def update_marker_definition( + self, name, bpf=None, tag=None, direction=_UNSET, color=None, highlight_duration=None, data_link_type=_UNSET + ): """ Update a marker definition and sync every inherited copy on every link. """ if name not in self._marker_definitions: - raise ControllerNotFoundError( - f"Marker definition '{name}' not found in this project" - ) + raise ControllerNotFoundError(f"Marker definition '{name}' not found in this project") d = self._marker_definitions[name] if bpf is not None: @@ -1270,9 +1279,9 @@ class Project: # Links that currently carry an inherited copy of this definition. affected = [ - link for link in self._links.values() - if f"global-{name}" in link.markers - and link.markers[f"global-{name}"].get("inherited_from") == name + link + for link in self._links.values() + if f"global-{name}" in link.markers and link.markers[f"global-{name}"].get("inherited_from") == name ] if data_link_type is not _UNSET: @@ -1291,9 +1300,15 @@ class Project: for link in affected: try: await link.update_marker( - f"global-{name}", bpf=d["bpf"], tag=d.get("tag"), direction=d.get("direction"), - color=d.get("color"), highlight_duration=d.get("highlight_duration"), inherited=True, - dump=False, memory_only=True + f"global-{name}", + bpf=d["bpf"], + tag=d.get("tag"), + direction=d.get("direction"), + color=d.get("color"), + highlight_duration=d.get("highlight_duration"), + inherited=True, + dump=False, + memory_only=True, ) except ControllerError as e: log.warning("Failed to sync marker global-%s on link %s: %s", name, link.id, e) @@ -1307,16 +1322,14 @@ class Project: """ if name not in self._marker_definitions: - raise ControllerNotFoundError( - f"Marker definition '{name}' not found in this project" - ) + raise ControllerNotFoundError(f"Marker definition '{name}' not found in this project") del self._marker_definitions[name] affected = [ - link for link in self._links.values() - if f"global-{name}" in link.markers - and link.markers[f"global-{name}"].get("inherited_from") == name + link + for link in self._links.values() + if f"global-{name}" in link.markers and link.markers[f"global-{name}"].get("inherited_from") == name ] for link in affected: try: @@ -1382,9 +1395,7 @@ class Project: ) if per_compute: - await asyncio.gather( - *[_dispatch(c, n) for c, n in per_compute.items()] - ) + await asyncio.gather(*[_dispatch(c, n) for c, n in per_compute.items()]) async def apply_defs_to_new_link(self, link): """ @@ -1403,10 +1414,7 @@ class Project: # after; per-def dumps here would be N full topology writes. await link.inherit_marker(def_name, d, dump=False) except ControllerError as e: - log.warning( - "Marker definition '%s' could not be applied to new link %s: %s", - def_name, link.id, e - ) + log.warning("Marker definition '%s' could not be applied to new link %s: %s", def_name, link.id, e) async def _marker_apply_concurrently(self, links, operation, fail_msg): """ @@ -1428,10 +1436,7 @@ class Project: if not links: return _t0 = time.time() - log.info( - "Project '%s' [%s]: fanning out marker operation to %d links...", - self._name, self._id, len(links) - ) + log.info("Project '%s' [%s]: fanning out marker operation to %d links...", self._name, self._id, len(links)) sem = asyncio.Semaphore(32) async def guarded(link): @@ -1442,10 +1447,7 @@ class Project: log.warning(fail_msg(link, e)) await asyncio.gather(*(guarded(link) for link in links)) - log.info( - "Project '%s' [%s]: marker fan-out done in %.2fs", - self._name, self._id, time.time() - _t0 - ) + log.info("Project '%s' [%s]: marker fan-out done in %.2fs", self._name, self._id, time.time() - _t0) @property def snapshots(self): @@ -1512,7 +1514,7 @@ class Project: for snapshot in self._snapshots.values(): self._snapshot_conf.append(snapshot.asdict()) try: - with open(self._snapshot_conf_path, 'w+') as f: + with open(self._snapshot_conf_path, "w+") as f: json.dump(self._snapshot_conf, f, indent=4) except OSError as e: log.error("Cannot write snapshot config '{}': {}".format(self._snapshot_conf_path, e)) @@ -1874,8 +1876,7 @@ class Project: result = validate_bpf_syntax(bpf) if not result.get("valid"): log.warning( - "Dropping marker definition '%s' on load: invalid BPF (%s)", - def_name, result.get("error") + "Dropping marker definition '%s' on load: invalid BPF (%s)", def_name, result.get("error") ) continue clean_defs[def_name] = d @@ -1986,9 +1987,7 @@ class Project: ) if per_compute: - await asyncio.gather( - *[_dispatch_batch(c, n) for c, n in per_compute.items()] - ) + await asyncio.gather(*[_dispatch_batch(c, n) for c, n in per_compute.items()]) # Finalise every link: wire node/port back-references, mark created, # notify clients, and apply project-level marker definitions. @@ -2096,12 +2095,7 @@ class Project: # Do not compress the exported project when duplicating with aiozipstream.ZipFile(compression=zipfile.ZIP_STORED) as zstream: await export_project( - zstream, - self, - tmpdir, - keep_compute_ids=True, - include_snapshots=True, - allow_all_nodes=True + zstream, self, tmpdir, keep_compute_ids=True, include_snapshots=True, allow_all_nodes=True ) # export the project to a temporary location @@ -2120,7 +2114,7 @@ class Project: f, name=name, reset_mac_addresses=reset_mac_addresses, - keep_compute_ids=True + keep_compute_ids=True, ) log.info(f"Project '{project.name}' duplicated in {time.time() - begin:.4f} seconds") @@ -2161,8 +2155,12 @@ class Project: else: new_project_path = p_work.joinpath(new_project_id) # copy dir - await wait_run_in_executor(shutil.copytree, self.path, new_project_path.as_posix(), symlinks=True, ignore_dangling_symlinks=True) - log.info("Project content copied from '{}' to '{}' in {}s".format(self.path, new_project_path, time.time() - t0)) + await wait_run_in_executor( + shutil.copytree, self.path, new_project_path.as_posix(), symlinks=True, ignore_dangling_symlinks=True + ) + log.info( + "Project content copied from '{}' to '{}' in {}s".format(self.path, new_project_path, time.time() - t0) + ) # Read the topology file using the actual filename (self._filename), not self.name # This handles the case where a project has been renamed but we need to read the actual file @@ -2181,7 +2179,7 @@ class Project: regenerate_topology_ids(topology, new_project_path, reset_mac_addresses) # dump the updated .gns3 project file - dot_gns3_path = new_project_path.joinpath('{}.gns3'.format(project_name)) + dot_gns3_path = new_project_path.joinpath("{}.gns3".format(project_name)) topology["project_id"] = new_project_id with open(dot_gns3_path, "w+") as f: json.dump(topology, f, indent=4, sort_keys=True) @@ -2359,13 +2357,7 @@ class Project: data["z"] = z data["locked"] = False # duplicated node must not be locked new_node_uuid = str(uuid.uuid4()) - new_node = await self.add_node( - node.compute, - node.name, - new_node_uuid, - node_type=node_type, - **data - ) + new_node = await self.add_node(node.compute, node.name, new_node_uuid, node_type=node_type, **data) try: await node.post("/duplicate", timeout=None, data={"destination_node_id": new_node_uuid}) except ControllerNotFoundError: diff --git a/gns3server/controller/snapshot.py b/gns3server/controller/snapshot.py index ce98dcbe0..d203830f7 100644 --- a/gns3server/controller/snapshot.py +++ b/gns3server/controller/snapshot.py @@ -42,6 +42,7 @@ FILENAME_DATETIME_FORMAT = "%d%m%y_%H%M%S" # Used to create a description of the snapshot with a human-readable date and time DESCRIPTION_DATETIME_FORMAT = "%Y-%m-%d at %H:%M:%S" + class Snapshot: """ A snapshot object @@ -69,10 +70,16 @@ class Snapshot: else: self._name = filename.rsplit("_", 2)[0] datestring = filename.replace(self._name + "_", "").split(".")[0] - self._created_at = int(datetime.strptime(datestring, FILENAME_DATETIME_FORMAT).replace(tzinfo=timezone.utc).timestamp()) + self._created_at = int( + datetime.strptime(datestring, FILENAME_DATETIME_FORMAT).replace(tzinfo=timezone.utc).timestamp() + ) if not description: - date = datetime.fromtimestamp(self._created_at, tz=timezone.utc).replace(tzinfo=None).strftime(DESCRIPTION_DATETIME_FORMAT) + date = ( + datetime.fromtimestamp(self._created_at, tz=timezone.utc) + .replace(tzinfo=None) + .strftime(DESCRIPTION_DATETIME_FORMAT) + ) description = "Snapshot '{}' taken on {}".format(self._name, date) self._description = description @@ -149,7 +156,7 @@ class Snapshot: location=self._project.path, auto_start=self._project.auto_start, auto_open=self._project.auto_open, - auto_close=self._project.auto_close + auto_close=self._project.auto_close, ) log.info("Snapshot '{}' restored in {:.4f} seconds".format(self.name, time.time() - begin)) except (OSError, PermissionError) as e: @@ -165,5 +172,5 @@ class Snapshot: "created_at": self._created_at, "description": self._description, "filename": self._filename, - "project_id": self._project.id + "project_id": self._project.id, } diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index a71b88dd6..5048be4ce 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -35,7 +35,12 @@ from .drawing import Drawing from .node import Node from .link import Link -from gns3server.utils.hostname import is_ios_hostname_valid, is_rfc1123_hostname_valid, to_rfc1123_hostname, to_ios_hostname +from gns3server.utils.hostname import ( + is_ios_hostname_valid, + is_rfc1123_hostname_valid, + to_rfc1123_hostname, + to_ios_hostname, +) from gns3server.schemas.controller.topology import Topology from gns3server.schemas.compute.dynamips_nodes import DynamipsCreate @@ -227,7 +232,7 @@ def _convert_2_2_0(topo, topo_path): new_name = to_rfc1123_hostname(node["name"]) log.info(f"Convert node name {node['name']} to {new_name} (RFC1123)") node["name"] = new_name - if node["node_type"] in ("dynamips", "iou") and not is_ios_hostname_valid(node["name"] ): + if node["node_type"] in ("dynamips", "iou") and not is_ios_hostname_valid(node["name"]): new_name = to_ios_hostname(node["name"]) log.info(f"Convert node name {node['name']} to {new_name} (IOS)") node["name"] = new_name diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index 766958bc4..f5fdcbb9c 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -29,9 +29,17 @@ from gns3server.utils.packet_filter_validation import validate_bpf_syntax, Filte # (which has no uBridge) and "ethernet_hub" (still Dynamips-hosted, no # uBridge of its own). "ethernet_switch" hosts markers on the per-port # uBridge relays of its brctl kernel-bridge backend. -_MARKER_CAPABLE_TYPES = frozenset({ - "vpcs", "qemu", "docker", "iou", "dynamips", "cloud", "ethernet_switch", -}) +_MARKER_CAPABLE_TYPES = frozenset( + { + "vpcs", + "qemu", + "docker", + "iou", + "dynamips", + "cloud", + "ethernet_switch", + } +) log = logging.getLogger(__name__) @@ -58,7 +66,7 @@ class UDPLink(Link): def deferred(self): """Whether NIO creation is waiting for missing node images.""" return self._deferred - + def _get_node_filters(self, node1, node2): """ Determine which node gets the active filters applied. @@ -81,10 +89,14 @@ class UDPLink(Link): without an NIO rebuild. """ return { - name: {"bpf": m["bpf"], "tag": m.get("tag"), "link_id": self._id, - "direction": m.get("direction"), - "data_link_type": m.get("data_link_type", "DLT_EN10MB"), - "enabled": m.get("enabled", True)} + name: { + "bpf": m["bpf"], + "tag": m.get("tag"), + "link_id": self._id, + "direction": m.get("direction"), + "data_link_type": m.get("data_link_type", "DLT_EN10MB"), + "enabled": m.get("enabled", True), + } for name, m in self._markers.items() if m.get("capture_node_id") == node.id } @@ -183,33 +195,24 @@ class UDPLink(Link): tuples returned by :meth:`_prepare`. """ - (node1, adapter_number1, port_number1, nio_data1), \ - (node2, adapter_number2, port_number2, nio_data2) = entries + (node1, adapter_number1, port_number1, nio_data1), (node2, adapter_number2, port_number2, nio_data2) = entries # The two ends are independent once the ports and peer addresses are # known — each node talks to its own compute/uBridge with no shared # lock between them — so the two POSTs overlap. If either fails, roll # back whichever side succeeded before re-raising the first error. results = await asyncio.gather( - node1.post( - f"/adapters/{adapter_number1}/ports/{port_number1}/nio", data=nio_data1, timeout=120 - ), - node2.post( - f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=nio_data2, timeout=120 - ), + node1.post(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", data=nio_data1, timeout=120), + node2.post(f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=nio_data2, timeout=120), return_exceptions=True, ) errors = [result for result in results if isinstance(result, Exception)] if errors: cleanup = [] if not isinstance(results[0], Exception): - cleanup.append( - node1.delete(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", timeout=120) - ) + cleanup.append(node1.delete(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", timeout=120)) if not isinstance(results[1], Exception): - cleanup.append( - node2.delete(f"/adapters/{adapter_number2}/ports/{port_number2}/nio", timeout=120) - ) + cleanup.append(node2.delete(f"/adapters/{adapter_number2}/ports/{port_number2}/nio", timeout=120)) if cleanup: await asyncio.gather(*cleanup, return_exceptions=True) raise errors[0] @@ -320,7 +323,9 @@ class UDPLink(Link): ), data=data, ) - await super().start_capture(data_link_type=data_link_type, capture_file_name=capture_file_name, wireshark=wireshark, jwt_token=jwt_token) + await super().start_capture( + data_link_type=data_link_type, capture_file_name=capture_file_name, wireshark=wireshark, jwt_token=jwt_token + ) async def stop_capture(self): """ @@ -378,10 +383,7 @@ class UDPLink(Link): # Prefer started. for node in self._nodes: - if ( - node["node"].node_type in _MARKER_CAPABLE_TYPES - and node["node"].status == "started" - ): + if node["node"].node_type in _MARKER_CAPABLE_TYPES and node["node"].status == "started": return node # Accept stopped but capable (marker rides NIO, applied at start). @@ -389,10 +391,7 @@ class UDPLink(Link): if node["node"].node_type in _MARKER_CAPABLE_TYPES: return node - raise ControllerError( - "Cannot add marker because no device on this link supports " - "traffic insight" - ) + raise ControllerError("Cannot add marker because no device on this link supports traffic insight") def _node_by_id(self, node_id): """ @@ -415,9 +414,7 @@ class UDPLink(Link): f"marker — no uBridge bridge to attach the filter to" ) return node - raise ControllerNotFoundError( - f"Node {node_id} is not an endpoint of link {self._id}" - ) + raise ControllerNotFoundError(f"Node {node_id} is not an endpoint of link {self._id}") async def node_updated(self, node): """ @@ -431,7 +428,21 @@ class UDPLink(Link): # explicitly deletes a marker via the REST API, and a marker is torn # down automatically only when its link is deleted. - async def start_marker(self, name, bpf, tag=None, direction=None, data_link_type="DLT_EN10MB", capture_node_id=None, color=None, highlight_duration=None, enabled=True, inherited_from=None, dump=True, memory_only=False): + async def start_marker( + self, + name, + bpf, + tag=None, + direction=None, + data_link_type="DLT_EN10MB", + capture_node_id=None, + color=None, + highlight_duration=None, + enabled=True, + inherited_from=None, + dump=True, + memory_only=False, + ): """ Attach a traffic-insight marker to this link. @@ -571,7 +582,19 @@ class UDPLink(Link): if dump: self._project.dump() - async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=_UNSET, color=None, highlight_duration=None, inherited=False, dump=True, memory_only=False): + async def update_marker( + self, + name, + bpf=None, + tag=None, + enabled=None, + direction=_UNSET, + color=None, + highlight_duration=None, + inherited=False, + dump=True, + memory_only=False, + ): """ Update an existing marker's fields and push to uBridge fine-grained — no full NIO reapply, so sibling markers' pcaps stay open. bpf/tag/direction diff --git a/gns3server/core/tasks.py b/gns3server/core/tasks.py index 0b745a067..427fc2dd8 100644 --- a/gns3server/core/tasks.py +++ b/gns3server/core/tasks.py @@ -76,8 +76,7 @@ async def startup(app: FastAPI) -> None: # to give it a chance to process API requests global auto_discover_images_task_handle auto_discover_images_task_handle = asyncio.get_event_loop().call_later( - 5, - lambda: asyncio.create_task(discover_images_on_filesystem(app)) + 5, lambda: asyncio.create_task(discover_images_on_filesystem(app)) ) for module in MODULES: @@ -98,6 +97,7 @@ async def startup(app: FastAPI) -> None: if MCP_AVAILABLE: from gns3server.agent.mcp import set_mcp_server_ready + set_mcp_server_ready(True) log.info("GNS3 server startup completed") diff --git a/gns3server/crash_report.py b/gns3server/crash_report.py index 66ceea1f8..34fa73b6c 100644 --- a/gns3server/crash_report.py +++ b/gns3server/crash_report.py @@ -53,7 +53,6 @@ if __version_info__[3] != 0: class CrashReport: - """ Report crash to a third party service """ @@ -74,10 +73,9 @@ class CrashReport: # Don't send log records as events. sentry_logging = LoggingIntegration(level=logging.INFO, event_level=None) try: - sentry_sdk.init(dsn=CrashReport.DSN, - release=__version__, - default_integrations=False, - integrations=[sentry_logging]) + sentry_sdk.init( + dsn=CrashReport.DSN, release=__version__, default_integrations=False, integrations=[sentry_logging] + ) except Exception as e: log.error("Crash report could not be sent: {}".format(e)) return @@ -88,7 +86,6 @@ class CrashReport: "os:win_32": " ".join(platform.win32_ver()), "os:mac": "{} {}".format(platform.mac_ver()[0], platform.mac_ver()[2]), "os:linux": distro.name(pretty=True), - } with sentry_sdk.configure_scope() as scope: @@ -137,7 +134,6 @@ class CrashReport: return if Config.instance().settings.Server.report_errors: - if not SENTRY_SDK_AVAILABLE: log.warning("Cannot capture exception: Sentry SDK is not available") return diff --git a/gns3server/db/models/acl.py b/gns3server/db/models/acl.py index 20df831d7..32f7f7248 100644 --- a/gns3server/db/models/acl.py +++ b/gns3server/db/models/acl.py @@ -26,7 +26,6 @@ log = logging.getLogger(__name__) class ACE(BaseTable): - __tablename__ = "acl" ace_id = Column(GUID, primary_key=True, default=generate_uuid) @@ -34,11 +33,11 @@ class ACE(BaseTable): path = Column(String) propagate = Column(Boolean, default=True) allowed = Column(Boolean, default=True) - user_id = Column(GUID, ForeignKey('users.user_id', ondelete="CASCADE")) + user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE")) user = relationship("User", back_populates="acl_entries") - group_id = Column(GUID, ForeignKey('user_groups.user_group_id', ondelete="CASCADE")) + group_id = Column(GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE")) group = relationship("UserGroup", back_populates="acl_entries") - role_id = Column(GUID, ForeignKey('roles.role_id', ondelete="CASCADE")) + role_id = Column(GUID, ForeignKey("roles.role_id", ondelete="CASCADE")) role = relationship("Role", back_populates="acl_entries") __table_args__ = ( diff --git a/gns3server/db/models/api_keys.py b/gns3server/db/models/api_keys.py index b43ecf5f2..5cc40eb0b 100644 --- a/gns3server/db/models/api_keys.py +++ b/gns3server/db/models/api_keys.py @@ -20,7 +20,6 @@ from .base import BaseTable, GUID class ApiKey(BaseTable): - __tablename__ = "api_keys" api_key_id = Column(GUID, primary_key=True) diff --git a/gns3server/db/models/base.py b/gns3server/db/models/base.py index 97bd2fcba..cf726ec79 100644 --- a/gns3server/db/models/base.py +++ b/gns3server/db/models/base.py @@ -84,7 +84,7 @@ class ListType(TypeDecorator): impl = VARCHAR cache_ok = True - def __init__(self, separator=',', *args, **kwargs): + def __init__(self, separator=",", *args, **kwargs): self._separator = separator super().__init__(*args, **kwargs) @@ -92,8 +92,7 @@ class ListType(TypeDecorator): def process_bind_param(self, value, dialect): if value is not None: if any(self._separator in str(item) for item in value): - raise ListException(f"List values cannot contain '{self._separator}'" - f"Please use a different separator.") + raise ListException(f"List values cannot contain '{self._separator}'Please use a different separator.") return self._separator.join(map(str, value)) def process_result_value(self, value, dialect): @@ -104,7 +103,6 @@ class ListType(TypeDecorator): class BaseTable(Base): - __abstract__ = True created_at = Column(DateTime, server_default=func.current_timestamp()) diff --git a/gns3server/db/models/computes.py b/gns3server/db/models/computes.py index 71043d885..dff176cda 100644 --- a/gns3server/db/models/computes.py +++ b/gns3server/db/models/computes.py @@ -21,7 +21,6 @@ from .base import BaseTable, GUID class Compute(BaseTable): - __tablename__ = "computes" compute_id = Column(GUID, primary_key=True) diff --git a/gns3server/db/models/images.py b/gns3server/db/models/images.py index 529afe4bf..9755d8470 100644 --- a/gns3server/db/models/images.py +++ b/gns3server/db/models/images.py @@ -25,12 +25,11 @@ image_template_map = Table( "image_template_map", Base.metadata, Column("image_id", Integer, ForeignKey("images.image_id", ondelete="CASCADE")), - Column("template_id", GUID, ForeignKey("templates.template_id", ondelete="CASCADE")) + Column("template_id", GUID, ForeignKey("templates.template_id", ondelete="CASCADE")), ) class Image(BaseTable): - __tablename__ = "images" image_id = Column(Integer, primary_key=True, autoincrement=True) diff --git a/gns3server/db/models/llm_model_configs.py b/gns3server/db/models/llm_model_configs.py index 77d65cbb7..778d16bbc 100644 --- a/gns3server/db/models/llm_model_configs.py +++ b/gns3server/db/models/llm_model_configs.py @@ -50,21 +50,28 @@ class LLMModelConfig(BaseTable): __table_args__ = ( # Ensure a config belongs to either a user or a group, not both CheckConstraint( - "(user_id IS NOT NULL AND group_id IS NULL) OR " - "(user_id IS NULL AND group_id IS NOT NULL)", - name="single_owner_check" + "(user_id IS NOT NULL AND group_id IS NULL) OR (user_id IS NULL AND group_id IS NOT NULL)", + name="single_owner_check", ), # Validate model_type values CheckConstraint( "model_type IN ('text', 'vision', 'stt', 'tts', 'multimodal', 'embedding', 'reranking', 'other')", - name="valid_model_type_check" + name="valid_model_type_check", ), # Each user can have at most one default config (partial unique index, PostgreSQL only) - Index("unique_user_default", "user_id", unique=True, - postgresql_where=text("is_default = TRUE AND user_id IS NOT NULL")), + Index( + "unique_user_default", + "user_id", + unique=True, + postgresql_where=text("is_default = TRUE AND user_id IS NOT NULL"), + ), # Each group can have at most one default config (partial unique index, PostgreSQL only) - Index("unique_group_default", "group_id", unique=True, - postgresql_where=text("is_default = TRUE AND group_id IS NOT NULL")), + Index( + "unique_group_default", + "group_id", + unique=True, + postgresql_where=text("is_default = TRUE AND group_id IS NOT NULL"), + ), # Indexes for efficient queries Index("idx_llm_model_configs_user_id", "user_id"), Index("idx_llm_model_configs_group_id", "group_id"), diff --git a/gns3server/db/models/pools.py b/gns3server/db/models/pools.py index dce9bf5b2..d8c011cbc 100644 --- a/gns3server/db/models/pools.py +++ b/gns3server/db/models/pools.py @@ -29,12 +29,11 @@ resource_pool_map = Table( "resource_pool_map", Base.metadata, Column("resource_id", GUID, ForeignKey("resources.resource_id", ondelete="CASCADE")), - Column("resource_pool_id", GUID, ForeignKey("resource_pools.resource_pool_id", ondelete="CASCADE")) + Column("resource_pool_id", GUID, ForeignKey("resource_pools.resource_pool_id", ondelete="CASCADE")), ) class Resource(BaseTable): - __tablename__ = "resources" resource_id = Column(GUID, primary_key=True) @@ -44,7 +43,6 @@ class Resource(BaseTable): class ResourcePool(BaseTable): - __tablename__ = "resource_pools" resource_pool_id = Column(GUID, primary_key=True, default=generate_uuid) diff --git a/gns3server/db/models/privileges.py b/gns3server/db/models/privileges.py index c446b17ee..49a904ed8 100644 --- a/gns3server/db/models/privileges.py +++ b/gns3server/db/models/privileges.py @@ -29,12 +29,11 @@ privilege_role_map = Table( "privilege_role_map", Base.metadata, Column("privilege_id", GUID, ForeignKey("privileges.privilege_id", ondelete="CASCADE")), - Column("role_id", GUID, ForeignKey("roles.role_id", ondelete="CASCADE")) + Column("role_id", GUID, ForeignKey("roles.role_id", ondelete="CASCADE")), ) class Privilege(BaseTable): - __tablename__ = "privileges" privilege_id = Column(GUID, primary_key=True, default=generate_uuid) @@ -43,210 +42,60 @@ class Privilege(BaseTable): roles = relationship("Role", secondary=privilege_role_map, back_populates="privileges") -@event.listens_for(Privilege.__table__, 'after_create') +@event.listens_for(Privilege.__table__, "after_create") def create_default_roles(target, connection, **kw): default_privileges = [ - { - "description": "Create or delete a user", - "name": "User.Allocate" - }, - { - "description": "View a user", - "name": "User.Audit" - }, - { - "description": "Update a user", - "name": "User.Modify" - }, - { - "description": "Create or delete a group", - "name": "Group.Allocate" - }, - { - "description": "View a group", - "name": "Group.Audit" - }, - { - "description": "Update a group", - "name": "Group.Modify" - }, - { - "description": "Create or delete a role", - "name": "Role.Allocate" - }, - { - "description": "View a role", - "name": "Role.Audit" - }, - { - "description": "Update a role", - "name": "Role.Modify" - }, - { - "description": "Create or delete an ACE", - "name": "ACE.Allocate" - }, - { - "description": "View an ACE", - "name": "ACE.Audit" - }, - { - "description": "Update an ACE", - "name": "ACE.Modify" - }, - { - "description": "Create or delete a resource pool", - "name": "Pool.Allocate" - }, - { - "description": "View a resource pool", - "name": "Pool.Audit" - }, - { - "description": "Update a resource pool", - "name": "Pool.Modify" - }, - { - "description": "Create or delete a template", - "name": "Template.Allocate" - }, - { - "description": "View a template", - "name": "Template.Audit" - }, - { - "description": "Update a template", - "name": "Template.Modify" - }, - { - "description": "Create or delete a project", - "name": "Project.Allocate" - }, - { - "description": "View a project", - "name": "Project.Audit" - }, - { - "description": "Update a project", - "name": "Project.Modify" - }, - { - "description": "Create or delete project snapshots", - "name": "Snapshot.Allocate" - }, - { - "description": "Restore a snapshot", - "name": "Snapshot.Restore" - }, - { - "description": "View a snapshot", - "name": "Snapshot.Audit" - }, - { - "description": "Create or delete a node", - "name": "Node.Allocate" - }, - { - "description": "View a node", - "name": "Node.Audit" - }, - { - "description": "Update a node", - "name": "Node.Modify" - }, - { - "description": "Console access to a node", - "name": "Node.Console" - }, - { - "description": "Power management for a node", - "name": "Node.PowerMgmt" - }, - { - "description": "Create or delete a link", - "name": "Link.Allocate" - }, - { - "description": "View a link", - "name": "Link.Audit" - }, - { - "description": "Update a link", - "name": "Link.Modify" - }, - { - "description": "Capture packets on a link", - "name": "Link.Capture" - }, - { - "description": "Create or delete a drawing", - "name": "Drawing.Allocate" - }, - { - "description": "View a drawing", - "name": "Drawing.Audit" - }, - { - "description": "Update a drawing", - "name": "Drawing.Modify" - }, - { - "description": "Create or delete a symbol", - "name": "Symbol.Allocate" - }, - { - "description": "View a symbol", - "name": "Symbol.Audit" - }, - { - "description": "Create or delete an image", - "name": "Image.Allocate" - }, - { - "description": "View an image", - "name": "Image.Audit" - }, - { - "description": "Create or delete a compute", - "name": "Compute.Allocate" - }, - { - "description": "Update a compute", - "name": "Compute.Modify" - }, - { - "description": "View a compute", - "name": "Compute.Audit" - }, - { - "description": "Install an appliance", - "name": "Appliance.Allocate" - }, - { - "description": "View an appliance", - "name": "Appliance.Audit" - }, - { - "description": "Create or delete an LLM model configuration", - "name": "LLMConfig.Allocate" - }, - { - "description": "View an LLM model configuration", - "name": "LLMConfig.Audit" - }, - { - "description": "Update an LLM model configuration", - "name": "LLMConfig.Modify" - }, - { - "description": "View server settings", - "name": "Server.Audit" - }, - { - "description": "Update server settings", - "name": "Server.Modify" - } + {"description": "Create or delete a user", "name": "User.Allocate"}, + {"description": "View a user", "name": "User.Audit"}, + {"description": "Update a user", "name": "User.Modify"}, + {"description": "Create or delete a group", "name": "Group.Allocate"}, + {"description": "View a group", "name": "Group.Audit"}, + {"description": "Update a group", "name": "Group.Modify"}, + {"description": "Create or delete a role", "name": "Role.Allocate"}, + {"description": "View a role", "name": "Role.Audit"}, + {"description": "Update a role", "name": "Role.Modify"}, + {"description": "Create or delete an ACE", "name": "ACE.Allocate"}, + {"description": "View an ACE", "name": "ACE.Audit"}, + {"description": "Update an ACE", "name": "ACE.Modify"}, + {"description": "Create or delete a resource pool", "name": "Pool.Allocate"}, + {"description": "View a resource pool", "name": "Pool.Audit"}, + {"description": "Update a resource pool", "name": "Pool.Modify"}, + {"description": "Create or delete a template", "name": "Template.Allocate"}, + {"description": "View a template", "name": "Template.Audit"}, + {"description": "Update a template", "name": "Template.Modify"}, + {"description": "Create or delete a project", "name": "Project.Allocate"}, + {"description": "View a project", "name": "Project.Audit"}, + {"description": "Update a project", "name": "Project.Modify"}, + {"description": "Create or delete project snapshots", "name": "Snapshot.Allocate"}, + {"description": "Restore a snapshot", "name": "Snapshot.Restore"}, + {"description": "View a snapshot", "name": "Snapshot.Audit"}, + {"description": "Create or delete a node", "name": "Node.Allocate"}, + {"description": "View a node", "name": "Node.Audit"}, + {"description": "Update a node", "name": "Node.Modify"}, + {"description": "Console access to a node", "name": "Node.Console"}, + {"description": "Power management for a node", "name": "Node.PowerMgmt"}, + {"description": "Create or delete a link", "name": "Link.Allocate"}, + {"description": "View a link", "name": "Link.Audit"}, + {"description": "Update a link", "name": "Link.Modify"}, + {"description": "Capture packets on a link", "name": "Link.Capture"}, + {"description": "Create or delete a drawing", "name": "Drawing.Allocate"}, + {"description": "View a drawing", "name": "Drawing.Audit"}, + {"description": "Update a drawing", "name": "Drawing.Modify"}, + {"description": "Create or delete a symbol", "name": "Symbol.Allocate"}, + {"description": "View a symbol", "name": "Symbol.Audit"}, + {"description": "Create or delete an image", "name": "Image.Allocate"}, + {"description": "View an image", "name": "Image.Audit"}, + {"description": "Create or delete a compute", "name": "Compute.Allocate"}, + {"description": "Update a compute", "name": "Compute.Modify"}, + {"description": "View a compute", "name": "Compute.Audit"}, + {"description": "Install an appliance", "name": "Appliance.Allocate"}, + {"description": "View an appliance", "name": "Appliance.Audit"}, + {"description": "Create or delete an LLM model configuration", "name": "LLMConfig.Allocate"}, + {"description": "View an LLM model configuration", "name": "LLMConfig.Audit"}, + {"description": "Update an LLM model configuration", "name": "LLMConfig.Modify"}, + {"description": "View server settings", "name": "Server.Audit"}, + {"description": "Update server settings", "name": "Server.Modify"}, ] stmt = target.insert().values(default_privileges) @@ -258,6 +107,7 @@ def create_default_roles(target, connection, **kw): def add_privileges_to_role(target, connection, role, privileges): from .roles import Role + roles_table = Role.__table__ privileges_table = Privilege.__table__ @@ -272,10 +122,11 @@ def add_privileges_to_role(target, connection, role, privileges): connection.execute(stmt) -@event.listens_for(privilege_role_map, 'after_create') +@event.listens_for(privilege_role_map, "after_create") def add_privileges_to_default_roles(target, connection, **kw): from .roles import Role + roles_table = Role.__table__ stmt = roles_table.select().where(roles_table.c.name == "Administrator") result = connection.execute(stmt) @@ -317,7 +168,7 @@ def add_privileges_to_default_roles(target, connection, **kw): "Appliance.Allocate", "Appliance.Audit", "LLMConfig.Audit", - "LLMConfig.Modify" + "LLMConfig.Modify", ) add_privileges_to_role(target, connection, "User", user_privileges) @@ -333,7 +184,7 @@ def add_privileges_to_default_roles(target, connection, **kw): "Symbol.Audit", "Image.Audit", "Compute.Audit", - "Appliance.Audit" + "Appliance.Audit", ) add_privileges_to_role(target, connection, "Auditor", auditor_privileges) @@ -348,7 +199,7 @@ def add_privileges_to_default_roles(target, connection, **kw): "Image.Allocate", "Image.Audit", "Appliance.Allocate", - "Appliance.Audit" + "Appliance.Audit", ) add_privileges_to_role(target, connection, "Template manager", template_manager_privileges) @@ -360,20 +211,13 @@ def add_privileges_to_default_roles(target, connection, **kw): "User.Modify", "Group.Allocate", "Group.Audit", - "Group.Modify" + "Group.Modify", ) add_privileges_to_role(target, connection, "User manager", user_manager_privileges) # add required privileges to the "ACL manager" role - acl_manager_privileges = ( - "Role.Allocate", - "Role.Audit", - "Role.Modify", - "ACE.Allocate", - "ACE.Audit", - "ACE.Modify" - ) + acl_manager_privileges = ("Role.Allocate", "Role.Audit", "Role.Modify", "ACE.Allocate", "ACE.Audit", "ACE.Modify") add_privileges_to_role(target, connection, "ACL manager", acl_manager_privileges) diff --git a/gns3server/db/models/roles.py b/gns3server/db/models/roles.py index ea02365fe..73c29dacb 100644 --- a/gns3server/db/models/roles.py +++ b/gns3server/db/models/roles.py @@ -27,7 +27,6 @@ log = logging.getLogger(__name__) class Role(BaseTable): - __tablename__ = "roles" role_id = Column(GUID, primary_key=True, default=generate_uuid) @@ -38,7 +37,7 @@ class Role(BaseTable): acl_entries = relationship("ACE") -@event.listens_for(Role.__table__, 'after_create') +@event.listens_for(Role.__table__, "after_create") def create_default_roles(target, connection, **kw): default_roles = [ @@ -48,7 +47,7 @@ def create_default_roles(target, connection, **kw): {"name": "Template manager", "description": "Role to manage templates", "is_builtin": True}, {"name": "User manager", "description": "Role to manage users and groups", "is_builtin": True}, {"name": "ACL manager", "description": "Role to manage other roles and the ACL", "is_builtin": True}, - {"name": "No Access", "description": "Role with no privileges (used to forbid access)", "is_builtin": True} + {"name": "No Access", "description": "Role with no privileges (used to forbid access)", "is_builtin": True}, ] stmt = target.insert().values(default_roles) diff --git a/gns3server/db/models/templates.py b/gns3server/db/models/templates.py index 759cc0fc8..62ea74a9e 100644 --- a/gns3server/db/models/templates.py +++ b/gns3server/db/models/templates.py @@ -24,7 +24,6 @@ from .images import image_template_map class Template(BaseTable): - __tablename__ = "templates" template_id = Column(GUID, primary_key=True, default=generate_uuid) @@ -49,7 +48,6 @@ class Template(BaseTable): class CloudTemplate(Template): - __tablename__ = "cloud_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) @@ -63,7 +61,6 @@ class CloudTemplate(Template): class DockerTemplate(Template): - __tablename__ = "docker_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) @@ -89,7 +86,6 @@ class DockerTemplate(Template): class DynamipsTemplate(Template): - __tablename__ = "dynamips_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) @@ -132,7 +128,6 @@ class DynamipsTemplate(Template): class EthernetHubTemplate(Template): - __tablename__ = "ethernet_hub_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) @@ -142,7 +137,6 @@ class EthernetHubTemplate(Template): class EthernetSwitchTemplate(Template): - __tablename__ = "ethernet_switch_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) @@ -153,7 +147,6 @@ class EthernetSwitchTemplate(Template): class IOUTemplate(Template): - __tablename__ = "iou_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) @@ -173,7 +166,6 @@ class IOUTemplate(Template): class QemuTemplate(Template): - __tablename__ = "qemu_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) @@ -220,7 +212,6 @@ class QemuTemplate(Template): class VirtualBoxTemplate(Template): - __tablename__ = "virtualbox_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) @@ -243,7 +234,6 @@ class VirtualBoxTemplate(Template): class VMwareTemplate(Template): - __tablename__ = "vmware_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) @@ -265,7 +255,6 @@ class VMwareTemplate(Template): class VPCSTemplate(Template): - __tablename__ = "vpcs_templates" template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) diff --git a/gns3server/db/models/users.py b/gns3server/db/models/users.py index 5c14c3c15..d9fc9aa29 100644 --- a/gns3server/db/models/users.py +++ b/gns3server/db/models/users.py @@ -31,12 +31,11 @@ user_group_map = Table( "user_group_map", Base.metadata, Column("user_id", GUID, ForeignKey("users.user_id", ondelete="CASCADE")), - Column("user_group_id", GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE")) + Column("user_group_id", GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE")), ) class User(BaseTable): - __tablename__ = "users" user_id = Column(GUID, primary_key=True, default=generate_uuid) @@ -52,7 +51,7 @@ class User(BaseTable): acl_entries = relationship("ACE") -@event.listens_for(User.__table__, 'after_create') +@event.listens_for(User.__table__, "after_create") def create_default_super_admin(target, connection, **kw): config = Config.instance().settings @@ -63,7 +62,7 @@ def create_default_super_admin(target, connection, **kw): username=default_admin_username, full_name="Super Administrator", hashed_password=hashed_password, - is_superadmin=True + is_superadmin=True, ) connection.execute(stmt) connection.commit() @@ -71,7 +70,6 @@ def create_default_super_admin(target, connection, **kw): class UserGroup(BaseTable): - __tablename__ = "user_groups" user_group_id = Column(GUID, primary_key=True, default=generate_uuid) @@ -81,13 +79,10 @@ class UserGroup(BaseTable): acl_entries = relationship("ACE") -@event.listens_for(UserGroup.__table__, 'after_create') +@event.listens_for(UserGroup.__table__, "after_create") def create_default_user_groups(target, connection, **kw): - default_groups = [ - {"name": "Administrators", "is_builtin": True}, - {"name": "Users", "is_builtin": True} - ] + default_groups = [{"name": "Administrators", "is_builtin": True}, {"name": "Users", "is_builtin": True}] stmt = target.insert().values(default_groups) connection.execute(stmt) diff --git a/gns3server/db/repositories/api_keys.py b/gns3server/db/repositories/api_keys.py index ae56dd37e..d43d44dd1 100644 --- a/gns3server/db/repositories/api_keys.py +++ b/gns3server/db/repositories/api_keys.py @@ -30,7 +30,6 @@ log = logging.getLogger(__name__) class ApiKeysRepository(BaseRepository): - def __init__(self, db_session: AsyncSession) -> None: super().__init__(db_session) @@ -55,11 +54,7 @@ class ApiKeysRepository(BaseRepository): return result.scalars().first() async def get_api_keys_by_user(self, user_id: UUID) -> List[models.ApiKey]: - query = ( - select(models.ApiKey) - .where(models.ApiKey.user_id == user_id) - .order_by(models.ApiKey.created_at.desc()) - ) + query = select(models.ApiKey).where(models.ApiKey.user_id == user_id).order_by(models.ApiKey.created_at.desc()) result = await self._db_session.execute(query) return list(result.scalars().all()) @@ -69,31 +64,19 @@ class ApiKeysRepository(BaseRepository): return result.scalars().first() async def revoke_api_key(self, api_key_id: UUID) -> bool: - query = ( - update(models.ApiKey) - .where(models.ApiKey.api_key_id == api_key_id) - .values(revoked=True) - ) + query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(revoked=True) result = await self._db_session.execute(query) await self._db_session.commit() return result.rowcount > 0 async def restore_api_key(self, api_key_id: UUID) -> bool: - query = ( - update(models.ApiKey) - .where(models.ApiKey.api_key_id == api_key_id) - .values(revoked=False) - ) + query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(revoked=False) result = await self._db_session.execute(query) await self._db_session.commit() return result.rowcount > 0 async def update_last_used(self, api_key_id: UUID) -> None: - query = ( - update(models.ApiKey) - .where(models.ApiKey.api_key_id == api_key_id) - .values(last_used_at=func.now()) - ) + query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(last_used_at=func.now()) await self._db_session.execute(query) await self._db_session.commit() diff --git a/gns3server/db/repositories/computes.py b/gns3server/db/repositories/computes.py index 181b92aa6..4f3aac7f4 100644 --- a/gns3server/db/repositories/computes.py +++ b/gns3server/db/repositories/computes.py @@ -27,7 +27,6 @@ from gns3server import schemas class ComputesRepository(BaseRepository): - def __init__(self, db_session: AsyncSession) -> None: super().__init__(db_session) @@ -72,9 +71,7 @@ class ComputesRepository(BaseRepository): if compute_update.password is not None: update_values["password"] = compute_update.password.get_secret_value() - query = update(models.Compute).\ - where(models.Compute.compute_id == compute_id).\ - values(update_values) + query = update(models.Compute).where(models.Compute.compute_id == compute_id).values(update_values) await self._db_session.execute(query) await self._db_session.commit() diff --git a/gns3server/db/repositories/images.py b/gns3server/db/repositories/images.py index 8f6c64dc9..c709c9820 100644 --- a/gns3server/db/repositories/images.py +++ b/gns3server/db/repositories/images.py @@ -31,7 +31,6 @@ log = logging.getLogger(__name__) class ImagesRepository(BaseRepository): - def __init__(self, db_session: AsyncSession) -> None: super().__init__(db_session) @@ -43,8 +42,9 @@ class ImagesRepository(BaseRepository): image_dir, image_name = os.path.split(image_path) if image_dir: - query = select(models.Image).\ - where(models.Image.filename == image_name, models.Image.path.endswith(image_path)) + query = select(models.Image).where( + models.Image.filename == image_name, models.Image.path.endswith(image_path) + ) else: query = select(models.Image).where(models.Image.filename == image_name) result = await self._db_session.execute(query) @@ -56,8 +56,9 @@ class ImagesRepository(BaseRepository): """ if image_dir: - query = select(models.Image).\ - where(models.Image.checksum == checksum, models.Image.path.startswith(image_dir)) + query = select(models.Image).where( + models.Image.checksum == checksum, models.Image.path.startswith(image_dir) + ) result = await self._db_session.execute(query) return result.scalars().one_or_none() else: @@ -82,9 +83,7 @@ class ImagesRepository(BaseRepository): Get all templates that an image belongs to. """ - query = select(models.Template).\ - join(models.Template.images).\ - filter(models.Image.image_id == image_id) + query = select(models.Template).join(models.Template.images).filter(models.Image.image_id == image_id) result = await self._db_session.execute(query) return result.scalars().all() @@ -101,7 +100,7 @@ class ImagesRepository(BaseRepository): image_size=image_size, path=path, checksum=checksum, - checksum_algorithm=checksum_algorithm + checksum_algorithm=checksum_algorithm, ) self._db_session.add(db_image) @@ -114,9 +113,11 @@ class ImagesRepository(BaseRepository): Update an image. """ - query = update(models.Image).\ - where(models.Image.path == image_path).\ - values(checksum=checksum, checksum_algorithm=checksum_algorithm) + query = ( + update(models.Image) + .where(models.Image.path == image_path) + .values(checksum=checksum, checksum_algorithm=checksum_algorithm) + ) await self._db_session.execute(query) await self._db_session.commit() @@ -132,9 +133,11 @@ class ImagesRepository(BaseRepository): image_dir, image_name = os.path.split(image_path) if image_dir: - query = delete(models.Image).\ - where(models.Image.filename == image_name, models.Image.path.endswith(image_path)).\ - execution_options(synchronize_session=False) + query = ( + delete(models.Image) + .where(models.Image.filename == image_name, models.Image.path.endswith(image_path)) + .execution_options(synchronize_session=False) + ) else: query = delete(models.Image).where(models.Image.filename == image_name) result = await self._db_session.execute(query) @@ -146,8 +149,7 @@ class ImagesRepository(BaseRepository): Prune images not attached to any template. """ - query = select(models.Image).\ - filter(~models.Image.templates.any()) + query = select(models.Image).filter(~models.Image.templates.any()) result = await self._db_session.execute(query) images = result.scalars().all() images_deleted = 0 diff --git a/gns3server/db/repositories/llm_model_configs.py b/gns3server/db/repositories/llm_model_configs.py index f5ed28b46..f65ca8cac 100644 --- a/gns3server/db/repositories/llm_model_configs.py +++ b/gns3server/db/repositories/llm_model_configs.py @@ -50,44 +50,36 @@ class LLMModelConfigsRepository(BaseRepository): async def get_user_config(self, config_id: UUID) -> Optional[models.LLMModelConfig]: """Get a user's LLM model configuration by ID.""" query = select(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.config_id == config_id, - models.LLMModelConfig.user_id.isnot(None) - ) + and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.user_id.isnot(None)) ) result = await self._db_session.execute(query) return result.scalars().first() async def get_user_configs(self, user_id: UUID) -> List[models.LLMModelConfig]: """Get all LLM model configurations for a user.""" - query = select(models.LLMModelConfig).where( - models.LLMModelConfig.user_id == user_id - ).order_by(models.LLMModelConfig.created_at) + query = ( + select(models.LLMModelConfig) + .where(models.LLMModelConfig.user_id == user_id) + .order_by(models.LLMModelConfig.created_at) + ) result = await self._db_session.execute(query) return result.scalars().all() async def get_user_default_config(self, user_id: UUID) -> Optional[models.LLMModelConfig]: """Get a user's default LLM model configuration.""" query = select(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.user_id == user_id, - models.LLMModelConfig.is_default - ) + and_(models.LLMModelConfig.user_id == user_id, models.LLMModelConfig.is_default) ) result = await self._db_session.execute(query) return result.scalars().first() async def create_user_config( - self, - user_id: UUID, - name: str, - model_type: str, - config_data: Dict[str, Any], - is_default: bool = False + self, user_id: UUID, name: str, model_type: str, config_data: Dict[str, Any], is_default: bool = False ) -> models.LLMModelConfig: """Create a new LLM model configuration for a user.""" # Encrypt API key if present from gns3server.utils.encryption import encrypt + config_to_store = config_data.copy() if "api_key" in config_to_store and config_to_store["api_key"]: try: @@ -105,7 +97,7 @@ class LLMModelConfigsRepository(BaseRepository): user_id=user_id, is_default=is_default, created_at=now, - updated_at=now + updated_at=now, ) self._db_session.add(db_config) await self._db_session.commit() @@ -113,11 +105,7 @@ class LLMModelConfigsRepository(BaseRepository): return db_config async def update_user_config( - self, - config_id: UUID, - user_id: UUID, - updates: Dict[str, Any], - expected_version: Optional[int] = None + self, config_id: UUID, user_id: UUID, updates: Dict[str, Any], expected_version: Optional[int] = None ) -> Optional[models.LLMModelConfig]: """ Update a user's LLM model configuration. @@ -130,10 +118,7 @@ class LLMModelConfigsRepository(BaseRepository): :raises ValueError: If version mismatch (concurrent modification) """ query = select(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.config_id == config_id, - models.LLMModelConfig.user_id == user_id - ) + and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.user_id == user_id) ) result = await self._db_session.execute(query) db_config = result.scalars().first() @@ -193,10 +178,7 @@ class LLMModelConfigsRepository(BaseRepository): async def delete_user_config(self, config_id: UUID, user_id: UUID) -> bool: """Delete a user's LLM model configuration.""" query = delete(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.config_id == config_id, - models.LLMModelConfig.user_id == user_id - ) + and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.user_id == user_id) ) result = await self._db_session.execute(query) await self._db_session.commit() @@ -208,22 +190,16 @@ class LLMModelConfigsRepository(BaseRepository): # First, unset current default await self._db_session.execute( update(models.LLMModelConfig) - .where( - and_( - models.LLMModelConfig.user_id == user_id, - models.LLMModelConfig.is_default - ) - ) + .where(and_(models.LLMModelConfig.user_id == user_id, models.LLMModelConfig.is_default)) .values(is_default=False, updated_at=now) ) # Set new default - query = update(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.config_id == config_id, - models.LLMModelConfig.user_id == user_id - ) - ).values(is_default=True, updated_at=now) + query = ( + update(models.LLMModelConfig) + .where(and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.user_id == user_id)) + .values(is_default=True, updated_at=now) + ) result = await self._db_session.execute(query) await self._db_session.commit() return result.rowcount > 0 @@ -233,44 +209,36 @@ class LLMModelConfigsRepository(BaseRepository): async def get_group_config(self, config_id: UUID) -> Optional[models.LLMModelConfig]: """Get a group's LLM model configuration by ID.""" query = select(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.config_id == config_id, - models.LLMModelConfig.group_id.isnot(None) - ) + and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.group_id.isnot(None)) ) result = await self._db_session.execute(query) return result.scalars().first() async def get_group_configs(self, group_id: UUID) -> List[models.LLMModelConfig]: """Get all LLM model configurations for a group.""" - query = select(models.LLMModelConfig).where( - models.LLMModelConfig.group_id == group_id - ).order_by(models.LLMModelConfig.created_at) + query = ( + select(models.LLMModelConfig) + .where(models.LLMModelConfig.group_id == group_id) + .order_by(models.LLMModelConfig.created_at) + ) result = await self._db_session.execute(query) return result.scalars().all() async def get_group_default_config(self, group_id: UUID) -> Optional[models.LLMModelConfig]: """Get a group's default LLM model configuration.""" query = select(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.group_id == group_id, - models.LLMModelConfig.is_default - ) + and_(models.LLMModelConfig.group_id == group_id, models.LLMModelConfig.is_default) ) result = await self._db_session.execute(query) return result.scalars().first() async def create_group_config( - self, - group_id: UUID, - name: str, - model_type: str, - config_data: Dict[str, Any], - is_default: bool = False + self, group_id: UUID, name: str, model_type: str, config_data: Dict[str, Any], is_default: bool = False ) -> models.LLMModelConfig: """Create a new LLM model configuration for a group.""" # Encrypt API key if present from gns3server.utils.encryption import encrypt + config_to_store = config_data.copy() if "api_key" in config_to_store and config_to_store["api_key"]: try: @@ -288,7 +256,7 @@ class LLMModelConfigsRepository(BaseRepository): group_id=group_id, is_default=is_default, created_at=now, - updated_at=now + updated_at=now, ) self._db_session.add(db_config) await self._db_session.commit() @@ -296,11 +264,7 @@ class LLMModelConfigsRepository(BaseRepository): return db_config async def update_group_config( - self, - config_id: UUID, - group_id: UUID, - updates: Dict[str, Any], - expected_version: Optional[int] = None + self, config_id: UUID, group_id: UUID, updates: Dict[str, Any], expected_version: Optional[int] = None ) -> Optional[models.LLMModelConfig]: """ Update a group's LLM model configuration. @@ -313,10 +277,7 @@ class LLMModelConfigsRepository(BaseRepository): :raises ValueError: If version mismatch (concurrent modification) """ query = select(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.config_id == config_id, - models.LLMModelConfig.group_id == group_id - ) + and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.group_id == group_id) ) result = await self._db_session.execute(query) db_config = result.scalars().first() @@ -376,10 +337,7 @@ class LLMModelConfigsRepository(BaseRepository): async def delete_group_config(self, config_id: UUID, group_id: UUID) -> bool: """Delete a group's LLM model configuration.""" query = delete(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.config_id == config_id, - models.LLMModelConfig.group_id == group_id - ) + and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.group_id == group_id) ) result = await self._db_session.execute(query) await self._db_session.commit() @@ -391,22 +349,16 @@ class LLMModelConfigsRepository(BaseRepository): # First, unset current default await self._db_session.execute( update(models.LLMModelConfig) - .where( - and_( - models.LLMModelConfig.group_id == group_id, - models.LLMModelConfig.is_default - ) - ) + .where(and_(models.LLMModelConfig.group_id == group_id, models.LLMModelConfig.is_default)) .values(is_default=False, updated_at=now) ) # Set new default - query = update(models.LLMModelConfig).where( - and_( - models.LLMModelConfig.config_id == config_id, - models.LLMModelConfig.group_id == group_id - ) - ).values(is_default=True, updated_at=now) + query = ( + update(models.LLMModelConfig) + .where(and_(models.LLMModelConfig.config_id == config_id, models.LLMModelConfig.group_id == group_id)) + .values(is_default=True, updated_at=now) + ) result = await self._db_session.execute(query) await self._db_session.commit() return result.rowcount > 0 @@ -414,10 +366,7 @@ class LLMModelConfigsRepository(BaseRepository): # Inheritance methods async def get_user_effective_configs( - self, - user_id: UUID, - current_user_id: Optional[UUID] = None, - current_user_is_superadmin: bool = False + self, user_id: UUID, current_user_id: Optional[UUID] = None, current_user_is_superadmin: bool = False ) -> Dict[str, Any]: """ Get user's effective configurations (own + inherited from groups). @@ -429,9 +378,7 @@ class LLMModelConfigsRepository(BaseRepository): user_configs = await self.get_user_configs(user_id) # Get user's groups - query = select(models.UserGroup).\ - join(models.UserGroup.users).\ - filter(models.User.user_id == user_id) + query = select(models.UserGroup).join(models.UserGroup.users).filter(models.User.user_id == user_id) result = await self._db_session.execute(query) user_groups = result.scalars().all() @@ -453,28 +400,8 @@ class LLMModelConfigsRepository(BaseRepository): # Always hide API key for security config_dict = self._hide_api_key(config.config) - configs_with_source.append({ - "config_id": config.config_id, - "name": config.name, - "model_type": config.model_type, - "config": config_dict, - "user_id": config.user_id, - "group_id": config.group_id, - "is_default": config.is_default, - "version": config.version, - "created_at": config.created_at, - "updated_at": config.updated_at, - "source": "user", - "group_name": None - }) - - # Add inherited group configs (always shown, regardless of user configs) - for group_id, configs in group_configs_map.items(): - for config in configs: - # Always hide API key for security - config_dict = self._hide_api_key(config.config) - - configs_with_source.append({ + configs_with_source.append( + { "config_id": config.config_id, "name": config.name, "model_type": config.model_type, @@ -485,9 +412,33 @@ class LLMModelConfigsRepository(BaseRepository): "version": config.version, "created_at": config.created_at, "updated_at": config.updated_at, - "source": "group", - "group_name": group_names_map[group_id] - }) + "source": "user", + "group_name": None, + } + ) + + # Add inherited group configs (always shown, regardless of user configs) + for group_id, configs in group_configs_map.items(): + for config in configs: + # Always hide API key for security + config_dict = self._hide_api_key(config.config) + + configs_with_source.append( + { + "config_id": config.config_id, + "name": config.name, + "model_type": config.model_type, + "config": config_dict, + "user_id": config.user_id, + "group_id": config.group_id, + "is_default": config.is_default, + "version": config.version, + "created_at": config.created_at, + "updated_at": config.updated_at, + "source": "group", + "group_name": group_names_map[group_id], + } + ) # Select default_config with proper priority: # 1. User's config marked with is_default: true @@ -508,7 +459,4 @@ class LLMModelConfigsRepository(BaseRepository): if default_config is None and configs_with_source: default_config = configs_with_source[0] - return { - "configs": configs_with_source, - "default_config": default_config - } + return {"configs": configs_with_source, "default_config": default_config} diff --git a/gns3server/db/repositories/pools.py b/gns3server/db/repositories/pools.py index 336c29c8d..90615ee68 100644 --- a/gns3server/db/repositories/pools.py +++ b/gns3server/db/repositories/pools.py @@ -32,7 +32,6 @@ log = logging.getLogger(__name__) class ResourcePoolsRepository(BaseRepository): - def __init__(self, db_session: AsyncSession) -> None: super().__init__(db_session) @@ -61,9 +60,7 @@ class ResourcePoolsRepository(BaseRepository): """ db_resource = models.Resource( - resource_id=resource.resource_id, - resource_type=resource.resource_type, - name=resource.name + resource_id=resource.resource_id, resource_type=resource.resource_type, name=resource.name ) self._db_session.add(db_resource) await self._db_session.commit() @@ -85,9 +82,11 @@ class ResourcePoolsRepository(BaseRepository): Get all resource memberships in resource pools. """ - query = select(models.ResourcePool).\ - join(models.ResourcePool.resources).\ - filter(models.Resource.resource_id == resource_id) + query = ( + select(models.ResourcePool) + .join(models.ResourcePool.resources) + .filter(models.Resource.resource_id == resource_id) + ) result = await self._db_session.execute(query) return result.scalars().all() @@ -131,18 +130,18 @@ class ResourcePoolsRepository(BaseRepository): return db_resource_pool async def update_resource_pool( - self, - resource_pool_id: UUID, - resource_pool_update: schemas.ResourcePoolUpdate + self, resource_pool_id: UUID, resource_pool_update: schemas.ResourcePoolUpdate ) -> Optional[models.ResourcePool]: """ Update a resource pool. """ update_values = resource_pool_update.model_dump(exclude_unset=True) - query = update(models.ResourcePool).\ - where(models.ResourcePool.resource_pool_id == resource_pool_id).\ - values(update_values) + query = ( + update(models.ResourcePool) + .where(models.ResourcePool.resource_pool_id == resource_pool_id) + .values(update_values) + ) await self._db_session.execute(query) await self._db_session.commit() @@ -170,17 +169,17 @@ class ResourcePoolsRepository(BaseRepository): return result.rowcount > 0 async def add_resource_to_pool( - self, - resource_pool_id: UUID, - resource: models.Resource + self, resource_pool_id: UUID, resource: models.Resource ) -> Union[None, models.ResourcePool]: """ Add a resource to a resource pool. """ - query = select(models.ResourcePool).\ - options(selectinload(models.ResourcePool.resources)).\ - where(models.ResourcePool.resource_pool_id == resource_pool_id) + query = ( + select(models.ResourcePool) + .options(selectinload(models.ResourcePool.resources)) + .where(models.ResourcePool.resource_pool_id == resource_pool_id) + ) result = await self._db_session.execute(query) resource_pool_db = result.scalars().first() if not resource_pool_db: @@ -192,17 +191,17 @@ class ResourcePoolsRepository(BaseRepository): return resource_pool_db async def remove_resource_from_pool( - self, - resource_pool_id: UUID, - resource: models.Resource + self, resource_pool_id: UUID, resource: models.Resource ) -> Union[None, models.ResourcePool]: """ Remove a resource from a resource pool. """ - query = select(models.ResourcePool).\ - options(selectinload(models.ResourcePool.resources)).\ - where(models.ResourcePool.resource_pool_id == resource_pool_id) + query = ( + select(models.ResourcePool) + .options(selectinload(models.ResourcePool.resources)) + .where(models.ResourcePool.resource_pool_id == resource_pool_id) + ) result = await self._db_session.execute(query) resource_pool_db = result.scalars().first() if not resource_pool_db: @@ -219,9 +218,11 @@ class ResourcePoolsRepository(BaseRepository): Get all resources from a resource pool. """ - query = select(models.Resource).\ - join(models.Resource.resource_pools).\ - filter(models.ResourcePool.resource_pool_id == resource_pool_id) + query = ( + select(models.Resource) + .join(models.Resource.resource_pools) + .filter(models.ResourcePool.resource_pool_id == resource_pool_id) + ) result = await self._db_session.execute(query) return result.scalars().all() diff --git a/gns3server/db/repositories/rbac.py b/gns3server/db/repositories/rbac.py index bd8f1ad9a..13bfd067c 100644 --- a/gns3server/db/repositories/rbac.py +++ b/gns3server/db/repositories/rbac.py @@ -33,7 +33,6 @@ log = logging.getLogger(__name__) class RbacRepository(BaseRepository): - def __init__(self, db_session: AsyncSession) -> None: super().__init__(db_session) @@ -43,9 +42,7 @@ class RbacRepository(BaseRepository): Get a role by its ID. """ - query = select(models.Role).\ - options(selectinload(models.Role.privileges)).\ - where(models.Role.role_id == role_id) + query = select(models.Role).options(selectinload(models.Role.privileges)).where(models.Role.role_id == role_id) result = await self._db_session.execute(query) return result.scalars().first() @@ -54,9 +51,7 @@ class RbacRepository(BaseRepository): Get a role by its name. """ - query = select(models.Role).\ - options(selectinload(models.Role.privileges)).\ - where(models.Role.name == name) + query = select(models.Role).options(selectinload(models.Role.privileges)).where(models.Role.name == name) result = await self._db_session.execute(query) return result.scalars().first() @@ -82,19 +77,13 @@ class RbacRepository(BaseRepository): await self._db_session.commit() return await self.get_role(db_role.role_id) - async def update_role( - self, - role_id: UUID, - role_update: schemas.RoleUpdate - ) -> Optional[models.Role]: + async def update_role(self, role_id: UUID, role_update: schemas.RoleUpdate) -> Optional[models.Role]: """ Update a role. """ update_values = role_update.model_dump(exclude_unset=True) - query = update(models.Role).\ - where(models.Role.role_id == role_id).\ - values(update_values) + query = update(models.Role).where(models.Role.role_id == role_id).values(update_values) await self._db_session.execute(query) await self._db_session.commit() @@ -113,18 +102,12 @@ class RbacRepository(BaseRepository): await self._db_session.commit() return result.rowcount > 0 - async def add_privilege_to_role( - self, - role_id: UUID, - privilege: models.Privilege - ) -> Union[None, models.Role]: + async def add_privilege_to_role(self, role_id: UUID, privilege: models.Privilege) -> Union[None, models.Role]: """ Add a privilege to a role. """ - query = select(models.Role).\ - options(selectinload(models.Role.privileges)).\ - where(models.Role.role_id == role_id) + query = select(models.Role).options(selectinload(models.Role.privileges)).where(models.Role.role_id == role_id) result = await self._db_session.execute(query) role_db = result.scalars().first() if not role_db: @@ -142,18 +125,12 @@ class RbacRepository(BaseRepository): await self._db_session.refresh(role_db) return role_db - async def remove_privilege_from_role( - self, - role_id: UUID, - privilege: models.Privilege - ) -> Union[None, models.Role]: + async def remove_privilege_from_role(self, role_id: UUID, privilege: models.Privilege) -> Union[None, models.Role]: """ Remove a privilege from a role. """ - query = select(models.Role).\ - options(selectinload(models.Role.privileges)).\ - where(models.Role.role_id == role_id) + query = select(models.Role).options(selectinload(models.Role.privileges)).where(models.Role.role_id == role_id) result = await self._db_session.execute(query) role_db = result.scalars().first() if not role_db: @@ -169,9 +146,7 @@ class RbacRepository(BaseRepository): Get all the role privileges. """ - query = select(models.Privilege).\ - join(models.Privilege.roles).\ - filter(models.Role.role_id == role_id) + query = select(models.Privilege).join(models.Privilege.roles).filter(models.Role.role_id == role_id) result = await self._db_session.execute(query) return result.scalars().all() @@ -237,15 +212,11 @@ class RbacRepository(BaseRepository): This method includes related user, group, and role information. """ - query = select(models.ACE).\ - where( - (models.ACE.path == path) | (models.ACE.path.startswith(path + "/")) - ).\ - options( - selectinload(models.ACE.user), - selectinload(models.ACE.group), - selectinload(models.ACE.role) - ) + query = ( + select(models.ACE) + .where((models.ACE.path == path) | (models.ACE.path.startswith(path + "/"))) + .options(selectinload(models.ACE.user), selectinload(models.ACE.group), selectinload(models.ACE.role)) + ) result = await self._db_session.execute(query) return result.scalars().all() @@ -254,8 +225,7 @@ class RbacRepository(BaseRepository): Check if an ACE exists. """ - query = select(models.ACE).\ - where(models.ACE.path == path) + query = select(models.ACE).where(models.ACE.path == path) result = await self._db_session.execute(query) return result.scalars().first() is not None @@ -271,19 +241,13 @@ class RbacRepository(BaseRepository): await self._db_session.refresh(db_ace) return db_ace - async def update_ace( - self, - ace_id: UUID, - ace_update: schemas.ACEUpdate - ) -> Optional[models.ACE]: + async def update_ace(self, ace_id: UUID, ace_update: schemas.ACEUpdate) -> Optional[models.ACE]: """ Update an ACE """ update_values = ace_update.model_dump(exclude_unset=True) - query = update(models.ACE).\ - where(models.ACE.ace_id == ace_id).\ - values(update_values) + query = update(models.ACE).where(models.ACE.ace_id == ace_id).values(update_values) await self._db_session.execute(query) await self._db_session.commit() @@ -307,9 +271,7 @@ class RbacRepository(BaseRepository): Delete all ACEs starting with path. """ - query = delete(models.ACE).\ - where(models.ACE.path.startswith(path)).\ - execution_options(synchronize_session=False) + query = delete(models.ACE).where(models.ACE.path.startswith(path)).execution_options(synchronize_session=False) result = await self._db_session.execute(query) log.debug(f"{result.rowcount} ACE(s) have been deleted") @@ -344,9 +306,11 @@ class RbacRepository(BaseRepository): for ace_path, ace_propagate, ace_allowed, ace_privilege in aces: if ace_path.startswith("/pool"): resource_pool_id = ace_path.split("/")[2] - query = select(models.Resource). \ - join(models.Resource.resource_pools). \ - filter(models.ResourcePool.resource_pool_id == resource_pool_id) + query = ( + select(models.Resource) + .join(models.Resource.resource_pools) + .filter(models.ResourcePool.resource_pool_id == resource_pool_id) + ) result = await self._db_session.execute(query) resources = result.scalars().all() @@ -366,13 +330,15 @@ class RbacRepository(BaseRepository): Retrieve all user ACEs matching the user_id and privilege name. """ - query = select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name).\ - join(models.Privilege.roles).\ - join(models.Role.acl_entries).\ - join(models.ACE.user). \ - filter(models.User.user_id == user_id).\ - filter(models.Privilege.name == privilege_name).\ - order_by(models.ACE.path.desc()) + query = ( + select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name) + .join(models.Privilege.roles) + .join(models.Role.acl_entries) + .join(models.ACE.user) + .filter(models.User.user_id == user_id) + .filter(models.Privilege.name == privilege_name) + .order_by(models.ACE.path.desc()) + ) result = await self._db_session.execute(query) return result.all() @@ -382,13 +348,15 @@ class RbacRepository(BaseRepository): Retrieve all group ACEs matching the user_id and privilege name. """ - query = select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name). \ - join(models.Privilege.roles). \ - join(models.Role.acl_entries). \ - join(models.ACE.group). \ - join(models.UserGroup.users).\ - filter(models.User.user_id == user_id). \ - filter(models.Privilege.name == privilege_name) + query = ( + select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name) + .join(models.Privilege.roles) + .join(models.Role.acl_entries) + .join(models.ACE.group) + .join(models.UserGroup.users) + .filter(models.User.user_id == user_id) + .filter(models.Privilege.name == privilege_name) + ) result = await self._db_session.execute(query) return result.all() @@ -404,12 +372,7 @@ class RbacRepository(BaseRepository): pool_resources.extend(await self._get_resources_in_pools(group_aces)) return list(set(pool_resources)) - async def get_accessible_project_ids( - self, - user_id: UUID, - privilege_name: str, - all_project_ids: List[str] - ): + async def get_accessible_project_ids(self, user_id: UUID, privilege_name: str, all_project_ids: List[str]): """ Batch check which projects a user can access via direct ACE or resource pools. Performs 3 fixed DB queries regardless of project count. @@ -448,8 +411,11 @@ class RbacRepository(BaseRepository): user_denied.add(pid) # --- User ACE: pool check --- - user_pool_ids = {ace_path.split("/")[2] for ace_path, _, ace_allowed, _ in user_aces - if ace_path.startswith("/pools/") and ace_allowed} + user_pool_ids = { + ace_path.split("/")[2] + for ace_path, _, ace_allowed, _ in user_aces + if ace_path.startswith("/pools/") and ace_allowed + } pool_accessible_ids = set() for pool_id, project_ids in pool_to_projects.items(): @@ -470,8 +436,11 @@ class RbacRepository(BaseRepository): pass # --- Group ACE: pool check --- - group_pool_ids = {ace_path.split("/")[2] for ace_path, _, ace_allowed, _ in group_aces - if ace_path.startswith("/pools/") and ace_allowed} + group_pool_ids = { + ace_path.split("/")[2] + for ace_path, _, ace_allowed, _ in group_aces + if ace_path.startswith("/pools/") and ace_allowed + } for pool_id, project_ids in pool_to_projects.items(): if pool_id in group_pool_ids: diff --git a/gns3server/db/repositories/templates.py b/gns3server/db/repositories/templates.py index a08b58053..582404301 100644 --- a/gns3server/db/repositories/templates.py +++ b/gns3server/db/repositories/templates.py @@ -47,7 +47,6 @@ TEMPLATE_TYPE_TO_MODEL = { class TemplatesRepository(BaseRepository): - def __init__(self, db_session: AsyncSession) -> None: super().__init__(db_session) @@ -57,17 +56,21 @@ class TemplatesRepository(BaseRepository): async def get_template(self, template_id: UUID) -> Union[None, models.Template]: - query = select(models.Template).\ - options(selectinload(models.Template.images)).\ - where(models.Template.template_id == template_id) + query = ( + select(models.Template) + .options(selectinload(models.Template.images)) + .where(models.Template.template_id == template_id) + ) result = await self._db_session.execute(query) return result.scalars().first() async def get_template_by_name_and_version(self, name: str, version: str) -> Union[None, models.Template]: - query = select(models.Template).\ - options(selectinload(models.Template.images)).\ - where(models.Template.name == name, models.Template.version == version) + query = ( + select(models.Template) + .options(selectinload(models.Template.images)) + .where(models.Template.name == name, models.Template.version == version) + ) result = await self._db_session.execute(query) return result.scalars().first() @@ -76,9 +79,9 @@ class TemplatesRepository(BaseRepository): Return the first template with this name, regardless of version. """ - query = select(models.Template).\ - options(selectinload(models.Template.images)).\ - where(models.Template.name == name) + query = ( + select(models.Template).options(selectinload(models.Template.images)).where(models.Template.name == name) + ) result = await self._db_session.execute(query) return result.scalars().first() @@ -115,9 +118,11 @@ class TemplatesRepository(BaseRepository): async def duplicate_template(self, template_id: UUID) -> Optional[schemas.Template]: - query = select(models.Template).\ - options(selectinload(models.Template.images)).\ - where(models.Template.template_id == template_id) + query = ( + select(models.Template) + .options(selectinload(models.Template.images)) + .where(models.Template.template_id == template_id) + ) db_template = (await self._db_session.execute(query)).scalars().first() if db_template: # duplicate db object with new primary key (template_id) @@ -136,8 +141,9 @@ class TemplatesRepository(BaseRepository): image_dir, image_name = os.path.split(image_path) if image_dir: - query = select(models.Image).\ - where(models.Image.filename == image_name, models.Image.path.endswith(image_path)) + query = select(models.Image).where( + models.Image.filename == image_name, models.Image.path.endswith(image_path) + ) else: query = select(models.Image).where(models.Image.filename == image_name) query = query.order_by(models.Image.image_id) @@ -151,18 +157,16 @@ class TemplatesRepository(BaseRepository): ) return images[0] if images else None - async def add_image_to_template( - self, - template_id: UUID, - image: models.Image - ) -> Union[None, models.Template]: + async def add_image_to_template(self, template_id: UUID, image: models.Image) -> Union[None, models.Template]: """ Add an image to template. """ - query = select(models.Template).\ - options(selectinload(models.Template.images)).\ - where(models.Template.template_id == template_id) + query = ( + select(models.Template) + .options(selectinload(models.Template.images)) + .where(models.Template.template_id == template_id) + ) result = await self._db_session.execute(query) template_in_db = result.scalars().first() if not template_in_db: @@ -173,18 +177,16 @@ class TemplatesRepository(BaseRepository): await self._db_session.refresh(template_in_db) return template_in_db - async def remove_image_from_template( - self, - template_id: UUID, - image: models.Image - ) -> Union[None, models.Template]: + async def remove_image_from_template(self, template_id: UUID, image: models.Image) -> Union[None, models.Template]: """ Remove an image from a template. """ - query = select(models.Template).\ - options(selectinload(models.Template.images)).\ - where(models.Template.template_id == template_id) + query = ( + select(models.Template) + .options(selectinload(models.Template.images)) + .where(models.Template.template_id == template_id) + ) result = await self._db_session.execute(query) template_in_db = result.scalars().first() if not template_in_db: @@ -201,8 +203,6 @@ class TemplatesRepository(BaseRepository): Return all images attached to a template. """ - query = select(models.Image).\ - join(models.Image.templates).\ - filter(models.Template.template_id == template_id) + query = select(models.Image).join(models.Image.templates).filter(models.Template.template_id == template_id) result = await self._db_session.execute(query) return result.scalars().all() diff --git a/gns3server/db/repositories/users.py b/gns3server/db/repositories/users.py index ee005b2bf..f7e098dc4 100644 --- a/gns3server/db/repositories/users.py +++ b/gns3server/db/repositories/users.py @@ -33,7 +33,6 @@ log = logging.getLogger(__name__) class UsersRepository(BaseRepository): - def __init__(self, db_session: AsyncSession) -> None: super().__init__(db_session) @@ -82,10 +81,7 @@ class UsersRepository(BaseRepository): hashed_password = self._auth_service.hash_password(user.password.get_secret_value()) db_user = models.User( - username=user.username, - email=user.email, - full_name=user.full_name, - hashed_password=hashed_password + username=user.username, email=user.email, full_name=user.full_name, hashed_password=hashed_password ) self._db_session.add(db_user) await self._db_session.commit() @@ -102,9 +98,7 @@ class UsersRepository(BaseRepository): if password: update_values["hashed_password"] = self._auth_service.hash_password(password=password.get_secret_value()) - query = update(models.User).\ - where(models.User.user_id == user_id).\ - values(update_values) + query = update(models.User).where(models.User.user_id == user_id).values(update_values) await self._db_session.execute(query) await self._db_session.commit() @@ -118,9 +112,11 @@ class UsersRepository(BaseRepository): Increment token_version to invalidate all existing tokens for the user. """ - query = update(models.User).\ - where(models.User.user_id == user_id).\ - values(token_version=models.User.token_version + 1) + query = ( + update(models.User) + .where(models.User.user_id == user_id) + .values(token_version=models.User.token_version + 1) + ) await self._db_session.execute(query) await self._db_session.commit() @@ -147,8 +143,9 @@ class UsersRepository(BaseRepository): # this is useful for manual password recovery like: # sqlite3 gns3_controller.db "UPDATE users SET hashed_password = null WHERE username = 'admin';" if user.hashed_password is None: - log.warning(f"User '{username}' has been authenticated without a password " - f"configured. Please set a new password.") + log.warning( + f"User '{username}' has been authenticated without a password configured. Please set a new password." + ) return user if not self._auth_service.verify_password(password, user.hashed_password): return None @@ -168,9 +165,7 @@ class UsersRepository(BaseRepository): Get all user memberships (user groups). """ - query = select(models.UserGroup).\ - join(models.UserGroup.users).\ - filter(models.User.user_id == user_id) + query = select(models.UserGroup).join(models.UserGroup.users).filter(models.User.user_id == user_id) result = await self._db_session.execute(query) return result.scalars().all() @@ -214,18 +209,14 @@ class UsersRepository(BaseRepository): return db_user_group async def update_user_group( - self, - user_group_id: UUID, - user_group_update: schemas.UserGroupUpdate + self, user_group_id: UUID, user_group_update: schemas.UserGroupUpdate ) -> Optional[models.UserGroup]: """ Update a user group. """ update_values = user_group_update.model_dump(exclude_unset=True) - query = update(models.UserGroup).\ - where(models.UserGroup.user_group_id == user_group_id).\ - values(update_values) + query = update(models.UserGroup).where(models.UserGroup.user_group_id == user_group_id).values(update_values) await self._db_session.execute(query) await self._db_session.commit() @@ -244,18 +235,16 @@ class UsersRepository(BaseRepository): await self._db_session.commit() return result.rowcount > 0 - async def add_member_to_user_group( - self, - user_group_id: UUID, - user: models.User - ) -> Union[None, models.UserGroup]: + async def add_member_to_user_group(self, user_group_id: UUID, user: models.User) -> Union[None, models.UserGroup]: """ Add a member to a user group. """ - query = select(models.UserGroup).\ - options(selectinload(models.UserGroup.users)).\ - where(models.UserGroup.user_group_id == user_group_id) + query = ( + select(models.UserGroup) + .options(selectinload(models.UserGroup.users)) + .where(models.UserGroup.user_group_id == user_group_id) + ) result = await self._db_session.execute(query) user_group_db = result.scalars().first() if not user_group_db: @@ -267,17 +256,17 @@ class UsersRepository(BaseRepository): return user_group_db async def remove_member_from_user_group( - self, - user_group_id: UUID, - user: models.User + self, user_group_id: UUID, user: models.User ) -> Union[None, models.UserGroup]: """ Remove a member from a user group. """ - query = select(models.UserGroup).\ - options(selectinload(models.UserGroup.users)).\ - where(models.UserGroup.user_group_id == user_group_id) + query = ( + select(models.UserGroup) + .options(selectinload(models.UserGroup.users)) + .where(models.UserGroup.user_group_id == user_group_id) + ) result = await self._db_session.execute(query) user_group_db = result.scalars().first() if not user_group_db: @@ -293,9 +282,7 @@ class UsersRepository(BaseRepository): Get all members from a user group. """ - query = select(models.User).\ - join(models.User.groups).\ - filter(models.UserGroup.user_group_id == user_group_id) + query = select(models.User).join(models.User.groups).filter(models.UserGroup.user_group_id == user_group_id) result = await self._db_session.execute(query) return result.scalars().all() diff --git a/gns3server/db/tasks.py b/gns3server/db/tasks.py index 285d95b54..8b3395156 100644 --- a/gns3server/db/tasks.py +++ b/gns3server/db/tasks.py @@ -79,7 +79,9 @@ async def connect_to_db(app: FastAPI) -> None: db_path = os.path.join(Config.instance().config_dir, "gns3_controller.db") db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}") - engine = create_async_engine(db_url, connect_args={"check_same_thread": False, "timeout": 20}, future=True, pool_size=512, max_overflow=1024) + engine = create_async_engine( + db_url, connect_args={"check_same_thread": False, "timeout": 20}, future=True, pool_size=512, max_overflow=1024 + ) # Register PRAGMA on the sync engine to ensure it fires for async connections @event.listens_for(engine.sync_engine, "connect") @@ -91,19 +93,21 @@ async def connect_to_db(app: FastAPI) -> None: # Verify WAL mode is active async with engine.connect() as _verify_conn: + def _check_wal(conn): cursor = conn.connection.cursor() cursor.execute("PRAGMA journal_mode") row = cursor.fetchone() cursor.close() return row[0] if row else "unknown" + wal_mode = await _verify_conn.run_sync(_check_wal) log.info(f"SQLite journal mode: {wal_mode}") if wal_mode and wal_mode.upper() != "WAL": log.warning("WAL mode not active - concurrent writes may cause 'database is locked' errors") alembic_cfg = config.Config() alembic_cfg.set_main_option("script_location", "gns3server:db_migrations") - #alembic_cfg.set_main_option('sqlalchemy.url', db_url) + # alembic_cfg.set_main_option('sqlalchemy.url', db_url) try: async with engine.connect() as conn: current_rev, head_rev = await conn.run_sync(check_revision, alembic_cfg) @@ -116,27 +120,27 @@ async def connect_to_db(app: FastAPI) -> None: inspector = sa.inspect(connection) tables = inspector.get_table_names() - if 'users' not in tables: - return 'new' # Truly new database + if "users" not in tables: + return "new" # Truly new database # Check for new feature columns that indicate this is already migrated - columns = [col['name'] for col in inspector.get_columns('users')] - if 'llm_model_configs' in tables: + columns = [col["name"] for col in inspector.get_columns("users")] + if "llm_model_configs" in tables: # The llm_model_configs table already exists (created from code) - return 'new_with_llm_configs' + return "new_with_llm_configs" else: # Old database without llm_model_configs table, needs migration - return 'old_needs_migration' + return "old_needs_migration" db_state = await conn.run_sync(check_db_state) - if db_state == 'new': + if db_state == "new": # Truly new database: create all tables and stamp await conn.run_sync(Base.metadata.create_all) await conn.run_sync(run_stamp, alembic_cfg) await conn.commit() log.info("Created new database and stamped to head revision") - elif db_state == 'new_with_llm_configs': + elif db_state == "new_with_llm_configs": # Database already has llm_model_configs table (from Base.metadata.create_all) # Just stamp the version await conn.run_sync(run_stamp, alembic_cfg) @@ -216,6 +220,7 @@ async def update_disk_checksums(updated_disks: List[str]) -> None: """ from gns3server.api.server import app + async with AsyncSession(app.state._db_engine) as db_session: images_repository = ImagesRepository(db_session) for path in updated_disks: @@ -226,6 +231,7 @@ async def update_disk_checksums(updated_disks: List[str]) -> None: if image.checksum != checksum: await images_repository.update_image(path, checksum, "md5") + class EventHandler(PatternMatchingEventHandler): """ Watchdog event handler. @@ -237,7 +243,7 @@ class EventHandler(PatternMatchingEventHandler): self._queue = queue # ignore temporary files, md5sum files, hidden files and directories - super().__init__(ignore_patterns=["*.tmp", "*.md5sum", ".*"], ignore_directories = True, **kwargs) + super().__init__(ignore_patterns=["*.tmp", "*.md5sum", ".*"], ignore_directories=True, **kwargs) def on_closed(self, event: FileSystemEvent) -> None: # monitor for closed files (e.g. when a file has finished to be copied) @@ -245,6 +251,7 @@ class EventHandler(PatternMatchingEventHandler): return # ignore custom IOU libraries self._loop.call_soon_threadsafe(self._queue.put_nowait, event) + class EventIterator(object): """ Watchdog Event iterator. @@ -263,13 +270,11 @@ class EventIterator(object): raise StopAsyncIteration return item + async def monitor_images_on_filesystem(app: FastAPI): def watchdog( - path: str, - queue: asyncio.Queue, - loop: asyncio.BaseEventLoop, - app: FastAPI, recursive: bool = False + path: str, queue: asyncio.Queue, loop: asyncio.BaseEventLoop, app: FastAPI, recursive: bool = False ) -> None: """ Thread to monitor a directory for new images. @@ -294,7 +299,7 @@ async def monitor_images_on_filesystem(app: FastAPI): loop = asyncio.get_event_loop() server_config = Config.instance().settings.Server image_dir = os.path.expanduser(server_config.images_path) - asyncio.get_event_loop().run_in_executor(None, watchdog,image_dir, queue, loop, app, True) + asyncio.get_event_loop().run_in_executor(None, watchdog, image_dir, queue, loop, app, True) async for filesystem_event in EventIterator(queue): # read the file system event from the queue @@ -352,7 +357,7 @@ async def get_user_llm_config_full(user_id: str, app: FastAPI) -> Optional[dict] result = await repo.get_user_effective_configs( user_uuid, current_user_id=user_uuid, # Viewing own config - current_user_is_superadmin=False + current_user_is_superadmin=False, ) if not result or not result.get("default_config"): @@ -393,7 +398,7 @@ async def get_user_llm_config_full(user_id: str, app: FastAPI) -> Optional[dict] "group_name": default_config.get("group_name"), "user_id": str(full_config.user_id) if full_config.user_id else None, "group_id": str(full_config.group_id) if full_config.group_id else None, - **config_data # provider, api_key, model, temperature, etc. + **config_data, # provider, api_key, model, temperature, etc. } # Validate required fields @@ -415,4 +420,3 @@ async def get_user_llm_config_full(user_id: str, app: FastAPI) -> Optional[dict] except Exception as e: log.error(f"Failed to retrieve LLM config for user {user_id}: {e}", exc_info=True) return None - diff --git a/gns3server/db_migrations/env.py b/gns3server/db_migrations/env.py index 043c832bb..f60fef2f6 100644 --- a/gns3server/db_migrations/env.py +++ b/gns3server/db_migrations/env.py @@ -21,6 +21,7 @@ if config.config_file_name is not None: # from myapp import mymodel # target_metadata = mymodel.Base.metadata from gns3server.db.models.base import Base + target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, @@ -63,9 +64,10 @@ def do_run_migrations(connection): async def run_async_migrations(): from gns3server.config import Config as GNS3Config + db_path = os.path.join(GNS3Config.instance().config_dir, "gns3_controller.db") db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}") - config.set_main_option('sqlalchemy.url', db_url) + config.set_main_option("sqlalchemy.url", db_url) connectable = async_engine_from_config( config.get_section(config.config_ini_section), diff --git a/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py b/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py index fa7b99c6d..27bdd04ea 100644 --- a/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py +++ b/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py @@ -5,13 +5,14 @@ Revises: 98083573d011 Create Date: 2026-03-03 """ + from alembic import op import sqlalchemy as sa from sqlalchemy import inspect # revision identifiers, used by Alembic. -revision = '20260303_create_llm_model_configs' -down_revision = '98083573d011' +revision = "20260303_create_llm_model_configs" +down_revision = "98083573d011" branch_labels = None depends_on = None @@ -25,43 +26,44 @@ def upgrade() -> None: # Check if table already exists (idempotent for databases created from code) tables = inspector.get_table_names() - if 'llm_model_configs' in tables: + if "llm_model_configs" in tables: # Table already exists from Base.metadata.create_all, skip creation return # Create llm_model_configs table with cross-database compatible types op.create_table( - 'llm_model_configs', - sa.Column('config_id', sa.String(32), primary_key=True), - sa.Column('name', sa.String(100), nullable=False), - sa.Column('model_type', sa.String(50), nullable=False), - sa.Column('config', sa.JSON(), nullable=False), - sa.Column('user_id', sa.String(32), sa.ForeignKey('users.user_id', ondelete='CASCADE'), nullable=True), - sa.Column('group_id', sa.String(32), sa.ForeignKey('user_groups.user_group_id', ondelete='CASCADE'), nullable=True), - sa.Column('is_default', sa.Boolean(), default=False, nullable=False), - sa.Column('version', sa.Integer(), nullable=False, server_default='0'), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.Column('updated_at', sa.DateTime(), nullable=True), + "llm_model_configs", + sa.Column("config_id", sa.String(32), primary_key=True), + sa.Column("name", sa.String(100), nullable=False), + sa.Column("model_type", sa.String(50), nullable=False), + sa.Column("config", sa.JSON(), nullable=False), + sa.Column("user_id", sa.String(32), sa.ForeignKey("users.user_id", ondelete="CASCADE"), nullable=True), + sa.Column( + "group_id", sa.String(32), sa.ForeignKey("user_groups.user_group_id", ondelete="CASCADE"), nullable=True + ), + sa.Column("is_default", sa.Boolean(), default=False, nullable=False), + sa.Column("version", sa.Integer(), nullable=False, server_default="0"), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("updated_at", sa.DateTime(), nullable=True), sa.CheckConstraint( - "(user_id IS NOT NULL AND group_id IS NULL) OR " - "(user_id IS NULL AND group_id IS NOT NULL)", - name='single_owner_check' + "(user_id IS NOT NULL AND group_id IS NULL) OR (user_id IS NULL AND group_id IS NOT NULL)", + name="single_owner_check", ), sa.CheckConstraint( "model_type IN ('text', 'vision', 'stt', 'tts', 'multimodal', 'embedding', 'reranking', 'other')", - name='valid_model_type_check' + name="valid_model_type_check", ), ) # Create indexes for efficient queries - op.create_index('idx_llm_model_configs_user_id', 'llm_model_configs', ['user_id']) - op.create_index('idx_llm_model_configs_group_id', 'llm_model_configs', ['group_id']) - op.create_index('idx_llm_model_configs_model_type', 'llm_model_configs', ['model_type']) + op.create_index("idx_llm_model_configs_user_id", "llm_model_configs", ["user_id"]) + op.create_index("idx_llm_model_configs_group_id", "llm_model_configs", ["group_id"]) + op.create_index("idx_llm_model_configs_model_type", "llm_model_configs", ["model_type"]) # PostgreSQL-specific indexes - if dialect_name == 'postgresql': + if dialect_name == "postgresql": # GIN index for JSONB config column - op.create_index('idx_llm_model_configs_config', 'llm_model_configs', ['config'], postgresql_using='gin') + op.create_index("idx_llm_model_configs_config", "llm_model_configs", ["config"], postgresql_using="gin") # Partial unique indexes for default configs op.execute(""" @@ -82,14 +84,14 @@ def downgrade() -> None: dialect_name = conn.dialect.name # Drop indexes - op.drop_index('idx_llm_model_configs_model_type', table_name='llm_model_configs') - op.drop_index('idx_llm_model_configs_group_id', table_name='llm_model_configs') - op.drop_index('idx_llm_model_configs_user_id', table_name='llm_model_configs') + op.drop_index("idx_llm_model_configs_model_type", table_name="llm_model_configs") + op.drop_index("idx_llm_model_configs_group_id", table_name="llm_model_configs") + op.drop_index("idx_llm_model_configs_user_id", table_name="llm_model_configs") - if dialect_name == 'postgresql': - op.drop_index('idx_llm_model_configs_config', table_name='llm_model_configs') - op.drop_index('unique_user_default', table_name='llm_model_configs') - op.drop_index('unique_group_default', table_name='llm_model_configs') + if dialect_name == "postgresql": + op.drop_index("idx_llm_model_configs_config", table_name="llm_model_configs") + op.drop_index("unique_user_default", table_name="llm_model_configs") + op.drop_index("unique_group_default", table_name="llm_model_configs") # Drop table - op.drop_table('llm_model_configs') + op.drop_table("llm_model_configs") diff --git a/gns3server/db_migrations/versions/7ceeddd9c9a8_init.py b/gns3server/db_migrations/versions/7ceeddd9c9a8_init.py index ca9a5ff2c..4c218cf5e 100644 --- a/gns3server/db_migrations/versions/7ceeddd9c9a8_init.py +++ b/gns3server/db_migrations/versions/7ceeddd9c9a8_init.py @@ -1,16 +1,17 @@ """init Revision ID: 7ceeddd9c9a8 -Revises: +Revises: Create Date: 2023-01-11 08:55:25.116126 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '7ceeddd9c9a8' +revision = "7ceeddd9c9a8" down_revision = None branch_labels = None depends_on = None diff --git a/gns3server/db_migrations/versions/8f2a1c4e9d3b_add_extra_configs_to_docker_templates.py b/gns3server/db_migrations/versions/8f2a1c4e9d3b_add_extra_configs_to_docker_templates.py index 2be50f287..64d1489b4 100644 --- a/gns3server/db_migrations/versions/8f2a1c4e9d3b_add_extra_configs_to_docker_templates.py +++ b/gns3server/db_migrations/versions/8f2a1c4e9d3b_add_extra_configs_to_docker_templates.py @@ -5,22 +5,23 @@ Revises: f0b0de2a9 Create Date: 2026-08-14 10:00:00.000000 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '8f2a1c4e9d3b' -down_revision = 'f0b0de2a9' +revision = "8f2a1c4e9d3b" +down_revision = "f0b0de2a9" branch_labels = None depends_on = None def upgrade() -> None: - op.add_column('docker_templates', sa.Column('extra_configs', sa.JSON())) + op.add_column("docker_templates", sa.Column("extra_configs", sa.JSON())) def downgrade() -> None: - op.drop_column('docker_templates', 'extra_configs') + op.drop_column("docker_templates", "extra_configs") diff --git a/gns3server/db_migrations/versions/98083573d011_add_tags_field_to_templates.py b/gns3server/db_migrations/versions/98083573d011_add_tags_field_to_templates.py index eeafe3afd..567aa3107 100644 --- a/gns3server/db_migrations/versions/98083573d011_add_tags_field_to_templates.py +++ b/gns3server/db_migrations/versions/98083573d011_add_tags_field_to_templates.py @@ -5,22 +5,23 @@ Revises: 9a5292aa4389 Create Date: 2026-02-23 18:23:59.857607 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '98083573d011' -down_revision = '9a5292aa4389' +revision = "98083573d011" +down_revision = "9a5292aa4389" branch_labels = None depends_on = None def upgrade() -> None: - op.add_column('templates', sa.Column('tags', sa.String())) + op.add_column("templates", sa.Column("tags", sa.String())) def downgrade() -> None: - op.drop_column('templates', 'tags') + op.drop_column("templates", "tags") diff --git a/gns3server/db_migrations/versions/9a5292aa4389_add_mac_address_field_in_docker_.py b/gns3server/db_migrations/versions/9a5292aa4389_add_mac_address_field_in_docker_.py index 5fb1597a5..e85dc00f7 100644 --- a/gns3server/db_migrations/versions/9a5292aa4389_add_mac_address_field_in_docker_.py +++ b/gns3server/db_migrations/versions/9a5292aa4389_add_mac_address_field_in_docker_.py @@ -5,23 +5,23 @@ Revises: 7ceeddd9c9a8 Create Date: 2024-09-18 17:52:53.429522 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '9a5292aa4389' -down_revision = '7ceeddd9c9a8' +revision = "9a5292aa4389" +down_revision = "7ceeddd9c9a8" branch_labels = None depends_on = None def upgrade() -> None: - op.add_column('docker_templates', sa.Column('mac_address', sa.String())) + op.add_column("docker_templates", sa.Column("mac_address", sa.String())) def downgrade() -> None: - op.drop_column('docker_templates', 'mac_address') - + op.drop_column("docker_templates", "mac_address") diff --git a/gns3server/db_migrations/versions/a8829e6c069b_add_llm_config_privileges.py b/gns3server/db_migrations/versions/a8829e6c069b_add_llm_config_privileges.py index cb87fd15b..fb146edd7 100644 --- a/gns3server/db_migrations/versions/a8829e6c069b_add_llm_config_privileges.py +++ b/gns3server/db_migrations/versions/a8829e6c069b_add_llm_config_privileges.py @@ -5,33 +5,34 @@ Revises: aff810fc119a Create Date: 2026-05-26 """ + from alembic import op import sqlalchemy as sa from uuid import uuid4 # revision identifiers, used by Alembic. -revision = 'a8829e6c069b' -down_revision = 'aff810fc119a' +revision = "a8829e6c069b" +down_revision = "aff810fc119a" branch_labels = None depends_on = None privileges_table = sa.table( - 'privileges', - sa.column('privilege_id', sa.String), - sa.column('name', sa.String), - sa.column('description', sa.String), + "privileges", + sa.column("privilege_id", sa.String), + sa.column("name", sa.String), + sa.column("description", sa.String), ) roles_table = sa.table( - 'roles', - sa.column('role_id', sa.String), - sa.column('name', sa.String), + "roles", + sa.column("role_id", sa.String), + sa.column("name", sa.String), ) privilege_role_map = sa.table( - 'privilege_role_map', - sa.column('privilege_id', sa.String), - sa.column('role_id', sa.String), + "privilege_role_map", + sa.column("privilege_id", sa.String), + sa.column("role_id", sa.String), ) @@ -48,9 +49,7 @@ def upgrade() -> None: privilege_ids = {} for priv in new_privileges: result = conn.execute( - sa.select(privileges_table.c.privilege_id).where( - privileges_table.c.name == priv["name"] - ) + sa.select(privileges_table.c.privilege_id).where(privileges_table.c.name == priv["name"]) ).fetchone() if result: @@ -67,9 +66,7 @@ def upgrade() -> None: privilege_ids[priv["name"]] = priv_id # Add LLMConfig.Audit and LLMConfig.Modify to the User role - user_role = conn.execute( - sa.select(roles_table.c.role_id).where(roles_table.c.name == "User") - ).fetchone() + user_role = conn.execute(sa.select(roles_table.c.role_id).where(roles_table.c.name == "User")).fetchone() if user_role: user_role_id = user_role[0] @@ -85,17 +82,13 @@ def upgrade() -> None: def downgrade() -> None: conn = op.get_bind() - user_role = conn.execute( - sa.select(roles_table.c.role_id).where(roles_table.c.name == "User") - ).fetchone() + user_role = conn.execute(sa.select(roles_table.c.role_id).where(roles_table.c.name == "User")).fetchone() if user_role: user_role_id = user_role[0] for priv_name in ("LLMConfig.Audit", "LLMConfig.Modify"): priv = conn.execute( - sa.select(privileges_table.c.privilege_id).where( - privileges_table.c.name == priv_name - ) + sa.select(privileges_table.c.privilege_id).where(privileges_table.c.name == priv_name) ).fetchone() if priv: conn.execute( @@ -106,8 +99,4 @@ def downgrade() -> None: ) for priv_name in ("LLMConfig.Allocate", "LLMConfig.Audit", "LLMConfig.Modify"): - conn.execute( - privileges_table.delete().where( - privileges_table.c.name == priv_name - ) - ) + conn.execute(privileges_table.delete().where(privileges_table.c.name == priv_name)) diff --git a/gns3server/db_migrations/versions/aff810fc119a_add_token_version_to_users_table.py b/gns3server/db_migrations/versions/aff810fc119a_add_token_version_to_users_table.py index f9f82cc43..8de44a283 100644 --- a/gns3server/db_migrations/versions/aff810fc119a_add_token_version_to_users_table.py +++ b/gns3server/db_migrations/versions/aff810fc119a_add_token_version_to_users_table.py @@ -5,20 +5,22 @@ Revises: ec4b7b198555 Create Date: 2026-04-06 19:49:12.155446 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'aff810fc119a' -down_revision = 'ec4b7b198555' +revision = "aff810fc119a" +down_revision = "ec4b7b198555" branch_labels = None depends_on = None def upgrade() -> None: - op.add_column('users', sa.Column('token_version', sa.Integer(), nullable=False, server_default='0')) + op.add_column("users", sa.Column("token_version", sa.Integer(), nullable=False, server_default="0")) + def downgrade() -> None: - op.drop_column('users', 'token_version') + op.drop_column("users", "token_version") diff --git a/gns3server/db_migrations/versions/b3c7e2a91d4f_add_netmiko_device_type_to_templates.py b/gns3server/db_migrations/versions/b3c7e2a91d4f_add_netmiko_device_type_to_templates.py index bc0241c12..661b33d69 100644 --- a/gns3server/db_migrations/versions/b3c7e2a91d4f_add_netmiko_device_type_to_templates.py +++ b/gns3server/db_migrations/versions/b3c7e2a91d4f_add_netmiko_device_type_to_templates.py @@ -5,22 +5,23 @@ Revises: 8f2a1c4e9d3b Create Date: 2026-08-16 10:00:00.000000 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'b3c7e2a91d4f' -down_revision = '8f2a1c4e9d3b' +revision = "b3c7e2a91d4f" +down_revision = "8f2a1c4e9d3b" branch_labels = None depends_on = None def upgrade() -> None: - op.add_column('templates', sa.Column('netmiko_device_type', sa.String())) + op.add_column("templates", sa.Column("netmiko_device_type", sa.String())) def downgrade() -> None: - op.drop_column('templates', 'netmiko_device_type') + op.drop_column("templates", "netmiko_device_type") diff --git a/gns3server/db_migrations/versions/c7e4a9f1d2b6_add_appliance_metadata_to_templates.py b/gns3server/db_migrations/versions/c7e4a9f1d2b6_add_appliance_metadata_to_templates.py index 7c7b18fbb..e0725102f 100644 --- a/gns3server/db_migrations/versions/c7e4a9f1d2b6_add_appliance_metadata_to_templates.py +++ b/gns3server/db_migrations/versions/c7e4a9f1d2b6_add_appliance_metadata_to_templates.py @@ -5,22 +5,23 @@ Revises: b3c7e2a91d4f Create Date: 2026-08-16 12:00:00.000000 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'c7e4a9f1d2b6' -down_revision = 'b3c7e2a91d4f' +revision = "c7e4a9f1d2b6" +down_revision = "b3c7e2a91d4f" branch_labels = None depends_on = None def upgrade() -> None: - op.add_column('templates', sa.Column('appliance_metadata', sa.JSON())) + op.add_column("templates", sa.Column("appliance_metadata", sa.JSON())) def downgrade() -> None: - op.drop_column('templates', 'appliance_metadata') + op.drop_column("templates", "appliance_metadata") diff --git a/gns3server/db_migrations/versions/ec4b7b198555_convert_pickletype_fields_to_json.py b/gns3server/db_migrations/versions/ec4b7b198555_convert_pickletype_fields_to_json.py index 6b393940e..d25782c13 100644 --- a/gns3server/db_migrations/versions/ec4b7b198555_convert_pickletype_fields_to_json.py +++ b/gns3server/db_migrations/versions/ec4b7b198555_convert_pickletype_fields_to_json.py @@ -5,12 +5,13 @@ Revises: 20260303_create_llm_model_configs Create Date: 2026-04-03 19:51:06.173013 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'ec4b7b198555' -down_revision = '20260303_create_llm_model_configs' +revision = "ec4b7b198555" +down_revision = "20260303_create_llm_model_configs" branch_labels = None depends_on = None @@ -33,13 +34,13 @@ def convert_pickle_to_json(conn, table_name: str, column_name: str) -> None: json_data = json.dumps(data) conn.execute( sa.text(f"UPDATE {table_name} SET {column_name} = :data WHERE template_id = :template_id"), - {"data": json_data, "template_id": row.template_id} + {"data": json_data, "template_id": row.template_id}, ) else: # Set NULL if there is no data to be converted conn.execute( sa.text(f"UPDATE {table_name} SET {column_name} = NULL WHERE template_id = :template_id"), - {"template_id": row.template_id} + {"template_id": row.template_id}, ) @@ -61,13 +62,13 @@ def convert_json_to_pickle(conn, table_name: str, column_name: str) -> None: pickle_data = pickle.dumps(data) conn.execute( sa.text(f"UPDATE {table_name} SET {column_name} = :data WHERE template_id = :template_id"), - {"data": pickle_data, "template_id": row.template_id} + {"data": pickle_data, "template_id": row.template_id}, ) else: # Set NULL if there is no data to be converted conn.execute( sa.text(f"UPDATE {table_name} SET {column_name} = NULL WHERE template_id = :template_id"), - {"template_id": row.template_id} + {"template_id": row.template_id}, ) @@ -86,21 +87,22 @@ def upgrade() -> None: convert_pickle_to_json(conn, "virtualbox_templates", "custom_adapters") convert_pickle_to_json(conn, "vmware_templates", "custom_adapters") - with op.batch_alter_table('cloud_templates') as batch_op: - batch_op.alter_column('ports_mapping', type_=sa.JSON()) - with op.batch_alter_table('docker_templates') as batch_op: - batch_op.alter_column('extra_volumes', type_=sa.JSON()) - batch_op.alter_column('custom_adapters', type_=sa.JSON()) - with op.batch_alter_table('ethernet_hub_templates') as batch_op: - batch_op.alter_column('ports_mapping', type_=sa.JSON()) - with op.batch_alter_table('ethernet_switch_templates') as batch_op: - batch_op.alter_column('ports_mapping', type_=sa.JSON()) - with op.batch_alter_table('qemu_templates') as batch_op: - batch_op.alter_column('custom_adapters', type_=sa.JSON()) - with op.batch_alter_table('virtualbox_templates') as batch_op: - batch_op.alter_column('custom_adapters', type_=sa.JSON()) - with op.batch_alter_table('vmware_templates') as batch_op: - batch_op.alter_column('custom_adapters', type_=sa.JSON()) + with op.batch_alter_table("cloud_templates") as batch_op: + batch_op.alter_column("ports_mapping", type_=sa.JSON()) + with op.batch_alter_table("docker_templates") as batch_op: + batch_op.alter_column("extra_volumes", type_=sa.JSON()) + batch_op.alter_column("custom_adapters", type_=sa.JSON()) + with op.batch_alter_table("ethernet_hub_templates") as batch_op: + batch_op.alter_column("ports_mapping", type_=sa.JSON()) + with op.batch_alter_table("ethernet_switch_templates") as batch_op: + batch_op.alter_column("ports_mapping", type_=sa.JSON()) + with op.batch_alter_table("qemu_templates") as batch_op: + batch_op.alter_column("custom_adapters", type_=sa.JSON()) + with op.batch_alter_table("virtualbox_templates") as batch_op: + batch_op.alter_column("custom_adapters", type_=sa.JSON()) + with op.batch_alter_table("vmware_templates") as batch_op: + batch_op.alter_column("custom_adapters", type_=sa.JSON()) + def downgrade() -> None: @@ -117,18 +119,18 @@ def downgrade() -> None: convert_json_to_pickle(conn, "virtualbox_templates", "custom_adapters") convert_json_to_pickle(conn, "vmware_templates", "custom_adapters") - with op.batch_alter_table('cloud_templates') as batch_op: - batch_op.alter_column('ports_mapping', type_=sa.PickleType()) - with op.batch_alter_table('docker_templates') as batch_op: - batch_op.alter_column('extra_volumes', type_=sa.PickleType()) - batch_op.alter_column('custom_adapters', type_=sa.PickleType()) - with op.batch_alter_table('ethernet_hub_templates') as batch_op: - batch_op.alter_column('ports_mapping', type_=sa.PickleType()) - with op.batch_alter_table('ethernet_switch_templates') as batch_op: - batch_op.alter_column('ports_mapping', type_=sa.PickleType()) - with op.batch_alter_table('qemu_templates') as batch_op: - batch_op.alter_column('custom_adapters', type_=sa.PickleType()) - with op.batch_alter_table('virtualbox_templates') as batch_op: - batch_op.alter_column('custom_adapters', type_=sa.PickleType()) - with op.batch_alter_table('vmware_templates') as batch_op: - batch_op.alter_column('custom_adapters', type_=sa.PickleType()) + with op.batch_alter_table("cloud_templates") as batch_op: + batch_op.alter_column("ports_mapping", type_=sa.PickleType()) + with op.batch_alter_table("docker_templates") as batch_op: + batch_op.alter_column("extra_volumes", type_=sa.PickleType()) + batch_op.alter_column("custom_adapters", type_=sa.PickleType()) + with op.batch_alter_table("ethernet_hub_templates") as batch_op: + batch_op.alter_column("ports_mapping", type_=sa.PickleType()) + with op.batch_alter_table("ethernet_switch_templates") as batch_op: + batch_op.alter_column("ports_mapping", type_=sa.PickleType()) + with op.batch_alter_table("qemu_templates") as batch_op: + batch_op.alter_column("custom_adapters", type_=sa.PickleType()) + with op.batch_alter_table("virtualbox_templates") as batch_op: + batch_op.alter_column("custom_adapters", type_=sa.PickleType()) + with op.batch_alter_table("vmware_templates") as batch_op: + batch_op.alter_column("custom_adapters", type_=sa.PickleType()) diff --git a/gns3server/db_migrations/versions/f0b0de2a9_add_api_keys_table.py b/gns3server/db_migrations/versions/f0b0de2a9_add_api_keys_table.py index 9c8b4fdb6..a52dd05d6 100644 --- a/gns3server/db_migrations/versions/f0b0de2a9_add_api_keys_table.py +++ b/gns3server/db_migrations/versions/f0b0de2a9_add_api_keys_table.py @@ -5,13 +5,14 @@ Revises: a8829e6c069b Create Date: 2026-06-11 10:00:00.000000 """ + from alembic import op import sqlalchemy as sa import gns3server.db.models.base as models # revision identifiers, used by Alembic. -revision = 'f0b0de2a9' -down_revision = 'a8829e6c069b' +revision = "f0b0de2a9" +down_revision = "a8829e6c069b" branch_labels = None depends_on = None @@ -19,25 +20,25 @@ depends_on = None def upgrade() -> None: op.create_table( - 'api_keys', - sa.Column('api_key_id', models.GUID(), nullable=False), - sa.Column('user_id', models.GUID(), nullable=False), - sa.Column('name', sa.String(128), nullable=False), - sa.Column('key_hash', sa.String(128), nullable=False), - sa.Column('key_prefix', sa.String(8), nullable=False), - sa.Column('last_used_at', sa.DateTime(), nullable=True), - sa.Column('created_at', sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), - sa.Column('revoked', sa.Boolean(), default=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('api_key_id'), + "api_keys", + sa.Column("api_key_id", models.GUID(), nullable=False), + sa.Column("user_id", models.GUID(), nullable=False), + sa.Column("name", sa.String(128), nullable=False), + sa.Column("key_hash", sa.String(128), nullable=False), + sa.Column("key_prefix", sa.String(8), nullable=False), + sa.Column("last_used_at", sa.DateTime(), nullable=True), + sa.Column("created_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.func.current_timestamp(), nullable=False), + sa.Column("revoked", sa.Boolean(), default=False, nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["users.user_id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("api_key_id"), ) - op.create_index('ix_api_keys_user_id', 'api_keys', ['user_id']) - op.create_index('ix_api_keys_key_hash', 'api_keys', ['key_hash']) + op.create_index("ix_api_keys_user_id", "api_keys", ["user_id"]) + op.create_index("ix_api_keys_key_hash", "api_keys", ["key_hash"]) def downgrade() -> None: - op.drop_index('ix_api_keys_key_hash', table_name='api_keys') - op.drop_index('ix_api_keys_user_id', table_name='api_keys') - op.drop_table('api_keys') + op.drop_index("ix_api_keys_key_hash", table_name="api_keys") + op.drop_index("ix_api_keys_user_id", table_name="api_keys") + op.drop_table("api_keys") diff --git a/gns3server/logger.py b/gns3server/logger.py index 2baba0fd3..e2f184bca 100644 --- a/gns3server/logger.py +++ b/gns3server/logger.py @@ -17,7 +17,6 @@ """Provide a pretty logging on console""" - import logging import sys import os @@ -28,10 +27,10 @@ from logging.handlers import RotatingFileHandler class ColouredFormatter(logging.Formatter): - RESET = "\x1B[0m" - RED = "\x1B[31m" - YELLOW = "\x1B[33m" - GREEN = "\x1B[32m" + RESET = "\x1b[0m" + RED = "\x1b[31m" + YELLOW = "\x1b[33m" + GREEN = "\x1b[32m" PINK = "\x1b[35m" def format(self, record, colour=False): diff --git a/gns3server/main.py b/gns3server/main.py index 4069f5e3a..150987d41 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -62,6 +62,7 @@ def daemonize(): print("Second fork failed: %d (%s)\n" % (e.errno, e.strerror), file=sys.stderr) sys.exit(1) + def parse_arguments(argv): """ Parse command line arguments @@ -69,6 +70,7 @@ def parse_arguments(argv): :param argv: Array of command line arguments """ from gns3server.version import __version__ + parser = argparse.ArgumentParser(description=f"GNS3 server version {__version__}") parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__) parser.add_argument("--host", help="run on the given host/IP address") @@ -78,16 +80,12 @@ def parse_arguments(argv): parser.add_argument("--certfile", help="SSL cert file") parser.add_argument("--certkey", help="SSL key file") parser.add_argument("-L", "--local", action="store_true", help="local mode (allows some insecure operations)") - parser.add_argument( - "-A", "--allow", action="store_true", help="allow remote connections to local console ports" - ) + parser.add_argument("-A", "--allow", action="store_true", help="allow remote connections to local console ports") parser.add_argument("-q", "--quiet", default=False, action="store_true", help="do not show logs on stdout") parser.add_argument("-d", "--debug", default=False, action="store_true", help="show debug logs") parser.add_argument("--logfile", "--log", help="send output to logfile instead of console") parser.add_argument("--logmaxsize", default=10000000, help="maximum logfile size in bytes (default is 10MB)") - parser.add_argument( - "--logbackupcount", default=10, help="number of historical log files to keep (default is 10)" - ) + parser.add_argument("--logbackupcount", default=10, help="number of historical log files to keep (default is 10)") parser.add_argument( "--logcompression", default=False, action="store_true", help="compress inactive (historical) logs" ) @@ -118,9 +116,7 @@ def _raise_open_files_limit(target=65535): new_soft = min(target, hard) resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft, hard)) if new_soft < target: - log.warning( - f"Open-files limit raised to {new_soft} (hard limit), below the requested {target}" - ) + log.warning(f"Open-files limit raised to {new_soft} (hard limit), below the requested {target}") else: log.info(f"Open-files limit raised from {soft} to {new_soft}") except (OSError, ValueError) as e: @@ -141,6 +137,7 @@ def main(): try: parser, args = parse_arguments(sys.argv[1:]) from gns3server.server import Server + asyncio.run(Server().run(parser, args)) except KeyboardInterrupt: pass diff --git a/gns3server/schemas/__init__.py b/gns3server/schemas/__init__.py index bc8cca5b9..23056d58c 100644 --- a/gns3server/schemas/__init__.py +++ b/gns3server/schemas/__init__.py @@ -20,16 +20,51 @@ from .common import ErrorMessage from .version import Version # Controller schemas -from .controller.links import LinkCreate, LinkUpdate, Link, UDPPortInfo, EthernetPortInfo, LinkCapture, MarkerCreate, MarkerUpdate, MarkerDefinitionCreate -from .controller.computes import ComputeCreate, ComputeUpdate, ComputeVirtualBoxVM, ComputeVMwareVM, ComputeDockerImage, AutoIdlePC, Compute +from .controller.links import ( + LinkCreate, + LinkUpdate, + Link, + UDPPortInfo, + EthernetPortInfo, + LinkCapture, + MarkerCreate, + MarkerUpdate, + MarkerDefinitionCreate, +) +from .controller.computes import ( + ComputeCreate, + ComputeUpdate, + ComputeVirtualBoxVM, + ComputeVMwareVM, + ComputeDockerImage, + AutoIdlePC, + Compute, +) from .controller.templates import TemplateCreate, TemplateUpdate, TemplateUsage, Template from .controller.images import Image, ImageType from .controller.appliances import ApplianceVersion, ApplianceVersionV8, Appliance from .controller.drawings import Drawing from .controller.gns3vm import GNS3VM from .controller.nodes import NodeCreate, NodeUpdate, NodeDuplicate, NodeCapture, Node -from .controller.projects import ProjectCreate, ProjectUpdate, ProjectDuplicate, Project, ProjectFile, ProjectCompression, NodeFile -from .controller.users import UserCreate, UserUpdate, LoggedInUserUpdate, User, Credentials, UserGroupCreate, UserGroupUpdate, UserGroup +from .controller.projects import ( + ProjectCreate, + ProjectUpdate, + ProjectDuplicate, + Project, + ProjectFile, + ProjectCompression, + NodeFile, +) +from .controller.users import ( + UserCreate, + UserUpdate, + LoggedInUserUpdate, + User, + Credentials, + UserGroupCreate, + UserGroupUpdate, + UserGroup, +) # Conditionally import AI-related schemas try: @@ -40,7 +75,7 @@ try: LLMModelConfigResponse, LLMModelConfigWithSource, LLMModelConfigInheritedResponse, - LLMModelConfigListResponse + LLMModelConfigListResponse, ) from .controller.chat import ( OpenAIToolCall, @@ -49,7 +84,7 @@ try: OpenAIMessage, ConversationHistory, ChatSession, - RenameSession + RenameSession, ) except ImportError: # AI schemas are not available (should not happen as they don't depend on external libs) @@ -89,7 +124,7 @@ from .controller.templates.dynamips_templates import ( C3745DynamipsTemplate, C3745DynamipsTemplateUpdate, C7200DynamipsTemplate, - C7200DynamipsTemplateUpdate + C7200DynamipsTemplateUpdate, ) # Compute schemas diff --git a/gns3server/schemas/compute/atm_switch_nodes.py b/gns3server/schemas/compute/atm_switch_nodes.py index 836fbfa37..9a9db0845 100644 --- a/gns3server/schemas/compute/atm_switch_nodes.py +++ b/gns3server/schemas/compute/atm_switch_nodes.py @@ -50,6 +50,5 @@ class ATMSwitchUpdate(ATMSwitchBase): class ATMSwitch(ATMSwitchBase): - project_id: UUID status: Optional[NodeStatus] = None diff --git a/gns3server/schemas/compute/cloud_nodes.py b/gns3server/schemas/compute/cloud_nodes.py index 28e01c72d..4cda4ce23 100644 --- a/gns3server/schemas/compute/cloud_nodes.py +++ b/gns3server/schemas/compute/cloud_nodes.py @@ -23,19 +23,16 @@ from ..common import NodeStatus class HostInterfaceType(str, Enum): - ethernet = "ethernet" tap = "tap" class IPAddressFamily(str, Enum): - ipv4 = "ipv4" ipv6 = "ipv6" class InterfaceStatus(str, Enum): - up = "up" down = "down" @@ -115,7 +112,6 @@ class UDPPort(BaseModel): class CloudConsoleType(str, Enum): - telnet = "telnet" ssh = "ssh" vnc = "vnc" @@ -160,7 +156,6 @@ class CloudUpdate(CloudBase): class Cloud(CloudBase): - project_id: UUID node_id: UUID ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] diff --git a/gns3server/schemas/compute/docker_nodes.py b/gns3server/schemas/compute/docker_nodes.py index d752b7586..4359df723 100644 --- a/gns3server/schemas/compute/docker_nodes.py +++ b/gns3server/schemas/compute/docker_nodes.py @@ -54,11 +54,15 @@ class DockerBase(BaseModel): usage: Optional[str] = Field(None, description="How to use the Docker container") start_command: Optional[str] = Field(None, description="Docker CMD entry") adapters: Optional[int] = Field(None, ge=0, le=99, description="Number of adapters") - mac_address: Optional[str] = Field(None, description="Base MAC address", pattern="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$") + mac_address: Optional[str] = Field( + None, description="Base MAC address", pattern="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$" + ) environment: Optional[str] = Field(None, description="Docker environment variables") extra_hosts: Optional[str] = Field(None, description="Docker extra hosts (added to /etc/hosts)") extra_volumes: Optional[List[str]] = Field(None, description="Additional directories to make persistent") - extra_configs: Optional[List[ExtraConfig]] = Field(None, description="Configuration files injected into the container (bind-mounted read-only)") + extra_configs: Optional[List[ExtraConfig]] = Field( + None, description="Configuration files injected into the container (bind-mounted read-only)" + ) startup_config_content: Optional[str] = Field( None, description="Startup-config content (IOL runner images: materialized into the node's NVRAM at start)" ) @@ -76,10 +80,11 @@ class DockerCreate(DockerBase): None, ge=1, le=1022, description="IOL application ID for iol-runner images (allocated by the controller)" ) image_digest: Optional[str] = Field( - None, pattern=r"^sha256:[a-f0-9]{64}$", + None, + pattern=r"^sha256:[a-f0-9]{64}$", description="Image id the controller expects for 'image' (sha256:, resolved from the Docker " - "daemon on the controller host). When set and the compute holds a different image " - "under the same tag, the image is reported as missing so the controller re-syncs it" + "daemon on the controller host). When set and the compute holds a different image " + "under the same tag, the image is reported as missing so the controller re-syncs it", ) @@ -93,7 +98,6 @@ class DockerUpdate(DockerBase): class Docker(DockerBase): - container_id: str = Field( ..., min_length=12, max_length=64, pattern="^[a-f0-9]+$", description="Docker container ID (read only)" ) diff --git a/gns3server/schemas/compute/dynamips_nodes.py b/gns3server/schemas/compute/dynamips_nodes.py index 1ab8e649b..d01020a28 100644 --- a/gns3server/schemas/compute/dynamips_nodes.py +++ b/gns3server/schemas/compute/dynamips_nodes.py @@ -189,7 +189,6 @@ class DynamipsUpdate(DynamipsBase): class Dynamips(DynamipsBase): - name: str node_id: UUID project_id: UUID diff --git a/gns3server/schemas/compute/ethernet_hub_nodes.py b/gns3server/schemas/compute/ethernet_hub_nodes.py index 803be8f3b..866e57a38 100644 --- a/gns3server/schemas/compute/ethernet_hub_nodes.py +++ b/gns3server/schemas/compute/ethernet_hub_nodes.py @@ -22,7 +22,6 @@ from ..common import NodeStatus class EthernetHubPort(BaseModel): - name: str port_number: int @@ -55,7 +54,6 @@ class EthernetHubUpdate(EthernetHubBase): class EthernetHub(EthernetHubBase): - name: str node_id: UUID project_id: UUID diff --git a/gns3server/schemas/compute/ethernet_switch_nodes.py b/gns3server/schemas/compute/ethernet_switch_nodes.py index ddb4b32cc..16bd63b3e 100644 --- a/gns3server/schemas/compute/ethernet_switch_nodes.py +++ b/gns3server/schemas/compute/ethernet_switch_nodes.py @@ -23,14 +23,12 @@ from ..common import NodeStatus class EthernetSwitchPortType(str, Enum): - access = "access" dot1q = "dot1q" qinq = "qinq" class EthernetSwitchEtherType(str, Enum): - ethertype_8021q = "0x8100" ethertype_qinq = "0x88A8" ethertype_8021q9100 = "0x9100" @@ -38,12 +36,13 @@ class EthernetSwitchEtherType(str, Enum): class EthernetSwitchPort(BaseModel): - name: str port_number: int type: EthernetSwitchPortType = Field(..., description="Port type") vlan: int = Field(..., ge=1, le=4094, description="VLAN number") - ethertype: Optional[EthernetSwitchEtherType] = Field(EthernetSwitchEtherType.ethertype_8021q, description="QinQ Ethertype") + ethertype: Optional[EthernetSwitchEtherType] = Field( + EthernetSwitchEtherType.ethertype_8021q, description="QinQ Ethertype" + ) @model_validator(mode="after") def check_ethertype(self) -> "EthernetSwitchPort": @@ -92,7 +91,6 @@ class EthernetSwitchUpdate(EthernetSwitchBase): class EthernetSwitch(EthernetSwitchBase): - name: str node_id: UUID project_id: UUID diff --git a/gns3server/schemas/compute/frame_relay_switch_nodes.py b/gns3server/schemas/compute/frame_relay_switch_nodes.py index 6b759eb43..67db0bf3e 100644 --- a/gns3server/schemas/compute/frame_relay_switch_nodes.py +++ b/gns3server/schemas/compute/frame_relay_switch_nodes.py @@ -50,6 +50,5 @@ class FrameRelaySwitchUpdate(FrameRelaySwitchBase): class FrameRelaySwitch(FrameRelaySwitchBase): - project_id: UUID status: Optional[NodeStatus] = None diff --git a/gns3server/schemas/compute/iou_nodes.py b/gns3server/schemas/compute/iou_nodes.py index c04b6f0ef..f4dd42ce5 100644 --- a/gns3server/schemas/compute/iou_nodes.py +++ b/gns3server/schemas/compute/iou_nodes.py @@ -66,7 +66,6 @@ class IOUUpdate(IOUBase): class IOU(IOUBase): - project_id: UUID = Field(..., description="Project ID") node_directory: str = Field(..., description="Path to the node working directory (read only)") command_line: str = Field(..., description="Last command line used to start IOU (read only)") @@ -74,6 +73,5 @@ class IOU(IOUBase): class IOUStart(BaseModel): - iourc_content: Optional[str] = Field(None, description="Content of the iourc file") license_check: Optional[bool] = Field(None, description="Whether the IOU license should be checked") diff --git a/gns3server/schemas/compute/nat_nodes.py b/gns3server/schemas/compute/nat_nodes.py index ef91c8dd6..95a703320 100644 --- a/gns3server/schemas/compute/nat_nodes.py +++ b/gns3server/schemas/compute/nat_nodes.py @@ -23,7 +23,6 @@ from ..common import NodeStatus class HostInterfaceType(str, Enum): - ethernet = "ethernet" tap = "tap" @@ -115,7 +114,6 @@ class NATUpdate(NATBase): class NAT(NATBase): - project_id: UUID node_id: UUID ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] diff --git a/gns3server/schemas/compute/nios.py b/gns3server/schemas/compute/nios.py index a1d8641b3..f05d82737 100644 --- a/gns3server/schemas/compute/nios.py +++ b/gns3server/schemas/compute/nios.py @@ -21,7 +21,6 @@ from enum import Enum class UDPNIOType(str, Enum): - udp = "nio_udp" @@ -40,7 +39,6 @@ class UDPNIO(BaseModel): class EthernetNIOType(str, Enum): - ethernet = "nio_ethernet" @@ -54,7 +52,6 @@ class EthernetNIO(BaseModel): class TAPNIOType(str, Enum): - tap = "nio_tap" diff --git a/gns3server/schemas/compute/qemu_nodes.py b/gns3server/schemas/compute/qemu_nodes.py index 658fb26d5..4596677c8 100644 --- a/gns3server/schemas/compute/qemu_nodes.py +++ b/gns3server/schemas/compute/qemu_nodes.py @@ -23,7 +23,6 @@ from ..common import NodeStatus, CustomAdapter class QemuPlatform(str, Enum): - aarch64 = "aarch64" alpha = "alpha" arm = "arm" @@ -94,7 +93,6 @@ class QemuOnCloseAction(str, Enum): class QemuProcessPriority(str, Enum): - realtime = "realtime" very_high = "very high" high = "high" @@ -236,7 +234,6 @@ class QemuUpdate(QemuBase): class Qemu(QemuBase): - project_id: UUID = Field(..., description="Project ID") node_directory: str = Field(..., description="Path to the node working directory (read only)") command_line: str = Field(..., description="Last command line used to start IOU (read only)") @@ -244,6 +241,5 @@ class Qemu(QemuBase): class QemuBinaryPath(BaseModel): - path: str version: str diff --git a/gns3server/schemas/compute/virtualbox_nodes.py b/gns3server/schemas/compute/virtualbox_nodes.py index faa6ed7ee..06ad9f831 100644 --- a/gns3server/schemas/compute/virtualbox_nodes.py +++ b/gns3server/schemas/compute/virtualbox_nodes.py @@ -43,7 +43,6 @@ class VirtualBoxOnCloseAction(str, Enum): class VirtualBoxAdapterType(str, Enum): - pcnet_pci_ii = ("PCnet-PCI II (Am79C970A)",) pcnet_fast_iii = ("PCNet-FAST III (Am79C973)",) intel_pro_1000_mt_desktop = ("Intel PRO/1000 MT Desktop (82540EM)",) @@ -92,7 +91,6 @@ class VirtualBoxUpdate(VirtualBoxBase): class VirtualBox(VirtualBoxBase): - project_id: UUID = Field(..., description="Project ID") node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)") status: NodeStatus = Field(..., description="Container status (read only)") diff --git a/gns3server/schemas/compute/vmware_nodes.py b/gns3server/schemas/compute/vmware_nodes.py index 770f6f7c6..ee670a2bc 100644 --- a/gns3server/schemas/compute/vmware_nodes.py +++ b/gns3server/schemas/compute/vmware_nodes.py @@ -97,7 +97,6 @@ class VMwareUpdate(VMwareBase): class VMware(VMwareBase): - project_id: UUID = Field(..., description="Project ID") node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)") status: NodeStatus = Field(..., description="Container status (read only)") diff --git a/gns3server/schemas/compute/vpcs_nodes.py b/gns3server/schemas/compute/vpcs_nodes.py index 1b2278536..30cd40960 100644 --- a/gns3server/schemas/compute/vpcs_nodes.py +++ b/gns3server/schemas/compute/vpcs_nodes.py @@ -62,7 +62,6 @@ class VPCSUpdate(VPCSBase): class VPCS(VPCSBase): - project_id: UUID = Field(..., description="Project ID") node_directory: str = Field(..., description="Path to the node working directory (read only)") status: NodeStatus = Field(..., description="Container status (read only)") diff --git a/gns3server/schemas/config.py b/gns3server/schemas/config.py index 227129b28..7b1eb19fb 100644 --- a/gns3server/schemas/config.py +++ b/gns3server/schemas/config.py @@ -17,96 +17,86 @@ import socket from enum import Enum -from pydantic import ( - ConfigDict, - BaseModel, - Field, - SecretStr, - FilePath, - DirectoryPath, - field_validator, - model_validator -) +from pydantic import ConfigDict, BaseModel, Field, SecretStr, FilePath, DirectoryPath, field_validator, model_validator from typing import List, Optional class ControllerSettings(BaseModel): - jwt_secret_key: Optional[str] = Field( None, description="Secret key used to sign the JWT authentication tokens " - "(normally managed via the secrets directory, not the configuration file)") + "(normally managed via the secrets directory, not the configuration file)", + ) jwt_algorithm: str = Field("HS256", description="Algorithm used to sign the JWT tokens") jwt_access_token_expire_minutes: int = Field( - 1440, description="Lifetime of the JWT access tokens in minutes (24 hours by default)") + 1440, description="Lifetime of the JWT access tokens in minutes (24 hours by default)" + ) jwt_refresh_token_expire_minutes: int = Field( - 43200, description="Lifetime of the JWT refresh tokens in minutes (30 days by default)") + 43200, description="Lifetime of the JWT refresh tokens in minutes (30 days by default)" + ) default_admin_username: str = Field( "admin", description="Username of the super admin account seeded when the controller database is created; " - "changing it has no effect until the database is re-created (which resets the account)") + "changing it has no effect until the database is re-created (which resets the account)", + ) default_admin_password: SecretStr = Field( SecretStr("admin"), description="Password of the super admin account seeded when the controller database is created; " - "changing it has no effect until the database is re-created (which resets the account)") + "changing it has no effect until the database is re-created (which resets the account)", + ) model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) class VPCSSettings(BaseModel): - vpcs_path: str = Field("vpcs", description="VPCS executable location, default: search in PATH") model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) class DynamipsSettings(BaseModel): - - allocate_aux_console_ports: bool = Field( - False, description="Allocate auxiliary console ports on IOS routers") + allocate_aux_console_ports: bool = Field(False, description="Allocate auxiliary console ports on IOS routers") mmap_support: bool = Field( - True, description="Use memory-mapped flash files (mmap) to lower the memory usage of routers") + True, description="Use memory-mapped flash files (mmap) to lower the memory usage of routers" + ) dynamips_path: str = Field("dynamips", description="Dynamips executable location, default: search in PATH") sparse_memory_support: bool = Field( - True, description="Use sparse memory allocation to lower the memory usage of routers") + True, description="Use sparse memory allocation to lower the memory usage of routers" + ) ghost_ios_support: bool = Field( - True, description="Enable Ghost IOS support to share memory between identical IOS images") + True, description="Enable Ghost IOS support to share memory between identical IOS images" + ) model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) class IOUSettings(BaseModel): - iourc_path: Optional[str] = Field( - None, description="Path of your .iourc file, the file is searched in $HOME/.iourc if not provided") + None, description="Path of your .iourc file, the file is searched in $HOME/.iourc if not provided" + ) license_check: bool = Field( True, description="Validate the iourc license file (if disabled, IOU will not start and no errors " - "will be shown when the license is invalid)") + "will be shown when the license is invalid)", + ) model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) class QemuSettings(BaseModel): - - enable_monitor: bool = Field( - True, description="Use the Qemu monitor feature to communicate with Qemu VMs") + enable_monitor: bool = Field(True, description="Use the Qemu monitor feature to communicate with Qemu VMs") monitor_host: str = Field("127.0.0.1", description="IP used to listen for the monitor") - enable_hardware_acceleration: bool = Field( - True, description="Enable hardware acceleration (KVM)") + enable_hardware_acceleration: bool = Field(True, description="Enable hardware acceleration (KVM)") require_hardware_acceleration: bool = Field( - False, description="Require hardware acceleration in order to start VMs") - allow_unsafe_options: bool = Field( - False, description="Allow unsafe additional command line options") - ovmf_firmware_dir: str = Field( - "/usr/share/OVMF", description="Path to the OVMF firmware directory") + False, description="Require hardware acceleration in order to start VMs" + ) + allow_unsafe_options: bool = Field(False, description="Allow unsafe additional command line options") + ovmf_firmware_dir: str = Field("/usr/share/OVMF", description="Path to the OVMF firmware directory") model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) class VirtualBoxSettings(BaseModel): - vboxmanage_path: Optional[str] = None model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) class VMwareSettings(BaseModel): - vmrun_path: Optional[str] = None vmnet_start_range: int = Field(2, ge=1, le=255) vmnet_end_range: int = Field(255, ge=1, le=255) # should be limited to 19 on Windows @@ -121,15 +111,15 @@ class VMwareSettings(BaseModel): class WebWiresharkSettings(BaseModel): - enabled: bool = Field( - True, description="Enable the Web Wireshark feature (container-based Wireshark in the browser)") - image: str = Field( - "gns3/web-wireshark:latest", description="Docker image for the Web Wireshark containers") + True, description="Enable the Web Wireshark feature (container-based Wireshark in the browser)" + ) + image: str = Field("gns3/web-wireshark:latest", description="Docker image for the Web Wireshark containers") network_subnet: str = Field( "172.31.0.0/22", description="Docker network subnet for the Web Wireshark containers (change it if it conflicts " - "with your existing network)") + "with your existing network)", + ) memory: str = Field("2g", description='Memory limit per container (e.g. "512m", "2g")') cpus: float = Field(1.0, description="CPU cores per container (e.g. 1.0, 2.0)") pids_limit: int = Field(1000, description="Process limit per container") @@ -137,13 +127,11 @@ class WebWiresharkSettings(BaseModel): class ServerProtocol(str, Enum): - http = "http" https = "https" class UbridgeControlTransport(str, Enum): - # TCP control channel: -H host:port. ubridge now binds loopback by default, # so this is reachable only locally. Retained for backward compatibility. tcp = "tcp" @@ -153,7 +141,6 @@ class UbridgeControlTransport(str, Enum): class BuiltinSymbolTheme(str, Enum): - classic = "Classic" affinity_square_blue = "Affinity-square-blue" affinity_square_red = "Affinity-square-red" @@ -164,20 +151,20 @@ class BuiltinSymbolTheme(str, Enum): class ServerSettings(BaseModel): - local: bool = Field( - False, - description="Local server mode, set by the --local command line argument (not meant to be set by hand)") + False, description="Local server mode, set by the --local command line argument (not meant to be set by hand)" + ) enable_http_auth: bool = Field(True, description="Enable compute HTTP authentication") name: str = Field( f"{socket.gethostname()} (controller)", - description="Server name, default is what is returned by socket.gethostname()") - protocol: ServerProtocol = Field( - ServerProtocol.http, description="Protocol used by the server: http or https") + description="Server name, default is what is returned by socket.gethostname()", + ) + protocol: ServerProtocol = Field(ServerProtocol.http, description="Protocol used by the server: http or https") host: str = Field("0.0.0.0", description="IP address where the server listens for connections") port: int = Field(3080, gt=0, le=65535, description="HTTP port used to control the server") secrets_dir: Optional[DirectoryPath] = Field( - None, description="Directory where secrets are stored (e.g. the JWT secret key)") + None, description="Directory where secrets are stored (e.g. the JWT secret key)" + ) certfile: Optional[FilePath] = Field(None, description="SSL certificate file, requires enable_ssl") certkey: Optional[FilePath] = Field(None, description="SSL key file, requires enable_ssl") enable_ssl: bool = Field(False, description="Enable SSL encryption") @@ -189,80 +176,98 @@ class ServerSettings(BaseModel): resources_path: Optional[str] = Field( None, description="Path where files like built-in appliances and Docker resources are stored " - "(defaults to the local user data directory)") + "(defaults to the local user data directory)", + ) default_symbol_theme: BuiltinSymbolTheme = Field( BuiltinSymbolTheme.affinity_square_blue, - description='Default symbol theme, e.g. "Classic" or "Affinity-square-blue"') - allow_raw_images: bool = Field( - True, description="Allow raw images to be uploaded to the server") - auto_discover_images: bool = Field( - True, description="Automatically discover images in the images directory") - report_errors: bool = Field( - True, description="Automatically send crash reports to the GNS3 team") + description='Default symbol theme, e.g. "Classic" or "Affinity-square-blue"', + ) + allow_raw_images: bool = Field(True, description="Allow raw images to be uploaded to the server") + auto_discover_images: bool = Field(True, description="Automatically discover images in the images directory") + report_errors: bool = Field(True, description="Automatically send crash reports to the GNS3 team") additional_images_paths: List[str] = Field( default_factory=list, - description="Additional paths to look for images (semicolon-separated in the configuration file)") + description="Additional paths to look for images (semicolon-separated in the configuration file)", + ) console_start_port_range: int = Field( - 5000, gt=0, le=65535, description="First console port of the range allocated to devices") + 5000, gt=0, le=65535, description="First console port of the range allocated to devices" + ) console_end_port_range: int = Field( - 10000, gt=0, le=65535, description="Last console port of the range allocated to devices") + 10000, gt=0, le=65535, description="Last console port of the range allocated to devices" + ) vnc_console_start_port_range: int = Field( - 5900, ge=5900, le=65535, description="First VNC console port of the range allocated to devices") + 5900, ge=5900, le=65535, description="First VNC console port of the range allocated to devices" + ) vnc_console_end_port_range: int = Field( - 10000, ge=5900, le=65535, description="Last VNC console port of the range allocated to devices") + 10000, ge=5900, le=65535, description="Last VNC console port of the range allocated to devices" + ) udp_start_port_range: int = Field( - 10000, gt=0, le=65535, - description="First UDP port of the range allocated for inter-device communication (two ports per link)") + 10000, + gt=0, + le=65535, + description="First UDP port of the range allocated for inter-device communication (two ports per link)", + ) udp_end_port_range: int = Field( - 30000, gt=0, le=65535, - description="Last UDP port of the range allocated for inter-device communication (two ports per link)") + 30000, + gt=0, + le=65535, + description="Last UDP port of the range allocated for inter-device communication (two ports per link)", + ) ubridge_path: str = Field("ubridge", description="uBridge executable location, default: search in PATH") ubridge_control_transport: UbridgeControlTransport = Field( UbridgeControlTransport.unix, description='uBridge control channel transport: "unix" (AF_UNIX + SO_PEERCRED, recommended ' - 'on Linux) or "tcp" (loopback, kept for backward compatibility)') + 'on Linux) or "tcp" (loopback, kept for backward compatibility)', + ) marker_listen_host: str = Field( "127.0.0.1", description="Marker (traffic-insight) UDP sink listen host: one listener per compute process " - "receives uBridge MARK signals from every uBridge on this host") + "receives uBridge MARK signals from every uBridge on this host", + ) marker_listen_port: int = Field( - 3070, ge=0, le=65535, - description="Marker UDP sink listen port (0 lets the operating system choose a free port)") - compute_username: str = Field( - "gns3", description='Username for compute HTTP authentication, "gns3" is the default') + 3070, ge=0, le=65535, description="Marker UDP sink listen port (0 lets the operating system choose a free port)" + ) + compute_username: str = Field("gns3", description='Username for compute HTTP authentication, "gns3" is the default') compute_password: SecretStr = Field( SecretStr(""), - description="Password for compute HTTP authentication, a randomly generated password is used if not set") + description="Password for compute HTTP authentication, a randomly generated password is used if not set", + ) allowed_interfaces: List[str] = Field( default_factory=list, description="Only allow these interfaces to be used by GNS3, for the Cloud node for example " - "(comma-separated; do not forget virbr0 for the NAT node to work)") + "(comma-separated; do not forget virbr0 for the NAT node to work)", + ) default_nat_interface: Optional[str] = Field( - None, description="Interface used by the NAT node, default is virbr0 on Linux (requires libvirt)") + None, description="Interface used by the NAT node, default is virbr0 on Linux (requires libvirt)" + ) allow_remote_console: bool = Field( False, description="Allow console connections from remote machines " - "(console ports only accept local connections by default)") + "(console ports only accept local connections by default)", + ) enable_builtin_templates: bool = Field(True, description="Enable the built-in templates") install_builtin_appliances: bool = Field(True, description="Install the built-in appliances") skills_repo_url: str = Field( "https://github.com/gns3/gns3-skills.git", description="Git repository URL for the external GNS3 Copilot skills " - "(injection skills, prompts and device skills)") + "(injection skills, prompts and device skills)", + ) skills_repo_branch: str = Field("main", description="Git branch of the skills repository") skills_auto_update: bool = Field( - True, description="Automatically pull updates from the skills repository when reloading") + True, description="Automatically pull updates from the skills repository when reloading" + ) mcp_enable_dns_rebinding_protection: bool = Field( False, - description="Enable MCP transport DNS rebinding protection " - "(allowed hosts and origins must be configured)") + description="Enable MCP transport DNS rebinding protection (allowed hosts and origins must be configured)", + ) mcp_allowed_hosts: list[str] = Field( default_factory=list, description='Allowed hosts for MCP connections, only "host:*" port wildcards are supported ' - '(e.g. "127.0.0.1:*")') + '(e.g. "127.0.0.1:*")', + ) mcp_allowed_origins: list[str] = Field( - default_factory=list, - description='Allowed origins for MCP connections (e.g. "http://localhost:*")') + default_factory=list, description='Allowed origins for MCP connections (e.g. "http://localhost:*")' + ) model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) @@ -325,7 +330,6 @@ class ServerSettings(BaseModel): class ServerConfig(BaseModel): - Server: ServerSettings = ServerSettings() Controller: ControllerSettings = ControllerSettings() VPCS: VPCSSettings = VPCSSettings() diff --git a/gns3server/schemas/controller/appliances.py b/gns3server/schemas/controller/appliances.py index 9fb818b8a..a4aecaa94 100644 --- a/gns3server/schemas/controller/appliances.py +++ b/gns3server/schemas/controller/appliances.py @@ -27,330 +27,346 @@ from ..common import ExtraConfig # Shared Enums # ============================================================================ + class Category(str, Enum): """Appliance category enum""" - router = 'router' - multilayer_switch = 'multilayer_switch' - switch = 'switch' - firewall = 'firewall' - guest = 'guest' + router = "router" + multilayer_switch = "multilayer_switch" + switch = "switch" + firewall = "firewall" + guest = "guest" class Status(str, Enum): """Appliance status enum""" - stable = 'stable' - experimental = 'experimental' - broken = 'broken' + stable = "stable" + experimental = "experimental" + broken = "broken" class Availability(str, Enum): """Image availability enum""" - free = 'free' - with_registration = 'with-registration' - free_to_try = 'free-to-try' - service_contract = 'service-contract' + free = "free" + with_registration = "with-registration" + free_to_try = "free-to-try" + service_contract = "service-contract" class Compression(str, Enum): """Compression type enum""" - bzip2 = 'bzip2' - gzip = 'gzip' - lzma = 'lzma' - xz = 'xz' - rar = 'rar' - zip = 'zip' - field_7z = '7z' + bzip2 = "bzip2" + gzip = "gzip" + lzma = "lzma" + xz = "xz" + rar = "rar" + zip = "zip" + field_7z = "7z" class DynamipsPlatform(str, Enum): """Dynamips platform enum""" - c1700 = 'c1700' - c2600 = 'c2600' - c2691 = 'c2691' - c3725 = 'c3725' - c3745 = 'c3745' - c3600 = 'c3600' - c7200 = 'c7200' + c1700 = "c1700" + c2600 = "c2600" + c2691 = "c2691" + c3725 = "c3725" + c3745 = "c3745" + c3600 = "c3600" + c7200 = "c7200" class DynamipsChassis(str, Enum): """Dynamips chassis enum""" - chassis_1720 = '1720' - chassis_1721 = '1721' - chassis_1750 = '1750' - chassis_1751 = '1751' - chassis_1760 = '1760' - chassis_2610 = '2610' - chassis_2620 = '2620' - chassis_2610XM = '2610XM' - chassis_2620XM = '2620XM' - chassis_2650XM = '2650XM' - chassis_2621 = '2621' - chassis_2611XM = '2611XM' - chassis_2621XM = '2621XM' - chassis_2651XM = '2651XM' - chassis_3620 = '3620' - chassis_3640 = '3640' - chassis_3660 = '3660' - _ = '' + chassis_1720 = "1720" + chassis_1721 = "1721" + chassis_1750 = "1750" + chassis_1751 = "1751" + chassis_1760 = "1760" + chassis_2610 = "2610" + chassis_2620 = "2620" + chassis_2610XM = "2610XM" + chassis_2620XM = "2620XM" + chassis_2650XM = "2650XM" + chassis_2621 = "2621" + chassis_2611XM = "2611XM" + chassis_2621XM = "2621XM" + chassis_2651XM = "2651XM" + chassis_3620 = "3620" + chassis_3640 = "3640" + chassis_3660 = "3660" + _ = "" class DynamipsSlot(str, Enum): """Dynamips slot enum""" - C7200_IO_2FE = 'C7200-IO-2FE' - C7200_IO_FE = 'C7200-IO-FE' - C7200_IO_GE_E = 'C7200-IO-GE-E' - NM_16ESW = 'NM-16ESW' - NM_1E = 'NM-1E' - NM_1FE_TX = 'NM-1FE-TX' - NM_4E = 'NM-4E' - NM_4T = 'NM-4T' - PA_2FE_TX = 'PA-2FE-TX' - PA_4E = 'PA-4E' - PA_4T_ = 'PA-4T+' - PA_8E = 'PA-8E' - PA_8T = 'PA-8T' - PA_A1 = 'PA-A1' - PA_FE_TX = 'PA-FE-TX' - PA_GE = 'PA-GE' - PA_POS_OC3 = 'PA-POS-OC3' - C2600_MB_2FE = 'C2600-MB-2FE' - C2600_MB_1E = 'C2600-MB-1E' - C1700_MB_1FE = 'C1700-MB-1FE' - C2600_MB_2E = 'C2600-MB-2E' - C2600_MB_1FE = 'C2600-MB-1FE' - C1700_MB_WIC1 = 'C1700-MB-WIC1' - GT96100_FE = 'GT96100-FE' - Leopard_2FE = 'Leopard-2FE' - _ = '' + C7200_IO_2FE = "C7200-IO-2FE" + C7200_IO_FE = "C7200-IO-FE" + C7200_IO_GE_E = "C7200-IO-GE-E" + NM_16ESW = "NM-16ESW" + NM_1E = "NM-1E" + NM_1FE_TX = "NM-1FE-TX" + NM_4E = "NM-4E" + NM_4T = "NM-4T" + PA_2FE_TX = "PA-2FE-TX" + PA_4E = "PA-4E" + PA_4T_ = "PA-4T+" + PA_8E = "PA-8E" + PA_8T = "PA-8T" + PA_A1 = "PA-A1" + PA_FE_TX = "PA-FE-TX" + PA_GE = "PA-GE" + PA_POS_OC3 = "PA-POS-OC3" + C2600_MB_2FE = "C2600-MB-2FE" + C2600_MB_1E = "C2600-MB-1E" + C1700_MB_1FE = "C1700-MB-1FE" + C2600_MB_2E = "C2600-MB-2E" + C2600_MB_1FE = "C2600-MB-1FE" + C1700_MB_WIC1 = "C1700-MB-WIC1" + GT96100_FE = "GT96100-FE" + Leopard_2FE = "Leopard-2FE" + _ = "" + class DynamipsWic(str, Enum): """Dynamips WIC enum""" - WIC_1ENET = 'WIC-1ENET' - WIC_1T = 'WIC-1T' - WIC_2T = 'WIC-2T' - _ = '' + WIC_1ENET = "WIC-1ENET" + WIC_1T = "WIC-1T" + WIC_2T = "WIC-2T" + _ = "" + class DynamipsMidplane(str, Enum): """Dynamips midplane enum""" - std = 'std' - vxr = 'vxr' + std = "std" + vxr = "vxr" class DynamipsNpe(str, Enum): """Dynamips NPE enum""" - npe_100 = 'npe-100' - npe_150 = 'npe-150' - npe_175 = 'npe-175' - npe_200 = 'npe-200' - npe_225 = 'npe-225' - npe_300 = 'npe-300' - npe_400 = 'npe-400' - npe_g2 = 'npe-g2' + npe_100 = "npe-100" + npe_150 = "npe-150" + npe_175 = "npe-175" + npe_200 = "npe-200" + npe_225 = "npe-225" + npe_300 = "npe-300" + npe_400 = "npe-400" + npe_g2 = "npe-g2" class QemuConsoleType(str, Enum): """Qemu console type enum""" - telnet = 'telnet' - ssh = 'ssh' - vnc = 'vnc' - spice = 'spice' - spice_agent = 'spice+agent' - none = 'none' + telnet = "telnet" + ssh = "ssh" + vnc = "vnc" + spice = "spice" + spice_agent = "spice+agent" + none = "none" class QemuAdapterType(str, Enum): """Qemu adapter type enum""" - e1000 = 'e1000' - i82550 = 'i82550' - i82551 = 'i82551' - i82557a = 'i82557a' - i82557b = 'i82557b' - i82557c = 'i82557c' - i82558a = 'i82558a' - i82558b = 'i82558b' - i82559a = 'i82559a' - i82559b = 'i82559b' - i82559c = 'i82559c' - i82559er = 'i82559er' - i82562 = 'i82562' - i82801 = 'i82801' - igb = 'igb' - ne2k_pci = 'ne2k_pci' - pcnet = 'pcnet' - rtl8139 = 'rtl8139' - virtio = 'virtio' - virtio_net_pci = 'virtio-net-pci' - vmxnet3 = 'vmxnet3' + e1000 = "e1000" + i82550 = "i82550" + i82551 = "i82551" + i82557a = "i82557a" + i82557b = "i82557b" + i82557c = "i82557c" + i82558a = "i82558a" + i82558b = "i82558b" + i82559a = "i82559a" + i82559b = "i82559b" + i82559c = "i82559c" + i82559er = "i82559er" + i82562 = "i82562" + i82801 = "i82801" + igb = "igb" + ne2k_pci = "ne2k_pci" + pcnet = "pcnet" + rtl8139 = "rtl8139" + virtio = "virtio" + virtio_net_pci = "virtio-net-pci" + vmxnet3 = "vmxnet3" class QemuPlatform(str, Enum): """Qemu platform enum""" - aarch64 = 'aarch64' - alpha = 'alpha' - arm = 'arm' - cris = 'cris' - i386 = 'i386' - lm32 = 'lm32' - m68k = 'm68k' - microblaze = 'microblaze' - microblazeel = 'microblazeel' - mips = 'mips' - mips64 = 'mips64' - mips64el = 'mips64el' - mipsel = 'mipsel' - moxie = 'moxie' - or32 = 'or32' - ppc = 'ppc' - ppc64 = 'ppc64' - ppcemb = 'ppcemb' - s390x = 's390x' - sh4 = 'sh4' - sh4eb = 'sh4eb' - sparc = 'sparc' - sparc64 = 'sparc64' - tricore = 'tricore' - unicore32 = 'unicore32' - x86_64 = 'x86_64' - xtensa = 'xtensa' - xtensaeb = 'xtensaeb' + aarch64 = "aarch64" + alpha = "alpha" + arm = "arm" + cris = "cris" + i386 = "i386" + lm32 = "lm32" + m68k = "m68k" + microblaze = "microblaze" + microblazeel = "microblazeel" + mips = "mips" + mips64 = "mips64" + mips64el = "mips64el" + mipsel = "mipsel" + moxie = "moxie" + or32 = "or32" + ppc = "ppc" + ppc64 = "ppc64" + ppcemb = "ppcemb" + s390x = "s390x" + sh4 = "sh4" + sh4eb = "sh4eb" + sparc = "sparc" + sparc64 = "sparc64" + tricore = "tricore" + unicore32 = "unicore32" + x86_64 = "x86_64" + xtensa = "xtensa" + xtensaeb = "xtensaeb" class QemuDiskInterface(str, Enum): """Disk interface enum""" - ide = 'ide' - sata = 'sata' - nvme = 'nvme' - scsi = 'scsi' - sd = 'sd' - mtd = 'mtd' - floppy = 'floppy' - pflash = 'pflash' - virtio = 'virtio' - none = 'none' + ide = "ide" + sata = "sata" + nvme = "nvme" + scsi = "scsi" + sd = "sd" + mtd = "mtd" + floppy = "floppy" + pflash = "pflash" + virtio = "virtio" + none = "none" class QemuBootPriority(str, Enum): """Boot priority enum""" - c = 'c' - d = 'd' - n = 'n' - cn = 'cn' - cd = 'cd' - dn = 'dn' - dc = 'dc' - nc = 'nc' - nd = 'nd' + c = "c" + d = "d" + n = "n" + cn = "cn" + cd = "cd" + dn = "dn" + dc = "dc" + nc = "nc" + nd = "nd" class QemuOnClose(str, Enum): """Qemu on_close action enum""" - power_off = 'power_off' - shutdown_signal = 'shutdown_signal' - save_vm_state = 'save_vm_state' + power_off = "power_off" + shutdown_signal = "shutdown_signal" + save_vm_state = "save_vm_state" class QemuProcessPriority(str, Enum): """Qemu process priority enum""" - realtime = 'realtime' - very_high = 'very high' - high = 'high' - normal = 'normal' - low = 'low' - very_low = 'very low' + realtime = "realtime" + very_high = "very high" + high = "high" + normal = "normal" + low = "low" + very_low = "very low" class DockerConsoleType(str, Enum): """Docker console type enum""" - telnet = 'telnet' - ssh = 'ssh' - vnc = 'vnc' - http = 'http' - https = 'https' - none = 'none' - docker_exec = 'docker_exec' + telnet = "telnet" + ssh = "ssh" + vnc = "vnc" + http = "http" + https = "https" + none = "none" + docker_exec = "docker_exec" class ChecksumType(str, Enum): """Checksum type enum""" - md5 = 'md5' + md5 = "md5" class TemplateType(str, Enum): """Template type enum""" - docker = 'docker' - iou = 'iou' - dynamips = 'dynamips' - qemu = 'qemu' + docker = "docker" + iou = "iou" + dynamips = "dynamips" + qemu = "qemu" + # ============================================================================ # Version 1-6 Specific Enums # ============================================================================ + class Kvm(str, Enum): """KVM requirements enum""" - require = 'require' - allow = 'allow' - disable = 'disable' + require = "require" + allow = "allow" + disable = "disable" + # ============================================================================ # Version 1-6 Models # ============================================================================ + class Docker(BaseModel): """Docker configuration for v1-6""" - adapters: int = Field(..., title='Number of Ethernet adapters') - image: str = Field(..., title='Docker image in the Docker Hub') - start_command: Optional[str] = Field(None, title='Command executed when the container start. Empty will use the default') - environment: Optional[str] = Field(None, title='One KEY=VAR environment by line') - console_type: Optional[DockerConsoleType] = Field(None, title='Type of console connection for the administration of the appliance') - console_http_port: Optional[int] = Field(None, description='Internal port in the container of the HTTP server') - console_http_path: Optional[str] = Field(None, description='Path of the web interface') - extra_hosts: Optional[str] = Field(None, description='Hosts which will be written to /etc/hosts into container') - extra_volumes: Optional[List[str]] = Field(None, description='Additional directories to make persistent that are not included in the images VOLUME directive') - extra_configs: Optional[List[ExtraConfig]] = Field(None, description='Configuration files injected into the container (bind-mounted read-only)') + adapters: int = Field(..., title="Number of Ethernet adapters") + image: str = Field(..., title="Docker image in the Docker Hub") + start_command: Optional[str] = Field( + None, title="Command executed when the container start. Empty will use the default" + ) + environment: Optional[str] = Field(None, title="One KEY=VAR environment by line") + console_type: Optional[DockerConsoleType] = Field( + None, title="Type of console connection for the administration of the appliance" + ) + console_http_port: Optional[int] = Field(None, description="Internal port in the container of the HTTP server") + console_http_path: Optional[str] = Field(None, description="Path of the web interface") + extra_hosts: Optional[str] = Field(None, description="Hosts which will be written to /etc/hosts into container") + extra_volumes: Optional[List[str]] = Field( + None, + description="Additional directories to make persistent that are not included in the images VOLUME directive", + ) + extra_configs: Optional[List[ExtraConfig]] = Field( + None, description="Configuration files injected into the container (bind-mounted read-only)" + ) class Iou(BaseModel): """IOU configuration for v1-6""" - ethernet_adapters: int = Field(..., title='Number of Ethernet adapters') - serial_adapters: int = Field(..., title='Number of serial adapters') - nvram: int = Field(..., title='Host NVRAM') - ram: int = Field(..., title='Host RAM') - startup_config: str = Field(..., title='Config loaded at startup') + ethernet_adapters: int = Field(..., title="Number of Ethernet adapters") + serial_adapters: int = Field(..., title="Number of serial adapters") + nvram: int = Field(..., title="Host NVRAM") + ram: int = Field(..., title="Host RAM") + startup_config: str = Field(..., title="Config loaded at startup") class Dynamips(BaseModel): """Dynamips configuration for v1-6""" - chassis: Optional[DynamipsChassis] = Field(None, title='Chassis type') - platform: DynamipsPlatform = Field(..., title='Platform type') - ram: Annotated[int, Field(ge=1)] = Field(..., title='Amount of ram') - nvram: Annotated[int, Field(ge=1)] = Field(..., title='Amount of nvram') - startup_config: Optional[str] = Field(None, title='Config loaded at startup') + chassis: Optional[DynamipsChassis] = Field(None, title="Chassis type") + platform: DynamipsPlatform = Field(..., title="Platform type") + ram: Annotated[int, Field(ge=1)] = Field(..., title="Amount of ram") + nvram: Annotated[int, Field(ge=1)] = Field(..., title="Amount of nvram") + startup_config: Optional[str] = Field(None, title="Config loaded at startup") wic0: Optional[DynamipsWic] = None wic1: Optional[DynamipsWic] = None wic2: Optional[DynamipsWic] = None @@ -368,69 +384,76 @@ class Dynamips(BaseModel): class Qemu(BaseModel): """QEMU configuration for v1-6""" - adapter_type: QemuAdapterType = Field(..., title='Type of network adapter') - adapters: int = Field(..., title='Number of adapters') - ram: int = Field(..., title='RAM allocated to the appliance (MB)') - cpus: Optional[int] = Field(None, title='Number of Virtual CPU') - hda_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hda_disk_image') - hdb_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdb_disk_image') - hdc_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdc_disk_image') - hdd_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdd_disk_image') - arch: QemuPlatform = Field(..., title='Architecture emulated') - console_type: QemuConsoleType = Field(..., title='Type of console connection for the administration of the appliance') - boot_priority: Optional[QemuBootPriority] = Field(None, title='Disk boot priority') - kernel_command_line: Optional[str] = Field(None, title='Command line parameters sent to the kernel') - kvm: Kvm = Field(..., title='KVM requirements') - options: Optional[str] = Field(None, title='Optional additional qemu command line options') - cpu_throttling: Optional[Annotated[float, Field(ge=0.0, le=100.0)]] = Field(None, title='Throttle the CPU') - on_close: Optional[QemuOnClose] = Field(None, title='Action to execute on the VM is closed') - process_priority: Optional[QemuProcessPriority] = Field(None, title='Process priority for QEMU') + adapter_type: QemuAdapterType = Field(..., title="Type of network adapter") + adapters: int = Field(..., title="Number of adapters") + ram: int = Field(..., title="RAM allocated to the appliance (MB)") + cpus: Optional[int] = Field(None, title="Number of Virtual CPU") + hda_disk_interface: Optional[QemuDiskInterface] = Field( + None, title="Disk interface for the installed hda_disk_image" + ) + hdb_disk_interface: Optional[QemuDiskInterface] = Field( + None, title="Disk interface for the installed hdb_disk_image" + ) + hdc_disk_interface: Optional[QemuDiskInterface] = Field( + None, title="Disk interface for the installed hdc_disk_image" + ) + hdd_disk_interface: Optional[QemuDiskInterface] = Field( + None, title="Disk interface for the installed hdd_disk_image" + ) + arch: QemuPlatform = Field(..., title="Architecture emulated") + console_type: QemuConsoleType = Field( + ..., title="Type of console connection for the administration of the appliance" + ) + boot_priority: Optional[QemuBootPriority] = Field(None, title="Disk boot priority") + kernel_command_line: Optional[str] = Field(None, title="Command line parameters sent to the kernel") + kvm: Kvm = Field(..., title="KVM requirements") + options: Optional[str] = Field(None, title="Optional additional qemu command line options") + cpu_throttling: Optional[Annotated[float, Field(ge=0.0, le=100.0)]] = Field(None, title="Throttle the CPU") + on_close: Optional[QemuOnClose] = Field(None, title="Action to execute on the VM is closed") + process_priority: Optional[QemuProcessPriority] = Field(None, title="Process priority for QEMU") class ApplianceVersionImages(BaseModel): """Appliance version images configuration for v1-6""" - kernel_image: Optional[str] = Field(None, title='Kernel image') - initrd: Optional[str] = Field(None, title='Initrd disk image') - image: Optional[str] = Field(None, title='OS image') - bios_image: Optional[str] = Field(None, title='Bios image') - hda_disk_image: Optional[str] = Field(None, title='Hda disk image') - hdb_disk_image: Optional[str] = Field(None, title='Hdc disk image') - hdc_disk_image: Optional[str] = Field(None, title='Hdd disk image') - hdd_disk_image: Optional[str] = Field(None, title='Hdd diskimage') - cdrom_image: Optional[str] = Field(None, title='cdrom image') + kernel_image: Optional[str] = Field(None, title="Kernel image") + initrd: Optional[str] = Field(None, title="Initrd disk image") + image: Optional[str] = Field(None, title="OS image") + bios_image: Optional[str] = Field(None, title="Bios image") + hda_disk_image: Optional[str] = Field(None, title="Hda disk image") + hdb_disk_image: Optional[str] = Field(None, title="Hdc disk image") + hdc_disk_image: Optional[str] = Field(None, title="Hdd disk image") + hdd_disk_image: Optional[str] = Field(None, title="Hdd diskimage") + cdrom_image: Optional[str] = Field(None, title="cdrom image") class ApplianceVersion(BaseModel): """Appliance version definition for v1-6""" - name: str = Field(..., title='Name of the version') - idlepc: Optional[str] = Field(None, pattern='^0x[0-9a-f]{8}') - images: Optional[ApplianceVersionImages] = Field(None, title='Images used for this version') + name: str = Field(..., title="Name of the version") + idlepc: Optional[str] = Field(None, pattern="^0x[0-9a-f]{8}") + images: Optional[ApplianceVersionImages] = Field(None, title="Images used for this version") class ApplianceImage(BaseModel): """Appliance image definition - compatible with both versions""" - filename: str = Field(..., title='Filename') - version: str = Field(..., title='Version of the file') - md5sum: Optional[str] = Field(None, title='md5sum of the file', pattern='^[a-f0-9]{32}$') - filesize: int = Field(..., title='File size in bytes') + filename: str = Field(..., title="Filename") + version: str = Field(..., title="Version of the file") + md5sum: Optional[str] = Field(None, title="md5sum of the file", pattern="^[a-f0-9]{32}$") + filesize: int = Field(..., title="File size in bytes") download_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field( - None, - title='Download url where you can download the appliance from a browser' + None, title="Download url where you can download the appliance from a browser" ) direct_download_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field( None, - title='Optional. Non authenticated url to the image file where you can download the image.', + title="Optional. Non authenticated url to the image file where you can download the image.", ) - compression: Optional[Compression] = Field( - None, title='Optional, compression type of direct download url image.' - ) - checksum: Optional[str] = Field(None, title='checksum of the image file') - checksum_type: Optional[ChecksumType] = Field(None, title='checksum type of the image file') + compression: Optional[Compression] = Field(None, title="Optional, compression type of direct download url image.") + checksum: Optional[str] = Field(None, title="checksum of the image file") + checksum_type: Optional[ChecksumType] = Field(None, title="checksum type of the image file") compression_target: Optional[str] = Field( - None, title='Optional, file name of the image file inside the compressed file.' + None, title="Optional, file name of the image file inside the compressed file." ) @@ -438,84 +461,79 @@ class ApplianceImage(BaseModel): # Version 8 Models # ============================================================================ + class CustomAdapterItem(BaseModel): """Custom adapter configuration (v8)""" - adapter_number: int = Field(..., title='Adapter number') - port_name: Optional[str] = Field(None, title='Custom port name') - adapter_type: Optional[QemuAdapterType] = Field(None, title='Custom adapter type') + adapter_number: int = Field(..., title="Adapter number") + port_name: Optional[str] = Field(None, title="Custom port name") + adapter_type: Optional[QemuAdapterType] = Field(None, title="Custom adapter type") mac_address: Optional[str] = Field( None, - title='Custom MAC address', - pattern=r'^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$', + title="Custom MAC address", + pattern=r"^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$", ) class DockerPropertiesV8(BaseModel): """Docker template properties (v8)""" - name: Optional[str] = Field(None, title='Name of the template') - category: Optional[Category] = Field(None, title='Category of the template') - default_name_format: Optional[str] = Field(None, title='Default name format') - usage: Optional[str] = Field(None, title='How to use the template') - symbol: Optional[str] = Field(None, title='Symbol of the template') - image: str = Field(..., title='Docker image') - adapters: Optional[int] = Field(None, title='Number of ethernet adapters') + name: Optional[str] = Field(None, title="Name of the template") + category: Optional[Category] = Field(None, title="Category of the template") + default_name_format: Optional[str] = Field(None, title="Default name format") + usage: Optional[str] = Field(None, title="How to use the template") + symbol: Optional[str] = Field(None, title="Symbol of the template") + image: str = Field(..., title="Docker image") + adapters: Optional[int] = Field(None, title="Number of ethernet adapters") start_command: Optional[str] = Field( - None, title='Command executed when the container start. Empty will use the default' + None, title="Command executed when the container start. Empty will use the default" ) - environment: Optional[str] = Field(None, title='One KEY=VAR environment by line') - console_type: Optional[DockerConsoleType] = Field( - None, title='Type of console' - ) - console_http_port: Optional[int] = Field( - None, title='Internal port in the container of the HTTP server' - ) - console_http_path: Optional[str] = Field(None, title='Path of the web interface') + environment: Optional[str] = Field(None, title="One KEY=VAR environment by line") + console_type: Optional[DockerConsoleType] = Field(None, title="Type of console") + console_http_port: Optional[int] = Field(None, title="Internal port in the container of the HTTP server") + console_http_path: Optional[str] = Field(None, title="Path of the web interface") console_resolution: Optional[str] = Field( None, - title='Console resolution for VNC, for example 1024x768', - pattern=r'^[0-9]+x[0-9]+$', + title="Console resolution for VNC, for example 1024x768", + pattern=r"^[0-9]+x[0-9]+$", ) - extra_hosts: Optional[str] = Field(None, title='Docker extra hosts (added to /etc/hosts)') - extra_volumes: Optional[List[str]] = Field( - None, title='Additional directories to make persistent' - ) - custom_adapters: Optional[List[CustomAdapterItem]] = Field(None, title='Custom adapters') + extra_hosts: Optional[str] = Field(None, title="Docker extra hosts (added to /etc/hosts)") + extra_volumes: Optional[List[str]] = Field(None, title="Additional directories to make persistent") + custom_adapters: Optional[List[CustomAdapterItem]] = Field(None, title="Custom adapters") extra_configs: Optional[List[ExtraConfig]] = Field( - None, title='Configuration files injected into the container (bind-mounted read-only)' + None, title="Configuration files injected into the container (bind-mounted read-only)" ) class IouPropertiesV8(BaseModel): """IOU template properties (v8)""" - name: Optional[str] = Field(None, title='Name of the template') - category: Optional[Category] = Field(None, title='Category of the template') - default_name_format: Optional[str] = Field(None, title='Default name format') - usage: Optional[str] = Field(None, title='How to use the template') - symbol: Optional[str] = Field(None, title='Symbol of the template') - ethernet_adapters: Optional[int] = Field(None, title='Number of ethernet adapters') - serial_adapters: Optional[int] = Field(None, title='Number of serial adapters') - ram: Optional[int] = Field(None, title='Host RAM') - nvram: Optional[int] = Field(None, title='Host NVRAM') - startup_config: Optional[str] = Field(None, title='Config loaded at startup') + name: Optional[str] = Field(None, title="Name of the template") + category: Optional[Category] = Field(None, title="Category of the template") + default_name_format: Optional[str] = Field(None, title="Default name format") + usage: Optional[str] = Field(None, title="How to use the template") + symbol: Optional[str] = Field(None, title="Symbol of the template") + ethernet_adapters: Optional[int] = Field(None, title="Number of ethernet adapters") + serial_adapters: Optional[int] = Field(None, title="Number of serial adapters") + ram: Optional[int] = Field(None, title="Host RAM") + nvram: Optional[int] = Field(None, title="Host NVRAM") + startup_config: Optional[str] = Field(None, title="Config loaded at startup") class DynamipsPropertiesV8(BaseModel): """Dynamips template properties (v8)""" - name: Optional[str] = Field(None, title='Name of the template') - category: Optional[Category] = Field(None, title='Category of the template') - default_name_format: Optional[str] = Field(None, title='Default name format') - usage: Optional[str] = Field(None, title='How to use the template') - symbol: Optional[str] = Field(None, title='Symbol of the template') - chassis: Optional[DynamipsChassis] = Field(None, title='Chassis type') - platform: Optional[DynamipsPlatform] = Field(None, title='Platform type') - ram: Optional[Annotated[int, Field(ge=1)]] = Field(None, title='Amount of ram') - nvram: Optional[Annotated[int, Field(ge=1)]] = Field(None, title='Amount of nvram') - idlepc: Optional[str] = Field(None, pattern=r'^0x[0-9a-f]{8}') - startup_config: Optional[str] = Field(None, title='Config loaded at startup') + name: Optional[str] = Field(None, title="Name of the template") + category: Optional[Category] = Field(None, title="Category of the template") + default_name_format: Optional[str] = Field(None, title="Default name format") + usage: Optional[str] = Field(None, title="How to use the template") + symbol: Optional[str] = Field(None, title="Symbol of the template") + chassis: Optional[DynamipsChassis] = Field(None, title="Chassis type") + platform: Optional[DynamipsPlatform] = Field(None, title="Platform type") + ram: Optional[Annotated[int, Field(ge=1)]] = Field(None, title="Amount of ram") + nvram: Optional[Annotated[int, Field(ge=1)]] = Field(None, title="Amount of nvram") + idlepc: Optional[str] = Field(None, pattern=r"^0x[0-9a-f]{8}") + startup_config: Optional[str] = Field(None, title="Config loaded at startup") wic0: Optional[str] = Field(None) wic1: Optional[str] = Field(None) wic2: Optional[str] = Field(None) @@ -533,47 +551,51 @@ class DynamipsPropertiesV8(BaseModel): class QemuPropertiesV8(BaseModel): """Qemu template properties (v8)""" - name: Optional[str] = Field(None, title='Name of the template') - category: Optional[Category] = Field(None, title='Category of the template') - default_name_format: Optional[str] = Field(None, title='Default name format') - usage: Optional[str] = Field(None, title='How to use the template') - symbol: Optional[str] = Field(None, title='Symbol of the template') - adapter_type: Optional[QemuAdapterType] = Field(None, title='Type of network adapter') - adapters: Optional[int] = Field(None, title='Number of adapters') - custom_adapters: Optional[List[CustomAdapterItem]] = Field(None, title='Custom adapters') - first_port_name: Optional[str] = Field( - None, title='Optional name of the first networking port example: eth0' - ) - port_name_format: Optional[str] = Field( - None, title='Optional formating of the networking port example: eth{0}' - ) + name: Optional[str] = Field(None, title="Name of the template") + category: Optional[Category] = Field(None, title="Category of the template") + default_name_format: Optional[str] = Field(None, title="Default name format") + usage: Optional[str] = Field(None, title="How to use the template") + symbol: Optional[str] = Field(None, title="Symbol of the template") + adapter_type: Optional[QemuAdapterType] = Field(None, title="Type of network adapter") + adapters: Optional[int] = Field(None, title="Number of adapters") + custom_adapters: Optional[List[CustomAdapterItem]] = Field(None, title="Custom adapters") + first_port_name: Optional[str] = Field(None, title="Optional name of the first networking port example: eth0") + port_name_format: Optional[str] = Field(None, title="Optional formating of the networking port example: eth{0}") port_segment_size: Optional[int] = Field( None, - title='Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2', + title="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2", ) linked_clone: Optional[bool] = Field(None, title="False if you don't want to use a single image for all nodes") - ram: Optional[int] = Field(None, title='Ram allocated to the appliance (MB)') - cpus: Optional[int] = Field(None, title='Number of Virtual CPU') - hda_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hda_disk_image') - hdb_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdb_disk_image') - hdc_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdc_disk_image') - hdd_disk_interface: Optional[QemuDiskInterface] = Field(None, title='Disk interface for the installed hdd_disk_image') - platform: Optional[QemuPlatform] = Field(None, title='Platform to emulate') + ram: Optional[int] = Field(None, title="Ram allocated to the appliance (MB)") + cpus: Optional[int] = Field(None, title="Number of Virtual CPU") + hda_disk_interface: Optional[QemuDiskInterface] = Field( + None, title="Disk interface for the installed hda_disk_image" + ) + hdb_disk_interface: Optional[QemuDiskInterface] = Field( + None, title="Disk interface for the installed hdb_disk_image" + ) + hdc_disk_interface: Optional[QemuDiskInterface] = Field( + None, title="Disk interface for the installed hdc_disk_image" + ) + hdd_disk_interface: Optional[QemuDiskInterface] = Field( + None, title="Disk interface for the installed hdd_disk_image" + ) + platform: Optional[QemuPlatform] = Field(None, title="Platform to emulate") console_type: Optional[QemuConsoleType] = Field( - None, title='Type of console connection for the administration of the appliance' + None, title="Type of console connection for the administration of the appliance" ) boot_priority: Optional[QemuBootPriority] = Field( None, - title='Optional define the disk boot priory. Refer to -boot option in qemu manual for more details.', + title="Optional define the disk boot priory. Refer to -boot option in qemu manual for more details.", ) - kernel_command_line: Optional[str] = Field(None, title='Command line parameters send to the kernel') - kvm: Optional[Kvm] = Field(None, title='KVM requirements') - options: Optional[str] = Field(None, title='Optional additional qemu command line options') - cpu_throttling: Optional[Annotated[int, Field(ge=0, le=800)]] = Field(None, title='Throttle the CPU') - tpm: Optional[bool] = Field(None, title='Enable the Trusted Platform Module (TPM)') - uefi: Optional[bool] = Field(None, title='Enable the UEFI boot mode') - on_close: Optional[QemuOnClose] = Field(None, title='Action to execute on the VM is closed') - process_priority: Optional[QemuProcessPriority] = Field(None, title='Process priority for QEMU') + kernel_command_line: Optional[str] = Field(None, title="Command line parameters send to the kernel") + kvm: Optional[Kvm] = Field(None, title="KVM requirements") + options: Optional[str] = Field(None, title="Optional additional qemu command line options") + cpu_throttling: Optional[Annotated[int, Field(ge=0, le=800)]] = Field(None, title="Throttle the CPU") + tpm: Optional[bool] = Field(None, title="Enable the Trusted Platform Module (TPM)") + uefi: Optional[bool] = Field(None, title="Enable the UEFI boot mode") + on_close: Optional[QemuOnClose] = Field(None, title="Action to execute on the VM is closed") + process_priority: Optional[QemuProcessPriority] = Field(None, title="Process priority for QEMU") _V8_PROPERTIES_MODELS = { @@ -587,16 +609,15 @@ _V8_PROPERTIES_MODELS = { class TemplateSetting(BaseModel): """Emulator settings configuration (v8)""" - name: Optional[str] = Field(None, title='Name of the settings set') - default: Optional[bool] = Field(None, title='Whether these are the default settings') - inherit_default_properties: Optional[bool] = Field(True, title='Whether the default properties should be used') - template_type: TemplateType = Field(..., title='Type of emulator properties') + name: Optional[str] = Field(None, title="Name of the settings set") + default: Optional[bool] = Field(None, title="Whether these are the default settings") + inherit_default_properties: Optional[bool] = Field(True, title="Whether the default properties should be used") + template_type: TemplateType = Field(..., title="Type of emulator properties") template_properties: Union[QemuPropertiesV8, DynamipsPropertiesV8, IouPropertiesV8, DockerPropertiesV8] = Field( - ..., - title='Properties for the template' + ..., title="Properties for the template" ) - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def _validate_template_properties(cls, data): """ @@ -621,68 +642,76 @@ class TemplateSetting(BaseModel): class ApplianceVersionV8(BaseModel): """Appliance version definition (v8)""" - name: str = Field(..., title='Name of the version') - settings: Optional[str] = Field(None, title='Template settings to use to run the version') - idlepc: Optional[str] = Field(None, pattern=r'^0x[0-9a-f]{8}') - category: Optional[Category] = Field(None, title='Category of the version') - installation_instructions: Optional[str] = Field(None, title='Optional installation instructions for the version') - usage: Optional[str] = Field(None, title='Optional instructions about using the version') - default_username: Optional[str] = Field(None, title='Default username for the version') - default_password: Optional[str] = Field(None, title='Default password for the version') - symbol: Optional[str] = Field(None, title='An optional symbol for the version') - images: Optional[ApplianceVersionImages] = Field(None, title='Images used for this version') + name: str = Field(..., title="Name of the version") + settings: Optional[str] = Field(None, title="Template settings to use to run the version") + idlepc: Optional[str] = Field(None, pattern=r"^0x[0-9a-f]{8}") + category: Optional[Category] = Field(None, title="Category of the version") + installation_instructions: Optional[str] = Field(None, title="Optional installation instructions for the version") + usage: Optional[str] = Field(None, title="Optional instructions about using the version") + default_username: Optional[str] = Field(None, title="Default username for the version") + default_password: Optional[str] = Field(None, title="Default password for the version") + symbol: Optional[str] = Field(None, title="An optional symbol for the version") + images: Optional[ApplianceVersionImages] = Field(None, title="Images used for this version") # ============================================================================ # Child Models with Discriminated Union # ============================================================================ + class ApplianceV1_6(BaseModel): """GNS3 Appliance model for registry versions 1-6""" - registry_version: Literal[1, 2, 3, 4, 5, 6] = Field(..., title='Version of the registry compatible with this appliance') - appliance_id: UUID = Field(..., title='Appliance ID') - name: str = Field(..., title='Appliance name') - builtin: Optional[bool] = Field(None, title='Whether the appliance is builtin or not') - category: Category = Field(..., title='Category of the appliance') - description: str = Field(..., title='Description of the appliance. Could be a marketing description') - vendor_name: str = Field(..., title='Name of the vendor') - vendor_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field(None, title='Website of the vendor') - documentation_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field( - None, - title='An optional documentation for using the appliance on vendor website' + registry_version: Literal[1, 2, 3, 4, 5, 6] = Field( + ..., title="Version of the registry compatible with this appliance" ) - product_name: str = Field(..., title='Product name') - product_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field(None, title='An optional product url on vendor website') - status: Status = Field(..., title='Document if the appliance is working or not') + appliance_id: UUID = Field(..., title="Appliance ID") + name: str = Field(..., title="Appliance name") + builtin: Optional[bool] = Field(None, title="Whether the appliance is builtin or not") + category: Category = Field(..., title="Category of the appliance") + description: str = Field(..., title="Description of the appliance. Could be a marketing description") + vendor_name: str = Field(..., title="Name of the vendor") + vendor_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field( + None, title="Website of the vendor" + ) + documentation_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field( + None, title="An optional documentation for using the appliance on vendor website" + ) + product_name: str = Field(..., title="Product name") + product_url: Optional[Union[AnyUrl, Annotated[str, Field(max_length=0)]]] = Field( + None, title="An optional product url on vendor website" + ) + status: Status = Field(..., title="Document if the appliance is working or not") availability: Optional[Availability] = Field( None, - title='About image availability: can be downloaded directly; download requires a free registration; paid but a trial version (time or feature limited) is available; not available publicly', + title="About image availability: can be downloaded directly; download requires a free registration; paid but a trial version (time or feature limited) is available; not available publicly", ) - maintainer: str = Field(..., title='Maintainer name') - maintainer_email: Optional[Union[EmailStr, Annotated[str, Field(max_length=0)]]] = Field(None, title='Maintainer email') - usage: Optional[str] = Field(None, title='How to use the appliance') - symbol: Optional[str] = Field(None, title='An optional symbol for the appliance') + maintainer: str = Field(..., title="Maintainer name") + maintainer_email: Optional[Union[EmailStr, Annotated[str, Field(max_length=0)]]] = Field( + None, title="Maintainer email" + ) + usage: Optional[str] = Field(None, title="How to use the appliance") + symbol: Optional[str] = Field(None, title="An optional symbol for the appliance") netmiko_device_type: Optional[str] = Field( - None, title='Device type for Netmiko-based automation tools', pattern=r'^[a-z0-9_]+$|^$' + None, title="Device type for Netmiko-based automation tools", pattern=r"^[a-z0-9_]+$|^$" ) - first_port_name: Optional[str] = Field(None, title='Optional name of the first networking port example: eth0') - port_name_format: Optional[str] = Field(None, title='Optional formating of the networking port example: eth{0}') + first_port_name: Optional[str] = Field(None, title="Optional name of the first networking port example: eth0") + port_name_format: Optional[str] = Field(None, title="Optional formating of the networking port example: eth{0}") port_segment_size: Optional[int] = Field( None, - title='Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2', + title="Optional port segment size. A port segment is a block of port. For example Ethernet0/0 Ethernet0/1 is the module 0 with a port segment size of 2", ) custom_adapters: Optional[List[CustomAdapterItem]] = Field( - None, title='Optional per-adapter overrides (port name, adapter type, MAC address)' + None, title="Optional per-adapter overrides (port name, adapter type, MAC address)" ) linked_clone: Optional[bool] = Field(None, title="False if you don't want to use a single image for all nodes") - docker: Optional[Docker] = Field(None, title='Docker specific options') - iou: Optional[Iou] = Field(None, title='IOU specific options') - dynamips: Optional[Dynamips] = Field(None, title='Dynamips specific options') - qemu: Optional[Qemu] = Field(None, title='Qemu specific options') - tags: Optional[List[str]] = Field(None, title='User-defined metadata tags for the appliance') - images: Optional[List[ApplianceImage]] = Field(None, title='Images for this appliance') - versions: Optional[List[ApplianceVersion]] = Field(None, title='Versions of the appliance') + docker: Optional[Docker] = Field(None, title="Docker specific options") + iou: Optional[Iou] = Field(None, title="IOU specific options") + dynamips: Optional[Dynamips] = Field(None, title="Dynamips specific options") + qemu: Optional[Qemu] = Field(None, title="Qemu specific options") + tags: Optional[List[str]] = Field(None, title="User-defined metadata tags for the appliance") + images: Optional[List[ApplianceImage]] = Field(None, title="Images for this appliance") + versions: Optional[List[ApplianceVersion]] = Field(None, title="Versions of the appliance") class ApplianceV8(BaseModel): @@ -690,42 +719,44 @@ class ApplianceV8(BaseModel): registry_version: Literal[8] = Field( ..., - title='Version of the registry compatible with this appliance (version >=8 introduced breaking changes)', + title="Version of the registry compatible with this appliance (version >=8 introduced breaking changes)", ) appliance_id: str = Field( ..., - title='Appliance ID', - pattern=r'^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$', + title="Appliance ID", + pattern=r"^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", ) - name: str = Field(..., title='Appliance name') - builtin: Optional[bool] = Field(None, title='Whether the appliance is builtin or not') - category: Category = Field(..., title='Category of the appliance') - description: str = Field(..., title='Description of the appliance. Could be a marketing description') - vendor_name: str = Field(..., title='Name of the vendor') - vendor_url: AnyUrl = Field(..., title='Website of the vendor') - vendor_logo_url: Optional[AnyUrl] = Field(None, title='Link to the vendor logo (used by the GNS3 marketplace)') - documentation_url: Optional[AnyUrl] = Field(None, title='An optional documentation for using the appliance on vendor website') - product_name: str = Field(..., title='Product name') - product_url: Optional[AnyUrl] = Field(None, title='An optional product url on vendor website') - status: Status = Field(..., title='Document if the appliance is working or not') + name: str = Field(..., title="Appliance name") + builtin: Optional[bool] = Field(None, title="Whether the appliance is builtin or not") + category: Category = Field(..., title="Category of the appliance") + description: str = Field(..., title="Description of the appliance. Could be a marketing description") + vendor_name: str = Field(..., title="Name of the vendor") + vendor_url: AnyUrl = Field(..., title="Website of the vendor") + vendor_logo_url: Optional[AnyUrl] = Field(None, title="Link to the vendor logo (used by the GNS3 marketplace)") + documentation_url: Optional[AnyUrl] = Field( + None, title="An optional documentation for using the appliance on vendor website" + ) + product_name: str = Field(..., title="Product name") + product_url: Optional[AnyUrl] = Field(None, title="An optional product url on vendor website") + status: Status = Field(..., title="Document if the appliance is working or not") availability: Optional[Availability] = Field( None, - title='About image availability: can be downloaded directly; download requires a free registration; paid but a trial version (time or feature limited) is available; not available publicly', + title="About image availability: can be downloaded directly; download requires a free registration; paid but a trial version (time or feature limited) is available; not available publicly", ) - maintainer: str = Field(..., title='Maintainer name') - maintainer_email: EmailStr = Field(..., title='Maintainer email') - installation_instructions: Optional[str] = Field(None, title='Optional installation instructions') - usage: Optional[str] = Field(None, title='How to use the appliance') - default_username: Optional[str] = Field(None, title='Default username for the appliance') - default_password: Optional[str] = Field(None, title='Default password for the appliance') - symbol: Optional[str] = Field(None, title='An optional symbol for the appliance') + maintainer: str = Field(..., title="Maintainer name") + maintainer_email: EmailStr = Field(..., title="Maintainer email") + installation_instructions: Optional[str] = Field(None, title="Optional installation instructions") + usage: Optional[str] = Field(None, title="How to use the appliance") + default_username: Optional[str] = Field(None, title="Default username for the appliance") + default_password: Optional[str] = Field(None, title="Default password for the appliance") + symbol: Optional[str] = Field(None, title="An optional symbol for the appliance") netmiko_device_type: Optional[str] = Field( - None, title='Device type for Netmiko-based automation tools', pattern=r'^[a-z0-9_]+$|^$' + None, title="Device type for Netmiko-based automation tools", pattern=r"^[a-z0-9_]+$|^$" ) - tags: Optional[List[str]] = Field(None, title='User-defined metadata tags for the appliance') - settings: List[TemplateSetting] = Field(..., title='Settings for running the appliance') - images: Optional[List[ApplianceImage]] = Field(None, title='Images for this appliance') - versions: Optional[List[ApplianceVersionV8]] = Field(None, title='Versions of the appliance') + tags: Optional[List[str]] = Field(None, title="User-defined metadata tags for the appliance") + settings: List[TemplateSetting] = Field(..., title="Settings for running the appliance") + images: Optional[List[ApplianceImage]] = Field(None, title="Images for this appliance") + versions: Optional[List[ApplianceVersionV8]] = Field(None, title="Versions of the appliance") # ============================================================================ @@ -735,10 +766,10 @@ class ApplianceV8(BaseModel): # Define the discriminated union type ApplianceUnion = Annotated[ Union[ - Annotated[ApplianceV1_6, Tag('v1_6')], - Annotated[ApplianceV8, Tag('v8')], + Annotated[ApplianceV1_6, Tag("v1_6")], + Annotated[ApplianceV8, Tag("v8")], ], - Discriminator('registry_version'), + Discriminator("registry_version"), ] """ Discriminated union type supporting both registry versions 1-6 and 8. diff --git a/gns3server/schemas/controller/base.py b/gns3server/schemas/controller/base.py index 08f446998..e413dd479 100644 --- a/gns3server/schemas/controller/base.py +++ b/gns3server/schemas/controller/base.py @@ -20,6 +20,5 @@ from pydantic import BaseModel class DateTimeModelMixin(BaseModel): - created_at: Optional[datetime] = None updated_at: Optional[datetime] = None diff --git a/gns3server/schemas/controller/chat.py b/gns3server/schemas/controller/chat.py index 36f58036e..9289baf0b 100644 --- a/gns3server/schemas/controller/chat.py +++ b/gns3server/schemas/controller/chat.py @@ -40,7 +40,7 @@ class ChatRequest(BaseModel): None, description="LLM temperature parameter (NOTE: currently not used. " "Temperature is loaded from user's LLM config in database. " - "Reserved for future runtime override support.)" + "Reserved for future runtime override support.)", ) mode: Literal["text"] = Field(default="text", description="Interaction mode") @@ -49,14 +49,14 @@ class ChatResponse(BaseModel): """Chat streaming response model.""" type: Literal[ - "content", # AI text content - "tool_call", # Tool call request - "tool_start", # Tool execution started - "tool_end", # Tool execution completed - "error", # Error message - "done", # Stream ended - "heartbeat", # Keep-alive signal - "abort" # Stream aborted + "content", # AI text content + "tool_call", # Tool call request + "tool_start", # Tool execution started + "tool_end", # Tool execution completed + "error", # Error message + "done", # Stream ended + "heartbeat", # Keep-alive signal + "abort", # Stream aborted ] = Field(..., description="Response message type") content: Optional[str] = Field(None, description="Text content (for type=content)") message_id: Optional[str] = Field(None, description="Message ID") diff --git a/gns3server/schemas/controller/computes.py b/gns3server/schemas/controller/computes.py index ecb4e3339..1c87d95a0 100644 --- a/gns3server/schemas/controller/computes.py +++ b/gns3server/schemas/controller/computes.py @@ -16,14 +16,7 @@ import uuid -from pydantic import ( - ConfigDict, - BaseModel, - Field, - SecretStr, - field_validator, - model_validator -) +from pydantic import ConfigDict, BaseModel, Field, SecretStr, field_validator, model_validator from typing import List, Optional, Union, Any from enum import Enum @@ -60,27 +53,22 @@ class ComputeCreate(ComputeBase): """ compute_id: Union[str, uuid.UUID] = None - model_config = ConfigDict(json_schema_extra={ - "example": { - "name": "My compute", - "host": "127.0.0.1", - "port": 3080, - "user": "user", - "password": "password" + model_config = ConfigDict( + json_schema_extra={ + "example": {"name": "My compute", "host": "127.0.0.1", "port": 3080, "user": "user", "password": "password"} } - }) + ) - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def set_default_compute_id_and_name(cls, data: Any) -> Any: if "compute_id" not in data: - data['compute_id'] = uuid.uuid5( - uuid.NAMESPACE_URL, - f"{data.get('protocol')}://{data.get('host')}:{data.get('port')}" + data["compute_id"] = uuid.uuid5( + uuid.NAMESPACE_URL, f"{data.get('protocol')}://{data.get('host')}:{data.get('port')}" ) if "name" not in data: - data['name'] = f"{data.get('protocol')}://{data.get('user', '')}@{data.get('host')}:{data.get('port')}" + data["name"] = f"{data.get('protocol')}://{data.get('user', '')}@{data.get('host')}:{data.get('port')}" return data @@ -94,12 +82,14 @@ class ComputeUpdate(ComputeBase): port: Optional[int] = Field(None, gt=0, le=65535) user: Optional[str] = None password: Optional[SecretStr] = None - model_config = ConfigDict(json_schema_extra={ - "example": { - "host": "10.0.0.1", - "port": 8080, + model_config = ConfigDict( + json_schema_extra={ + "example": { + "host": "10.0.0.1", + "port": 8080, + } } - }) + ) class Capabilities(BaseModel): @@ -165,4 +155,6 @@ class AutoIdlePC(BaseModel): platform: str = Field(..., description="Cisco platform") image: str = Field(..., description="Image path") ram: int = Field(..., description="Amount of RAM in MB") - model_config = ConfigDict(json_schema_extra={"example": {"platform": "c7200", "image": "/path/to/c7200_image.bin", "ram": 256}}) + model_config = ConfigDict( + json_schema_extra={"example": {"platform": "c7200", "image": "/path/to/c7200_image.bin", "ram": 256}} + ) diff --git a/gns3server/schemas/controller/images.py b/gns3server/schemas/controller/images.py index 417bc086f..ae98bcb6b 100644 --- a/gns3server/schemas/controller/images.py +++ b/gns3server/schemas/controller/images.py @@ -21,7 +21,6 @@ from .base import DateTimeModelMixin class ImageType(str, Enum): - qemu = "qemu" ios = "ios" iou = "iou" diff --git a/gns3server/schemas/controller/iou_license.py b/gns3server/schemas/controller/iou_license.py index 72c80100b..fcbfc488a 100644 --- a/gns3server/schemas/controller/iou_license.py +++ b/gns3server/schemas/controller/iou_license.py @@ -18,6 +18,5 @@ from pydantic import BaseModel, Field class IOULicense(BaseModel): - iourc_content: str = Field(..., description="Content of iourc file") license_check: bool = Field(..., description="Whether the license must be checked or not") diff --git a/gns3server/schemas/controller/links.py b/gns3server/schemas/controller/links.py index fef63f8b1..24c9f00e9 100644 --- a/gns3server/schemas/controller/links.py +++ b/gns3server/schemas/controller/links.py @@ -43,7 +43,6 @@ class LinkType(str, Enum): class LinkStyle(BaseModel): - color: Optional[str] = None width: Optional[int] = None type: Optional[int] = None @@ -63,50 +62,36 @@ class LinkBase(BaseModel): link_style: Optional[LinkStyle] = None filters: Optional[dict] = None markers: Optional[dict] = Field( - None, - description="Traffic-insight markers on this link: name → {bpf, tag, enabled}" - ) - show_filters_icon: Optional[bool] = Field( - True, - description="Show filters icon in Web UI" + None, description="Traffic-insight markers on this link: name → {bpf, tag, enabled}" ) + show_filters_icon: Optional[bool] = Field(True, description="Show filters icon in Web UI") class LinkCreate(LinkBase): - link_id: UUID = Field(default_factory=uuid4) nodes: List[LinkNode] = Field(..., min_length=2, max_length=2) class LinkUpdate(LinkBase): - pass class Link(LinkBase): - link_id: UUID project_id: Optional[UUID] = None link_type: Optional[LinkType] = None - capturing: Optional[bool] = Field( - None, - description="Read only property. True if a capture running on the link" - ) + capturing: Optional[bool] = Field(None, description="Read only property. True if a capture running on the link") capture_file_name: Optional[str] = Field( - None, - description="Read only property. The name of the capture file if a capture is running" + None, description="Read only property. The name of the capture file if a capture is running" ) capture_file_path: Optional[str] = Field( - None, - description="Read only property. The full path of the capture file if a capture is running" + None, description="Read only property. The full path of the capture file if a capture is running" ) capture_compute_id: Optional[str] = Field( - None, - description="Read only property. The compute identifier where a capture is running" + None, description="Read only property. The compute identifier where a capture is running" ) wireshark: Optional[bool] = Field( - False, - description="Read only property. True if a Web Wireshark session is active on the link" + False, description="Read only property. True if a Web Wireshark session is active on the link" ) @@ -121,6 +106,7 @@ class UDPPortInfo(BaseModel): rport: int type: str + class EthernetPortInfo(BaseModel): """ Ethernet port information. @@ -153,7 +139,7 @@ class MarkerCreate(BaseModel): None, pattern=r"^[A-Za-z0-9][A-Za-z0-9_.-]*$", max_length=32, - description='Unique marker name on the link. Auto-generated when absent.', + description="Unique marker name on the link. Auto-generated when absent.", ) bpf: str tag: Optional[int] = None @@ -226,9 +212,7 @@ class MarkerUpdate(BaseModel): description="Direction filter; 'both' or an explicit null clears it to both. Omit to keep.", ) color: Optional[str] = Field(None, description="Hex color render hint, e.g. '#ff5722'") - highlight_duration: Optional[int] = Field( - None, ge=1, description="UI highlight duration in ms; null = UI default" - ) + highlight_duration: Optional[int] = Field(None, ge=1, description="UI highlight duration in ms; null = UI default") enabled: Optional[bool] = Field(None, description="Toggle the marker on/off (instant).") @field_validator("direction", mode="before") @@ -288,5 +272,3 @@ class MarkerDefinitionCreate(BaseModel): @classmethod def _both_to_none(cls, v): return None if v == "both" else v - - diff --git a/gns3server/schemas/controller/llm_model_configs.py b/gns3server/schemas/controller/llm_model_configs.py index bcac68c1c..d5de5ee0b 100644 --- a/gns3server/schemas/controller/llm_model_configs.py +++ b/gns3server/schemas/controller/llm_model_configs.py @@ -22,7 +22,7 @@ from .base import DateTimeModelMixin # Valid model types -ModelType = Literal['text', 'vision', 'stt', 'tts', 'multimodal', 'embedding', 'reranking', 'other'] +ModelType = Literal["text", "vision", "stt", "tts", "multimodal", "embedding", "reranking", "other"] # Core model config schema (stored in config JSONB field) @@ -51,8 +51,7 @@ class LLMModelConfigData(BaseModel): "balanced", description="Context trimming strategy: conservative (60%), balanced (75%), aggressive (85%)" ) copilot_mode: Optional[str] = Field( - None, - description="GNS3-Copilot mode: 'teaching_assistant' or 'lab_automation_assistant'" + None, description="GNS3-Copilot mode: 'teaching_assistant' or 'lab_automation_assistant'" ) # Allow extra fields for extensibility @@ -81,8 +80,7 @@ class LLMModelConfigCreate(BaseModel): "balanced", description="Context trimming strategy" ) copilot_mode: Optional[str] = Field( - None, - description="GNS3-Copilot mode: 'teaching_assistant' or 'lab_automation_assistant'" + None, description="GNS3-Copilot mode: 'teaching_assistant' or 'lab_automation_assistant'" ) # Allow extra config fields @@ -112,14 +110,13 @@ class LLMModelConfigUpdate(BaseModel): None, description="Context trimming strategy" ) copilot_mode: Optional[str] = Field( - None, - description="GNS3-Copilot mode: 'teaching_assistant' or 'lab_automation_assistant'" + None, description="GNS3-Copilot mode: 'teaching_assistant' or 'lab_automation_assistant'" ) # Allow extra config fields model_config = ConfigDict(extra="allow") - @field_validator('max_tokens', mode='before') + @field_validator("max_tokens", mode="before") @classmethod def validate_max_tokens(cls, v): """Handle string 'null' values for max_tokens.""" @@ -150,8 +147,7 @@ class LLMModelConfigDataWithoutSecret(BaseModel): "balanced", description="Context trimming strategy: conservative (60%), balanced (75%), aggressive (85%)" ) copilot_mode: Optional[str] = Field( - None, - description="GNS3-Copilot mode: 'teaching_assistant' or 'lab_automation_assistant'" + None, description="GNS3-Copilot mode: 'teaching_assistant' or 'lab_automation_assistant'" ) # Allow extra fields for extensibility diff --git a/gns3server/schemas/controller/nodes.py b/gns3server/schemas/controller/nodes.py index 048ff1364..43d8ca5b4 100644 --- a/gns3server/schemas/controller/nodes.py +++ b/gns3server/schemas/controller/nodes.py @@ -157,13 +157,11 @@ class NodeBase(BaseModel): first_port_name: Optional[str] = Field(None, description="Name of the first port") custom_adapters: Optional[List[CustomAdapter]] = None tags: Optional[List[str]] = Field( - default_factory=list, - description="User-defined metadata tags (e.g. 'vendor:cisco' or 'model:7200')" + default_factory=list, description="User-defined metadata tags (e.g. 'vendor:cisco' or 'model:7200')" ) class NodeCreate(NodeBase): - node_id: UUID = Field(default_factory=uuid4) @@ -178,9 +176,9 @@ class NodeUpdate(NodeBase): class Node(NodeBase): - - template_id: Optional[UUID] = Field(None, - description="Template UUID from which the node has been created. Read only") + template_id: Optional[UUID] = Field( + None, description="Template UUID from which the node has been created. Read only" + ) project_id: Optional[UUID] = None node_directory: Optional[str] = Field(None, description="Working directory of the node. Read only") status: Optional[NodeStatus] = Field(None, description="Node status. Read only") diff --git a/gns3server/schemas/controller/pools.py b/gns3server/schemas/controller/pools.py index 0235cc1c2..d0d4f7957 100644 --- a/gns3server/schemas/controller/pools.py +++ b/gns3server/schemas/controller/pools.py @@ -23,7 +23,6 @@ from .base import DateTimeModelMixin class ResourceType(str, Enum): - project = "project" @@ -47,7 +46,6 @@ class ResourceCreate(ResourceBase): class Resource(DateTimeModelMixin, ResourceBase): - model_config = ConfigDict(from_attributes=True) @@ -76,6 +74,5 @@ class ResourcePoolUpdate(ResourcePoolBase): class ResourcePool(DateTimeModelMixin, ResourcePoolBase): - resource_pool_id: UUID model_config = ConfigDict(from_attributes=True) diff --git a/gns3server/schemas/controller/projects.py b/gns3server/schemas/controller/projects.py index ab79a344a..ddfbde02e 100644 --- a/gns3server/schemas/controller/projects.py +++ b/gns3server/schemas/controller/projects.py @@ -31,13 +31,11 @@ class ProjectStatus(str, Enum): class Supplier(BaseModel): - logo: str = Field(..., description="Path to the project supplier logo") url: Optional[HttpUrl] = Field(None, description="URL to the project supplier site") class Variable(BaseModel): - name: str = Field(..., description="Variable name") value: Optional[str] = Field(None, description="Variable value") @@ -91,7 +89,6 @@ class ProjectUpdate(ProjectBase): class Project(ProjectBase): - project_id: UUID name: Optional[str] = None status: Optional[ProjectStatus] = None @@ -100,7 +97,6 @@ class Project(ProjectBase): class ProjectFile(BaseModel): - path: str = Field(..., description="File path") md5sum: str = Field(..., description="File checksum") diff --git a/gns3server/schemas/controller/rbac.py b/gns3server/schemas/controller/rbac.py index 9d44e5c45..11e0c1d6e 100644 --- a/gns3server/schemas/controller/rbac.py +++ b/gns3server/schemas/controller/rbac.py @@ -32,13 +32,11 @@ class PrivilegeBase(BaseModel): class Privilege(DateTimeModelMixin, PrivilegeBase): - privilege_id: UUID model_config = ConfigDict(from_attributes=True) class ACEType(str, Enum): - user = "user" group = "group" @@ -75,7 +73,6 @@ class ACEUpdate(ACEBase): class ACE(DateTimeModelMixin, ACEBase): - ace_id: UUID model_config = ConfigDict(from_attributes=True) @@ -106,7 +103,6 @@ class RoleUpdate(RoleBase): class Role(DateTimeModelMixin, RoleBase): - role_id: UUID is_builtin: bool privileges: List[Privilege] diff --git a/gns3server/schemas/controller/settings.py b/gns3server/schemas/controller/settings.py index 73bace5bf..d64d029e1 100644 --- a/gns3server/schemas/controller/settings.py +++ b/gns3server/schemas/controller/settings.py @@ -47,11 +47,9 @@ SECRET_MASK = "**********" class ServerSettingsResponse(ServerSettings): - # plain strings instead of FilePath/DirectoryPath: paths are validated when # the settings are loaded or updated, not when echoed back to the client - secrets_dir: Optional[str] = Field( - None, description="Directory where secrets are stored (e.g. the JWT secret key)") + secrets_dir: Optional[str] = Field(None, description="Directory where secrets are stored (e.g. the JWT secret key)") certfile: Optional[str] = Field(None, description="SSL certificate file, requires enable_ssl") certkey: Optional[str] = Field(None, description="SSL key file, requires enable_ssl") # Optional overrides: typed as plain "str = None" in the config schema, @@ -59,28 +57,30 @@ class ServerSettingsResponse(ServerSettings): resources_path: Optional[str] = Field( None, description="Path where files like built-in appliances and Docker resources are stored " - "(defaults to the local user data directory)") + "(defaults to the local user data directory)", + ) default_nat_interface: Optional[str] = Field( - None, description="Interface used by the NAT node, default is virbr0 on Linux (requires libvirt)") + None, description="Interface used by the NAT node, default is virbr0 on Linux (requires libvirt)" + ) class ControllerSettingsResponse(ControllerSettings): - # never serialized: managed via the secrets directory, not the configuration file jwt_secret_key: Optional[str] = Field( - default=None, exclude=True, + default=None, + exclude=True, description="Secret key used to sign the JWT authentication tokens " - "(normally managed via the secrets directory, not the configuration file)") + "(normally managed via the secrets directory, not the configuration file)", + ) class IOUSettingsResponse(IOUSettings): - iourc_path: Optional[str] = Field( - None, description="Path of your .iourc file, the file is searched in $HOME/.iourc if not provided") + None, description="Path of your .iourc file, the file is searched in $HOME/.iourc if not provided" + ) class SettingsResponse(BaseModel): - Server: ServerSettingsResponse Controller: ControllerSettingsResponse VPCS: VPCSSettings @@ -160,14 +160,12 @@ class ControllerSettingsUpdate(BaseModel): class VPCSSettingsUpdate(BaseModel): - model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) vpcs_path: Optional[str] = None class DynamipsSettingsUpdate(BaseModel): - model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) allocate_aux_console_ports: Optional[bool] = None @@ -178,7 +176,6 @@ class DynamipsSettingsUpdate(BaseModel): class IOUSettingsUpdate(BaseModel): - model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) iourc_path: Optional[str] = None @@ -186,7 +183,6 @@ class IOUSettingsUpdate(BaseModel): class QemuSettingsUpdate(BaseModel): - model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) enable_monitor: Optional[bool] = None @@ -198,7 +194,6 @@ class QemuSettingsUpdate(BaseModel): class WebWiresharkSettingsUpdate(BaseModel): - model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) enabled: Optional[bool] = None @@ -210,7 +205,6 @@ class WebWiresharkSettingsUpdate(BaseModel): class SettingsUpdate(BaseModel): - model_config = ConfigDict(extra="forbid") Server: Optional[ServerSettingsUpdate] = None @@ -223,8 +217,7 @@ class SettingsUpdate(BaseModel): class SettingsUpdateResponse(SettingsResponse): - restart_required: List[str] = Field( default_factory=list, - description="Changed 'Section.option' settings that require a server restart to take effect" + description="Changed 'Section.option' settings that require a server restart to take effect", ) diff --git a/gns3server/schemas/controller/snapshots.py b/gns3server/schemas/controller/snapshots.py index cb6657fba..4bdd6e938 100644 --- a/gns3server/schemas/controller/snapshots.py +++ b/gns3server/schemas/controller/snapshots.py @@ -28,6 +28,7 @@ class SnapshotBase(BaseModel): name: str = Field(..., description="Name of the snapshot") description: Optional[str] = Field(None, description="Description of the snapshot") + class SnapshotCreate(SnapshotBase): """ Properties for snapshot creation. @@ -37,7 +38,6 @@ class SnapshotCreate(SnapshotBase): class Snapshot(SnapshotBase): - snapshot_id: UUID project_id: UUID name: str = Field(..., description="Name of the snapshot") diff --git a/gns3server/schemas/controller/templates/__init__.py b/gns3server/schemas/controller/templates/__init__.py index 3c75c6b1f..b71c64772 100644 --- a/gns3server/schemas/controller/templates/__init__.py +++ b/gns3server/schemas/controller/templates/__init__.py @@ -43,9 +43,7 @@ class ApplianceMetadata(BaseModel): model_config = ConfigDict(extra="allow") - appliance_id: Optional[str] = Field( - None, description="ID of the appliance the template was installed from" - ) + appliance_id: Optional[str] = Field(None, description="ID of the appliance the template was installed from") description: Optional[str] = None vendor_name: Optional[str] = None vendor_url: Optional[str] = None @@ -82,12 +80,10 @@ class TemplateBase(BaseModel): pattern=r"^[a-z0-9_]+$|^$", ) tags: Optional[List[str]] = Field( - default_factory=list, - description="User-defined metadata tags (e.g. 'vendor:cisco' or 'model:7200')" + default_factory=list, description="User-defined metadata tags (e.g. 'vendor:cisco' or 'model:7200')" ) appliance_metadata: Optional[ApplianceMetadata] = Field( - None, - description="Metadata inherited from the appliance the template was installed from" + None, description="Metadata inherited from the appliance the template was installed from" ) @@ -106,7 +102,6 @@ class TemplateUpdate(TemplateBase): class Template(DateTimeModelMixin, TemplateBase): - template_id: UUID name: str category: Category @@ -117,7 +112,6 @@ class Template(DateTimeModelMixin, TemplateBase): class TemplateUsage(BaseModel): - x: int y: int name: Optional[str] = Field(None, description="Use this name to create a new node") diff --git a/gns3server/schemas/controller/templates/cloud_templates.py b/gns3server/schemas/controller/templates/cloud_templates.py index 1754911a2..e9ec0f6bf 100644 --- a/gns3server/schemas/controller/templates/cloud_templates.py +++ b/gns3server/schemas/controller/templates/cloud_templates.py @@ -16,19 +16,13 @@ from . import Category, TemplateBase -from gns3server.schemas.compute.cloud_nodes import ( - EthernetPort, - TAPPort, - UDPPort, - CloudConsoleType -) +from gns3server.schemas.compute.cloud_nodes import EthernetPort, TAPPort, UDPPort, CloudConsoleType from pydantic import Field from typing import Optional, Union, List class CloudTemplate(TemplateBase): - category: Optional[Category] = Category.guest default_name_format: Optional[str] = "Cloud{0}" symbol: Optional[str] = "cloud" @@ -40,5 +34,4 @@ class CloudTemplate(TemplateBase): class CloudTemplateUpdate(CloudTemplate): - pass diff --git a/gns3server/schemas/controller/templates/docker_templates.py b/gns3server/schemas/controller/templates/docker_templates.py index 3d7688cbb..1c7a320e3 100644 --- a/gns3server/schemas/controller/templates/docker_templates.py +++ b/gns3server/schemas/controller/templates/docker_templates.py @@ -23,13 +23,14 @@ from typing import Optional, List class DockerTemplate(TemplateBase): - category: Optional[Category] = Category.guest default_name_format: Optional[str] = "{name}-{0}" symbol: Optional[str] = "docker_guest" image: str = Field(..., description="Docker image name") adapters: Optional[int] = Field(1, ge=0, le=100, description="Number of adapters") - mac_address: Optional[str] = Field("", description="Base MAC address", pattern="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$|^$") + mac_address: Optional[str] = Field( + "", description="Base MAC address", pattern="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$|^$" + ) start_command: Optional[str] = Field("", description="Docker CMD entry") environment: Optional[str] = Field("", description="Docker environment variables") console_type: Optional[ConsoleType] = Field(ConsoleType.telnet, description="Console type") @@ -49,12 +50,13 @@ class DockerTemplate(TemplateBase): ) extra_hosts: Optional[str] = Field("", description="Docker extra hosts (added to /etc/hosts)") extra_volumes: Optional[List] = Field([], description="Additional directories to make persistent") - extra_configs: Optional[List[ExtraConfig]] = Field(default_factory=list, description="Configuration files injected into the container (bind-mounted read-only)") + extra_configs: Optional[List[ExtraConfig]] = Field( + default_factory=list, description="Configuration files injected into the container (bind-mounted read-only)" + ) memory: Optional[int] = Field(0, ge=0, description="Maximum amount of memory the container can use in MB") cpus: Optional[float] = Field(0, ge=0, description="Maximum amount of CPU resources the container can use") custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters") class DockerTemplateUpdate(DockerTemplate): - - image: Optional[str] = Field(None, description="Docker image name") \ No newline at end of file + image: Optional[str] = Field(None, description="Docker image name") diff --git a/gns3server/schemas/controller/templates/dynamips_templates.py b/gns3server/schemas/controller/templates/dynamips_templates.py index 86a359cde..90180d99b 100644 --- a/gns3server/schemas/controller/templates/dynamips_templates.py +++ b/gns3server/schemas/controller/templates/dynamips_templates.py @@ -31,7 +31,6 @@ from enum import Enum class DynamipsTemplate(TemplateBase): - category: Optional[Category] = Category.router default_name_format: Optional[str] = "R{0}" symbol: Optional[str] = "router" @@ -69,7 +68,6 @@ class DynamipsTemplate(TemplateBase): class C7200DynamipsTemplate(DynamipsTemplate): - ram: Optional[int] = Field(512, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(512, gt=0, description="Amount of NVRAM in KB") npe: Optional[DynamipsNPE] = Field(DynamipsNPE.npe_400, description="NPE model") @@ -78,13 +76,11 @@ class C7200DynamipsTemplate(DynamipsTemplate): class C7200DynamipsTemplateUpdate(C7200DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") image: Optional[str] = Field(None, description="Path to the IOS image") class C3725DynamipsTemplate(DynamipsTemplate): - ram: Optional[int] = Field(128, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(256, gt=0, description="Amount of NVRAM in KB") iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") @@ -92,13 +88,11 @@ class C3725DynamipsTemplate(DynamipsTemplate): class C3725DynamipsTemplateUpdate(C3725DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") image: Optional[str] = Field(None, description="Path to the IOS image") class C3745DynamipsTemplate(DynamipsTemplate): - ram: Optional[int] = Field(256, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(256, gt=0, description="Amount of NVRAM in KB") iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") @@ -106,20 +100,17 @@ class C3745DynamipsTemplate(DynamipsTemplate): class C3745DynamipsTemplateUpdate(C3745DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") image: Optional[str] = Field(None, description="Path to the IOS image") class C3600ChassisType(str, Enum): - chassis_3620 = "3620" chassis_3640 = "3640" chassis_3660 = "3660" class C3600DynamipsTemplate(DynamipsTemplate): - chassis: Optional[C3600ChassisType] = Field("c3660", description="Chassis type") ram: Optional[int] = Field(192, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(128, gt=0, description="Amount of NVRAM in KB") @@ -128,13 +119,11 @@ class C3600DynamipsTemplate(DynamipsTemplate): class C3600DynamipsTemplateUpdate(C3600DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") image: Optional[str] = Field(None, description="Path to the IOS image") class C2691DynamipsTemplate(DynamipsTemplate): - ram: Optional[int] = Field(192, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(256, gt=0, description="Amount of NVRAM in KB") iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") @@ -142,13 +131,11 @@ class C2691DynamipsTemplate(DynamipsTemplate): class C2691DynamipsTemplateUpdate(C2691DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") image: Optional[str] = Field(None, description="Path to the IOS image") class C2600ChassisType(str, Enum): - chassis_2610 = "2610" chassis_2620 = "2620" chassis_2610xm = "2610XM" @@ -161,7 +148,6 @@ class C2600ChassisType(str, Enum): class C2600DynamipsTemplate(DynamipsTemplate): - chassis: Optional[C2600ChassisType] = Field("2651XM", description="Chassis type") ram: Optional[int] = Field(160, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(128, gt=0, description="Amount of NVRAM in KB") @@ -170,13 +156,11 @@ class C2600DynamipsTemplate(DynamipsTemplate): class C2600DynamipsTemplateUpdate(C2600DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") image: Optional[str] = Field(None, description="Path to the IOS image") class C1700ChassisType(str, Enum): - chassis_1720 = "1720" chassis_1721 = "1721" chassis_1750 = "1750" @@ -185,7 +169,6 @@ class C1700ChassisType(str, Enum): class C1700DynamipsTemplate(DynamipsTemplate): - chassis: Optional[C1700ChassisType] = Field("1760", description="Chassis type") ram: Optional[int] = Field(160, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(128, gt=0, description="Amount of NVRAM in KB") @@ -194,6 +177,5 @@ class C1700DynamipsTemplate(DynamipsTemplate): class C1700DynamipsTemplateUpdate(C1700DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") image: Optional[str] = Field(None, description="Path to the IOS image") diff --git a/gns3server/schemas/controller/templates/ethernet_hub_templates.py b/gns3server/schemas/controller/templates/ethernet_hub_templates.py index 23b04241b..a5b675937 100644 --- a/gns3server/schemas/controller/templates/ethernet_hub_templates.py +++ b/gns3server/schemas/controller/templates/ethernet_hub_templates.py @@ -34,7 +34,6 @@ DEFAULT_PORTS = [ class EthernetHubTemplate(TemplateBase): - category: Optional[Category] = Category.switch default_name_format: Optional[str] = "Hub{0}" symbol: Optional[str] = "hub" @@ -42,5 +41,4 @@ class EthernetHubTemplate(TemplateBase): class EthernetHubTemplateUpdate(EthernetHubTemplate): - pass diff --git a/gns3server/schemas/controller/templates/ethernet_switch_templates.py b/gns3server/schemas/controller/templates/ethernet_switch_templates.py index f4e83a0ac..5e6f81f03 100644 --- a/gns3server/schemas/controller/templates/ethernet_switch_templates.py +++ b/gns3server/schemas/controller/templates/ethernet_switch_templates.py @@ -44,7 +44,6 @@ class ConsoleType(str, Enum): class EthernetSwitchTemplate(TemplateBase): - category: Optional[Category] = Category.switch default_name_format: Optional[str] = "Switch{0}" symbol: Optional[str] = "ethernet_switch" @@ -53,5 +52,4 @@ class EthernetSwitchTemplate(TemplateBase): class EthernetSwitchTemplateUpdate(EthernetSwitchTemplate): - pass diff --git a/gns3server/schemas/controller/templates/iou_templates.py b/gns3server/schemas/controller/templates/iou_templates.py index c0a8d398b..02ab06696 100644 --- a/gns3server/schemas/controller/templates/iou_templates.py +++ b/gns3server/schemas/controller/templates/iou_templates.py @@ -23,7 +23,6 @@ from typing import Optional class IOUTemplate(TemplateBase): - category: Optional[Category] = Category.router default_name_format: Optional[str] = "IOU{0}" symbol: Optional[str] = "multilayer_switch" @@ -46,5 +45,4 @@ class IOUTemplate(TemplateBase): class IOUTemplateUpdate(IOUTemplate): - path: Optional[str] = Field(None, description="Path of IOU executable") diff --git a/gns3server/schemas/controller/templates/qemu_templates.py b/gns3server/schemas/controller/templates/qemu_templates.py index 13abaf40d..116dfc33c 100644 --- a/gns3server/schemas/controller/templates/qemu_templates.py +++ b/gns3server/schemas/controller/templates/qemu_templates.py @@ -32,7 +32,6 @@ from typing import Optional, List class QemuTemplate(TemplateBase): - category: Optional[Category] = Category.guest default_name_format: Optional[str] = "{name}-{0}" symbol: Optional[str] = "qemu_guest" @@ -62,13 +61,21 @@ class QemuTemplate(TemplateBase): aux_type: Optional[QemuConsoleType] = Field(QemuConsoleType.none, description="Auxiliary console type") boot_priority: Optional[QemuBootPriority] = Field(QemuBootPriority.c, description="QEMU boot priority") hda_disk_image: Optional[str] = Field("", description="QEMU hda disk image path") - hda_disk_interface: Optional[QemuDiskInterfaceType] = Field(QemuDiskInterfaceType.none, description="QEMU hda interface") + hda_disk_interface: Optional[QemuDiskInterfaceType] = Field( + QemuDiskInterfaceType.none, description="QEMU hda interface" + ) hdb_disk_image: Optional[str] = Field("", description="QEMU hdb disk image path") - hdb_disk_interface: Optional[QemuDiskInterfaceType] = Field(QemuDiskInterfaceType.none, description="QEMU hdb interface") + hdb_disk_interface: Optional[QemuDiskInterfaceType] = Field( + QemuDiskInterfaceType.none, description="QEMU hdb interface" + ) hdc_disk_image: Optional[str] = Field("", description="QEMU hdc disk image path") - hdc_disk_interface: Optional[QemuDiskInterfaceType] = Field(QemuDiskInterfaceType.none, description="QEMU hdc interface") + hdc_disk_interface: Optional[QemuDiskInterfaceType] = Field( + QemuDiskInterfaceType.none, description="QEMU hdc interface" + ) hdd_disk_image: Optional[str] = Field("", description="QEMU hdd disk image path") - hdd_disk_interface: Optional[QemuDiskInterfaceType] = Field(QemuDiskInterfaceType.none, description="QEMU hdd interface") + hdd_disk_interface: Optional[QemuDiskInterfaceType] = Field( + QemuDiskInterfaceType.none, description="QEMU hdd interface" + ) cdrom_image: Optional[str] = Field("", description="QEMU cdrom image path") initrd: Optional[str] = Field("", description="QEMU initrd path") kernel_image: Optional[str] = Field("", description="QEMU kernel image path") @@ -82,13 +89,16 @@ class QemuTemplate(TemplateBase): ) tpm: Optional[bool] = Field(False, description="Enable Trusted Platform Module (TPM)") uefi: Optional[bool] = Field(False, description="Enable UEFI boot mode") - on_close: Optional[QemuOnCloseAction] = Field(QemuOnCloseAction.power_off, description="Action to execute on the VM is closed") + on_close: Optional[QemuOnCloseAction] = Field( + QemuOnCloseAction.power_off, description="Action to execute on the VM is closed" + ) cpu_throttling: Optional[int] = Field(0, ge=0, le=800, description="Percentage of CPU allowed for QEMU") - process_priority: Optional[QemuProcessPriority] = Field(QemuProcessPriority.normal, description="Process priority for QEMU") + process_priority: Optional[QemuProcessPriority] = Field( + QemuProcessPriority.normal, description="Process priority for QEMU" + ) options: Optional[str] = Field("", description="Additional QEMU options") custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters") class QemuTemplateUpdate(QemuTemplate): - pass diff --git a/gns3server/schemas/controller/templates/virtualbox_templates.py b/gns3server/schemas/controller/templates/virtualbox_templates.py index 922cae6b7..3cce1cd6e 100644 --- a/gns3server/schemas/controller/templates/virtualbox_templates.py +++ b/gns3server/schemas/controller/templates/virtualbox_templates.py @@ -19,7 +19,7 @@ from gns3server.schemas.compute.virtualbox_nodes import ( VirtualBoxConsoleType, VirtualBoxAdapterType, VirtualBoxOnCloseAction, - CustomAdapter + CustomAdapter, ) from pydantic import Field @@ -27,7 +27,6 @@ from typing import Optional, List class VirtualBoxTemplate(TemplateBase): - category: Optional[Category] = Category.guest default_name_format: Optional[str] = "{name}-{0}" symbol: Optional[str] = "vbox_guest" @@ -38,7 +37,8 @@ class VirtualBoxTemplate(TemplateBase): 1, ge=0, le=36, description="Number of adapters" ) # 36 is the maximum given by the ICH9 chipset in VirtualBox use_any_adapter: Optional[bool] = Field(False, description="Allow GNS3 to use any VirtualBox adapter") - adapter_type: Optional[VirtualBoxAdapterType] = Field(VirtualBoxAdapterType.intel_pro_1000_mt_desktop, description="VirtualBox adapter type" + adapter_type: Optional[VirtualBoxAdapterType] = Field( + VirtualBoxAdapterType.intel_pro_1000_mt_desktop, description="VirtualBox adapter type" ) first_port_name: Optional[str] = Field("", description="Optional name of the first networking port example: eth0") port_name_format: Optional[str] = Field( @@ -60,5 +60,4 @@ class VirtualBoxTemplate(TemplateBase): class VirtualBoxTemplateUpdate(VirtualBoxTemplate): - vmname: Optional[str] = Field(None, description="VirtualBox VM name (in VirtualBox itself)") diff --git a/gns3server/schemas/controller/templates/vmware_templates.py b/gns3server/schemas/controller/templates/vmware_templates.py index 555e51896..4aed1ee90 100644 --- a/gns3server/schemas/controller/templates/vmware_templates.py +++ b/gns3server/schemas/controller/templates/vmware_templates.py @@ -20,7 +20,7 @@ from gns3server.schemas.compute.vmware_nodes import ( VMwareConsoleType, VMwareAdapterType, VMwareOnCloseAction, - CustomAdapter + CustomAdapter, ) from pydantic import Field @@ -28,7 +28,6 @@ from typing import Optional, List class VMwareTemplate(TemplateBase): - category: Optional[Category] = Category.guest default_name_format: Optional[str] = "{name}-{0}" symbol: Optional[str] = "vmware_guest" @@ -48,7 +47,9 @@ class VMwareTemplate(TemplateBase): adapter_type: Optional[VMwareAdapterType] = Field(VMwareAdapterType.e1000, description="VMware adapter type") use_any_adapter: Optional[bool] = Field(False, description="Allow GNS3 to use any VMware adapter") headless: Optional[bool] = Field(False, description="Headless mode") - on_close: Optional[VMwareOnCloseAction] = Field(VMwareOnCloseAction.power_off, description="Action to execute on the VM is closed") + on_close: Optional[VMwareOnCloseAction] = Field( + VMwareOnCloseAction.power_off, description="Action to execute on the VM is closed" + ) console_type: Optional[VMwareConsoleType] = Field(VMwareConsoleType.none, description="Console type") console_auto_start: Optional[bool] = Field( False, description="Automatically start the console when the node has started" @@ -57,5 +58,4 @@ class VMwareTemplate(TemplateBase): class VMwareTemplateUpdate(VMwareTemplate): - vmx_path: Optional[str] = Field(None, description="Path to the vmx file") diff --git a/gns3server/schemas/controller/templates/vpcs_templates.py b/gns3server/schemas/controller/templates/vpcs_templates.py index 79d6f1c3e..e9f6e0b4f 100644 --- a/gns3server/schemas/controller/templates/vpcs_templates.py +++ b/gns3server/schemas/controller/templates/vpcs_templates.py @@ -23,7 +23,6 @@ from typing import Optional class VPCSTemplate(TemplateBase): - category: Optional[Category] = Category.guest default_name_format: Optional[str] = "PC{0}" symbol: Optional[str] = "vpcs_guest" @@ -35,5 +34,4 @@ class VPCSTemplate(TemplateBase): class VPCSTemplateUpdate(VPCSTemplate): - pass diff --git a/gns3server/schemas/controller/tokens.py b/gns3server/schemas/controller/tokens.py index 61945e257..29197acfa 100644 --- a/gns3server/schemas/controller/tokens.py +++ b/gns3server/schemas/controller/tokens.py @@ -19,14 +19,12 @@ from pydantic import BaseModel class Token(BaseModel): - access_token: str token_type: str refresh_token: Optional[str] = None class TokenData(BaseModel): - username: Optional[str] = None token_version: int = 0 token_use: str = "access" diff --git a/gns3server/schemas/controller/topology.py b/gns3server/schemas/controller/topology.py index 1b9dcfddc..26ff78b3e 100644 --- a/gns3server/schemas/controller/topology.py +++ b/gns3server/schemas/controller/topology.py @@ -33,12 +33,10 @@ from uuid import UUID class TopologyType(str, Enum): - topology = "topology" class TopologyContent(BaseModel): - computes: List[Compute] = Field(..., description="List of computes") drawings: List[Drawing] = Field(..., description="List of drawings") links: List[Link] = Field(..., description="List of links") @@ -46,7 +44,6 @@ class TopologyContent(BaseModel): class Topology(BaseModel): - project_id: UUID = Field(..., description="Project UUID") type: TopologyType = Field(..., description="Type of file. It's always topology") revision: int = Field(..., description="Version of the .gns3 specification") diff --git a/gns3server/schemas/controller/users.py b/gns3server/schemas/controller/users.py index aa4af2e38..4fed51d49 100644 --- a/gns3server/schemas/controller/users.py +++ b/gns3server/schemas/controller/users.py @@ -61,7 +61,6 @@ class LoggedInUserUpdate(BaseModel): class User(DateTimeModelMixin, UserBase): - user_id: UUID last_login: Optional[datetime] = None is_superadmin: bool = False @@ -93,13 +92,11 @@ class UserGroupUpdate(UserGroupBase): class UserGroup(DateTimeModelMixin, UserGroupBase): - user_group_id: UUID is_builtin: bool model_config = ConfigDict(from_attributes=True) class Credentials(BaseModel): - username: str password: str diff --git a/gns3server/schemas/qemu_disk_image.py b/gns3server/schemas/qemu_disk_image.py index 98007a4f0..a3ebccba6 100644 --- a/gns3server/schemas/qemu_disk_image.py +++ b/gns3server/schemas/qemu_disk_image.py @@ -78,7 +78,6 @@ class QemuDiskImageAdapterType(str, Enum): class QemuDiskImageBase(BaseModel): - format: QemuDiskImageFormat = Field(..., description="Image format type") size: int = Field(..., description="Image size in Megabytes") preallocation: Optional[QemuDiskImagePreallocation] = None @@ -92,12 +91,10 @@ class QemuDiskImageBase(BaseModel): class QemuDiskImageCreate(QemuDiskImageBase): - pass class QemuDiskImageUpdate(QemuDiskImageBase): - format: Optional[QemuDiskImageFormat] = Field(None, description="Image format type") size: Optional[int] = Field(None, description="Image size in Megabytes") extend: Optional[int] = Field(None, description="Number of Megabytes to extend the image") diff --git a/gns3server/schemas/version.py b/gns3server/schemas/version.py index f13074d5b..db4b95ed5 100644 --- a/gns3server/schemas/version.py +++ b/gns3server/schemas/version.py @@ -19,7 +19,6 @@ from typing import Optional class Version(BaseModel): - controller_host: Optional[str] = Field(None, description="Controller hostname or IP address") version: str = Field(..., description="Version number") local: Optional[bool] = Field(None, description="Whether this is a local server or not") diff --git a/gns3server/server.py b/gns3server/server.py index be2b62564..7d7eb3186 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -44,7 +44,6 @@ log = logging.getLogger(__name__) class Server: - _stream_handler = None @staticmethod @@ -157,6 +156,7 @@ class Server: def _signal_handling(self): from gns3server.controller import Controller + def signal_handler(signame, *args): try: @@ -245,11 +245,13 @@ class Server: log.info("Compute authentication is disabled") elif not config.Server.compute_password.get_secret_value(): alphabet = string.ascii_letters + string.digits + string.punctuation - generated_password = ''.join(secrets.choice(alphabet) for _ in range(16)) + generated_password = "".join(secrets.choice(alphabet) for _ in range(16)) config.Server.compute_password = SecretStr(generated_password) - log.warning(f"Compute authentication is enabled with username '{config.Server.compute_username}' and " - f"a randomly generated password. Please set a password in the config file if this compute " - f"is to be used by an external controller") + log.warning( + f"Compute authentication is enabled with username '{config.Server.compute_username}' and " + f"a randomly generated password. Please set a password in the config file if this compute " + f"is to be used by an external controller" + ) else: log.info(f"Compute authentication is enabled with username '{config.Server.compute_username}'") @@ -274,6 +276,7 @@ class Server: try: import truststore + truststore.inject_into_ssl() log.info("Using system certificate store for SSL connections") except ImportError: @@ -284,6 +287,7 @@ class Server: port = config.Server.port from gns3server.compute.port_manager import PortManager + PortManager.instance().console_host = host self._signal_handling() @@ -306,7 +310,7 @@ class Server: access_log=access_log, ssl_certfile=config.Server.certfile, ssl_keyfile=config.Server.certkey, - lifespan="on" + lifespan="on", ) # overwrite uvicorn loggers with our own logger diff --git a/gns3server/services/authentication.py b/gns3server/services/authentication.py index 90f943b99..3d165c55d 100644 --- a/gns3server/services/authentication.py +++ b/gns3server/services/authentication.py @@ -48,16 +48,15 @@ def _extract_alg(token: str) -> str: class AuthService: - def hash_password(self, password: str) -> str: salt = bcrypt.gensalt() - hashed_password = bcrypt.hashpw(password=password.encode('utf-8'), salt=salt) - return hashed_password.decode('utf-8') + hashed_password = bcrypt.hashpw(password=password.encode("utf-8"), salt=salt) + return hashed_password.decode("utf-8") def verify_password(self, password, hashed_password) -> bool: - return bcrypt.checkpw(password=password.encode('utf-8'), hashed_password=hashed_password.encode('utf-8')) + return bcrypt.checkpw(password=password.encode("utf-8"), hashed_password=hashed_password.encode("utf-8")) def _create_token(self, username, token_version, token_type, expires_in, secret_key=None) -> str: """Shared helper to create any kind of signed JWT token.""" @@ -80,7 +79,9 @@ class AuthService: expires_in = Config.instance().settings.Controller.jwt_access_token_expire_minutes return self._create_token(username, token_version, "access", expires_in, secret_key) - def create_refresh_token(self, username, token_version: int = 0, secret_key: str = None, expires_in: int = 0) -> str: + def create_refresh_token( + self, username, token_version: int = 0, secret_key: str = None, expires_in: int = 0 + ) -> str: if not expires_in: expires_in = Config.instance().settings.Controller.jwt_refresh_token_expire_minutes diff --git a/gns3server/services/templates.py b/gns3server/services/templates.py index 66b793566..10a167c89 100644 --- a/gns3server/services/templates.py +++ b/gns3server/services/templates.py @@ -158,11 +158,11 @@ BUILTIN_TEMPLATES = [ class TemplatesService: - def __init__(self, templates_repo: TemplatesRepository): self._templates_repo = templates_repo from gns3server.controller import Controller + self._controller = Controller.instance() # resolve built-in template symbols @@ -233,8 +233,10 @@ class TemplatesService: if await self._templates_repo.get_template_by_name_and_version(template_create.name, template_create.version): if template_create.version: - raise ControllerError(f"A template with name '{template_create.name}' and " - f"version {template_create.version} already exists") + raise ControllerError( + f"A template with name '{template_create.name}' and " + f"version {template_create.version} already exists" + ) else: raise ControllerError(f"A template with name '{template_create.name}' already exists") @@ -364,11 +366,7 @@ class TemplatesService: if not os.path.exists(path): return [] - return [ - {"filename": f} - for f in sorted(os.listdir(path)) - if os.path.isfile(os.path.join(path, f)) - ] + return [{"filename": f} for f in sorted(os.listdir(path)) if os.path.isfile(os.path.join(path, f))] def get_file(self, template_id: str, filename: str): safe_filename = os.path.basename(filename) @@ -395,4 +393,4 @@ class TemplatesService: with open(path, "w", encoding="utf-8") as f: f.write(content) except OSError as e: - raise ControllerError(str(e)) \ No newline at end of file + raise ControllerError(str(e)) diff --git a/gns3server/utils/__init__.py b/gns3server/utils/__init__.py index e00d24b75..c1e485692 100644 --- a/gns3server/utils/__init__.py +++ b/gns3server/utils/__init__.py @@ -111,6 +111,7 @@ def is_ipv6_enabled() -> bool: return True raise + def md5sum(filename): """ Calculate the MD5 checksum of a file. diff --git a/gns3server/utils/asyncio/__init__.py b/gns3server/utils/asyncio/__init__.py index 1b9e443a3..345cd77a8 100644 --- a/gns3server/utils/asyncio/__init__.py +++ b/gns3server/utils/asyncio/__init__.py @@ -73,7 +73,9 @@ async def subprocess_check_output(*args, cwd=None, env=None, stderr=False): proc = await asyncio.create_subprocess_exec(*args, stderr=asyncio.subprocess.PIPE, cwd=cwd, env=env) _, output = await proc.communicate() else: - proc = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, cwd=cwd, env=env) + proc = await asyncio.create_subprocess_exec( + *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, cwd=cwd, env=env + ) output, _ = await proc.communicate() if output is None: return "" diff --git a/gns3server/utils/asyncio/aiozipstream.py b/gns3server/utils/asyncio/aiozipstream.py index c8f69c8e2..a6e1fc49b 100644 --- a/gns3server/utils/asyncio/aiozipstream.py +++ b/gns3server/utils/asyncio/aiozipstream.py @@ -56,20 +56,24 @@ def _get_compressor(compress_type, compresslevel=None): if compress_type == zipfile.ZIP_DEFLATED: from zipfile import zlib + if compresslevel is not None: return zlib.compressobj(compresslevel, zlib.DEFLATED, -15) return zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) elif compress_type == zipfile.ZIP_BZIP2: from zipfile import bz2 + if compresslevel is not None: return bz2.BZ2Compressor(compresslevel) return bz2.BZ2Compressor() # compresslevel is ignored for ZIP_LZMA elif compress_type == zipfile.ZIP_LZMA: from zipfile import LZMACompressor + return LZMACompressor() elif compress_type == ZIP_ZSTANDARD: import zstandard as zstd + if compresslevel is not None: # for threads param a value of -1 means to set the number of threads to the number of detected logical CPUs return zstd.ZstdCompressor(level=compresslevel, threads=-1).compressobj() @@ -141,13 +145,13 @@ class ZipInfo(zipfile.ZipInfo): class ZipFile(zipfile.ZipFile): def __init__( - self, - fileobj=None, - mode="w", - compression=zipfile.ZIP_STORED, - allowZip64=True, - compresslevel=None, - chunksize=32768 + self, + fileobj=None, + mode="w", + compression=zipfile.ZIP_STORED, + allowZip64=True, + compresslevel=None, + chunksize=32768, ): """Open the ZIP file with mode write "w".""" @@ -158,11 +162,7 @@ class ZipFile(zipfile.ZipFile): self._comment = b"" zipfile.ZipFile.__init__( - self, fileobj, - mode=mode, - compression=compression, - compresslevel=compresslevel, - allowZip64=allowZip64 + self, fileobj, mode=mode, compression=compression, compresslevel=compresslevel, allowZip64=allowZip64 ) self._chunksize = chunksize self.paths_to_write = [] @@ -232,7 +232,7 @@ class ZipFile(zipfile.ZipFile): "filename": filename, "arcname": arcname, "compress_type": compress_type, - "compresslevel": compresslevel + "compresslevel": compresslevel, } self.paths_to_write.append(kwargs) @@ -245,7 +245,7 @@ class ZipFile(zipfile.ZipFile): "arcname": arcname, "iterable": iterable, "compress_type": compress_type, - "compresslevel": compresslevel + "compresslevel": compresslevel, } self.paths_to_write.append(kwargs) diff --git a/gns3server/utils/asyncio/embed_shell.py b/gns3server/utils/asyncio/embed_shell.py index 907ace2ff..3efd0bdbc 100644 --- a/gns3server/utils/asyncio/embed_shell.py +++ b/gns3server/utils/asyncio/embed_shell.py @@ -114,7 +114,7 @@ class EmbedShell: if not cmd[0].strip(): return "" - for (name, meth) in inspect.getmembers(self): + for name, meth in inspect.getmembers(self): if name == cmd[0]: cmd.pop(0) res = await meth(*cmd) @@ -177,10 +177,10 @@ class UnstoppableEventLoop(EventLoop): self._loop = loop def close(self): - " Ignore. " + "Ignore." def stop(self): - " Ignore. " + "Ignore." def run_in_executor(self, *args, **kwargs): return self._loop.run_in_executor(*args, **kwargs) @@ -264,18 +264,18 @@ class ShellConnection(TelnetConnection): self.reset() def reset(self): - """ Resets terminal screen""" + """Resets terminal screen""" self._cli.reset() self._cli.buffers[DEFAULT_BUFFER].reset() self._cli.renderer.request_absolute_cursor_position() self._cli._redraw() def write(self, data): - """ Compat with CLI""" + """Compat with CLI""" self.send(data) def flush(self): - """ Compat with CLI""" + """Compat with CLI""" pass diff --git a/gns3server/utils/asyncio/input_stream.py b/gns3server/utils/asyncio/input_stream.py index e2d936e91..6a9c983c6 100644 --- a/gns3server/utils/asyncio/input_stream.py +++ b/gns3server/utils/asyncio/input_stream.py @@ -4,7 +4,6 @@ Parser for VT100 input stream. # Copied from prompt_toolkit/terminal/vt100_input.py due to dependency on termios (which is not available on Windows) - import re import six @@ -40,7 +39,7 @@ _mouse_event_prefix_re = re.compile("^" + re.escape("\x1b[") + r"( None: encryption_key_path = os.path.join(secrets_dir, "gns3_encryption_key") if not os.path.exists(encryption_key_path): - log.info( - f"No encryption key found, generating one in " - f"'{encryption_key_path}'..." - ) + log.info(f"No encryption key found, generating one in '{encryption_key_path}'...") try: key = Fernet.generate_key() os.makedirs(secrets_dir, exist_ok=True) @@ -55,26 +52,17 @@ def init_encryption(secrets_dir: str) -> None: # Set restrictive permissions (owner read/write only) os.chmod(encryption_key_path, 0o600) except OSError as e: - log.error( - "Could not create encryption key file " - f"'{encryption_key_path}': {e}" - ) + log.error(f"Could not create encryption key file '{encryption_key_path}': {e}") raise try: with open(encryption_key_path, encoding="utf-8") as f: key_content = f.read().strip() - key_bytes = ( - key_content.encode() - if isinstance(key_content, str) - else key_content - ) + key_bytes = key_content.encode() if isinstance(key_content, str) else key_content _fernet = Fernet(key_bytes) log.debug("Encryption initialized successfully") except OSError as e: - log.error( - f"Could not read encryption key file '{encryption_key_path}': {e}" - ) + log.error(f"Could not read encryption key file '{encryption_key_path}': {e}") raise @@ -88,9 +76,7 @@ def encrypt(plaintext: str) -> str: """ if _fernet is None: - raise RuntimeError( - "Encryption not initialized. Call init_encryption() first." - ) + raise RuntimeError("Encryption not initialized. Call init_encryption() first.") if not plaintext: return "" @@ -110,9 +96,7 @@ def decrypt(ciphertext: str) -> str: """ if _fernet is None: - raise RuntimeError( - "Encryption not initialized. Call init_encryption() first." - ) + raise RuntimeError("Encryption not initialized. Call init_encryption() first.") if not ciphertext: return "" @@ -139,10 +123,11 @@ def is_encrypted(value: str) -> bool: # Check for Fernet prefix and valid format try: # Fernet tokens always start with 'gAAAAA' in base64 - if not value.startswith('gAAAAA'): + if not value.startswith("gAAAAA"): return False # Attempt to decode as URL-safe base64 import base64 + decoded = base64.urlsafe_b64decode(value) # Fernet tokens have a specific format (minimum 32 bytes) return len(decoded) >= 32 diff --git a/gns3server/utils/get_resource.py b/gns3server/utils/get_resource.py index 0891e8f12..20ea3e009 100644 --- a/gns3server/utils/get_resource.py +++ b/gns3server/utils/get_resource.py @@ -26,6 +26,7 @@ except ImportError: from contextlib import ExitStack + resource_manager = ExitStack() atexit.register(resource_manager.close) diff --git a/gns3server/utils/hostname.py b/gns3server/utils/hostname.py index 828acf5e5..bb9291e3f 100644 --- a/gns3server/utils/hostname.py +++ b/gns3server/utils/hostname.py @@ -38,15 +38,15 @@ def to_ios_hostname(name): """ # Replace invalid characters with hyphens - name = re.sub(r'[^a-zA-Z0-9-]', '-', name) + name = re.sub(r"[^a-zA-Z0-9-]", "-", name) # Ensure the hostname starts with a letter - if not re.search(r'^[a-zA-Z]', name): - name = 'a' + name + if not re.search(r"^[a-zA-Z]", name): + name = "a" + name # Ensure the hostname ends with a letter or digit - if not re.search(r'[a-zA-Z0-9]$', name): - name = name.rstrip('-') + '0' + if not re.search(r"[a-zA-Z0-9]$", name): + name = name.rstrip("-") + "0" # Truncate the hostname to 63 characters name = name[:63] @@ -87,24 +87,24 @@ def to_rfc1123_hostname(name: str) -> str: """ # Replace invalid characters with hyphens - name = re.sub(r'[^a-zA-Z0-9-.]', '-', name) + name = re.sub(r"[^a-zA-Z0-9-.]", "-", name) # Remove trailing dot if it exists - name = name.rstrip('.') + name = name.rstrip(".") # Ensure each label is not longer than 63 characters - labels = name.split('.') + labels = name.split(".") labels = [label[:63] for label in labels] # Remove leading and trailing hyphens from each label if they exist - labels = [label.strip('-') for label in labels] + labels = [label.strip("-") for label in labels] # Check if the TLD is all-numeric and if so, replace it with "invalid" if re.match(r"[0-9]+$", labels[-1]): - labels[-1] = 'invalid' + labels[-1] = "invalid" # Join the labels back together - name = '.'.join(labels) + name = ".".join(labels) # Ensure the total length is not longer than 253 characters name = name[:253] diff --git a/gns3server/utils/images.py b/gns3server/utils/images.py index a1074191d..c06e22bec 100644 --- a/gns3server/utils/images.py +++ b/gns3server/utils/images.py @@ -55,7 +55,6 @@ async def list_images(image_type): default_directory = default_images_directory(image_type) for directory in images_directories(image_type): - # We limit recursion to path outside the default images directory # the reason is in the default directory manage file organization and # it should be flatten to keep things simple @@ -89,15 +88,19 @@ async def list_images(image_type): with open(os.path.join(root, filename), "rb") as f: # read the first 7 bytes of the file. elf_header_start = f.read(7) - if image_type == "dynamips" and elf_header_start != b'\x7fELF\x01\x02\x01': + if image_type == "dynamips" and elf_header_start != b"\x7fELF\x01\x02\x01": # IOS images must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1 log.warning(f"IOS image {filename} does not start with a valid ELF magic number, skipping...") continue - elif image_type == "iou" and elf_header_start != b'\x7fELF\x02\x01\x01' and elf_header_start != b'\x7fELF\x01\x01\x01': + elif ( + image_type == "iou" + and elf_header_start != b"\x7fELF\x02\x01\x01" + and elf_header_start != b"\x7fELF\x01\x01\x01" + ): # IOU images must start with the ELF magic number, be 32-bit or 64-bit, little endian and have an ELF version of 1 log.warning(f"IOU image {filename} does not start with a valid ELF magic number, skipping...") continue - elif image_type == "qemu" and elf_header_start[:4] == b'\x7fELF': + elif image_type == "qemu" and elf_header_start[:4] == b"\x7fELF": # QEMU images should not start with an ELF magic number log.warning(f"QEMU image {filename} starts with an ELF magic number, skipping...") continue @@ -117,7 +120,7 @@ async def list_images(image_type): def get_builtin_disks() -> List[str]: builtin_disks = [] - for entry in importlib_resources.files('gns3server').joinpath("disks").iterdir(): + for entry in importlib_resources.files("gns3server").joinpath("disks").iterdir(): if entry.is_file(): builtin_disks.append(entry.name) return builtin_disks @@ -132,8 +135,10 @@ async def read_image_info(path: str, expected_image_type: str = None) -> dict: if len(image_header) >= header_magic_len: detected_image_type = check_valid_image_header(path, image_header) if expected_image_type and detected_image_type != expected_image_type: - raise InvalidImageError(f"Detected image type for '{path}' is {detected_image_type}, " - f"expected type is {expected_image_type}") + raise InvalidImageError( + f"Detected image type for '{path}' is {detected_image_type}, " + f"expected type is {expected_image_type}" + ) else: raise InvalidImageError(f"Image '{path}' is too small to be valid") except OSError as e: @@ -306,7 +311,6 @@ def remove_checksum(path): class InvalidImageError(Exception): - def __init__(self, message: str): super().__init__() self._message = message @@ -317,14 +321,14 @@ class InvalidImageError(Exception): def check_valid_image_header(path: str, data: bytes, allow_raw_image: bool = False) -> str: - if data[:7] == b'\x7fELF\x01\x02\x01': + if data[:7] == b"\x7fELF\x01\x02\x01": # for IOS images: file must start with the ELF magic number, be 32-bit, big endian and have an ELF version of 1 return "ios" - elif data[:7] == b'\x7fELF\x01\x01\x01' or data[:7] == b'\x7fELF\x02\x01\x01': + elif data[:7] == b"\x7fELF\x01\x01\x01" or data[:7] == b"\x7fELF\x02\x01\x01": # for IOU images: file must start with the ELF magic number, be 32-bit or 64-bit, little endian and # have an ELF version of 1 (normal IOS images are big endian!) return "iou" - elif data[:4] == b'QFI\xfb' or data[:4] == b'KDMV': + elif data[:4] == b"QFI\xfb" or data[:4] == b"KDMV": # for Qemy images: file must be QCOW2 or VMDK return "qemu" else: @@ -334,12 +338,12 @@ def check_valid_image_header(path: str, data: bytes, allow_raw_image: bool = Fal async def write_image( - image_filename: str, - image_path: str, - stream: AsyncGenerator[bytes, None], - images_repo: ImagesRepository, - check_image_header=True, - allow_raw_image=False + image_filename: str, + image_path: str, + stream: AsyncGenerator[bytes, None], + images_repo: ImagesRepository, + check_image_header=True, + allow_raw_image=False, ) -> models.Image: image_dir, image_name = os.path.split(image_filename) @@ -370,15 +374,17 @@ async def write_image( image_path = os.path.abspath(os.path.join(directory, image_filename)) if os.path.exists(image_path): - raise InvalidImageError(f"File '{image_path}' already exists, " - f"please choose a different name or remove the existing image") + raise InvalidImageError( + f"File '{image_path}' already exists, please choose a different name or remove the existing image" + ) checksum = checksum.hexdigest() image_dir = os.path.dirname(image_path) duplicate_image = await images_repo.get_image_by_checksum(checksum, image_dir) if duplicate_image: - raise InvalidImageError(f"Image '{duplicate_image.filename}' with the " - f"same checksum already exists in '{image_dir}'") + raise InvalidImageError( + f"Image '{duplicate_image.filename}' with the same checksum already exists in '{image_dir}'" + ) shutil.move(tmp_path, image_path) os.chmod(image_path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC) @@ -390,10 +396,5 @@ async def write_image( log.warning(f"Could not remove '{tmp_path}'") return await images_repo.add_image( - image_name, - image_type, - image_size, - image_path, - checksum, - checksum_algorithm="md5" + image_name, image_type, image_size, image_path, checksum, checksum_algorithm="md5" ) diff --git a/gns3server/utils/interfaces.py b/gns3server/utils/interfaces.py index 1bc8b8051..f48db2c8a 100644 --- a/gns3server/utils/interfaces.py +++ b/gns3server/utils/interfaces.py @@ -49,11 +49,11 @@ def _get_windows_interfaces_from_registry(): netcard, _ = winreg.QueryValueEx(hkeycard, "Description") connection = ( r"SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}" - + fr"\{guid}\Connection" + + rf"\{guid}\Connection" ) hkeycon = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, connection) name, _ = winreg.QueryValueEx(hkeycon, "Name") - interface = fr"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{guid}" + interface = rf"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{guid}" hkeyinterface = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, interface) is_dhcp_enabled, _ = winreg.QueryValueEx(hkeyinterface, "EnableDHCP") if is_dhcp_enabled: @@ -160,7 +160,6 @@ def is_interface_up(interface): """ if sys.platform.startswith("linux") or sys.platform.startswith("openbsd"): - if interface not in psutil.net_if_addrs(): return False @@ -218,13 +217,9 @@ def interfaces(): # legacy single-value behavior (keeps the last IPv4 seen) ip_address = addr.address netmask = addr.netmask - ip_addresses.append( - {"family": "ipv4", "address": addr.address, "netmask": addr.netmask or None} - ) + ip_addresses.append({"family": "ipv4", "address": addr.address, "netmask": addr.netmask or None}) elif addr.family == socket.AF_INET6: - ip_addresses.append( - {"family": "ipv6", "address": addr.address, "netmask": addr.netmask or None} - ) + ip_addresses.append({"family": "ipv6", "address": addr.address, "netmask": addr.netmask or None}) if addr.family == psutil.AF_LINK: mac_address = addr.address if interface.startswith("tap"): @@ -283,7 +278,7 @@ def interfaces(): "vmware", "virtualbox", "gns3", - "veb" + "veb", ): if result["name"].lower().startswith(special_interface): result["special"] = True diff --git a/gns3server/utils/packet_filter_validation.py b/gns3server/utils/packet_filter_validation.py index 2ba67f6f5..cc2634546 100644 --- a/gns3server/utils/packet_filter_validation.py +++ b/gns3server/utils/packet_filter_validation.py @@ -11,6 +11,7 @@ log = logging.getLogger(__name__) class FilterValidationError(Exception): """Raised when packet filter parameters fail validation.""" + pass @@ -46,7 +47,7 @@ def validate_bpf_syntax(bpf_expression: str) -> Dict[str, Optional[str]]: # Strip "tcpdump: " prefix for prefix in ["tcpdump: "]: if line.startswith(prefix): - line = line[len(prefix):] + line = line[len(prefix) :] error_lines.append(line) error_msg = " ".join(error_lines) if error_lines else "Invalid BPF expression" log.warning("BPF syntax validation failed: %s", error_msg) @@ -56,10 +57,7 @@ def validate_bpf_syntax(bpf_expression: str) -> Dict[str, Optional[str]]: return {"valid": True, "error": None} except FileNotFoundError: - log.warning( - "tcpdump not found, skipping BPF syntax validation. " - "Install tcpdump to enable BPF validation." - ) + log.warning("tcpdump not found, skipping BPF syntax validation. Install tcpdump to enable BPF validation.") return {"valid": True, "error": None} except Exception as e: @@ -85,31 +83,17 @@ def validate_filter_parameters(filter_type: str, values: List[Any]) -> None: "params_count": 1, "ranges": [(-1, 32767)], # min, max "names": ["Frequency"], - "units": ["th packet"] - }, - "packet_loss": { - "params_count": 1, - "ranges": [(0, 100)], - "names": ["Chance"], - "units": ["%"] + "units": ["th packet"], }, + "packet_loss": {"params_count": 1, "ranges": [(0, 100)], "names": ["Chance"], "units": ["%"]}, "delay": { "params_count": 2, # latency, jitter "ranges": [(1, 32767), (0, 32767)], # ubridge rejects latency <= 0 "names": ["Latency", "Jitter"], - "units": ["ms", "ms"] + "units": ["ms", "ms"], }, - "corrupt": { - "params_count": 1, - "ranges": [(0, 100)], - "names": ["Chance"], - "units": ["%"] - }, - "bpf": { - "params_count": 1, - "is_text": True, - "names": ["Filters"] - } + "corrupt": {"params_count": 1, "ranges": [(0, 100)], "names": ["Chance"], "units": ["%"]}, + "bpf": {"params_count": 1, "is_text": True, "names": ["Filters"]}, } if filter_type not in VALIDATION_RULES: @@ -119,18 +103,14 @@ def validate_filter_parameters(filter_type: str, values: List[Any]) -> None: # Check parameter count if len(values) != rules["params_count"]: - raise FilterValidationError( - f"{filter_type} expects {rules['params_count']} parameter(s), got {len(values)}" - ) + raise FilterValidationError(f"{filter_type} expects {rules['params_count']} parameter(s), got {len(values)}") # Validate each parameter for i, value in enumerate(values): if rules.get("is_text"): # Text validation (BPF) if not isinstance(value, str): - raise FilterValidationError( - f"{filter_type} parameter {rules['names'][i]} must be a string" - ) + raise FilterValidationError(f"{filter_type} parameter {rules['names'][i]} must be a string") # Validate BPF syntax using tshark (same method as gns3_copilot) # The value may be a multi-line string; each line becomes a @@ -251,4 +231,4 @@ def validate_all_filters(filters: Dict[str, List[Any]]) -> None: if not values or (isinstance(values, list) and len(values) == 0): continue - validate_filter_parameters(filter_type, values) \ No newline at end of file + validate_filter_parameters(filter_type, values) diff --git a/gns3server/utils/request_utils.py b/gns3server/utils/request_utils.py index aee3bdb2e..297a9e90b 100644 --- a/gns3server/utils/request_utils.py +++ b/gns3server/utils/request_utils.py @@ -73,7 +73,7 @@ def extract_client_info(scope: Dict[str, Any], auth_service_instance: Optional[A "path": path, "method": method, "username": username, - "user_info": user_info + "user_info": user_info, } diff --git a/gns3server/utils/uninstall_ai_features.py b/gns3server/utils/uninstall_ai_features.py index 44b545e25..141ff9ce7 100644 --- a/gns3server/utils/uninstall_ai_features.py +++ b/gns3server/utils/uninstall_ai_features.py @@ -15,7 +15,7 @@ import argparse def _find_base_dir(): """Find the project root directory.""" - if hasattr(sys, '_MEIPASS'): + if hasattr(sys, "_MEIPASS"): return os.path.dirname(sys.executable) return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -71,9 +71,7 @@ def uninstall(packages, yes=False): for package in packages: try: result = subprocess.run( - [sys.executable, "-m", "pip", "uninstall", "-y", package], - capture_output=True, - text=True + [sys.executable, "-m", "pip", "uninstall", "-y", package], capture_output=True, text=True ) if result.returncode == 0: print(f" Removed: {package}") @@ -87,13 +85,8 @@ def uninstall(packages, yes=False): def main(): - parser = argparse.ArgumentParser( - description="Uninstall AI Features dependencies (AI Copilot + MCP)" - ) - parser.add_argument( - "-y", "--yes", action="store_true", - help="Automatically confirm uninstallation" - ) + parser = argparse.ArgumentParser(description="Uninstall AI Features dependencies (AI Copilot + MCP)") + parser.add_argument("-y", "--yes", action="store_true", help="Automatically confirm uninstallation") args = parser.parse_args() packages = get_ai_packages() diff --git a/gns3server/utils/uuid_validator.py b/gns3server/utils/uuid_validator.py index 0ad316777..08524f4c9 100644 --- a/gns3server/utils/uuid_validator.py +++ b/gns3server/utils/uuid_validator.py @@ -7,10 +7,7 @@ import argparse import re -UUID_PATTERN = re.compile( - r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', - re.IGNORECASE -) +UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE) def validate_uuid(value: str) -> str: diff --git a/gns3server/utils/websocket_to_websocket.py b/gns3server/utils/websocket_to_websocket.py index 6155b3ca1..88efdbfd2 100644 --- a/gns3server/utils/websocket_to_websocket.py +++ b/gns3server/utils/websocket_to_websocket.py @@ -41,7 +41,7 @@ async def websocket_proxy( target_url: str, requested_protocols: list = None, buffer_size: int = 65536, - timeout: Optional[float] = None + timeout: Optional[float] = None, ) -> None: """ Proxy binary WebSocket data between client and target WebSocket server. @@ -116,28 +116,20 @@ async def websocket_proxy( if sys.version_info >= (3, 11, 0): aws = [ asyncio.create_task(forward_client_to_target(target_ws)), - asyncio.create_task(forward_target_to_client(target_ws)) + asyncio.create_task(forward_target_to_client(target_ws)), ] else: - aws = [ - forward_client_to_target(target_ws), - forward_target_to_client(target_ws) - ] + aws = [forward_client_to_target(target_ws), forward_target_to_client(target_ws)] try: - done, pending = await asyncio.wait( - aws, - return_when=asyncio.FIRST_COMPLETED - ) + done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED) except Exception as e: log.error(f"asyncio.wait raised exception: {e}") # Check for exceptions for task in done: if task.exception(): - log.warning( - f"WebSocket proxy task exception: {task.exception()}" - ) + log.warning(f"WebSocket proxy task exception: {task.exception()}") # Cancel pending tasks for task in pending: @@ -145,16 +137,10 @@ async def websocket_proxy( except aiohttp.ClientError as e: log.error(f"WebSocket proxy connection error: {e}") - await client_ws.close( - code=status.WS_1011_INTERNAL_ERROR, - reason=f"Proxy connection failed: {e}" - ) + await client_ws.close(code=status.WS_1011_INTERNAL_ERROR, reason=f"Proxy connection failed: {e}") except Exception as e: log.error(f"WebSocket proxy unexpected error: {e}") - await client_ws.close( - code=status.WS_1011_INTERNAL_ERROR, - reason=str(e) - ) + await client_ws.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(e)) async def websocket_proxy_with_manual_accept( @@ -162,7 +148,7 @@ async def websocket_proxy_with_manual_accept( target_url: str, requested_protocols: list = None, buffer_size: int = 65536, - timeout: Optional[float] = None + timeout: Optional[float] = None, ) -> None: """ Proxy binary WebSocket data between client and target WebSocket server. @@ -181,7 +167,7 @@ async def websocket_proxy_with_manual_accept( Raises: aiohttp.ClientError: If connection to target fails """ - client_info = f"{client_ws.client.host}:{client_ws.client.port}" if hasattr(client_ws, 'client') else "unknown" + client_info = f"{client_ws.client.host}:{client_ws.client.port}" if hasattr(client_ws, "client") else "unknown" async def forward_client_to_target(target_ws): """Client → Target: Forward binary WebSocket data.""" @@ -252,20 +238,14 @@ async def websocket_proxy_with_manual_accept( if sys.version_info >= (3, 11, 0): aws = [ asyncio.create_task(forward_client_to_target(target_ws)), - asyncio.create_task(forward_target_to_client(target_ws)) + asyncio.create_task(forward_target_to_client(target_ws)), ] else: - aws = [ - forward_client_to_target(target_ws), - forward_target_to_client(target_ws) - ] + aws = [forward_client_to_target(target_ws), forward_target_to_client(target_ws)] log.info(f"About to call asyncio.wait with {len(aws)} tasks") try: - done, pending = await asyncio.wait( - aws, - return_when=asyncio.FIRST_COMPLETED - ) + done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED) log.info(f"asyncio.wait returned. done={len(done)}, pending={len(pending)}") except Exception as e: log.error(f"asyncio.wait raised exception: {e}") @@ -274,9 +254,7 @@ async def websocket_proxy_with_manual_accept( # Check for exceptions for task in done: if task.exception(): - log.warning( - f"WebSocket proxy task exception: {task.exception()}" - ) + log.warning(f"WebSocket proxy task exception: {task.exception()}") # Cancel pending tasks for task in pending: diff --git a/gns3server/utils/zipfile_zstd/__init__.py b/gns3server/utils/zipfile_zstd/__init__.py index 61f10fd79..4aae04421 100644 --- a/gns3server/utils/zipfile_zstd/__init__.py +++ b/gns3server/utils/zipfile_zstd/__init__.py @@ -1,4 +1,3 @@ - # NOTE: this patches the standard zipfile module from . import _zipfile @@ -7,4 +6,3 @@ from zipfile import ( ZIP_ZSTANDARD, ZSTANDARD_VERSION, ) - diff --git a/gns3server/utils/zipfile_zstd/_patcher.py b/gns3server/utils/zipfile_zstd/_patcher.py index 83d7fc7c5..ee2d19812 100644 --- a/gns3server/utils/zipfile_zstd/_patcher.py +++ b/gns3server/utils/zipfile_zstd/_patcher.py @@ -2,7 +2,6 @@ import functools class patch: - originals = {} def __init__(self, host, name): @@ -17,4 +16,3 @@ class patch: setattr(self.host, self.name, func) return func - diff --git a/gns3server/utils/zipfile_zstd/_zipfile.py b/gns3server/utils/zipfile_zstd/_zipfile.py index c082fe49f..2d5d200fa 100644 --- a/gns3server/utils/zipfile_zstd/_zipfile.py +++ b/gns3server/utils/zipfile_zstd/_zipfile.py @@ -6,16 +6,16 @@ from ._patcher import patch zipfile.ZIP_ZSTANDARD = 93 -zipfile.compressor_names[zipfile.ZIP_ZSTANDARD] = 'zstandard' +zipfile.compressor_names[zipfile.ZIP_ZSTANDARD] = "zstandard" zipfile.ZSTANDARD_VERSION = 20 -@patch(zipfile, '_check_compression') +@patch(zipfile, "_check_compression") def zstd_check_compression(compression): if compression == zipfile.ZIP_ZSTANDARD: pass else: - patch.originals['_check_compression'](compression) + patch.originals["_check_compression"](compression) class ZstdDecompressObjWrapper: @@ -23,42 +23,44 @@ class ZstdDecompressObjWrapper: self.o = o def __getattr__(self, attr): - if attr == 'eof': + if attr == "eof": return False return getattr(self.o, attr) -@patch(zipfile, '_get_decompressor') +@patch(zipfile, "_get_decompressor") def zstd_get_decompressor(compress_type): if compress_type == zipfile.ZIP_ZSTANDARD: return ZstdDecompressObjWrapper(zstd.ZstdDecompressor(max_window_size=2147483648).decompressobj()) else: - return patch.originals['_get_decompressor'](compress_type) + return patch.originals["_get_decompressor"](compress_type) -if 'compresslevel' in inspect.signature(zipfile._get_compressor).parameters: - @patch(zipfile, '_get_compressor') +if "compresslevel" in inspect.signature(zipfile._get_compressor).parameters: + + @patch(zipfile, "_get_compressor") def zstd_get_compressor(compress_type, compresslevel=None): if compress_type == zipfile.ZIP_ZSTANDARD: if compresslevel is None: compresslevel = 3 return zstd.ZstdCompressor(level=compresslevel, threads=-1).compressobj() else: - return patch.originals['_get_compressor'](compress_type, compresslevel=compresslevel) + return patch.originals["_get_compressor"](compress_type, compresslevel=compresslevel) else: - @patch(zipfile, '_get_compressor') + + @patch(zipfile, "_get_compressor") def zstd_get_compressor(compress_type, compresslevel=None): if compress_type == zipfile.ZIP_ZSTANDARD: if compresslevel is None: compresslevel = 3 return zstd.ZstdCompressor(level=compresslevel, threads=-1).compressobj() else: - return patch.originals['_get_compressor'](compress_type) + return patch.originals["_get_compressor"](compress_type) -@patch(zipfile.ZipInfo, 'FileHeader') +@patch(zipfile.ZipInfo, "FileHeader") def zstd_FileHeader(self, zip64=None): if self.compress_type == zipfile.ZIP_ZSTANDARD: self.create_version = max(self.create_version, zipfile.ZSTANDARD_VERSION) self.extract_version = max(self.extract_version, zipfile.ZSTANDARD_VERSION) - return patch.originals['FileHeader'](self, zip64=zip64) + return patch.originals["FileHeader"](self, zip64=zip64) diff --git a/gns3server/version.py b/gns3server/version.py index bc810eb50..a081bec6d 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -29,6 +29,7 @@ if "dev" in __version__: try: import os import subprocess + if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")): r = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip("\n") __version__ += "+" + r diff --git a/scripts/extract_mermaid.py b/scripts/extract_mermaid.py index 6a299fb04..aae2044b9 100755 --- a/scripts/extract_mermaid.py +++ b/scripts/extract_mermaid.py @@ -104,10 +104,15 @@ def convert_mermaid_to_svg( result = subprocess.run( [ - "npx", "--yes", MMDC_PACKAGE, - "-i", mmd_path, - "-o", output_path, - "-b", background, + "npx", + "--yes", + MMDC_PACKAGE, + "-i", + mmd_path, + "-o", + output_path, + "-b", + background, ], capture_output=True, text=True, @@ -173,17 +178,12 @@ def process_file( def main(): - parser = argparse.ArgumentParser( - description="Extract mermaid diagrams from Markdown and convert to SVG." - ) + parser = argparse.ArgumentParser(description="Extract mermaid diagrams from Markdown and convert to SVG.") parser.add_argument( "source", nargs="?", default=None, - help=( - "Markdown file or directory to scan. " - "Defaults to all *-overview*.md in docs/gns3-copilot/implemented/." - ), + help=("Markdown file or directory to scan. Defaults to all *-overview*.md in docs/gns3-copilot/implemented/."), ) parser.add_argument( "dest", @@ -197,23 +197,27 @@ def main(): help=f"Path to Chrome binary (auto-detected: {DEFAULT_CHROME_PATH})", ) parser.add_argument( - "--background", "-b", + "--background", + "-b", default="transparent", help="SVG background color (default: transparent)", ) parser.add_argument( - "--timeout", "-t", + "--timeout", + "-t", type=int, default=30, help="Timeout in seconds per diagram (default: 30)", ) parser.add_argument( - "--check", "-c", + "--check", + "-c", action="store_true", help="Check environment and exit (no conversion).", ) parser.add_argument( - "--install", "-i", + "--install", + "-i", action="store_true", help="Install missing dependencies (Chrome, mermaid-cli).", ) @@ -236,9 +240,7 @@ def main(): # 2. Install Chrome via puppeteer chrome = args.chrome or DEFAULT_CHROME_PATH if chrome and os.path.exists(chrome): - result = subprocess.run( - [chrome, "--version"], capture_output=True, text=True, timeout=10 - ) + result = subprocess.run([chrome, "--version"], capture_output=True, text=True, timeout=10) version = result.stdout.strip() if result.returncode == 0 else "unknown" print(f" Chrome: already installed ({version})") else: @@ -259,7 +261,9 @@ def main(): print(" mermaid-cli: caching...") ret = subprocess.run( ["npx", "--yes", MMDC_PACKAGE, "--version"], - capture_output=True, text=True, timeout=60, + capture_output=True, + text=True, + timeout=60, ) if ret.returncode == 0: print(" mermaid-cli: ready") @@ -285,10 +289,7 @@ def main(): # 2. npx / Node.js try: - result = subprocess.run( - ["npx", "--version"], - capture_output=True, text=True, timeout=10 - ) + result = subprocess.run(["npx", "--version"], capture_output=True, text=True, timeout=10) if result.returncode == 0: print(f" npx: {result.stdout.strip()}") else: @@ -302,10 +303,7 @@ def main(): chrome = args.chrome or DEFAULT_CHROME_PATH if chrome and os.path.exists(chrome): try: - result = subprocess.run( - [chrome, "--version"], - capture_output=True, text=True, timeout=10 - ) + result = subprocess.run([chrome, "--version"], capture_output=True, text=True, timeout=10) version = result.stdout.strip() if result.returncode == 0 else "?" print(f" Chrome: {version}") print(f" Path: {chrome}") @@ -319,8 +317,7 @@ def main(): # 4. @mermaid-js/mermaid-cli try: result = subprocess.run( - ["npx", "--yes", MMDC_PACKAGE, "--version"], - capture_output=True, text=True, timeout=30 + ["npx", "--yes", MMDC_PACKAGE, "--version"], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: print(" mermaid-cli: available") @@ -366,13 +363,10 @@ def main(): docs_dir = os.path.join(project_root, "docs", "gns3-copilot", "implemented") if os.path.isdir(docs_dir): files_to_process = sorted( - os.path.join(docs_dir, f) - for f in os.listdir(docs_dir) - if "-overview" in f and f.endswith(".md") + os.path.join(docs_dir, f) for f in os.listdir(docs_dir) if "-overview" in f and f.endswith(".md") ) else: - print("No source specified and default docs directory not found.", - file=sys.stderr) + print("No source specified and default docs directory not found.", file=sys.stderr) sys.exit(1) if not files_to_process: @@ -396,8 +390,7 @@ def main(): chrome = args.chrome or DEFAULT_CHROME_PATH if not chrome: print( - "WARNING: Chrome not found in puppeteer cache. " - "Run: npx puppeteer browsers install chrome-headless-shell", + "WARNING: Chrome not found in puppeteer cache. Run: npx puppeteer browsers install chrome-headless-shell", file=sys.stderr, ) else: diff --git a/scripts/publish_api_documentation.py b/scripts/publish_api_documentation.py index 20d33316a..ab2e15704 100644 --- a/scripts/publish_api_documentation.py +++ b/scripts/publish_api_documentation.py @@ -24,19 +24,19 @@ from gns3server.api.server import app if __name__ == "__main__": - with open("../docs/openapi.json", "w") as fd: fd.write(json.dumps(app.openapi())) - swagger_html = get_swagger_ui_html(openapi_url="openapi.json", - title=app.title + " - Swagger UI", - oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url) + swagger_html = get_swagger_ui_html( + openapi_url="openapi.json", + title=app.title + " - Swagger UI", + oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, + ) with open("../docs/index.html", "w") as fd: fd.write(swagger_html.body.decode()) - redoc_html = get_redoc_html(openapi_url="openapi.json", - title=app.title + " - ReDoc") + redoc_html = get_redoc_html(openapi_url="openapi.json", title=app.title + " - ReDoc") with open("../docs/redoc.html", "w") as fd: fd.write(redoc_html.body.decode()) diff --git a/scripts/tag.py b/scripts/tag.py index 27c280a5d..3b7d3e02d 100755 --- a/scripts/tag.py +++ b/scripts/tag.py @@ -10,9 +10,7 @@ from pathlib import Path def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( - description=( - "Read gns3server/version.py, extract __version__, and create an annotated git tag." - ) + description=("Read gns3server/version.py, extract __version__, and create an annotated git tag.") ) parser.add_argument( "--dry-run", @@ -51,7 +49,7 @@ def main() -> int: return 1 try: - tag = 'v' + get_version(repo_root) + tag = "v" + get_version(repo_root) except (OSError, ValueError) as exc: print(f"Error: {exc}", file=sys.stderr) return 1 @@ -81,5 +79,3 @@ def main() -> int: if __name__ == "__main__": raise SystemExit(main()) - - diff --git a/scripts/welcome.py b/scripts/welcome.py index 77de8d75e..dc3d5907f 100644 --- a/scripts/welcome.py +++ b/scripts/welcome.py @@ -31,7 +31,7 @@ from dialog import Dialog, PythonDialogBug class Welcome_dialog: def __init__(self): try: - locale.setlocale(locale.LC_ALL, '') + locale.setlocale(locale.LC_ALL, "") except locale.Error: # Not supported via SSH pass @@ -45,35 +45,36 @@ class Welcome_dialog: """ Return the active IP """ - #request 'ip addr' data in JSON format from shell - ip_addr_response = subprocess.run(['ip', '--json', 'addr'],capture_output=True) - - #process response, decode and use json.loads to convert the string to a dict + # request 'ip addr' data in JSON format from shell + ip_addr_response = subprocess.run(["ip", "--json", "addr"], capture_output=True) + + # process response, decode and use json.loads to convert the string to a dict ip_addr_data = convert(ip_addr_response.stdout.decode("utf-8")) - - #search ip_addr_data for the first ip adress that is not under a virtual bridge or loopback interface + + # search ip_addr_data for the first ip adress that is not under a virtual bridge or loopback interface for i in ip_addr_data: - if ('virbr' in i['ifname']) or ('lo' in i['ifname']): + if ("virbr" in i["ifname"]) or ("lo" in i["ifname"]): continue try: - if 'UP' in i['flags']: - ip_addr = i['addr_info'][0]['local'] + if "UP" in i["flags"]: + ip_addr = i["addr_info"][0]["local"] break except: continue ip_addr = None - + return ip_addr def repair_remote_install(self): """ This method is only called by remote-install.sh during setup to ensure it is setting the same IP as shown by Dialog - """ + """ ip_addr = self.get_ip() - subprocess.run(["sed", "-i", f"s/host = 0.0.0.0/host = {ip_addr}/", "/etc/gns3/gns3_server.conf"],capture_output=False) - subprocess.run(["service", "gns3", "stop"],capture_output=False) - subprocess.run(["service", "gns3", "start"],capture_output=False) - + subprocess.run( + ["sed", "-i", f"s/host = 0.0.0.0/host = {ip_addr}/", "/etc/gns3/gns3_server.conf"], capture_output=False + ) + subprocess.run(["service", "gns3", "stop"], capture_output=False) + subprocess.run(["service", "gns3", "start"], capture_output=False) def get_config(self): """ @@ -84,16 +85,14 @@ class Welcome_dialog: config.read([path], encoding="utf-8") return config - def write_config(self, config): """ Write the config file """ - with open(os.path.expanduser("~/.config/GNS3/gns3_server.conf"), 'w') as f: + with open(os.path.expanduser("~/.config/GNS3/gns3_server.conf"), "w") as f: config.write(f) - def gns3_major_version(self): """ Returns the GNS3 major server version @@ -105,7 +104,6 @@ class Welcome_dialog: return match.group(0) return "" - def gns3_version(self): """ Return the GNS3 server version @@ -115,25 +113,34 @@ class Welcome_dialog: except (subprocess.CalledProcessError, FileNotFoundError): return None - def gns3vm_version(self): """ Return the GNS3 VM version """ try: - with open('/home/gns3/.config/GNS3/gns3vm_version') as f: + with open("/home/gns3/.config/GNS3/gns3vm_version") as f: return f.read().strip() except FileNotFoundError: return "Remote Install" - def mode(self): - if self.display.yesno("This feature is for testers only. You may break your GNS3 installation. Are you REALLY sure you want to continue?", yes_label="Exit (Safe option)", no_label="Continue") == self.display.OK: + if ( + self.display.yesno( + "This feature is for testers only. You may break your GNS3 installation. Are you REALLY sure you want to continue?", + yes_label="Exit (Safe option)", + no_label="Continue", + ) + == self.display.OK + ): return - code, tag = self.display.menu("Select the GNS3 version", - choices=[("2.1", "Stable release for this GNS3 VM (RECOMMENDED)"), - ("2.1dev", "Development version for stable release"), - ("2.2", "Latest stable release")]) + code, tag = self.display.menu( + "Select the GNS3 version", + choices=[ + ("2.1", "Stable release for this GNS3 VM (RECOMMENDED)"), + ("2.1dev", "Development version for stable release"), + ("2.2", "Latest stable release"), + ], + ) self.display.clear() if code == Dialog.OK: os.makedirs(os.path.expanduser("~/.config/GNS3"), exist_ok=True) @@ -142,7 +149,6 @@ class Welcome_dialog: self.update(force=True) - def get_release(self): try: with open(os.path.expanduser("~/.config/GNS3/gns3_release")) as f: @@ -160,15 +166,21 @@ class Welcome_dialog: except OSError: return "1.5" - def update(self, force=False): if not force: - if self.display.yesno("It is recommended to ensure all Nodes are shutdown before upgrading. Continue?") != self.display.OK: + if ( + self.display.yesno("It is recommended to ensure all Nodes are shutdown before upgrading. Continue?") + != self.display.OK + ): return - code, option = self.display.menu("Select an option", - choices=[("Upgrade GNS3", "Upgrades only the GNS3 pakage and dependences."), - ("Upgrade All", "Upgrades all avaiable packages"), - ("Dist Upgrade", "Upgrades all avaiable packages and the Linux Kernel. Requires a reboot.")]) + code, option = self.display.menu( + "Select an option", + choices=[ + ("Upgrade GNS3", "Upgrades only the GNS3 pakage and dependences."), + ("Upgrade All", "Upgrades all avaiable packages"), + ("Dist Upgrade", "Upgrades all avaiable packages and the Linux Kernel. Requires a reboot."), + ], + ) if code == Dialog.OK: if option == "Upgrade GNS3": ret = os.system( @@ -196,18 +208,23 @@ class Welcome_dialog: if self.display.yesno("Reboot now?") == self.display.OK: os.system("sudo reboot now") - def migrate(self): """ Migrate GNS3 VM data. """ - code, option = self.display.menu("Select an option", - choices=[("Setup", "Configure this VM to send data to another GNS3 VM"), - ("Send", "Send images and projects to another GNS3 VM")]) + code, option = self.display.menu( + "Select an option", + choices=[ + ("Setup", "Configure this VM to send data to another GNS3 VM"), + ("Send", "Send images and projects to another GNS3 VM"), + ], + ) self.display.clear() if code == Dialog.OK: - (answer, destination) = self.display.inputbox("What is IP address or hostname of the other GNS3 VM?", init="172.16.1.128") + (answer, destination) = self.display.inputbox( + "What is IP address or hostname of the other GNS3 VM?", init="172.16.1.128" + ) if answer != self.display.OK: return if destination == self.get_ip(): @@ -217,13 +234,19 @@ class Welcome_dialog: # first make sure they are no files belonging to root os.system("sudo chown -R gns3:gns3 /opt/gns3") # then rsync the data - command = r"rsync -az --progress -e 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /home/gns3/.ssh/gns3-vm-key' /opt/gns3 gns3@{}:/opt".format(destination) + command = r"rsync -az --progress -e 'ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /home/gns3/.ssh/gns3-vm-key' /opt/gns3 gns3@{}:/opt".format( + destination + ) ret = os.system('bash -c "{}"'.format(command)) time.sleep(10) if ret != 0: self.display.msgbox("Could not send data to the other GNS3 VM located at {}".format(destination)) else: - self.display.msgbox("Images and projects have been successfully sent to the other GNS3 VM located at {}".format(destination)) + self.display.msgbox( + "Images and projects have been successfully sent to the other GNS3 VM located at {}".format( + destination + ) + ) elif option == "Setup": script = """ if [ ! -f ~/.ssh/gns3-vm-key ] @@ -237,8 +260,11 @@ class Welcome_dialog: if ret != 0: self.display.msgbox("Error while setting up the migrate feature") else: - self.display.msgbox("Configuration successful, you can now send data to the GNS3 VM located at {} without password".format(destination)) - + self.display.msgbox( + "Configuration successful, you can now send data to the GNS3 VM located at {} without password".format( + destination + ) + ) def shrink_disk(self): @@ -247,7 +273,12 @@ class Welcome_dialog: self.display.msgbox("Shrinking the disk is only supported when running inside VMware") return - if self.display.yesno("Would you like to shrink the VM disk? The VM will reboot at the end of the process. Continue?") != self.display.OK: + if ( + self.display.yesno( + "Would you like to shrink the VM disk? The VM will reboot at the end of the process. Continue?" + ) + != self.display.OK + ): return os.system("sudo service gns3 stop") @@ -256,7 +287,7 @@ class Welcome_dialog: os.system("sudo vmware-toolbox-cmd disk shrink /") self.display.msgbox("The GNS3 VM will reboot") - os.execvp("sudo", ['/usr/bin/sudo', "reboot"]) + os.execvp("sudo", ["/usr/bin/sudo", "reboot"]) def vm_information(self): """ @@ -269,10 +300,11 @@ class Welcome_dialog: if version is None: content += "GNS3 is not installed please install it with sudo pip3 install gns3-server. Or download a preinstalled VM.\n\n" else: - content = "GNS3 version: {gns3_version}\nVM version: {gns3vm_version}\nKVM support available: {kvm}\n\n".format( - gns3vm_version=self.gns3vm_version(), - gns3_version=version, - kvm=self.kvm_support()) + content = ( + "GNS3 version: {gns3_version}\nVM version: {gns3vm_version}\nKVM support available: {kvm}\n\n".format( + gns3vm_version=self.gns3vm_version(), gns3_version=version, kvm=self.kvm_support() + ) + ) ip = self.get_ip() @@ -297,13 +329,12 @@ Images and projects are located in /opt/gns3 self.display.msgbox(content) # If it's an scp command or any bugs except: - os.execvp("bash", ['/bin/bash']) - + os.execvp("bash", ["/bin/bash"]) def check_internet_connectivity(self): self.display.pause("Please wait...\n\n") try: - response = urllib.request.urlopen('http://pypi.python.org/', timeout=5) + response = urllib.request.urlopen("http://pypi.python.org/", timeout=5) except urllib.request.URLError as err: self.display.infobox("Can't connect to Internet (pypi.python.org): {}".format(str(err))) time.sleep(15) @@ -311,14 +342,12 @@ Images and projects are located in /opt/gns3 self.display.infobox("Connection to Internet: OK") time.sleep(2) - def keyboard_configuration(): """ Allow user to change the keyboard layout """ os.system("/usr/bin/sudo dpkg-reconfigure keyboard-configuration") - def set_security(self): config = self.get_config() if self.display.yesno("Enable server authentication?") == self.display.OK: @@ -338,7 +367,6 @@ Images and projects are located in /opt/gns3 self.write_config(config) - def log(self): os.system("/usr/bin/sudo chmod 755 /var/log/upstart/gns3.log") with open("/var/log/upstart/gns3.log") as f: @@ -349,7 +377,6 @@ Images and projects are located in /opt/gns3 except (KeyboardInterrupt, MemoryError): return - def edit_config(self): """ Edit GNS3 configuration file @@ -361,7 +388,6 @@ Images and projects are located in /opt/gns3 else: os.system("nano ~/.config/GNS3/gns3_server.conf") - def edit_network(self): """ Edit network configuration file @@ -369,27 +395,30 @@ Images and projects are located in /opt/gns3 if self.display.yesno("The server will reboot at the end of the process. Continue?") != self.display.OK: return os.system("sudo nano /etc/network/interfaces") - os.execvp("sudo", ['/usr/bin/sudo', "reboot"]) - + os.execvp("sudo", ["/usr/bin/sudo", "reboot"]) def edit_proxy(self): """ Configure proxy settings """ - res, http_proxy = self.display.inputbox(text="HTTP proxy string, for example http://:@:. Leave empty for no proxy.") + res, http_proxy = self.display.inputbox( + text="HTTP proxy string, for example http://:@:. Leave empty for no proxy." + ) if res != self.display.OK: return - res, https_proxy = self.display.inputbox(text="HTTPS proxy string, for example http://:@:. Leave empty for no proxy.") + res, https_proxy = self.display.inputbox( + text="HTTPS proxy string, for example http://:@:. Leave empty for no proxy." + ) if res != self.display.OK: return - with open('/tmp/00proxy', 'w+') as f: + with open("/tmp/00proxy", "w+") as f: f.write('Acquire::http::Proxy "' + http_proxy + '";') os.system("sudo mv /tmp/00proxy /etc/apt/apt.conf.d/00proxy") os.system("sudo chown root /etc/apt/apt.conf.d/00proxy") os.system("sudo chmod 744 /etc/apt/apt.conf.d/00proxy") - with open('/tmp/proxy.sh', 'w+') as f: + with open("/tmp/proxy.sh", "w+") as f: f.write('export http_proxy="' + http_proxy + '"\n') f.write('export https_proxy="' + https_proxy + '"\n') f.write('export HTTP_PROXY="' + http_proxy + '"\n') @@ -400,8 +429,7 @@ Images and projects are located in /opt/gns3 os.system("sudo cp /etc/profile.d/proxy.sh /etc/default/docker") self.display.msgbox("The GNS3 VM will reboot") - os.execvp("sudo", ['/usr/bin/sudo', "reboot"]) - + os.execvp("sudo", ["/usr/bin/sudo", "reboot"]) def kvm_support(self): """ @@ -409,7 +437,6 @@ Images and projects are located in /opt/gns3 """ return subprocess.call("kvm-ok") == 0 - def kvm_control(self): """ Check if KVM is correctly configured @@ -420,40 +447,53 @@ Images and projects are located in /opt/gns3 try: if config.getboolean("Qemu", "enable_kvm") is True: if kvm_ok is False: - if self.display.yesno("KVM is not available!\n\nQemu VM will crash!!\n\nThe reason could be unsupported hardware or another virtualization solution is already running.\n\nDisable KVM and get lower performances?") == self.display.OK: + if ( + self.display.yesno( + "KVM is not available!\n\nQemu VM will crash!!\n\nThe reason could be unsupported hardware or another virtualization solution is already running.\n\nDisable KVM and get lower performances?" + ) + == self.display.OK + ): config.set("Qemu", "enable_kvm", False) self.write_config(config) - os.execvp("sudo", ['/usr/bin/sudo', "reboot"]) + os.execvp("sudo", ["/usr/bin/sudo", "reboot"]) else: if kvm_ok is True: - if self.display.yesno("KVM is available on your computer.\n\nEnable KVM and get better performances?") == self.display.OK: + if ( + self.display.yesno( + "KVM is available on your computer.\n\nEnable KVM and get better performances?" + ) + == self.display.OK + ): config.set("Qemu", "enable_kvm", True) self.write_config(config) - os.execvp("sudo", ['/usr/bin/sudo', "reboot"]) + os.execvp("sudo", ["/usr/bin/sudo", "reboot"]) except configparser.NoSectionError: return - def display_loop(self): try: while True: - code, tag = self.display.menu("GNS3 {}".format(self.gns3_version()), - choices=[("Information", "Display VM information"), - ("Upgrade", "Upgrade GNS3"), - ("Migrate", "Migrate data to another GNS3 VM"), - ("Shell", "Open a console"), - ("Security", "Configure authentication"), - ("Keyboard", "Change keyboard layout"), - ("Configure", "Edit server configuration (advanced users ONLY)"), - ("Proxy", "Configure proxy settings"), - ("Networking", "Configure networking settings"), - ("Log", "Show server log"), - ("Test", "Check internet connection"), - ("Shrink", "Shrink the VM disk"), - ("Version", "Select the GNS3 version"), - ("Restore", "Restore the VM (if you have trouble for upgrade)"), - ("Reboot", "Reboot the VM"), - ("Shutdown", "Shutdown the VM")]) + code, tag = self.display.menu( + "GNS3 {}".format(self.gns3_version()), + choices=[ + ("Information", "Display VM information"), + ("Upgrade", "Upgrade GNS3"), + ("Migrate", "Migrate data to another GNS3 VM"), + ("Shell", "Open a console"), + ("Security", "Configure authentication"), + ("Keyboard", "Change keyboard layout"), + ("Configure", "Edit server configuration (advanced users ONLY)"), + ("Proxy", "Configure proxy settings"), + ("Networking", "Configure networking settings"), + ("Log", "Show server log"), + ("Test", "Check internet connection"), + ("Shrink", "Shrink the VM disk"), + ("Version", "Select the GNS3 version"), + ("Restore", "Restore the VM (if you have trouble for upgrade)"), + ("Reboot", "Reboot the VM"), + ("Shutdown", "Shutdown the VM"), + ], + ) self.display.clear() if code == Dialog.OK: if tag == "Shell": @@ -462,11 +502,11 @@ Images and projects are located in /opt/gns3 elif tag == "Version": self.mode() elif tag == "Restore": - os.execvp("sudo", ['/usr/bin/sudo', "/usr/local/bin/gns3restore"]) + os.execvp("sudo", ["/usr/bin/sudo", "/usr/local/bin/gns3restore"]) elif tag == "Reboot": - os.execvp("sudo", ['/usr/bin/sudo', "reboot"]) + os.execvp("sudo", ["/usr/bin/sudo", "reboot"]) elif tag == "Shutdown": - os.execvp("sudo", ['/usr/bin/sudo', "poweroff"]) + os.execvp("sudo", ["/usr/bin/sudo", "poweroff"]) elif tag == "Upgrade": self.update() elif tag == "Information": @@ -492,6 +532,7 @@ Images and projects are located in /opt/gns3 except KeyboardInterrupt: sys.exit(0) + if __name__ == "__main__": ws = Welcome_dialog() ws.vm_information() diff --git a/tests/agent/mcp/conftest.py b/tests/agent/mcp/conftest.py index 3b1e35952..d286d4dc2 100644 --- a/tests/agent/mcp/conftest.py +++ b/tests/agent/mcp/conftest.py @@ -1,4 +1,5 @@ """MCP test fixtures.""" + import pytest diff --git a/tests/agent/mcp/test_device_tools.py b/tests/agent/mcp/test_device_tools.py index 5c5cd8c63..6a7c04169 100644 --- a/tests/agent/mcp/test_device_tools.py +++ b/tests/agent/mcp/test_device_tools.py @@ -4,6 +4,7 @@ Device config tool tests with mocked topology and Nornir layers. Covers the VPCS node-type guard and the error contract shared by device_config_send / device_show_run / vpcs_config_set. """ + import json import pytest @@ -18,16 +19,18 @@ def _topology_ports(node_type): class TestVPCSNodeTypeGuard: - def test_non_vpcs_node_is_rejected(self): from gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko import VPCSCommands - with patch(f"{VPCS_MOD}.get_device_ports_from_topology", - return_value=_topology_ports("iou")) as topo: - result = VPCSCommands()._run(json.dumps({ - "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", - "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], - })) + with patch(f"{VPCS_MOD}.get_device_ports_from_topology", return_value=_topology_ports("iou")) as topo: + result = VPCSCommands()._run( + json.dumps( + { + "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", + "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], + } + ) + ) assert topo.called assert len(result) == 1 assert result[0]["device_name"] == "PC1" @@ -37,12 +40,15 @@ class TestVPCSNodeTypeGuard: def test_missing_node_type_is_rejected(self): from gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko import VPCSCommands - with patch(f"{VPCS_MOD}.get_device_ports_from_topology", - return_value={"PC1": {"port": 5000}}): - result = VPCSCommands()._run(json.dumps({ - "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", - "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], - })) + with patch(f"{VPCS_MOD}.get_device_ports_from_topology", return_value={"PC1": {"port": 5000}}): + result = VPCSCommands()._run( + json.dumps( + { + "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", + "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], + } + ) + ) assert result[0]["status"] == "failed" assert "unknown-type" in result[0]["error"] @@ -54,13 +60,18 @@ class TestVPCSNodeTypeGuard: host_result = MagicMock(failed=False) host_result.result = "OK" nornir.run.return_value = {"PC1": host_result} - with patch(f"{VPCS_MOD}.get_device_ports_from_topology", - return_value=_topology_ports("vpcs")), \ - patch.object(VPCSCommands, "_initialize_nornir", return_value=nornir): - result = tool._run(json.dumps({ - "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", - "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], - })) + with ( + patch(f"{VPCS_MOD}.get_device_ports_from_topology", return_value=_topology_ports("vpcs")), + patch.object(VPCSCommands, "_initialize_nornir", return_value=nornir), + ): + result = tool._run( + json.dumps( + { + "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", + "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], + } + ) + ) assert result[0]["status"] == "success" assert result[0]["output"] == "OK" @@ -72,13 +83,18 @@ class TestVPCSNodeTypeGuard: host_result = MagicMock(failed=True) host_result.result = "Command failed (ReadTimeout)" nornir.run.return_value = {"PC1": host_result} - with patch(f"{VPCS_MOD}.get_device_ports_from_topology", - return_value=_topology_ports("vpcs")), \ - patch.object(VPCSCommands, "_initialize_nornir", return_value=nornir): - result = tool._run(json.dumps({ - "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", - "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], - })) + with ( + patch(f"{VPCS_MOD}.get_device_ports_from_topology", return_value=_topology_ports("vpcs")), + patch.object(VPCSCommands, "_initialize_nornir", return_value=nornir), + ): + result = tool._run( + json.dumps( + { + "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", + "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], + } + ) + ) assert result[0]["status"] == "failed" assert result[0]["error"] == "Command failed (ReadTimeout)" assert "output" not in result[0] @@ -93,12 +109,15 @@ class TestDeviceToolErrorContract: def test_topology_level_error_has_status(self): from gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko import VPCSCommands - with patch(f"{VPCS_MOD}.get_device_ports_from_topology", - side_effect=ValueError("topology unreachable")): - result = VPCSCommands()._run(json.dumps({ - "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", - "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], - })) + with patch(f"{VPCS_MOD}.get_device_ports_from_topology", side_effect=ValueError("topology unreachable")): + result = VPCSCommands()._run( + json.dumps( + { + "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", + "device_configs": [{"device_name": "PC1", "commands": ["ip 10.0.0.1/24"]}], + } + ) + ) assert result == [{"status": "failed", "error": "topology unreachable"}] def test_config_tool_topology_level_error_has_status(self): @@ -106,13 +125,18 @@ class TestDeviceToolErrorContract: ExecuteMultipleDeviceConfigCommands, ) - with patch("gns3server.agent.gns3_copilot.tools_v2.config_tools_nornir" - ".get_device_ports_from_topology", - side_effect=ValueError("no valid devices")): - result = ExecuteMultipleDeviceConfigCommands()._run(json.dumps({ - "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", - "device_configs": [{"device_name": "R1", "config_commands": ["int lo0"]}], - })) + with patch( + "gns3server.agent.gns3_copilot.tools_v2.config_tools_nornir.get_device_ports_from_topology", + side_effect=ValueError("no valid devices"), + ): + result = ExecuteMultipleDeviceConfigCommands()._run( + json.dumps( + { + "project_id": "0c0fde25-6ead-4413-a283-ea8fd2324291", + "device_configs": [{"device_name": "R1", "config_commands": ["int lo0"]}], + } + ) + ) assert result == [{"status": "failed", "error": "no valid devices"}] def test_mcp_handler_param_error_has_status(self, ctx=None): @@ -124,10 +148,12 @@ class TestDeviceToolErrorContract: for handler in (device_config_send_handler, device_show_run_handler, vpcs_config_set_handler): result = handler({}, {"server_url": "http://x", "jwt_token": "t"}) - assert result == [{ - "status": "failed", - "error": result[0]["error"], # message text may differ per handler - }] + assert result == [ + { + "status": "failed", + "error": result[0]["error"], # message text may differ per handler + } + ] assert "required" in result[0]["error"] def test_template_render_error_has_status(self): diff --git a/tests/agent/mcp/test_handlers.py b/tests/agent/mcp/test_handlers.py index 87501bf30..d98f754b0 100644 --- a/tests/agent/mcp/test_handlers.py +++ b/tests/agent/mcp/test_handlers.py @@ -3,6 +3,7 @@ MCP handler unit tests with mocked Gns3Connector. Tests that handlers correctly transform tool parameters into HTTP calls. """ + import json import pytest @@ -31,11 +32,11 @@ def ctx(): class TestProject: - mod = "projects" def test_list(self, ctx): from gns3server.agent.mcp.projects import list_projects_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn([{"project_id": "p1", "name": "Test", "status": "opened"}]) result = list_projects_handler({}, ctx) @@ -43,6 +44,7 @@ class TestProject: def test_get(self, ctx): from gns3server.agent.mcp.projects import get_project_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"project_id": "p1"}) result = get_project_handler({"project_id": "p1"}, ctx) @@ -50,10 +52,12 @@ class TestProject: def test_get_missing_id(self, ctx): from gns3server.agent.mcp.projects import get_project_handler + assert "error" in get_project_handler({}, ctx) def test_create(self, ctx): from gns3server.agent.mcp.projects import create_project_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"project_id": "p1"}) m.return_value = conn @@ -65,16 +69,16 @@ class TestProject: def test_create_without_auto_close(self, ctx): from gns3server.agent.mcp.projects import create_project_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"project_id": "p2"}) m.return_value = conn create_project_handler({"name": "New"}, ctx) - conn.http_call.assert_called_once_with( - "post", f"{conn.base_url}/projects", json_data={"name": "New"} - ) + conn.http_call.assert_called_once_with("post", f"{conn.base_url}/projects", json_data={"name": "New"}) def test_delete(self, ctx): from gns3server.agent.mcp.projects import delete_project_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({}) result = delete_project_handler({"project_id": "p1"}, ctx) @@ -82,6 +86,7 @@ class TestProject: def test_open(self, ctx): from gns3server.agent.mcp.projects import open_project_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"status": "opened"}) result = open_project_handler({"project_id": "p1"}, ctx) @@ -89,6 +94,7 @@ class TestProject: def test_close(self, ctx): from gns3server.agent.mcp.projects import close_project_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"status": "closed"}) result = close_project_handler({"project_id": "p1"}, ctx) @@ -96,6 +102,7 @@ class TestProject: def test_update(self, ctx): from gns3server.agent.mcp.projects import update_project_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"name": "Updated"}) result = update_project_handler({"project_id": "p1", "name": "Updated"}, ctx) @@ -103,6 +110,7 @@ class TestProject: def test_stats(self, ctx): from gns3server.agent.mcp.projects import get_project_stats_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"nodes": 5, "links": 3}) result = get_project_stats_handler({"project_id": "p1"}, ctx) @@ -113,19 +121,21 @@ class TestProject: class TestNode: - - def test_list_fields(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_nodes_handler + with patch(f"{AH}._get_connector") as m: - m.return_value = _mock_conn([ - {"node_id": "n1", "name": "R1", "status": "started", "node_type": "qemu", "console": 5000}, - ]) + m.return_value = _mock_conn( + [ + {"node_id": "n1", "name": "R1", "status": "started", "node_type": "qemu", "console": 5000}, + ] + ) result = get_nodes_handler({"project_id": "p1", "fields": ["name", "status"]}, ctx) assert result == {"nodes": [{"name": "R1", "status": "started"}], "count": 1} def test_list_invalid_fields(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_nodes_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn([]) result = get_nodes_handler({"project_id": "p1", "fields": "not-a-list"}, ctx) @@ -133,6 +143,7 @@ class TestNode: def test_get(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"node_id": "n1", "name": "R1"}) result = get_node_handler({"project_id": "p1", "node_id": "n1"}, ctx) @@ -140,68 +151,99 @@ class TestNode: def test_create_single_passes_name(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"node_id": "n1", "name": "MyRouter"}) m.return_value = conn - result = create_node_handler({ - "project_id": "p1", "template_id": "t1", - "name": "MyRouter", "x": 100, "y": 200, - }, ctx) + result = create_node_handler( + { + "project_id": "p1", + "template_id": "t1", + "name": "MyRouter", + "x": 100, + "y": 200, + }, + ctx, + ) conn.http_call.assert_called_with( - "post", "http://192.168.1.3:3080/v3/projects/p1/templates/t1", + "post", + "http://192.168.1.3:3080/v3/projects/p1/templates/t1", json_data={"x": 100, "y": 200, "compute_id": "local", "name": "MyRouter"}, ) assert result == {"node_id": "n1", "name": "MyRouter"} def test_create_fields_filter(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"node_id": "n1", "name": "R1", "status": "started"}) - result = create_node_handler({ - "project_id": "p1", "template_id": "t1", - "fields": ["node_id", "name"], - }, ctx) + result = create_node_handler( + { + "project_id": "p1", + "template_id": "t1", + "fields": ["node_id", "name"], + }, + ctx, + ) assert result == {"node_id": "n1", "name": "R1"} def test_create_fields_validation(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn() m.return_value = conn - result = create_node_handler({ - "project_id": "p1", "template_id": "t1", "fields": "not-a-list", - }, ctx) + result = create_node_handler( + { + "project_id": "p1", + "template_id": "t1", + "fields": "not-a-list", + }, + ctx, + ) assert "error" in result assert "fields must be a list" in result["error"] conn.http_call.assert_not_called() def test_create_batch_inherits_template_id(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"node_id": "n1", "name": "R1"}) - result = create_node_handler({ - "project_id": "p1", "template_id": "default-tpl", - "nodes": [{"name": "R1", "x": 0, "y": 0}], - }, ctx) + result = create_node_handler( + { + "project_id": "p1", + "template_id": "default-tpl", + "nodes": [{"name": "R1", "x": 0, "y": 0}], + }, + ctx, + ) assert result[0]["status"] == "success" def test_create_batch_preserves_submission_order(self, ctx): import time from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn() + def _http_call(method, url, json_data=None, **kwargs): # first submissions sleep longest so completion order is reversed time.sleep({"slow": 0.25, "mid": 0.1}.get(json_data.get("name"), 0.0)) resp = MagicMock() resp.json.return_value = {"node_id": "n1", "name": json_data["name"]} return resp + conn.http_call.side_effect = _http_call m.return_value = conn - result = create_node_handler({ - "project_id": "p1", "template_id": "t1", - "nodes": [{"name": "slow"}, {"name": "mid"}, {"name": "fast"}], - }, ctx) + result = create_node_handler( + { + "project_id": "p1", + "template_id": "t1", + "nodes": [{"name": "slow"}, {"name": "mid"}, {"name": "fast"}], + }, + ctx, + ) assert [r["node"]["name"] for r in result] == ["slow", "mid", "fast"] def test_create_batch_default_names_created_sequentially(self, ctx): @@ -246,10 +288,12 @@ class TestNode: def test_create_missing_project_id(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_node_handler + assert create_node_handler({}, ctx) == {"error": "project_id is required"} def test_delete_batch(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import delete_node_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({}) result = delete_node_handler({"project_id": "p1", "node_ids": ["n1", "n2"]}, ctx) @@ -259,6 +303,7 @@ class TestNode: def test_start_batch(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import start_node_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"status": "started"}) result = start_node_handler({"project_id": "p1", "node_ids": ["n1"]}, ctx) @@ -266,6 +311,7 @@ class TestNode: def test_stop_batch(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import stop_node_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"status": "stopped"}) result = stop_node_handler({"project_id": "p1", "node_ids": ["n1"]}, ctx) @@ -273,6 +319,7 @@ class TestNode: def test_suspend_batch(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import suspend_node_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"status": "suspended"}) result = suspend_node_handler({"project_id": "p1", "node_ids": ["n1"]}, ctx) @@ -281,6 +328,7 @@ class TestNode: def test_console(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_console_info_handler from gns3server.services import access_ticket_service + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"console_url": "ws://host/console"}) result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, ctx) @@ -297,6 +345,7 @@ class TestNode: def test_console_vnc_uses_same_ticket(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_console_info_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"console_type": "vnc"}) result = get_node_console_info_handler({"project_id": "p1", "node_id": "n1"}, ctx) @@ -306,6 +355,7 @@ class TestNode: def test_console_without_username_omits_token(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_console_info_handler + unauthenticated_ctx = {k: v for k, v in ctx.items() if k != "jwt_username"} with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"console_url": "ws://host/console"}) @@ -321,6 +371,7 @@ class TestNode: def test_file_get_keeps_trailing_newline(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler + with patch(f"{AH}._get_connector") as m: m.return_value = self._file_conn("line1\nline2\n") result = get_node_file_handler({"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg"}, ctx) @@ -331,6 +382,7 @@ class TestNode: def test_file_get_keeps_crlf_endings(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler + with patch(f"{AH}._get_connector") as m: m.return_value = self._file_conn("line1\r\nline2\r\n") result = get_node_file_handler({"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg"}, ctx) @@ -339,6 +391,7 @@ class TestNode: def test_file_get_without_trailing_newline(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler + with patch(f"{AH}._get_connector") as m: m.return_value = self._file_conn("line1\nline2") result = get_node_file_handler({"project_id": "p1", "node_id": "n1", "file_path": "startup.cfg"}, ctx) @@ -346,6 +399,7 @@ class TestNode: def test_file_get_pagination(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_node_file_handler + with patch(f"{AH}._get_connector") as m: m.return_value = self._file_conn("line1\nline2\nline3\n") result = get_node_file_handler( @@ -361,10 +415,9 @@ class TestNode: class TestLink: - - def test_list(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_links_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn([{"link_id": "l1", "link_type": "ethernet"}]) result = get_links_handler({"project_id": "p1", "fields": ["link_id"]}, ctx) @@ -372,6 +425,7 @@ class TestLink: def test_get(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_link_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"link_id": "l1", "link_type": "ethernet"}) result = get_link_handler({"project_id": "p1", "link_id": "l1"}, ctx) @@ -379,49 +433,66 @@ class TestLink: def test_create_compact_format(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_link_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"link_id": "l1", "link_type": "ethernet", "nodes": []}) m.return_value = conn - result = create_link_handler({ - "project_id": "p1", - "nodes": ["n1", 0, 0, "n2", 0, 0], - }, ctx) + result = create_link_handler( + { + "project_id": "p1", + "nodes": ["n1", 0, 0, "n2", 0, 0], + }, + ctx, + ) conn.http_call.assert_called_with( - "post", "http://192.168.1.3:3080/v3/projects/p1/links", - json_data={"nodes": [ - {"node_id": "n1", "adapter_number": 0, "port_number": 0}, - {"node_id": "n2", "adapter_number": 0, "port_number": 0}, - ]}, + "post", + "http://192.168.1.3:3080/v3/projects/p1/links", + json_data={ + "nodes": [ + {"node_id": "n1", "adapter_number": 0, "port_number": 0}, + {"node_id": "n2", "adapter_number": 0, "port_number": 0}, + ] + }, ) def test_create_standard_format(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_link_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"link_id": "l1"}) - result = create_link_handler({ - "project_id": "p1", - "nodes": [ - {"node_id": "n1", "adapter_number": 0, "port_number": 0}, - {"node_id": "n2", "adapter_number": 0, "port_number": 0}, - ], - }, ctx) + result = create_link_handler( + { + "project_id": "p1", + "nodes": [ + {"node_id": "n1", "adapter_number": 0, "port_number": 0}, + {"node_id": "n2", "adapter_number": 0, "port_number": 0}, + ], + }, + ctx, + ) assert result["link_id"] == "l1" def test_create_fields_validation(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_link_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn() m.return_value = conn - result = create_link_handler({ - "project_id": "p1", "fields": "bad", - "nodes": ["n1", 0, 0, "n2", 0, 0], - }, ctx) + result = create_link_handler( + { + "project_id": "p1", + "fields": "bad", + "nodes": ["n1", 0, 0, "n2", 0, 0], + }, + ctx, + ) assert "error" in result assert "fields must be a list" in result["error"] conn.http_call.assert_not_called() def test_delete_batch(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import delete_link_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({}) result = delete_link_handler({"project_id": "p1", "link_ids": ["l1", "l2"]}, ctx) @@ -432,8 +503,10 @@ class TestLink: def test_create_batch_preserves_submission_order(self, ctx): import time from gns3server.agent.gns3_copilot.gns3_client.api_handlers import create_link_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn() + def _http_call(method, url, json_data=None, **kwargs): # first submission sleeps longest so completion order is reversed first_node = json_data["nodes"][0]["node_id"] @@ -441,30 +514,41 @@ class TestLink: resp = MagicMock() resp.json.return_value = {"link_id": f"link-{first_node}"} return resp + conn.http_call.side_effect = _http_call m.return_value = conn - result = create_link_handler({ - "project_id": "p1", - "links": [ - {"nodes": ["n1", 0, 0, "n2", 0, 0]}, - {"nodes": ["n3", 0, 0, "n4", 0, 0]}, - ], - "fields": ["link_id"], - }, ctx) + result = create_link_handler( + { + "project_id": "p1", + "links": [ + {"nodes": ["n1", 0, 0, "n2", 0, 0]}, + {"nodes": ["n3", 0, 0, "n4", 0, 0]}, + ], + "fields": ["link_id"], + }, + ctx, + ) assert [r["link"]["link_id"] for r in result] == ["link-n1", "link-n3"] def test_update(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import update_link_handler + with patch(f"{AH}._get_connector") as m: m.return_value = _mock_conn({"link_id": "l1", "suspend": True}) - result = update_link_handler({ - "project_id": "p1", "link_id": "l1", "suspend": True, - }, ctx) + result = update_link_handler( + { + "project_id": "p1", + "link_id": "l1", + "suspend": True, + }, + ctx, + ) assert result["suspend"] is True def test_download_capture_file(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import download_capture_file_handler from gns3server.services import access_ticket_service + result = download_capture_file_handler({"project_id": "p1", "link_id": "l1"}, ctx) # a short path-bound ticket replaces the long Bearer JWT in the URL assert "?token=gns3t_" in result["download_url"] @@ -478,6 +562,7 @@ class TestLink: def test_download_capture_file_batch(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import download_capture_file_handler from gns3server.services import access_ticket_service + result = download_capture_file_handler({"project_id": "p1", "link_ids": ["l1", "l2"]}, ctx) assert result["count"] == 2 # each link gets its own ticket bound to its own download path @@ -492,12 +577,12 @@ class TestLink: class TestSymbol: - mod = "symbols" def test_get_symbol_download(self, ctx): from gns3server.agent.mcp.symbols import get_symbol_handler from gns3server.services import access_ticket_service + result = get_symbol_handler({"symbol_id": "router.svg"}, ctx) assert "?token=gns3t_" in result["download_url"] assert "Bearer" not in result["curl_command"] @@ -507,6 +592,7 @@ class TestSymbol: def test_get_symbol_without_username_omits_token(self, ctx): from gns3server.agent.mcp.symbols import get_symbol_handler + unauthenticated_ctx = {k: v for k, v in ctx.items() if k != "jwt_username"} result = get_symbol_handler({"symbol_id": "router.svg"}, unauthenticated_ctx) assert "token=" not in result["download_url"] @@ -517,11 +603,11 @@ class TestSymbol: class TestAppliance: - mod = "appliances" def test_get(self, ctx): from gns3server.agent.mcp.appliances import get_appliance_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"appliance_id": "a1", "name": "Cisco ISE"}) result = get_appliance_handler({"appliance_id": "a1"}, ctx) @@ -529,24 +615,34 @@ class TestAppliance: def test_install_with_version(self, ctx): from gns3server.agent.mcp.appliances import install_appliance_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"template_id": "t1", "name": "FRR", "version": "8.2.2", "template_type": "docker"}) m.return_value = conn - result = install_appliance_handler({ - "appliance_id": "a1", "version": "2.7.0.356", - }, ctx) + result = install_appliance_handler( + { + "appliance_id": "a1", + "version": "2.7.0.356", + }, + ctx, + ) conn.http_call.assert_called_with( - "post", "http://192.168.1.3:3080/v3/appliances/a1/install", + "post", + "http://192.168.1.3:3080/v3/appliances/a1/install", params={"version": "2.7.0.356"}, ) assert result["template"] == { - "template_id": "t1", "name": "FRR", "version": "8.2.2", "template_type": "docker", + "template_id": "t1", + "name": "FRR", + "version": "8.2.2", + "template_type": "docker", } def test_install_empty_body(self, ctx): # a 204-style empty response must not blow up with a JSON decode error # (the template is still created server-side) from gns3server.agent.mcp.appliances import install_appliance_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn() conn.http_call.return_value.content = b"" @@ -558,6 +654,7 @@ class TestAppliance: def test_install_missing_id(self, ctx): from gns3server.agent.mcp.appliances import install_appliance_handler + result = install_appliance_handler({}, ctx) assert "error" in result @@ -566,21 +663,29 @@ class TestAppliance: class TestTemplate: - mod = "templates" def test_list_fields(self, ctx): from gns3server.agent.mcp.templates import list_templates_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: - m.return_value = _mock_conn([ - {"template_id": "t1", "name": "Cisco 7200", "template_type": "dynamips", - "category": "router", "default_name_format": "{name}-{0}"}, - ]) + m.return_value = _mock_conn( + [ + { + "template_id": "t1", + "name": "Cisco 7200", + "template_type": "dynamips", + "category": "router", + "default_name_format": "{name}-{0}", + }, + ] + ) result = list_templates_handler({"fields": ["template_id", "name"]}, ctx) assert result["templates"] == [{"template_id": "t1", "name": "Cisco 7200"}] def test_list_invalid_field(self, ctx): from gns3server.agent.mcp.templates import list_templates_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn() result = list_templates_handler({"fields": ["does_not_exist"]}, ctx) @@ -588,6 +693,7 @@ class TestTemplate: def test_get(self, ctx): from gns3server.agent.mcp.templates import get_template_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({"template_id": "t1", "name": "Test"}) result = get_template_handler({"template_id": "t1"}, ctx) @@ -595,6 +701,7 @@ class TestTemplate: def test_delete(self, ctx): from gns3server.agent.mcp.templates import delete_template_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: m.return_value = _mock_conn({}) result = delete_template_handler({"template_id": "t1"}, ctx) @@ -605,16 +712,18 @@ class TestTemplate: class TestImage: - mod = "images" def test_install_manifest(self, ctx): from gns3server.agent.mcp.images import install_images_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: - conn = _mock_conn({ - "created": [{"template_id": "t1", "name": "Empty VM", "version": "100G", "template_type": "qemu"}], - "skipped": [{"name": "csr1000v.qcow2", "reason": "image is already used by one or more templates"}], - }) + conn = _mock_conn( + { + "created": [{"template_id": "t1", "name": "Empty VM", "version": "100G", "template_type": "qemu"}], + "skipped": [{"name": "csr1000v.qcow2", "reason": "image is already used by one or more templates"}], + } + ) m.return_value = conn result = install_images_handler({}, ctx) assert result["created"][0]["name"] == "Empty VM" @@ -622,6 +731,7 @@ class TestImage: def test_install_empty_body(self, ctx): from gns3server.agent.mcp.images import install_images_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn() conn.http_call.return_value.content = b"" @@ -632,11 +742,11 @@ class TestImage: class TestCompute: - mod = "computes" def test_get_local_by_default(self, ctx): from gns3server.agent.mcp.computes import get_compute_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"compute_id": "local", "name": "local"}) m.return_value = conn @@ -647,6 +757,7 @@ class TestCompute: def test_get_explicit_compute_id(self, ctx): from gns3server.agent.mcp.computes import get_compute_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn({"compute_id": "4fcfb6b5-5b0b-4f43-bd5e-e8ae2a69c8e6"}) m.return_value = conn @@ -656,6 +767,7 @@ class TestCompute: def test_images_local_by_default(self, ctx): from gns3server.agent.mcp.computes import get_compute_images_handler + with patch(f"{BASE}.{self.mod}._get_connector") as m: conn = _mock_conn(["img1.qcow2"]) m.return_value = conn @@ -666,6 +778,7 @@ class TestCompute: def test_images_requires_emulator(self, ctx): from gns3server.agent.mcp.computes import get_compute_images_handler + assert "error" in get_compute_images_handler({}, ctx) @@ -675,101 +788,120 @@ class TestCompute: class TestLinkMarker: """link_marker_handler direction tri-state: omit=preserve, tx/rx=set, both=clear (→ null).""" - def test_update_direction_both_clears(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn link_marker_handler( - {"project_id": "p", "link_id": "l", "action": "update", - "marker_name": "icmp", "direction": "both"}, ctx, + {"project_id": "p", "link_id": "l", "action": "update", "marker_name": "icmp", "direction": "both"}, + ctx, ) conn.http_call.assert_called_with( - "put", "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp", + "put", + "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp", json_data={"direction": None}, ) def test_update_direction_tx_sets(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn link_marker_handler( - {"project_id": "p", "link_id": "l", "action": "update", - "marker_name": "icmp", "direction": "tx"}, ctx, + {"project_id": "p", "link_id": "l", "action": "update", "marker_name": "icmp", "direction": "tx"}, + ctx, ) conn.http_call.assert_called_with( - "put", "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp", + "put", + "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp", json_data={"direction": "tx"}, ) def test_update_direction_omitted_preserved(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn link_marker_handler( - {"project_id": "p", "link_id": "l", "action": "update", - "marker_name": "icmp", "tag": 1}, ctx, + {"project_id": "p", "link_id": "l", "action": "update", "marker_name": "icmp", "tag": 1}, + ctx, ) conn.http_call.assert_called_with( - "put", "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp", + "put", + "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp", json_data={"tag": 1}, ) def test_create_direction_both_omitted(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn link_marker_handler( - {"project_id": "p", "link_id": "l", "action": "create", - "bpf": "icmp", "direction": "both"}, ctx, + {"project_id": "p", "link_id": "l", "action": "create", "bpf": "icmp", "direction": "both"}, + ctx, ) conn.http_call.assert_called_with( - "post", "http://192.168.1.3:3080/v3/projects/p/links/l/markers", + "post", + "http://192.168.1.3:3080/v3/projects/p/links/l/markers", json_data={"bpf": "icmp"}, ) def test_create_data_link_type_passthrough(self, ctx): """create passes a serial WAN encapsulation through; update ignores it.""" from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn link_marker_handler( - {"project_id": "p", "link_id": "l", "action": "create", - "bpf": "icmp", "data_link_type": "DLT_C_HDLC"}, ctx, + {"project_id": "p", "link_id": "l", "action": "create", "bpf": "icmp", "data_link_type": "DLT_C_HDLC"}, + ctx, ) conn.http_call.assert_called_with( - "post", "http://192.168.1.3:3080/v3/projects/p/links/l/markers", + "post", + "http://192.168.1.3:3080/v3/projects/p/links/l/markers", json_data={"bpf": "icmp", "data_link_type": "DLT_C_HDLC"}, ) conn = _mock_conn({"name": "icmp"}) m.return_value = conn link_marker_handler( - {"project_id": "p", "link_id": "l", "action": "update", - "marker_name": "icmp", "tag": 1, "data_link_type": "DLT_PPP_SERIAL"}, ctx, + { + "project_id": "p", + "link_id": "l", + "action": "update", + "marker_name": "icmp", + "tag": 1, + "data_link_type": "DLT_PPP_SERIAL", + }, + ctx, ) # create-only: dropped from the update body conn.http_call.assert_called_with( - "put", "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp", + "put", + "http://192.168.1.3:3080/v3/projects/p/links/l/markers/icmp", json_data={"tag": 1}, ) def test_create_direction_tx(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import link_marker_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "icmp"}) m.return_value = conn link_marker_handler( - {"project_id": "p", "link_id": "l", "action": "create", - "bpf": "icmp", "direction": "tx"}, ctx, + {"project_id": "p", "link_id": "l", "action": "create", "bpf": "icmp", "direction": "tx"}, + ctx, ) conn.http_call.assert_called_with( - "post", "http://192.168.1.3:3080/v3/projects/p/links/l/markers", + "post", + "http://192.168.1.3:3080/v3/projects/p/links/l/markers", json_data={"bpf": "icmp", "direction": "tx"}, ) @@ -782,67 +914,76 @@ class TestMarkerDefinition: meaning — any direction passed is ignored, never reaching the request body. """ - def test_create_builds_body(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "arp"}) m.return_value = conn marker_definition_handler( - {"project_id": "p", "action": "create", - "bpf": "arp", "tag": 1, "color": "#fff"}, ctx, + {"project_id": "p", "action": "create", "bpf": "arp", "tag": 1, "color": "#fff"}, + ctx, ) conn.http_call.assert_called_with( - "post", "http://192.168.1.3:3080/v3/projects/p/marker-definitions", + "post", + "http://192.168.1.3:3080/v3/projects/p/marker-definitions", json_data={"bpf": "arp", "tag": 1, "color": "#fff"}, ) def test_create_ignores_direction(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "arp"}) m.return_value = conn marker_definition_handler( - {"project_id": "p", "action": "create", - "bpf": "arp", "direction": "tx"}, ctx, + {"project_id": "p", "action": "create", "bpf": "arp", "direction": "tx"}, + ctx, ) conn.http_call.assert_called_with( - "post", "http://192.168.1.3:3080/v3/projects/p/marker-definitions", + "post", + "http://192.168.1.3:3080/v3/projects/p/marker-definitions", json_data={"bpf": "arp"}, ) def test_update_builds_body(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "arp"}) m.return_value = conn marker_definition_handler( - {"project_id": "p", "action": "update", - "def_name": "arp", "tag": 1}, ctx, + {"project_id": "p", "action": "update", "def_name": "arp", "tag": 1}, + ctx, ) conn.http_call.assert_called_with( - "put", "http://192.168.1.3:3080/v3/projects/p/marker-definitions/arp", + "put", + "http://192.168.1.3:3080/v3/projects/p/marker-definitions/arp", json_data={"tag": 1}, ) def test_update_ignores_direction(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler + with patch(f"{AH}._get_connector") as m: conn = _mock_conn({"name": "arp"}) m.return_value = conn marker_definition_handler( - {"project_id": "p", "action": "update", - "def_name": "arp", "tag": 1, "direction": "rx"}, ctx, + {"project_id": "p", "action": "update", "def_name": "arp", "tag": 1, "direction": "rx"}, + ctx, ) conn.http_call.assert_called_with( - "put", "http://192.168.1.3:3080/v3/projects/p/marker-definitions/arp", + "put", + "http://192.168.1.3:3080/v3/projects/p/marker-definitions/arp", json_data={"tag": 1}, ) def test_update_requires_a_field(self, ctx): from gns3server.agent.gns3_copilot.gns3_client.api_handlers import marker_definition_handler + with patch(f"{AH}._get_connector"): result = marker_definition_handler( - {"project_id": "p", "action": "update", "def_name": "arp"}, ctx, + {"project_id": "p", "action": "update", "def_name": "arp"}, + ctx, ) assert "error" in result diff --git a/tests/agent/mcp/test_tool_params.py b/tests/agent/mcp/test_tool_params.py index 06f4ae173..258436462 100644 --- a/tests/agent/mcp/test_tool_params.py +++ b/tests/agent/mcp/test_tool_params.py @@ -8,6 +8,7 @@ This catches issues like: - A tool parameter is defined but never passed to the handler - A handler reads a param that was never defined or passed """ + import ast import os import sys @@ -112,7 +113,7 @@ def _get_handler_params(handler_name): filename = HANDLER_FILES.get(handler_name) if not filename: return None - filepath = (MCP_DIR if not filename.startswith('gns3server/') else REPO_ROOT) / filename + filepath = (MCP_DIR if not filename.startswith("gns3server/") else REPO_ROOT) / filename if not filepath.exists(): return None @@ -128,10 +129,13 @@ def _get_handler_params(handler_name): # request body from all params) accepts any key — return a wildcard and # let the caller skip static consistency checks for it. for sub in ast.walk(node): - if (isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) - and sub.func.attr == "items" - and isinstance(sub.func.value, ast.Name) - and sub.func.value.id in ("params", "params_data")): + if ( + isinstance(sub, ast.Call) + and isinstance(sub.func, ast.Attribute) + and sub.func.attr == "items" + and isinstance(sub.func.value, ast.Name) + and sub.func.value.id in ("params", "params_data") + ): return {"*"} # Found the handler function, search for params.get("xxx") for sub in ast.walk(node): @@ -141,8 +145,10 @@ def _get_handler_params(handler_name): continue # params.get("xxx") or params_data.get("xxx") func_obj = sub.func - if (hasattr(func_obj.value, "id") and func_obj.value.id in ("params", "params_data", "link_data", "node_data")) or \ - (hasattr(func_obj.value, "attr") and func_obj.value.attr == "get"): + if ( + hasattr(func_obj.value, "id") + and func_obj.value.id in ("params", "params_data", "link_data", "node_data") + ) or (hasattr(func_obj.value, "attr") and func_obj.value.attr == "get"): if sub.args and isinstance(sub.args[0], ast.Constant) and isinstance(sub.args[0].value, str): params.add(sub.args[0].value) return params @@ -180,8 +186,12 @@ def _initial_params_keys(fn_node): """ for stmt in ast.walk(fn_node): if isinstance(stmt, ast.Assign): - if (len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name) - and stmt.targets[0].id == "params" and isinstance(stmt.value, ast.Dict)): + if ( + len(stmt.targets) == 1 + and isinstance(stmt.targets[0], ast.Name) + and stmt.targets[0].id == "params" + and isinstance(stmt.value, ast.Dict) + ): return _dict_literal_keys(stmt.value) return None @@ -196,8 +206,13 @@ def _dispatch_args(node): """ if isinstance(node.func, ast.Name) and node.func.id == "_run_handler_sync": args = node.args - elif (isinstance(node.func, ast.Attribute) and node.func.attr == "to_thread" - and node.args and isinstance(node.args[0], ast.Name) and node.args[0].id == "_run_handler_sync"): + elif ( + isinstance(node.func, ast.Attribute) + and node.func.attr == "to_thread" + and node.args + and isinstance(node.args[0], ast.Name) + and node.args[0].id == "_run_handler_sync" + ): args = node.args[1:] else: return None, None @@ -238,8 +253,14 @@ def test_tool_handler_param_consistency(): exact = True elif isinstance(second_arg, ast.Name) and second_arg.id == "params": # Tool builds 'params' as a variable — resolve its initial dict literal. - fn = next((n for n in ast.walk(tree) - if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == tool_name), None) + fn = next( + ( + n + for n in ast.walk(tree) + if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == tool_name + ), + None, + ) passed_keys = _initial_params_keys(fn) if fn is not None else None else: passed_keys = None @@ -259,9 +280,7 @@ def test_tool_handler_param_consistency(): # Check: every passed key is read by the handler extra_passed = passed_keys - handler_params - assert not extra_passed, ( - f"[{tool_name}] Params passed to handler '{handler_name}' but not read: {extra_passed}" - ) + assert not extra_passed, f"[{tool_name}] Params passed to handler '{handler_name}' but not read: {extra_passed}" if not group["exact"]: # The initial literal underestimates what the tool passes (keys may be @@ -271,11 +290,21 @@ def test_tool_handler_param_consistency(): # Check: every handler param is passed (except common/optional ones) missing = handler_params - passed_keys # Filter out well-known optional params that handlers check - known_optional = {"fields", "template", "name", "version", "compute_id", - "x", "y", "link_type", "filters", "suspend", "link_style", - "show_filters_icon", "label"} + known_optional = { + "fields", + "template", + "name", + "version", + "compute_id", + "x", + "y", + "link_type", + "filters", + "suspend", + "link_style", + "show_filters_icon", + "label", + } truly_missing = missing - known_optional if truly_missing: - pytest.fail( - f"[{tool_name}] Handler '{handler_name}' reads params not passed: {truly_missing}" - ) + pytest.fail(f"[{tool_name}] Handler '{handler_name}' reads params not passed: {truly_missing}") diff --git a/tests/api/routes/compute/test_capabilities.py b/tests/api/routes/compute/test_capabilities.py index 071c629fa..09d4186eb 100644 --- a/tests/api/routes/compute/test_capabilities.py +++ b/tests/api/routes/compute/test_capabilities.py @@ -30,28 +30,56 @@ pytestmark = pytest.mark.asyncio class TestCapabilitiesRoutes: - async def test_get(self, app: FastAPI, compute_client: AsyncClient, windows_platform) -> None: response = await compute_client.get(app.url_path_for("compute:get_capabilities")) assert response.status_code == status.HTTP_200_OK - assert response.json() == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'docker', 'iou'], - 'version': __version__, - 'platform': sys.platform, - 'cpus': psutil.cpu_count(logical=True), - 'memory': psutil.virtual_memory().total, - 'disk_size': psutil.disk_usage(get_default_project_directory()).total, - } - + assert response.json() == { + "node_types": [ + "cloud", + "ethernet_hub", + "ethernet_switch", + "nat", + "vpcs", + "virtualbox", + "dynamips", + "frame_relay_switch", + "atm_switch", + "qemu", + "vmware", + "docker", + "iou", + ], + "version": __version__, + "platform": sys.platform, + "cpus": psutil.cpu_count(logical=True), + "memory": psutil.virtual_memory().total, + "disk_size": psutil.disk_usage(get_default_project_directory()).total, + } async def test_get_on_gns3vm(self, app: FastAPI, compute_client: AsyncClient, on_gns3vm) -> None: response = await compute_client.get(app.url_path_for("compute:get_capabilities")) assert response.status_code == status.HTTP_200_OK - assert response.json() == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'docker', 'iou'], - 'version': __version__, - 'platform': sys.platform, - 'cpus': psutil.cpu_count(logical=True), - 'memory': psutil.virtual_memory().total, - 'disk_size': psutil.disk_usage(get_default_project_directory()).total, - } + assert response.json() == { + "node_types": [ + "cloud", + "ethernet_hub", + "ethernet_switch", + "nat", + "vpcs", + "virtualbox", + "dynamips", + "frame_relay_switch", + "atm_switch", + "qemu", + "vmware", + "docker", + "iou", + ], + "version": __version__, + "platform": sys.platform, + "cpus": psutil.cpu_count(logical=True), + "memory": psutil.virtual_memory().total, + "disk_size": psutil.disk_usage(get_default_project_directory()).total, + } diff --git a/tests/api/routes/compute/test_cloud_nodes.py b/tests/api/routes/compute/test_cloud_nodes.py index b19895c11..05949e5e6 100644 --- a/tests/api/routes/compute/test_cloud_nodes.py +++ b/tests/api/routes/compute/test_cloud_nodes.py @@ -29,231 +29,181 @@ pytestmark = pytest.mark.asyncio class TestCloudNodesRoutes: - @pytest_asyncio.fixture async def vm(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, on_gns3vm) -> dict: with asyncio_patch("gns3server.compute.builtin.nodes.cloud.Cloud._start_ubridge"): - response = await compute_client.post(app.url_path_for("compute:create_cloud", project_id=compute_project.id), - json={"name": "Cloud 1"}) + response = await compute_client.post( + app.url_path_for("compute:create_cloud", project_id=compute_project.id), json={"name": "Cloud 1"} + ) assert response.status_code == status.HTTP_201_CREATED return response.json() - - async def test_cloud_create( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project - ) -> None: + async def test_cloud_create(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project) -> None: with asyncio_patch("gns3server.compute.builtin.nodes.cloud.Cloud._start_ubridge"): - response = await compute_client.post(app.url_path_for("compute:create_cloud", project_id=compute_project.id), - json={"name": "Cloud 1"}) + response = await compute_client.post( + app.url_path_for("compute:create_cloud", project_id=compute_project.id), json={"name": "Cloud 1"} + ) assert response.status_code == 201 assert response.json()["name"] == "Cloud 1" assert response.json()["project_id"] == compute_project.id - async def test_get_cloud( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict ) -> None: - response = await compute_client.get(app.url_path_for("compute:get_cloud", project_id=vm["project_id"], node_id=vm["node_id"])) + response = await compute_client.get( + app.url_path_for("compute:get_cloud", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "Cloud 1" assert response.json()["project_id"] == compute_project.id assert response.json()["status"] == "started" - async def test_cloud_nio_create_udp( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict ) -> None: - params = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} - - url = app.url_path_for("compute:create_cloud_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - response = await compute_client.post(url, json=params) - assert response.status_code == status.HTTP_201_CREATED - assert response.json()["type"] == "nio_udp" - - - async def test_cloud_nio_update_udp( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict - ) -> None: - - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1", - "filters": {"packet_loss": 10} - } + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} url = app.url_path_for( "compute:create_cloud_nio", project_id=vm["project_id"], node_id=vm["node_id"], adapter_number="0", - port_number="0") + port_number="0", + ) + response = await compute_client.post(url, json=params) + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["type"] == "nio_udp" + + async def test_cloud_nio_update_udp( + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict + ) -> None: + + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1", "filters": {"packet_loss": 10}} + + url = app.url_path_for( + "compute:create_cloud_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["filters"] == {"packet_loss": 10} params["filters"].clear() - url = app.url_path_for("compute:create_cloud_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:create_cloud_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) response = await compute_client.put(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" assert response.json()["filters"] == {} - async def test_cloud_delete_nio( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict ) -> None: - params = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} - url = app.url_path_for("compute:create_cloud_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:create_cloud_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) await compute_client.post(url, json=params) - url = app.url_path_for("compute:delete_cloud_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:delete_cloud_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with asyncio_patch("gns3server.compute.builtin.nodes.cloud.Cloud._start_ubridge"): response = await compute_client.delete(url) assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_cloud_delete( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict ) -> None: response = await compute_client.delete( - app.url_path_for( - "compute:delete_cloud", - project_id=vm["project_id"], - node_id=vm["node_id"] - ) + app.url_path_for("compute:delete_cloud", project_id=vm["project_id"], node_id=vm["node_id"]) ) assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_cloud_stop( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict ) -> None: response = await compute_client.post( - app.url_path_for( - "compute:stop_cloud", - project_id=vm["project_id"], - node_id=vm["node_id"] - ) + app.url_path_for("compute:stop_cloud", project_id=vm["project_id"], node_id=vm["node_id"]) ) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED - async def test_cloud_suspend( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict ) -> None: response = await compute_client.post( - app.url_path_for( - "compute:suspend_cloud", - project_id=vm["project_id"], - node_id=vm["node_id"] - ) + app.url_path_for("compute:suspend_cloud", project_id=vm["project_id"], node_id=vm["node_id"]) ) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + async def test_cloud_update(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - async def test_cloud_update( - self, app: FastAPI, - compute_client: AsyncClient, - vm: dict - ) -> None: - - response = await compute_client.put(app.url_path_for("compute:update_cloud", project_id=vm["project_id"], node_id=vm["node_id"]), - json={"name": "test"}) + response = await compute_client.put( + app.url_path_for("compute:update_cloud", project_id=vm["project_id"], node_id=vm["node_id"]), + json={"name": "test"}, + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test" + async def test_cloud_start_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - async def test_cloud_start_capture( - self, app: FastAPI, - compute_client: AsyncClient, - vm: dict - ) -> None: - - params = { - "capture_file_name": "test.pcap", - "data_link_type": "DLT_EN10MB" - } + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} with asyncio_patch("gns3server.compute.builtin.nodes.cloud.Cloud.start_capture") as mock: - response = await compute_client.post(app.url_path_for("compute:start_cloud_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0"), - json=params) + response = await compute_client.post( + app.url_path_for( + "compute:start_cloud_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ), + json=params, + ) assert response.status_code == status.HTTP_200_OK assert mock.called assert "test.pcap" in response.json()["pcap_file_path"] - async def test_cloud_stop_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.builtin.nodes.cloud.Cloud.stop_capture") as mock: - response = await compute_client.post(app.url_path_for("compute:stop_cloud_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0")) + response = await compute_client.post( + app.url_path_for( + "compute:stop_cloud_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + ) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - # @pytest.mark.asyncio # async def test_cloud_pcap(self, compute_api, vm, compute_project): # diff --git a/tests/api/routes/compute/test_compute.py b/tests/api/routes/compute/test_compute.py index 04a38b2b8..0edb27aa6 100644 --- a/tests/api/routes/compute/test_compute.py +++ b/tests/api/routes/compute/test_compute.py @@ -30,18 +30,13 @@ pytestmark = pytest.mark.asyncio class TestComputeRoutes: + async def test_udp_allocation(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project) -> None: - async def test_udp_allocation( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project - ) -> None: - - response = await compute_client.post(app.url_path_for("compute:allocate_udp_port", project_id=compute_project.id), json={}) + response = await compute_client.post( + app.url_path_for("compute:allocate_udp_port", project_id=compute_project.id), json={} + ) assert response.status_code == status.HTTP_201_CREATED - assert response.json()['udp_port'] is not None - + assert response.json()["udp_port"] is not None async def test_interfaces(self, app: FastAPI, compute_client: AsyncClient) -> None: @@ -49,33 +44,30 @@ class TestComputeRoutes: assert response.status_code == status.HTTP_200_OK assert isinstance(response.json(), list) - async def test_version_output(self, app: FastAPI, compute_client: AsyncClient) -> None: response = await compute_client.get(app.url_path_for("compute:compute_version")) assert response.status_code == status.HTTP_200_OK - assert response.json() == {'version': __version__} - + assert response.json() == {"version": __version__} async def test_compute_authentication(self, app: FastAPI, compute_client: AsyncClient) -> None: - response = await compute_client.get(app.url_path_for("compute:compute_version"), auth=("admin", "invalid_password")) + response = await compute_client.get( + app.url_path_for("compute:compute_version"), auth=("admin", "invalid_password") + ) assert response.status_code == status.HTTP_401_UNAUTHORIZED - # @pytest.mark.asyncio # async def test_debug_output(compute_api): # # response = await compute_api.get('/debug') # assert response.status_code == 200 - async def test_statistics_output(self, app: FastAPI, compute_client: AsyncClient) -> None: response = await compute_client.get(app.url_path_for("compute:compute_statistics")) assert response.status_code == status.HTTP_200_OK - async def test_compute_auth_disabled(self, app: FastAPI, compute_client: AsyncClient) -> None: mock_settings = MagicMock() @@ -85,9 +77,10 @@ class TestComputeRoutes: mock_server_settings.compute_password.get_secret_value.return_value = "testpass" mock_settings.settings.Server = mock_server_settings - with patch("gns3server.api.routes.compute.dependencies.authentication.Config.instance", return_value=mock_settings): + with patch( + "gns3server.api.routes.compute.dependencies.authentication.Config.instance", return_value=mock_settings + ): response = await compute_client.get( - app.url_path_for("compute:compute_version"), - auth=("wrong_user", "wrong_password") + app.url_path_for("compute:compute_version"), auth=("wrong_user", "wrong_password") ) assert response.status_code == status.HTTP_200_OK diff --git a/tests/api/routes/compute/test_docker_nodes.py b/tests/api/routes/compute/test_docker_nodes.py index b9fa79fd8..fecc0ef56 100644 --- a/tests/api/routes/compute/test_docker_nodes.py +++ b/tests/api/routes/compute/test_docker_nodes.py @@ -31,7 +31,6 @@ pytestmark = pytest.mark.asyncio class TestDockerNodesRoutes: - @pytest.fixture def base_params(self) -> dict: """Return standard parameters""" @@ -44,11 +43,10 @@ class TestDockerNodesRoutes: "environment": "YES=1\nNO=0", "console_type": "telnet", "console_resolution": "1280x1024", - "extra_hosts": "test:127.0.0.1" + "extra_hosts": "test:127.0.0.1", } return params - # @pytest.yield_fixture(autouse=True) # def mock_connection(): # @@ -58,30 +56,20 @@ class TestDockerNodesRoutes: # yield # Docker._instance = None - @pytest_asyncio.fixture - async def vm( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict - ) -> dict: + async def vm(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict) -> dict: with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "nginx"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "8bd8153ea8f5"}): with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="exited"): - response = await compute_client.post(app.url_path_for("compute:create_docker_node", project_id=compute_project.id), - json=base_params) + response = await compute_client.post( + app.url_path_for("compute:create_docker_node", project_id=compute_project.id), json=base_params + ) assert response.status_code == status.HTTP_201_CREATED return response.json() - async def test_docker_create( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict ) -> None: with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "nginx"}]): @@ -99,12 +87,8 @@ class TestDockerNodesRoutes: assert response.json()["console_resolution"] == "1280x1024" assert response.json()["extra_hosts"] == "test:127.0.0.1" - async def test_docker_create_iol_startup_config( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict ) -> None: """ The controller materializes the GNS3_IOL_STARTUP_CONFIG knob into @@ -157,13 +141,13 @@ class TestDockerNodesRoutes: ), ) async def test_docker_create_with_invalid_name( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - name: str, - status_code: int + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + base_params: dict, + name: str, + status_code: int, ) -> None: base_params["name"] = name @@ -174,81 +158,71 @@ class TestDockerNodesRoutes: ) assert response.status_code == status_code - async def test_docker_start(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.start", return_value=True) as mock: - - response = await compute_client.post(app.url_path_for("compute:start_docker_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:start_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_docker_stop(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.stop", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:stop_docker_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:stop_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_docker_reload(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.restart", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:reload_docker_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:reload_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_docker_delete(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.delete", return_value=True) as mock: - response = await compute_client.delete(app.url_path_for("compute:delete_docker_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.delete( + app.url_path_for("compute:delete_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_docker_pause(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.pause", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:pause_docker_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:pause_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_docker_unpause(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.unpause", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:unpause_docker_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:unpause_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_docker_nio_create_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} - url = app.url_path_for("compute:create_docker_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:create_docker_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" @@ -281,7 +255,6 @@ class TestDockerNodesRoutes: assert response.status_code == status.HTTP_201_CREATED assert response.json()["added"] == 2 - async def test_docker_update_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: params = { @@ -290,25 +263,29 @@ class TestDockerNodesRoutes: "rport": 4343, "rhost": "127.0.0.1", "filters": {"packet_loss": 10}, - "suspend": False + "suspend": False, } - url = app.url_path_for("compute:create_docker_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:create_docker_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["filters"] == {"packet_loss": 10} params["filters"] = {} params["suspend"] = True - url = app.url_path_for("compute:update_docker_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:update_docker_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.adapter_update_nio_binding"): response = await compute_client.put(url, json=params) assert response.status_code == status.HTTP_201_CREATED @@ -318,30 +295,34 @@ class TestDockerNodesRoutes: async def test_docker_delete_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - url = app.url_path_for("compute:delete_docker_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:delete_docker_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.adapter_remove_nio_binding"): response = await compute_client.delete(url) assert response.status_code == status.HTTP_204_NO_CONTENT - - async def test_docker_update(self, app: FastAPI, compute_client: AsyncClient, vm: dict, free_console_port: int) -> None: + async def test_docker_update( + self, app: FastAPI, compute_client: AsyncClient, vm: dict, free_console_port: int + ) -> None: params = { "name": "test", "console": free_console_port, "start_command": "yes", "environment": "GNS3=1\nGNS4=0", - "extra_hosts": "test:127.0.0.1" + "extra_hosts": "test:127.0.0.1", } with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.update") as mock: - response = await compute_client.put(app.url_path_for("compute:update_docker_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json=params) + response = await compute_client.put( + app.url_path_for("compute:update_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]), + json=params, + ) assert response.status_code == 200 assert mock.called @@ -352,10 +333,7 @@ class TestDockerNodesRoutes: assert response.json()["extra_hosts"] == "test:127.0.0.1" async def test_docker_update_empty_strings_do_not_recreate_container( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project ) -> None: """ Web clients serialize empty form fields as "" while unset values are @@ -390,14 +368,15 @@ class TestDockerNodesRoutes: assert response.json()["start_command"] is None assert response.json()["console_http_path"] == "/" - async def test_docker_start_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - url = app.url_path_for("compute:start_docker_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:start_docker_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with patch("gns3server.compute.docker.docker_vm.DockerVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.start_capture") as mock: @@ -407,14 +386,15 @@ class TestDockerNodesRoutes: assert mock.called assert "test.pcap" in response.json()["pcap_file_path"] - async def test_docker_stop_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - url = app.url_path_for("compute:stop_docker_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:stop_docker_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with patch("gns3server.compute.docker.docker_vm.DockerVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.stop_capture") as mock: @@ -422,18 +402,21 @@ class TestDockerNodesRoutes: assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - - async def test_docker_duplicate(self, app: FastAPI, compute_client: AsyncClient, vm: dict, base_params: dict) -> None: + async def test_docker_duplicate( + self, app: FastAPI, compute_client: AsyncClient, vm: dict, base_params: dict + ) -> None: # create destination node first with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "nginx"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "8bd8153ea8f5"}): - response = await compute_client.post(app.url_path_for("compute:create_docker_node", - project_id=vm["project_id"]), json=base_params) + response = await compute_client.post( + app.url_path_for("compute:create_docker_node", project_id=vm["project_id"]), json=base_params + ) assert response.status_code == status.HTTP_201_CREATED params = {"destination_node_id": response.json()["node_id"]} - response = await compute_client.post(app.url_path_for("compute:duplicate_docker_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json=params) + response = await compute_client.post( + app.url_path_for("compute:duplicate_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]), + json=params, + ) assert response.status_code == status.HTTP_201_CREATED diff --git a/tests/api/routes/compute/test_dynamips_nodes.py b/tests/api/routes/compute/test_dynamips_nodes.py index 840d2b3d2..a586aa508 100644 --- a/tests/api/routes/compute/test_dynamips_nodes.py +++ b/tests/api/routes/compute/test_dynamips_nodes.py @@ -28,7 +28,6 @@ pytestmark = pytest.mark.asyncio class TestDynamipsNodesRoutes: - # @pytest.yield_fixture(scope="module") # async def vm(compute_api, compute_project, fake_image): # @@ -47,7 +46,6 @@ class TestDynamipsNodesRoutes: # #with asyncio_patch("gns3server.compute.dynamips.Dynamips.find_dynamips", return_value=dynamips_path): # # yield response.json - # async def test_dynamips_vm_create(compute_api, compute_project, fake_image): # # params = { @@ -66,7 +64,6 @@ class TestDynamipsNodesRoutes: # assert response.json["project_id"] == compute_project.id # assert response.json["dynamips_id"] - # def test_dynamips_vm_get(compute_api, project, vm): # response = compute_api.get("/projects/{project_id}/dynamips/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True) # assert response.status == 200 @@ -102,7 +99,6 @@ class TestDynamipsNodesRoutes: # assert mock.called # assert response.status == 204 - # def test_vbox_nio_create_udp(compute_api, vm): # # with asyncio_patch('gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_add_nio_binding') as mock: @@ -142,48 +138,42 @@ class TestDynamipsNodesRoutes: # assert response.json["name"] == "test" # assert response.json["console"] == free_console_port - @pytest.fixture def fake_image(self, tmpdir) -> str: """Create a fake Dynamips image on disk""" path = str(tmpdir / "7200.bin") with open(path, "wb+") as f: - f.write(b'\x7fELF\x01\x02\x01') + f.write(b"\x7fELF\x01\x02\x01") os.chmod(path, stat.S_IREAD) return path - @pytest.fixture def fake_file(self, tmpdir) -> str: """Create a fake file disk""" path = str(tmpdir / "7200.txt") with open(path, "w+") as f: - f.write('1') + f.write("1") os.chmod(path, stat.S_IREAD) return path - async def test_images( - self, app: FastAPI, - compute_client: AsyncClient, - tmpdir, fake_image: str, - fake_file: str + self, app: FastAPI, compute_client: AsyncClient, tmpdir, fake_image: str, fake_file: str ) -> None: with patch("gns3server.utils.images.default_images_directory", return_value=str(tmpdir)): response = await compute_client.get(app.url_path_for("compute:get_dynamips_images")) assert response.status_code == status.HTTP_200_OK - assert response.json() == [{"filename": "7200.bin", - "path": "7200.bin", - "filesize": 7, - "md5sum": "b0d5aa897d937aced5a6b1046e8f7e2e"}] - + assert response.json() == [ + {"filename": "7200.bin", "path": "7200.bin", "filesize": 7, "md5sum": "b0d5aa897d937aced5a6b1046e8f7e2e"} + ] async def test_upload_image(self, app: FastAPI, compute_client: AsyncClient, images_dir: str) -> None: - response = await compute_client.post(app.url_path_for("compute:upload_dynamips_image", filename="test2"), content=b"TEST") + response = await compute_client.post( + app.url_path_for("compute:upload_dynamips_image", filename="test2"), content=b"TEST" + ) assert response.status_code == status.HTTP_204_NO_CONTENT with open(os.path.join(images_dir, "IOS", "test2")) as f: @@ -193,35 +183,33 @@ class TestDynamipsNodesRoutes: checksum = f.read() assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf" - async def test_upload_image_forbidden_location(self, app: FastAPI, compute_client: AsyncClient) -> None: file_path = "%2e%2e/hello" - response = await compute_client.post(app.url_path_for("compute:upload_dynamips_image", filename=file_path), content=b"TEST") + response = await compute_client.post( + app.url_path_for("compute:upload_dynamips_image", filename=file_path), content=b"TEST" + ) assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_download_image(self, app: FastAPI, compute_client: AsyncClient, images_dir: str) -> None: - response = await compute_client.post(app.url_path_for("compute:upload_dynamips_image", filename="test3"), content=b"TEST") + response = await compute_client.post( + app.url_path_for("compute:upload_dynamips_image", filename="test3"), content=b"TEST" + ) assert response.status_code == status.HTTP_204_NO_CONTENT response = await compute_client.get(app.url_path_for("compute:download_dynamips_image", filename="test3")) assert response.status_code == status.HTTP_200_OK - async def test_download_image_forbidden(self, app: FastAPI, compute_client: AsyncClient, tmpdir) -> None: file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd" response = await compute_client.get(app.url_path_for("compute:download_dynamips_image", filename=file_path)) assert response.status_code == status.HTTP_403_FORBIDDEN - @pytest.mark.skipif(os.getuid() == 0, reason="Root can delete any image") async def test_upload_image_permission_denied( - self, app: FastAPI, - compute_client: AsyncClient, - images_dir: str + self, app: FastAPI, compute_client: AsyncClient, images_dir: str ) -> None: os.makedirs(os.path.join(images_dir, "IOS"), exist_ok=True) @@ -229,5 +217,7 @@ class TestDynamipsNodesRoutes: f.write("") os.chmod(os.path.join(images_dir, "IOS", "test2.tmp"), 0) - response = await compute_client.post(app.url_path_for("compute:upload_dynamips_image", filename="test2"), content=b"TEST") + response = await compute_client.post( + app.url_path_for("compute:upload_dynamips_image", filename="test2"), content=b"TEST" + ) assert response.status_code == status.HTTP_409_CONFLICT diff --git a/tests/api/routes/compute/test_ethernet_switch_nodes.py b/tests/api/routes/compute/test_ethernet_switch_nodes.py index af2a12ef1..f552b8c21 100644 --- a/tests/api/routes/compute/test_ethernet_switch_nodes.py +++ b/tests/api/routes/compute/test_ethernet_switch_nodes.py @@ -34,12 +34,14 @@ pytestmark = pytest.mark.asyncio class TestEthernetSwitchNodesRoutes: - @pytest_asyncio.fixture(autouse=True) async def stub_ubridge(self): """Keep uBridge from really starting and capture every command.""" - with asyncio_patch(f"{_NODE}._start_ubridge"), asyncio_patch(f"{_NODE}._stop_ubridge"), \ - asyncio_patch(f"{_NODE}._ubridge_send"): + with ( + asyncio_patch(f"{_NODE}._start_ubridge"), + asyncio_patch(f"{_NODE}._stop_ubridge"), + asyncio_patch(f"{_NODE}._ubridge_send"), + ): yield @pytest_asyncio.fixture @@ -47,8 +49,7 @@ class TestEthernetSwitchNodesRoutes: params = {"name": "Ethernet Switch"} response = await compute_client.post( - app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id), - json=params + app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id), json=params ) assert response.status_code == status.HTTP_201_CREATED @@ -65,15 +66,12 @@ class TestEthernetSwitchNodesRoutes: return {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} async def test_ethernet_switch_create( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project ) -> None: params = {"name": "Ethernet Switch 1"} response = await compute_client.post( - app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id), - json=params + app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id), json=params ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "Ethernet Switch 1" @@ -83,25 +81,24 @@ class TestEthernetSwitchNodesRoutes: # creation stands up the kernel bridge with VLAN filtering node = compute_project.get_node(response.json()["node_id"]) br = node._bridge_name - node._ubridge_send.assert_has_calls([ - call(f'brctl delete "{br}"'), - call(f'brctl create "{br}"'), - call(f'link set "{br}" up'), - call(f'brctl vlanfiltering "{br}" on'), - ]) + node._ubridge_send.assert_has_calls( + [ + call(f'brctl delete "{br}"'), + call(f'brctl create "{br}"'), + call(f'link set "{br}" up'), + call(f'brctl vlanfiltering "{br}" on'), + ] + ) async def test_ethernet_switch_get( - self, app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: response = await compute_client.get( app.url_path_for( "compute:get_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"] + node_id=ethernet_switch["node_id"], ) ) assert response.status_code == status.HTTP_200_OK @@ -110,18 +107,13 @@ class TestEthernetSwitchNodesRoutes: assert response.json()["status"] == "started" async def test_ethernet_switch_duplicate( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: # create destination switch first params = {"name": "Ethernet Switch 2"} response = await compute_client.post( - app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id), - json=params + app.url_path_for("compute:create_ethernet_switch", project_id=compute_project.id), json=params ) assert response.status_code == status.HTTP_201_CREATED @@ -130,16 +122,14 @@ class TestEthernetSwitchNodesRoutes: app.url_path_for( "compute:duplicate_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"]), json=params + node_id=ethernet_switch["node_id"], + ), + json=params, ) assert response.status_code == status.HTTP_201_CREATED async def test_ethernet_switch_update( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: params = {"name": "test", "console_type": "none"} @@ -148,8 +138,9 @@ class TestEthernetSwitchNodesRoutes: app.url_path_for( "compute:update_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"]), - json=params + node_id=ethernet_switch["node_id"], + ), + json=params, ) assert response.status_code == status.HTTP_200_OK @@ -160,11 +151,7 @@ class TestEthernetSwitchNodesRoutes: node._ubridge_send.assert_not_called() async def test_ethernet_switch_update_ports_qinq_proto( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: # a QinQ port with the 802.1ad ethertype must switch the bridge protocol @@ -179,8 +166,9 @@ class TestEthernetSwitchNodesRoutes: app.url_path_for( "compute:update_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"]), - json=port_params + node_id=ethernet_switch["node_id"], + ), + json=port_params, ) assert response.status_code == status.HTTP_200_OK @@ -190,55 +178,60 @@ class TestEthernetSwitchNodesRoutes: @pytest.mark.parametrize( "ports_settings", ( - {"name": "Ethernet0", "port_number": 0, "type": "dot42q", "vlan": 1}, # bad type - {"name": "Ethernet0", "port_number": 0, "type": "access"}, # missing vlan - {"name": "Ethernet0", "port_number": 0, "type": "dot1q", "vlan": 1, - "ethertype": "0x88A8"}, # ethertype only for qinq - {"name": "Ethernet0", "port_number": 0, "type": "qinq", "vlan": 1, - "ethertype": "0x4242"}, # bad ethertype - {"name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 0}, # vlan < 1 - {"name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 4242}, # vlan > 4094 - ) + {"name": "Ethernet0", "port_number": 0, "type": "dot42q", "vlan": 1}, # bad type + {"name": "Ethernet0", "port_number": 0, "type": "access"}, # missing vlan + { + "name": "Ethernet0", + "port_number": 0, + "type": "dot1q", + "vlan": 1, + "ethertype": "0x88A8", + }, # ethertype only for qinq + {"name": "Ethernet0", "port_number": 0, "type": "qinq", "vlan": 1, "ethertype": "0x4242"}, # bad ethertype + {"name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 0}, # vlan < 1 + {"name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 4242}, # vlan > 4094 + ), ) async def test_ethernet_switch_update_ports_invalid( - self, - app: FastAPI, - compute_client: AsyncClient, - ethernet_switch: dict, - ports_settings: dict, + self, + app: FastAPI, + compute_client: AsyncClient, + ethernet_switch: dict, + ports_settings: dict, ) -> None: response = await compute_client.put( app.url_path_for( "compute:update_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"]), - json={"ports_mapping": [ports_settings]} + node_id=ethernet_switch["node_id"], + ), + json={"ports_mapping": [ports_settings]}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT async def test_ethernet_switch_delete( - self, app: FastAPI, - compute_client: AsyncClient, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict ) -> None: response = await compute_client.delete( app.url_path_for( "compute:delete_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"] + node_id=ethernet_switch["node_id"], ) ) assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_ethernet_switch_start(self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict) -> None: + async def test_ethernet_switch_start( + self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict + ) -> None: response = await compute_client.post( app.url_path_for( "compute:start_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"] + node_id=ethernet_switch["node_id"], ) ) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED @@ -249,39 +242,39 @@ class TestEthernetSwitchNodesRoutes: app.url_path_for( "compute:stop_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"] + node_id=ethernet_switch["node_id"], ) ) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED - async def test_ethernet_switch_suspend(self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict) -> None: + async def test_ethernet_switch_suspend( + self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict + ) -> None: response = await compute_client.post( app.url_path_for( "compute:suspend_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"] + node_id=ethernet_switch["node_id"], ) ) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED - async def test_ethernet_switch_reload(self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict) -> None: + async def test_ethernet_switch_reload( + self, app: FastAPI, compute_client: AsyncClient, ethernet_switch: dict + ) -> None: response = await compute_client.post( app.url_path_for( "compute:reload_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"] + node_id=ethernet_switch["node_id"], ) ) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED async def test_ethernet_switch_create_udp_access( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: url = app.url_path_for( @@ -289,7 +282,7 @@ class TestEthernetSwitchNodesRoutes: project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) response = await compute_client.post(url, json=self._udp_params()) assert response.status_code == status.HTTP_201_CREATED @@ -301,23 +294,21 @@ class TestEthernetSwitchNodesRoutes: tap = f"{br}-0" relay = f"{node.id}-0" # access VLAN 1 (default): drop default PVID 1, re-add 1 as PVID/untagged - node._ubridge_send.assert_has_calls([ - call(f"bridge create {relay}"), - call(f'bridge add_nio_tap {relay} "{tap}"'), - call(f'brctl addif "{br}" "{tap}"'), - call(f'brctl vlan_del "{br}" "{tap}" 1'), - call(f'brctl vlan_add "{br}" "{tap}" 1 pvid untagged'), - call(f"bridge add_nio_udp {relay} {nio.lport} {nio.rhost} {nio.rport}"), - call(f"bridge reset_packet_filters {relay}"), - call(f"bridge start {relay}"), - ]) + node._ubridge_send.assert_has_calls( + [ + call(f"bridge create {relay}"), + call(f'bridge add_nio_tap {relay} "{tap}"'), + call(f'brctl addif "{br}" "{tap}"'), + call(f'brctl vlan_del "{br}" "{tap}" 1'), + call(f'brctl vlan_add "{br}" "{tap}" 1 pvid untagged'), + call(f"bridge add_nio_udp {relay} {nio.lport} {nio.rhost} {nio.rport}"), + call(f"bridge reset_packet_filters {relay}"), + call(f"bridge start {relay}"), + ] + ) async def test_ethernet_switch_create_udp_dot1q( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: # make port 0 a dot1q trunk with native VLAN 10 @@ -325,10 +316,13 @@ class TestEthernetSwitchNodesRoutes: app.url_path_for( "compute:update_ethernet_switch", project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"]), - json={"ports_mapping": [ - {"name": "Ethernet0", "port_number": 0, "type": "dot1q", "vlan": 10}, - ]} + node_id=ethernet_switch["node_id"], + ), + json={ + "ports_mapping": [ + {"name": "Ethernet0", "port_number": 0, "type": "dot1q", "vlan": 10}, + ] + }, ) node = compute_project.get_node(ethernet_switch["node_id"]) node._ubridge_send.reset_mock() @@ -338,7 +332,7 @@ class TestEthernetSwitchNodesRoutes: project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) response = await compute_client.post(url, json=self._udp_params()) assert response.status_code == status.HTTP_201_CREATED @@ -346,18 +340,16 @@ class TestEthernetSwitchNodesRoutes: br = node._bridge_name tap = f"{br}-0" # trunk: drop default 1, admit all VIDs tagged, mark native 10 PVID/untagged - node._ubridge_send.assert_has_calls([ - call(f'brctl vlan_del "{br}" "{tap}" 1'), - call(f'brctl vlan_add "{br}" "{tap}" 1 vid 4094'), - call(f'brctl vlan_add "{br}" "{tap}" 10 pvid untagged'), - ]) + node._ubridge_send.assert_has_calls( + [ + call(f'brctl vlan_del "{br}" "{tap}" 1'), + call(f'brctl vlan_add "{br}" "{tap}" 1 vid 4094'), + call(f'brctl vlan_add "{br}" "{tap}" 10 pvid untagged'), + ] + ) async def test_ethernet_switch_delete_nio( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: url = app.url_path_for( @@ -365,7 +357,7 @@ class TestEthernetSwitchNodesRoutes: project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) await compute_client.post(url, json=self._udp_params()) @@ -377,7 +369,7 @@ class TestEthernetSwitchNodesRoutes: project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) response = await compute_client.delete(url) assert response.status_code == status.HTTP_204_NO_CONTENT @@ -386,17 +378,15 @@ class TestEthernetSwitchNodesRoutes: br = node._bridge_name tap = f"{br}-0" relay = f"{node.id}-0" - node._ubridge_send.assert_has_calls([ - call(f'brctl delif "{br}" "{tap}"'), - call(f"bridge delete {relay}"), - ]) + node._ubridge_send.assert_has_calls( + [ + call(f'brctl delif "{br}" "{tap}"'), + call(f"bridge delete {relay}"), + ] + ) async def test_ethernet_switch_update_nio( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: url = app.url_path_for( @@ -404,7 +394,7 @@ class TestEthernetSwitchNodesRoutes: project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) params = self._udp_params() params["filters"] = {"delay": [10, 0]} @@ -421,7 +411,7 @@ class TestEthernetSwitchNodesRoutes: project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) response = await compute_client.put(url, json=params) assert response.status_code == status.HTTP_201_CREATED @@ -432,11 +422,7 @@ class TestEthernetSwitchNodesRoutes: node._ubridge_send.assert_any_call(f"bridge reset_packet_filters {relay}") async def test_ethernet_switch_toggle_marker( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: # a marker installed via the NIO registers in the node's filter-bridge map @@ -445,7 +431,7 @@ class TestEthernetSwitchNodesRoutes: project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) params = self._udp_params() params["markers"] = {"icmp": {"bpf": "icmp", "link_id": "link-1", "enabled": True}} @@ -459,7 +445,7 @@ class TestEthernetSwitchNodesRoutes: "compute:toggle_ethernet_switch_marker", project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], - marker_name="icmp" + marker_name="icmp", ) response = await compute_client.put(url, json={"enabled": False}) assert response.status_code == status.HTTP_200_OK @@ -472,17 +458,13 @@ class TestEthernetSwitchNodesRoutes: "compute:toggle_ethernet_switch_marker", project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], - marker_name="nope" + marker_name="nope", ) response = await compute_client.put(url, json={"enabled": True}) assert response.status_code == status.HTTP_404_NOT_FOUND async def test_ethernet_switch_start_capture( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: # capture needs a wired port @@ -491,7 +473,7 @@ class TestEthernetSwitchNodesRoutes: project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) await compute_client.post(url, json=self._udp_params()) @@ -499,11 +481,13 @@ class TestEthernetSwitchNodesRoutes: node._ubridge_send.reset_mock() params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} - url = app.url_path_for("compute:start_ethernet_switch_capture", - project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:start_ethernet_switch_capture", + project_id=ethernet_switch["project_id"], + node_id=ethernet_switch["node_id"], + adapter_number="0", + port_number="0", + ) response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_200_OK @@ -512,11 +496,7 @@ class TestEthernetSwitchNodesRoutes: node._ubridge_send.assert_any_call(f'bridge start_capture {relay} "{node.get_nio(0).pcap_output_file}"') async def test_ethernet_switch_stop_capture( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ethernet_switch: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ethernet_switch: dict ) -> None: # start a capture first @@ -526,17 +506,19 @@ class TestEthernetSwitchNodesRoutes: project_id=ethernet_switch["project_id"], node_id=ethernet_switch["node_id"], adapter_number="0", - port_number="0" + port_number="0", ), - json=self._udp_params() + json=self._udp_params(), ) await compute_client.post( - app.url_path_for("compute:start_ethernet_switch_capture", - project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"], - adapter_number="0", - port_number="0"), - json={"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} + app.url_path_for( + "compute:start_ethernet_switch_capture", + project_id=ethernet_switch["project_id"], + node_id=ethernet_switch["node_id"], + adapter_number="0", + port_number="0", + ), + json={"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"}, ) node = compute_project.get_node(ethernet_switch["node_id"]) @@ -544,11 +526,13 @@ class TestEthernetSwitchNodesRoutes: relay = f"{node.id}-0" response = await compute_client.post( - app.url_path_for("compute:stop_ethernet_switch_capture", - project_id=ethernet_switch["project_id"], - node_id=ethernet_switch["node_id"], - adapter_number="0", - port_number="0") + app.url_path_for( + "compute:stop_ethernet_switch_capture", + project_id=ethernet_switch["project_id"], + node_id=ethernet_switch["node_id"], + adapter_number="0", + port_number="0", + ) ) assert response.status_code == status.HTTP_204_NO_CONTENT node._ubridge_send.assert_any_call(f"bridge stop_capture {relay}") diff --git a/tests/api/routes/compute/test_images.py b/tests/api/routes/compute/test_images.py index 9aff40a3d..62209eea1 100644 --- a/tests/api/routes/compute/test_images.py +++ b/tests/api/routes/compute/test_images.py @@ -24,27 +24,22 @@ pytestmark = pytest.mark.asyncio class TestImagesRoutes: - async def test_pull_docker_image(self, app: FastAPI, compute_client: AsyncClient) -> None: with asyncio_patch("gns3server.compute.docker.Docker.pull_image") as mock: response = await compute_client.post( - app.url_path_for("compute:pull_docker_image"), - json={"image": "nginx:latest"} + app.url_path_for("compute:pull_docker_image"), json={"image": "nginx:latest"} ) mock.assert_called_once_with("nginx:latest", force=True) assert response.status_code == status.HTTP_204_NO_CONTENT @pytest.mark.parametrize("image", ["", " ", "nginx latest"]) async def test_pull_docker_image_rejects_invalid_name( - self, app: FastAPI, compute_client: AsyncClient, image: str + self, app: FastAPI, compute_client: AsyncClient, image: str ) -> None: with asyncio_patch("gns3server.compute.docker.Docker.pull_image") as mock: - response = await compute_client.post( - app.url_path_for("compute:pull_docker_image"), - json={"image": image} - ) + response = await compute_client.post(app.url_path_for("compute:pull_docker_image"), json={"image": image}) mock.assert_not_called() assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT @@ -52,8 +47,7 @@ class TestImagesRoutes: with asyncio_patch("gns3server.compute.docker.Docker.load_image") as mock: response = await compute_client.post( - app.url_path_for("compute:load_docker_image"), - content=b"docker-save-tar-bytes" + app.url_path_for("compute:load_docker_image"), content=b"docker-save-tar-bytes" ) mock.assert_called_once() # the tar is streamed to the Docker daemon as an async iterable diff --git a/tests/api/routes/compute/test_iou_nodes.py b/tests/api/routes/compute/test_iou_nodes.py index 18143d680..565ed3070 100644 --- a/tests/api/routes/compute/test_iou_nodes.py +++ b/tests/api/routes/compute/test_iou_nodes.py @@ -32,55 +32,44 @@ pytestmark = pytest.mark.asyncio class TestIOUNodesRoutes: - @pytest.fixture def fake_iou_bin(self, images_dir) -> str: """Create a fake IOU image on disk""" path = os.path.join(images_dir, "IOU", "iou.bin") with open(path, "w+") as f: - f.write('\x7fELF\x01\x01\x01') + f.write("\x7fELF\x01\x01\x01") os.chmod(path, stat.S_IREAD | stat.S_IEXEC) return path - @pytest.fixture def base_params(self, tmpdir, fake_iou_bin) -> dict: """Return standard parameters""" return {"application_id": 42, "name": "IOU-TEST-1", "path": "iou.bin"} - @pytest_asyncio.fixture - async def vm( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict - ) -> dict: + async def vm(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict) -> dict: - response = await compute_client.post(app.url_path_for("compute:create_iou_node", project_id=compute_project.id), json=base_params) + response = await compute_client.post( + app.url_path_for("compute:create_iou_node", project_id=compute_project.id), json=base_params + ) assert response.status_code == status.HTTP_201_CREATED return response.json() - def startup_config_file(self, compute_project: Project, vm: dict) -> str: directory = os.path.join(compute_project.path, "project-files", "iou", vm["node_id"]) os.makedirs(directory, exist_ok=True) return os.path.join(directory, "startup-config.cfg") - async def test_iou_create( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict ) -> None: - response = await compute_client.post(app.url_path_for("compute:create_iou_node", project_id=compute_project.id), json=base_params) + response = await compute_client.post( + app.url_path_for("compute:create_iou_node", project_id=compute_project.id), json=base_params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "IOU-TEST-1" assert response.json()["project_id"] == compute_project.id @@ -90,13 +79,8 @@ class TestIOUNodesRoutes: assert response.json()["nvram"] == 256 assert response.json()["l1_keepalives"] is False - async def test_iou_create_with_params( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict ) -> None: params = base_params @@ -108,7 +92,9 @@ class TestIOUNodesRoutes: params["startup_config_content"] = "hostname test" params["use_default_iou_values"] = False - response = await compute_client.post(app.url_path_for("compute:create_iou_node", project_id=compute_project.id), json=params) + response = await compute_client.post( + app.url_path_for("compute:create_iou_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "IOU-TEST-1" assert response.json()["project_id"] == compute_project.id @@ -122,7 +108,6 @@ class TestIOUNodesRoutes: with open(self.startup_config_file(compute_project, response.json())) as f: assert f.read() == "hostname test" - @pytest.mark.parametrize( "name, status_code", ( @@ -136,13 +121,13 @@ class TestIOUNodesRoutes: ), ) async def test_iou_create_with_invalid_name( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - name: str, - status_code: int + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + base_params: dict, + name: str, + status_code: int, ) -> None: base_params["name"] = name @@ -151,40 +136,33 @@ class TestIOUNodesRoutes: ) assert response.status_code == status_code - async def test_iou_create_startup_config_already_exist( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict) -> None: + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict + ) -> None: """We don't erase a startup-config if already exist at project creation""" node_id = str(uuid.uuid4()) - startup_config_file_path = self.startup_config_file(compute_project, {'node_id': node_id}) - with open(startup_config_file_path, 'w+') as f: + startup_config_file_path = self.startup_config_file(compute_project, {"node_id": node_id}) + with open(startup_config_file_path, "w+") as f: f.write("echo hello") params = base_params params["node_id"] = node_id params["startup_config_content"] = "hostname test" - response = await compute_client.post(app.url_path_for("compute:create_iou_node", project_id=compute_project.id), json=params) + response = await compute_client.post( + app.url_path_for("compute:create_iou_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED with open(self.startup_config_file(compute_project, response.json())) as f: assert f.read() == "echo hello" + async def test_iou_get(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict) -> None: - async def test_iou_get( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict - ) -> None: - - response = await compute_client.get(app.url_path_for("compute:get_iou_node", project_id=vm["project_id"], node_id=vm["node_id"])) + response = await compute_client.get( + app.url_path_for("compute:get_iou_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "IOU-TEST-1" assert response.json()["project_id"] == compute_project.id @@ -194,73 +172,62 @@ class TestIOUNodesRoutes: assert response.json()["nvram"] == 256 assert response.json()["l1_keepalives"] is False - async def test_iou_start(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.iou.iou_vm.IOUVM.start", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:start_iou_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json={}) + response = await compute_client.post( + app.url_path_for("compute:start_iou_node", project_id=vm["project_id"], node_id=vm["node_id"]), json={} + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_iou_start_with_iourc(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: params = {"iourc_content": "test"} with asyncio_patch("gns3server.compute.iou.iou_vm.IOUVM.start", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:start_iou_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json=params) + response = await compute_client.post( + app.url_path_for("compute:start_iou_node", project_id=vm["project_id"], node_id=vm["node_id"]), + json=params, + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_iou_stop(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.iou.iou_vm.IOUVM.stop", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:stop_iou_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:stop_iou_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_iou_reload(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.iou.iou_vm.IOUVM.reload", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:reload_iou_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:reload_iou_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_iou_delete(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: with asyncio_patch("gns3server.compute.iou.IOU.delete_node", return_value=True) as mock: - response = await compute_client.delete(app.url_path_for("compute:delete_iou_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.delete( + app.url_path_for("compute:delete_iou_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_iou_suspend(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: response = await compute_client.delete( - app.url_path_for( - "compute:suspend_iou_node", - project_id=vm["project_id"], - node_id=vm["node_id"]) + app.url_path_for("compute:suspend_iou_node", project_id=vm["project_id"], node_id=vm["node_id"]) ) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED async def test_iou_update( - self, app: FastAPI, - compute_client: AsyncClient, - vm: dict, - free_console_port: int + self, app: FastAPI, compute_client: AsyncClient, vm: dict, free_console_port: int ) -> None: params = { @@ -274,9 +241,9 @@ class TestIOUNodesRoutes: "use_default_iou_values": True, } - response = await compute_client.put(app.url_path_for("compute:update_iou_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json=params) + response = await compute_client.put( + app.url_path_for("compute:update_iou_node", project_id=vm["project_id"], node_id=vm["node_id"]), json=params + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test" assert response.json()["console"] == free_console_port @@ -287,173 +254,141 @@ class TestIOUNodesRoutes: assert response.json()["l1_keepalives"] is True assert response.json()["use_default_iou_values"] is True - async def test_iou_nio_create_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - params = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} - url = app.url_path_for("compute:create_iou_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="1", - port_number="0") + url = app.url_path_for( + "compute:create_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="1", + port_number="0", + ) response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" - async def test_iou_nio_update_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1", - "filters": {"packet_loss": 10} - } + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1", "filters": {"packet_loss": 10}} - url = app.url_path_for("compute:create_iou_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="1", - port_number="0") + url = app.url_path_for( + "compute:create_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="1", + port_number="0", + ) response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["filters"] == {"packet_loss": 10} params["filters"].clear() - url = app.url_path_for("compute:update_iou_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="1", - port_number="0") + url = app.url_path_for( + "compute:update_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="1", + port_number="0", + ) response = await compute_client.put(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" assert response.json()["filters"] == {} - async def test_iou_nio_create_ethernet( - self, - app: FastAPI, - compute_client: AsyncClient, - vm: dict, - ethernet_device: str + self, app: FastAPI, compute_client: AsyncClient, vm: dict, ethernet_device: str ) -> None: - params = { - "type": "nio_ethernet", - "ethernet_device": ethernet_device - } + params = {"type": "nio_ethernet", "ethernet_device": ethernet_device} - url = app.url_path_for("compute:create_iou_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="1", - port_number="0") + url = app.url_path_for( + "compute:create_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="1", + port_number="0", + ) response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_ethernet" assert response.json()["ethernet_device"] == ethernet_device - async def test_iou_nio_create_ethernet_different_port( - self, - app: FastAPI, - compute_client: AsyncClient, - vm: dict, - ethernet_device: str + self, app: FastAPI, compute_client: AsyncClient, vm: dict, ethernet_device: str ) -> None: - params = { - "type": "nio_ethernet", - "ethernet_device": ethernet_device - } + params = {"type": "nio_ethernet", "ethernet_device": ethernet_device} - url = app.url_path_for("compute:create_iou_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="3") + url = app.url_path_for( + "compute:create_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="3", + ) response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_ethernet" assert response.json()["ethernet_device"] == ethernet_device - async def test_iou_nio_create_tap( - self, - app: FastAPI, - compute_client: AsyncClient, - vm: dict, - ethernet_device: str + self, app: FastAPI, compute_client: AsyncClient, vm: dict, ethernet_device: str ) -> None: - params = { - "type": "nio_tap", - "tap_device": ethernet_device - } + params = {"type": "nio_tap", "tap_device": ethernet_device} - url = app.url_path_for("compute:create_iou_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="1", - port_number="0") + url = app.url_path_for( + "compute:create_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="1", + port_number="0", + ) with patch("gns3server.compute.base_manager.BaseManager.has_privileged_access", return_value=True): response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_tap" + async def test_iou_delete_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - async def test_iou_delete_nio( - self, - app: FastAPI, - compute_client: AsyncClient, - vm: dict - ) -> None: + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1" - } - - url = app.url_path_for("compute:create_iou_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="1", - port_number="0") + url = app.url_path_for( + "compute:create_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="1", + port_number="0", + ) await compute_client.post(url, json=params) - url = app.url_path_for("compute:delete_iou_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="1", - port_number="0") + url = app.url_path_for( + "compute:delete_iou_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="1", + port_number="0", + ) response = await compute_client.delete(url) assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_iou_start_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - params = { - "capture_file_name": "test.pcap", - "data_link_type": "DLT_EN10MB" - } + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} - url = app.url_path_for("compute:start_iou_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:start_iou_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with patch("gns3server.compute.iou.iou_vm.IOUVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.iou.iou_vm.IOUVM.start_capture") as mock: @@ -462,14 +397,15 @@ class TestIOUNodesRoutes: assert mock.called assert "test.pcap" in response.json()["pcap_file_path"] - async def test_iou_stop_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - url = app.url_path_for("compute:stop_iou_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:stop_iou_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with patch("gns3server.compute.iou.iou_vm.IOUVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.iou.iou_vm.IOUVM.stop_capture") as mock: @@ -477,7 +413,6 @@ class TestIOUNodesRoutes: assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - # @pytest.mark.asyncio # async def test_iou_pcap(compute_api, vm, compute_project): # @@ -486,18 +421,20 @@ class TestIOUNodesRoutes: # response = await compute_client.get("/projects/{project_id}/iou/nodes/{node_id}/adapters/0/ports/0/pcap".format(project_id=compute_project.id, node_id=vm["node_id"]), raw=True) # assert response.status_code == status.HTTP_200_OK - async def test_images(self, app: FastAPI, compute_client: AsyncClient, fake_iou_bin: str) -> None: response = await compute_client.get(app.url_path_for("compute:get_iou_images")) assert response.status_code == status.HTTP_200_OK - assert response.json() == [{"filename": "iou.bin", "path": "iou.bin", "filesize": 7, "md5sum": "e573e8f5c93c6c00783f20c7a170aa6c"}] - + assert response.json() == [ + {"filename": "iou.bin", "path": "iou.bin", "filesize": 7, "md5sum": "e573e8f5c93c6c00783f20c7a170aa6c"} + ] async def test_upload_image(self, app: FastAPI, compute_client: AsyncClient, tmpdir) -> None: with patch("gns3server.compute.IOU.get_images_directory", return_value=str(tmpdir)): - response = await compute_client.post(app.url_path_for("compute:upload_iou_image", filename="test2"), content=b"TEST") + response = await compute_client.post( + app.url_path_for("compute:upload_iou_image", filename="test2"), content=b"TEST" + ) assert response.status_code == status.HTTP_204_NO_CONTENT with open(str(tmpdir / "test2")) as f: @@ -507,39 +444,42 @@ class TestIOUNodesRoutes: checksum = f.read() assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf" - async def test_upload_image_forbidden_location(self, app: FastAPI, compute_client: AsyncClient) -> None: file_path = "%2e%2e/hello" - response = await compute_client.post(app.url_path_for("compute:upload_dynamips_image", filename=file_path), content=b"TEST") + response = await compute_client.post( + app.url_path_for("compute:upload_dynamips_image", filename=file_path), content=b"TEST" + ) assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_download_image(self, app: FastAPI, compute_client: AsyncClient, images_dir: str) -> None: - response = await compute_client.post(app.url_path_for("compute:upload_dynamips_image", filename="test3"), content=b"TEST") + response = await compute_client.post( + app.url_path_for("compute:upload_dynamips_image", filename="test3"), content=b"TEST" + ) assert response.status_code == status.HTTP_204_NO_CONTENT response = await compute_client.get(app.url_path_for("compute:download_dynamips_image", filename="test3")) assert response.status_code == status.HTTP_200_OK - async def test_download_image_forbidden(self, app: FastAPI, compute_client: AsyncClient, tmpdir) -> None: file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd" response = await compute_client.get(app.url_path_for("compute:download_iou_image", filename=file_path)) assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_iou_duplicate(self, app: FastAPI, compute_client: AsyncClient, vm: dict, base_params: dict) -> None: # create destination node first - response = await compute_client.post(app.url_path_for("compute:create_iou_node", project_id=vm["project_id"]), json=base_params) + response = await compute_client.post( + app.url_path_for("compute:create_iou_node", project_id=vm["project_id"]), json=base_params + ) assert response.status_code == status.HTTP_201_CREATED params = {"destination_node_id": response.json()["node_id"]} - response = await compute_client.post(app.url_path_for("compute:duplicate_iou_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json=params) + response = await compute_client.post( + app.url_path_for("compute:duplicate_iou_node", project_id=vm["project_id"], node_id=vm["node_id"]), + json=params, + ) assert response.status_code == status.HTTP_201_CREATED diff --git a/tests/api/routes/compute/test_nat_nodes.py b/tests/api/routes/compute/test_nat_nodes.py index fea31fa01..aafa427aa 100644 --- a/tests/api/routes/compute/test_nat_nodes.py +++ b/tests/api/routes/compute/test_nat_nodes.py @@ -28,102 +28,81 @@ pytestmark = pytest.mark.asyncio class TestNATNodesRoutes: - @pytest_asyncio.fixture async def vm( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - ubridge_path: str, - on_gns3vm + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, ubridge_path: str, on_gns3vm ) -> dict: with asyncio_patch("gns3server.compute.builtin.nodes.nat.Nat._start_ubridge"): - response = await compute_client.post(app.url_path_for("compute:create_nat_node", project_id=compute_project.id), - json={"name": "Nat 1"}) + response = await compute_client.post( + app.url_path_for("compute:create_nat_node", project_id=compute_project.id), json={"name": "Nat 1"} + ) assert response.status_code == status.HTTP_201_CREATED return response.json() - async def test_nat_create( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - on_gns3vm + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, on_gns3vm ) -> None: with asyncio_patch("gns3server.compute.builtin.nodes.nat.Nat._start_ubridge"): - response = await compute_client.post(app.url_path_for("compute:create_nat_node", project_id=compute_project.id), - json={"name": "Nat 1"}) + response = await compute_client.post( + app.url_path_for("compute:create_nat_node", project_id=compute_project.id), json={"name": "Nat 1"} + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "Nat 1" assert response.json()["project_id"] == compute_project.id + async def test_nat_get(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict) -> None: - async def test_nat_get( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict - ) -> None: - - response = await compute_client.get(app.url_path_for("compute:get_nat_node", project_id=vm["project_id"], node_id=vm["node_id"])) + response = await compute_client.get( + app.url_path_for("compute:get_nat_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "Nat 1" assert response.json()["project_id"] == compute_project.id assert response.json()["status"] == "started" - async def test_nat_nio_create_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1" - } + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} - url = app.url_path_for("compute:create_nat_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:create_nat_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with asyncio_patch("gns3server.compute.builtin.nodes.nat.Nat.add_nio"): response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" - async def test_nat_nio_update_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1", - "filters": {"packet_loss": 10} - } + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1", "filters": {"packet_loss": 10}} - url = app.url_path_for("compute:create_nat_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:create_nat_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["filters"] == {"packet_loss": 10} params["filters"].clear() - url = app.url_path_for("compute:update_nat_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:update_nat_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) response = await compute_client.put(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" @@ -131,83 +110,79 @@ class TestNATNodesRoutes: async def test_nat_delete_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1" - } + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} - url = app.url_path_for("compute:create_nat_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:create_nat_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with asyncio_patch("gns3server.compute.builtin.nodes.nat.Nat.add_nio"): await compute_client.post(url, json=params) - url = app.url_path_for("compute:delete_nat_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:delete_nat_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with asyncio_patch("gns3server.compute.builtin.nodes.nat.Nat.remove_nio") as mock: response = await compute_client.delete(url) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_nat_delete(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - response = await compute_client.delete(app.url_path_for("compute:delete_nat_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.delete( + app.url_path_for("compute:delete_nat_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_nat_update(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - response = await compute_client.put(app.url_path_for("compute:update_nat_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json={"name": "test"}) + response = await compute_client.put( + app.url_path_for("compute:update_nat_node", project_id=vm["project_id"], node_id=vm["node_id"]), + json={"name": "test"}, + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test" - async def test_nat_start_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - params = { - "capture_file_name": "test.pcap", - "data_link_type": "DLT_EN10MB" - } + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} - url = app.url_path_for("compute:start_nat_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:start_nat_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with asyncio_patch("gns3server.compute.builtin.nodes.nat.Nat.start_capture") as mock: response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_200_OK assert mock.called assert "test.pcap" in response.json()["pcap_file_path"] - async def test_nat_stop_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - url = app.url_path_for("compute:stop_nat_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:stop_nat_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) with asyncio_patch("gns3server.compute.builtin.nodes.nat.Nat.stop_capture") as mock: response = await compute_client.post(url) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - # @pytest.mark.asyncio # async def test_nat_pcap(self, compute_api, vm, compute_project): # diff --git a/tests/api/routes/compute/test_notifications.py b/tests/api/routes/compute/test_notifications.py index c21b9e8e5..da8663e6c 100644 --- a/tests/api/routes/compute/test_notifications.py +++ b/tests/api/routes/compute/test_notifications.py @@ -27,16 +27,16 @@ from gns3server.compute.notification_manager import NotificationManager # # FIXME: how to test websockets # pass - #with compute_api.ws("/notifications/ws") as ws: +# with compute_api.ws("/notifications/ws") as ws: - # answer = await ws.receive_text() - # print(answer) - # answer = json.loads(answer) - # - # assert answer["action"] == "ping" - # - # NotificationManager.instance().emit("test", {}) - # - # answer = await ws.receive_text() - # answer = json.loads(answer) - # assert answer["action"] == "test" +# answer = await ws.receive_text() +# print(answer) +# answer = json.loads(answer) +# +# assert answer["action"] == "ping" +# +# NotificationManager.instance().emit("test", {}) +# +# answer = await ws.receive_text() +# answer = json.loads(answer) +# assert answer["action"] == "test" diff --git a/tests/api/routes/compute/test_projects.py b/tests/api/routes/compute/test_projects.py index 6131db742..88c4b463a 100644 --- a/tests/api/routes/compute/test_projects.py +++ b/tests/api/routes/compute/test_projects.py @@ -31,23 +31,15 @@ pytestmark = pytest.mark.asyncio class TestComputeProjectRoutes: - @pytest.fixture def base_params(self, tmpdir) -> dict: """Return standard parameters""" - params = { - "name": "test", - "project_id": str(uuid.uuid4()) - } + params = {"name": "test", "project_id": str(uuid.uuid4())} return params - async def test_create_project_without_dir( - self, - app: FastAPI, - compute_client: AsyncClient, - base_params: dict + self, app: FastAPI, compute_client: AsyncClient, base_params: dict ) -> None: response = await compute_client.post(app.url_path_for("compute:create_compute_project"), json=base_params) @@ -55,32 +47,27 @@ class TestComputeProjectRoutes: assert response.json()["project_id"] == base_params["project_id"] assert response.json()["name"] == base_params["name"] - - async def test_show_project( - self, - app: FastAPI, - compute_client: AsyncClient, - base_params: dict - ) -> None: + async def test_show_project(self, app: FastAPI, compute_client: AsyncClient, base_params: dict) -> None: response = await compute_client.post(app.url_path_for("compute:create_compute_project"), json=base_params) assert response.status_code == status.HTTP_201_CREATED - response = await compute_client.get(app.url_path_for("compute:get_compute_project", project_id=base_params["project_id"])) + response = await compute_client.get( + app.url_path_for("compute:get_compute_project", project_id=base_params["project_id"]) + ) - #print(response.json().keys()) - #assert len(response.json().keys()) == 3 + # print(response.json().keys()) + # assert len(response.json().keys()) == 3 assert response.json()["project_id"] == base_params["project_id"] assert response.json()["name"] == base_params["name"] assert response.json()["variables"] is None - async def test_show_project_invalid_uuid(self, app: FastAPI, compute_client: AsyncClient) -> None: - response = await compute_client.get(app.url_path_for("compute:get_compute_project", - project_id="50010203-0405-0607-0809-0a0b0c0d0e42")) + response = await compute_client.get( + app.url_path_for("compute:get_compute_project", project_id="50010203-0405-0607-0809-0a0b0c0d0e42") + ) assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_list_projects(self, app: FastAPI, compute_client: AsyncClient) -> dict: ProjectManager.instance()._projects = {} @@ -97,41 +84,43 @@ class TestComputeProjectRoutes: assert len(response.json()) == 2 assert "51010203-0405-0607-0809-0a0b0c0d0e0f" in [p["project_id"] for p in response.json()] - async def test_delete_project(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project) -> None: with asyncio_patch("gns3server.compute.project.Project.delete", return_value=True) as mock: - response = await compute_client.delete(app.url_path_for("compute:delete_compute_project", project_id=compute_project.id)) + response = await compute_client.delete( + app.url_path_for("compute:delete_compute_project", project_id=compute_project.id) + ) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - async def test_update_project(self, app: FastAPI, compute_client: AsyncClient, base_params: dict) -> None: response = await compute_client.post(app.url_path_for("compute:create_compute_project"), json=base_params) assert response.status_code == status.HTTP_201_CREATED params = {"variables": [{"name": "TEST1", "value": "VAL1"}]} - response = await compute_client.put(app.url_path_for("compute:update_compute_project", project_id=base_params["project_id"]), - json=params) + response = await compute_client.put( + app.url_path_for("compute:update_compute_project", project_id=base_params["project_id"]), json=params + ) assert response.status_code == status.HTTP_200_OK assert response.json()["variables"] == [{"name": "TEST1", "value": "VAL1"}] - async def test_delete_project_invalid_uuid(self, app: FastAPI, compute_client: AsyncClient) -> None: - response = await compute_client.delete(app.url_path_for("compute:delete_compute_project", project_id=str(uuid.uuid4()))) + response = await compute_client.delete( + app.url_path_for("compute:delete_compute_project", project_id=str(uuid.uuid4())) + ) assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_close_project(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project) -> None: with asyncio_patch("gns3server.compute.project.Project.close", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:close_compute_project", project_id=compute_project.id)) + response = await compute_client.post( + app.url_path_for("compute:close_compute_project", project_id=compute_project.id) + ) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - # @pytest.mark.asyncio # async def test_close_project_two_client_connected(compute_api, compute_project): # @@ -141,13 +130,13 @@ class TestComputeProjectRoutes: # assert response.status_code == status.HTTP_204_NO_CONTENT # assert not mock.called - async def test_close_project_invalid_uuid(self, app: FastAPI, compute_client: AsyncClient) -> None: - response = await compute_client.post(app.url_path_for("compute:close_compute_project", project_id=str(uuid.uuid4()))) + response = await compute_client.post( + app.url_path_for("compute:close_compute_project", project_id=str(uuid.uuid4())) + ) assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_get_file(self, app: FastAPI, compute_client: AsyncClient) -> None: project = ProjectManager.instance().create_project(project_id="01010203-0405-0607-0809-0a0b0c0d0e0b") @@ -155,79 +144,67 @@ class TestComputeProjectRoutes: with open(os.path.join(project.path, "hello"), "w+") as f: f.write("world") - response = await compute_client.get(app.url_path_for("compute:get_compute_project_file", project_id=project.id, file_path="hello")) + response = await compute_client.get( + app.url_path_for("compute:get_compute_project_file", project_id=project.id, file_path="hello") + ) assert response.status_code == status.HTTP_200_OK assert response.content == b"world" - response = await compute_client.get(app.url_path_for("compute:get_compute_project_file", project_id=project.id, file_path="false")) + response = await compute_client.get( + app.url_path_for("compute:get_compute_project_file", project_id=project.id, file_path="false") + ) assert response.status_code == status.HTTP_404_NOT_FOUND - response = await compute_client.get(app.url_path_for("compute:get_compute_project_file", - project_id=project.id, - file_path="../hello")) + response = await compute_client.get( + app.url_path_for("compute:get_compute_project_file", project_id=project.id, file_path="../hello") + ) assert response.status_code == status.HTTP_404_NOT_FOUND - - async def test_get_file_forbidden_location( - self, - app: FastAPI, - compute_client: AsyncClient, - config, - tmpdir - ) -> None: + async def test_get_file_forbidden_location(self, app: FastAPI, compute_client: AsyncClient, config, tmpdir) -> None: config.settings.Server.projects_path = str(tmpdir) project = ProjectManager.instance().create_project(project_id="01010203-0405-0607-0809-0a0b0c0d0e0b") file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd" response = await compute_client.get( - app.url_path_for( - "compute:get_compute_project_file", - project_id=project.id, - file_path=file_path - ) + app.url_path_for("compute:get_compute_project_file", project_id=project.id, file_path=file_path) ) assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_write_file(self, app: FastAPI, compute_client: AsyncClient, config, tmpdir) -> None: config.settings.Server.projects_path = str(tmpdir) project = ProjectManager.instance().create_project(project_id="01010203-0405-0607-0809-0a0b0c0d0e0b") - response = await compute_client.post(app.url_path_for("compute:write_compute_project_file", - project_id=project.id, - file_path="hello"), content=b"world") + response = await compute_client.post( + app.url_path_for("compute:write_compute_project_file", project_id=project.id, file_path="hello"), + content=b"world", + ) assert response.status_code == status.HTTP_204_NO_CONTENT with open(os.path.join(project.path, "hello")) as f: assert f.read() == "world" - response = await compute_client.post(app.url_path_for("compute:write_compute_project_file", - project_id=project.id, - file_path="../hello")) + response = await compute_client.post( + app.url_path_for("compute:write_compute_project_file", project_id=project.id, file_path="../hello") + ) assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_write_file_forbidden_location( - self, - app: FastAPI, - compute_client: AsyncClient, - config, - tmpdir + self, app: FastAPI, compute_client: AsyncClient, config, tmpdir ) -> None: config.settings.Server.projects_path = str(tmpdir) project = ProjectManager.instance().create_project(project_id="01010203-0405-0607-0809-0a0b0c0d0e0b") file_path = "%2e%2e/hello" - response = await compute_client.post(app.url_path_for("compute:write_compute_project_file", - project_id=project.id, - file_path=file_path), content=b"world") + response = await compute_client.post( + app.url_path_for("compute:write_compute_project_file", project_id=project.id, file_path=file_path), + content=b"world", + ) assert response.status_code == status.HTTP_403_FORBIDDEN class TestBatchNIOEdgeCases: - @pytest.mark.asyncio async def test_dynamips_router_dispatch_to_slot_add_nio_binding(self): """_add_nio_binding dispatches Dynamips router to slot_add_nio_binding.""" @@ -276,7 +253,7 @@ class TestBatchNIOEdgeCases: dyn = _FakeDynamips() base = _FakeBase() - assert len(inspect.signature(dyn.create_nio).parameters) == 2 # Dynamips + assert len(inspect.signature(dyn.create_nio).parameters) == 2 # Dynamips assert len(inspect.signature(base.create_nio).parameters) == 1 # standard assert inspect.iscoroutinefunction(dyn.create_nio) assert not inspect.iscoroutinefunction(base.create_nio) diff --git a/tests/api/routes/compute/test_qemu_nodes.py b/tests/api/routes/compute/test_qemu_nodes.py index 1bed9cf39..3f53d3538 100644 --- a/tests/api/routes/compute/test_qemu_nodes.py +++ b/tests/api/routes/compute/test_qemu_nodes.py @@ -33,113 +33,91 @@ pytestmark = pytest.mark.asyncio class TestQemuNodesRoutes: - @pytest.fixture def fake_qemu_bin(self, monkeypatch, tmpdir) -> str: - + monkeypatch.setenv("PATH", str(tmpdir)) bin_path = os.path.join(os.environ["PATH"], "qemu-system-x86_64") with open(bin_path, "w+") as f: f.write("1") os.chmod(bin_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) return bin_path - - + @pytest.fixture def fake_qemu_vm(self, images_dir) -> str: - + img_dir = os.path.join(images_dir, "QEMU") bin_path = os.path.join(img_dir, "linux载.img") with open(bin_path, "w+") as f: f.write("1234567") os.chmod(bin_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) return bin_path - - + @pytest.fixture def fake_qemu_img_binary(self, tmpdir): - + bin_path = str(tmpdir / "qemu-img") with open(bin_path, "w+") as f: f.write("1") os.chmod(bin_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) return bin_path - - + @pytest.fixture def base_params(self, tmpdir, fake_qemu_bin) -> dict: """Return standard parameters""" - + return {"name": "QEMU-TEST-1", "qemu_path": fake_qemu_bin} - - + @pytest_asyncio.fixture async def qemu_vm( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict ) -> None: - + response = await compute_client.post( - app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), - json=base_params + app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=base_params ) assert response.status_code == status.HTTP_201_CREATED return response.json() - - + async def test_qemu_create( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - fake_qemu_bin: str + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict, fake_qemu_bin: str ) -> None: - - response = await compute_client.post(app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=base_params) + + response = await compute_client.post( + app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=base_params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "QEMU-TEST-1" assert response.json()["project_id"] == compute_project.id assert response.json()["qemu_path"] == fake_qemu_bin assert response.json()["platform"] == "x86_64" - - + async def test_qemu_create_platform( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - fake_qemu_bin: str + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict, fake_qemu_bin: str ): - + base_params["qemu_path"] = None base_params["platform"] = "x86_64" - response = await compute_client.post(app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=base_params) + response = await compute_client.post( + app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=base_params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "QEMU-TEST-1" assert response.json()["project_id"] == compute_project.id assert response.json()["qemu_path"] == fake_qemu_bin assert response.json()["platform"] == "x86_64" - - + @pytest.mark.asyncio async def test_qemu_create_with_params( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - fake_qemu_vm: str + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict, fake_qemu_vm: str ): - + params = base_params params["ram"] = 1024 params["hda_disk_image"] = "linux载.img" - response = await compute_client.post(app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params) + response = await compute_client.post( + app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "QEMU-TEST-1" assert response.json()["project_id"] == compute_project.id @@ -147,12 +125,7 @@ class TestQemuNodesRoutes: assert response.json()["hda_disk_image"] == "linux载.img" async def test_qemu_create_discards_stale_overlay_for_replacement( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - fake_qemu_vm: str + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict, fake_qemu_vm: str ): node_id = str(uuid.uuid4()) @@ -182,12 +155,7 @@ class TestQemuNodesRoutes: assert not os.path.exists(stale_checksum) async def test_qemu_create_preserves_existing_overlay_without_reset_marker( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - fake_qemu_vm: str + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict, fake_qemu_vm: str ): node_id = str(uuid.uuid4()) @@ -210,12 +178,7 @@ class TestQemuNodesRoutes: assert os.path.exists(overlay) async def test_qemu_create_preserves_overlay_when_replacement_is_missing( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - fake_qemu_vm: str + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict, fake_qemu_vm: str ): node_id = str(uuid.uuid4()) @@ -239,12 +202,7 @@ class TestQemuNodesRoutes: assert os.path.exists(overlay) async def test_qemu_create_resets_valid_replacement_when_another_disk_is_missing( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - fake_qemu_vm: str + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict, fake_qemu_vm: str ): node_id = str(uuid.uuid4()) @@ -267,8 +225,7 @@ class TestQemuNodesRoutes: assert response.status_code == status.HTTP_409_CONFLICT assert not os.path.exists(stale_overlay) - - + @pytest.mark.parametrize( "name, status_code", ( @@ -286,21 +243,21 @@ class TestQemuNodesRoutes: ), ) async def test_qemu_create_with_invalid_name( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - base_params: dict, - name: str, - status_code: int + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + base_params: dict, + name: str, + status_code: int, ) -> None: - + base_params["name"] = name response = await compute_client.post( app.url_path_for("compute:create_qemu_node", project_id=compute_project.id), json=base_params ) assert response.status_code == status_code - + # async def test_qemu_create_with_project_file(self, app: FastAPI, # compute_client: AsyncClient, # compute_project: Project, @@ -321,16 +278,9 @@ class TestQemuNodesRoutes: # assert response.status_code == status.HTTP_201_CREATED # assert response.json()["hda_disk_image"] == "hello.img" # assert response.json()["hda_disk_image_md5sum"] == "7d793037a0760186574b0282f2f435e7" - - - async def test_qemu_get( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - qemu_vm: dict - ): - + + async def test_qemu_get(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, qemu_vm: dict): + response = await compute_client.get( app.url_path_for("compute:get_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]) ) @@ -338,151 +288,130 @@ class TestQemuNodesRoutes: assert response.json()["name"] == "QEMU-TEST-1" assert response.json()["project_id"] == compute_project.id assert response.json()["node_directory"] == os.path.join( - compute_project.path, - "project-files", - "qemu", - qemu_vm["node_id"] + compute_project.path, "project-files", "qemu", qemu_vm["node_id"] ) - - + async def test_qemu_start(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict) -> None: - + with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM.start", return_value=True) as mock: response = await compute_client.post( - app.url_path_for("compute:start_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]) + app.url_path_for( + "compute:start_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"] + ) ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_stop(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict) -> None: - + with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM.stop", return_value=True) as mock: response = await compute_client.post( app.url_path_for("compute:stop_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]) ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_reload(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict) -> None: - + with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM.reload", return_value=True) as mock: response = await compute_client.post( - app.url_path_for("compute:reload_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]) + app.url_path_for( + "compute:reload_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"] + ) ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_suspend(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict) -> None: - + with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM.suspend", return_value=True) as mock: response = await compute_client.post( - app.url_path_for("compute:suspend_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]) + app.url_path_for( + "compute:suspend_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"] + ) ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_resume(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict) -> None: - + with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM.resume", return_value=True) as mock: response = await compute_client.post( - app.url_path_for("compute:resume_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]) + app.url_path_for( + "compute:resume_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"] + ) ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_delete(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict) -> None: - + with asyncio_patch("gns3server.compute.qemu.Qemu.delete_node", return_value=True) as mock: response = await compute_client.delete( - app.url_path_for("compute:delete_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]) + app.url_path_for( + "compute:delete_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"] + ) ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_update( - self, - app: FastAPI, - compute_client: AsyncClient, - qemu_vm: dict, - free_console_port: int, - fake_qemu_vm: str + self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict, free_console_port: int, fake_qemu_vm: str ) -> None: - - params = { - "name": "test", - "console": free_console_port, - "ram": 1024, - "hdb_disk_image": "linux载.img" - } - + + params = {"name": "test", "console": free_console_port, "ram": 1024, "hdb_disk_image": "linux载.img"} + response = await compute_client.put( app.url_path_for("compute:update_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]), - json=params + json=params, ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test" assert response.json()["console"] == free_console_port assert response.json()["hdb_disk_image"] == "linux载.img" assert response.json()["ram"] == 1024 - - + async def test_qemu_nio_create_udp(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict) -> None: - - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1" - } - + + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} + with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM.add_ubridge_udp_connection"): await compute_client.put( - app.url_path_for("compute:update_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]), - json={"adapters": 2} + app.url_path_for( + "compute:update_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"] + ), + json={"adapters": 2}, ) - + url = app.url_path_for( "compute:create_qemu_node_nio", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], adapter_number="1", - port_number="0" + port_number="0", ) response = await compute_client.post(url, json=params) - + assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" - - + async def test_qemu_nio_update_udp(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict) -> None: - - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1", - "filters": {"packet_loss": 10} - } - + + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1", "filters": {"packet_loss": 10}} + await compute_client.put( app.url_path_for("compute:update_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]), - json={"adapters": 2} + json={"adapters": 2}, ) - + url = app.url_path_for( "compute:create_qemu_node_nio", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], adapter_number="1", - port_number="0" + port_number="0", ) - + response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["filters"] == {"packet_loss": 10} @@ -493,127 +422,125 @@ class TestQemuNodesRoutes: project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], adapter_number="1", - port_number="0" + port_number="0", ) response = await compute_client.put(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" assert response.json()["filters"] == {} - - + async def test_qemu_delete_nio(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict) -> None: - - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1" - } - + + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} + with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM._ubridge_send"): await compute_client.put( - app.url_path_for("compute:update_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]), - json={"adapters": 2} + app.url_path_for( + "compute:update_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"] + ), + json={"adapters": 2}, ) - + url = app.url_path_for( "compute:create_qemu_node_nio", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], adapter_number="1", - port_number="0" + port_number="0", ) await compute_client.post(url, json=params) - + url = app.url_path_for( "compute:delete_qemu_node_nio", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], adapter_number="1", - port_number="0" + port_number="0", ) response = await compute_client.delete(url) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_images(self, app: FastAPI, compute_client: AsyncClient, fake_qemu_vm) -> None: - + response = await compute_client.get(app.url_path_for("compute:get_qemu_images")) assert response.status_code == status.HTTP_200_OK print(response.json()) - assert {"filename": "linux载.img", "path": "linux载.img", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7} in response.json() - - + assert { + "filename": "linux载.img", + "path": "linux载.img", + "md5sum": "fcea920f7412b5da7be0cf42b8c93759", + "filesize": 7, + } in response.json() + async def test_upload_image(self, app: FastAPI, compute_client: AsyncClient, tmpdir: str) -> None: - + with patch("gns3server.compute.Qemu.get_images_directory", return_value=str(tmpdir)): - - response = await compute_client.post(app.url_path_for("compute:upload_qemu_image", - filename="test2使"), content=b"TEST") + response = await compute_client.post( + app.url_path_for("compute:upload_qemu_image", filename="test2使"), content=b"TEST" + ) assert response.status_code == status.HTTP_204_NO_CONTENT - + with open(str(tmpdir / "test2使")) as f: assert f.read() == "TEST" - + with open(str(tmpdir / "test2使.md5sum")) as f: checksum = f.read() assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf" - - - async def test_upload_image_ova(self, app: FastAPI, compute_client: AsyncClient, tmpdir:str) -> None: - + + async def test_upload_image_ova(self, app: FastAPI, compute_client: AsyncClient, tmpdir: str) -> None: + with patch("gns3server.compute.Qemu.get_images_directory", return_value=str(tmpdir)): - - response = await compute_client.post(app.url_path_for("compute:upload_qemu_image", - filename="test2.ova/test2.vmdk"), content=b"TEST") + response = await compute_client.post( + app.url_path_for("compute:upload_qemu_image", filename="test2.ova/test2.vmdk"), content=b"TEST" + ) assert response.status_code == status.HTTP_204_NO_CONTENT - + with open(str(tmpdir / "test2.ova" / "test2.vmdk")) as f: assert f.read() == "TEST" - + with open(str(tmpdir / "test2.ova" / "test2.vmdk.md5sum")) as f: checksum = f.read() assert checksum == "033bd94b1168d7e4f0d644c3c95e35bf" - - + async def test_upload_image_forbidden_location( - self, app: FastAPI, - compute_client: AsyncClient, - tmpdir: str + self, app: FastAPI, compute_client: AsyncClient, tmpdir: str ) -> None: - - response = await compute_client.post(app.url_path_for("compute:upload_qemu_image", - filename="/qemu/images/../../test2"), content=b"TEST") + + response = await compute_client.post( + app.url_path_for("compute:upload_qemu_image", filename="/qemu/images/../../test2"), content=b"TEST" + ) assert response.status_code == status.HTTP_403_FORBIDDEN - - + async def test_download_image(self, app: FastAPI, compute_client: AsyncClient, images_dir: str) -> None: - - response = await compute_client.post(app.url_path_for("compute:upload_qemu_image", filename="test3"), content=b"TEST") + + response = await compute_client.post( + app.url_path_for("compute:upload_qemu_image", filename="test3"), content=b"TEST" + ) assert response.status_code == status.HTTP_204_NO_CONTENT - + response = await compute_client.get(app.url_path_for("compute:download_qemu_image", filename="test3")) assert response.status_code == status.HTTP_200_OK - - + async def test_download_image_forbidden_location(self, app: FastAPI, compute_client: AsyncClient, tmpdir) -> None: - + file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd" response = await compute_client.get(app.url_path_for("compute:download_qemu_image", filename=file_path)) assert response.status_code == status.HTTP_403_FORBIDDEN - - + @pytest.mark.skipif(os.getuid() == 0, reason="Root can delete any image") - async def test_upload_image_permission_denied(self, app: FastAPI, compute_client: AsyncClient, images_dir: str) -> None: - + async def test_upload_image_permission_denied( + self, app: FastAPI, compute_client: AsyncClient, images_dir: str + ) -> None: + with open(os.path.join(images_dir, "QEMU", "test2.tmp"), "w+") as f: f.write("") os.chmod(os.path.join(images_dir, "QEMU", "test2.tmp"), 0) - - response = await compute_client.post(app.url_path_for("compute:upload_qemu_image", filename="test2"), content=b"TEST") + + response = await compute_client.post( + app.url_path_for("compute:upload_qemu_image", filename="test2"), content=b"TEST" + ) assert response.status_code == status.HTTP_409_CONFLICT - - + # @pytest.mark.asyncio # async def test_create_img_relative(self, app: FastAPI, compute_client: AsyncClient): # @@ -665,69 +592,62 @@ class TestQemuNodesRoutes: # with asyncio_patch("gns3server.compute.Qemu.create_disk"): # response = await compute_client.post(app.url_path_for("compute:create_qemu_image"), json=params) # assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_capabilities(self, app: FastAPI, compute_client: AsyncClient) -> None: - + with asyncio_patch("gns3server.compute.Qemu.get_kvm_archs", return_value=["x86_64"]): response = await compute_client.get(app.url_path_for("compute:get_qemu_capabilities")) assert response.json()["kvm"] == ["x86_64"] - - + async def test_qemu_duplicate( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - qemu_vm: dict, - base_params: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, qemu_vm: dict, base_params: dict ) -> None: - + # create destination node first response = await compute_client.post( - app.url_path_for("compute:create_qemu_node", project_id=qemu_vm["project_id"]), - json=base_params + app.url_path_for("compute:create_qemu_node", project_id=qemu_vm["project_id"]), json=base_params ) - + assert response.status_code == status.HTTP_201_CREATED params = {"destination_node_id": response.json()["node_id"]} response = await compute_client.post( - app.url_path_for("compute:duplicate_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"]), - json=params + app.url_path_for( + "compute:duplicate_qemu_node", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"] + ), + json=params, ) assert response.status_code == status.HTTP_201_CREATED - - + async def test_qemu_create_disk_image( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - fake_qemu_img_binary: str, - qemu_vm: dict, + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + fake_qemu_img_binary: str, + qemu_vm: dict, ): - + options = { "format": "qcow2", "preallocation": "metadata", "cluster_size": 64, "refcount_bits": 12, "lazy_refcounts": "off", - "size": 30 + "size": 30, } - + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as qemu_img: response = await compute_client.post( app.url_path_for( "compute:create_qemu_disk_image", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], - disk_name="disk.qcow2" + disk_name="disk.qcow2", ), - json=options + json=options, ) assert response.status_code == status.HTTP_204_NO_CONTENT - + args, kwargs = qemu_img.call_args assert args == ( fake_qemu_img_binary, @@ -743,39 +663,34 @@ class TestQemuNodesRoutes: "-o", "refcount_bits=12", os.path.join(qemu_vm["node_directory"], "disk.qcow2"), - "30M" + "30M", ) - - + async def test_qemu_create_disk_image_already_exists( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - fake_qemu_img_binary: str, - qemu_vm: dict, + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + fake_qemu_img_binary: str, + qemu_vm: dict, ): - + node = compute_project.get_node(qemu_vm["node_id"]) shutil.copy("tests/resources/empty8G.qcow2", os.path.join(node.working_dir, "disk.qcow2")) - - options = { - "format": "qcow2", - "size": 100 - } - + + options = {"format": "qcow2", "size": 100} + response = await compute_client.post( app.url_path_for( "compute:create_qemu_disk_image", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], - disk_name="disk.qcow2" + disk_name="disk.qcow2", ), - json=options + json=options, ) assert response.status_code == status.HTTP_409_CONFLICT - - + # async def test_qemu_create_disk_image_with_not_supported_characters_by_filesystem( # app: FastAPI, # compute_client: AsyncClient, @@ -803,125 +718,116 @@ class TestQemuNodesRoutes: # json=options # ) # assert response.status_code == status.HTTP_409_CONFLICT - - + async def test_qemu_update_disk_image( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - fake_qemu_img_binary: str, - qemu_vm: dict, + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + fake_qemu_img_binary: str, + qemu_vm: dict, ) -> None: - + node = compute_project.get_node(qemu_vm["node_id"]) shutil.copy("tests/resources/empty8G.qcow2", os.path.join(node.working_dir, "disk.qcow2")) - + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as qemu_img: response = await compute_client.put( app.url_path_for( "compute:update_qemu_disk_image", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], - disk_name="disk.qcow2" + disk_name="disk.qcow2", ), - json={"extend": 10} + json={"extend": 10}, ) assert response.status_code == status.HTTP_204_NO_CONTENT - + assert qemu_img.called args, kwargs = qemu_img.call_args assert args == ( fake_qemu_img_binary, "resize", os.path.join(qemu_vm["node_directory"], "disk.qcow2"), - "+10M" + "+10M", ) - - + async def test_qemu_delete_disk_image( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - qemu_vm: dict, + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + qemu_vm: dict, ) -> None: - + node = compute_project.get_node(qemu_vm["node_id"]) shutil.copy("tests/resources/empty8G.qcow2", os.path.join(node.working_dir, "disk.qcow2")) - + response = await compute_client.delete( app.url_path_for( "compute:delete_qemu_disk_image", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], - disk_name="disk.qcow2" + disk_name="disk.qcow2", ) ) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_delete_disk_image_missing_image( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - qemu_vm: dict, + self, + app: FastAPI, + compute_client: AsyncClient, + compute_project: Project, + qemu_vm: dict, ) -> None: - + response = await compute_client.delete( app.url_path_for( "compute:delete_qemu_disk_image", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], - disk_name="unknown_image.qcow2" + disk_name="unknown_image.qcow2", ) ) assert response.status_code == status.HTTP_409_CONFLICT - - + @pytest.mark.asyncio async def test_qemu_start_capture(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict): - - params = { - "capture_file_name": "test.pcap", - "data_link_type": "DLT_EN10MB" - } - + + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} + url = app.url_path_for( "compute:start_qemu_node_capture", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) - + with patch("gns3server.compute.qemu.qemu_vm.QemuVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM.start_capture") as mock: response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_200_OK assert mock.called assert "test.pcap" in response.json()["pcap_file_path"] - - + @pytest.mark.asyncio async def test_qemu_stop_capture(self, app: FastAPI, compute_client: AsyncClient, qemu_vm: dict): - + url = app.url_path_for( "compute:stop_qemu_node_capture", project_id=qemu_vm["project_id"], node_id=qemu_vm["node_id"], adapter_number="0", - port_number="0" + port_number="0", ) - + with patch("gns3server.compute.qemu.qemu_vm.QemuVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM.stop_capture") as mock: response = await compute_client.post(url) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - - + # @pytest.mark.asyncio # async def test_qemu_pcap(self, app: FastAPI, compute_client: AsyncClient, vm, compute_project): # diff --git a/tests/api/routes/compute/test_virtualbox_nodes.py b/tests/api/routes/compute/test_virtualbox_nodes.py index e29f104dc..e50420518 100644 --- a/tests/api/routes/compute/test_virtualbox_nodes.py +++ b/tests/api/routes/compute/test_virtualbox_nodes.py @@ -29,129 +29,119 @@ pytestmark = pytest.mark.asyncio class TestVirtualBoxNodesRoutes: - @pytest_asyncio.fixture async def vm(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project) -> None: - + vboxmanage_path = "/fake/VboxManage" - params = { - "name": "VMTEST", - "vmname": "VMTEST", - "linked_clone": False - } - - with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:create_virtualbox_node", project_id=compute_project.id), - json=params) + params = {"name": "VMTEST", "vmname": "VMTEST", "linked_clone": False} + + with asyncio_patch( + "gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True + ) as mock: + response = await compute_client.post( + app.url_path_for("compute:create_virtualbox_node", project_id=compute_project.id), json=params + ) assert mock.called assert response.status_code == status.HTTP_201_CREATED - + with patch("gns3server.compute.virtualbox.VirtualBox.find_vboxmanage", return_value=vboxmanage_path): return response.json() - - + async def test_vbox_create(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project) -> None: - - params = { - "name": "VM1", - "vmname": "VM1", - "linked_clone": False - } - + + params = {"name": "VM1", "vmname": "VM1", "linked_clone": False} + with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.create", return_value=True): - response = await compute_client.post(app.url_path_for("compute:create_virtualbox_node", project_id=compute_project.id), - json=params) + response = await compute_client.post( + app.url_path_for("compute:create_virtualbox_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "VM1" assert response.json()["project_id"] == compute_project.id - - - async def test_vbox_get(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict) -> None: - - response = await compute_client.get(app.url_path_for("compute:get_virtualbox_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + + async def test_vbox_get( + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict + ) -> None: + + response = await compute_client.get( + app.url_path_for("compute:get_virtualbox_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "VMTEST" assert response.json()["project_id"] == compute_project.id - - + async def test_vbox_start(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - + with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.start", return_value=True) as mock: - - response = await compute_client.post(app.url_path_for("compute:start_virtualbox_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:start_virtualbox_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vbox_stop(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - + with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.stop", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:stop_virtualbox_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:stop_virtualbox_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vbox_suspend(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.suspend", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:suspend_virtualbox_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + + with asyncio_patch( + "gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.suspend", return_value=True + ) as mock: + response = await compute_client.post( + app.url_path_for("compute:suspend_virtualbox_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vbox_resume(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.resume", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:resume_virtualbox_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + + with asyncio_patch( + "gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.resume", return_value=True + ) as mock: + response = await compute_client.post( + app.url_path_for("compute:resume_virtualbox_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vbox_reload(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.reload", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:reload_virtualbox_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + + with asyncio_patch( + "gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.reload", return_value=True + ) as mock: + response = await compute_client.post( + app.url_path_for("compute:reload_virtualbox_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vbox_nio_create_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1" - } - - url = app.url_path_for("compute:create_virtualbox_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - - with asyncio_patch('gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_add_nio_binding') as mock: + + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} + + url = app.url_path_for( + "compute:create_virtualbox_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + + with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_add_nio_binding") as mock: response = await compute_client.post(url, json=params) assert mock.called args, kwgars = mock.call_args assert args[0] == 0 - + assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" - - + # @pytest.mark.asyncio # async def test_vbox_nio_update_udp(self, app: FastAPI, compute_client: AsyncClient, vm): # @@ -169,78 +159,76 @@ class TestVirtualBoxNodesRoutes: # # assert response.status_code == status.HTTP_201_CREATED # assert response.json()["type"] == "nio_udp" - - + async def test_vbox_delete_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - url = app.url_path_for("compute:delete_virtualbox_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - - with asyncio_patch('gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_remove_nio_binding') as mock: + + url = app.url_path_for( + "compute:delete_virtualbox_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + + with asyncio_patch( + "gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.adapter_remove_nio_binding" + ) as mock: response = await compute_client.delete(url) assert mock.called args, kwgars = mock.call_args assert args[0] == 0 assert response.status_code == status.HTTP_204_NO_CONTENT - - + @pytest.mark.asyncio async def test_vbox_update(self, app: FastAPI, compute_client: AsyncClient, vm, free_console_port): - - params = { - "name": "test", - "console": free_console_port - } - - response = await compute_client.put(app.url_path_for("compute:update_virtualbox_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json=params) + + params = {"name": "test", "console": free_console_port} + + response = await compute_client.put( + app.url_path_for("compute:update_virtualbox_node", project_id=vm["project_id"], node_id=vm["node_id"]), + json=params, + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test" assert response.json()["console"] == free_console_port - - + @pytest.mark.asyncio async def test_virtualbox_start_capture(self, app: FastAPI, compute_client: AsyncClient, vm): - - params = { - "capture_file_name": "test.pcap", - "data_link_type": "DLT_EN10MB" - } - - url = app.url_path_for("compute:start_virtualbox_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - + + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} + + url = app.url_path_for( + "compute:start_virtualbox_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + with patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.start_capture") as mock: response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_200_OK assert mock.called assert "test.pcap" in response.json()["pcap_file_path"] - - + @pytest.mark.asyncio async def test_virtualbox_stop_capture(self, app: FastAPI, compute_client: AsyncClient, vm): - - url = app.url_path_for("compute:stop_virtualbox_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - + + url = app.url_path_for( + "compute:stop_virtualbox_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + with patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.virtualbox.virtualbox_vm.VirtualBoxVM.stop_capture") as mock: response = await compute_client.post(url) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - - + # @pytest.mark.asyncio # async def test_virtualbox_pcap(self, app: FastAPI, compute_client: AsyncClient, vm, compute_project): # diff --git a/tests/api/routes/compute/test_vmware_nodes.py b/tests/api/routes/compute/test_vmware_nodes.py index ff875ab28..6b42eb61a 100644 --- a/tests/api/routes/compute/test_vmware_nodes.py +++ b/tests/api/routes/compute/test_vmware_nodes.py @@ -29,147 +29,121 @@ pytestmark = pytest.mark.asyncio class TestVMwareNodesRoutes: - @pytest_asyncio.fixture async def vm(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vmx_path: str) -> dict: - - params = { - "name": "VMTEST", - "vmx_path": vmx_path, - "linked_clone": False - } - + + params = {"name": "VMTEST", "vmx_path": vmx_path, "linked_clone": False} + with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.create", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:create_vmware_node", project_id=compute_project.id), - json=params) + response = await compute_client.post( + app.url_path_for("compute:create_vmware_node", project_id=compute_project.id), json=params + ) assert mock.called assert response.status_code == status.HTTP_201_CREATED return response.json() - @pytest.fixture def vmx_path(self, tmpdir: str) -> str: """ Return a fake VMX file """ - + path = str(tmpdir / "test.vmx") - with open(path, 'w+') as f: + with open(path, "w+") as f: f.write("1") return path - - + async def test_vmware_create( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vmx_path: str + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vmx_path: str ) -> None: - - params = { - "name": "VM1", - "vmx_path": vmx_path, - "linked_clone": False - } - + + params = {"name": "VM1", "vmx_path": vmx_path, "linked_clone": False} + with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.create", return_value=True): - response = await compute_client.post(app.url_path_for("compute:create_vmware_node", project_id=compute_project.id), - json=params) + response = await compute_client.post( + app.url_path_for("compute:create_vmware_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "VM1" assert response.json()["project_id"] == compute_project.id - - + async def test_vmware_get( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict ) -> None: - - response = await compute_client.get(app.url_path_for("compute:get_vmware_node", project_id=vm["project_id"], node_id=vm["node_id"])) + + response = await compute_client.get( + app.url_path_for("compute:get_vmware_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "VMTEST" assert response.json()["project_id"] == compute_project.id - - + async def test_vmware_start(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - + with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.start", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:start_vmware_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:start_vmware_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vmware_stop(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - + with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.stop", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:stop_vmware_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:stop_vmware_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vmware_suspend(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - + with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.suspend", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:suspend_vmware_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:suspend_vmware_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vmware_resume(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - + with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.resume", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:resume_vmware_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:resume_vmware_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vmware_reload(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - + with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.reload", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:reload_vmware_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:reload_vmware_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vmware_nio_create_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1" - } - - url = app.url_path_for("compute:create_vmware_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - - with asyncio_patch('gns3server.compute.vmware.vmware_vm.VMwareVM.adapter_add_nio_binding') as mock: + + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} + + url = app.url_path_for( + "compute:create_vmware_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + + with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.adapter_add_nio_binding") as mock: response = await compute_client.post(url, json=params) assert mock.called args, kwgars = mock.call_args assert args[0] == 0 - + assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" - - + # @pytest.mark.asyncio # async def test_vmware_nio_update_udp(self, app: FastAPI, compute_client: AsyncClient, vm): # @@ -187,83 +161,74 @@ class TestVMwareNodesRoutes: # response = await compute_client.put("/projects/{project_id}/vmware/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]), params) # assert response.status_code == status.HTTP_201_CREATED # assert response.json()["type"] == "nio_udp" - - + async def test_vmware_delete_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - url = app.url_path_for("compute:delete_vmware_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - - with asyncio_patch('gns3server.compute.vmware.vmware_vm.VMwareVM.adapter_remove_nio_binding') as mock: + + url = app.url_path_for( + "compute:delete_vmware_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + + with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.adapter_remove_nio_binding") as mock: response = await compute_client.delete(url) assert mock.called args, kwgars = mock.call_args assert args[0] == 0 - + assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vmware_update( - self, - app: FastAPI, - compute_client: AsyncClient, - vm: dict, - free_console_port: int + self, app: FastAPI, compute_client: AsyncClient, vm: dict, free_console_port: int ) -> None: - - params = { - "name": "test", - "console": free_console_port - } - - response = await compute_client.put(app.url_path_for("compute:update_vmware_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json=params) + + params = {"name": "test", "console": free_console_port} + + response = await compute_client.put( + app.url_path_for("compute:update_vmware_node", project_id=vm["project_id"], node_id=vm["node_id"]), + json=params, + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test" assert response.json()["console"] == free_console_port - - + async def test_vmware_start_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - params = { - "capture_file_name": "test.pcap", - "data_link_type": "DLT_EN10MB" - } - - url = app.url_path_for("compute:start_vmware_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - + + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} + + url = app.url_path_for( + "compute:start_vmware_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + with patch("gns3server.compute.vmware.vmware_vm.VMwareVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.start_capture") as mock: - response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_200_OK assert mock.called assert "test.pcap" in response.json()["pcap_file_path"] - - + async def test_vmware_stop_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - url = app.url_path_for("compute:stop_vmware_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - + + url = app.url_path_for( + "compute:stop_vmware_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + with patch("gns3server.compute.vmware.vmware_vm.VMwareVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.vmware.vmware_vm.VMwareVM.stop_capture") as mock: response = await compute_client.post(url) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - - + # @pytest.mark.asyncio # async def test_vmware_pcap(self, app: FastAPI, compute_client: AsyncClient, vm, compute_project): # diff --git a/tests/api/routes/compute/test_vpcs_nodes.py b/tests/api/routes/compute/test_vpcs_nodes.py index 7de416690..5b9073332 100644 --- a/tests/api/routes/compute/test_vpcs_nodes.py +++ b/tests/api/routes/compute/test_vpcs_nodes.py @@ -29,288 +29,247 @@ pytestmark = pytest.mark.asyncio class TestVPCSNodesRoutes: - @pytest_asyncio.fixture async def vm(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project) -> None: - + params = {"name": "PC TEST 1"} - response = await compute_client.post(app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params) + response = await compute_client.post( + app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED return response.json() - - + async def test_vpcs_create(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project) -> None: - + params = {"name": "PC TEST 1"} - response = await compute_client.post(app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params) + response = await compute_client.post( + app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "PC TEST 1" assert response.json()["project_id"] == compute_project.id - - + async def test_vpcs_get( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict ) -> None: - - response = await compute_client.get(app.url_path_for("compute:get_vpcs_node", project_id=vm["project_id"], node_id=vm["node_id"])) + + response = await compute_client.get( + app.url_path_for("compute:get_vpcs_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "PC TEST 1" assert response.json()["project_id"] == compute_project.id assert response.json()["status"] == "stopped" - - + async def test_vpcs_create_startup_script( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project ) -> None: - - params = { - "name": "PC TEST 1", - "startup_script": "ip 192.168.1.2\necho TEST" - } - - response = await compute_client.post(app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params) + + params = {"name": "PC TEST 1", "startup_script": "ip 192.168.1.2\necho TEST"} + + response = await compute_client.post( + app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "PC TEST 1" assert response.json()["project_id"] == compute_project.id - - + async def test_vpcs_create_port( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - free_console_port: int + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, free_console_port: int ) -> None: - - params = { - "name": "PC TEST 1", - "console": free_console_port - } - - response = await compute_client.post(app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params) + + params = {"name": "PC TEST 1", "console": free_console_port} + + response = await compute_client.post( + app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "PC TEST 1" assert response.json()["project_id"] == compute_project.id assert response.json()["console"] == free_console_port - - + async def test_vpcs_nio_create_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1" - } - - url = app.url_path_for("compute:create_vpcs_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - + + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} + + url = app.url_path_for( + "compute:create_vpcs_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.add_ubridge_udp_connection"): response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" - - + async def test_vpcs_nio_update_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1", - "filters": {"packet_loss": 10} - } - - url = app.url_path_for("compute:create_vpcs_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - + + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1", "filters": {"packet_loss": 10}} + + url = app.url_path_for( + "compute:create_vpcs_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.add_ubridge_udp_connection"): response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["filters"] == {"packet_loss": 10} params["filters"].clear() - url = app.url_path_for("compute:update_vpcs_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:update_vpcs_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) response = await compute_client.put(url, json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" assert response.json()["filters"] == {} - + async def test_vpcs_delete_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1" - } - + + params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} + with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM._ubridge_send"): - url = app.url_path_for("compute:create_vpcs_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + url = app.url_path_for( + "compute:create_vpcs_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) await compute_client.post(url, json=params) - - url = app.url_path_for("compute:delete_vpcs_node_nio", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") + + url = app.url_path_for( + "compute:delete_vpcs_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) response = await compute_client.delete(url) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_vpcs_start(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - + with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.start", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:start_vpcs_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) - assert mock.called - assert response.status_code == status.HTTP_204_NO_CONTENT - - - async def test_vpcs_stop(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.stop", return_value=True) as mock: - - response = await compute_client.post(app.url_path_for("compute:stop_vpcs_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) - assert mock.called - assert response.status_code == status.HTTP_204_NO_CONTENT - - - async def test_vpcs_reload(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.reload", return_value=True) as mock: - response = await compute_client.post(app.url_path_for("compute:reload_vpcs_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) - assert mock.called - assert response.status_code == status.HTTP_204_NO_CONTENT - - - async def test_vpcs_delete(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - with asyncio_patch("gns3server.compute.vpcs.VPCS.delete_node", return_value=True) as mock: - response = await compute_client.delete(app.url_path_for("compute:delete_vpcs_node", - project_id=vm["project_id"], - node_id=vm["node_id"])) + response = await compute_client.post( + app.url_path_for("compute:start_vpcs_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT + async def test_vpcs_stop(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: + + with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.stop", return_value=True) as mock: + response = await compute_client.post( + app.url_path_for("compute:stop_vpcs_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) + assert mock.called + assert response.status_code == status.HTTP_204_NO_CONTENT + + async def test_vpcs_reload(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: + + with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.reload", return_value=True) as mock: + response = await compute_client.post( + app.url_path_for("compute:reload_vpcs_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) + assert mock.called + assert response.status_code == status.HTTP_204_NO_CONTENT + + async def test_vpcs_delete(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: + + with asyncio_patch("gns3server.compute.vpcs.VPCS.delete_node", return_value=True) as mock: + response = await compute_client.delete( + app.url_path_for("compute:delete_vpcs_node", project_id=vm["project_id"], node_id=vm["node_id"]) + ) + assert mock.called + assert response.status_code == status.HTTP_204_NO_CONTENT async def test_vpcs_suspend(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: response = await compute_client.post( - app.url_path_for( - "compute:suspend_vpcs_node", - project_id=vm["project_id"], - node_id=vm["node_id"] - ) + app.url_path_for("compute:suspend_vpcs_node", project_id=vm["project_id"], node_id=vm["node_id"]) ) assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED - async def test_vpcs_duplicate( - self, - app: FastAPI, - compute_client: AsyncClient, - compute_project: Project, - vm: dict + self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, vm: dict ) -> None: - + # create destination node first params = {"name": "PC TEST 1"} - response = await compute_client.post(app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params) + response = await compute_client.post( + app.url_path_for("compute:create_vpcs_node", project_id=compute_project.id), json=params + ) assert response.status_code == status.HTTP_201_CREATED - + params = {"destination_node_id": response.json()["node_id"]} - response = await compute_client.post(app.url_path_for("compute:duplicate_vpcs_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json=params) + response = await compute_client.post( + app.url_path_for("compute:duplicate_vpcs_node", project_id=vm["project_id"], node_id=vm["node_id"]), + json=params, + ) assert response.status_code == status.HTTP_201_CREATED - - + async def test_vpcs_update( - self, - app: FastAPI, - compute_client: AsyncClient, - vm: dict, - free_console_port: int + self, app: FastAPI, compute_client: AsyncClient, vm: dict, free_console_port: int ) -> None: - + console_port = free_console_port - params = { - "name": "test", - "console": console_port - } - - response = await compute_client.put(app.url_path_for("compute:update_vpcs_node", - project_id=vm["project_id"], - node_id=vm["node_id"]), json=params) + params = {"name": "test", "console": console_port} + + response = await compute_client.put( + app.url_path_for("compute:update_vpcs_node", project_id=vm["project_id"], node_id=vm["node_id"]), + json=params, + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test" assert response.json()["console"] == console_port - - + async def test_vpcs_start_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - params = { - "capture_file_name": "test.pcap", - "data_link_type": "DLT_EN10MB" - } - - url = app.url_path_for("compute:start_vpcs_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - + + params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"} + + url = app.url_path_for( + "compute:start_vpcs_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + with patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.start_capture") as mock: response = await compute_client.post(url, json=params) assert response.status_code == status.HTTP_200_OK assert mock.called assert "test.pcap" in response.json()["pcap_file_path"] - - + async def test_vpcs_stop_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: - - url = app.url_path_for("compute:stop_vpcs_node_capture", - project_id=vm["project_id"], - node_id=vm["node_id"], - adapter_number="0", - port_number="0") - + + url = app.url_path_for( + "compute:stop_vpcs_node_capture", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + with patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.is_running", return_value=True): with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.stop_capture") as mock: response = await compute_client.post(url) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - - + # @pytest.mark.asyncio # async def test_vpcs_pcap(self, app: FastAPI, compute_client: AsyncClient, vm, compute_project: Project): # diff --git a/tests/api/routes/controller/test_access_tickets.py b/tests/api/routes/controller/test_access_tickets.py index fd5f451a6..b70f135b5 100644 --- a/tests/api/routes/controller/test_access_tickets.py +++ b/tests/api/routes/controller/test_access_tickets.py @@ -172,49 +172,38 @@ class TestAccessTicketWebSocketAuth: monkeypatch.setattr( HTTPClient, "get_client", - classmethod(lambda cls: SimpleNamespace(ws_connect=lambda *args, **kwargs: compute_ws)) + classmethod(lambda cls: SimpleNamespace(ws_connect=lambda *args, **kwargs: compute_ws)), ) return compute_ws async def test_console_ws_accepts_valid_ticket( - self, - app: FastAPI, - base_client: AsyncClient, - compute_credentials, - project: Project, - node: Node, - monkeypatch + self, app: FastAPI, base_client: AsyncClient, compute_credentials, project: Project, node: Node, monkeypatch ) -> None: # admin is the seeded superadmin, so the RBAC privilege check is skipped ticket = access_ticket_service.mint("admin", 0, project_id=project.id, node_id=node.id) - self._forward_compute_ws(monkeypatch, [ - aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "device output", None), - ]) + self._forward_compute_ws( + monkeypatch, + [ + aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "device output", None), + ], + ) async with self._ws_client(app, base_client) as client: async with aconnect_ws( - f"/v3/projects/{project.id}/nodes/{node.id}/console/ws", - client, - params={"token": ticket} + f"/v3/projects/{project.id}/nodes/{node.id}/console/ws", client, params={"token": ticket} ) as ws: # reaching the compute forwarding loop means ticket auth succeeded assert await ws.receive_text() == "device output" async def test_console_ws_rejects_ticket_bound_to_other_node( - self, - app: FastAPI, - base_client: AsyncClient, - project: Project, - node: Node + self, app: FastAPI, base_client: AsyncClient, project: Project, node: Node ) -> None: other_node = "00000000-0000-0000-0000-000000000000" ticket = access_ticket_service.mint("admin", 0, project_id=project.id, node_id=other_node) async with self._ws_client(app, base_client) as client: async with aconnect_ws( - f"/v3/projects/{project.id}/nodes/{node.id}/console/ws", - client, - params={"token": ticket} + f"/v3/projects/{project.id}/nodes/{node.id}/console/ws", client, params={"token": ticket} ) as ws: notification = await ws.receive_json() assert notification["event"]["message"] == ( @@ -224,28 +213,18 @@ class TestAccessTicketWebSocketAuth: ) async def test_console_ws_rejects_ticket_with_stale_token_version( - self, - app: FastAPI, - base_client: AsyncClient, - project: Project, - node: Node + self, app: FastAPI, base_client: AsyncClient, project: Project, node: Node ) -> None: # logging out bumps the user's token_version: outstanding tickets must die with it ticket = access_ticket_service.mint("admin", 999, project_id=project.id, node_id=node.id) async with self._ws_client(app, base_client) as client: async with aconnect_ws( - f"/v3/projects/{project.id}/nodes/{node.id}/console/ws", - client, - params={"token": ticket} + f"/v3/projects/{project.id}/nodes/{node.id}/console/ws", client, params={"token": ticket} ) as ws: notification = await ws.receive_json() assert "Token has been revoked for 'admin'" in notification["event"]["message"] - async def test_ticket_rejected_on_non_console_websocket( - self, - app: FastAPI, - base_client: AsyncClient - ) -> None: + async def test_ticket_rejected_on_non_console_websocket(self, app: FastAPI, base_client: AsyncClient) -> None: # the controller notification stream shares the WS auth dependency but has # no node binding: a console ticket must not authenticate it ticket = access_ticket_service.mint("admin", 0, project_id="p1", node_id="n1") @@ -263,42 +242,26 @@ class TestAccessTicketRestAuth: pytestmark = pytest.mark.asyncio - async def test_rest_accepts_path_bound_ticket( - self, - app: FastAPI, - base_client: AsyncClient - ) -> None: + async def test_rest_accepts_path_bound_ticket(self, app: FastAPI, base_client: AsyncClient) -> None: # base_client carries no Authorization header, so the ?token= parameter is used ticket = access_ticket_service.mint("admin", 0, path="/v3/projects") response = await base_client.get("/v3/projects", params={"token": ticket}) assert response.status_code == status.HTTP_200_OK - async def test_rest_rejects_ticket_bound_to_other_path( - self, - app: FastAPI, - base_client: AsyncClient - ) -> None: + async def test_rest_rejects_ticket_bound_to_other_path(self, app: FastAPI, base_client: AsyncClient) -> None: ticket = access_ticket_service.mint("admin", 0, path="/v3/symbols/router.svg/raw") response = await base_client.get("/v3/projects", params={"token": ticket}) assert response.status_code == status.HTTP_401_UNAUTHORIZED assert "Invalid or expired access ticket" in response.text - async def test_rest_rejects_node_bound_ticket( - self, - app: FastAPI, - base_client: AsyncClient - ) -> None: + async def test_rest_rejects_node_bound_ticket(self, app: FastAPI, base_client: AsyncClient) -> None: # node-bound (console) tickets must not authenticate REST resources ticket = access_ticket_service.mint("admin", 0, project_id="p1", node_id="n1") response = await base_client.get("/v3/projects", params={"token": ticket}) assert response.status_code == status.HTTP_401_UNAUTHORIZED - async def test_rest_rejects_ticket_with_stale_token_version( - self, - app: FastAPI, - base_client: AsyncClient - ) -> None: + async def test_rest_rejects_ticket_with_stale_token_version(self, app: FastAPI, base_client: AsyncClient) -> None: # logging out bumps the user's token_version: outstanding tickets must die with it ticket = access_ticket_service.mint("admin", 999, path="/v3/projects") response = await base_client.get("/v3/projects", params={"token": ticket}) diff --git a/tests/api/routes/controller/test_acl.py b/tests/api/routes/controller/test_acl.py index 83be66e1b..915ab6da5 100644 --- a/tests/api/routes/controller/test_acl.py +++ b/tests/api/routes/controller/test_acl.py @@ -32,7 +32,6 @@ pytestmark = pytest.mark.asyncio class TestACLRoutes: - @pytest_asyncio.fixture async def group_id(self, db_session: AsyncSession) -> str: @@ -50,33 +49,18 @@ class TestACLRoutes: return role_id async def test_create_ace( - self, - app: FastAPI, - authorized_client: AsyncClient, - db_session: AsyncSession, - test_user: User, - role_id: str + self, app: FastAPI, authorized_client: AsyncClient, db_session: AsyncSession, test_user: User, role_id: str ) -> None: # allow the user to create an ACE rbac_repo = RbacRepository(db_session) admin_role_id = (await rbac_repo.get_role_by_name("Administrator")).role_id - ace = ACECreate( - path="/access/acl", - ace_type="user", - user_id=test_user.user_id, - role_id=admin_role_id - ) + ace = ACECreate(path="/access/acl", ace_type="user", user_id=test_user.user_id, role_id=admin_role_id) await rbac_repo.create_ace(ace) # add an ACE on /projects to allow user to create a project path = f"/projects" - new_ace = { - "path": path, - "ace_type": "user", - "user_id": str(test_user.user_id), - "role_id": role_id - } + new_ace = {"path": path, "ace_type": "user", "user_id": str(test_user.user_id), "role_id": role_id} response = await authorized_client.post(app.url_path_for("create_ace"), json=new_ace) assert response.status_code == status.HTTP_201_CREATED @@ -87,21 +71,11 @@ class TestACLRoutes: response = await authorized_client.post(app.url_path_for("create_project"), json={"name": "test"}) assert response.status_code == status.HTTP_201_CREATED - async def test_create_ace_not_existing_endpoint( - self, - app: FastAPI, - client: AsyncClient, - group_id: str, - role_id: str + self, app: FastAPI, client: AsyncClient, group_id: str, role_id: str ) -> None: - new_ace = { - "path": "/projects/invalid", - "ace_type": "group", - "group_id": group_id, - "role_id": role_id - } + new_ace = {"path": "/projects/invalid", "ace_type": "group", "group_id": group_id, "role_id": role_id} response = await client.post(app.url_path_for("create_ace"), json=new_ace) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -137,35 +111,23 @@ class TestACLRoutes: assert len(response.json()) == 2 async def test_update_ace( - self, app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - test_user: User, - role_id: str + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, test_user: User, role_id: str ) -> None: rbac_repo = RbacRepository(db_session) ace_in_db = await rbac_repo.get_ace_by_path(f"/projects") - update_ace = { - "path": f"/appliances", - "ace_type": "user", - "user_id": str(test_user.user_id), - "role_id": role_id - } - response = await client.put( - app.url_path_for("update_ace", ace_id=ace_in_db.ace_id), - json=update_ace - ) + update_ace = {"path": f"/appliances", "ace_type": "user", "user_id": str(test_user.user_id), "role_id": role_id} + response = await client.put(app.url_path_for("update_ace", ace_id=ace_in_db.ace_id), json=update_ace) assert response.status_code == status.HTTP_200_OK updated_ace_in_db = await rbac_repo.get_ace(ace_in_db.ace_id) assert updated_ace_in_db.path == f"/appliances" async def test_delete_ace( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, + self, + app: FastAPI, + client: AsyncClient, + db_session: AsyncSession, ) -> None: rbac_repo = RbacRepository(db_session) @@ -174,22 +136,17 @@ class TestACLRoutes: assert response.status_code == status.HTTP_204_NO_CONTENT async def test_ace_cleanup( - self, - app: FastAPI, - authorized_client: AsyncClient, - db_session: AsyncSession, - test_user: User, - role_id: str, + self, + app: FastAPI, + authorized_client: AsyncClient, + db_session: AsyncSession, + test_user: User, + role_id: str, ) -> None: # allow the user to create projects rbac_repo = RbacRepository(db_session) - ace = ACECreate( - path="/projects", - ace_type="user", - user_id=test_user.user_id, - role_id=role_id - ) + ace = ACECreate(path="/projects", ace_type="user", user_id=test_user.user_id, role_id=role_id) await rbac_repo.create_ace(ace) response = await authorized_client.post(app.url_path_for("create_project"), json={"name": "test2"}) @@ -197,12 +154,7 @@ class TestACLRoutes: project_id = response.json()["project_id"] path = f"/projects/{project_id}" - ace = ACECreate( - path=path, - ace_type="user", - user_id=test_user.user_id, - role_id=role_id - ) + ace = ACECreate(path=path, ace_type="user", user_id=test_user.user_id, role_id=role_id) await rbac_repo.create_ace(ace) assert await rbac_repo.get_ace_by_path(path) diff --git a/tests/api/routes/controller/test_appliances.py b/tests/api/routes/controller/test_appliances.py index 356fe2b08..c37f18ddd 100644 --- a/tests/api/routes/controller/test_appliances.py +++ b/tests/api/routes/controller/test_appliances.py @@ -27,7 +27,6 @@ pytestmark = pytest.mark.asyncio class TestApplianceRoutes: - # @pytest.fixture(autouse=True) # def _install_builtin_appliances(self, controller: Controller): # @@ -63,7 +62,9 @@ class TestApplianceRoutes: response = await client.post(app.url_path_for("install_appliance", appliance_id=appliance_id), params=params) assert response.status_code == status.HTTP_400_BAD_REQUEST - async def test_qemu_appliance_install_with_version(self, app: FastAPI, client: AsyncClient, images_dir: str) -> None: + async def test_qemu_appliance_install_with_version( + self, app: FastAPI, client: AsyncClient, images_dir: str + ) -> None: shutil.copy("tests/resources/empty8G.qcow2", os.path.join(images_dir, "QEMU", "empty8G.qcow2")) appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance @@ -73,7 +74,9 @@ class TestApplianceRoutes: assert response.json()["name"] == "Empty VM" assert response.json()["version"] == "8G" - async def test_qemu_appliance_install_without_version(self, app: FastAPI, client: AsyncClient, images_dir: str) -> None: + async def test_qemu_appliance_install_without_version( + self, app: FastAPI, client: AsyncClient, images_dir: str + ) -> None: appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance response = await client.post(app.url_path_for("install_appliance", appliance_id=appliance_id)) @@ -82,24 +85,18 @@ class TestApplianceRoutes: async def test_add_version_appliance(self, app: FastAPI, client: AsyncClient) -> None: appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance - new_version = { - "name": "99G", - "images": { - "hda_disk_image": "empty99G.qcow2" - } - } - response = await client.post(app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version) + new_version = {"name": "99G", "images": {"hda_disk_image": "empty99G.qcow2"}} + response = await client.post( + app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version + ) assert response.status_code == status.HTTP_201_CREATED assert new_version in response.json()["versions"] async def test_add_existing_version_appliance(self, app: FastAPI, client: AsyncClient) -> None: appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance - new_version = { - "name": "8G", - "images": { - "hda_disk_image": "empty8G.qcow2" - } - } - response = await client.post(app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version) + new_version = {"name": "8G", "images": {"hda_disk_image": "empty8G.qcow2"}} + response = await client.post( + app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version + ) assert response.status_code == status.HTTP_409_CONFLICT diff --git a/tests/api/routes/controller/test_computes.py b/tests/api/routes/controller/test_computes.py index 5b864f02f..ea2317821 100644 --- a/tests/api/routes/controller/test_computes.py +++ b/tests/api/routes/controller/test_computes.py @@ -30,15 +30,9 @@ from tests.utils import asyncio_patch, AsyncioMagicMock class TestComputeRoutes: - async def test_compute_create(self, app: FastAPI, client: AsyncClient) -> None: - params = { - "protocol": "http", - "host": "localhost", - "port": 84, - "user": "julien", - "password": "secure"} + params = {"protocol": "http", "host": "localhost", "port": 84, "user": "julien", "password": "secure"} response = await client.post(app.url_path_for("create_compute"), json=params) assert response.status_code == status.HTTP_201_CREATED @@ -57,7 +51,8 @@ class TestComputeRoutes: "host": "127.0.0.1", "port": 84, "user": "julien", - "password": "secure"} + "password": "secure", + } response = await client.post(app.url_path_for("create_compute"), json=params) assert response.status_code == status.HTTP_201_CREATED @@ -82,7 +77,8 @@ class TestComputeRoutes: async def test_compute_get_local(self, app: FastAPI, client: AsyncClient, controller) -> None: await controller.add_compute( - compute_id="local", name="local", host="127.0.0.1", port=3080, force=True, connect=False) + compute_id="local", name="local", host="127.0.0.1", port=3080, force=True, connect=False + ) response = await client.get(app.url_path_for("get_compute", compute_id="local")) assert response.status_code == status.HTTP_200_OK @@ -91,13 +87,7 @@ class TestComputeRoutes: async def test_compute_update(self, app: FastAPI, client: AsyncClient, test_compute: Compute) -> None: - params = { - "protocol": "http", - "host": "localhost", - "port": 42, - "user": "julien", - "password": "secure" - } + params = {"protocol": "http", "host": "localhost", "port": 42, "user": "julien", "password": "secure"} response = await client.post(app.url_path_for("create_compute"), json=params) assert response.status_code == status.HTTP_201_CREATED @@ -118,29 +108,22 @@ class TestComputeRoutes: class TestComputeFeatures: - async def test_compute_list_docker_images(self, app: FastAPI, client: AsyncClient) -> None: - params = { - "protocol": "http", - "host": "localhost", - "port": 84, - "user": "julien", - "password": "secure" - } + params = {"protocol": "http", "host": "localhost", "port": 84, "user": "julien", "password": "secure"} response = await client.post(app.url_path_for("create_compute"), json=params) assert response.status_code == status.HTTP_201_CREATED compute_id = response.json()["compute_id"] - with asyncio_patch("gns3server.controller.compute.Compute.forward", return_value=[{"image": "docker1"}, {"image": "docker2"}]) as mock: + with asyncio_patch( + "gns3server.controller.compute.Compute.forward", return_value=[{"image": "docker1"}, {"image": "docker2"}] + ) as mock: response = await client.get(app.url_path_for("docker_get_images", compute_id=compute_id)) mock.assert_called_with("GET", "docker", "images") assert response.json() == [{"image": "docker1"}, {"image": "docker2"}] - async def test_compute_pull_docker_image( - self, app: FastAPI, client: AsyncClient, test_compute: Compute - ) -> None: + async def test_compute_pull_docker_image(self, app: FastAPI, client: AsyncClient, test_compute: Compute) -> None: compute = MagicMock() compute.forward = AsyncioMagicMock(return_value={}) @@ -148,20 +131,14 @@ class TestComputeFeatures: controller.return_value.get_compute.return_value = compute response = await client.post( app.url_path_for("docker_pull_image", compute_id=test_compute.compute_id), - json={"image": "nginx:latest"} + json={"image": "nginx:latest"}, ) compute.forward.assert_called_with("POST", "docker", "images/pull", data={"image": "nginx:latest"}) assert response.status_code == status.HTTP_204_NO_CONTENT async def test_compute_list_virtualbox_vms(self, app: FastAPI, client: AsyncClient) -> None: - params = { - "protocol": "http", - "host": "localhost", - "port": 4242, - "user": "julien", - "password": "secure" - } + params = {"protocol": "http", "host": "localhost", "port": 4242, "user": "julien", "password": "secure"} response = await client.post(app.url_path_for("get_computes"), json=params) assert response.status_code == status.HTTP_201_CREATED compute_id = response.json()["compute_id"] @@ -173,13 +150,7 @@ class TestComputeFeatures: async def test_compute_list_vmware_vms(self, app: FastAPI, client: AsyncClient) -> None: - params = { - "protocol": "http", - "host": "localhost", - "port": 4243, - "user": "julien", - "password": "secure" - } + params = {"protocol": "http", "host": "localhost", "port": 4243, "user": "julien", "password": "secure"} response = await client.post(app.url_path_for("get_computes"), json=params) assert response.status_code == status.HTTP_201_CREATED compute_id = response.json()["compute_id"] diff --git a/tests/api/routes/controller/test_controller.py b/tests/api/routes/controller/test_controller.py index 43d672302..b71a9cf2c 100644 --- a/tests/api/routes/controller/test_controller.py +++ b/tests/api/routes/controller/test_controller.py @@ -28,7 +28,6 @@ pytestmark = pytest.mark.asyncio class TestControllerRoutes: - async def test_shutdown_local(self, app: FastAPI, client: AsyncClient, config: Config, monkeypatch) -> None: # monkeypatch (not bare assignment): a global `os.kill = MagicMock()` @@ -41,14 +40,12 @@ class TestControllerRoutes: response = await client.post(app.url_path_for("shutdown")) assert response.status_code == status.HTTP_204_NO_CONTENT assert kill_mock.called - - + async def test_shutdown_non_local(self, app: FastAPI, client: AsyncClient, config: Config) -> None: - + response = await client.post(app.url_path_for("shutdown")) assert response.status_code == status.HTTP_403_FORBIDDEN - - + # @pytest.mark.asyncio # async def test_debug(controller_api, config, tmpdir): # @@ -68,9 +65,8 @@ class TestControllerRoutes: # config.set("Server", "local", False) # response = await controller_api.post('/debug') # assert response.status_code == 403 - - + async def test_statistics_output(self, app: FastAPI, client: AsyncClient) -> None: - + response = await client.get(app.url_path_for("statistics")) assert response.status_code == status.HTTP_200_OK diff --git a/tests/api/routes/controller/test_drawings.py b/tests/api/routes/controller/test_drawings.py index 62f00d5f5..2d491f863 100644 --- a/tests/api/routes/controller/test_drawings.py +++ b/tests/api/routes/controller/test_drawings.py @@ -27,90 +27,77 @@ pytestmark = pytest.mark.asyncio class TestDrawingsRoutes: - async def test_create_drawing(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + params = { "svg": '', "x": 10, "y": 20, - "z": 0 + "z": 0, } - + response = await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["drawing_id"] is not None - - + async def test_get_drawing(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + params = { "svg": '', "x": 10, "y": 20, - "z": 0 + "z": 0, } - + response = await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params) - response = await client.get(app.url_path_for( - "get_drawing", - project_id=project.id, - drawing_id=response.json()["drawing_id"]) + response = await client.get( + app.url_path_for("get_drawing", project_id=project.id, drawing_id=response.json()["drawing_id"]) ) assert response.status_code == 200 assert response.json()["x"] == 10 - - + async def test_update_drawing(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + params = { "svg": '', "x": 10, "y": 20, - "z": 0 + "z": 0, } - + response = await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params) - response = await client.put(app.url_path_for( - "update_drawing", - project_id=project.id, - drawing_id=response.json()["drawing_id"]), - json={"x": 42} + response = await client.put( + app.url_path_for("update_drawing", project_id=project.id, drawing_id=response.json()["drawing_id"]), + json={"x": 42}, ) - + assert response.status_code == status.HTTP_200_OK assert response.json()["x"] == 42 - - + async def test_all_drawings(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + params = { "svg": '', "x": 10, "y": 20, - "z": 0 + "z": 0, } - + await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params) response = await client.get(app.url_path_for("get_drawings", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert len(response.json()) == 1 - + # test listing links from a closed project await project.close(ignore_notification=True) response = await client.get(app.url_path_for("get_drawings", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert len(response.json()) == 1 - - + async def test_delete_drawing(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + drawing = Drawing(project) project._drawings = {drawing.id: drawing} - response = await client.delete(app.url_path_for( - "delete_drawing", - project_id=project.id, - drawing_id=drawing.id) - ) + response = await client.delete(app.url_path_for("delete_drawing", project_id=project.id, drawing_id=drawing.id)) assert response.status_code == status.HTTP_204_NO_CONTENT assert drawing.id not in project.drawings diff --git a/tests/api/routes/controller/test_gns3vm.py b/tests/api/routes/controller/test_gns3vm.py index ce10bfb8c..27a8b9184 100644 --- a/tests/api/routes/controller/test_gns3vm.py +++ b/tests/api/routes/controller/test_gns3vm.py @@ -25,33 +25,27 @@ pytestmark = pytest.mark.asyncio class TestGNS3VMRoutes: - async def test_list_vms(self, app: FastAPI, client: AsyncClient) -> None: - - with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.list", return_value=[{"vmname": "test"}]): + + with asyncio_patch( + "gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.list", return_value=[{"vmname": "test"}] + ): response = await client.get(app.url_path_for("get_vms", engine="vmware")) assert response.status_code == status.HTTP_200_OK - assert response.json() == [ - { - "vmname": "test" - } - ] - - + assert response.json() == [{"vmname": "test"}] + async def test_engines(self, app: FastAPI, client: AsyncClient) -> None: - + response = await client.get(app.url_path_for("get_engines")) assert response.status_code == status.HTTP_200_OK assert len(response.json()) > 0 - - + async def test_put_gns3vm(self, app: FastAPI, client: AsyncClient) -> None: - + response = await client.put(app.url_path_for("update_gns3vm_settings"), json={"vmname": "TEST VM"}) assert response.status_code == status.HTTP_200_OK assert response.json()["vmname"] == "TEST VM" - - + async def test_get_gns3vm(self, app: FastAPI, client: AsyncClient) -> None: response = await client.get(app.url_path_for("get_gns3vm_settings")) assert response.status_code == status.HTTP_200_OK diff --git a/tests/api/routes/controller/test_groups.py b/tests/api/routes/controller/test_groups.py index 642f283e0..69ff6183f 100644 --- a/tests/api/routes/controller/test_groups.py +++ b/tests/api/routes/controller/test_groups.py @@ -28,7 +28,6 @@ pytestmark = pytest.mark.asyncio class TestGroupRoutes: - async def test_create_group(self, app: FastAPI, client: AsyncClient) -> None: new_group = {"name": "group1"} @@ -56,47 +55,30 @@ class TestGroupRoutes: update_group = {"name": "group42"} response = await client.put( - app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id), - json=update_group + app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id), json=update_group ) assert response.status_code == status.HTTP_200_OK updated_group_in_db = await user_repo.get_user_group(group_in_db.user_group_id) assert updated_group_in_db.name == "group42" - async def test_cannot_update_admin_group( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_cannot_update_admin_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: user_repo = UsersRepository(db_session) group_in_db = await user_repo.get_user_group_by_name("Administrators") update_group = {"name": "Hackers"} response = await client.put( - app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id), - json=update_group + app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id), json=update_group ) assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_delete_group( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_delete_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: user_repo = UsersRepository(db_session) group_in_db = await user_repo.get_user_group_by_name("group42") response = await client.delete(app.url_path_for("delete_user_group", user_group_id=group_in_db.user_group_id)) assert response.status_code == status.HTTP_204_NO_CONTENT - async def test_cannot_delete_admin_group( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_cannot_delete_admin_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: user_repo = UsersRepository(db_session) group_in_db = await user_repo.get_user_group_by_name("Administrators") @@ -105,32 +87,21 @@ class TestGroupRoutes: class TestGroupMembersRoutes: - async def test_add_to_group_already_member( - self, - app: FastAPI, - client: AsyncClient, - test_user: User, - db_session: AsyncSession + self, app: FastAPI, client: AsyncClient, test_user: User, db_session: AsyncSession ) -> None: user_repo = UsersRepository(db_session) group_in_db = await user_repo.get_user_group_by_name("Users") response = await client.put( app.url_path_for( - "add_member_to_group", - user_group_id=group_in_db.user_group_id, - user_id=str(test_user.user_id) + "add_member_to_group", user_group_id=group_in_db.user_group_id, user_id=str(test_user.user_id) ) ) assert response.status_code == status.HTTP_400_BAD_REQUEST async def test_add_member_to_group( - self, - app: FastAPI, - client: AsyncClient, - test_user: User, - db_session: AsyncSession + self, app: FastAPI, client: AsyncClient, test_user: User, db_session: AsyncSession ) -> None: user_repo = UsersRepository(db_session) @@ -140,9 +111,7 @@ class TestGroupMembersRoutes: group_in_db = await user_repo.create_user_group(new_user_group) response = await client.put( app.url_path_for( - "add_member_to_group", - user_group_id=group_in_db.user_group_id, - user_id=str(test_user.user_id) + "add_member_to_group", user_group_id=group_in_db.user_group_id, user_id=str(test_user.user_id) ) ) assert response.status_code == status.HTTP_204_NO_CONTENT @@ -150,29 +119,16 @@ class TestGroupMembersRoutes: assert len(members) == 1 assert members[0].username == test_user.username - async def test_get_user_group_members( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_get_user_group_members(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: user_repo = UsersRepository(db_session) group_in_db = await user_repo.get_user_group_by_name("test_group") - response = await client.get( - app.url_path_for( - "get_user_group_members", - user_group_id=group_in_db.user_group_id) - ) + response = await client.get(app.url_path_for("get_user_group_members", user_group_id=group_in_db.user_group_id)) assert response.status_code == status.HTTP_200_OK assert len(response.json()) == 1 async def test_remove_member_from_group( - self, - app: FastAPI, - client: AsyncClient, - test_user: User, - db_session: AsyncSession + self, app: FastAPI, client: AsyncClient, test_user: User, db_session: AsyncSession ) -> None: user_repo = UsersRepository(db_session) @@ -180,9 +136,7 @@ class TestGroupMembersRoutes: response = await client.delete( app.url_path_for( - "remove_member_from_group", - user_group_id=group_in_db.user_group_id, - user_id=str(test_user.user_id) + "remove_member_from_group", user_group_id=group_in_db.user_group_id, user_id=str(test_user.user_id) ), ) assert response.status_code == status.HTTP_204_NO_CONTENT diff --git a/tests/api/routes/controller/test_images.py b/tests/api/routes/controller/test_images.py index ea0b2f587..716897a30 100644 --- a/tests/api/routes/controller/test_images.py +++ b/tests/api/routes/controller/test_images.py @@ -43,7 +43,7 @@ def iou_32_bit_image(tmpdir) -> str: path = os.path.join(tmpdir, "iou_32bit.bin") with open(path, "wb+") as f: - f.write(b'\x7fELF\x01\x01\x01') + f.write(b"\x7fELF\x01\x01\x01") return path @@ -55,7 +55,7 @@ def iou_64_bit_image(tmpdir) -> str: path = os.path.join(tmpdir, "iou_64bit.bin") with open(path, "wb+") as f: - f.write(b'\x7fELF\x02\x01\x01') + f.write(b"\x7fELF\x02\x01\x01") return path @@ -67,7 +67,7 @@ def ios_image(tmpdir) -> str: path = os.path.join(tmpdir, "ios_image.bin") with open(path, "wb+") as f: - f.write(b'\x7fELF\x01\x02\x01') + f.write(b"\x7fELF\x01\x02\x01") return path @@ -79,7 +79,7 @@ def qcow2_image(tmpdir) -> str: path = os.path.join(tmpdir, "image.qcow2") with open(path, "wb+") as f: - f.write(b'QFI\xfb\x00\x00\x00') + f.write(b"QFI\xfb\x00\x00\x00") return path @@ -91,7 +91,7 @@ def invalid_image(tmpdir) -> str: path = os.path.join(tmpdir, "invalid_image.bin") with open(path, "wb+") as f: - f.write(b'\x01\x01\x01\x01') + f.write(b"\x01\x01\x01\x01") return path @@ -103,21 +103,21 @@ def empty_image(tmpdir) -> str: path = os.path.join(tmpdir, "empty_image.bin") with open(path, "wb+") as f: - f.write(b'') + f.write(b"") return path class TestImageRoutes: - async def test_create_image(self, app: FastAPI, client: AsyncClient, images_dir) -> None: Qemu.instance().create_disk_image = AsyncioMagicMock() path = os.path.join(os.path.join(images_dir, "QEMU", "new_image.qcow2")) with open(path, "wb+") as f: - f.write(b'QFI\xfb\x00\x00\x00') + f.write(b"QFI\xfb\x00\x00\x00") image_name = os.path.basename(path) response = await client.post( - app.url_path_for("create_qemu_image", image_path=image_name), json={"format": "qcow2", "size": 30}) + app.url_path_for("create_qemu_image", image_path=image_name), json={"format": "qcow2", "size": 30} + ) assert response.status_code == status.HTTP_201_CREATED @pytest.mark.parametrize( @@ -129,18 +129,18 @@ class TestImageRoutes: ("ios", "ios_image", True), ("ios", "invalid_image", False), ("qemu", "qcow2_image", True), - ("qemu", "empty_image", False) + ("qemu", "empty_image", False), ), ) async def test_upload_image( - self, - app: FastAPI, - client: AsyncClient, - images_dir: str, - image_type: str, - fixture_name: str, - valid_request: bool, - request + self, + app: FastAPI, + client: AsyncClient, + images_dir: str, + image_type: str, + fixture_name: str, + valid_request: bool, + request, ) -> None: image_path = request.getfixturevalue(fixture_name) @@ -150,9 +150,7 @@ class TestImageRoutes: image_data = f.read() image_checksum.update(image_data) - response = await client.post( - app.url_path_for("upload_image", image_path=image_name), - content=image_data) + response = await client.post(app.url_path_for("upload_image", image_path=image_name), content=image_data) if valid_request: assert response.status_code == status.HTTP_201_CREATED @@ -179,21 +177,15 @@ class TestImageRoutes: with open(qcow2_image, "rb") as f: image_data = f.read() - response = await client.post( - app.url_path_for("upload_image", image_path="image1.qcow2"), - content=image_data) + response = await client.post(app.url_path_for("upload_image", image_path="image1.qcow2"), content=image_data) assert response.status_code == status.HTTP_201_CREATED # same image with same name is uploaded again, it should return 409 Conflict - response = await client.post( - app.url_path_for("upload_image", image_path="image1.qcow2"), - content=image_data) + response = await client.post(app.url_path_for("upload_image", image_path="image1.qcow2"), content=image_data) assert response.status_code == status.HTTP_409_CONFLICT # same image with different name but same checksum is uploaded again, it should return 409 Conflict - response = await client.post( - app.url_path_for("upload_image", image_path="image2.qcow2"), - content=image_data) + response = await client.post(app.url_path_for("upload_image", image_path="image2.qcow2"), content=image_data) assert response.status_code == status.HTTP_409_CONFLICT async def test_image_delete(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None: @@ -208,14 +200,14 @@ class TestImageRoutes: response = await client.get(app.url_path_for("get_image", image_path=image_name)) assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_image_deleted_on_disk(self, app: FastAPI, client: AsyncClient, images_dir: str, qcow2_image: str) -> None: + async def test_image_deleted_on_disk( + self, app: FastAPI, client: AsyncClient, images_dir: str, qcow2_image: str + ) -> None: image_name = os.path.basename(qcow2_image) with open(qcow2_image, "rb") as f: image_data = f.read() - response = await client.post( - app.url_path_for("upload_image", image_path=image_name), - content=image_data) + response = await client.post(app.url_path_for("upload_image", image_path=image_name), content=image_data) assert response.status_code == status.HTTP_201_CREATED response = await client.delete(app.url_path_for("delete_image", image_path=image_name)) @@ -230,42 +222,30 @@ class TestImageRoutes: ), ) async def test_upload_image_subdir( - self, - app: FastAPI, - client: AsyncClient, - images_dir: str, - qcow2_image: str, - subdir: str, - expected_result: int, - db_session: AsyncSession + self, + app: FastAPI, + client: AsyncClient, + images_dir: str, + qcow2_image: str, + subdir: str, + expected_result: int, + db_session: AsyncSession, ) -> None: image_name = os.path.basename(qcow2_image) with open(qcow2_image, "rb") as f: image_data = f.read() image_path = os.path.join(subdir, image_name) - response = await client.post( - app.url_path_for("upload_image", image_path=image_path), - content=image_data) + response = await client.post(app.url_path_for("upload_image", image_path=image_path), content=image_data) assert response.status_code == expected_result - async def test_image_delete_multiple_match( - self, - app: FastAPI, - client: AsyncClient, - qcow2_image: str - ) -> None: + async def test_image_delete_multiple_match(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None: image_name = os.path.basename(qcow2_image) response = await client.delete(app.url_path_for("delete_image", image_path=image_name)) assert response.status_code == status.HTTP_400_BAD_REQUEST - async def test_image_delete_with_subdir( - self, - app: FastAPI, - client: AsyncClient, - qcow2_image: str - ) -> None: + async def test_image_delete_with_subdir(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None: image_name = os.path.basename(qcow2_image) image_path = os.path.join("subdir", image_name) @@ -302,17 +282,20 @@ class TestImageRoutes: with open(os.path.join(project_dir, f"{name}.gns3"), "w+") as f: json.dump(topology, f) return await controller.add_project( - project_id=project_id, name=name, path=project_dir, - filename=f"{name}.gns3", status="closed", + project_id=project_id, + name=name, + path=project_dir, + filename=f"{name}.gns3", + status="closed", ) async def test_image_delete_used_by_project( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller, - db_session: AsyncSession, - tmpdir: str, + self, + app: FastAPI, + client: AsyncClient, + controller: Controller, + db_session: AsyncSession, + tmpdir: str, ) -> None: """ An image referenced by a node in a project must not be deleted, @@ -321,15 +304,21 @@ class TestImageRoutes: image_path = os.path.join(tmpdir, "used.qcow2") with open(image_path, "wb+") as f: - f.write(b'\x42\x42\x42\x42') + f.write(b"\x42\x42\x42\x42") images_repo = ImagesRepository(db_session) await images_repo.add_image("used.qcow2", "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5") guarded_project = await self._add_closed_project_with_node( - controller, "Guarded", - {"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local", - "name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}}, + controller, + "Guarded", + { + "node_id": str(uuid.uuid4()), + "node_type": "qemu", + "compute_id": "local", + "name": "n1", + "properties": {"hda_disk_image_backing_file": "used.qcow2"}, + }, ) response = await client.delete(app.url_path_for("delete_image", image_path="used.qcow2")) @@ -344,12 +333,12 @@ class TestImageRoutes: assert not os.path.exists(image_path) async def test_prune_images_keeps_project_referenced( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller, - db_session: AsyncSession, - tmpdir: str, + self, + app: FastAPI, + client: AsyncClient, + controller: Controller, + db_session: AsyncSession, + tmpdir: str, ) -> None: """ Pruning must keep images referenced by a project node while @@ -360,13 +349,19 @@ class TestImageRoutes: for filename in ("used.qcow2", "unused.qcow2"): image_path = os.path.join(tmpdir, filename) with open(image_path, "wb+") as f: - f.write(b'\x42\x42\x42\x42') + f.write(b"\x42\x42\x42\x42") await images_repo.add_image(filename, "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5") await self._add_closed_project_with_node( - controller, "Guarded", - {"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local", - "name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}}, + controller, + "Guarded", + { + "node_id": str(uuid.uuid4()), + "node_type": "qemu", + "compute_id": "local", + "name": "n1", + "properties": {"hda_disk_image_backing_file": "used.qcow2"}, + }, ) response = await client.delete(app.url_path_for("prune_images")) @@ -378,10 +373,7 @@ class TestImageRoutes: assert not os.path.exists(os.path.join(tmpdir, "unused.qcow2")) async def test_image_upload_create_appliance( - self, app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - controller: Controller + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, controller: Controller ) -> None: await controller.appliance_manager.install_builtin_appliances() @@ -393,7 +385,8 @@ class TestImageRoutes: response = await client.post( app.url_path_for("upload_image", image_path=image_name), params={"install_appliances": "true"}, - content=image_data) + content=image_data, + ) assert response.status_code == status.HTTP_201_CREATED templates_repo = TemplatesRepository(db_session) @@ -404,19 +397,14 @@ class TestImageRoutes: await templates_repo.delete_template(templates[0].template_id) async def test_install_all( - self, app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - controller: Controller + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, controller: Controller ) -> None: image_path = "tests/resources/empty100G.qcow2" image_name = os.path.basename(image_path) with open(image_path, "rb") as f: image_data = f.read() - response = await client.post( - app.url_path_for("upload_image", image_path=image_name), - content=image_data) + response = await client.post(app.url_path_for("upload_image", image_path=image_name), content=image_data) assert response.status_code == status.HTTP_201_CREATED controller.appliance_manager.load_appliances() # make sure appliances are loaded @@ -437,10 +425,7 @@ class TestImageRoutes: await templates_repo.delete_template(templates[0].template_id) async def test_install_all_skips_existing_template_name( - self, app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - controller: Controller + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, controller: Controller ) -> None: # two images matching two versions of the same appliance must not # produce two templates with the same name @@ -455,8 +440,8 @@ class TestImageRoutes: with open(image_path, "rb") as f: image_data = f.read() response = await client.post( - app.url_path_for("upload_image", image_path=os.path.basename(image_path)), - content=image_data) + app.url_path_for("upload_image", image_path=os.path.basename(image_path)), content=image_data + ) assert response.status_code == status.HTTP_201_CREATED controller.appliance_manager.load_appliances() # make sure appliances are loaded @@ -473,4 +458,4 @@ class TestImageRoutes: templates = await templates_repo.get_templates() assert len(templates) == 1 assert templates[0].name == "Empty VM" - await templates_repo.delete_template(templates[0].template_id) \ No newline at end of file + await templates_repo.delete_template(templates[0].template_id) diff --git a/tests/api/routes/controller/test_links.py b/tests/api/routes/controller/test_links.py index bc50e053c..a81ca094b 100644 --- a/tests/api/routes/controller/test_links.py +++ b/tests/api/routes/controller/test_links.py @@ -37,57 +37,43 @@ pytestmark = pytest.mark.asyncio class TestLinkRoutes: - @pytest_asyncio.fixture async def nodes(self, compute: Compute, project: Project) -> Tuple[Node, Node]: - + response = MagicMock() response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - + node1 = await project.add_node(compute, "node1", None, node_type="qemu") node1._ports = [EthernetPort("E0", 0, 0, 3)] node2 = await project.add_node(compute, "node2", None, node_type="qemu") node2._ports = [EthernetPort("E0", 0, 2, 4)] return node1, node2 - - + async def test_create_link( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - nodes: Tuple[Node, Node] + self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node] ) -> None: - + node1, node2 = nodes - - filters = { - "delay": [10, 0], - "frequency_drop": [50] - } + + filters = {"delay": [10, 0], "frequency_drop": [50]} with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: - response = await client.post(app.url_path_for("create_link", project_id=project.id), json={ - "nodes": [ - { - "node_id": node1.id, - "adapter_number": 0, - "port_number": 3, - "label": { - "text": "Text", - "x": 42, - "y": 0 - } - }, - { - "node_id": node2.id, - "adapter_number": 2, - "port_number": 4 - } - ], - "filters": filters - }) + response = await client.post( + app.url_path_for("create_link", project_id=project.id), + json={ + "nodes": [ + { + "node_id": node1.id, + "adapter_number": 0, + "port_number": 3, + "label": {"text": "Text", "x": 42, "y": 0}, + }, + {"node_id": node2.id, "adapter_number": 2, "port_number": 4}, + ], + "filters": filters, + }, + ) assert mock.called assert response.status_code == status.HTTP_201_CREATED @@ -96,264 +82,193 @@ class TestLinkRoutes: assert response.json()["nodes"][0]["label"]["x"] == 42 assert len(project.links) == 1 assert list(project.links.values())[0].filters == filters - - + async def test_create_link_failure( - self, - app: FastAPI, - client: AsyncClient, - compute: Compute, - project: Project + self, app: FastAPI, client: AsyncClient, compute: Compute, project: Project ) -> None: """ Make sure the link is deleted if we failed to create it. - + The failure is triggered by connecting the link to itself """ - + response = MagicMock() response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - + node1 = await project.add_node(compute, "node1", None, node_type="qemu") node1._ports = [EthernetPort("E0", 0, 0, 3), EthernetPort("E0", 0, 0, 4)] - - response = await client.post(app.url_path_for("create_link", project_id=project.id), json={ - "nodes": [ - { - "node_id": node1.id, - "adapter_number": 0, - "port_number": 3, - "label": { - "text": "Text", - "x": 42, - "y": 0 - } - }, - { - "node_id": node1.id, - "adapter_number": 0, - "port_number": 4 - } - ] - }) - - assert response.status_code == status.HTTP_409_CONFLICT - assert len(project.links) == 0 - - - async def test_get_link( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - nodes: Tuple[Node, Node] - ) -> None: - - node1, node2 = nodes - with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: - response = await client.post(app.url_path_for("create_link", project_id=project.id), json={ + + response = await client.post( + app.url_path_for("create_link", project_id=project.id), + json={ "nodes": [ { "node_id": node1.id, "adapter_number": 0, "port_number": 3, - "label": { - "text": "Text", - "x": 42, - "y": 0 - } + "label": {"text": "Text", "x": 42, "y": 0}, }, - { - "node_id": node2.id, - "adapter_number": 2, - "port_number": 4 - } + {"node_id": node1.id, "adapter_number": 0, "port_number": 4}, ] - }) - + }, + ) + + assert response.status_code == status.HTTP_409_CONFLICT + assert len(project.links) == 0 + + async def test_get_link( + self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node] + ) -> None: + + node1, node2 = nodes + with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: + response = await client.post( + app.url_path_for("create_link", project_id=project.id), + json={ + "nodes": [ + { + "node_id": node1.id, + "adapter_number": 0, + "port_number": 3, + "label": {"text": "Text", "x": 42, "y": 0}, + }, + {"node_id": node2.id, "adapter_number": 2, "port_number": 4}, + ] + }, + ) + assert mock.called link_id = response.json()["link_id"] assert response.json()["nodes"][0]["label"]["x"] == 42 response = await client.get(app.url_path_for("get_link", project_id=project.id, link_id=link_id)) assert response.status_code == status.HTTP_200_OK assert response.json()["nodes"][0]["label"]["x"] == 42 - - + async def test_update_link_suspend( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - nodes: Tuple[Node, Node] + self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node] ) -> None: - + node1, node2 = nodes with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: - response = await client.post(app.url_path_for("create_link", project_id=project.id), json={ + response = await client.post( + app.url_path_for("create_link", project_id=project.id), + json={ + "nodes": [ + { + "node_id": node1.id, + "adapter_number": 0, + "port_number": 3, + "label": {"text": "Text", "x": 42, "y": 0}, + }, + {"node_id": node2.id, "adapter_number": 2, "port_number": 4}, + ] + }, + ) + + assert mock.called + link_id = response.json()["link_id"] + assert response.json()["nodes"][0]["label"]["x"] == 42 + + response = await client.put( + app.url_path_for("update_link", project_id=project.id, link_id=link_id), + json={ "nodes": [ { "node_id": node1.id, "adapter_number": 0, "port_number": 3, - "label": { - "text": "Text", - "x": 42, - "y": 0 - } + "label": {"text": "Hello", "x": 64, "y": 0}, }, - { - "node_id": node2.id, - "adapter_number": 2, - "port_number": 4 - } - ] - }) - - assert mock.called - link_id = response.json()["link_id"] - assert response.json()["nodes"][0]["label"]["x"] == 42 - - response = await client.put(app.url_path_for("update_link", project_id=project.id, link_id=link_id), json={ - "nodes": [ - { - "node_id": node1.id, - "adapter_number": 0, - "port_number": 3, - "label": { - "text": "Hello", - "x": 64, - "y": 0 - } - }, - { - "node_id": node2.id, - "adapter_number": 2, - "port_number": 4 - } - ], - "suspend": True - }) - + {"node_id": node2.id, "adapter_number": 2, "port_number": 4}, + ], + "suspend": True, + }, + ) + assert response.status_code == status.HTTP_200_OK assert response.json()["nodes"][0]["label"]["x"] == 64 assert response.json()["suspend"] assert response.json()["filters"] == {} - - + async def test_update_link( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - nodes: Tuple[Node, Node] + self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node] ) -> None: - - filters = { - "delay": [10, 0], - "frequency_drop": [50] - } + + filters = {"delay": [10, 0], "frequency_drop": [50]} node1, node2 = nodes with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: - response = await client.post(app.url_path_for("create_link", project_id=project.id), json={ + response = await client.post( + app.url_path_for("create_link", project_id=project.id), + json={ + "nodes": [ + { + "node_id": node1.id, + "adapter_number": 0, + "port_number": 3, + "label": {"text": "Text", "x": 42, "y": 0}, + }, + {"node_id": node2.id, "adapter_number": 2, "port_number": 4}, + ] + }, + ) + + assert mock.called + link_id = response.json()["link_id"] + assert response.json()["nodes"][0]["label"]["x"] == 42 + + response = await client.put( + app.url_path_for("update_link", project_id=project.id, link_id=link_id), + json={ "nodes": [ { "node_id": node1.id, "adapter_number": 0, "port_number": 3, - "label": { - "text": "Text", - "x": 42, - "y": 0 - } + "label": {"text": "Hello", "x": 64, "y": 0}, }, - { - "node_id": node2.id, - "adapter_number": 2, - "port_number": 4 - } - ] - }) - - assert mock.called - link_id = response.json()["link_id"] - assert response.json()["nodes"][0]["label"]["x"] == 42 - - response = await client.put(app.url_path_for("update_link", project_id=project.id, link_id=link_id), json={ - "nodes": [ - { - "node_id": node1.id, - "adapter_number": 0, - "port_number": 3, - "label": { - "text": "Hello", - "x": 64, - "y": 0 - } - }, - { - "node_id": node2.id, - "adapter_number": 2, - "port_number": 4 - } - ], - "filters": filters - }) - + {"node_id": node2.id, "adapter_number": 2, "port_number": 4}, + ], + "filters": filters, + }, + ) + assert response.status_code == status.HTTP_200_OK assert response.json()["nodes"][0]["label"]["x"] == 64 assert list(project.links.values())[0].filters == filters - - + async def test_list_link( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - nodes: Tuple[Node, Node] + self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node] ) -> None: - - filters = { - "delay": [10, 0], - "frequency_drop": [50] - } - + + filters = {"delay": [10, 0], "frequency_drop": [50]} + node1, node2 = nodes nodes = [ - { - "node_id": node1.id, - "adapter_number": 0, - "port_number": 3 - }, - { - "node_id": node2.id, - "adapter_number": 2, - "port_number": 4 - } + {"node_id": node1.id, "adapter_number": 0, "port_number": 3}, + {"node_id": node2.id, "adapter_number": 2, "port_number": 4}, ] with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock: - await client.post(app.url_path_for("create_link", project_id=project.id), json={ - "nodes": nodes, - "filters": filters - }) - + await client.post( + app.url_path_for("create_link", project_id=project.id), json={"nodes": nodes, "filters": filters} + ) + assert mock.called response = await client.get(app.url_path_for("get_links", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert len(response.json()) == 1 assert response.json()[0]["filters"] == filters - + # test listing links from a closed project await project.close(ignore_notification=True) response = await client.get(app.url_path_for("get_links", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert len(response.json()) == 1 assert response.json()[0]["filters"] == filters - - + async def test_reset_link(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + link = UDPLink(project) project._links = {link.id: link} with asyncio_patch("gns3server.controller.udp_link.UDPLink.delete") as delete_mock: @@ -362,28 +277,27 @@ class TestLinkRoutes: assert delete_mock.called assert create_mock.called assert response.status_code == status.HTTP_200_OK - - + async def test_start_capture(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + link = Link(project) project._links = {link.id: link} with asyncio_patch("gns3server.controller.link.Link.start_capture") as mock: - response = await client.post(app.url_path_for("start_capture", project_id=project.id, link_id=link.id), json={}) + response = await client.post( + app.url_path_for("start_capture", project_id=project.id, link_id=link.id), json={} + ) assert mock.called assert response.status_code == status.HTTP_201_CREATED - - + async def test_stop_capture(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + link = Link(project) project._links = {link.id: link} with asyncio_patch("gns3server.controller.link.Link.stop_capture") as mock: response = await client.post(app.url_path_for("stop_capture", project_id=project.id, link_id=link.id)) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + # async def test_pcap(controller_api, http_client, project): # # async def pcap_capture(): @@ -403,20 +317,18 @@ class TestLinkRoutes: # assert mock.called # assert response.status_code == 200 # assert b'hello' == response.body - - + async def test_delete_link(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + link = Link(project) project._links = {link.id: link} with asyncio_patch("gns3server.controller.link.Link.delete") as mock: response = await client.delete(app.url_path_for("delete_link", project_id=project.id, link_id=link.id)) assert mock.called assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_list_filters(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + link = Link(project) project._links = {link.id: link} with patch("gns3server.controller.link.Link.available_filters", return_value=FILTERS) as mock: @@ -425,7 +337,6 @@ class TestLinkRoutes: assert response.status_code == status.HTTP_200_OK assert response.json() == FILTERS - async def test_get_udp_interface(self, app: FastAPI, client: AsyncClient, project: Project) -> None: """ Test getting UDP tunnel interface information from a link. @@ -433,12 +344,12 @@ class TestLinkRoutes: link = Link(project) project._links = {link.id: link} - + cloud_node = MagicMock() cloud_node.node_type = "cloud" cloud_node.id = str(uuid.uuid4()) cloud_node.name = "Cloud1" - + compute = MagicMock() response = MagicMock() response.json = { @@ -449,15 +360,15 @@ class TestLinkRoutes: "lport": 20000, "rhost": "127.0.0.1", "rport": 30000, - "name": "UDP tunnel 1" + "name": "UDP tunnel 1", } ] } compute.get = AsyncioMagicMock(return_value=response) cloud_node.compute = compute - + link._nodes = [{"node": cloud_node, "port_number": 1}] - + response = await client.get(app.url_path_for("get_iface", project_id=project.id, link_id=link.id)) assert response.status_code == status.HTTP_200_OK @@ -468,7 +379,6 @@ class TestLinkRoutes: assert result["rport"] == 30000 assert result["type"] == "udp" - async def test_get_ethernet_interface(self, app: FastAPI, client: AsyncClient, project: Project) -> None: """ Test getting ethernet interface information from a link. @@ -484,14 +394,7 @@ class TestLinkRoutes: compute = MagicMock() response = MagicMock() response.json = { - "ports_mapping": [ - { - "port_number": 1, - "type": "ethernet", - "interface": "eth0", - "name": "Ethernet 1" - } - ] + "ports_mapping": [{"port_number": 1, "type": "ethernet", "interface": "eth0", "name": "Ethernet 1"}] } compute.get = AsyncioMagicMock(return_value=response) cloud_node.compute = compute @@ -504,4 +407,4 @@ class TestLinkRoutes: result = response.json() assert result["node_id"] == cloud_node.id assert result["interface"] == "eth0" - assert result["type"] == "ethernet" \ No newline at end of file + assert result["type"] == "ethernet" diff --git a/tests/api/routes/controller/test_marker_replay.py b/tests/api/routes/controller/test_marker_replay.py index b5db7398d..072190f4c 100644 --- a/tests/api/routes/controller/test_marker_replay.py +++ b/tests/api/routes/controller/test_marker_replay.py @@ -55,27 +55,29 @@ def _add_marker(project, tag, enabled, node_id, frames=None): """Create a paused/capturing link+marker and optionally its pcap.""" link = UDPLink(project) - link._markers["icmp"] = {"bpf": "icmp", "tag": tag, "enabled": enabled, "color": None, - "highlight_duration": None, "capture_node_id": node_id, - "direction": None, "data_link_type": "DLT_EN10MB"} + link._markers["icmp"] = { + "bpf": "icmp", + "tag": tag, + "enabled": enabled, + "color": None, + "highlight_duration": None, + "capture_node_id": node_id, + "direction": None, + "data_link_type": "DLT_EN10MB", + } project._links[link.id] = link if frames is not None: - _write_pcap( - f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames - ) + _write_pcap(f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames) return link class TestReplayRoutes: - async def test_range_409_while_capturing(self, app: FastAPI, client: AsyncClient, project: Project) -> None: _add_marker(project, tag=7, enabled=False, node_id="n1") running = _add_marker(project, tag=7, enabled=True, node_id="n2") - response = await client.get( - app.url_path_for("replay_tag_range", project_id=project.id, tag=7) - ) + response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=7)) assert response.status_code == status.HTTP_409_CONFLICT assert "icmp" in response.json()["message"] assert running.id in response.json()["message"] @@ -84,23 +86,25 @@ class TestReplayRoutes: _add_marker(project, tag=7, enabled=False, node_id="n1") - response = await client.get( - app.url_path_for("replay_tag_range", project_id=project.id, tag=99) - ) + response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=99)) assert response.status_code == status.HTTP_404_NOT_FOUND async def test_range_501_without_sharkd(self, app: FastAPI, client: AsyncClient, project: Project) -> None: # A non-empty pcap: the engine must be consulted, and without sharkd # the whole feature is unavailable (hard requirement, no degraded mode). - _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 123456, _icmp_frame()), - ]) + _add_marker( + project, + tag=7, + enabled=False, + node_id="n1", + frames=[ + (1693472000, 123456, _icmp_frame()), + ], + ) with patch("gns3server.controller.marker_replay.shutil.which", return_value=None): - response = await client.get( - app.url_path_for("replay_tag_range", project_id=project.id, tag=7) - ) + response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=7)) assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED # The app's HTTPException handler unifies the body as {"message": …}. assert "sharkd" in response.json()["message"] @@ -115,9 +119,16 @@ class TestReplayRoutes: project._links.update({r1.id: r1, r2.id: r2}) def _wire(link, node_id, frames): - link._markers["icmp"] = {"bpf": "icmp", "tag": 7, "enabled": False, "color": None, - "highlight_duration": None, "capture_node_id": node_id, - "direction": None, "data_link_type": "DLT_EN10MB"} + link._markers["icmp"] = { + "bpf": "icmp", + "tag": 7, + "enabled": False, + "color": None, + "highlight_duration": None, + "capture_node_id": node_id, + "direction": None, + "data_link_type": "DLT_EN10MB", + } _write_pcap(f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames) # r1→r2 captures at t1 and t3; r2→r3 captures at t2 and t3 (same µs @@ -132,9 +143,7 @@ class TestReplayRoutes: monkeypatch.setattr(marker_replay, "_columns_for", fake_columns) - response = await client.get( - app.url_path_for("replay_tag_range", project_id=project.id, tag=7) - ) + response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=7)) assert response.status_code == status.HTTP_200_OK body = response.json() assert body["tag"] == 7 @@ -143,8 +152,10 @@ class TestReplayRoutes: assert body["end"] == "1693472002.000000" assert [f["node_id"] for f in body["frames"]] == ["n1", "n2", "n1", "n2"] assert [f["ts"] for f in body["frames"]] == [ - "1693472000.500000", "1693472001.000000", - "1693472002.000000", "1693472002.000000", + "1693472000.500000", + "1693472001.000000", + "1693472002.000000", + "1693472002.000000", ] # Wireshark-style columns ride along on every frame entry. assert body["frames"][0]["src"] == "10.0.0.1" @@ -156,9 +167,15 @@ class TestReplayRoutes: self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch ) -> None: - _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 0, b"a" * 60), - ]) + _add_marker( + project, + tag=7, + enabled=False, + node_id="n1", + frames=[ + (1693472000, 0, b"a" * 60), + ], + ) async def fake_columns(pcap, filter_expr): return {1: _cols()} @@ -172,14 +189,18 @@ class TestReplayRoutes: assert response.status_code == status.HTTP_200_OK assert response.json() == {"frames": []} - async def test_frames_window_hit( - self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch - ) -> None: + async def test_frames_window_hit(self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch) -> None: - _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 0, b"a" * 60), - (1693472000, 150000, b"a" * 60), - ]) + _add_marker( + project, + tag=7, + enabled=False, + node_id="n1", + frames=[ + (1693472000, 0, b"a" * 60), + (1693472000, 150000, b"a" * 60), + ], + ) async def fake_columns(pcap, filter_expr): return {1: _cols(), 2: _cols()} @@ -191,15 +212,19 @@ class TestReplayRoutes: params={"ts": "1693472000.000000", "window_ms": 150}, ) assert response.status_code == status.HTTP_200_OK - assert [f["ts"] for f in response.json()["frames"]] == [ - "1693472000.000000", "1693472000.150000" - ] + assert [f["ts"] for f in response.json()["frames"]] == ["1693472000.000000", "1693472000.150000"] async def test_detail_404_on_ts_mismatch(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 123456, _icmp_frame()), - ]) + link = _add_marker( + project, + tag=7, + enabled=False, + node_id="n1", + frames=[ + (1693472000, 123456, _icmp_frame()), + ], + ) response = await client.get( app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), @@ -216,9 +241,16 @@ class TestReplayRoutes: project._links.update({r1.id: r1, r2.id: r2}) def _wire(link, node_id, frames): - link._markers["icmp"] = {"bpf": "icmp", "tag": 7, "enabled": False, "color": None, - "highlight_duration": None, "capture_node_id": node_id, - "direction": None, "data_link_type": "DLT_EN10MB"} + link._markers["icmp"] = { + "bpf": "icmp", + "tag": 7, + "enabled": False, + "color": None, + "highlight_duration": None, + "capture_node_id": node_id, + "direction": None, + "data_link_type": "DLT_EN10MB", + } _write_pcap(f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames) _wire(r1, "n1", [(1693472000, 0, b"a" * 60), (1693472002, 0, b"a" * 60)]) @@ -254,15 +286,19 @@ class TestReplayRoutes: self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions ) -> None: - _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 123456, _icmp_frame()), - (1693472001, 0, _tcp_syn_frame()), - ]) + _add_marker( + project, + tag=7, + enabled=False, + node_id="n1", + frames=[ + (1693472000, 123456, _icmp_frame()), + (1693472001, 0, _tcp_syn_frame()), + ], + ) # Unfiltered: both frames with real engine columns. - response = await client.get( - app.url_path_for("replay_tag_range", project_id=project.id, tag=7) - ) + response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=7)) assert response.status_code == status.HTTP_200_OK frames = response.json()["frames"] assert [f["proto"] for f in frames] == ["ICMP", "TCP"] @@ -285,9 +321,15 @@ class TestReplayRoutes: self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions ) -> None: - _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 123456, _icmp_frame()), - ]) + _add_marker( + project, + tag=7, + enabled=False, + node_id="n1", + frames=[ + (1693472000, 123456, _icmp_frame()), + ], + ) response = await client.get( app.url_path_for("replay_tag_range", project_id=project.id, tag=7), @@ -308,14 +350,19 @@ class TestReplayRoutes: self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions ) -> None: - link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 123456, _icmp_frame()), - ]) + link = _add_marker( + project, + tag=7, + enabled=False, + node_id="n1", + frames=[ + (1693472000, 123456, _icmp_frame()), + ], + ) response = await client.get( app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), - params={"ts": "1693472000.123456", "node_id": "n1", - "link_id": link.id, "marker": "icmp"}, + params={"ts": "1693472000.123456", "node_id": "n1", "link_id": link.id, "marker": "icmp"}, ) assert response.status_code == status.HTTP_200_OK body = response.json() @@ -345,12 +392,17 @@ class TestReplayRoutes: # Two frames in the same microsecond: only the explicit frame number # (from the frame list entry) tells them apart. - link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 123456, _icmp_frame()), - (1693472000, 123456, _tcp_syn_frame()), - ]) - common = {"ts": "1693472000.123456", "node_id": "n1", - "link_id": link.id, "marker": "icmp"} + link = _add_marker( + project, + tag=7, + enabled=False, + node_id="n1", + frames=[ + (1693472000, 123456, _icmp_frame()), + (1693472000, 123456, _tcp_syn_frame()), + ], + ) + common = {"ts": "1693472000.123456", "node_id": "n1", "link_id": link.id, "marker": "icmp"} response = await client.get( app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), @@ -369,18 +421,21 @@ class TestReplayRoutes: assert response.json()["hex"] == _tcp_syn_frame().hex() @sharkd_present - async def test_detail_501_when_sharkd_disappears( - self, app: FastAPI, client: AsyncClient, project: Project - ) -> None: + async def test_detail_501_when_sharkd_disappears(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[ - (1693472000, 123456, _icmp_frame()), - ]) + link = _add_marker( + project, + tag=7, + enabled=False, + node_id="n1", + frames=[ + (1693472000, 123456, _icmp_frame()), + ], + ) with patch("gns3server.controller.marker_replay.shutil.which", return_value=None): response = await client.get( app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7), - params={"ts": "1693472000.123456", "node_id": "n1", - "link_id": link.id, "marker": "icmp"}, + params={"ts": "1693472000.123456", "node_id": "n1", "link_id": link.id, "marker": "icmp"}, ) assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED diff --git a/tests/api/routes/controller/test_markers.py b/tests/api/routes/controller/test_markers.py index 53e57e7d9..2e088ed3c 100644 --- a/tests/api/routes/controller/test_markers.py +++ b/tests/api/routes/controller/test_markers.py @@ -35,14 +35,17 @@ pytestmark = pytest.mark.asyncio def _inherited(link, name="arp"): """Inject an inherited marker so the controller's inheritance guard can fire.""" link._markers[f"global-{name}"] = { - "bpf": name, "tag": None, "enabled": True, "color": None, - "highlight_duration": None, "capture_node_id": "node-id", + "bpf": name, + "tag": None, + "enabled": True, + "color": None, + "highlight_duration": None, + "capture_node_id": "node-id", "inherited_from": name, } class TestMarkerRoutes: - # ----------------------------------------------------------------------- # Per-link markers # ----------------------------------------------------------------------- @@ -81,7 +84,9 @@ class TestMarkerRoutes: _, kwargs = mock.call_args assert kwargs["name"] == "web" - async def test_create_marker_global_prefix_rejected(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + async def test_create_marker_global_prefix_rejected( + self, app: FastAPI, client: AsyncClient, project: Project + ) -> None: link = UDPLink(project) project._links = {link.id: link} @@ -105,7 +110,9 @@ class TestMarkerRoutes: ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - async def test_create_marker_name_too_long_rejected(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + async def test_create_marker_name_too_long_rejected( + self, app: FastAPI, client: AsyncClient, project: Project + ) -> None: link = UDPLink(project) project._links = {link.id: link} @@ -119,13 +126,17 @@ class TestMarkerRoutes: async def test_get_markers(self, app: FastAPI, client: AsyncClient, project: Project) -> None: link = UDPLink(project) - link._markers["web"] = {"bpf": "tcp port 80", "tag": None, "enabled": True, - "color": None, "highlight_duration": 800, "capture_node_id": "n1"} + link._markers["web"] = { + "bpf": "tcp port 80", + "tag": None, + "enabled": True, + "color": None, + "highlight_duration": 800, + "capture_node_id": "n1", + } project._links = {link.id: link} - response = await client.get( - app.url_path_for("get_markers", project_id=project.id, link_id=link.id) - ) + response = await client.get(app.url_path_for("get_markers", project_id=project.id, link_id=link.id)) assert response.status_code == status.HTTP_200_OK assert response.json()["web"]["highlight_duration"] == 800 @@ -197,7 +208,9 @@ class TestMarkerRoutes: assert kwargs["bpf"] == "arp" assert kwargs["highlight_duration"] == 1200 - async def test_create_marker_definition_global_prefix_rejected(self, app: FastAPI, client: AsyncClient, project: Project) -> None: + async def test_create_marker_definition_global_prefix_rejected( + self, app: FastAPI, client: AsyncClient, project: Project + ) -> None: with asyncio_patch("gns3server.controller.project.Project.create_marker_definition") as mock: response = await client.post( @@ -235,9 +248,7 @@ class TestMarkerRoutes: } project._links = {} - response = await client.get( - app.url_path_for("get_marker_definitions", project_id=project.id) - ) + response = await client.get(app.url_path_for("get_marker_definitions", project_id=project.id)) assert response.status_code == status.HTTP_200_OK body = response.json() assert body["arp"]["bpf"] == "arp" @@ -251,13 +262,17 @@ class TestMarkerRoutes: async def test_get_project_markers(self, app: FastAPI, client: AsyncClient, project: Project) -> None: link = UDPLink(project) - link._markers["icmp"] = {"bpf": "icmp", "tag": 1, "enabled": True, "color": "#ff5722", - "highlight_duration": 800, "capture_node_id": "node-1"} + link._markers["icmp"] = { + "bpf": "icmp", + "tag": 1, + "enabled": True, + "color": "#ff5722", + "highlight_duration": 800, + "capture_node_id": "node-1", + } project._links = {link.id: link} - response = await client.get( - app.url_path_for("get_project_markers", project_id=project.id) - ) + response = await client.get(app.url_path_for("get_project_markers", project_id=project.id)) assert response.status_code == status.HTTP_200_OK body = response.json() key = f"{link.id}/icmp" diff --git a/tests/api/routes/controller/test_netmiko.py b/tests/api/routes/controller/test_netmiko.py index 84fe8934c..6384161bb 100644 --- a/tests/api/routes/controller/test_netmiko.py +++ b/tests/api/routes/controller/test_netmiko.py @@ -24,7 +24,6 @@ pytestmark = pytest.mark.asyncio class TestNetmikoRoutes: - async def test_device_types(self, app: FastAPI, client: AsyncClient) -> None: """ Test listing the device types supported by the installed Netmiko library. diff --git a/tests/api/routes/controller/test_nodes.py b/tests/api/routes/controller/test_nodes.py index 6183503d7..d86e13fe0 100644 --- a/tests/api/routes/controller/test_nodes.py +++ b/tests/api/routes/controller/test_nodes.py @@ -49,74 +49,69 @@ pytestmark = pytest.mark.asyncio class TestNodeRoutes: - - @pytest.fixture def node(self, project: Project, compute: Compute) -> Node: - + node = Node(project, compute, "test", node_type="vpcs") project._nodes[node.id] = node return node - - + async def test_create_node(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None: - + response = MagicMock() response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - - response = await client.post(app.url_path_for("create_node", project_id=project.id), json={ - "name": "test", - "node_type": "vpcs", - "compute_id": "example.com", - "properties": { - "startup_script": "echo test" - } - }) - + + response = await client.post( + app.url_path_for("create_node", project_id=project.id), + json={ + "name": "test", + "node_type": "vpcs", + "compute_id": "example.com", + "properties": {"startup_script": "echo test"}, + }, + ) + assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "test" assert "name" not in response.json()["properties"] - - + async def test_list_node(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None: - + response = MagicMock() response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - - await client.post(app.url_path_for("create_node", project_id=project.id), json={ - "name": "test", - "node_type": "vpcs", - "compute_id": "example.com", - "properties": { - "startup_script": "echo test" - } - }) - + + await client.post( + app.url_path_for("create_node", project_id=project.id), + json={ + "name": "test", + "node_type": "vpcs", + "compute_id": "example.com", + "properties": {"startup_script": "echo test"}, + }, + ) + response = await client.get(app.url_path_for("get_nodes", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert response.json()[0]["name"] == "test" - + # test listing nodes from a closed project await project.close(ignore_notification=True) response = await client.get(app.url_path_for("get_nodes", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert response.json()[0]["name"] == "test" + async def test_list_node_with_missing_image( self, app: FastAPI, client: AsyncClient, project: Project, node: Node ) -> None: - node._missing_images = [ - {"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"} - ] + node._missing_images = [{"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"}] response = await client.get(app.url_path_for("get_nodes", project_id=project.id)) assert response.status_code == status.HTTP_200_OK data = response.json()[0] assert data["missing_image"] is True - assert data["missing_images"] == [ - {"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"} - ] + assert data["missing_images"] == [{"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"}] @pytest.mark.parametrize( "tags, expected_match", @@ -129,37 +124,33 @@ class TestNodeRoutes: ), ) async def test_list_nodes_with_tags( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - tags: list, - expected_match: bool + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, tags: list, expected_match: bool ) -> None: response = MagicMock() response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - await client.post(app.url_path_for("create_node", project_id=project.id), json={ - "name": "test", - "node_type": "vpcs", - "compute_id": "example.com", - "tags": ["tag1", "tag2"], - "properties": { - "startup_script": "echo test" - } - }) + await client.post( + app.url_path_for("create_node", project_id=project.id), + json={ + "name": "test", + "node_type": "vpcs", + "compute_id": "example.com", + "tags": ["tag1", "tag2"], + "properties": {"startup_script": "echo test"}, + }, + ) - await client.post(app.url_path_for("create_node", project_id=project.id), json={ - "name": "test2", - "node_type": "vpcs", - "compute_id": "example.com", - "tags": ["tag3", "tag4"], - "properties": { - "startup_script": "echo test" - } - }) + await client.post( + app.url_path_for("create_node", project_id=project.id), + json={ + "name": "test2", + "node_type": "vpcs", + "compute_id": "example.com", + "tags": ["tag3", "tag4"], + "properties": {"startup_script": "echo test"}, + }, + ) params = {"tags": tags} response = await client.get(app.url_path_for("get_nodes", project_id=project.id), params=params) @@ -169,161 +160,106 @@ class TestNodeRoutes: else: assert len(response.json()) == 0 - - async def test_get_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute - ) -> None: - + async def test_get_node(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None: + response = MagicMock() response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - - response = await client.post(app.url_path_for("create_node", project_id=project.id), json={ - "name": "test", - "node_type": "vpcs", - "compute_id": "example.com", - "properties": { - "startup_script": "echo test" - } - }) - - response = await client.get(app.url_path_for("get_node", project_id=project.id, node_id=response.json()["node_id"])) + + response = await client.post( + app.url_path_for("create_node", project_id=project.id), + json={ + "name": "test", + "node_type": "vpcs", + "compute_id": "example.com", + "properties": {"startup_script": "echo test"}, + }, + ) + + response = await client.get( + app.url_path_for("get_node", project_id=project.id, node_id=response.json()["node_id"]) + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test" - - + async def test_update_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = MagicMock() response.json = {"console": 2048} compute.put = AsyncioMagicMock(return_value=response) - - response = await client.put(app.url_path_for("update_node", project_id=project.id, node_id=node.id), json={ - "name": "test", - "node_type": "vpcs", - "compute_id": "example.com", - "tags": ["tag1", "tag2"], - "properties": { - "startup_script": "echo test" - } - }) - + + response = await client.put( + app.url_path_for("update_node", project_id=project.id, node_id=node.id), + json={ + "name": "test", + "node_type": "vpcs", + "compute_id": "example.com", + "tags": ["tag1", "tag2"], + "properties": {"startup_script": "echo test"}, + }, + ) + assert response.status_code == 200 assert response.json()["name"] == "test" assert "name" not in response.json()["properties"] assert response.json()["tags"] == ["tag1", "tag2"] + async def test_start_all_nodes(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None: - async def test_start_all_nodes( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute - ) -> None: - compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("start_all_nodes", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - - async def test_stop_all_nodes( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute - ) -> None: - + + async def test_stop_all_nodes(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None: + compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("stop_all_nodes", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_suspend_all_nodes( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute ) -> None: - + compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("suspend_all_nodes", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_reload_all_nodes( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute ) -> None: - + compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("reload_all_nodes", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_reset_console_all_nodes( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute ) -> None: - + compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("reset_console_all_nodes", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_start_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("start_node", project_id=project.id, node_id=node.id), json={}) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_stop_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("stop_node", project_id=project.id, node_id=node.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - + async def test_suspend_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: compute.post = AsyncioMagicMock() @@ -331,12 +267,7 @@ class TestNodeRoutes: assert response.status_code == status.HTTP_204_NO_CONTENT async def test_suspend_node_unsupported( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: # node types without suspend support (e.g. VPCS, IOU) must surface the @@ -348,12 +279,7 @@ class TestNodeRoutes: assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED async def test_suspend_all_nodes_tolerates_unsupported( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: # suspending all nodes of a mixed project stays best-effort: nodes @@ -363,259 +289,162 @@ class TestNodeRoutes: ) response = await client.post(app.url_path_for("suspend_all_nodes", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - - async def test_reload_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node - ): - + + async def test_reload_node(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node): + compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("reload_node", project_id=project.id, node_id=node.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_isolate_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ): - + compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("isolate_node", project_id=project.id, node_id=node.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_unisolate_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + compute.post = AsyncioMagicMock() response = await client.post(app.url_path_for("unisolate_node", project_id=project.id, node_id=node.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_duplicate_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = MagicMock() response.json({"console": 2035}) compute.post = AsyncioMagicMock(return_value=response) - - response = await client.post(app.url_path_for("duplicate_node", project_id=project.id, node_id=node.id), - json={"x": 10, "y": 5, "z": 0}) + + response = await client.post( + app.url_path_for("duplicate_node", project_id=project.id, node_id=node.id), json={"x": 10, "y": 5, "z": 0} + ) assert response.status_code == status.HTTP_201_CREATED - - + async def test_delete_node( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + compute.post = AsyncioMagicMock() response = await client.delete(app.url_path_for("delete_node", project_id=project.id, node_id=node.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_dynamips_idle_pc( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = MagicMock() response.json = {"idlepc": "0x60606f54"} compute.get = AsyncioMagicMock(return_value=response) - + node._node_type = "dynamips" # force Dynamips node type response = await client.get(app.url_path_for("auto_idlepc", project_id=project.id, node_id=node.id)) assert response.status_code == status.HTTP_200_OK assert response.json()["idlepc"] == "0x60606f54" - - + async def test_dynamips_idle_pc_wrong_node_type( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = await client.get(app.url_path_for("auto_idlepc", project_id=project.id, node_id=node.id)) assert response.status_code == status.HTTP_400_BAD_REQUEST - - + async def test_dynamips_idlepc_proposals( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = MagicMock() response.json = ["0x60606f54", "0x33805a22"] compute.get = AsyncioMagicMock(return_value=response) - + node._node_type = "dynamips" # force Dynamips node type response = await client.get(app.url_path_for("idlepc_proposals", project_id=project.id, node_id=node.id)) assert response.status_code == status.HTTP_200_OK assert response.json() == ["0x60606f54", "0x33805a22"] - - + async def test_dynamips_idlepc_proposals_wrong_node_type( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = await client.get(app.url_path_for("idlepc_proposals", project_id=project.id, node_id=node.id)) assert response.status_code == status.HTTP_400_BAD_REQUEST - - + async def test_qemu_disk_image_create( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = MagicMock() compute.post = AsyncioMagicMock(return_value=response) - + node._node_type = "qemu" # force Qemu node type response = await client.post( app.url_path_for("create_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2"), - json={"format": "qcow2", "size": 30} + json={"format": "qcow2", "size": 30}, ) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_disk_image_create_wrong_node_type( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = await client.post( app.url_path_for("create_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2"), - json={"format": "qcow2", "size": 30} + json={"format": "qcow2", "size": 30}, ) assert response.status_code == status.HTTP_400_BAD_REQUEST - - + async def test_qemu_disk_image_update( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = MagicMock() compute.put = AsyncioMagicMock(return_value=response) - + node._node_type = "qemu" # force Qemu node type response = await client.put( app.url_path_for("update_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2"), - json={"extend": 10} + json={"extend": 10}, ) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_disk_image_update_wrong_node_type( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = await client.put( app.url_path_for("update_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2"), - json={"extend": 10} + json={"extend": 10}, ) assert response.status_code == status.HTTP_400_BAD_REQUEST - - + async def test_qemu_disk_image_delete( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = MagicMock() compute.delete = AsyncioMagicMock(return_value=response) - + node._node_type = "qemu" # force Qemu node type response = await client.delete( app.url_path_for("delete_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2") ) assert response.status_code == status.HTTP_204_NO_CONTENT - - + async def test_qemu_disk_image_delete_wrong_node_type( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + response = await client.delete( app.url_path_for("delete_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2") ) assert response.status_code == status.HTTP_400_BAD_REQUEST - - + async def test_get_file( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: # Mock the streaming response @@ -632,56 +461,52 @@ class TestNodeRoutes: compute.http_query = AsyncioMagicMock(return_value=mock_response) - response = await client.get(app.url_path_for("get_file", project_id=project.id, node_id=node.id, file_path="hello")) + response = await client.get( + app.url_path_for("get_file", project_id=project.id, node_id=node.id, file_path="hello") + ) assert response.status_code == status.HTTP_200_OK - assert response.content == b'world' + assert response.content == b"world" compute.http_query.assert_called_with( "GET", "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello".format( - project_id=project.id, - node_id=node.id), + project_id=project.id, node_id=node.id + ), timeout=None, - stream=True) + stream=True, + ) - response = await client.get(app.url_path_for( - "get_file", - project_id=project.id, - node_id=node.id, - file_path="../hello")) + response = await client.get( + app.url_path_for("get_file", project_id=project.id, node_id=node.id, file_path="../hello") + ) assert response.status_code == status.HTTP_404_NOT_FOUND - - + async def test_post_file( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: - + compute.http_query = AsyncioMagicMock() - response = await client.post(app.url_path_for( - "post_file", - project_id=project.id, - node_id=node.id, - file_path="hello"), content=b"hello") + response = await client.post( + app.url_path_for("post_file", project_id=project.id, node_id=node.id, file_path="hello"), content=b"hello" + ) assert response.status_code == status.HTTP_201_CREATED # Verify http_query was called with stream parameter compute.http_query.assert_called_once() call_args = compute.http_query.call_args assert call_args[0][0] == "POST" - assert call_args[0][1] == "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello".format(project_id=project.id, node_id=node.id) + assert call_args[0][1] == "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello".format( + project_id=project.id, node_id=node.id + ) assert call_args[1]["timeout"] is None # data should be an async generator from request.stream() assert hasattr(call_args[1]["data"], "__aiter__") - - response = await client.get("/projects/{project_id}/nodes/{node_id}/files/../hello".format(project_id=project.id, node_id=node.id)) + + response = await client.get( + "/projects/{project_id}/nodes/{node_id}/files/../hello".format(project_id=project.id, node_id=node.id) + ) assert response.status_code == status.HTTP_404_NOT_FOUND - - + # @pytest.mark.asyncio # async def test_get_and_post_with_nested_paths_normalization(controller_api, project, node, compute): # @@ -701,8 +526,6 @@ class TestNodeRoutes: # compute.http_query.assert_called_with("POST", "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello/nested".format(project_id=project.id, node_id=node.id), data=b'hello', timeout=None, raw=True) - - class FakeComputeConsoleWebSocket: """ Stand-in for the aiohttp ClientWebSocketResponse returned when connecting @@ -823,21 +646,21 @@ class TestNodeConsoleWebSocketRoutes: monkeypatch.setattr( HTTPClient, "get_client", - classmethod(lambda cls: SimpleNamespace(ws_connect=lambda *args, **kwargs: compute_ws)) + classmethod(lambda cls: SimpleNamespace(ws_connect=lambda *args, **kwargs: compute_ws)), ) return compute_ws async def test_console_forwards_compute_output_to_client( - self, - compute_credentials, - node: Node, - monkeypatch + self, compute_credentials, node: Node, monkeypatch ) -> None: - compute_ws = self._forward_compute_ws(monkeypatch, [ - aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "device output", None), - aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x00\x01", None), - ]) + compute_ws = self._forward_compute_ws( + monkeypatch, + [ + aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "device output", None), + aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x00\x01", None), + ], + ) websocket = FakeClientWebSocket() await ws_console(websocket, current_user=MagicMock(), node=node) @@ -846,19 +669,18 @@ class TestNodeConsoleWebSocketRoutes: assert compute_ws.closed async def test_console_client_disconnect_mid_stream( - self, - compute_credentials, - node: Node, - monkeypatch, - caplog + self, compute_credentials, node: Node, monkeypatch, caplog ) -> None: # regression test: the client disconnects while the compute is still # streaming console output, send must not leak WebSocketDisconnect - compute_ws = self._forward_compute_ws(monkeypatch, [ - aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 1", None), - aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 2", None), - aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 3", None), - ]) + compute_ws = self._forward_compute_ws( + monkeypatch, + [ + aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 1", None), + aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 2", None), + aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 3", None), + ], + ) websocket = FakeClientWebSocket(fail_after=1) with caplog.at_level(logging.INFO): @@ -868,17 +690,15 @@ class TestNodeConsoleWebSocketRoutes: assert compute_ws.closed assert any("has disconnected from controller console WebSocket" in record.message for record in caplog.records) - async def test_vnc_console_client_disconnect_mid_stream( - self, - compute_credentials, - node: Node, - monkeypatch - ) -> None: + async def test_vnc_console_client_disconnect_mid_stream(self, compute_credentials, node: Node, monkeypatch) -> None: # regression test: same as above for the VNC console forwarding loop - compute_ws = self._forward_compute_ws(monkeypatch, [ - aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x01\x02", None), - aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x03\x04", None), - ]) + compute_ws = self._forward_compute_ws( + monkeypatch, + [ + aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x01\x02", None), + aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x03\x04", None), + ], + ) websocket = FakeClientWebSocket(fail_after=0) await vnc_console(websocket, current_user=MagicMock(), node=node) @@ -978,10 +798,9 @@ class TestConsoleWebSocketRoutes: stack = contextlib.ExitStack() http_client = MagicMock() http_client.ws_connect = MagicMock(return_value=_FakeComputeWebSocketContext(fake_ws)) - stack.enter_context(patch( - "gns3server.api.routes.controller.nodes.HTTPClient.get_client", - return_value=http_client - )) + stack.enter_context( + patch("gns3server.api.routes.controller.nodes.HTTPClient.get_client", return_value=http_client) + ) stack.enter_context(patch.object(ASGIWebSocketAsyncNetworkStream, "send", _conforming_stream_send)) return stack @@ -1000,13 +819,7 @@ class TestConsoleWebSocketRoutes: await asyncio.sleep(0.05) async def test_console_ws_client_disconnect( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node, - caplog + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node, caplog ) -> None: """ A client disconnecting while the compute keeps streaming console output @@ -1033,27 +846,23 @@ class TestConsoleWebSocketRoutes: assert fake_ws.stream_cancelled, "the compute -> client forwarding task should have been cancelled" assert fake_ws.closed, "the compute console WebSocket should have been closed" assert any( - "has disconnected from controller console WebSocket" in record.getMessage() - for record in caplog.records + "has disconnected from controller console WebSocket" in record.getMessage() for record in caplog.records ) async def test_console_ws_compute_closes_session( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node ) -> None: """ When the compute closes the console WebSocket the client should receive the frames sent before the close, then the close itself. """ - fake_ws = _FakeComputeWebSocket(messages=[ - aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"output", ""), - aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "done", ""), - ]) + fake_ws = _FakeComputeWebSocket( + messages=[ + aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"output", ""), + aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "done", ""), + ] + ) with self._patches(fake_ws): async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app=app)) as ws_client: async with aconnect_ws( @@ -1069,13 +878,7 @@ class TestConsoleWebSocketRoutes: assert fake_ws.closed async def test_vnc_console_ws_client_disconnect( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - compute: Compute, - node: Node, - caplog + self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node, caplog ) -> None: """ Same as the console test, for the VNC endpoint (binary frames only). @@ -1097,6 +900,5 @@ class TestConsoleWebSocketRoutes: assert fake_ws.stream_cancelled, "the compute -> client forwarding task should have been cancelled" assert fake_ws.closed, "the compute VNC console WebSocket should have been closed" assert any( - "has disconnected from controller VNC console WebSocket" in record.getMessage() - for record in caplog.records + "has disconnected from controller VNC console WebSocket" in record.getMessage() for record in caplog.records ) diff --git a/tests/api/routes/controller/test_pools.py b/tests/api/routes/controller/test_pools.py index 38782e442..f3b78cccb 100644 --- a/tests/api/routes/controller/test_pools.py +++ b/tests/api/routes/controller/test_pools.py @@ -32,7 +32,6 @@ pytestmark = pytest.mark.asyncio class TestPoolRoutes: - async def test_resource_pool(self, app: FastAPI, client: AsyncClient) -> None: new_group = {"name": "pool1"} @@ -60,28 +59,23 @@ class TestPoolRoutes: update_pool = {"name": "pool42"} response = await client.put( - app.url_path_for("update_resource_pool", resource_pool_id=pool_in_db.resource_pool_id), - json=update_pool + app.url_path_for("update_resource_pool", resource_pool_id=pool_in_db.resource_pool_id), json=update_pool ) assert response.status_code == status.HTTP_200_OK updated_pool_in_db = await pools_repo.get_resource_pool(pool_in_db.resource_pool_id) assert updated_pool_in_db.name == "pool42" - async def test_resource_group( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_resource_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: pools_repo = ResourcePoolsRepository(db_session) pool_in_db = await pools_repo.get_resource_pool_by_name("pool42") - response = await client.delete(app.url_path_for("delete_resource_pool", resource_pool_id=pool_in_db.resource_pool_id)) + response = await client.delete( + app.url_path_for("delete_resource_pool", resource_pool_id=pool_in_db.resource_pool_id) + ) assert response.status_code == status.HTTP_204_NO_CONTENT class TestResourcesPoolRoutes: - @pytest_asyncio.fixture async def project(self, app: FastAPI, client: AsyncClient, controller: Controller) -> Project: project_id = str(uuid.uuid4()) @@ -90,11 +84,7 @@ class TestResourcesPoolRoutes: return controller.get_project(project_id) async def test_add_resource_to_pool( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - project: Project + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, project: Project ) -> None: pools_repo = ResourcePoolsRepository(db_session) @@ -104,9 +94,7 @@ class TestResourcesPoolRoutes: pool_in_db = await pools_repo.create_resource_pool(new_resource_pool) response = await client.put( app.url_path_for( - "add_resource_to_pool", - resource_pool_id=pool_in_db.resource_pool_id, - resource_id=str(project.id) + "add_resource_to_pool", resource_pool_id=pool_in_db.resource_pool_id, resource_id=str(project.id) ) ) assert response.status_code == status.HTTP_204_NO_CONTENT @@ -115,11 +103,7 @@ class TestResourcesPoolRoutes: assert str(resources[0].resource_id) == project.id async def test_add_to_resource_already_in_resource_pool( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - project: Project + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, project: Project ) -> None: pools_repo = ResourcePoolsRepository(db_session) @@ -132,34 +116,23 @@ class TestResourcesPoolRoutes: app.url_path_for( "add_resource_to_pool", resource_pool_id=pool_in_db.resource_pool_id, - resource_id=str(resource.resource_id) + resource_id=str(resource.resource_id), ) ) assert response.status_code == status.HTTP_400_BAD_REQUEST - async def test_get_pool_resources( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_get_pool_resources(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: pools_repo = ResourcePoolsRepository(db_session) pool_in_db = await pools_repo.get_resource_pool_by_name("pool1") response = await client.get( - app.url_path_for( - "get_pool_resources", - resource_pool_id=pool_in_db.resource_pool_id) + app.url_path_for("get_pool_resources", resource_pool_id=pool_in_db.resource_pool_id) ) assert response.status_code == status.HTTP_200_OK assert len(response.json()) == 2 async def test_remove_resource_from_pool( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - project: Project + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, project: Project ) -> None: pools_repo = ResourcePoolsRepository(db_session) @@ -173,9 +146,7 @@ class TestResourcesPoolRoutes: response = await client.delete( app.url_path_for( - "remove_resource_from_pool", - resource_pool_id=pool_in_db.resource_pool_id, - resource_id=str(project.id) + "remove_resource_from_pool", resource_pool_id=pool_in_db.resource_pool_id, resource_id=str(project.id) ), ) assert response.status_code == status.HTTP_204_NO_CONTENT diff --git a/tests/api/routes/controller/test_privileges.py b/tests/api/routes/controller/test_privileges.py index 273ac1bec..6efcae597 100644 --- a/tests/api/routes/controller/test_privileges.py +++ b/tests/api/routes/controller/test_privileges.py @@ -19,7 +19,6 @@ pytestmark = pytest.mark.asyncio class TestPrivilegesRoute: - async def test_get_privileges(self, app: FastAPI, client: AsyncClient) -> None: response = await client.get(app.url_path_for("get_privileges")) assert response.status_code == status.HTTP_200_OK diff --git a/tests/api/routes/controller/test_projects.py b/tests/api/routes/controller/test_projects.py index c5d32922a..e94b7e273 100644 --- a/tests/api/routes/controller/test_projects.py +++ b/tests/api/routes/controller/test_projects.py @@ -35,202 +35,157 @@ pytestmark = pytest.mark.asyncio class TestControllerProjectRoutes: - @pytest_asyncio.fixture async def project(self, app: FastAPI, client: AsyncClient, controller: Controller) -> Project: - + project_id = str(uuid.uuid4()) params = {"name": "test", "project_id": project_id} await client.post(app.url_path_for("create_project"), json=params) return controller.get_project(project_id) - - + async def test_create_project_with_path( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller, - config + self, app: FastAPI, client: AsyncClient, controller: Controller, config ) -> None: - - params = {"name": "test", "path": str(config.settings.Server.projects_path), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"} + + params = { + "name": "test", + "path": str(config.settings.Server.projects_path), + "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", + } response = await client.post(app.url_path_for("create_project"), json=params) # The projects directory itself must never become a project directory assert response.status_code == status.HTTP_403_FORBIDDEN - params = {"name": "test", "path": os.path.join(str(config.settings.Server.projects_path), "custom"), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"} + params = { + "name": "test", + "path": os.path.join(str(config.settings.Server.projects_path), "custom"), + "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f", + } response = await client.post(app.url_path_for("create_project"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "test" assert response.json()["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f" assert response.json()["status"] == "opened" - - - async def test_create_project_without_dir( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller - ) -> None: - + + async def test_create_project_without_dir(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None: + params = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f"} response = await client.post(app.url_path_for("create_project"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["project_id"] == "10010203-0405-0607-0809-0a0b0c0d0e0f" assert response.json()["name"] == "test" - - - async def test_create_project_with_uuid( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller - ) -> None: - + + async def test_create_project_with_uuid(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None: + params = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f"} response = await client.post(app.url_path_for("create_project"), json=params) assert response.status_code == 201 assert response.json()["project_id"] == "30010203-0405-0607-0809-0a0b0c0d0e0f" assert response.json()["name"] == "test" - - + async def test_create_project_with_variables( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller + self, app: FastAPI, client: AsyncClient, controller: Controller ) -> None: - - variables = [ - {"name": "TEST1"}, - {"name": "TEST2", "value": "value1"} - ] + + variables = [{"name": "TEST1"}, {"name": "TEST2", "value": "value1"}] params = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f", "variables": variables} response = await client.post(app.url_path_for("create_project"), json=params) assert response.status_code == 201 - assert response.json()["variables"] == [ - {"name": "TEST1"}, - {"name": "TEST2", "value": "value1"} - ] - + assert response.json()["variables"] == [{"name": "TEST1"}, {"name": "TEST2", "value": "value1"}] async def test_create_project_with_supplier( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller + self, app: FastAPI, client: AsyncClient, controller: Controller ) -> None: - - supplier = { - 'logo': 'logo.png', - 'url': 'http://example.com/' - } + + supplier = {"logo": "logo.png", "url": "http://example.com/"} params = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f", "supplier": supplier} response = await client.post(app.url_path_for("create_project"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["supplier"] == supplier - - + async def test_update_project(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None: - + params = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f"} response = await client.post(app.url_path_for("create_project"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["project_id"] == "10010203-0405-0607-0809-0a0b0c0d0e0f" assert response.json()["name"] == "test" - + params = {"name": "test2"} - response = await client.put(app.url_path_for("update_project", project_id="10010203-0405-0607-0809-0a0b0c0d0e0f"), - json=params) + response = await client.put( + app.url_path_for("update_project", project_id="10010203-0405-0607-0809-0a0b0c0d0e0f"), json=params + ) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test2" - - + async def test_update_project_with_variables( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller + self, app: FastAPI, client: AsyncClient, controller: Controller ) -> None: - - variables = [ - {"name": "TEST1"}, - {"name": "TEST2", "value": "value1"} - ] + + variables = [{"name": "TEST1"}, {"name": "TEST2", "value": "value1"}] params = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f", "variables": variables} response = await client.post(app.url_path_for("create_project"), json=params) assert response.status_code == status.HTTP_201_CREATED - + params = {"name": "test2"} - response = await client.put(app.url_path_for("update_project", project_id="10010203-0405-0607-0809-0a0b0c0d0e0f"), - json=params) + response = await client.put( + app.url_path_for("update_project", project_id="10010203-0405-0607-0809-0a0b0c0d0e0f"), json=params + ) assert response.status_code == status.HTTP_200_OK assert response.json()["variables"] == variables - - + async def test_list_projects(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None: - + params = {"name": "test", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"} await client.post(app.url_path_for("create_project"), json=params) response = await client.get(app.url_path_for("get_projects")) assert response.status_code == status.HTTP_200_OK projects = response.json() assert projects[0]["name"] == "test" - - + async def test_get_project(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + response = await client.get(app.url_path_for("get_project", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert response.json()["name"] == "test" - - + async def test_delete_project( - self, - app: FastAPI, - client: AsyncClient, - project: Project, - controller: Controller + self, app: FastAPI, client: AsyncClient, project: Project, controller: Controller ) -> None: - + with asyncio_patch("gns3server.controller.project.Project.delete", return_value=True) as mock: response = await client.delete(app.url_path_for("delete_project", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called assert project not in controller.projects - - + async def test_delete_project_invalid_uuid(self, app: FastAPI, client: AsyncClient) -> None: - + response = await client.delete(app.url_path_for("delete_project", project_id=str(uuid.uuid4()))) assert response.status_code == status.HTTP_404_NOT_FOUND - - + async def test_close_project(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + with asyncio_patch("gns3server.controller.project.Project.close", return_value=True) as mock: response = await client.post(app.url_path_for("close_project", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT assert mock.called - - + async def test_open_project(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + with asyncio_patch("gns3server.controller.project.Project.open", return_value=True) as mock: response = await client.post(app.url_path_for("open_project", project_id=project.id)) assert response.status_code == status.HTTP_201_CREATED assert mock.called - - + async def test_load_project(self, app: FastAPI, client: AsyncClient, project: Project, config) -> None: - + with asyncio_patch("gns3server.controller.Controller.load_project", return_value=project) as mock: response = await client.post(app.url_path_for("load_project"), json={"path": "/tmp/test.gns3"}) assert response.status_code == status.HTTP_201_CREATED mock.assert_called_with("/tmp/test.gns3") assert response.json()["project_id"] == project.id - - + # @pytest.mark.asyncio # async def test_notification(controller_api, http_client, project, controller): # @@ -250,8 +205,7 @@ class TestControllerProjectRoutes: # # response = await controller_api.get("/projects/{project_id}/notifications".format(project_id=uuid.uuid4())) # assert response.status_code == 404 - - + # @pytest.mark.asyncio # async def test_notification_ws(controller_api, http_client, controller, project): # @@ -269,179 +223,155 @@ class TestControllerProjectRoutes: # await ws.close() # # assert project.status_code == "opened" - - - async def test_export_with_images( - self, - app: FastAPI, - client: AsyncClient, - tmpdir, - project: Project - ) -> None: - + + async def test_export_with_images(self, app: FastAPI, client: AsyncClient, tmpdir, project: Project) -> None: + project.dump = MagicMock() os.makedirs(project.path, exist_ok=True) - with open(os.path.join(project.path, 'a'), 'w+') as f: - f.write('hello') - + with open(os.path.join(project.path, "a"), "w+") as f: + f.write("hello") + os.makedirs(str(tmpdir / "IOS")) with open(str(tmpdir / "IOS" / "test.image"), "w+") as f: f.write("AAA") - - topology = { - "topology": { - "nodes": [ - { - "properties": { - "image": "test.image" - }, - "node_type": "dynamips" - } - ] - } - } - with open(os.path.join(project.path, "test.gns3"), 'w+') as f: + + topology = {"topology": {"nodes": [{"properties": {"image": "test.image"}, "node_type": "dynamips"}]}} + with open(os.path.join(project.path, "test.gns3"), "w+") as f: json.dump(topology, f) - + with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS")): - response = await client.get(app.url_path_for("export_project", project_id=project.id), - params={"include_images": "yes"}) + response = await client.get( + app.url_path_for("export_project", project_id=project.id), params={"include_images": "yes"} + ) assert response.status_code == status.HTTP_200_OK - assert response.headers['CONTENT-TYPE'] == 'application/gns3project' - assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name) - - with open(str(tmpdir / 'project.zip'), 'wb+') as f: + assert response.headers["CONTENT-TYPE"] == "application/gns3project" + assert response.headers[ + "CONTENT-DISPOSITION" + ] == "attachment; filename=\"{name}.gns3project\"; filename*=UTF-8''{name}.gns3project".format( + name=project.name + ) + + with open(str(tmpdir / "project.zip"), "wb+") as f: f.write(response.content) - - with zipfile_zstd.ZipFile(str(tmpdir / 'project.zip')) as myzip: + + with zipfile_zstd.ZipFile(str(tmpdir / "project.zip")) as myzip: with myzip.open("a") as myfile: content = myfile.read() assert content == b"hello" myzip.getinfo("images/IOS/test.image") - - + async def test_export_without_images(self, app: FastAPI, client: AsyncClient, tmpdir, project: Project) -> None: - + project.dump = MagicMock() os.makedirs(project.path, exist_ok=True) - with open(os.path.join(project.path, 'a'), 'w+') as f: - f.write('hello') - + with open(os.path.join(project.path, "a"), "w+") as f: + f.write("hello") + os.makedirs(str(tmpdir / "IOS")) with open(str(tmpdir / "IOS" / "test.image"), "w+") as f: f.write("AAA") - - topology = { - "topology": { - "nodes": [ - { - "properties": { - "image": "test.image" - }, - "node_type": "dynamips" - } - ] - } - } - with open(os.path.join(project.path, "test.gns3"), 'w+') as f: + + topology = {"topology": {"nodes": [{"properties": {"image": "test.image"}, "node_type": "dynamips"}]}} + with open(os.path.join(project.path, "test.gns3"), "w+") as f: json.dump(topology, f) - - with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),): - response = await client.get(app.url_path_for("export_project", project_id=project.id), - params={"include_images": "0"}) + + with patch( + "gns3server.compute.Dynamips.get_images_directory", + return_value=str(tmpdir / "IOS"), + ): + response = await client.get( + app.url_path_for("export_project", project_id=project.id), params={"include_images": "0"} + ) assert response.status_code == status.HTTP_200_OK - assert response.headers['CONTENT-TYPE'] == 'application/gns3project' - assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name) - with open(str(tmpdir / 'project.zip'), 'wb+') as f: + assert response.headers["CONTENT-TYPE"] == "application/gns3project" + assert response.headers[ + "CONTENT-DISPOSITION" + ] == "attachment; filename=\"{name}.gns3project\"; filename*=UTF-8''{name}.gns3project".format( + name=project.name + ) + with open(str(tmpdir / "project.zip"), "wb+") as f: f.write(response.content) - - with zipfile_zstd.ZipFile(str(tmpdir / 'project.zip')) as myzip: + + with zipfile_zstd.ZipFile(str(tmpdir / "project.zip")) as myzip: with myzip.open("a") as myfile: content = myfile.read() assert content == b"hello" # Image should not exported with pytest.raises(KeyError): myzip.getinfo("images/IOS/test.image") - - + @pytest.mark.parametrize( "compression, compression_level, status_code", ( - ("none", None, status.HTTP_200_OK), - ("none", 4, status.HTTP_400_BAD_REQUEST), - ("zip", None, status.HTTP_200_OK), - ("zip", 1, status.HTTP_200_OK), - ("zip", 12, status.HTTP_400_BAD_REQUEST), - ("bzip2", None, status.HTTP_200_OK), - ("bzip2", 1, status.HTTP_200_OK), - ("bzip2", 13, status.HTTP_400_BAD_REQUEST), - ("lzma", None, status.HTTP_200_OK), - ("lzma", 1, status.HTTP_400_BAD_REQUEST), - ("zstd", None, status.HTTP_200_OK), - ("zstd", 12, status.HTTP_200_OK), - ("zstd", 23, status.HTTP_400_BAD_REQUEST), - ) + ("none", None, status.HTTP_200_OK), + ("none", 4, status.HTTP_400_BAD_REQUEST), + ("zip", None, status.HTTP_200_OK), + ("zip", 1, status.HTTP_200_OK), + ("zip", 12, status.HTTP_400_BAD_REQUEST), + ("bzip2", None, status.HTTP_200_OK), + ("bzip2", 1, status.HTTP_200_OK), + ("bzip2", 13, status.HTTP_400_BAD_REQUEST), + ("lzma", None, status.HTTP_200_OK), + ("lzma", 1, status.HTTP_400_BAD_REQUEST), + ("zstd", None, status.HTTP_200_OK), + ("zstd", 12, status.HTTP_200_OK), + ("zstd", 23, status.HTTP_400_BAD_REQUEST), + ), ) async def test_export_compression( - self, - app: FastAPI, - client: AsyncClient, - tmpdir, - project: Project, - compression: str, - compression_level: int, - status_code: int + self, + app: FastAPI, + client: AsyncClient, + tmpdir, + project: Project, + compression: str, + compression_level: int, + status_code: int, ) -> None: - + project.dump = MagicMock() os.makedirs(project.path, exist_ok=True) - - topology = { - "topology": { - "nodes": [ - { - "node_type": "qemu" - } - ] - } - } - with open(os.path.join(project.path, "test.gns3"), 'w+') as f: + + topology = {"topology": {"nodes": [{"node_type": "qemu"}]}} + with open(os.path.join(project.path, "test.gns3"), "w+") as f: json.dump(topology, f) - + params = {"compression": compression} if compression_level: params["compression_level"] = compression_level response = await client.get(app.url_path_for("export_project", project_id=project.id), params=params) assert response.status_code == status_code - + if response.status_code == status.HTTP_200_OK: - assert response.headers['CONTENT-TYPE'] == 'application/gns3project' - assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name) - with open(str(tmpdir / 'project.zip'), 'wb+') as f: + assert response.headers["CONTENT-TYPE"] == "application/gns3project" + assert response.headers[ + "CONTENT-DISPOSITION" + ] == "attachment; filename=\"{name}.gns3project\"; filename*=UTF-8''{name}.gns3project".format( + name=project.name + ) + with open(str(tmpdir / "project.zip"), "wb+") as f: f.write(response.content) - - with zipfile_zstd.ZipFile(str(tmpdir / 'project.zip')) as myzip: + + with zipfile_zstd.ZipFile(str(tmpdir / "project.zip")) as myzip: with myzip.open("project.gns3") as myfile: myfile.read() - - + async def test_get_file(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + os.makedirs(project.path, exist_ok=True) - with open(os.path.join(project.path, 'hello'), 'w+') as f: - f.write('world') - + with open(os.path.join(project.path, "hello"), "w+") as f: + f.write("world") + response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="hello")) assert response.status_code == status.HTTP_200_OK assert response.content == b"world" - + response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="false")) assert response.status_code == status.HTTP_404_NOT_FOUND - + response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="../hello")) assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_get_project_gns3_file(self, app: FastAPI, client: AsyncClient, project: Project) -> None: response = await client.get(app.url_path_for("get_project_gns3_file", project_id=project.id)) @@ -451,7 +381,6 @@ class TestControllerProjectRoutes: assert topology["name"] == "test" assert "topology" in topology - async def test_get_project_gns3_file_raw_content(self, app: FastAPI, client: AsyncClient, project: Project) -> None: # the endpoint must serve the raw file content, without the project being opened @@ -470,145 +399,142 @@ class TestControllerProjectRoutes: assert response.status_code == status.HTTP_200_OK assert response.json() == topology - async def test_get_project_gns3_file_project_not_found(self, app: FastAPI, client: AsyncClient) -> None: response = await client.get(app.url_path_for("get_project_gns3_file", project_id=str(uuid.uuid4()))) assert response.status_code == status.HTTP_404_NOT_FOUND - async def test_get_file_forbidden_location(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd" response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path=file_path)) assert response.status_code == status.HTTP_403_FORBIDDEN - - + async def test_write_file(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - - response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="hello"), - content=b"world") + + response = await client.post( + app.url_path_for("write_file", project_id=project.id, file_path="hello"), content=b"world" + ) assert response.status_code == status.HTTP_204_NO_CONTENT - + with open(os.path.join(project.path, "hello")) as f: assert f.read() == "world" - + response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="../hello")) assert response.status_code == status.HTTP_404_NOT_FOUND - - + async def test_write_file_forbidden_location(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + file_path = "%2e%2e/hello" - response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path=file_path), - content=b"world") + response = await client.post( + app.url_path_for("write_file", project_id=project.id, file_path=file_path), content=b"world" + ) assert response.status_code == status.HTTP_403_FORBIDDEN - - + async def test_write_and_get_file_with_leading_slashes_in_filename( - self, - app: FastAPI, - client: AsyncClient, - project: Project) -> None: - - response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="//hello"), - content=b"world") + self, app: FastAPI, client: AsyncClient, project: Project + ) -> None: + + response = await client.post( + app.url_path_for("write_file", project_id=project.id, file_path="//hello"), content=b"world" + ) assert response.status_code == status.HTTP_403_FORBIDDEN - + response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="//hello")) assert response.status_code == status.HTTP_403_FORBIDDEN - - + async def test_import(self, app: FastAPI, client: AsyncClient, tmpdir, controller: Controller) -> None: - - with zipfile_zstd.ZipFile(str(tmpdir / "test.zip"), 'w') as myzip: - myzip.writestr("project.gns3", b'{"project_id": "c6992992-ac72-47dc-833b-54aa334bcd05", "version": "2.0.0", "name": "test"}') + + with zipfile_zstd.ZipFile(str(tmpdir / "test.zip"), "w") as myzip: + myzip.writestr( + "project.gns3", + b'{"project_id": "c6992992-ac72-47dc-833b-54aa334bcd05", "version": "2.0.0", "name": "test"}', + ) myzip.writestr("demo", b"hello") - + project_id = str(uuid.uuid4()) with open(str(tmpdir / "test.zip"), "rb") as f: response = await client.post(app.url_path_for("import_project", project_id=project_id), content=f.read()) assert response.status_code == status.HTTP_201_CREATED - + project = controller.get_project(project_id) with open(os.path.join(project.path, "demo")) as f: content = f.read() assert content == "hello" - - + async def test_import_with_project_name( - self, - app: FastAPI, - client: AsyncClient, - tmpdir, - controller: Controller + self, app: FastAPI, client: AsyncClient, tmpdir, controller: Controller ) -> None: - - with zipfile_zstd.ZipFile(str(tmpdir / "test.zip"), 'w') as myzip: - myzip.writestr("project.gns3", b'{"project_id": "c6992992-ac72-47dc-833b-54aa334bcd05", "version": "2.0.0", "name": "test"}') + + with zipfile_zstd.ZipFile(str(tmpdir / "test.zip"), "w") as myzip: + myzip.writestr( + "project.gns3", + b'{"project_id": "c6992992-ac72-47dc-833b-54aa334bcd05", "version": "2.0.0", "name": "test"}', + ) myzip.writestr("demo", b"hello") - + project_id = str(uuid.uuid4()) with open(str(tmpdir / "test.zip"), "rb") as f: response = await client.post( app.url_path_for("import_project", project_id=project_id), content=f.read(), - params={"name": "my-imported-project-name"} + params={"name": "my-imported-project-name"}, ) assert response.status_code == status.HTTP_201_CREATED project = controller.get_project(project_id) assert project.name == "my-imported-project-name" - - + async def test_duplicate(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - - response = await client.post(app.url_path_for("duplicate_project", project_id=project.id), json={"name": "hello"}) + + response = await client.post( + app.url_path_for("duplicate_project", project_id=project.id), json={"name": "hello"} + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == "hello" - - + async def test_lock_unlock(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None: - + # add a drawing and node to the project params = { "svg": '', "x": 10, "y": 20, - "z": 0 + "z": 0, } - + response = await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params) assert response.status_code == status.HTTP_201_CREATED - + response = MagicMock() response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - - response = await client.post(app.url_path_for("create_node", project_id=project.id), json={ - "name": "test", - "node_type": "vpcs", - "compute_id": "example.com", - "properties": { - "startup_script": "echo test" - } - }) + + response = await client.post( + app.url_path_for("create_node", project_id=project.id), + json={ + "name": "test", + "node_type": "vpcs", + "compute_id": "example.com", + "properties": {"startup_script": "echo test"}, + }, + ) assert response.status_code == status.HTTP_201_CREATED - + response = await client.post(app.url_path_for("lock_project", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - + for drawing in project.drawings.values(): assert drawing.locked is True for node in project.nodes.values(): assert node.locked is True - + response = await client.get(app.url_path_for("locked_project", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert response.json() is True - + response = await client.post(app.url_path_for("unlock_project", project_id=project.id)) assert response.status_code == status.HTTP_204_NO_CONTENT - + for drawing in project.drawings.values(): assert drawing.locked is False for node in project.nodes.values(): diff --git a/tests/api/routes/controller/test_roles.py b/tests/api/routes/controller/test_roles.py index a98be8bf0..bdc6f6651 100644 --- a/tests/api/routes/controller/test_roles.py +++ b/tests/api/routes/controller/test_roles.py @@ -27,7 +27,6 @@ pytestmark = pytest.mark.asyncio class TestRolesRoutes: - async def test_create_role(self, app: FastAPI, client: AsyncClient) -> None: new_role = {"name": "role1"} @@ -54,36 +53,22 @@ class TestRolesRoutes: role_in_db = await rbac_repo.get_role_by_name("role1") update_role = {"name": "role42"} - response = await client.put( - app.url_path_for("update_role", role_id=role_in_db.role_id), - json=update_role - ) + response = await client.put(app.url_path_for("update_role", role_id=role_in_db.role_id), json=update_role) assert response.status_code == status.HTTP_200_OK updated_role_in_db = await rbac_repo.get_role(role_in_db.role_id) assert updated_role_in_db.name == "role42" async def test_cannot_update_builtin_user_role( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession ) -> None: rbac_repo = RbacRepository(db_session) role_in_db = await rbac_repo.get_role_by_name("User") update_role = {"name": "Hackers"} - response = await client.put( - app.url_path_for("update_role", role_id=role_in_db.role_id), - json=update_role - ) + response = await client.put(app.url_path_for("update_role", role_id=role_in_db.role_id), json=update_role) assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_delete_role( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_delete_role(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: rbac_repo = RbacRepository(db_session) role_in_db = await rbac_repo.get_role_by_name("role42") @@ -91,10 +76,7 @@ class TestRolesRoutes: assert response.status_code == status.HTTP_204_NO_CONTENT async def test_cannot_delete_builtin_administrator_role( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession ) -> None: rbac_repo = RbacRepository(db_session) @@ -104,13 +86,7 @@ class TestRolesRoutes: class TestRolesPrivilegesRoutes: - - async def test_add_privilege_to_role( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_add_privilege_to_role(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: rbac_repo = RbacRepository(db_session) role_in_db = await rbac_repo.get_role_by_name("User") @@ -118,38 +94,24 @@ class TestRolesPrivilegesRoutes: response = await client.put( app.url_path_for( - "add_privilege_to_role", - role_id=role_in_db.role_id, - privilege_id=str(privilege.privilege_id) + "add_privilege_to_role", role_id=role_in_db.role_id, privilege_id=str(privilege.privilege_id) ) ) assert response.status_code == status.HTTP_204_NO_CONTENT privileges = await rbac_repo.get_role_privileges(role_in_db.role_id) assert len(privileges) == 27 # 25 default privileges + 2 LLMConfig privileges - async def test_get_role_privileges( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_get_role_privileges(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: rbac_repo = RbacRepository(db_session) role_in_db = await rbac_repo.get_role_by_name("User") - response = await client.get( - app.url_path_for( - "get_role_privileges", - role_id=role_in_db.role_id) - ) + response = await client.get(app.url_path_for("get_role_privileges", role_id=role_in_db.role_id)) assert response.status_code == status.HTTP_200_OK assert len(response.json()) == 27 # 25 default privileges + 2 LLMConfig privileges async def test_remove_privilege_from_role( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession ) -> None: rbac_repo = RbacRepository(db_session) @@ -158,9 +120,7 @@ class TestRolesPrivilegesRoutes: response = await client.delete( app.url_path_for( - "remove_privilege_from_role", - role_id=role_in_db.role_id, - privilege_id=str(privilege.privilege_id) + "remove_privilege_from_role", role_id=role_in_db.role_id, privilege_id=str(privilege.privilege_id) ), ) assert response.status_code == status.HTTP_204_NO_CONTENT diff --git a/tests/api/routes/controller/test_settings.py b/tests/api/routes/controller/test_settings.py index 21f6d6e56..200ca9c6d 100644 --- a/tests/api/routes/controller/test_settings.py +++ b/tests/api/routes/controller/test_settings.py @@ -46,15 +46,12 @@ def stable_jwt_secret(config): class TestSettingsRoutes: - async def test_get_settings(self, app: FastAPI, client: AsyncClient) -> None: response = await client.get(app.url_path_for("get_server_settings")) assert response.status_code == status.HTTP_200_OK body = response.json() - assert sorted(body.keys()) == [ - "Controller", "Dynamips", "IOU", "Qemu", "Server", "VPCS", "WebWireshark" - ] + assert sorted(body.keys()) == ["Controller", "Dynamips", "IOU", "Qemu", "Server", "VPCS", "WebWireshark"] # deprecated sections are not exposed assert "VirtualBox" not in body assert "VMware" not in body @@ -71,7 +68,8 @@ class TestSettingsRoutes: # underlying httpx client and its default headers depend on the fixture # instantiation order response = await client.get( - app.url_path_for("get_server_settings"), headers={"Authorization": "Bearer invalid_token"}) + app.url_path_for("get_server_settings"), headers={"Authorization": "Bearer invalid_token"} + ) assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_get_settings_forbidden(self, app: FastAPI, client: AsyncClient, test_user) -> None: @@ -79,23 +77,28 @@ class TestSettingsRoutes: # the "User" role has no Server.Audit privilege token = auth_service.create_access_token(test_user.username, secret_key=DEFAULT_JWT_SECRET_KEY) response = await client.get( - app.url_path_for("get_server_settings"), headers={"Authorization": f"Bearer {token}"}) + app.url_path_for("get_server_settings"), headers={"Authorization": f"Bearer {token}"} + ) assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_put_settings(self, app: FastAPI, client: AsyncClient, config: Config, - stable_jwt_secret: str) -> None: + async def test_put_settings( + self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str + ) -> None: - response = await client.put(app.url_path_for("update_server_settings"), json={ - "Server": { - "port": 3081, - "allowed_interfaces": ["eth0"], - "default_symbol_theme": "Classic", - "report_errors": False, + response = await client.put( + app.url_path_for("update_server_settings"), + json={ + "Server": { + "port": 3081, + "allowed_interfaces": ["eth0"], + "default_symbol_theme": "Classic", + "report_errors": False, + }, + "Qemu": { + "enable_monitor": False, + }, }, - "Qemu": { - "enable_monitor": False, - }, - }) + ) assert response.status_code == status.HTTP_200_OK body = response.json() assert body["Server"]["port"] == 3081 @@ -116,7 +119,8 @@ class TestSettingsRoutes: assert parsed["Qemu"]["enable_monitor"] == "False" async def test_put_settings_preserves_unknown_options( - self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str) -> None: + self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str + ) -> None: with open(config._main_config_file, "w") as f: f.write("[Server]\nhost = 127.0.0.1\nfrobnicate = 42\n") @@ -130,11 +134,13 @@ class TestSettingsRoutes: assert parsed["Server"]["host"] == "127.0.0.1" async def test_put_settings_secrets( - self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str) -> None: + self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str + ) -> None: # masked secret means "unchanged": nothing is written response = await client.put( - app.url_path_for("update_server_settings"), json={"Server": {"compute_password": SECRET_MASK}}) + app.url_path_for("update_server_settings"), json={"Server": {"compute_password": SECRET_MASK}} + ) assert response.status_code == status.HTTP_200_OK parsed = configparser.ConfigParser() parsed.read(config._main_config_file) @@ -142,7 +148,8 @@ class TestSettingsRoutes: # empty string means "unchanged" too response = await client.put( - app.url_path_for("update_server_settings"), json={"Server": {"compute_password": ""}}) + app.url_path_for("update_server_settings"), json={"Server": {"compute_password": ""}} + ) assert response.status_code == status.HTTP_200_OK parsed = configparser.ConfigParser() parsed.read(config._main_config_file) @@ -150,7 +157,8 @@ class TestSettingsRoutes: # an explicit new value is written in clear text (like a hand-edited file) response = await client.put( - app.url_path_for("update_server_settings"), json={"Server": {"compute_password": "secret123"}}) + app.url_path_for("update_server_settings"), json={"Server": {"compute_password": "secret123"}} + ) assert response.status_code == status.HTTP_200_OK assert response.json()["Server"]["compute_password"] == SECRET_MASK # masked in the response parsed = configparser.ConfigParser() @@ -158,7 +166,8 @@ class TestSettingsRoutes: assert parsed["Server"]["compute_password"] == "secret123" async def test_put_settings_null_removes_option( - self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str) -> None: + self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str + ) -> None: with open(config._main_config_file, "w") as f: f.write("[Server]\nhost = 127.0.0.1\n") @@ -172,7 +181,8 @@ class TestSettingsRoutes: assert response.json()["Server"]["host"] == "0.0.0.0" # default restored async def test_put_settings_validation_failure( - self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str) -> None: + self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str + ) -> None: with open(config._main_config_file, "w") as f: f.write("[Server]\nhost = 127.0.0.1\n") @@ -180,22 +190,25 @@ class TestSettingsRoutes: content_before = f.read() # cross-field violation: console_end_port_range must be > console_start_port_range - response = await client.put(app.url_path_for("update_server_settings"), json={ - "Server": {"console_start_port_range": 10000, "console_end_port_range": 5000}}) + response = await client.put( + app.url_path_for("update_server_settings"), + json={"Server": {"console_start_port_range": 10000, "console_end_port_range": 5000}}, + ) assert response.status_code == status.HTTP_400_BAD_REQUEST with open(config._main_config_file) as f: assert f.read() == content_before async def test_put_settings_unknown_option_rejected( - self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str) -> None: + self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str + ) -> None: - response = await client.put( - app.url_path_for("update_server_settings"), json={"Server": {"prot": "http"}}) + response = await client.put(app.url_path_for("update_server_settings"), json={"Server": {"prot": "http"}}) assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT async def test_put_settings_deprecated_sections_rejected( - self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str) -> None: + self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str + ) -> None: response = await client.put(app.url_path_for("update_server_settings"), json={"VirtualBox": {}}) assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT @@ -203,14 +216,17 @@ class TestSettingsRoutes: assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT async def test_put_settings_jwt_secret_key_rejected( - self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str) -> None: + self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str + ) -> None: response = await client.put( - app.url_path_for("update_server_settings"), json={"Controller": {"jwt_secret_key": "nope"}}) + app.url_path_for("update_server_settings"), json={"Controller": {"jwt_secret_key": "nope"}} + ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT async def test_put_settings_conflict( - self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str, tmpdir) -> None: + self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str, tmpdir + ) -> None: override_path = str(tmpdir / "override.conf") with open(override_path, "w") as f: @@ -218,7 +234,9 @@ class TestSettingsRoutes: # a later configuration file takes precedence over the main one Config.instance()._files.append(override_path) - response = await client.put(app.url_path_for("update_server_settings"), json={"Server": {"host": "192.168.1.1"}}) + response = await client.put( + app.url_path_for("update_server_settings"), json={"Server": {"host": "192.168.1.1"}} + ) assert response.status_code == status.HTTP_409_CONFLICT async def test_put_settings_empty_body(self, app: FastAPI, client: AsyncClient, config: Config) -> None: diff --git a/tests/api/routes/controller/test_snapshots.py b/tests/api/routes/controller/test_snapshots.py index 5ef2cbe8b..1c2f60a5e 100644 --- a/tests/api/routes/controller/test_snapshots.py +++ b/tests/api/routes/controller/test_snapshots.py @@ -31,48 +31,52 @@ pytestmark = pytest.mark.asyncio class TestSnapshotRoutes: - @pytest_asyncio.fixture async def project(self, app: FastAPI, client: AsyncClient, controller: Controller) -> Project: - + u = str(uuid.uuid4()) params = {"name": "test", "project_id": u} await client.post(app.url_path_for("create_project"), json=params) controller_project = controller.get_project(u) return controller_project - - + @pytest_asyncio.fixture async def snapshot(self, project: Project): - + controller_snapshot = await project.snapshot("test") return controller_snapshot - - - async def test_list_snapshots(self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot) -> None: - + + async def test_list_snapshots( + self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot + ) -> None: + assert snapshot.name == "test" response = await client.get(app.url_path_for("get_snapshots", project_id=project.id)) assert response.status_code == status.HTTP_200_OK assert len(response.json()) == 1 - - - async def test_delete_snapshot(self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot) -> None: - - response = await client.delete(app.url_path_for("delete_snapshot", project_id=project.id, snapshot_id=snapshot.id)) + + async def test_delete_snapshot( + self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot + ) -> None: + + response = await client.delete( + app.url_path_for("delete_snapshot", project_id=project.id, snapshot_id=snapshot.id) + ) assert response.status_code == status.HTTP_204_NO_CONTENT assert not os.path.exists(snapshot.path) - - - async def test_restore_snapshot(self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot) -> None: - - response = await client.post(app.url_path_for("restore_snapshot", project_id=project.id, snapshot_id=snapshot.id)) + + async def test_restore_snapshot( + self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot + ) -> None: + + response = await client.post( + app.url_path_for("restore_snapshot", project_id=project.id, snapshot_id=snapshot.id) + ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["name"] == project.name - - + async def test_create_snapshot(self, app: FastAPI, client: AsyncClient, project: Project) -> None: - + response = await client.post(app.url_path_for("create_snapshot", project_id=project.id), json={"name": "snap1"}) assert response.status_code == status.HTTP_201_CREATED assert len(os.listdir(os.path.join(project.path, "snapshots"))) == 2 diff --git a/tests/api/routes/controller/test_symbols.py b/tests/api/routes/controller/test_symbols.py index 24e607205..7c28bff26 100644 --- a/tests/api/routes/controller/test_symbols.py +++ b/tests/api/routes/controller/test_symbols.py @@ -29,42 +29,39 @@ pytestmark = pytest.mark.asyncio class TestSymbolRoutes: - async def test_symbols(self, app: FastAPI, client: AsyncClient) -> None: - + response = await client.get(app.url_path_for("get_symbols")) - + assert response.status_code == status.HTTP_200_OK assert { - 'symbol_id': ':/symbols/classic/firewall.svg', - 'filename': 'firewall.svg', - 'builtin': True, - 'theme': 'Classic' + "symbol_id": ":/symbols/classic/firewall.svg", + "filename": "firewall.svg", + "builtin": True, + "theme": "Classic", } in response.json() - - + async def test_get(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None: - + controller.symbols.theme = "Classic" - url = app.url_path_for("get_symbol", symbol_id=urllib.parse.quote(':/symbols/classic/firewall.svg')) + url = app.url_path_for("get_symbol", symbol_id=urllib.parse.quote(":/symbols/classic/firewall.svg")) response = await client.get(url) assert response.status_code == status.HTTP_200_OK - assert response.headers['CONTENT-TYPE'] == 'image/svg+xml' - assert response.headers['CONTENT-LENGTH'] == '9381' - assert '' in response.text - + assert response.headers["CONTENT-TYPE"] == "image/svg+xml" + assert response.headers["CONTENT-LENGTH"] == "9381" + assert "" in response.text + # Reply with the default symbol response = await client.get(app.url_path_for("get_symbol", symbol_id="404.png")) assert response.status_code == status.HTTP_200_OK - - + async def test_upload(self, app: FastAPI, client: AsyncClient, symbols_dir: str) -> None: - + response = await client.post(app.url_path_for("upload_symbol", symbol_id="test2"), content=b"TEST") assert response.status_code == status.HTTP_204_NO_CONTENT - + with open(os.path.join(symbols_dir, "test2")) as f: assert f.read() == "TEST" - + response = await client.get(app.url_path_for("get_symbol", symbol_id="test2")) assert response.status_code == status.HTTP_200_OK diff --git a/tests/api/routes/controller/test_templates.py b/tests/api/routes/controller/test_templates.py index a1bf507e9..864cc1d57 100644 --- a/tests/api/routes/controller/test_templates.py +++ b/tests/api/routes/controller/test_templates.py @@ -41,7 +41,6 @@ pytestmark = pytest.mark.asyncio class TestTemplateRoutes: - async def test_route_exist(self, app: FastAPI, client: AsyncClient) -> None: new_template = { @@ -54,7 +53,7 @@ class TestTemplateRoutes: "compute_id": "local", "symbol": ":/symbols/vpcs_guest.svg", "template_type": "vpcs", - "tags": ["tag1", "tag2"] + "tags": ["tag1", "tag2"], } response = await client.post(app.url_path_for("create_template"), json=new_template) @@ -78,11 +77,7 @@ class TestTemplateRoutes: ), ) async def test_template_list_with_tags( - self, - app: FastAPI, - client: AsyncClient, - tags: list, - expected_match: bool + self, app: FastAPI, client: AsyncClient, tags: list, expected_match: bool ) -> None: params = {"tags": tags} @@ -100,11 +95,13 @@ class TestTemplateRoutes: async def test_template_get(self, app: FastAPI, client: AsyncClient) -> None: template_id = str(uuid.uuid4()) - params = {"template_id": template_id, - "name": "VPCS_TEST", - "version": "1.0", - "compute_id": "local", - "template_type": "vpcs"} + params = { + "template_id": template_id, + "name": "VPCS_TEST", + "version": "1.0", + "compute_id": "local", + "template_type": "vpcs", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED @@ -114,26 +111,22 @@ class TestTemplateRoutes: assert response.json()["template_id"] == template_id async def test_template_create_same_name_and_version( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller + self, app: FastAPI, client: AsyncClient, controller: Controller ) -> None: - params = {"name": "VPCS_TEST", - "version": "1.0", - "compute_id": "local", - "template_type": "vpcs"} + params = {"name": "VPCS_TEST", "version": "1.0", "compute_id": "local", "template_type": "vpcs"} response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_409_CONFLICT async def test_template_create_wrong_type(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None: - params = {"name": "VPCS_TEST", - "version": "2.0", - "compute_id": "local", - "template_type": "invalid_template_type"} + params = { + "name": "VPCS_TEST", + "version": "2.0", + "compute_id": "local", + "template_type": "invalid_template_type", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT @@ -148,7 +141,7 @@ class TestTemplateRoutes: "compute_id": "local", "template_type": "vpcs", "tags": ["tag1", "tag2"], - "netmiko_device_type": "generic_termserver_telnet" + "netmiko_device_type": "generic_termserver_telnet", } response = await client.post(app.url_path_for("create_template"), json=params) @@ -215,11 +208,13 @@ class TestTemplateRoutes: async def test_template_delete(self, app: FastAPI, client: AsyncClient) -> None: template_id = str(uuid.uuid4()) - params = {"template_id": template_id, - "name": "VPCS_TEST", - "version": "4.0", - "compute_id": "local", - "template_type": "vpcs"} + params = { + "template_id": template_id, + "name": "VPCS_TEST", + "version": "4.0", + "compute_id": "local", + "template_type": "vpcs", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED @@ -228,32 +223,34 @@ class TestTemplateRoutes: assert response.status_code == status.HTTP_204_NO_CONTENT async def test_template_delete_with_prune_images( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - tmpdir: str, + self, + app: FastAPI, + client: AsyncClient, + db_session: AsyncSession, + tmpdir: str, ) -> None: image1 = os.path.join(tmpdir, "image1.qcow2") with open(image1, "wb+") as f: - f.write(b'\x42\x42\x42\x42') + f.write(b"\x42\x42\x42\x42") image2 = os.path.join(tmpdir, "image2.qcow2") with open(image2, "wb+") as f: - f.write(b'\x42\x42\x42\x42') + f.write(b"\x42\x42\x42\x42") images_repo = ImagesRepository(db_session) await images_repo.add_image("image1.qcow2", "qemu", 42, image1, "e342eb86c1229b6c154367a5476969b5", "md5") await images_repo.add_image("image2.qcow2", "qemu", 42, image2, "e342eb86c1229b6c154367a5476969b5", "md5") template_id = str(uuid.uuid4()) - params = {"template_id": template_id, - "name": "QEMU_TEMPLATE", - "compute_id": "local", - "hda_disk_image": "image1.qcow2", - "hdb_disk_image": "image2.qcow2", - "template_type": "qemu"} + params = { + "template_id": template_id, + "name": "QEMU_TEMPLATE", + "compute_id": "local", + "hda_disk_image": "image1.qcow2", + "hdb_disk_image": "image2.qcow2", + "template_type": "qemu", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED @@ -263,8 +260,7 @@ class TestTemplateRoutes: assert len(images) == 2 response = await client.delete( - app.url_path_for("delete_template", template_id=template_id), - params={"prune_images": True} + app.url_path_for("delete_template", template_id=template_id), params={"prune_images": True} ) assert response.status_code == status.HTTP_204_NO_CONTENT @@ -291,15 +287,18 @@ class TestTemplateRoutes: # an explicit project_id marks this as an existing on-disk project # (same path load_project takes at startup) return await controller.add_project( - project_id=project_id, name=name, path=project_dir, - filename=f"{name}.gns3", status="closed", + project_id=project_id, + name=name, + path=project_dir, + filename=f"{name}.gns3", + status="closed", ) async def test_template_delete_used_by_project( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller, + self, + app: FastAPI, + client: AsyncClient, + controller: Controller, ) -> None: """ A template referenced by a node in a project must not be deleted, @@ -307,18 +306,22 @@ class TestTemplateRoutes: """ template_id = str(uuid.uuid4()) - params = {"template_id": template_id, - "name": "VPCS_GUARDED", - "compute_id": "local", - "template_type": "vpcs"} + params = {"template_id": template_id, "name": "VPCS_GUARDED", "compute_id": "local", "template_type": "vpcs"} response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED guarded_project = await self._add_closed_project_with_node( - controller, "Guarded", - {"node_id": str(uuid.uuid4()), "node_type": "vpcs", "compute_id": "local", - "name": "n1", "template_id": template_id, "properties": {}}, + controller, + "Guarded", + { + "node_id": str(uuid.uuid4()), + "node_type": "vpcs", + "compute_id": "local", + "name": "n1", + "template_id": template_id, + "properties": {}, + }, ) response = await client.delete(app.url_path_for("delete_template", template_id=template_id)) @@ -335,12 +338,12 @@ class TestTemplateRoutes: assert response.status_code == status.HTTP_204_NO_CONTENT async def test_template_delete_with_prune_images_used_by_project( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller, - db_session: AsyncSession, - tmpdir: str, + self, + app: FastAPI, + client: AsyncClient, + controller: Controller, + db_session: AsyncSession, + tmpdir: str, ) -> None: """ Pruning an image still referenced by a project node must be refused @@ -349,17 +352,19 @@ class TestTemplateRoutes: image_path = os.path.join(tmpdir, "used.qcow2") with open(image_path, "wb+") as f: - f.write(b'\x42\x42\x42\x42') + f.write(b"\x42\x42\x42\x42") images_repo = ImagesRepository(db_session) await images_repo.add_image("used.qcow2", "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5") template_id = str(uuid.uuid4()) - params = {"template_id": template_id, - "name": "QEMU_GUARDED", - "compute_id": "local", - "hda_disk_image": "used.qcow2", - "template_type": "qemu"} + params = { + "template_id": template_id, + "name": "QEMU_GUARDED", + "compute_id": "local", + "hda_disk_image": "used.qcow2", + "template_type": "qemu", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED @@ -367,14 +372,19 @@ class TestTemplateRoutes: # the node references the image but not the template: the template # check passes, the image check must still refuse the prune guarded_project = await self._add_closed_project_with_node( - controller, "Guarded", - {"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local", - "name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}}, + controller, + "Guarded", + { + "node_id": str(uuid.uuid4()), + "node_type": "qemu", + "compute_id": "local", + "name": "n1", + "properties": {"hda_disk_image_backing_file": "used.qcow2"}, + }, ) response = await client.delete( - app.url_path_for("delete_template", template_id=template_id), - params={"prune_images": True} + app.url_path_for("delete_template", template_id=template_id), params={"prune_images": True} ) assert response.status_code == status.HTTP_409_CONFLICT assert "Guarded" in response.json()["message"] @@ -386,8 +396,7 @@ class TestTemplateRoutes: controller.remove_project(guarded_project) response = await client.delete( - app.url_path_for("delete_template", template_id=template_id), - params={"prune_images": True} + app.url_path_for("delete_template", template_id=template_id), params={"prune_images": True} ) assert response.status_code == status.HTTP_204_NO_CONTENT assert not os.path.exists(image_path) @@ -415,34 +424,25 @@ class TestTemplateRoutes: async def test_get_base_config(self, app: FastAPI, client: AsyncClient): async def mock_get_current_active_user(): - return schemas.User( - username="admin", - user_id=uuid.uuid4(), - is_superadmin=True, - is_active=True - ) + return schemas.User(username="admin", user_id=uuid.uuid4(), is_superadmin=True, is_active=True) + app.dependency_overrides[get_current_active_user] = mock_get_current_active_user try: - create_resp = await client.post(app.url_path_for("create_template"), json={ - "name": "TEST", - "compute_id": "local", - "template_type": "vpcs" - }) + create_resp = await client.post( + app.url_path_for("create_template"), + json={"name": "TEST", "compute_id": "local", "template_type": "vpcs"}, + ) assert create_resp.status_code == 201 template_id = create_resp.json()["template_id"] await client.put( app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), - json={"content": "hello"} + json={"content": "hello"}, ) response = await client.get( - app.url_path_for( - "get_base_config", - template_id=template_id, - filename="test.txt" - ) + app.url_path_for("get_base_config", template_id=template_id, filename="test.txt") ) assert response.status_code == 200 @@ -453,27 +453,21 @@ class TestTemplateRoutes: async def test_update_base_config(self, app: FastAPI, client: AsyncClient): async def mock_get_current_active_user(): - return schemas.User( - username="admin", - user_id=uuid.uuid4(), - is_superadmin=True, - is_active=True - ) + return schemas.User(username="admin", user_id=uuid.uuid4(), is_superadmin=True, is_active=True) + app.dependency_overrides[get_current_active_user] = mock_get_current_active_user try: template_name = f"TEST_UPDATE_{uuid.uuid4().hex[:8]}" - create_resp = await client.post(app.url_path_for("create_template"), json={ - "name": template_name, - "compute_id": "local", - "template_type": "vpcs" - }) + create_resp = await client.post( + app.url_path_for("create_template"), + json={"name": template_name, "compute_id": "local", "template_type": "vpcs"}, + ) assert create_resp.status_code == 201 template_id = create_resp.json()["template_id"] payload = {"content": "hello world"} response = await client.put( - app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), - json=payload + app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), json=payload ) assert response.status_code == 200 @@ -484,26 +478,20 @@ class TestTemplateRoutes: async def test_update_base_config_missing_content(self, app: FastAPI, client: AsyncClient): async def mock_get_current_active_user(): - return schemas.User( - username="admin", - user_id=uuid.uuid4(), - is_superadmin=True, - is_active=True - ) + return schemas.User(username="admin", user_id=uuid.uuid4(), is_superadmin=True, is_active=True) + app.dependency_overrides[get_current_active_user] = mock_get_current_active_user try: template_name = f"TEST_MISSING_{uuid.uuid4().hex[:8]}" - create_resp = await client.post(app.url_path_for("create_template"), json={ - "name": template_name, - "compute_id": "local", - "template_type": "vpcs" - }) + create_resp = await client.post( + app.url_path_for("create_template"), + json={"name": template_name, "compute_id": "local", "template_type": "vpcs"}, + ) assert create_resp.status_code == 201 template_id = create_resp.json()["template_id"] response = await client.put( - app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), - json={} + app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), json={} ) assert response.status_code in (400, 422) @@ -520,35 +508,28 @@ class TestTemplateRoutes: async def test_list_base_configs(self, app: FastAPI, client: AsyncClient): async def mock_get_current_active_user(): - return schemas.User( - username="admin", - user_id=uuid.uuid4(), - is_superadmin=True, - is_active=True - ) + return schemas.User(username="admin", user_id=uuid.uuid4(), is_superadmin=True, is_active=True) + app.dependency_overrides[get_current_active_user] = mock_get_current_active_user try: template_name = f"TEST_LIST_{uuid.uuid4().hex[:8]}" - create_resp = await client.post(app.url_path_for("create_template"), json={ - "name": template_name, - "compute_id": "local", - "template_type": "vpcs" - }) + create_resp = await client.post( + app.url_path_for("create_template"), + json={"name": template_name, "compute_id": "local", "template_type": "vpcs"}, + ) assert create_resp.status_code == 201 template_id = create_resp.json()["template_id"] await client.put( app.url_path_for("update_base_config", template_id=template_id, filename="config1.txt"), - json={"content": "file1"} + json={"content": "file1"}, ) await client.put( app.url_path_for("update_base_config", template_id=template_id, filename="config2.txt"), - json={"content": "file2"} + json={"content": "file2"}, ) - response = await client.get( - app.url_path_for("list_base_configs", template_id=template_id) - ) + response = await client.get(app.url_path_for("list_base_configs", template_id=template_id)) assert response.status_code == 200 filenames = [item["filename"] for item in response.json()] @@ -559,14 +540,10 @@ class TestTemplateRoutes: class TestDuplicateTemplates: - async def test_template_duplicate(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None: template_id = str(uuid.uuid4()) - params = {"template_id": template_id, - "name": "VPCS_TEST", - "compute_id": "local", - "template_type": "vpcs"} + params = {"template_id": template_id, "name": "VPCS_TEST", "compute_id": "local", "template_type": "vpcs"} response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED @@ -582,10 +559,7 @@ class TestDuplicateTemplates: assert len(response.json()) == 9 # includes builtin templates async def test_template_duplicate_invalid_template_id( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller + self, app: FastAPI, client: AsyncClient, controller: Controller ) -> None: template_id = str(uuid.uuid4()) @@ -594,7 +568,6 @@ class TestDuplicateTemplates: class TestBuiltinTemplates: - async def test_list_builtin_templates(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None: response = await client.get(app.url_path_for("get_templates")) @@ -627,10 +600,7 @@ class TestBuiltinTemplates: assert response.status_code == status.HTTP_403_FORBIDDEN async def test_list_builtin_templates_not_enabled( - self, - app: FastAPI, - client: AsyncClient, - controller: Controller + self, app: FastAPI, client: AsyncClient, controller: Controller ) -> None: config = Config.instance() @@ -640,14 +610,15 @@ class TestBuiltinTemplates: class TestDynamipsTemplate: - async def test_c7200_dynamips_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c7200 template", - "platform": "c7200", - "compute_id": "local", - "image": "c7200-adventerprisek9-mz.124-24.T5.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c7200 template", + "platform": "c7200", + "compute_id": "local", + "image": "c7200-adventerprisek9-mz.124-24.T5.image", + "template_type": "dynamips", + } with asyncio_patch("gns3server.services.templates.TemplatesService._find_images", return_value=[]) as mock: response = await client.post(app.url_path_for("create_template"), json=params) @@ -655,45 +626,49 @@ class TestDynamipsTemplate: assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "dynamips", - "auto_delete_disks": False, - "builtin": False, - "category": "router", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "default_name_format": "R{0}", - "disk0": 0, - "disk1": 0, - "exec_area": 64, - "idlemax": 500, - "idlepc": "", - "idlesleep": 30, - "image": "c7200-adventerprisek9-mz.124-24.T5.image", - "mac_addr": "", - "midplane": "vxr", - "mmap": True, - "name": "Cisco c7200 template", - "npe": "npe-400", - "nvram": 512, - "platform": "c7200", - "private_config": "", - "ram": 512, - "sparsemem": True, - "startup_config": "ios_base_startup-config.txt", - "symbol": unittest.mock.ANY, - "system_id": "FTX0945W0MY"} + expected_response = { + "template_type": "dynamips", + "auto_delete_disks": False, + "builtin": False, + "category": "router", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "default_name_format": "R{0}", + "disk0": 0, + "disk1": 0, + "exec_area": 64, + "idlemax": 500, + "idlepc": "", + "idlesleep": 30, + "image": "c7200-adventerprisek9-mz.124-24.T5.image", + "mac_addr": "", + "midplane": "vxr", + "mmap": True, + "name": "Cisco c7200 template", + "npe": "npe-400", + "nvram": 512, + "platform": "c7200", + "private_config": "", + "ram": 512, + "sparsemem": True, + "startup_config": "ios_base_startup-config.txt", + "symbol": unittest.mock.ANY, + "system_id": "FTX0945W0MY", + } for item, value in expected_response.items(): assert response.json().get(item) == value async def test_c3745_dynamips_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c3745 template", - "platform": "c3745", - "compute_id": "local", - "image": "c3745-adventerprisek9-mz.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c3745 template", + "platform": "c3745", + "compute_id": "local", + "image": "c3745-adventerprisek9-mz.124-25d.image", + "template_type": "dynamips", + } with asyncio_patch("gns3server.services.templates.TemplatesService._find_images", return_value=[]) as mock: response = await client.post(app.url_path_for("create_template"), json=params) @@ -701,44 +676,48 @@ class TestDynamipsTemplate: assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "dynamips", - "auto_delete_disks": False, - "builtin": False, - "category": "router", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "default_name_format": "R{0}", - "disk0": 0, - "disk1": 0, - "exec_area": 64, - "idlemax": 500, - "idlepc": "", - "idlesleep": 30, - "image": "c3745-adventerprisek9-mz.124-25d.image", - "mac_addr": "", - "mmap": True, - "name": "Cisco c3745 template", - "iomem": 5, - "nvram": 256, - "platform": "c3745", - "private_config": "", - "ram": 256, - "sparsemem": True, - "startup_config": "ios_base_startup-config.txt", - "symbol": unittest.mock.ANY, - "system_id": "FTX0945W0MY"} + expected_response = { + "template_type": "dynamips", + "auto_delete_disks": False, + "builtin": False, + "category": "router", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "default_name_format": "R{0}", + "disk0": 0, + "disk1": 0, + "exec_area": 64, + "idlemax": 500, + "idlepc": "", + "idlesleep": 30, + "image": "c3745-adventerprisek9-mz.124-25d.image", + "mac_addr": "", + "mmap": True, + "name": "Cisco c3745 template", + "iomem": 5, + "nvram": 256, + "platform": "c3745", + "private_config": "", + "ram": 256, + "sparsemem": True, + "startup_config": "ios_base_startup-config.txt", + "symbol": unittest.mock.ANY, + "system_id": "FTX0945W0MY", + } for item, value in expected_response.items(): assert response.json().get(item) == value async def test_c3725_dynamips_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c3725 template", - "platform": "c3725", - "compute_id": "local", - "image": "c3725-adventerprisek9-mz.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c3725 template", + "platform": "c3725", + "compute_id": "local", + "image": "c3725-adventerprisek9-mz.124-25d.image", + "template_type": "dynamips", + } with asyncio_patch("gns3server.services.templates.TemplatesService._find_images", return_value=[]) as mock: response = await client.post(app.url_path_for("create_template"), json=params) @@ -746,45 +725,49 @@ class TestDynamipsTemplate: assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "dynamips", - "auto_delete_disks": False, - "builtin": False, - "category": "router", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "default_name_format": "R{0}", - "disk0": 0, - "disk1": 0, - "exec_area": 64, - "idlemax": 500, - "idlepc": "", - "idlesleep": 30, - "image": "c3725-adventerprisek9-mz.124-25d.image", - "mac_addr": "", - "mmap": True, - "name": "Cisco c3725 template", - "iomem": 5, - "nvram": 256, - "platform": "c3725", - "private_config": "", - "ram": 128, - "sparsemem": True, - "startup_config": "ios_base_startup-config.txt", - "symbol": unittest.mock.ANY, - "system_id": "FTX0945W0MY"} + expected_response = { + "template_type": "dynamips", + "auto_delete_disks": False, + "builtin": False, + "category": "router", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "default_name_format": "R{0}", + "disk0": 0, + "disk1": 0, + "exec_area": 64, + "idlemax": 500, + "idlepc": "", + "idlesleep": 30, + "image": "c3725-adventerprisek9-mz.124-25d.image", + "mac_addr": "", + "mmap": True, + "name": "Cisco c3725 template", + "iomem": 5, + "nvram": 256, + "platform": "c3725", + "private_config": "", + "ram": 128, + "sparsemem": True, + "startup_config": "ios_base_startup-config.txt", + "symbol": unittest.mock.ANY, + "system_id": "FTX0945W0MY", + } for item, value in expected_response.items(): assert response.json().get(item) == value async def test_c3600_dynamips_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c3600 template", - "platform": "c3600", - "chassis": "3660", - "compute_id": "local", - "image": "c3660-a3jk9s-mz.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c3600 template", + "platform": "c3600", + "chassis": "3660", + "compute_id": "local", + "image": "c3660-a3jk9s-mz.124-25d.image", + "template_type": "dynamips", + } with asyncio_patch("gns3server.services.templates.TemplatesService._find_images", return_value=[]) as mock: response = await client.post(app.url_path_for("create_template"), json=params) @@ -792,57 +775,63 @@ class TestDynamipsTemplate: assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "dynamips", - "auto_delete_disks": False, - "builtin": False, - "category": "router", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "default_name_format": "R{0}", - "disk0": 0, - "disk1": 0, - "exec_area": 64, - "idlemax": 500, - "idlepc": "", - "idlesleep": 30, - "image": "c3660-a3jk9s-mz.124-25d.image", - "mac_addr": "", - "mmap": True, - "name": "Cisco c3600 template", - "iomem": 5, - "nvram": 128, - "platform": "c3600", - "chassis": "3660", - "private_config": "", - "ram": 192, - "sparsemem": True, - "startup_config": "ios_base_startup-config.txt", - "symbol": unittest.mock.ANY, - "system_id": "FTX0945W0MY"} + expected_response = { + "template_type": "dynamips", + "auto_delete_disks": False, + "builtin": False, + "category": "router", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "default_name_format": "R{0}", + "disk0": 0, + "disk1": 0, + "exec_area": 64, + "idlemax": 500, + "idlepc": "", + "idlesleep": 30, + "image": "c3660-a3jk9s-mz.124-25d.image", + "mac_addr": "", + "mmap": True, + "name": "Cisco c3600 template", + "iomem": 5, + "nvram": 128, + "platform": "c3600", + "chassis": "3660", + "private_config": "", + "ram": 192, + "sparsemem": True, + "startup_config": "ios_base_startup-config.txt", + "symbol": unittest.mock.ANY, + "system_id": "FTX0945W0MY", + } for item, value in expected_response.items(): assert response.json().get(item) == value async def test_c3600_dynamips_template_create_wrong_chassis(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c3600 template with wrong chassis", - "platform": "c3600", - "chassis": "3650", - "compute_id": "local", - "image": "c3660-a3jk9s-mz.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c3600 template with wrong chassis", + "platform": "c3600", + "chassis": "3650", + "compute_id": "local", + "image": "c3660-a3jk9s-mz.124-25d.image", + "template_type": "dynamips", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_400_BAD_REQUEST async def test_c2691_dynamips_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c2691 template", - "platform": "c2691", - "compute_id": "local", - "image": "c2691-adventerprisek9-mz.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c2691 template", + "platform": "c2691", + "compute_id": "local", + "image": "c2691-adventerprisek9-mz.124-25d.image", + "template_type": "dynamips", + } with asyncio_patch("gns3server.services.templates.TemplatesService._find_images", return_value=[]) as mock: response = await client.post(app.url_path_for("create_template"), json=params) @@ -850,45 +839,49 @@ class TestDynamipsTemplate: assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "dynamips", - "auto_delete_disks": False, - "builtin": False, - "category": "router", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "default_name_format": "R{0}", - "disk0": 0, - "disk1": 0, - "exec_area": 64, - "idlemax": 500, - "idlepc": "", - "idlesleep": 30, - "image": "c2691-adventerprisek9-mz.124-25d.image", - "mac_addr": "", - "mmap": True, - "name": "Cisco c2691 template", - "iomem": 5, - "nvram": 256, - "platform": "c2691", - "private_config": "", - "ram": 192, - "sparsemem": True, - "startup_config": "ios_base_startup-config.txt", - "symbol": unittest.mock.ANY, - "system_id": "FTX0945W0MY"} + expected_response = { + "template_type": "dynamips", + "auto_delete_disks": False, + "builtin": False, + "category": "router", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "default_name_format": "R{0}", + "disk0": 0, + "disk1": 0, + "exec_area": 64, + "idlemax": 500, + "idlepc": "", + "idlesleep": 30, + "image": "c2691-adventerprisek9-mz.124-25d.image", + "mac_addr": "", + "mmap": True, + "name": "Cisco c2691 template", + "iomem": 5, + "nvram": 256, + "platform": "c2691", + "private_config": "", + "ram": 192, + "sparsemem": True, + "startup_config": "ios_base_startup-config.txt", + "symbol": unittest.mock.ANY, + "system_id": "FTX0945W0MY", + } for item, value in expected_response.items(): assert response.json().get(item) == value async def test_c2600_dynamips_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c2600 template", - "platform": "c2600", - "chassis": "2651XM", - "compute_id": "local", - "image": "c2600-adventerprisek9-mz.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c2600 template", + "platform": "c2600", + "chassis": "2651XM", + "compute_id": "local", + "image": "c2600-adventerprisek9-mz.124-25d.image", + "template_type": "dynamips", + } with asyncio_patch("gns3server.services.templates.TemplatesService._find_images", return_value=[]) as mock: response = await client.post(app.url_path_for("create_template"), json=params) @@ -896,58 +889,64 @@ class TestDynamipsTemplate: assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "dynamips", - "auto_delete_disks": False, - "builtin": False, - "category": "router", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "default_name_format": "R{0}", - "disk0": 0, - "disk1": 0, - "exec_area": 64, - "idlemax": 500, - "idlepc": "", - "idlesleep": 30, - "image": "c2600-adventerprisek9-mz.124-25d.image", - "mac_addr": "", - "mmap": True, - "name": "Cisco c2600 template", - "iomem": 15, - "nvram": 128, - "platform": "c2600", - "chassis": "2651XM", - "private_config": "", - "ram": 160, - "sparsemem": True, - "startup_config": "ios_base_startup-config.txt", - "symbol": unittest.mock.ANY, - "system_id": "FTX0945W0MY"} + expected_response = { + "template_type": "dynamips", + "auto_delete_disks": False, + "builtin": False, + "category": "router", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "default_name_format": "R{0}", + "disk0": 0, + "disk1": 0, + "exec_area": 64, + "idlemax": 500, + "idlepc": "", + "idlesleep": 30, + "image": "c2600-adventerprisek9-mz.124-25d.image", + "mac_addr": "", + "mmap": True, + "name": "Cisco c2600 template", + "iomem": 15, + "nvram": 128, + "platform": "c2600", + "chassis": "2651XM", + "private_config": "", + "ram": 160, + "sparsemem": True, + "startup_config": "ios_base_startup-config.txt", + "symbol": unittest.mock.ANY, + "system_id": "FTX0945W0MY", + } for item, value in expected_response.items(): assert response.json().get(item) == value async def test_c2600_dynamips_template_create_wrong_chassis(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c2600 template with wrong chassis", - "platform": "c2600", - "chassis": "2660XM", - "compute_id": "local", - "image": "c2600-adventerprisek9-mz.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c2600 template with wrong chassis", + "platform": "c2600", + "chassis": "2660XM", + "compute_id": "local", + "image": "c2600-adventerprisek9-mz.124-25d.image", + "template_type": "dynamips", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_400_BAD_REQUEST async def test_c1700_dynamips_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c1700 template", - "platform": "c1700", - "chassis": "1760", - "compute_id": "local", - "image": "c1700-adventerprisek9-mz.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c1700 template", + "platform": "c1700", + "chassis": "1760", + "compute_id": "local", + "image": "c1700-adventerprisek9-mz.124-25d.image", + "template_type": "dynamips", + } with asyncio_patch("gns3server.services.templates.TemplatesService._find_images", return_value=[]) as mock: response = await client.post(app.url_path_for("create_template"), json=params) @@ -955,71 +954,73 @@ class TestDynamipsTemplate: assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "dynamips", - "auto_delete_disks": False, - "builtin": False, - "category": "router", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "default_name_format": "R{0}", - "disk0": 0, - "disk1": 0, - "exec_area": 64, - "idlemax": 500, - "idlepc": "", - "idlesleep": 30, - "image": "c1700-adventerprisek9-mz.124-25d.image", - "mac_addr": "", - "mmap": True, - "name": "Cisco c1700 template", - "iomem": 15, - "nvram": 128, - "platform": "c1700", - "chassis": "1760", - "private_config": "", - "ram": 160, - "sparsemem": False, - "startup_config": "ios_base_startup-config.txt", - "symbol": unittest.mock.ANY, - "system_id": "FTX0945W0MY"} + expected_response = { + "template_type": "dynamips", + "auto_delete_disks": False, + "builtin": False, + "category": "router", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "default_name_format": "R{0}", + "disk0": 0, + "disk1": 0, + "exec_area": 64, + "idlemax": 500, + "idlepc": "", + "idlesleep": 30, + "image": "c1700-adventerprisek9-mz.124-25d.image", + "mac_addr": "", + "mmap": True, + "name": "Cisco c1700 template", + "iomem": 15, + "nvram": 128, + "platform": "c1700", + "chassis": "1760", + "private_config": "", + "ram": 160, + "sparsemem": False, + "startup_config": "ios_base_startup-config.txt", + "symbol": unittest.mock.ANY, + "system_id": "FTX0945W0MY", + } for item, value in expected_response.items(): assert response.json().get(item) == value async def test_c1700_dynamips_template_create_wrong_chassis(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c1700 template with wrong chassis", - "platform": "c1700", - "chassis": "1770", - "compute_id": "local", - "image": "c1700-adventerprisek9-mz.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c1700 template with wrong chassis", + "platform": "c1700", + "chassis": "1770", + "compute_id": "local", + "image": "c1700-adventerprisek9-mz.124-25d.image", + "template_type": "dynamips", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_400_BAD_REQUEST async def test_dynamips_template_create_wrong_platform(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cisco c3900 template", - "platform": "c3900", - "compute_id": "local", - "image": "c3900-test.124-25d.image", - "template_type": "dynamips"} + params = { + "name": "Cisco c3900 template", + "platform": "c3900", + "compute_id": "local", + "image": "c3900-test.124-25d.image", + "template_type": "dynamips", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_400_BAD_REQUEST class TestIOUTemplate: - async def test_iou_template_create(self, app: FastAPI, client: AsyncClient) -> None: image_path = str(Path("/path/to/i86bi_linux-ipbase-ms-12.4.bin")) - params = {"name": "IOU template", - "compute_id": "local", - "path": image_path, - "template_type": "iou"} + params = {"name": "IOU template", "compute_id": "local", "path": image_path, "template_type": "iou"} with asyncio_patch("gns3server.services.templates.TemplatesService._find_images", return_value=[]) as mock: response = await client.post(app.url_path_for("create_template"), json=params) @@ -1027,75 +1028,81 @@ class TestIOUTemplate: assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "iou", - "builtin": False, - "category": "router", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "default_name_format": "IOU{0}", - "ethernet_adapters": 2, - "name": "IOU template", - "nvram": 256, - "path": image_path, - "private_config": "", - "ram": 1024, - "serial_adapters": 2, - "startup_config": "iou_l3_base_startup-config.txt", - "symbol": unittest.mock.ANY, - "use_default_iou_values": False, - "l1_keepalives": False} + expected_response = { + "template_type": "iou", + "builtin": False, + "category": "router", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "default_name_format": "IOU{0}", + "ethernet_adapters": 2, + "name": "IOU template", + "nvram": 256, + "path": image_path, + "private_config": "", + "ram": 1024, + "serial_adapters": 2, + "startup_config": "iou_l3_base_startup-config.txt", + "symbol": unittest.mock.ANY, + "use_default_iou_values": False, + "l1_keepalives": False, + } for item, value in expected_response.items(): assert response.json().get(item) == value class TestDockerTemplate: - async def test_docker_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Docker template", - "compute_id": "local", - "image": "gns3/endhost:latest", - "template_type": "docker"} + params = { + "name": "Docker template", + "compute_id": "local", + "image": "gns3/endhost:latest", + "template_type": "docker", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"adapters": 1, - "template_type": "docker", - "builtin": False, - "category": "guest", - "compute_id": "local", - "console_auto_start": False, - "console_http_path": "/", - "console_http_port": 80, - "console_resolution": "1024x768", - "console_type": "telnet", - "default_name_format": "{name}-{0}", - "environment": "", - "extra_hosts": "", - "image": "gns3/endhost:latest", - "name": "Docker template", - "start_command": "", - "symbol": unittest.mock.ANY, - "custom_adapters": []} + expected_response = { + "adapters": 1, + "template_type": "docker", + "builtin": False, + "category": "guest", + "compute_id": "local", + "console_auto_start": False, + "console_http_path": "/", + "console_http_port": 80, + "console_resolution": "1024x768", + "console_type": "telnet", + "default_name_format": "{name}-{0}", + "environment": "", + "extra_hosts": "", + "image": "gns3/endhost:latest", + "name": "Docker template", + "start_command": "", + "symbol": unittest.mock.ANY, + "custom_adapters": [], + } for item, value in expected_response.items(): assert response.json().get(item) == value class TestQemuTemplate: - async def test_qemu_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Qemu template", - "compute_id": "local", - "platform": "i386", - "hda_disk_image": "IOSvL2-15.2.4.0.55E.qcow2", - "ram": 512, - "template_type": "qemu"} + params = { + "name": "Qemu template", + "compute_id": "local", + "platform": "i386", + "hda_disk_image": "IOSvL2-15.2.4.0.55E.qcow2", + "ram": 512, + "template_type": "qemu", + } with asyncio_patch("gns3server.services.templates.TemplatesService._find_images", return_value=[]) as mock: response = await client.post(app.url_path_for("create_template"), json=params) @@ -1103,368 +1110,314 @@ class TestQemuTemplate: assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"adapter_type": "e1000", - "adapters": 1, - "template_type": "qemu", - "bios_image": "", - "boot_priority": "c", - "builtin": False, - "category": "guest", - "cdrom_image": "", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "cpu_throttling": 0, - "cpus": 1, - "default_name_format": "{name}-{0}", - "first_port_name": "", - "hda_disk_image": "IOSvL2-15.2.4.0.55E.qcow2", - "hda_disk_interface": "none", - "hdb_disk_image": "", - "hdb_disk_interface": "none", - "hdc_disk_image": "", - "hdc_disk_interface": "none", - "hdd_disk_image": "", - "hdd_disk_interface": "none", - "initrd": "", - "kernel_command_line": "", - "kernel_image": "", - "linked_clone": True, - "mac_address": "", - "name": "Qemu template", - "on_close": "power_off", - "options": "", - "platform": "i386", - "port_name_format": "Ethernet{0}", - "port_segment_size": 0, - "process_priority": "normal", - "qemu_path": "", - "ram": 512, - "symbol": unittest.mock.ANY, - "usage": "", - "custom_adapters": []} + expected_response = { + "adapter_type": "e1000", + "adapters": 1, + "template_type": "qemu", + "bios_image": "", + "boot_priority": "c", + "builtin": False, + "category": "guest", + "cdrom_image": "", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "cpu_throttling": 0, + "cpus": 1, + "default_name_format": "{name}-{0}", + "first_port_name": "", + "hda_disk_image": "IOSvL2-15.2.4.0.55E.qcow2", + "hda_disk_interface": "none", + "hdb_disk_image": "", + "hdb_disk_interface": "none", + "hdc_disk_image": "", + "hdc_disk_interface": "none", + "hdd_disk_image": "", + "hdd_disk_interface": "none", + "initrd": "", + "kernel_command_line": "", + "kernel_image": "", + "linked_clone": True, + "mac_address": "", + "name": "Qemu template", + "on_close": "power_off", + "options": "", + "platform": "i386", + "port_name_format": "Ethernet{0}", + "port_segment_size": 0, + "process_priority": "normal", + "qemu_path": "", + "ram": 512, + "symbol": unittest.mock.ANY, + "usage": "", + "custom_adapters": [], + } for item, value in expected_response.items(): assert response.json().get(item) == value class TestVMwareTemplate: - async def test_vmware_template_create(self, app: FastAPI, client: AsyncClient) -> None: vmx_path = str(Path("/path/to/vm.vmx")) - params = {"name": "VMware template", - "compute_id": "local", - "template_type": "vmware", - "vmx_path": vmx_path} + params = {"name": "VMware template", "compute_id": "local", "template_type": "vmware", "vmx_path": vmx_path} response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"adapter_type": "e1000", - "adapters": 1, - "template_type": "vmware", - "builtin": False, - "category": "guest", - "compute_id": "local", - "console_auto_start": False, - "console_type": "none", - "default_name_format": "{name}-{0}", - "first_port_name": "", - "headless": False, - "linked_clone": False, - "name": "VMware template", - "on_close": "power_off", - "port_name_format": "Ethernet{0}", - "port_segment_size": 0, - "symbol": unittest.mock.ANY, - "use_any_adapter": False, - "vmx_path": vmx_path, - "custom_adapters": []} + expected_response = { + "adapter_type": "e1000", + "adapters": 1, + "template_type": "vmware", + "builtin": False, + "category": "guest", + "compute_id": "local", + "console_auto_start": False, + "console_type": "none", + "default_name_format": "{name}-{0}", + "first_port_name": "", + "headless": False, + "linked_clone": False, + "name": "VMware template", + "on_close": "power_off", + "port_name_format": "Ethernet{0}", + "port_segment_size": 0, + "symbol": unittest.mock.ANY, + "use_any_adapter": False, + "vmx_path": vmx_path, + "custom_adapters": [], + } for item, value in expected_response.items(): assert response.json().get(item) == value class TestVirtualBoxTemplate: - async def test_virtualbox_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "VirtualBox template", - "compute_id": "local", - "template_type": "virtualbox", - "vmname": "My VirtualBox VM"} + params = { + "name": "VirtualBox template", + "compute_id": "local", + "template_type": "virtualbox", + "vmname": "My VirtualBox VM", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - "adapters": 1, - "template_type": "virtualbox", - "builtin": False, - "category": "guest", - "compute_id": "local", - "console_auto_start": False, - "console_type": "none", - "default_name_format": "{name}-{0}", - "first_port_name": "", - "headless": False, - "linked_clone": False, - "name": "VirtualBox template", - "on_close": "power_off", - "port_name_format": "Ethernet{0}", - "port_segment_size": 0, - "ram": 256, - "symbol": unittest.mock.ANY, - "use_any_adapter": False, - "vmname": "My VirtualBox VM", - "custom_adapters": []} + expected_response = { + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "adapters": 1, + "template_type": "virtualbox", + "builtin": False, + "category": "guest", + "compute_id": "local", + "console_auto_start": False, + "console_type": "none", + "default_name_format": "{name}-{0}", + "first_port_name": "", + "headless": False, + "linked_clone": False, + "name": "VirtualBox template", + "on_close": "power_off", + "port_name_format": "Ethernet{0}", + "port_segment_size": 0, + "ram": 256, + "symbol": unittest.mock.ANY, + "use_any_adapter": False, + "vmname": "My VirtualBox VM", + "custom_adapters": [], + } for item, value in expected_response.items(): assert response.json().get(item) == value class TestVPCSTemplate: - async def test_vpcs_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "VPCS template", - "compute_id": "local", - "template_type": "vpcs"} + params = {"name": "VPCS template", "compute_id": "local", "template_type": "vpcs"} response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "vpcs", - "base_script_file": "vpcs_base_config.txt", - "builtin": False, - "category": "guest", - "compute_id": "local", - "console_auto_start": False, - "console_type": "telnet", - "default_name_format": "PC{0}", - "name": "VPCS template", - "symbol": unittest.mock.ANY} + expected_response = { + "template_type": "vpcs", + "base_script_file": "vpcs_base_config.txt", + "builtin": False, + "category": "guest", + "compute_id": "local", + "console_auto_start": False, + "console_type": "telnet", + "default_name_format": "PC{0}", + "name": "VPCS template", + "symbol": unittest.mock.ANY, + } for item, value in expected_response.items(): assert response.json().get(item) == value class TestEthernetSwitchTemplate: - async def test_ethernet_switch_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Ethernet switch template", - "compute_id": "local", - "template_type": "ethernet_switch"} + params = {"name": "Ethernet switch template", "compute_id": "local", "template_type": "ethernet_switch"} response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "ethernet_switch", - "builtin": False, - "category": "switch", - "compute_id": "local", - "console_type": "none", - "default_name_format": "Switch{0}", - "name": "Ethernet switch template", - "ports_mapping": [{"ethertype": "0x8100", - "name": "Ethernet0", - "port_number": 0, - "type": "access", - "vlan": 1 - }, - {"ethertype": "0x8100", - "name": "Ethernet1", - "port_number": 1, - "type": "access", - "vlan": 1 - }, - {"ethertype": "0x8100", - "name": "Ethernet2", - "port_number": 2, - "type": "access", - "vlan": 1 - }, - {"ethertype": "0x8100", - "name": "Ethernet3", - "port_number": 3, - "type": "access", - "vlan": 1 - }, - {"ethertype": "0x8100", - "name": "Ethernet4", - "port_number": 4, - "type": "access", - "vlan": 1 - }, - {"ethertype": "0x8100", - "name": "Ethernet5", - "port_number": 5, - "type": "access", - "vlan": 1 - }, - {"ethertype": "0x8100", - "name": "Ethernet6", - "port_number": 6, - "type": "access", - "vlan": 1 - }, - {"ethertype": "0x8100", - "name": "Ethernet7", - "port_number": 7, - "type": "access", - "vlan": 1 - }], - "symbol": unittest.mock.ANY} + expected_response = { + "template_type": "ethernet_switch", + "builtin": False, + "category": "switch", + "compute_id": "local", + "console_type": "none", + "default_name_format": "Switch{0}", + "name": "Ethernet switch template", + "ports_mapping": [ + {"ethertype": "0x8100", "name": "Ethernet0", "port_number": 0, "type": "access", "vlan": 1}, + {"ethertype": "0x8100", "name": "Ethernet1", "port_number": 1, "type": "access", "vlan": 1}, + {"ethertype": "0x8100", "name": "Ethernet2", "port_number": 2, "type": "access", "vlan": 1}, + {"ethertype": "0x8100", "name": "Ethernet3", "port_number": 3, "type": "access", "vlan": 1}, + {"ethertype": "0x8100", "name": "Ethernet4", "port_number": 4, "type": "access", "vlan": 1}, + {"ethertype": "0x8100", "name": "Ethernet5", "port_number": 5, "type": "access", "vlan": 1}, + {"ethertype": "0x8100", "name": "Ethernet6", "port_number": 6, "type": "access", "vlan": 1}, + {"ethertype": "0x8100", "name": "Ethernet7", "port_number": 7, "type": "access", "vlan": 1}, + ], + "symbol": unittest.mock.ANY, + } for item, value in expected_response.items(): assert response.json().get(item) == value class TestHubTemplate: - async def test_ethernet_hub_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Ethernet hub template", - "compute_id": "local", - "template_type": "ethernet_hub"} + params = {"name": "Ethernet hub template", "compute_id": "local", "template_type": "ethernet_hub"} response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"ports_mapping": [{"port_number": 0, - "name": "Ethernet0" - }, - {"port_number": 1, - "name": "Ethernet1" - }, - {"port_number": 2, - "name": "Ethernet2" - }, - {"port_number": 3, - "name": "Ethernet3" - }, - {"port_number": 4, - "name": "Ethernet4" - }, - {"port_number": 5, - "name": "Ethernet5" - }, - {"port_number": 6, - "name": "Ethernet6" - }, - {"port_number": 7, - "name": "Ethernet7" - }], - "compute_id": "local", - "name": "Ethernet hub template", - "symbol": unittest.mock.ANY, - "default_name_format": "Hub{0}", - "template_type": "ethernet_hub", - "category": "switch", - "builtin": False} + expected_response = { + "ports_mapping": [ + {"port_number": 0, "name": "Ethernet0"}, + {"port_number": 1, "name": "Ethernet1"}, + {"port_number": 2, "name": "Ethernet2"}, + {"port_number": 3, "name": "Ethernet3"}, + {"port_number": 4, "name": "Ethernet4"}, + {"port_number": 5, "name": "Ethernet5"}, + {"port_number": 6, "name": "Ethernet6"}, + {"port_number": 7, "name": "Ethernet7"}, + ], + "compute_id": "local", + "name": "Ethernet hub template", + "symbol": unittest.mock.ANY, + "default_name_format": "Hub{0}", + "template_type": "ethernet_hub", + "category": "switch", + "builtin": False, + } for item, value in expected_response.items(): assert response.json().get(item) == value class TestCloudTemplate: - async def test_cloud_template_create(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Cloud template", - "compute_id": "local", - "template_type": "cloud"} + params = {"name": "Cloud template", "compute_id": "local", "template_type": "cloud"} response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_201_CREATED assert response.json()["template_id"] is not None - expected_response = {"template_type": "cloud", - "builtin": False, - "category": "guest", - "compute_id": "local", - "default_name_format": "Cloud{0}", - "name": "Cloud template", - "ports_mapping": [], - "symbol": unittest.mock.ANY, - "remote_console_host": "127.0.0.1", - "remote_console_port": 23, - "remote_console_type": "none", - "remote_console_http_path": "/"} + expected_response = { + "template_type": "cloud", + "builtin": False, + "category": "guest", + "compute_id": "local", + "default_name_format": "Cloud{0}", + "name": "Cloud template", + "ports_mapping": [], + "symbol": unittest.mock.ANY, + "remote_console_host": "127.0.0.1", + "remote_console_port": 23, + "remote_console_type": "none", + "remote_console_http_path": "/", + } for item, value in expected_response.items(): assert response.json().get(item) == value class TestImageAssociationWithTemplate: - @pytest.mark.parametrize( "image_name, image_type, params", ( - ( - "c7200-adventerprisek9-mz.124-24.T5.image", - "ios", - { - "template_id": "6d85c8db-640f-4547-8955-bc132f7d7196", - "name": "Cisco c7200 template", - "platform": "c7200", - "compute_id": "local", - "image": "", - "template_type": "dynamips" - } - ), - ( - "i86bi_linux-ipbase-ms-12.4.bin", - "iou", - { - "template_id": "0014185e-bdfe-454b-86cd-9009c23900c5", - "name": "IOU template", - "compute_id": "local", - "path": "", - "template_type": "iou" - } - ), - ( - "image.qcow2", - "qemu", - { - "template_id": "97ef56a5-7ae4-4795-ad4c-e7dcdd745cff", - "name": "Qemu template", - "compute_id": "local", - "platform": "i386", - "hda_disk_image": "", - "hdb_disk_image": "", - "hdc_disk_image": "", - "hdd_disk_image": "", - "cdrom_image": "", - "kernel_image": "", - "bios_image": "", - "ram": 512, - "template_type": "qemu" - } - ), + ( + "c7200-adventerprisek9-mz.124-24.T5.image", + "ios", + { + "template_id": "6d85c8db-640f-4547-8955-bc132f7d7196", + "name": "Cisco c7200 template", + "platform": "c7200", + "compute_id": "local", + "image": "", + "template_type": "dynamips", + }, + ), + ( + "i86bi_linux-ipbase-ms-12.4.bin", + "iou", + { + "template_id": "0014185e-bdfe-454b-86cd-9009c23900c5", + "name": "IOU template", + "compute_id": "local", + "path": "", + "template_type": "iou", + }, + ), + ( + "image.qcow2", + "qemu", + { + "template_id": "97ef56a5-7ae4-4795-ad4c-e7dcdd745cff", + "name": "Qemu template", + "compute_id": "local", + "platform": "i386", + "hda_disk_image": "", + "hdb_disk_image": "", + "hdc_disk_image": "", + "hdd_disk_image": "", + "cdrom_image": "", + "kernel_image": "", + "bios_image": "", + "ram": 512, + "template_type": "qemu", + }, + ), ), ) async def test_template_create_with_images( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - tmpdir: str, - image_name: str, - image_type: str, - params: dict + self, + app: FastAPI, + client: AsyncClient, + db_session: AsyncSession, + tmpdir: str, + image_name: str, + image_type: str, + params: dict, ) -> None: path = os.path.join(tmpdir, image_name) with open(path, "wb+") as f: - f.write(b'\x42\x42\x42\x42') + f.write(b"\x42\x42\x42\x42") images_repo = ImagesRepository(db_session) await images_repo.add_image(image_name, image_type, 42, path, "e342eb86c1229b6c154367a5476969b5", "md5") for key, value in params.items(): @@ -1481,53 +1434,53 @@ class TestImageAssociationWithTemplate: @pytest.mark.parametrize( "image_name, image_type, template_id, params", ( - ( - "c7200-adventerprisek9-mz.155-2.XB.image", - "ios", - "6d85c8db-640f-4547-8955-bc132f7d7196", - { - "image": "", - } - ), - ( - "i86bi-linux-l2-adventerprisek9-15.2d.bin", - "iou", - "0014185e-bdfe-454b-86cd-9009c23900c5", - { - "path": "", - } - ), - ( - "new_image.qcow2", - "qemu", - "97ef56a5-7ae4-4795-ad4c-e7dcdd745cff", - { - "hda_disk_image": "", - "hdb_disk_image": "", - "hdc_disk_image": "", - "hdd_disk_image": "", - "cdrom_image": "", - "kernel_image": "", - "bios_image": "", - } - ), + ( + "c7200-adventerprisek9-mz.155-2.XB.image", + "ios", + "6d85c8db-640f-4547-8955-bc132f7d7196", + { + "image": "", + }, + ), + ( + "i86bi-linux-l2-adventerprisek9-15.2d.bin", + "iou", + "0014185e-bdfe-454b-86cd-9009c23900c5", + { + "path": "", + }, + ), + ( + "new_image.qcow2", + "qemu", + "97ef56a5-7ae4-4795-ad4c-e7dcdd745cff", + { + "hda_disk_image": "", + "hdb_disk_image": "", + "hdc_disk_image": "", + "hdd_disk_image": "", + "cdrom_image": "", + "kernel_image": "", + "bios_image": "", + }, + ), ), ) async def test_template_update_with_images( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - tmpdir: str, - image_name: str, - image_type: str, - template_id: str, - params: dict + self, + app: FastAPI, + client: AsyncClient, + db_session: AsyncSession, + tmpdir: str, + image_name: str, + image_type: str, + template_id: str, + params: dict, ) -> None: path = os.path.join(tmpdir, image_name) with open(path, "wb+") as f: - f.write(b'\x42\x42\x42\x42') + f.write(b"\x42\x42\x42\x42") images_repo = ImagesRepository(db_session) await images_repo.add_image(image_name, image_type, 42, path, "e342eb86c1229b6c154367a5476969b5", "md5") @@ -1545,39 +1498,34 @@ class TestImageAssociationWithTemplate: @pytest.mark.parametrize( "template_id, params", ( - ( - "6d85c8db-640f-4547-8955-bc132f7d7196", - { - "image": "", - } - ), - ( - "0014185e-bdfe-454b-86cd-9009c23900c5", - { - "path": "", - } - ), - ( - "97ef56a5-7ae4-4795-ad4c-e7dcdd745cff", - { - "hda_disk_image": "", - "hdb_disk_image": "", - "hdc_disk_image": "", - "hdd_disk_image": "", - "cdrom_image": "", - "kernel_image": "", - "bios_image": "", - } - ), + ( + "6d85c8db-640f-4547-8955-bc132f7d7196", + { + "image": "", + }, + ), + ( + "0014185e-bdfe-454b-86cd-9009c23900c5", + { + "path": "", + }, + ), + ( + "97ef56a5-7ae4-4795-ad4c-e7dcdd745cff", + { + "hda_disk_image": "", + "hdb_disk_image": "", + "hdc_disk_image": "", + "hdd_disk_image": "", + "cdrom_image": "", + "kernel_image": "", + "bios_image": "", + }, + ), ), ) async def test_remove_images_from_template( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - template_id: str, - params: dict + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, template_id: str, params: dict ) -> None: for key, value in params.items(): @@ -1591,25 +1539,27 @@ class TestImageAssociationWithTemplate: assert len(db_template.images) == 0 async def test_template_create_with_image_in_subdir( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - tmpdir: str, + self, + app: FastAPI, + client: AsyncClient, + db_session: AsyncSession, + tmpdir: str, ) -> None: - params = {"name": "Qemu template", - "version": "1.0", - "compute_id": "local", - "platform": "i386", - "hda_disk_image": "subdir/image.qcow2", - "ram": 512, - "template_type": "qemu"} + params = { + "name": "Qemu template", + "version": "1.0", + "compute_id": "local", + "platform": "i386", + "hda_disk_image": "subdir/image.qcow2", + "ram": 512, + "template_type": "qemu", + } path = os.path.join(tmpdir, "subdir", "image.qcow2") os.makedirs(os.path.dirname(path)) with open(path, "wb+") as f: - f.write(b'\x42\x42\x42\x42') + f.write(b"\x42\x42\x42\x42") images_repo = ImagesRepository(db_session) await images_repo.add_image("image.qcow2", "qemu", 42, path, "e342eb86c1229b6c154367a5476969b5", "md5") @@ -1624,12 +1574,14 @@ class TestImageAssociationWithTemplate: async def test_template_create_with_non_existing_image(self, app: FastAPI, client: AsyncClient) -> None: - params = {"name": "Qemu template with non existing image", - "compute_id": "local", - "platform": "i386", - "hda_disk_image": "unkown_image.qcow2", - "ram": 512, - "template_type": "qemu"} + params = { + "name": "Qemu template with non existing image", + "compute_id": "local", + "platform": "i386", + "hda_disk_image": "unkown_image.qcow2", + "ram": 512, + "template_type": "qemu", + } response = await client.post(app.url_path_for("create_template"), json=params) assert response.status_code == status.HTTP_404_NOT_FOUND diff --git a/tests/api/routes/controller/test_users.py b/tests/api/routes/controller/test_users.py index e30719d02..8bdf437ad 100644 --- a/tests/api/routes/controller/test_users.py +++ b/tests/api/routes/controller/test_users.py @@ -35,7 +35,6 @@ pytestmark = pytest.mark.asyncio class TestUserRoutes: - async def test_route_exist(self, app: FastAPI, client: AsyncClient) -> None: new_user = {"username": "user1", "email": "user1@email.com", "password": "test_password"} @@ -43,10 +42,7 @@ class TestUserRoutes: assert response.status_code == status.HTTP_201_CREATED async def test_users_can_register_successfully( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession ) -> None: user_repo = UsersRepository(db_session) @@ -73,21 +69,21 @@ class TestUserRoutes: @pytest.mark.parametrize( "attr, value, status_code", ( - ("email", "user2@email.com", status.HTTP_400_BAD_REQUEST), - ("username", "user2", status.HTTP_400_BAD_REQUEST), - ("email", "invalid_email@one@two.io", status.HTTP_422_UNPROCESSABLE_CONTENT), - ("password", "short", status.HTTP_422_UNPROCESSABLE_CONTENT), - ("username", "user2@#$%^<>", status.HTTP_422_UNPROCESSABLE_CONTENT), - ("username", "ab", status.HTTP_422_UNPROCESSABLE_CONTENT), - ) + ("email", "user2@email.com", status.HTTP_400_BAD_REQUEST), + ("username", "user2", status.HTTP_400_BAD_REQUEST), + ("email", "invalid_email@one@two.io", status.HTTP_422_UNPROCESSABLE_CONTENT), + ("password", "short", status.HTTP_422_UNPROCESSABLE_CONTENT), + ("username", "user2@#$%^<>", status.HTTP_422_UNPROCESSABLE_CONTENT), + ("username", "ab", status.HTTP_422_UNPROCESSABLE_CONTENT), + ), ) async def test_user_registration_fails_when_credentials_are_taken( - self, - app: FastAPI, - client: AsyncClient, - attr: str, - value: str, - status_code: int, + self, + app: FastAPI, + client: AsyncClient, + attr: str, + value: str, + status_code: int, ) -> None: new_user = {"email": "not_taken@email.com", "username": "not_taken_username", "password": "test_password"} @@ -98,26 +94,26 @@ class TestUserRoutes: @pytest.mark.parametrize( "attr, value, status_code", ( - ("email", "user@email.com", status.HTTP_200_OK), - ("email", "user@email.com", status.HTTP_400_BAD_REQUEST), - ("username", "user2", status.HTTP_400_BAD_REQUEST), - ("email", "invalid_email@one@two.io", status.HTTP_422_UNPROCESSABLE_CONTENT), - ("password", "short", status.HTTP_422_UNPROCESSABLE_CONTENT), - ("username", "user2@#$%^<>", status.HTTP_422_UNPROCESSABLE_CONTENT), - ("username", "ab", status.HTTP_422_UNPROCESSABLE_CONTENT), - ("full_name", "John Doe", status.HTTP_200_OK), - ("password", "password123", status.HTTP_200_OK), - ("is_active", True, status.HTTP_200_OK), - ) + ("email", "user@email.com", status.HTTP_200_OK), + ("email", "user@email.com", status.HTTP_400_BAD_REQUEST), + ("username", "user2", status.HTTP_400_BAD_REQUEST), + ("email", "invalid_email@one@two.io", status.HTTP_422_UNPROCESSABLE_CONTENT), + ("password", "short", status.HTTP_422_UNPROCESSABLE_CONTENT), + ("username", "user2@#$%^<>", status.HTTP_422_UNPROCESSABLE_CONTENT), + ("username", "ab", status.HTTP_422_UNPROCESSABLE_CONTENT), + ("full_name", "John Doe", status.HTTP_200_OK), + ("password", "password123", status.HTTP_200_OK), + ("is_active", True, status.HTTP_200_OK), + ), ) async def test_update_user( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession, - attr: str, - value: str, - status_code: int, + self, + app: FastAPI, + client: AsyncClient, + db_session: AsyncSession, + attr: str, + value: str, + status_code: int, ) -> None: user_repo = UsersRepository(db_session) @@ -128,10 +124,7 @@ class TestUserRoutes: assert response.status_code == status_code async def test_users_saved_password_is_hashed( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession + self, app: FastAPI, client: AsyncClient, db_session: AsyncSession ) -> None: user_repo = UsersRepository(db_session) @@ -156,13 +149,8 @@ class TestUserRoutes: class TestAuthTokens: - async def test_can_create_token_successfully( - self, - app: FastAPI, - client: AsyncClient, - test_user: User, - config: Config + self, app: FastAPI, client: AsyncClient, test_user: User, config: Config ) -> None: jwt_secret = config.settings.Controller.jwt_secret_key @@ -173,11 +161,7 @@ class TestAuthTokens: assert username == test_user.username async def test_decode_token_with_wrong_algorithm( - self, - app: FastAPI, - client: AsyncClient, - test_user: User, - config: Config + self, app: FastAPI, client: AsyncClient, test_user: User, config: Config ) -> None: jwt_secret = config.settings.Controller.jwt_secret_key @@ -185,12 +169,7 @@ class TestAuthTokens: with pytest.raises(ValueError): jwt.decode(token, jwt_secret, algorithms=["HS256"]) - async def test_can_retrieve_username_from_token( - self, - app: FastAPI, - client: AsyncClient, - test_user: User - ) -> None: + async def test_can_retrieve_username_from_token(self, app: FastAPI, client: AsyncClient, test_user: User) -> None: token = auth_service.create_access_token(test_user.username) username = auth_service.get_username_from_token(token) @@ -199,19 +178,19 @@ class TestAuthTokens: @pytest.mark.parametrize( "wrong_secret, wrong_token", ( - ("use correct secret", "asdf"), # use wrong token - ("use correct secret", ""), # use wrong token - (OctKey.generate_key().as_dict()['k'], "use correct token"), # use wrong secret + ("use correct secret", "asdf"), # use wrong token + ("use correct secret", ""), # use wrong token + (OctKey.generate_key().as_dict()["k"], "use correct token"), # use wrong secret ), ) async def test_error_when_token_or_secret_is_wrong( - self, - app: FastAPI, - client: AsyncClient, - test_user: User, - wrong_secret: str, - wrong_token: Optional[str], - config, + self, + app: FastAPI, + client: AsyncClient, + test_user: User, + wrong_secret: str, + wrong_token: Optional[str], + config, ) -> None: token = auth_service.create_access_token(test_user.username) @@ -224,13 +203,8 @@ class TestAuthTokens: class TestUserLogin: - async def test_user_can_login_successfully_and_receives_valid_token( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, - config: Config + self, app: FastAPI, unauthorized_client: AsyncClient, test_user: User, config: Config ) -> None: jwt_secret = config.settings.Controller.jwt_secret_key @@ -290,22 +264,18 @@ class TestUserLogin: class TestUnauthorizedUser: - async def test_user_cannot_access_own_data_if_not_authenticated( - self, app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: response = await unauthorized_client.get(app.url_path_for("get_logged_in_user")) assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_user_can_authenticate_using_json( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, - config: Config + self, app: FastAPI, unauthorized_client: AsyncClient, test_user: User, config: Config ) -> None: credentials = { @@ -321,13 +291,13 @@ class TestUnauthorizedUser: response = await unauthorized_client.get(app.url_path_for("statistics"), params={"token": token}) assert response.status_code == status.HTTP_200_OK -class TestUserMe: +class TestUserMe: async def test_authenticated_user_can_retrieve_own_data( - self, - app: FastAPI, - authorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + authorized_client: AsyncClient, + test_user: User, ) -> None: response = await authorized_client.get(app.url_path_for("get_logged_in_user")) @@ -338,10 +308,10 @@ class TestUserMe: assert user.user_id == test_user.user_id async def test_authenticated_user_can_update_own_data( - self, - app: FastAPI, - authorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + authorized_client: AsyncClient, + test_user: User, ) -> None: response = await authorized_client.get(app.url_path_for("get_logged_in_user")) @@ -355,37 +325,33 @@ class TestUserMe: @pytest.mark.parametrize( "attr, value, status_code", ( - ("email", "user42@email.com", status.HTTP_200_OK), - ("email", "user42@email.com", status.HTTP_400_BAD_REQUEST), - ("full_name", "John Doe", status.HTTP_200_OK), - ("password", "password123", status.HTTP_200_OK), - ) + ("email", "user42@email.com", status.HTTP_200_OK), + ("email", "user42@email.com", status.HTTP_400_BAD_REQUEST), + ("full_name", "John Doe", status.HTTP_200_OK), + ("password", "password123", status.HTTP_200_OK), + ), ) async def test_authenticated_user_can_update_own_data( - self, - app: FastAPI, - authorized_client: AsyncClient, - attr: str, - value: str, - status_code: int, + self, + app: FastAPI, + authorized_client: AsyncClient, + attr: str, + value: str, + status_code: int, ) -> None: update_user = {} update_user[attr] = value - response = await authorized_client.put( - app.url_path_for("update_logged_in_user"), - json=update_user - ) + response = await authorized_client.put(app.url_path_for("update_logged_in_user"), json=update_user) assert response.status_code == status_code class TestLogout: - async def test_logout_returns_no_content( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: # login to get a fresh token that includes token_version @@ -395,17 +361,16 @@ class TestLogout: token = response.json()["access_token"] response = await unauthorized_client.post( - app.url_path_for("logout"), - headers={"Authorization": f"Bearer {token}"} + app.url_path_for("logout"), headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == status.HTTP_204_NO_CONTENT async def test_token_is_rejected_after_logout( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, - db_session: AsyncSession, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, + db_session: AsyncSession, ) -> None: # login and get a token @@ -415,24 +380,20 @@ class TestLogout: token = response.json()["access_token"] # logout — increments token_version - await unauthorized_client.post( - app.url_path_for("logout"), - headers={"Authorization": f"Bearer {token}"} - ) + await unauthorized_client.post(app.url_path_for("logout"), headers={"Authorization": f"Bearer {token}"}) # old token must now be rejected response = await unauthorized_client.get( - app.url_path_for("get_logged_in_user"), - headers={"Authorization": f"Bearer {token}"} + app.url_path_for("get_logged_in_user"), headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.json()["message"] == f"Token has been revoked for '{test_user.username}'" async def test_new_token_works_after_logout_and_relogin( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: credentials = {"username": test_user.username, "password": "user1_password"} @@ -440,10 +401,7 @@ class TestLogout: # login, then logout response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials) old_token = response.json()["access_token"] - await unauthorized_client.post( - app.url_path_for("logout"), - headers={"Authorization": f"Bearer {old_token}"} - ) + await unauthorized_client.post(app.url_path_for("logout"), headers={"Authorization": f"Bearer {old_token}"}) # login again to get a fresh token response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials) @@ -452,18 +410,17 @@ class TestLogout: # new token must work response = await unauthorized_client.get( - app.url_path_for("get_logged_in_user"), - headers={"Authorization": f"Bearer {new_token}"} + app.url_path_for("get_logged_in_user"), headers={"Authorization": f"Bearer {new_token}"} ) assert response.status_code == status.HTTP_200_OK assert response.json()["username"] == test_user.username async def test_stale_version_token_is_rejected( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, - db_session: AsyncSession, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, + db_session: AsyncSession, ) -> None: # craft a token with ver=0 while the user's token_version is already higher @@ -475,15 +432,14 @@ class TestLogout: stale_token = auth_service.create_access_token(test_user.username, token_version=0) response = await unauthorized_client.get( - app.url_path_for("get_logged_in_user"), - headers={"Authorization": f"Bearer {stale_token}"} + app.url_path_for("get_logged_in_user"), headers={"Authorization": f"Bearer {stale_token}"} ) assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_logout_without_token_returns_unauthorized( - self, - app: FastAPI, - unauthorized_client: AsyncClient, + self, + app: FastAPI, + unauthorized_client: AsyncClient, ) -> None: response = await unauthorized_client.post(app.url_path_for("logout")) @@ -491,27 +447,26 @@ class TestLogout: class TestRefreshToken: - async def test_login_returns_refresh_token( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: credentials = {"username": test_user.username, "password": "user1_password"} - response = await unauthorized_client.post(app.url_path_for("login"), - data=credentials, - headers={"content-type": "application/x-www-form-urlencoded"}) + response = await unauthorized_client.post( + app.url_path_for("login"), data=credentials, headers={"content-type": "application/x-www-form-urlencoded"} + ) assert response.status_code == status.HTTP_200_OK assert "refresh_token" in response.json() assert response.json().get("refresh_token") is not None async def test_authenticate_returns_refresh_token( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: credentials = {"username": test_user.username, "password": "user1_password"} @@ -521,10 +476,10 @@ class TestRefreshToken: assert response.json().get("refresh_token") is not None async def test_refresh_endpoint_returns_new_tokens( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: # authenticate to get a refresh token @@ -544,10 +499,10 @@ class TestRefreshToken: assert response.json().get("refresh_token") is not None async def test_new_access_token_from_refresh_works_on_protected_route( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: credentials = {"username": test_user.username, "password": "user1_password"} @@ -570,10 +525,10 @@ class TestRefreshToken: assert response.json()["username"] == test_user.username async def test_refresh_with_stale_token_after_logout( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: # authenticate and get a refresh token @@ -583,10 +538,7 @@ class TestRefreshToken: # logout — bumps token_version, invalidating the refresh token access_token = auth_response.json()["access_token"] - await unauthorized_client.post( - app.url_path_for("logout"), - headers={"Authorization": f"Bearer {access_token}"} - ) + await unauthorized_client.post(app.url_path_for("logout"), headers={"Authorization": f"Bearer {access_token}"}) # /refresh must now reject the stale refresh token response = await unauthorized_client.post( @@ -596,10 +548,10 @@ class TestRefreshToken: assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_refresh_rejects_access_token( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: # an access token presented at /refresh must be rejected @@ -614,10 +566,10 @@ class TestRefreshToken: assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_refresh_rejects_expired_token( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: # a refresh token with an already-expired timestamp @@ -630,9 +582,9 @@ class TestRefreshToken: assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_refresh_rejects_invalid_token( - self, - app: FastAPI, - unauthorized_client: AsyncClient, + self, + app: FastAPI, + unauthorized_client: AsyncClient, ) -> None: response = await unauthorized_client.post( @@ -642,10 +594,10 @@ class TestRefreshToken: assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_refresh_token_rejected_as_bearer( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - test_user: User, + self, + app: FastAPI, + unauthorized_client: AsyncClient, + test_user: User, ) -> None: # a refresh token used as a bearer access token must be rejected @@ -661,25 +613,14 @@ class TestRefreshToken: class TestSuperAdmin: - - async def test_super_admin_exists( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_super_admin_exists(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: user_repo = UsersRepository(db_session) admin_in_db = await user_repo.get_user_by_username("admin") assert admin_in_db is not None assert auth_service.verify_password("admin", admin_in_db.hashed_password) - async def test_cannot_delete_super_admin( - self, - app: FastAPI, - client: AsyncClient, - db_session: AsyncSession - ) -> None: + async def test_cannot_delete_super_admin(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: user_repo = UsersRepository(db_session) admin_in_db = await user_repo.get_user_by_username("admin") @@ -687,10 +628,7 @@ class TestSuperAdmin: assert response.status_code == status.HTTP_403_FORBIDDEN async def test_admin_can_login_after_password_recovery( - self, - app: FastAPI, - unauthorized_client: AsyncClient, - db_session: AsyncSession + self, app: FastAPI, unauthorized_client: AsyncClient, db_session: AsyncSession ) -> None: # set the admin password to null in the database @@ -705,4 +643,3 @@ class TestSuperAdmin: } response = await unauthorized_client.post(app.url_path_for("login"), data=login_data) assert response.status_code == status.HTTP_200_OK - diff --git a/tests/api/routes/controller/test_version.py b/tests/api/routes/controller/test_version.py index 95824067a..f8c205744 100644 --- a/tests/api/routes/controller/test_version.py +++ b/tests/api/routes/controller/test_version.py @@ -26,40 +26,36 @@ pytestmark = pytest.mark.asyncio class TestVersionRoutes: - - async def test_version_output(self, app: FastAPI, client: AsyncClient) -> None: response = await client.get(app.url_path_for("get_version")) assert response.status_code == status.HTTP_200_OK - assert response.json() == {'controller_host': '127.0.0.1', 'local': False, 'version': __version__} - - + assert response.json() == {"controller_host": "127.0.0.1", "local": False, "version": __version__} + async def test_version_input(self, app: FastAPI, client: AsyncClient) -> None: - - params = {'version': __version__} + + params = {"version": __version__} response = await client.post(app.url_path_for("check_version"), json=params) assert response.status_code == status.HTTP_200_OK - assert response.json() == {'version': __version__} - - + assert response.json() == {"version": __version__} + async def test_version_invalid_input(self, app: FastAPI, client: AsyncClient) -> None: - - params = {'version': "0.4.2"} + + params = {"version": "0.4.2"} response = await client.post(app.url_path_for("check_version"), json=params) assert response.status_code == status.HTTP_409_CONFLICT - assert response.json() == {'message': 'Client version 0.4.2 is not the same as server version {}'.format(__version__)} - - + assert response.json() == { + "message": "Client version 0.4.2 is not the same as server version {}".format(__version__) + } + async def test_version_invalid_input_schema(self, app: FastAPI, client: AsyncClient) -> None: - - params = {'version': "0.4.2", "bla": "blu"} + + params = {"version": "0.4.2", "bla": "blu"} response = await client.post(app.url_path_for("check_version"), json=params) assert response.status_code == status.HTTP_409_CONFLICT - - + async def test_version_invalid_json(self, app: FastAPI, client: AsyncClient) -> None: - + params = "BOUM" response = await client.post(app.url_path_for("check_version"), json=params) assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT diff --git a/tests/api/routes/test_index.py b/tests/api/routes/test_index.py index 93cef6ddb..9cdb09851 100644 --- a/tests/api/routes/test_index.py +++ b/tests/api/routes/test_index.py @@ -30,12 +30,10 @@ pytestmark = pytest.mark.asyncio class TestIndexRoutes: - def get_static(self, filename): current_dir = os.path.dirname(os.path.abspath(__file__)) - return os.path.join(os.path.abspath(os.path.join(current_dir, '../..', '..', 'gns3server', 'static')), filename) - + return os.path.join(os.path.abspath(os.path.join(current_dir, "../..", "..", "gns3server", "static")), filename) async def test_debug(self, app: FastAPI, client: AsyncClient) -> None: @@ -45,7 +43,6 @@ class TestIndexRoutes: assert "Website" in html assert __version__ in html - # @pytest.mark.asyncio # async def test_controller(http_client, controller): # @@ -61,7 +58,6 @@ class TestIndexRoutes: # response = await http_client.get('/compute') # assert response.status_code == 200 - # @pytest.mark.asyncio # async def test_project(http_client, controller): # @@ -69,16 +65,14 @@ class TestIndexRoutes: # response = await http_client.get('/projects/{}'.format(project.id)) # assert response.status_code == 200 - async def test_web_ui(self, app: FastAPI, client: AsyncClient) -> None: response = await client.get(app.url_path_for("web_ui", file_path="index.html")) assert response.status_code == status.HTTP_200_OK - async def test_web_ui_not_found(self, app: FastAPI, client: AsyncClient, tmpdir: str) -> None: - with patch('gns3server.utils.get_resource.get_resource') as mock: + with patch("gns3server.utils.get_resource.get_resource") as mock: mock.return_value = str(tmpdir) response = await client.get(app.url_path_for("web_ui", file_path="not-found.txt")) # should serve web-ui/index.html diff --git a/tests/api/routes/test_routes.py b/tests/api/routes/test_routes.py index bc357d668..8628e427d 100644 --- a/tests/api/routes/test_routes.py +++ b/tests/api/routes/test_routes.py @@ -25,7 +25,6 @@ from httpx_ws.transport import ASGIWebSocketTransport from typing import Iterator, Sequence, Tuple - pytestmark = pytest.mark.asyncio ALLOWED_CONTROLLER_ENDPOINTS = [ @@ -64,14 +63,14 @@ def _join_paths(prefix: str, path: str) -> str: def _iter_routes( - routes: Sequence[BaseRoute], - prefix: str = "", - include_mounted_routes: bool = False + routes: Sequence[BaseRoute], prefix: str = "", include_mounted_routes: bool = False ) -> Iterator[Tuple[str, BaseRoute]]: for route in routes: if isinstance(route, _IncludedRouter): include_prefix = route.include_context.prefix or "" - yield from _iter_routes(route.original_router.routes, _join_paths(prefix, include_prefix), include_mounted_routes) + yield from _iter_routes( + route.original_router.routes, _join_paths(prefix, include_prefix), include_mounted_routes + ) continue if isinstance(route, (APIRoute, APIWebSocketRoute)): @@ -85,12 +84,9 @@ def _iter_routes( class TestRoutes: - # Controller endpoints have a OAuth2 bearer token authentication async def test_controller_endpoints_require_authentication( - self, - app: FastAPI, - unauthorized_client: AsyncClient + self, app: FastAPI, unauthorized_client: AsyncClient ) -> None: for path, route in _iter_routes(app.routes): @@ -99,23 +95,22 @@ class TestRoutes: if (path, method) not in ALLOWED_CONTROLLER_ENDPOINTS: request_path = path.rstrip("/") response = await getattr(unauthorized_client, method.lower())(request_path) - assert response.status_code == status.HTTP_401_UNAUTHORIZED, f"{method} {request_path} -> {response.status_code}" + assert response.status_code == status.HTTP_401_UNAUTHORIZED, ( + f"{method} {request_path} -> {response.status_code}" + ) elif isinstance(route, APIWebSocketRoute) and not path.startswith("/v3/compute"): params = {"token": "wrong_token"} async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app=app)) as client: async with aconnect_ws(path, client, params=params) as ws: json_notification = await ws.receive_json() - assert json_notification['event'] == { - 'message': 'Could not authenticate while connecting to controller WebSocket: ' - 'Invalid token (DecodeError) (received token sha256 prefix: 4d4f92fb)' + assert json_notification["event"] == { + "message": "Could not authenticate while connecting to controller WebSocket: " + "Invalid token (DecodeError) (received token sha256 prefix: 4d4f92fb)" } - # Compute endpoints have a basic HTTP authentication async def test_compute_endpoints_require_authentication( - self, - app: FastAPI, - unauthorized_client: AsyncClient + self, app: FastAPI, unauthorized_client: AsyncClient ) -> None: for path, route in _iter_routes(app.routes, include_mounted_routes=True): @@ -126,13 +121,15 @@ class TestRoutes: for method in list(route.methods): request_path = path.rstrip("/") response = await getattr(unauthorized_client, method.lower())(request_path) - #if response.status_code == status.HTTP_307_TEMPORARY_REDIRECT: + # if response.status_code == status.HTTP_307_TEMPORARY_REDIRECT: # response = await getattr(unauthorized_client, method.lower())(response.headers["location"]) - assert response.status_code == status.HTTP_401_UNAUTHORIZED, f"{method} {request_path} -> {response.status_code}" + assert response.status_code == status.HTTP_401_UNAUTHORIZED, ( + f"{method} {request_path} -> {response.status_code}" + ) elif isinstance(route, APIWebSocketRoute): async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app=app)) as client: async with aconnect_ws(path, client, auth=("wrong_user", "password123")) as ws: json_notification = await ws.receive_json() - assert json_notification['event'] == { - 'message': 'Could not authenticate while connecting to compute WebSocket: Could not validate credentials' + assert json_notification["event"] == { + "message": "Could not authenticate while connecting to compute WebSocket: Could not validate credentials" } diff --git a/tests/compute/builtin/nodes/test_cloud.py b/tests/compute/builtin/nodes/test_cloud.py index 60dbc1541..a25e390df 100644 --- a/tests/compute/builtin/nodes/test_cloud.py +++ b/tests/compute/builtin/nodes/test_cloud.py @@ -27,6 +27,7 @@ from tests.utils import asyncio_patch pytestmark = pytest.mark.asyncio + @pytest.fixture def nio(): @@ -63,19 +64,39 @@ async def test_json_with_ports(on_gns3vm, compute_project, manager): "remote_console_type": "none", "status": "stopped", "node_directory": cloud.working_dir, - "ports_mapping": [ - { - "interface": "virbr0", - "name": "virbr0", - "port_number": 0, - "type": "ethernet" - } - ], + "ports_mapping": [{"interface": "virbr0", "name": "virbr0", "port_number": 0, "type": "ethernet"}], "interfaces": [ - {'name': 'eth0', 'special': False, 'type': 'ethernet', 'ip_addresses': [], 'status': 'up', 'speed': 1000, 'mtu': 1500, 'flags': ['up', 'broadcast', 'running', 'multicast']}, - {'name': 'eth1', 'special': False, 'type': 'ethernet', 'ip_addresses': [], 'status': 'down', 'speed': 0, 'mtu': 1500, 'flags': ['broadcast']}, - {'name': 'virbr0', 'special': True, 'type': 'ethernet', 'ip_addresses': [], 'status': 'up', 'speed': 10000, 'mtu': 1500, 'flags': ['up', 'broadcast', 'running', 'multicast']} - ] + { + "name": "eth0", + "special": False, + "type": "ethernet", + "ip_addresses": [], + "status": "up", + "speed": 1000, + "mtu": 1500, + "flags": ["up", "broadcast", "running", "multicast"], + }, + { + "name": "eth1", + "special": False, + "type": "ethernet", + "ip_addresses": [], + "status": "down", + "speed": 0, + "mtu": 1500, + "flags": ["broadcast"], + }, + { + "name": "virbr0", + "special": True, + "type": "ethernet", + "ip_addresses": [], + "status": "up", + "speed": 10000, + "mtu": 1500, + "flags": ["up", "broadcast", "running", "multicast"], + }, + ], } @@ -97,24 +118,41 @@ async def test_json_without_ports(on_gns3vm, compute_project, manager): "status": "stopped", "node_directory": cloud.working_dir, "ports_mapping": [ - { - "interface": "eth0", - "name": "eth0", - "port_number": 0, - "type": "ethernet" - }, - { - "interface": "eth1", - "name": "eth1", - "port_number": 1, - "type": "ethernet" - } + {"interface": "eth0", "name": "eth0", "port_number": 0, "type": "ethernet"}, + {"interface": "eth1", "name": "eth1", "port_number": 1, "type": "ethernet"}, ], "interfaces": [ - {'name': 'eth0', 'special': False, 'type': 'ethernet', 'ip_addresses': [], 'status': 'up', 'speed': 1000, 'mtu': 1500, 'flags': ['up', 'broadcast', 'running', 'multicast']}, - {'name': 'eth1', 'special': False, 'type': 'ethernet', 'ip_addresses': [], 'status': 'down', 'speed': 0, 'mtu': 1500, 'flags': ['broadcast']}, - {'name': 'virbr0', 'special': True, 'type': 'ethernet', 'ip_addresses': [], 'status': 'up', 'speed': 10000, 'mtu': 1500, 'flags': ['up', 'broadcast', 'running', 'multicast']} - ] + { + "name": "eth0", + "special": False, + "type": "ethernet", + "ip_addresses": [], + "status": "up", + "speed": 1000, + "mtu": 1500, + "flags": ["up", "broadcast", "running", "multicast"], + }, + { + "name": "eth1", + "special": False, + "type": "ethernet", + "ip_addresses": [], + "status": "down", + "speed": 0, + "mtu": 1500, + "flags": ["broadcast"], + }, + { + "name": "virbr0", + "special": True, + "type": "ethernet", + "ip_addresses": [], + "status": "up", + "speed": 10000, + "mtu": 1500, + "flags": ["up", "broadcast", "running", "multicast"], + }, + ], } @@ -124,49 +162,22 @@ async def test_update_port_mappings(on_gns3vm, compute_project): """ ports1 = [ - { - "interface": "eth0", - "name": "eth0", - "port_number": 0, - "type": "ethernet" - }, - { - "interface": "eth1", - "name": "eth1", - "port_number": 1, - "type": "ethernet" - } + {"interface": "eth0", "name": "eth0", "port_number": 0, "type": "ethernet"}, + {"interface": "eth1", "name": "eth1", "port_number": 1, "type": "ethernet"}, ] cloud = Cloud("cloud1", str(uuid.uuid4()), compute_project, MagicMock(), ports=ports1) assert cloud.ports_mapping == ports1 ports2 = [ - { - "interface": "eth0", - "name": "eth0", - "port_number": 0, - "type": "ethernet" - }, - { - "interface": "eth1", - "name": "eth1", - "port_number": 2, - "type": "ethernet" - } + {"interface": "eth0", "name": "eth0", "port_number": 0, "type": "ethernet"}, + {"interface": "eth1", "name": "eth1", "port_number": 2, "type": "ethernet"}, ] cloud = Cloud("cloud2", str(uuid.uuid4()), compute_project, MagicMock(), ports=ports2) assert cloud.ports_mapping == ports1 async def test_linux_ethernet_raw_add_nio(linux_platform, compute_project, nio): - ports = [ - { - "interface": "eth0", - "name": "eth0", - "port_number": 0, - "type": "ethernet" - } - ] + ports = [{"interface": "eth0", "name": "eth0", "port_number": 0, "type": "ethernet"}] cloud = Cloud("cloud1", str(uuid.uuid4()), compute_project, MagicMock(), ports=ports) cloud.status = "started" @@ -176,43 +187,42 @@ async def test_linux_ethernet_raw_add_nio(linux_platform, compute_project, nio): with patch("gns3server.compute.builtin.nodes.cloud.Cloud._interfaces", return_value=[{"name": "eth0"}]): await cloud.add_nio(nio, 0) - ubridge_mock.assert_has_calls([ - call("bridge create {}-0".format(cloud._id)), - call("bridge add_nio_udp {}-0 4242 127.0.0.1 4343".format(cloud._id)), - call('bridge reset_packet_filters {}-0'.format(cloud._id)), - call("bridge add_nio_linux_raw {}-0 \"eth0\"".format(cloud._id)), - call("bridge start {}-0".format(cloud._id)), - ]) + ubridge_mock.assert_has_calls( + [ + call("bridge create {}-0".format(cloud._id)), + call("bridge add_nio_udp {}-0 4242 127.0.0.1 4343".format(cloud._id)), + call("bridge reset_packet_filters {}-0".format(cloud._id)), + call('bridge add_nio_linux_raw {}-0 "eth0"'.format(cloud._id)), + call("bridge start {}-0".format(cloud._id)), + ] + ) async def test_linux_ethernet_raw_add_nio_bridge(linux_platform, compute_project, nio): """ Bridge can't be connected directly to a cloud we use a tap in the middle """ - ports = [ - { - "interface": "bridge0", - "name": "bridge0", - "port_number": 0, - "type": "ethernet" - } - ] + ports = [{"interface": "bridge0", "name": "bridge0", "port_number": 0, "type": "ethernet"}] cloud = Cloud("cloud1", str(uuid.uuid4()), compute_project, MagicMock(), ports=ports) cloud.status = "started" with patch("shutil.which", return_value="/bin/ubridge"): with patch("gns3server.compute.base_manager.BaseManager.has_privileged_access", return_value=True): with asyncio_patch("gns3server.compute.builtin.nodes.cloud.Cloud._ubridge_send") as ubridge_mock: - with patch("gns3server.compute.builtin.nodes.cloud.Cloud._interfaces", return_value=[{"name": "bridge0"}]): + with patch( + "gns3server.compute.builtin.nodes.cloud.Cloud._interfaces", return_value=[{"name": "bridge0"}] + ): with patch("gns3server.utils.interfaces.is_interface_bridge", return_value=True): await cloud.add_nio(nio, 0) tap = "gns3tap0-0" - ubridge_mock.assert_has_calls([ - call("bridge create {}-0".format(cloud._id)), - call("bridge add_nio_udp {}-0 4242 127.0.0.1 4343".format(cloud._id)), - call('bridge reset_packet_filters {}-0'.format(cloud._id)), - call("bridge add_nio_tap \"{}-0\" \"{}\"".format(cloud._id, tap)), - call("brctl addif \"bridge0\" \"{}\"".format(tap)), - call("bridge start {}-0".format(cloud._id)), - ]) + ubridge_mock.assert_has_calls( + [ + call("bridge create {}-0".format(cloud._id)), + call("bridge add_nio_udp {}-0 4242 127.0.0.1 4343".format(cloud._id)), + call("bridge reset_packet_filters {}-0".format(cloud._id)), + call('bridge add_nio_tap "{}-0" "{}"'.format(cloud._id, tap)), + call('brctl addif "bridge0" "{}"'.format(tap)), + call("bridge start {}-0".format(cloud._id)), + ] + ) diff --git a/tests/compute/builtin/nodes/test_nat.py b/tests/compute/builtin/nodes/test_nat.py index bd89aeafe..3c0f7e06d 100644 --- a/tests/compute/builtin/nodes/test_nat.py +++ b/tests/compute/builtin/nodes/test_nat.py @@ -30,14 +30,7 @@ def test_json_gns3vm(on_gns3vm, compute_project): "node_id": nat.id, "project_id": compute_project.id, "status": "started", - "ports_mapping": [ - { - "interface": "virbr0", - "name": "nat0", - "port_number": 0, - "type": "ethernet" - } - ], + "ports_mapping": [{"interface": "virbr0", "name": "nat0", "port_number": 0, "type": "ethernet"}], "interfaces": [ { "name": "virbr0", @@ -51,9 +44,13 @@ def test_json_gns3vm(on_gns3vm, compute_project): def test_json_darwin(darwin_platform, compute_project): - with patch("gns3server.utils.interfaces.interfaces", return_value=[ + with patch( + "gns3server.utils.interfaces.interfaces", + return_value=[ {"name": "eth0", "special": False, "type": "ethernet"}, - {"name": "vmnet8", "special": True, "type": "ethernet"}]): + {"name": "vmnet8", "special": True, "type": "ethernet"}, + ], + ): nat = Nat("nat1", str(uuid.uuid4()), compute_project, MagicMock()) assert nat.asdict() == { "name": "nat1", @@ -61,14 +58,7 @@ def test_json_darwin(darwin_platform, compute_project): "node_id": nat.id, "project_id": compute_project.id, "status": "started", - "ports_mapping": [ - { - "interface": "vmnet8", - "name": "nat0", - "port_number": 0, - "type": "ethernet" - } - ], + "ports_mapping": [{"interface": "vmnet8", "name": "nat0", "port_number": 0, "type": "ethernet"}], "interfaces": [ { "name": "vmnet8", @@ -81,8 +71,10 @@ def test_json_darwin(darwin_platform, compute_project): def test_json_windows_with_full_name_of_interface(windows_platform, project): - with patch("gns3server.utils.interfaces.interfaces", return_value=[ - {"name": "VMware Network Adapter VMnet8", "special": True, "type": "ethernet"}]): + with patch( + "gns3server.utils.interfaces.interfaces", + return_value=[{"name": "VMware Network Adapter VMnet8", "special": True, "type": "ethernet"}], + ): nat = Nat("nat1", str(uuid.uuid4()), project, MagicMock()) assert nat.asdict() == { "name": "nat1", @@ -91,12 +83,7 @@ def test_json_windows_with_full_name_of_interface(windows_platform, project): "project_id": project.id, "status": "started", "ports_mapping": [ - { - "interface": "VMware Network Adapter VMnet8", - "name": "nat0", - "port_number": 0, - "type": "ethernet" - } + {"interface": "VMware Network Adapter VMnet8", "name": "nat0", "port_number": 0, "type": "ethernet"} ], "interfaces": [ { diff --git a/tests/compute/docker/test_docker.py b/tests/compute/docker/test_docker.py index c00d6c11b..14d347ba9 100644 --- a/tests/compute/docker/test_docker.py +++ b/tests/compute/docker/test_docker.py @@ -40,7 +40,7 @@ async def test_query_success(vm): response = MagicMock() response.status = 200 - response.headers = {'CONTENT-TYPE': 'application/json'} + response.headers = {"CONTENT-TYPE": "application/json"} async def read(): return b'{"c": false}' @@ -48,12 +48,14 @@ async def test_query_success(vm): response.read.side_effect = read vm._session.request = AsyncioMagicMock(return_value=response) data = await vm.query("POST", "test", data={"a": True}, params={"b": 1}) - vm._session.request.assert_called_with('POST', - 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), - data='{"a": true}', - headers={'content-type': 'application/json'}, - params={'b': 1}, - timeout=300) + vm._session.request.assert_called_with( + "POST", + "http://docker/v{}/test".format(DOCKER_MINIMUM_API_VERSION), + data='{"a": true}', + headers={"content-type": "application/json"}, + params={"b": 1}, + timeout=300, + ) assert data == {"c": False} @@ -71,12 +73,14 @@ async def test_query_error(vm): vm._session.request = AsyncioMagicMock(return_value=response) with pytest.raises(DockerError): await vm.query("POST", "test", data={"a": True}, params={"b": 1}) - vm._session.request.assert_called_with('POST', - 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), - data='{"a": true}', - headers={'content-type': 'application/json'}, - params={'b': 1}, - timeout=300) + vm._session.request.assert_called_with( + "POST", + "http://docker/v{}/test".format(DOCKER_MINIMUM_API_VERSION), + data='{"a": true}', + headers={"content-type": "application/json"}, + params={"b": 1}, + timeout=300, + ) @pytest.mark.asyncio @@ -92,12 +96,14 @@ async def test_query_error_json(vm): vm._session.request = AsyncioMagicMock(return_value=response) with pytest.raises(DockerError): await vm.query("POST", "test", data={"a": True}, params={"b": 1}) - vm._session.request.assert_called_with('POST', - 'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION), - data='{"a": true}', - headers={'content-type': 'application/json'}, - params={'b': 1}, - timeout=300) + vm._session.request.assert_called_with( + "POST", + "http://docker/v{}/test".format(DOCKER_MINIMUM_API_VERSION), + data='{"a": true}', + headers={"content-type": "application/json"}, + params={"b": 1}, + timeout=300, + ) @pytest.mark.asyncio @@ -105,28 +111,20 @@ async def test_list_images(): response = [ { - "RepoTags": [ - "ubuntu:12.04", - "ubuntu:precise", - "ubuntu:latest" - ], + "RepoTags": ["ubuntu:12.04", "ubuntu:precise", "ubuntu:latest"], "Id": "8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c", "Created": 1365714795, "Size": 131506275, - "VirtualSize": 131506275 + "VirtualSize": 131506275, }, { - "RepoTags": [ - "ubuntu:12.10", - "ubuntu:quantal", - ":" - ], + "RepoTags": ["ubuntu:12.10", "ubuntu:quantal", ":"], "ParentId": "27cf784147099545", "Id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", "Created": 1364102658, "Size": 24653, - "VirtualSize": 180116135 - } + "VirtualSize": 180116135, + }, ] with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: @@ -196,7 +194,6 @@ async def test_force_pull_image(): async def test_pull_image_error(): class Content: - def __init__(self): self._chunks = [b'{"error": "image not found"}', b""] @@ -217,7 +214,6 @@ async def test_pull_image_error(): async def test_pull_image_rejects_incomplete_response(): class Content: - def __init__(self): self._read = False @@ -241,7 +237,6 @@ async def test_pull_image_rejects_incomplete_response(): async def test_pull_image_propagates_timeout(): class Content: - async def read(self, size): raise asyncio.TimeoutError @@ -259,7 +254,6 @@ async def test_pull_image_propagates_timeout(): async def test_load_image(): class Content: - def __init__(self): self._chunks = [b'{"stream": "Loaded image ID: sha256:e90e34656806"}', b""] @@ -285,7 +279,6 @@ async def test_load_image(): async def test_load_image_error(): class Content: - def __init__(self): self._chunks = [b'{"error": "invalid tar file"}', b""] @@ -307,13 +300,12 @@ async def test_load_image_error(): @pytest.mark.asyncio async def test_docker_check_connection_docker_minimum_version(vm): - response = { - 'ApiVersion': '1.01', - 'Version': '1.12' - } + response = {"ApiVersion": "1.01", "Version": "1.12"} - with patch("gns3server.compute.docker.Docker.connector"), \ - asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): + with ( + patch("gns3server.compute.docker.Docker.connector"), + asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response), + ): vm._connected = False with pytest.raises(DockerError): await vm._check_connection() @@ -323,13 +315,14 @@ async def test_docker_check_connection_docker_minimum_version(vm): async def test_docker_check_connection_docker_preferred_version_against_newer(vm): response = { - 'ApiVersion': '1.52', - 'Version': '29.0.1', - + "ApiVersion": "1.52", + "Version": "29.0.1", } - with patch("gns3server.compute.docker.Docker.connector"), \ - asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): + with ( + patch("gns3server.compute.docker.Docker.connector"), + asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response), + ): vm._connected = False await vm._check_connection() assert vm._api_version == DOCKER_PREFERRED_API_VERSION @@ -338,14 +331,12 @@ async def test_docker_check_connection_docker_preferred_version_against_newer(vm @pytest.mark.asyncio async def test_docker_check_connection_docker_preferred_version_against_older(vm): - response = { - 'ApiVersion': '1.43', - 'Version': '24.0.2', - 'MinAPIVersion': '1.40' - } + response = {"ApiVersion": "1.43", "Version": "24.0.2", "MinAPIVersion": "1.40"} - with patch("gns3server.compute.docker.Docker.connector"), \ - asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): + with ( + patch("gns3server.compute.docker.Docker.connector"), + asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response), + ): vm._connected = False await vm._check_connection() assert vm._api_version == DOCKER_MINIMUM_API_VERSION @@ -355,12 +346,14 @@ async def test_docker_check_connection_docker_preferred_version_against_older(vm async def test_docker_check_connection_docker_unsupported_version(vm): response = { - 'ApiVersion': '1.25', - 'Version': '1.13.1', + "ApiVersion": "1.25", + "Version": "1.13.1", } - with patch("gns3server.compute.docker.Docker.connector"), \ - asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): + with ( + patch("gns3server.compute.docker.Docker.connector"), + asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response), + ): vm._connected = False with pytest.raises(DockerError) as e: await vm._check_connection() @@ -370,12 +363,14 @@ async def test_docker_check_connection_docker_unsupported_version(vm): async def test_install_busybox(): mock_process = MagicMock() - mock_process.returncode = 1 # means that busybox is not dynamically linked + mock_process.returncode = 1 # means that busybox is not dynamically linked mock_process.communicate = AsyncioMagicMock(return_value=(b"", b"not a dynamic executable")) with patch("gns3server.compute.docker.os.path.isfile", return_value=False): with patch("gns3server.compute.docker.shutil.which", return_value="/usr/bin/busybox"): - with asyncio_patch("gns3server.compute.docker.asyncio.create_subprocess_exec", return_value=mock_process) as create_subprocess_mock: + with asyncio_patch( + "gns3server.compute.docker.asyncio.create_subprocess_exec", return_value=mock_process + ) as create_subprocess_mock: with patch("gns3server.compute.docker.shutil.copy2") as copy2_mock: dst_dir = Docker.resources_path() await Docker.install_busybox(dst_dir) @@ -396,12 +391,18 @@ async def test_install_busybox_dynamic_linked(): mock_process.communicate = AsyncioMagicMock(return_value=(b"Dynamically linked library", b"")) with patch("os.path.isfile", return_value=False): - with patch("gns3server.compute.docker.shutil.which", side_effect=lambda name: "/usr/bin/busybox" if name == "busybox" else None): + with patch( + "gns3server.compute.docker.shutil.which", + side_effect=lambda name: "/usr/bin/busybox" if name == "busybox" else None, + ): with asyncio_patch("gns3server.compute.docker.asyncio.create_subprocess_exec", return_value=mock_process): with pytest.raises(DockerError) as e: dst_dir = Docker.resources_path() await Docker.install_busybox(dst_dir) - assert str(e.value) == "No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH" + assert ( + str(e.value) + == "No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH" + ) @pytest.mark.asyncio @@ -412,7 +413,10 @@ async def test_install_busybox_no_executables(): with pytest.raises(DockerError) as e: dst_dir = Docker.resources_path() await Docker.install_busybox(dst_dir) - assert str(e.value) == "No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH" + assert ( + str(e.value) + == "No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH" + ) @pytest.mark.asyncio @@ -494,4 +498,4 @@ async def test_check_host_readiness_continues_past_unreadable_key(caplog): message = " ".join(r.message for r in caplog.records) assert "max_user_instances=128" in message # collected before the gap - assert "modprobe fuse" in message # FUSE check still ran + assert "modprobe fuse" in message # FUSE check still ran diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index 5ad4eb4f8..6e340b21d 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -51,7 +51,7 @@ async def vm(compute_project, manager): vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", aux_type="none") vm._cid = "e90e34656842" - vm.mac_address = '02:42:3d:b7:93:00' + vm.mac_address = "02:42:3d:b7:93:00" # Interface monitoring has its own focused tests below. Keep unrelated # lifecycle tests isolated from a real Docker exec stream. vm._start_interface_monitor = AsyncioMagicMock() @@ -62,30 +62,30 @@ async def vm(compute_project, manager): def test_json(vm, compute_project): assert vm.asdict() == { - 'container_id': 'e90e34656842', - 'image': 'ubuntu:latest', - 'name': 'test', - 'project_id': compute_project.id, - 'node_id': vm.id, - 'adapters': 1, - 'mac_address': '02:42:3d:b7:93:00', - 'console': vm.console, - 'console_type': 'telnet', - 'aux_type': 'none', - 'console_resolution': '1024x768', - 'console_http_port': 80, - 'console_http_path': '/', - 'extra_hosts': None, - 'extra_volumes': [], - 'extra_configs': [], - 'memory': 0, - 'cpus': 0, - 'aux': vm.aux, - 'start_command': vm.start_command, - 'environment': vm.environment, - 'node_directory': vm.working_dir, - 'status': 'stopped', - 'usage': '' + "container_id": "e90e34656842", + "image": "ubuntu:latest", + "name": "test", + "project_id": compute_project.id, + "node_id": vm.id, + "adapters": 1, + "mac_address": "02:42:3d:b7:93:00", + "console": vm.console, + "console_type": "telnet", + "aux_type": "none", + "console_resolution": "1024x768", + "console_http_port": 80, + "console_http_path": "/", + "extra_hosts": None, + "extra_volumes": [], + "extra_configs": [], + "memory": 0, + "cpus": 0, + "aux": vm.aux, + "start_command": vm.start_command, + "environment": vm.environment, + "node_directory": vm.working_dir, + "status": "stopped", + "usage": "", } @@ -100,164 +100,143 @@ def test_start_command(vm): @pytest.mark.asyncio async def test_create(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" - } + "Target": "/gns3volumes/etc/network", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @pytest.mark.asyncio async def test_create_with_tag(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:16.04") await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" - } + "Target": "/gns3volumes/etc/network", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:16.04", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:16.04", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @pytest.mark.asyncio async def test_create_vnc(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu", console_type="vnc", console=5900) + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu", console_type="vnc", console=5900 + ) vm._start_vnc = MagicMock() vm._display = 42 await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" + "Target": "/gns3volumes/etc/network", }, { "Type": "bind", "Source": f"/tmp/.X11-unix/X{vm._display}", "Target": f"/tmp/.X11-unix/X{vm._display}", - "ReadOnly": True - } + "ReadOnly": True, + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network", - "QT_GRAPHICSSYSTEM=native", - "DISPLAY=:42" + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": [ + "container=docker", + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network", + "QT_GRAPHICSSYSTEM=native", + "DISPLAY=:42", ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._start_vnc.called assert vm._cid == "e90e34656806" assert vm._console_type == "vnc" @@ -267,10 +246,7 @@ async def test_create_vnc(compute_project, manager): async def test_create_with_extra_hosts(compute_project, manager): extra_hosts = "test:199.199.199.1\ntest2:199.199.199.1" - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: @@ -294,7 +270,7 @@ async def test_create_applies_env_host_config(compute_project, manager): "GNS3_SHM_SIZE=1024\n" "GNS3_DEVICES=/dev/fuse\n" "GNS3_EVIL=should-be-dropped\n" # GNS3_ -> never forwarded as env - "FOO=bar" # normal var -> forwarded + "FOO=bar" # normal var -> forwarded ) response = {"Id": "e90e34656806", "Warnings": []} @@ -310,10 +286,9 @@ async def test_create_applies_env_host_config(compute_project, manager): ] env = data["Env"] assert "FOO=bar" in env - assert not any( - e.startswith(("GNS3_SHM_SIZE=", "GNS3_DEVICES=", "GNS3_EVIL=")) - for e in env - ), "GNS3_ user vars must not leak into the container environment" + assert not any(e.startswith(("GNS3_SHM_SIZE=", "GNS3_DEVICES=", "GNS3_EVIL=")) for e in env), ( + "GNS3_ user vars must not leak into the container environment" + ) @pytest.mark.asyncio @@ -331,8 +306,9 @@ async def test_create_masks_systemd_units(compute_project, manager): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu", environment=environment) await vm.create() - masked = {m["Target"] for m in mock.call_args[1]["data"]["HostConfig"]["Mounts"] - if m.get("Source") == "/dev/null"} + masked = { + m["Target"] for m in mock.call_args[1]["data"]["HostConfig"]["Mounts"] if m.get("Source") == "/dev/null" + } for unit in DockerVM._UDEV_UNITS: assert f"/etc/systemd/system/{unit}" in masked for path in DockerVM._UDEVADM_PATHS: @@ -382,10 +358,7 @@ async def test_create_with_extra_configs_invalid_target(compute_project, manager @pytest.mark.asyncio async def test_create_with_colon_in_project_name(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): @@ -399,10 +372,7 @@ async def test_create_with_colon_in_project_name(compute_project, manager): async def test_create_with_extra_hosts_wrong_format(compute_project, manager): extra_hosts = "test" - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): @@ -415,30 +385,24 @@ async def test_create_with_extra_hosts_wrong_format(compute_project, manager): async def test_create_with_empty_extra_hosts(compute_project, manager): extra_hosts = "test:\n" - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu", extra_hosts=extra_hosts) await vm.create() called_kwargs = mock.call_args[1] - assert len([ e for e in called_kwargs["data"]["Env"] if "GNS3_EXTRA_HOSTS" in e]) == 0 + assert len([e for e in called_kwargs["data"]["Env"] if "GNS3_EXTRA_HOSTS" in e]) == 0 @pytest.mark.asyncio async def test_create_with_project_variables(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} compute_project.variables = [ {"name": "VAR1"}, {"name": "VAR2", "value": "VAL1"}, - {"name": "VAR3", "value": "2x${VAR2}"} + {"name": "VAR3", "value": "2x${VAR2}"}, ] with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): @@ -455,52 +419,43 @@ async def test_create_with_project_variables(compute_project, manager): @pytest.mark.asyncio async def test_create_start_cmd(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") vm._start_command = "/bin/ls" await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" - } + "Target": "/gns3volumes/etc/network", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/ls"], - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network" - ] - }) + "Volumes": {}, + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/ls"], + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network"], + }, + ) assert vm._cid == "e90e34656806" @@ -511,21 +466,18 @@ async def test_create_environment(compute_project, manager): internal variables """ - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu") vm.environment = "YES=1\nNO=0\nGNS3_MAX_ETHERNET=eth2" await vm.create() - assert mock.call_args[1]['data']['Env'] == [ + assert mock.call_args[1]["data"]["Env"] == [ "container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network", "YES=1", - "NO=0" + "NO=0", ] @@ -536,21 +488,18 @@ async def test_create_environment_with_last_new_line_character(compute_project, internal variables """ - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu") vm.environment = "YES=1\nNO=0\nGNS3_MAX_ETHERNET=eth2\n" await vm.create() - assert mock.call_args[1]['data']['Env'] == [ + assert mock.call_args[1]["data"]["Env"] == [ "container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network", "YES=1", - "NO=0" + "NO=0", ] @@ -568,13 +517,11 @@ async def test_create_image_not_available(compute_project, manager): @pytest.mark.asyncio async def test_create_image_digest_match(compute_project, manager): - response = { - "Id": "sha256:" + "a" * 64, - "Warnings": [] - } + response = {"Id": "sha256:" + "a" * 64, "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", - image_digest="sha256:" + "a" * 64) + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", image_digest="sha256:" + "a" * 64 + ) await vm.create() # the last query is the container creation: the digest check let it through assert mock.call_args[0] == ("POST", "containers/create?name={}".format(vm.docker_name)) @@ -584,12 +531,10 @@ async def test_create_image_digest_match(compute_project, manager): @pytest.mark.asyncio async def test_create_image_digest_mismatch(compute_project, manager): - response = { - "Id": "sha256:" + "b" * 64, - "Warnings": [] - } - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", - image_digest="sha256:" + "a" * 64) + response = {"Id": "sha256:" + "b" * 64, "Warnings": []} + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", image_digest="sha256:" + "a" * 64 + ) with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as query_mock: with pytest.raises(ImageMissingError, match="ubuntu:latest"): await vm.create() @@ -603,63 +548,58 @@ async def test_create_with_user(compute_project, manager): response = { "Id": "e90e34656806", "Warnings": [], - "Config" : { - "User" : "test", + "Config": { + "User": "test", }, } with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest") await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "User": "root", - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "User": "root", + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" - } + "Target": "/gns3volumes/etc/network", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network", - "GNS3_USER=test" + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": [ + "container=docker", + "GNS3_MAX_ETHERNET=eth0", + "GNS3_VOLUMES=/etc/network", + "GNS3_USER=test", ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @pytest.mark.asyncio async def test_create_with_extra_volumes_invalid_format_1(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["vol1"]) @@ -670,13 +610,12 @@ async def test_create_with_extra_volumes_invalid_format_1(compute_project, manag @pytest.mark.asyncio async def test_create_with_extra_volumes_invalid_format_2(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol1", ""]) + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol1", ""] + ) with pytest.raises(DockerError): await vm.create() @@ -684,13 +623,12 @@ async def test_create_with_extra_volumes_invalid_format_2(compute_project, manag @pytest.mark.asyncio async def test_create_with_extra_volumes_invalid_format_3(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol1/.."]) + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol1/.."] + ) with pytest.raises(DockerError): await vm.create() @@ -701,58 +639,52 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana response = { "Id": "e90e34656806", "Warnings": [], - "Config" : { - "Volumes" : { - "/vol/1": None - }, + "Config": { + "Volumes": {"/vol/1": None}, }, } with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1"]) + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1"] + ) await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" + "Target": "/gns3volumes/etc/network", }, { "Type": "bind", "Source": os.path.join(vm.working_dir, "vol", "1"), - "Target": "/gns3volumes/vol/1" - } + "Target": "/gns3volumes/vol/1", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network:/vol/1" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network:/vol/1"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @@ -765,50 +697,46 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag } with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1", "/vol/1"]) + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1", "/vol/1"] + ) await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" + "Target": "/gns3volumes/etc/network", }, { "Type": "bind", "Source": os.path.join(vm.working_dir, "vol", "1"), - "Target": "/gns3volumes/vol/1" - } + "Target": "/gns3volumes/vol/1", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network:/vol/1" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network:/vol/1"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @@ -821,50 +749,46 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man } with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1/", "/vol"]) + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1/", "/vol"] + ) await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" + "Target": "/gns3volumes/etc/network", }, { "Type": "bind", "Source": os.path.join(vm.working_dir, "vol"), - "Target": "/gns3volumes/vol" - } + "Target": "/gns3volumes/vol", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network:/vol" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network:/vol"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @@ -877,50 +801,46 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project, } with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol//", "/vol"]) + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol//", "/vol"] + ) await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" + "Target": "/gns3volumes/etc/network", }, { "Type": "bind", "Source": os.path.join(vm.working_dir, "vol"), - "Target": "/gns3volumes/vol" - } + "Target": "/gns3volumes/vol", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network:/vol" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network:/vol"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @@ -935,43 +855,37 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc"]) await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc"), - "Target": "/gns3volumes/etc" - } + "Target": "/gns3volumes/etc", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @@ -984,45 +898,46 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p } with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc/test", "/etc"]) + vm = DockerVM( + "test", + str(uuid.uuid4()), + compute_project, + manager, + "ubuntu:latest", + extra_volumes=["/etc/test", "/etc"], + ) await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc"), - "Target": "/gns3volumes/etc" - } + "Target": "/gns3volumes/etc", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @@ -1032,64 +947,58 @@ async def test_create_with_extra_volumes(compute_project, manager): response = { "Id": "e90e34656806", "Warnings": [], - "Config" : { - "Volumes" : { - "/vol/1": None - }, + "Config": { + "Volumes": {"/vol/1": None}, }, } with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/2"]) + vm = DockerVM( + "test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/2"] + ) await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" + "Target": "/gns3volumes/etc/network", }, { "Type": "bind", "Source": os.path.join(vm.working_dir, "vol", "1"), - "Target": "/gns3volumes/vol/1" + "Target": "/gns3volumes/vol/1", }, { "Type": "bind", "Source": os.path.join(vm.working_dir, "vol", "2"), - "Target": "/gns3volumes/vol/2" - } + "Target": "/gns3volumes/vol/2", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 0, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network:/vol/1:/vol/2" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network:/vol/1:/vol/2"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @@ -1106,7 +1015,7 @@ async def test_get_container_state(vm): "Pid": 0, "Restarting": False, "Running": True, - "StartedAt": "2015-01-06T15:47:32.072697474Z" + "StartedAt": "2015-01-06T15:47:32.072697474Z", } } with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): @@ -1126,12 +1035,7 @@ async def test_get_container_state(vm): @pytest.mark.asyncio async def test_is_running(vm): - response = { - "State": { - "Running": False, - "Paused": False - } - } + response = {"State": {"Running": False, "Paused": False}} with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): assert await vm.is_running() is False @@ -1173,7 +1077,9 @@ async def test_start(vm, manager, free_console_port, tmpdir): vm._add_ubridge_connection = AsyncioMagicMock() vm._start_console = AsyncioMagicMock() - nio = manager.create_nio({"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"} + ) await vm.adapter_add_nio_binding(0, nio) with patch("gns3server.compute.docker.Docker.install_busybox"): @@ -1221,17 +1127,25 @@ async def test_start_namespace_failed(vm, manager, free_console_port): assert vm.status != "started" vm.adapters = 1 - nio = manager.create_nio({"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"} + ) await vm.adapter_add_nio_binding(0, nio) with patch("gns3server.compute.docker.Docker.install_busybox"): with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="stopped"): with asyncio_patch("gns3server.compute.docker.Docker.query") as mock_query: with asyncio_patch("gns3server.compute.docker.DockerVM._start_ubridge") as mock_start_ubridge: - with asyncio_patch("gns3server.compute.docker.DockerVM._get_namespace", return_value=42) as mock_namespace: - with asyncio_patch("gns3server.compute.docker.DockerVM._add_ubridge_connection", side_effect=UbridgeNamespaceError()) as mock_add_ubridge_connection: - with asyncio_patch("gns3server.compute.docker.DockerVM._get_log", return_value='Hello not available') as mock_log: - + with asyncio_patch( + "gns3server.compute.docker.DockerVM._get_namespace", return_value=42 + ) as mock_namespace: + with asyncio_patch( + "gns3server.compute.docker.DockerVM._add_ubridge_connection", + side_effect=UbridgeNamespaceError(), + ) as mock_add_ubridge_connection: + with asyncio_patch( + "gns3server.compute.docker.DockerVM._get_log", return_value="Hello not available" + ) as mock_log: with pytest.raises(DockerError): await vm.start() @@ -1255,8 +1169,12 @@ async def test_start_without_nio(vm): with asyncio_patch("gns3server.compute.docker.Docker.query") as mock_query: with asyncio_patch("gns3server.compute.docker.DockerVM._start_ubridge") as mock_start_ubridge: with asyncio_patch("gns3server.compute.docker.DockerVM._get_namespace", return_value=42): - with asyncio_patch("gns3server.compute.docker.DockerVM._add_ubridge_connection") as mock_add_ubridge_connection: - with asyncio_patch("gns3server.compute.docker.DockerVM._start_console") as mock_start_console: + with asyncio_patch( + "gns3server.compute.docker.DockerVM._add_ubridge_connection" + ) as mock_add_ubridge_connection: + with asyncio_patch( + "gns3server.compute.docker.DockerVM._start_console" + ) as mock_start_console: await vm.start() mock_query.assert_called_with("POST", "containers/e90e34656842/start") @@ -1302,32 +1220,40 @@ async def test_restart(vm): async def test_read_interface_statuses(vm): reader = MagicMock() - reader.readline = AsyncMock(side_effect=[ - b"eth0=0x1003\r\n", - b"eth0=0x1003\r\n", # duplicate must not emit again - b"eth0=0x1002\r\n", - b"unknown=0x1003\r\n", - b"eth0=invalid\r\n", - b"", - ]) + reader.readline = AsyncMock( + side_effect=[ + b"eth0=0x1003\r\n", + b"eth0=0x1003\r\n", # duplicate must not emit again + b"eth0=0x1002\r\n", + b"unknown=0x1003\r\n", + b"eth0=invalid\r\n", + b"", + ] + ) with patch.object(vm.project, "emit") as emit: await DockerVM._read_interface_statuses(vm, reader) assert emit.call_args_list == [ - call("node.interface_status", { - "project_id": vm.project.id, - "node_id": vm.id, - "adapter_number": 0, - "port_number": 0, - "status": "started", - }), - call("node.interface_status", { - "project_id": vm.project.id, - "node_id": vm.id, - "adapter_number": 0, - "port_number": 0, - "status": "stopped", - }), + call( + "node.interface_status", + { + "project_id": vm.project.id, + "node_id": vm.id, + "adapter_number": 0, + "port_number": 0, + "status": "started", + }, + ), + call( + "node.interface_status", + { + "project_id": vm.project.id, + "node_id": vm.id, + "adapter_number": 0, + "port_number": 0, + "status": "stopped", + }, + ), ] @@ -1441,10 +1367,7 @@ async def test_stop_paused_container(vm): @pytest.mark.asyncio async def test_update(vm): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} original_console = vm.console original_aux = vm.aux @@ -1455,43 +1378,37 @@ async def test_update(vm): await vm.update() mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1}) - mock_query.assert_any_call("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { - "CapAdd": ["ALL"], - "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, - { - "Type": "bind", - "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" - } - ], - "Privileged": True, - "Memory": 0, - "NanoCpus": 0, - "UsernsMode": "host" + mock_query.assert_any_call( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { + "CapAdd": ["ALL"], + "Mounts": [ + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, + { + "Type": "bind", + "Source": os.path.join(vm.working_dir, "etc", "network"), + "Target": "/gns3volumes/etc/network", + }, + ], + "Privileged": True, + "Memory": 0, + "NanoCpus": 0, + "UsernsMode": "host", + }, + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + ) assert vm.console == original_console assert vm.aux == original_aux @@ -1499,10 +1416,7 @@ async def test_update(vm): @pytest.mark.asyncio async def test_update_vnc(vm): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} vm._console_type = "vnc" vm._console = 5900 @@ -1523,57 +1437,50 @@ async def test_update_vnc(vm): @pytest.mark.asyncio async def test_update_running(vm): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} original_console = vm.console vm.start = MagicMock() - with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]) as mock_list_images: + with asyncio_patch( + "gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}] + ) as mock_list_images: with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="running"): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock_query: await vm.update() mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1}) - mock_query.assert_any_call("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { - "CapAdd": ["ALL"], - "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, - { - "Type": "bind", - "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" - } - ], - "Privileged": True, - "Memory": 0, - "NanoCpus": 0, - "UsernsMode": "host" + mock_query.assert_any_call( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { + "CapAdd": ["ALL"], + "Mounts": [ + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, + { + "Type": "bind", + "Source": os.path.join(vm.working_dir, "etc", "network"), + "Target": "/gns3volumes/etc/network", + }, + ], + "Privileged": True, + "Memory": 0, + "NanoCpus": 0, + "UsernsMode": "host", + }, + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + ) assert vm.console == original_console assert vm.start.called @@ -1591,10 +1498,7 @@ async def test_delete(vm): @pytest.mark.asyncio async def test_close(vm, port_manager): - nio = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + nio = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio) await vm.adapter_add_nio_binding(0, nio) @@ -1625,11 +1529,7 @@ async def test_close_vnc(vm): @pytest.mark.asyncio async def test_get_namespace(vm): - response = { - "State": { - "Pid": 42 - } - } + response = {"State": {"Pid": 42}} with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock_query: assert await vm._get_namespace() == 42 mock_query.assert_called_with("GET", "containers/e90e34656842/json") @@ -1638,10 +1538,7 @@ async def test_get_namespace(vm): @pytest.mark.asyncio async def test_add_ubridge_connection(vm): - nio = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + nio = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio) nio.start_packet_capture("/tmp/capture.pcap") vm._ubridge_hypervisor = MagicMock() @@ -1649,15 +1546,15 @@ async def test_add_ubridge_connection(vm): await vm._add_ubridge_connection(nio, 0) calls = [ - call.send('bridge create bridge0'), + call.send("bridge create bridge0"), call.send("bridge add_nio_tap bridge0 tap-gns3-e0 off"), - call.send('docker move_to_ns tap-gns3-e0 42 eth0'), - call.send('bridge add_nio_udp bridge0 4242 127.0.0.1 4343'), + call.send("docker move_to_ns tap-gns3-e0 42 eth0"), + call.send("bridge add_nio_udp bridge0 4242 127.0.0.1 4343"), call.send('bridge start_capture bridge0 "/tmp/capture.pcap"'), - call.send('bridge start bridge0'), - call.send('bridge set_nio_tap_carrier bridge0 on') + call.send("bridge start bridge0"), + call.send("bridge set_nio_tap_carrier bridge0 on"), ] - assert 'bridge0' in vm._bridges + assert "bridge0" in vm._bridges # We need to check any_order otherwise mock is confused by asyncio vm._ubridge_hypervisor.assert_has_calls(calls, any_order=True) @@ -1670,11 +1567,7 @@ async def test_add_ubridge_connections_with_base_mac_address(vm): vm.adapters = 2 vm.mac_address = "02:42:42:42:42:00" - nio_params = { - "type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + nio_params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio_params) await vm._add_ubridge_connection(nio, 0) @@ -1683,10 +1576,10 @@ async def test_add_ubridge_connections_with_base_mac_address(vm): await vm._add_ubridge_connection(nio, 1) calls = [ - call.send('bridge create bridge0'), - call.send('bridge create bridge1'), - call.send('docker set_mac_addr tap-gns3-e0 02:42:42:42:42:00'), - call.send('docker set_mac_addr tap-gns3-e0 02:42:42:42:42:01') + call.send("bridge create bridge0"), + call.send("bridge create bridge1"), + call.send("docker set_mac_addr tap-gns3-e0 02:42:42:42:42:00"), + call.send("docker set_mac_addr tap-gns3-e0 02:42:42:42:42:01"), ] # We need to check any_order otherwise mock is confused by asyncio @@ -1703,12 +1596,11 @@ async def test_add_ubridge_connection_none_nio(vm): await vm._add_ubridge_connection(nio, 0) calls = [ - call.send('bridge create bridge0'), + call.send("bridge create bridge0"), call.send("bridge add_nio_tap bridge0 tap-gns3-e0 off"), - call.send('docker move_to_ns tap-gns3-e0 42 eth0'), - + call.send("docker move_to_ns tap-gns3-e0 42 eth0"), ] - assert 'bridge0' in vm._bridges + assert "bridge0" in vm._bridges # We need to check any_order ortherwise mock is confused by asyncio vm._ubridge_hypervisor.assert_has_calls(calls, any_order=True) @@ -1721,19 +1613,18 @@ async def test_set_adapter_carrier(vm): await vm._set_adapter_carrier(2, True) await vm._set_adapter_carrier(2, False) - vm._ubridge_send.assert_has_calls([ - call("bridge set_nio_tap_carrier bridge2 on"), - call("bridge set_nio_tap_carrier bridge2 off"), - ]) + vm._ubridge_send.assert_has_calls( + [ + call("bridge set_nio_tap_carrier bridge2 on"), + call("bridge set_nio_tap_carrier bridge2 off"), + ] + ) @pytest.mark.asyncio async def test_add_ubridge_connection_invalid_adapter_number(vm): - nio = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + nio = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio) with pytest.raises(DockerError): await vm._add_ubridge_connection(nio, 12) @@ -1742,13 +1633,9 @@ async def test_add_ubridge_connection_invalid_adapter_number(vm): @pytest.mark.asyncio async def test_add_ubridge_connection_no_free_interface(vm): - nio = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + nio = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio) with pytest.raises(DockerError): - # We create fake ethernet interfaces for docker interfaces = ["tap-gns3-e{}".format(index) for index in range(4096)] @@ -1759,10 +1646,7 @@ async def test_add_ubridge_connection_no_free_interface(vm): @pytest.mark.asyncio async def test_adapter_add_nio_binding_1(vm): - nio = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + nio = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio) await vm.adapter_add_nio_binding(0, nio) assert vm._ethernet_adapters[0].get_nio(0) == nio @@ -1771,9 +1655,7 @@ async def test_adapter_add_nio_binding_1(vm): @pytest.mark.asyncio async def test_adapter_add_nio_binding_sets_carrier(vm): - nio = vm.manager.create_nio({ - "type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1" - }) + nio = vm.manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) vm.status = "started" vm._ubridge_hypervisor = MagicMock() vm._connect_nio = AsyncioMagicMock() @@ -1788,10 +1670,7 @@ async def test_adapter_add_nio_binding_sets_carrier(vm): async def test_adapter_udpate_nio_binding_bridge_not_started(vm): vm._ubridge_apply_filters = AsyncioMagicMock() - nio = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + nio = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio) with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="running"): await vm.adapter_add_nio_binding(0, nio) @@ -1802,9 +1681,7 @@ async def test_adapter_udpate_nio_binding_bridge_not_started(vm): @pytest.mark.asyncio async def test_adapter_update_nio_binding_sets_suspended_carrier(vm): - nio = vm.manager.create_nio({ - "type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1" - }) + nio = vm.manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) nio.suspend = True vm.status = "started" vm._ubridge_hypervisor = MagicMock() @@ -1820,10 +1697,7 @@ async def test_adapter_update_nio_binding_sets_suspended_carrier(vm): @pytest.mark.asyncio async def test_adapter_add_nio_binding_invalid_adapter(vm): - nio = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + nio = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio) with pytest.raises(DockerError): await vm.adapter_add_nio_binding(12, nio) @@ -1835,10 +1709,7 @@ async def test_adapter_remove_nio_binding(vm): vm.ubridge = MagicMock() vm.ubridge.is_running.return_value = True - nio = {"type": "nio_udp", - "lport": 4242, - "rport": 4343, - "rhost": "127.0.0.1"} + nio = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"} nio = vm.manager.create_nio(nio) await vm.adapter_add_nio_binding(0, nio) vm.status = "started" @@ -1846,9 +1717,9 @@ async def test_adapter_remove_nio_binding(vm): with asyncio_patch("gns3server.compute.docker.DockerVM._ubridge_send") as delete_ubridge_mock: await vm.adapter_remove_nio_binding(0) assert vm._ethernet_adapters[0].get_nio(0) is None - delete_ubridge_mock.assert_any_call('bridge stop bridge0') - delete_ubridge_mock.assert_any_call('bridge remove_nio_udp bridge0 4242 127.0.0.1 4343') - delete_ubridge_mock.assert_any_call('bridge set_nio_tap_carrier bridge0 off') + delete_ubridge_mock.assert_any_call("bridge stop bridge0") + delete_ubridge_mock.assert_any_call("bridge remove_nio_udp bridge0 4242 127.0.0.1 4343") + delete_ubridge_mock.assert_any_call("bridge set_nio_tap_carrier bridge0 off") @pytest.mark.asyncio @@ -1862,7 +1733,9 @@ async def test_adapter_remove_nio_binding_invalid_adapter(vm): async def test_start_capture(vm, tmpdir, manager, free_console_port): output_file = str(tmpdir / "test.pcap") - nio = manager.create_nio({"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"} + ) await vm.adapter_add_nio_binding(0, nio) await vm.start_capture(0, output_file) assert vm._ethernet_adapters[0].get_nio(0).capturing @@ -1872,7 +1745,9 @@ async def test_start_capture(vm, tmpdir, manager, free_console_port): async def test_stop_capture(vm, tmpdir, manager, free_console_port): output_file = str(tmpdir / "test.pcap") - nio = manager.create_nio({"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"} + ) await vm.adapter_add_nio_binding(0, nio) await vm.start_capture(0, output_file) assert vm._ethernet_adapters[0].get_nio(0).capturing @@ -1884,7 +1759,7 @@ async def test_stop_capture(vm, tmpdir, manager, free_console_port): async def test_get_log(vm): async def read(): - return b'Hello\nWorld' + return b"Hello\nWorld" mock_query = MagicMock() mock_query.read = read @@ -1907,32 +1782,17 @@ async def test_get_image_information(compute_project, manager): @pytest.mark.asyncio async def test_mount_binds(vm): - image_infos = { - "Config": { - "Volumes": { - "/test/experimental": {} - } - } - } + image_infos = {"Config": {"Volumes": {"/test/experimental": {}}}} dst = os.path.join(vm.working_dir, "test/experimental") assert vm._mount_binds(image_infos) == [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" + "Target": "/gns3volumes/etc/network", }, - { - "Type": "bind", - "Source": dst, - "Target": "/gns3volumes/test/experimental" - } + {"Type": "bind", "Source": dst, "Target": "/gns3volumes/test/experimental"}, ] assert vm._volumes == ["/etc/network", "/test/experimental"] @@ -1948,7 +1808,25 @@ async def test_start_vnc(vm): with asyncio_patch("asyncio.create_subprocess_exec") as mock_exec: await vm._start_vnc() assert vm._display is not None - assert mock_exec.call_args[0] == ("/bin/Xtigervnc", "-extension", "MIT-SHM", "-geometry", vm.console_resolution, "-depth", "16", "-interface", "127.0.0.1", "-rfbport", str(vm.console), "-AlwaysShared", "-SecurityTypes", "None", "-desktop", "test", ":{}".format(vm._display)) + assert mock_exec.call_args[0] == ( + "/bin/Xtigervnc", + "-extension", + "MIT-SHM", + "-geometry", + vm.console_resolution, + "-depth", + "16", + "-interface", + "127.0.0.1", + "-rfbport", + str(vm.console), + "-AlwaysShared", + "-SecurityTypes", + "None", + "-desktop", + "test", + ":{}".format(vm._display), + ) mock_wait.assert_called_with("/tmp/.X11-unix/X{}".format(vm._display)) @@ -1973,7 +1851,7 @@ async def test_start_aux(vm): "/dev/null", stderr=asyncio.subprocess.STDOUT, stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE + stdout=asyncio.subprocess.PIPE, ) @@ -2001,7 +1879,18 @@ async def test_fix_permission(vm): process.returncode = 0 with asyncio_patch("asyncio.subprocess.create_subprocess_exec", return_value=process) as mock_exec: await vm._fix_permissions() - mock_exec.assert_called_with('docker', 'exec', 'e90e34656842', '/gns3/bin/busybox', 'sh', '-c', '(/gns3/bin/busybox find "/etc" -depth -print0 | /gns3/bin/busybox xargs -0 /gns3/bin/busybox stat -c \'%a:%u:%g:%n\' > "/etc/.gns3_perms") && /gns3/bin/busybox chmod -R u+rX "/etc" && /gns3/bin/busybox chown {}:{} -R "/etc"'.format(os.getuid(), os.getgid()), stderr=asyncio.subprocess.PIPE) + mock_exec.assert_called_with( + "docker", + "exec", + "e90e34656842", + "/gns3/bin/busybox", + "sh", + "-c", + '(/gns3/bin/busybox find "/etc" -depth -print0 | /gns3/bin/busybox xargs -0 /gns3/bin/busybox stat -c \'%a:%u:%g:%n\' > "/etc/.gns3_perms") && /gns3/bin/busybox chmod -R u+rX "/etc" && /gns3/bin/busybox chown {}:{} -R "/etc"'.format( + os.getuid(), os.getgid() + ), + stderr=asyncio.subprocess.PIPE, + ) assert process.wait.called @@ -2015,7 +1904,18 @@ async def test_fix_permission_not_running(vm): with asyncio_patch("gns3server.compute.docker.Docker.query") as mock_start: with asyncio_patch("asyncio.subprocess.create_subprocess_exec", return_value=process) as mock_exec: await vm._fix_permissions() - mock_exec.assert_called_with('docker', 'exec', 'e90e34656842', '/gns3/bin/busybox', 'sh', '-c', '(/gns3/bin/busybox find "/etc" -depth -print0 | /gns3/bin/busybox xargs -0 /gns3/bin/busybox stat -c \'%a:%u:%g:%n\' > "/etc/.gns3_perms") && /gns3/bin/busybox chmod -R u+rX "/etc" && /gns3/bin/busybox chown {}:{} -R "/etc"'.format(os.getuid(), os.getgid()), stderr=asyncio.subprocess.PIPE) + mock_exec.assert_called_with( + "docker", + "exec", + "e90e34656842", + "/gns3/bin/busybox", + "sh", + "-c", + '(/gns3/bin/busybox find "/etc" -depth -print0 | /gns3/bin/busybox xargs -0 /gns3/bin/busybox stat -c \'%a:%u:%g:%n\' > "/etc/.gns3_perms") && /gns3/bin/busybox chmod -R u+rX "/etc" && /gns3/bin/busybox chown {}:{} -R "/etc"'.format( + os.getuid(), os.getgid() + ), + stderr=asyncio.subprocess.PIPE, + ) assert mock_start.called assert process.wait.called @@ -2024,6 +1924,7 @@ async def test_fix_permission_not_running(vm): # Ownership reclaim for files the container left owned by root # --------------------------------------------------------------------------- + def _reclaim_proc(returncode=0, stderr=b""): process = MagicMock() @@ -2094,8 +1995,10 @@ async def test_reclaim_fails_when_helper_errors(vm, tmp_path): with patch.object(vm, "_directory_has_foreign_files", return_value=True): with patch.object(vm.manager, "resources_path", return_value="/gns3-share"): - with patch("asyncio.subprocess.create_subprocess_exec", - return_value=_reclaim_proc(returncode=1, stderr=b"chown: failed")): + with patch( + "asyncio.subprocess.create_subprocess_exec", + return_value=_reclaim_proc(returncode=1, stderr=b"chown: failed"), + ): assert await vm._reclaim_directory_ownership(str(directory)) is False @@ -2123,11 +2026,14 @@ async def test_delete_retries_after_reclaim(vm): real_rmtree(directory, onerror=onerror) with patch.object(vm, "close", new_callable=AsyncioMagicMock): - with patch.object(vm, "_reclaim_directory_ownership", new_callable=AsyncioMagicMock, return_value=True) as mock_reclaim: + with patch.object( + vm, "_reclaim_directory_ownership", new_callable=AsyncioMagicMock, return_value=True + ) as mock_reclaim: # First rmtree hits the root-owned leftovers, the retry (after # the reclaim) succeeds and really deletes the directory. - with patch("gns3server.compute.base_node.shutil.rmtree", - side_effect=rmtree_first_fails_then_deletes) as mock_rmtree: + with patch( + "gns3server.compute.base_node.shutil.rmtree", side_effect=rmtree_first_fails_then_deletes + ) as mock_rmtree: await vm.delete() assert mock_rmtree.call_count == 2 mock_reclaim.assert_called_once_with(vm.working_dir) @@ -2138,8 +2044,7 @@ async def test_delete_reports_root_files_when_reclaim_fails(vm): with patch.object(vm, "close", new_callable=AsyncioMagicMock): with patch.object(vm, "_reclaim_directory_ownership", new_callable=AsyncioMagicMock, return_value=False): - with patch("gns3server.compute.base_node.shutil.rmtree", - side_effect=OSError("permission denied")): + with patch("gns3server.compute.base_node.shutil.rmtree", side_effect=OSError("permission denied")): with pytest.raises(ComputeError, match="owned by another user"): await vm.delete() @@ -2164,7 +2069,7 @@ async def test_read_console_output_with_binary_mode(vm): input_stream = InputStreamMock() output_stream = MagicMock() - with asyncio_patch('gns3server.compute.docker.docker_vm.DockerVM.stop'): + with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.stop"): await vm._read_console_output(input_stream, output_stream) output_stream.feed_data.assert_called_once_with(b"test") @@ -2172,104 +2077,87 @@ async def test_read_console_output_with_binary_mode(vm): @pytest.mark.asyncio async def test_cpus(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", cpus=0.5) await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" - } + "Target": "/gns3volumes/etc/network", + }, ], "Privileged": True, "Memory": 0, "NanoCpus": 500000000, - "UsernsMode": "host" + "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" @pytest.mark.asyncio async def test_memory(compute_project, manager): - response = { - "Id": "e90e34656806", - "Warnings": [] - } + response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", memory=32) await vm.create() - mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={ - "Tty": True, - "OpenStdin": True, - "StdinOnce": False, - "HostConfig": - { + mock.assert_called_with( + "POST", + "containers/create?name={}".format(vm.docker_name), + data={ + "Tty": True, + "OpenStdin": True, + "StdinOnce": False, + "HostConfig": { "CapAdd": ["ALL"], "Mounts": [ - { - "Type": "bind", - "Source": Docker.resources_path(), - "Target": "/gns3", - "ReadOnly": True - }, + {"Type": "bind", "Source": Docker.resources_path(), "Target": "/gns3", "ReadOnly": True}, { "Type": "bind", "Source": os.path.join(vm.working_dir, "etc", "network"), - "Target": "/gns3volumes/etc/network" - } + "Target": "/gns3volumes/etc/network", + }, ], "Privileged": True, "Memory": 33554432, # 32MB in bytes "NanoCpus": 0, "UsernsMode": "host", }, - "Volumes": {}, - "NetworkDisabled": True, - "Hostname": "test", - "Image": "ubuntu:latest", - "Env": [ - "container=docker", - "GNS3_MAX_ETHERNET=eth0", - "GNS3_VOLUMES=/etc/network" - ], - "Entrypoint": ["/gns3/init.sh"], - "Cmd": ["/bin/sh"] - }) + "Volumes": {}, + "NetworkDisabled": True, + "Hostname": "test", + "Image": "ubuntu:latest", + "Env": ["container=docker", "GNS3_MAX_ETHERNET=eth0", "GNS3_VOLUMES=/etc/network"], + "Entrypoint": ["/gns3/init.sh"], + "Cmd": ["/bin/sh"], + }, + ) assert vm._cid == "e90e34656806" + @pytest.mark.asyncio async def test_stop_exited_container_no_stop_query(vm): @@ -2280,10 +2168,7 @@ async def test_stop_exited_container_no_stop_query(vm): with asyncio_patch("gns3server.compute.docker.Docker.query") as mock_query: vm._permissions_fixed = False await vm.stop() - assert not any( - call.args[:2] == ("POST", "containers/e90e34656842/kill") - for call in mock_query.mock_calls - ) + assert not any(call.args[:2] == ("POST", "containers/e90e34656842/kill") for call in mock_query.mock_calls) assert vm.status == "stopped" @@ -2343,8 +2228,14 @@ async def test_create_with_extra_configs_directory_target_rejected(compute_proje for bad in ("/", "/etc/", "///"): with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu", - extra_configs=[{"target": bad, "content": "x"}]) + vm = DockerVM( + "test", + str(uuid.uuid4()), + compute_project, + manager, + "ubuntu", + extra_configs=[{"target": bad, "content": "x"}], + ) with pytest.raises(DockerError): await vm.create() @@ -2374,13 +2265,21 @@ async def test_create_warns_when_extra_config_under_volume(compute_project, mana """ import logging + extra_configs = [{"target": "/xr-storage/config/foo.cfg", "content": "x"}] response = {"Id": "e90e34656806", "Warnings": []} with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): - vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu", - extra_configs=extra_configs, extra_volumes=["/xr-storage"]) + vm = DockerVM( + "test", + str(uuid.uuid4()), + compute_project, + manager, + "ubuntu", + extra_configs=extra_configs, + extra_volumes=["/xr-storage"], + ) with caplog.at_level(logging.WARNING, logger="gns3server.compute.docker.docker_vm"): await vm.create() diff --git a/tests/compute/docker/test_iol_docker_vm.py b/tests/compute/docker/test_iol_docker_vm.py index 4826b00a7..66861c8c5 100644 --- a/tests/compute/docker/test_iol_docker_vm.py +++ b/tests/compute/docker/test_iol_docker_vm.py @@ -72,13 +72,20 @@ async def manager(port_manager): return m -def _make_vm(compute_project, manager, environment="GNS3_IOL_RUNNER=1", - extra_volumes=None, adapters=4, console_type="telnet"): +def _make_vm( + compute_project, manager, environment="GNS3_IOL_RUNNER=1", extra_volumes=None, adapters=4, console_type="telnet" +): """Build an IOLDockerVM with a fake cid (no create() called).""" vm = IOLDockerVM( - "iol-xe-1", str(uuid.uuid4()), compute_project, manager, "iol-xe/iol-xe:17-18-02", - console_type=console_type, environment=environment, - extra_volumes=extra_volumes or [], adapters=adapters, + "iol-xe-1", + str(uuid.uuid4()), + compute_project, + manager, + "iol-xe/iol-xe:17-18-02", + console_type=console_type, + environment=environment, + extra_volumes=extra_volumes or [], + adapters=adapters, ) vm._cid = "e90e34656842" # mirrors the controller flow, which always delivers an allocated id @@ -127,40 +134,37 @@ def _wiring_dir(vm): # Factory selection # --------------------------------------------------------------------------- + def test_factory_selects_iol_for_env_marker(manager): - assert manager._select_node_class(console_type="telnet", - environment="GNS3_IOL_RUNNER=1") is IOLDockerVM + assert manager._select_node_class(console_type="telnet", environment="GNS3_IOL_RUNNER=1") is IOLDockerVM def test_factory_tolerates_whitespace_and_comma(manager): - assert manager._select_node_class(console_type="telnet", - environment=" GNS3_IOL_RUNNER=1,\nFOO=bar") is IOLDockerVM + assert manager._select_node_class(console_type="telnet", environment=" GNS3_IOL_RUNNER=1,\nFOO=bar") is IOLDockerVM def test_factory_docker_exec_wins_over_iol_marker(manager): - assert manager._select_node_class(console_type="docker_exec", - environment="GNS3_IOL_RUNNER=1") is VendorDockerVM + assert manager._select_node_class(console_type="docker_exec", environment="GNS3_IOL_RUNNER=1") is VendorDockerVM def test_factory_plain_environment_is_base(manager): - assert manager._select_node_class(console_type="telnet", - environment="FOO=bar\nGNS3_BAZ=nope") is DockerVM + assert manager._select_node_class(console_type="telnet", environment="FOO=bar\nGNS3_BAZ=nope") is DockerVM def test_factory_generic_unix_knob_selects_vendor(manager): - assert manager._select_node_class(console_type="telnet", - environment="GNS3_UNIX_SOCKET_NIO=1") is VendorDockerVM + assert manager._select_node_class(console_type="telnet", environment="GNS3_UNIX_SOCKET_NIO=1") is VendorDockerVM # --------------------------------------------------------------------------- # Knob parsing # --------------------------------------------------------------------------- + def test_marker_forces_skip_init_and_unix_nio(compute_project, manager): vm = _make_vm(compute_project, manager, environment="GNS3_IOL_RUNNER=1") @@ -172,12 +176,10 @@ def test_marker_forces_skip_init_and_unix_nio(compute_project, manager): def test_iol_memory_knob(compute_project, manager): - vm = _make_vm(compute_project, manager, - environment="GNS3_IOL_RUNNER=1\nGNS3_IOL_MEMORY=4096") + vm = _make_vm(compute_project, manager, environment="GNS3_IOL_RUNNER=1\nGNS3_IOL_MEMORY=4096") assert vm._iol_memory == 4096 - vm = _make_vm(compute_project, manager, - environment="GNS3_IOL_RUNNER=1\nGNS3_IOL_MEMORY=notanumber") + vm = _make_vm(compute_project, manager, environment="GNS3_IOL_RUNNER=1\nGNS3_IOL_MEMORY=notanumber") assert vm._iol_memory == 2048 @@ -185,15 +187,13 @@ def test_iol_memory_knob(compute_project, manager): # create() # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_create_keeps_image_entrypoint(compute_project, manager): - with asyncio_patch("gns3server.compute.docker.Docker.list_images", - return_value=[{"image": "iol-xe"}]): - with asyncio_patch("gns3server.compute.docker.Docker.query", - return_value=_create_response()) as mock: - with patch("asyncio.subprocess.create_subprocess_exec", - return_value=_seed_proc()): + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "iol-xe"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=_create_response()) as mock: + with patch("asyncio.subprocess.create_subprocess_exec", return_value=_seed_proc()): vm = _make_vm(compute_project, manager) await vm.create() sent = mock.call_args.kwargs["data"] @@ -205,12 +205,9 @@ async def test_create_keeps_image_entrypoint(compute_project, manager): @pytest.mark.asyncio async def test_create_auto_adds_config_and_tmp_run_volumes(compute_project, manager): - with asyncio_patch("gns3server.compute.docker.Docker.list_images", - return_value=[{"image": "iol-xe"}]): - with asyncio_patch("gns3server.compute.docker.Docker.query", - return_value=_create_response()) as mock: - with patch("asyncio.subprocess.create_subprocess_exec", - return_value=_seed_proc()): + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "iol-xe"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=_create_response()) as mock: + with patch("asyncio.subprocess.create_subprocess_exec", return_value=_seed_proc()): vm = _make_vm(compute_project, manager, extra_volumes=[]) await vm.create() sent = mock.call_args.kwargs["data"] @@ -235,12 +232,9 @@ async def test_create_start_command_becomes_runner_flags(compute_project, manage vm = _make_vm(compute_project, manager) vm.start_command = "-keep" - with asyncio_patch("gns3server.compute.docker.Docker.list_images", - return_value=[{"image": "iol-xe"}]): - with asyncio_patch("gns3server.compute.docker.Docker.query", - return_value=_create_response()) as mock: - with patch("asyncio.subprocess.create_subprocess_exec", - return_value=_seed_proc()): + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "iol-xe"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=_create_response()) as mock: + with patch("asyncio.subprocess.create_subprocess_exec", return_value=_seed_proc()): await vm.create() sent = mock.call_args.kwargs["data"] # start_command is the container CMD = extra iol-runner flags @@ -251,6 +245,7 @@ async def test_create_start_command_becomes_runner_flags(compute_project, manage # start() — runtime preparation # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_start_writes_iol_config(compute_project, manager): @@ -402,6 +397,7 @@ async def test_restart_is_graceful_stop_then_start(compute_project, manager): # Wiring — unix-socket NIO # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_add_ubridge_connection_unix_wiring(compute_project, manager): @@ -463,8 +459,7 @@ async def test_add_ubridge_connection_timeout_is_actionable(compute_project, man async def raise_timeout(path, timeout=60): raise asyncio.TimeoutError() - with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation", - side_effect=raise_timeout): + with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation", side_effect=raise_timeout): with pytest.raises(DockerError) as excinfo: await vm._add_ubridge_connection(None, 0) # the message names the adapter and the exact wiring path @@ -497,6 +492,7 @@ async def test_add_ubridge_connection_without_nio_still_wires(compute_project, m # IOU-style port model — 1 adapter = 4 ethernet ports # --------------------------------------------------------------------------- + def test_adapters_are_four_port_units(compute_project, manager): vm = _make_vm(compute_project, manager, adapters=2) @@ -523,7 +519,7 @@ async def test_wiring_addresses_ports_within_adapters(compute_project, manager): await vm._add_ubridge_connection(nio, 1, port_number=2) flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls) - assert 'bridge add_nio_unix bridge1_2 ' in flat + assert "bridge add_nio_unix bridge1_2 " in flat assert f'"{os.path.join(wiring_dir, "c06.sock")}"' in flat assert f'"{os.path.join(wiring_dir, "s06.sock")}"' in flat assert "add_nio_udp bridge1_2 4242 127.0.0.1 4343" in flat @@ -547,33 +543,59 @@ async def test_nio_binding_rejects_port_out_of_range(compute_project, manager): # Generic GNS3_UNIX_SOCKET_NIO knob on plain VendorDockerVM # --------------------------------------------------------------------------- + def test_env_unix_socket_nio_parsing(compute_project, manager): - vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", - console_type="docker_exec", - environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks") + vm = VendorDockerVM( + "vendor-1", + str(uuid.uuid4()), + compute_project, + manager, + "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks", + ) assert vm._unix_socket_nio is True assert vm._unix_socket_dir == "/var/run/socks" # invalid dirs are rejected, keeping the default - vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", - console_type="docker_exec", - environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=yes\nGNS3_UNIX_SOCKET_DIR=../../etc") + vm = VendorDockerVM( + "vendor-1", + str(uuid.uuid4()), + compute_project, + manager, + "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=yes\nGNS3_UNIX_SOCKET_DIR=../../etc", + ) assert vm._unix_socket_nio is True assert vm._unix_socket_dir == "/tmp" # off by default / explicit off - vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", - console_type="docker_exec", environment="GNS3_SKIP_INIT=1") + vm = VendorDockerVM( + "vendor-1", + str(uuid.uuid4()), + compute_project, + manager, + "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1", + ) assert vm._unix_socket_nio is False def test_unix_socket_dir_bound_from_runtime_dir(compute_project, manager): - vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", - console_type="docker_exec", - environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", - extra_volumes=[]) + vm = VendorDockerVM( + "vendor-1", + str(uuid.uuid4()), + compute_project, + manager, + "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", + extra_volumes=[], + ) # the socket directory is an ephemeral per-node directory from the # runtime dir, not a volume: writable by the (unprivileged) agent and # short enough for AF_UNIX @@ -584,10 +606,16 @@ def test_unix_socket_dir_bound_from_runtime_dir(compute_project, manager): assert socket_binds[0]["Source"] == _wiring_dir(vm) # a socket dir already covered by a persisted volume gets no extra bind - vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", - console_type="docker_exec", - environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", - extra_volumes=["/tmp"]) + vm = VendorDockerVM( + "vendor-1", + str(uuid.uuid4()), + compute_project, + manager, + "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1", + extra_volumes=["/tmp"], + ) binds = vm._mount_binds({"Config": {"Volumes": {}}}) assert not any(b.get("Source") == _wiring_dir(vm) for b in binds) assert any(b.get("Target") == "/tmp" for b in binds) @@ -596,9 +624,15 @@ def test_unix_socket_dir_bound_from_runtime_dir(compute_project, manager): @pytest.mark.asyncio async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manager): - vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", - console_type="docker_exec", - environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks") + vm = VendorDockerVM( + "vendor-1", + str(uuid.uuid4()), + compute_project, + manager, + "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks", + ) _mock_wiring(vm) wiring_dir = _wiring_dir(vm) os.makedirs(wiring_dir, exist_ok=True) @@ -659,10 +693,19 @@ async def test_tap_wired_vendor_keeps_carrier_and_monitor(compute_project, manag behavior: the guards only apply to unix-socket NIO. """ - vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest", - console_type="docker_exec", environment="GNS3_SKIP_INIT=1") - with patch.object(DockerVM, "_set_adapter_carrier", new=AsyncioMagicMock()) as carrier, \ - patch.object(DockerVM, "_start_interface_monitor", new=AsyncioMagicMock()) as monitor: + vm = VendorDockerVM( + "vendor-1", + str(uuid.uuid4()), + compute_project, + manager, + "vendor:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1", + ) + with ( + patch.object(DockerVM, "_set_adapter_carrier", new=AsyncioMagicMock()) as carrier, + patch.object(DockerVM, "_start_interface_monitor", new=AsyncioMagicMock()) as monitor, + ): await vm._set_adapter_carrier(0, True) await vm._start_interface_monitor() # the base-class attribute is replaced by a plain mock (no descriptor diff --git a/tests/compute/docker/test_vendor_docker_vm.py b/tests/compute/docker/test_vendor_docker_vm.py index e453a1b62..2839e93c8 100644 --- a/tests/compute/docker/test_vendor_docker_vm.py +++ b/tests/compute/docker/test_vendor_docker_vm.py @@ -50,6 +50,7 @@ from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Err # Helpers / fixtures # --------------------------------------------------------------------------- + def _create_response(vm, entrypoint=None, volumes=None): """Build the Docker /containers/create response (with image info merged).""" return { @@ -71,13 +72,18 @@ async def manager(port_manager): return m -def _make_vm(compute_project, manager, environment=None, console_type="docker_exec", - extra_volumes=None, adapters=4): +def _make_vm(compute_project, manager, environment=None, console_type="docker_exec", extra_volumes=None, adapters=4): """Build a VendorDockerVM with a fake cid (no create() called).""" vm = VendorDockerVM( - "srlinux-1", str(uuid.uuid4()), compute_project, manager, "srlinux:latest", - console_type=console_type, environment=environment, - extra_volumes=extra_volumes or [], adapters=adapters, + "srlinux-1", + str(uuid.uuid4()), + compute_project, + manager, + "srlinux:latest", + console_type=console_type, + environment=environment, + extra_volumes=extra_volumes or [], + adapters=adapters, ) vm._cid = "e90e34656842" # Interface monitoring belongs to DockerVM and is covered by its focused @@ -91,6 +97,7 @@ def _make_vm(compute_project, manager, environment=None, console_type="docker_ex # Factory selection # --------------------------------------------------------------------------- + def test_factory_selects_vendor_when_docker_exec(manager): assert manager._select_node_class(console_type="docker_exec") is VendorDockerVM @@ -123,12 +130,10 @@ async def test_create_node_sets_node_class(manager, compute_project, monkeypatch fake_super_create_node, ) - await manager.create_node("v", compute_project.id, str(uuid.uuid4()), - "srlinux:latest", console_type="docker_exec") + await manager.create_node("v", compute_project.id, str(uuid.uuid4()), "srlinux:latest", console_type="docker_exec") assert captured["cls"] is VendorDockerVM - await manager.create_node("v", compute_project.id, str(uuid.uuid4()), - "ubuntu:latest", console_type="telnet") + await manager.create_node("v", compute_project.id, str(uuid.uuid4()), "ubuntu:latest", console_type="telnet") assert captured["cls"] is DockerVM @@ -136,6 +141,7 @@ async def test_create_node_sets_node_class(manager, compute_project, monkeypatch # GNS3_* env parsing # --------------------------------------------------------------------------- + def test_env_skip_init_true(compute_project, manager): vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1") @@ -150,24 +156,23 @@ def test_env_skip_init_absent_defaults_true(compute_project, manager): def test_env_interface_names(compute_project, manager): - vm = _make_vm(compute_project, manager, - environment="GNS3_INTERFACE_NAMES=mgmt0,e1-1,e1-2,e1-3") + vm = _make_vm(compute_project, manager, environment="GNS3_INTERFACE_NAMES=mgmt0,e1-1,e1-2,e1-3") assert vm._interface_names == ["mgmt0", "e1-1", "e1-2", "e1-3"] def test_env_console_cmd(compute_project, manager): - vm = _make_vm(compute_project, manager, - environment="GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli") + vm = _make_vm(compute_project, manager, environment="GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli") assert vm._console_cmd == "/opt/srlinux/bin/sr_cli" def test_env_multiple_lines(compute_project, manager): - vm = _make_vm(compute_project, manager, - environment=("GNS3_SKIP_INIT=1\n" - "GNS3_INTERFACE_NAMES=mgmt0,e1-1\n" - "GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli\n")) + vm = _make_vm( + compute_project, + manager, + environment=("GNS3_SKIP_INIT=1\nGNS3_INTERFACE_NAMES=mgmt0,e1-1\nGNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli\n"), + ) assert vm._gns3_init is False assert vm._interface_names == ["mgmt0", "e1-1"] assert vm._console_cmd == "/opt/srlinux/bin/sr_cli" @@ -183,18 +188,22 @@ def test_env_console_cmd_default_none(compute_project, manager): # create() — init.sh skip, GNS3_MAX_ETHERNET, /etc/network drop # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_create_skip_init_omits_init_sh(compute_project, manager): response = _create_response(None, entrypoint=["/init"]) - with asyncio_patch("gns3server.compute.docker.Docker.list_images", - return_value=[{"image": "srlinux"}]): - with asyncio_patch("gns3server.compute.docker.Docker.query", - return_value=response) as mock: - vm = VendorDockerVM("srlinux-1", str(uuid.uuid4()), compute_project, - manager, "srlinux:latest", - console_type="docker_exec", - environment="GNS3_SKIP_INIT=1") + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "srlinux"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: + vm = VendorDockerVM( + "srlinux-1", + str(uuid.uuid4()), + compute_project, + manager, + "srlinux:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1", + ) await vm.create() # the Entrypoint must NOT contain /gns3/init.sh sent = mock.call_args.kwargs["data"] @@ -206,13 +215,11 @@ async def test_create_skip_init_omits_init_sh(compute_project, manager): async def test_create_without_skip_init_prepends_init_sh(compute_project, manager): response = _create_response(None, entrypoint=["/init"]) - with asyncio_patch("gns3server.compute.docker.Docker.list_images", - return_value=[{"image": "srlinux"}]): - with asyncio_patch("gns3server.compute.docker.Docker.query", - return_value=response) as mock: - vm = VendorDockerVM("srlinux-1", str(uuid.uuid4()), compute_project, - manager, "srlinux:latest", - console_type="docker_exec") + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "srlinux"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: + vm = VendorDockerVM( + "srlinux-1", str(uuid.uuid4()), compute_project, manager, "srlinux:latest", console_type="docker_exec" + ) await vm.create() sent = mock.call_args.kwargs["data"] # init.sh IS prepended when not skipping @@ -223,14 +230,18 @@ async def test_create_without_skip_init_prepends_init_sh(compute_project, manage async def test_create_interface_names_sets_max_ethernet(compute_project, manager): response = _create_response(None) - with asyncio_patch("gns3server.compute.docker.Docker.list_images", - return_value=[{"image": "srlinux"}]): - with asyncio_patch("gns3server.compute.docker.Docker.query", - return_value=response) as mock: - vm = VendorDockerVM("srlinux-1", str(uuid.uuid4()), compute_project, - manager, "srlinux:latest", adapters=4, - console_type="docker_exec", - environment="GNS3_SKIP_INIT=1\nGNS3_INTERFACE_NAMES=mgmt0,e1-1,e1-2,e1-3") + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "srlinux"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: + vm = VendorDockerVM( + "srlinux-1", + str(uuid.uuid4()), + compute_project, + manager, + "srlinux:latest", + adapters=4, + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1\nGNS3_INTERFACE_NAMES=mgmt0,e1-1,e1-2,e1-3", + ) await vm.create() sent = mock.call_args.kwargs["data"] # last interface (adapter index 3) should be e1-3, not eth3 @@ -244,17 +255,19 @@ async def test_create_drops_etc_network_for_skip_init(compute_project, manager): seed_proc = MagicMock() seed_proc.communicate = AsyncioMagicMock(return_value=(b"seedcid", b"")) seed_proc.returncode = 0 - with asyncio_patch("gns3server.compute.docker.Docker.list_images", - return_value=[{"image": "srlinux"}]): - with asyncio_patch("gns3server.compute.docker.Docker.query", - return_value=response) as mock: - with patch("asyncio.subprocess.create_subprocess_exec", - return_value=seed_proc): - vm = VendorDockerVM("srlinux-1", str(uuid.uuid4()), compute_project, - manager, "srlinux:latest", - console_type="docker_exec", - environment="GNS3_SKIP_INIT=1", - extra_volumes=["/etc/opt/srlinux"]) + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "srlinux"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: + with patch("asyncio.subprocess.create_subprocess_exec", return_value=seed_proc): + vm = VendorDockerVM( + "srlinux-1", + str(uuid.uuid4()), + compute_project, + manager, + "srlinux:latest", + console_type="docker_exec", + environment="GNS3_SKIP_INIT=1", + extra_volumes=["/etc/opt/srlinux"], + ) await vm.create() sent = mock.call_args.kwargs["data"] targets = [m["Target"] for m in sent["HostConfig"]["Mounts"]] @@ -277,13 +290,11 @@ async def test_create_drops_etc_network_for_skip_init(compute_project, manager): async def test_create_keeps_etc_network_without_skip_init(compute_project, manager): response = _create_response(None) - with asyncio_patch("gns3server.compute.docker.Docker.list_images", - return_value=[{"image": "srlinux"}]): - with asyncio_patch("gns3server.compute.docker.Docker.query", - return_value=response) as mock: - vm = VendorDockerVM("srlinux-1", str(uuid.uuid4()), compute_project, - manager, "srlinux:latest", - console_type="docker_exec") + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "srlinux"}]): + with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock: + vm = VendorDockerVM( + "srlinux-1", str(uuid.uuid4()), compute_project, manager, "srlinux:latest", console_type="docker_exec" + ) await vm.create() sent = mock.call_args.kwargs["data"] targets = [m["Target"] for m in sent["HostConfig"]["Mounts"]] @@ -294,11 +305,11 @@ async def test_create_keeps_etc_network_without_skip_init(compute_project, manag # Interface renaming (_get_container_ifname / move_to_ns) # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_move_to_ns_uses_renamed_interface(compute_project, manager): - vm = _make_vm(compute_project, manager, - environment="GNS3_SKIP_INIT=1\nGNS3_INTERFACE_NAMES=mgmt0,e1-1,e1-2,e1-3") + vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1\nGNS3_INTERFACE_NAMES=mgmt0,e1-1,e1-2,e1-3") vm._ubridge_hypervisor = MagicMock() vm._namespace = 42 nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}) @@ -325,6 +336,7 @@ async def test_move_to_ns_falls_back_to_eth(compute_project, manager): # start() — docker_exec console dispatch + volume bridge + permission fix # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_start_docker_exec_dispatches_console(compute_project, manager): @@ -373,6 +385,7 @@ async def test_start_without_skip_init_skips_vendor_passes(compute_project, mana # _fix_permissions — container-side, skips dead containers, targets volume paths # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_fix_permissions_skips_dead_container(compute_project, manager): @@ -411,8 +424,7 @@ async def test_fix_permissions_targets_volume_paths(compute_project, manager): proc.stderr = MagicMock() proc.stderr.read = AsyncioMagicMock(return_value=b"") - with patch("asyncio.subprocess.create_subprocess_exec", - return_value=proc) as mock_exec: + with patch("asyncio.subprocess.create_subprocess_exec", return_value=proc) as mock_exec: await vm._fix_permissions() # one exec per volume assert mock_exec.call_count == 2 @@ -422,13 +434,14 @@ async def test_fix_permissions_targets_volume_paths(compute_project, manager): script = call_obj.args[-1] # last positional arg is the sh -c script assert "/gns3volumes" not in script assert '"/etc/opt/srlinux"' in script or '"/var/log/srlinux"' in script - assert 'chown' in script + assert "chown" in script # --------------------------------------------------------------------------- # _prepare_volumes — host-side seeding (docker create + cp + rm) # --------------------------------------------------------------------------- + def _seed_proc(stdout=b"seedcid\n", returncode=0): proc = MagicMock() proc.communicate = AsyncioMagicMock(return_value=(stdout, b"")) @@ -439,12 +452,10 @@ def _seed_proc(stdout=b"seedcid\n", returncode=0): @pytest.mark.asyncio async def test_prepare_volumes_seeds_unmarked_volume(compute_project, manager): - vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1", - extra_volumes=["/etc/opt/srlinux"]) + vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1", extra_volumes=["/etc/opt/srlinux"]) image_info = {"Config": {"Volumes": {}}} - with patch("asyncio.subprocess.create_subprocess_exec", - return_value=_seed_proc()) as mock_exec: + with patch("asyncio.subprocess.create_subprocess_exec", return_value=_seed_proc()) as mock_exec: await vm._prepare_volumes(image_info) # docker create + docker cp + docker rm assert mock_exec.call_count == 3 @@ -465,8 +476,7 @@ async def test_prepare_volumes_never_overwrites_marked_volume(compute_project, m """Regression guard: a volume that ever started (marker present) holds the node's saved configuration — re-seeding would reset it to factory.""" - vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1", - extra_volumes=["/etc/opt/srlinux"]) + vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1", extra_volumes=["/etc/opt/srlinux"]) host_dir = os.path.join(vm.working_dir, "etc", "opt", "srlinux") os.makedirs(host_dir, exist_ok=True) marker = os.path.join(host_dir, ".gns3_perms") @@ -475,8 +485,7 @@ async def test_prepare_volumes_never_overwrites_marked_volume(compute_project, m with open(saved, "w") as f: f.write('{"user": "config"}') - with patch("asyncio.subprocess.create_subprocess_exec", - return_value=_seed_proc()) as mock_exec: + with patch("asyncio.subprocess.create_subprocess_exec", return_value=_seed_proc()) as mock_exec: await vm._prepare_volumes({"Config": {"Volumes": {}}}) mock_exec.assert_not_called() with open(saved) as f: @@ -488,8 +497,7 @@ async def test_prepare_volumes_tolerates_missing_image_path(compute_project, man """A volume path the image does not contain (e.g. XRd's /xr-storage-shadow) starts empty — cp fails, the marker is still written, no raise.""" - vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1", - extra_volumes=["/xr-storage-shadow"]) + vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1", extra_volumes=["/xr-storage-shadow"]) calls = {"n": 0} def proc_factory(*args, **kwargs): @@ -500,8 +508,7 @@ async def test_prepare_volumes_tolerates_missing_image_path(compute_project, man calls["n"] += 1 return proc - with patch("asyncio.subprocess.create_subprocess_exec", - side_effect=proc_factory): + with patch("asyncio.subprocess.create_subprocess_exec", side_effect=proc_factory): await vm._prepare_volumes({"Config": {"Volumes": {}}}) assert calls["n"] == 3 # rm still ran (finally path) host_dir = os.path.join(vm.working_dir, "xr-storage-shadow") @@ -512,8 +519,7 @@ async def test_prepare_volumes_tolerates_missing_image_path(compute_project, man async def test_prepare_volumes_skips_without_skip_init(compute_project, manager): vm = _make_vm(compute_project, manager) # no SKIP_INIT - with patch("asyncio.subprocess.create_subprocess_exec", - return_value=_seed_proc()) as mock_exec: + with patch("asyncio.subprocess.create_subprocess_exec", return_value=_seed_proc()) as mock_exec: await vm._prepare_volumes({"Config": {"Volumes": {"/etc/opt/srlinux": None}}}) mock_exec.assert_not_called() @@ -523,8 +529,7 @@ async def test_prepare_volumes_raises_when_seed_container_fails(compute_project, """If `docker create` itself fails, creation must abort loudly instead of binding an empty directory over the NOS's config path.""" - vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1", - extra_volumes=["/etc/opt/srlinux"]) + vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1", extra_volumes=["/etc/opt/srlinux"]) proc = _seed_proc(stdout=b"", returncode=1) with patch("asyncio.subprocess.create_subprocess_exec", return_value=proc): @@ -536,6 +541,7 @@ async def test_prepare_volumes_raises_when_seed_container_fails(compute_project, # _cleanup_console_resources # --------------------------------------------------------------------------- + def test_cleanup_console_resources_closes_writer(compute_project, manager): vm = _make_vm(compute_project, manager) @@ -558,11 +564,15 @@ def test_cleanup_console_resources_no_writer(compute_project, manager): # _LazyExecTelnetServer — upstream aliveness + reconnect/recreate logic # --------------------------------------------------------------------------- + def _make_lazy_server(compute_project, manager, environment="GNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli"): """Build a _LazyExecTelnetServer with _create_exec mocked out (no docker).""" vm = _make_vm(compute_project, manager, environment=environment) srv = _LazyExecTelnetServer( - vm, manager, "e90e34656842", "/opt/srlinux/bin/sr_cli", + vm, + manager, + "e90e34656842", + "/opt/srlinux/bin/sr_cli", allow_resize=vm._console_resize, ) srv._create_exec = AsyncioMagicMock() @@ -655,7 +665,8 @@ async def test_client_naws_ignored_when_resize_disabled(compute_project, manager geometry (paging CLIs need the tall default for concurrent netmiko).""" srv = _make_lazy_server( - compute_project, manager, + compute_project, + manager, environment="GNS3_CONSOLE_RESIZE=0", ) assert srv._allow_resize is False @@ -784,6 +795,7 @@ async def test_create_exec_cmd_has_no_while_true(compute_project, manager): # Container termination (graceful stop for vendor NOS) # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_terminate_container_graceful_stop(compute_project, manager): """With graceful=True (explicit user stop) vendor containers are SIGTERMed @@ -797,8 +809,7 @@ async def test_terminate_container_graceful_stop(compute_project, manager): await vm._terminate_container(graceful=True) - manager.http_query.assert_called_once_with( - "POST", "containers/e90e34656842/stop", params={"t": 60}, timeout=90) + manager.http_query.assert_called_once_with("POST", "containers/e90e34656842/stop", params={"t": 60}, timeout=90) manager.query.assert_not_called() # no kill on the graceful path @@ -826,8 +837,7 @@ async def test_terminate_container_already_stopped_is_silent(compute_project, ma from gns3server.compute.docker.docker_error import DockerHttp304Error vm = _make_vm(compute_project, manager) - manager.http_query = AsyncioMagicMock( - side_effect=DockerHttp304Error("Docker has returned an error: 304")) + manager.http_query = AsyncioMagicMock(side_effect=DockerHttp304Error("Docker has returned an error: 304")) await vm._terminate_container(graceful=True) # must not raise @@ -840,13 +850,9 @@ async def test_stop_uses_graceful_termination(compute_project, manager): vm = _make_vm(compute_project, manager) with patch.object(DockerVM, "_clean_servers", new=AsyncioMagicMock()): with patch.object(DockerVM, "_stop_ubridge", new=AsyncioMagicMock()): - with patch.object( - DockerVM, "_get_container_state", new=AsyncioMagicMock(return_value="running") - ): + with patch.object(DockerVM, "_get_container_state", new=AsyncioMagicMock(return_value="running")): vm._permissions_fixed = True - with patch.object( - VendorDockerVM, "_terminate_container", new=AsyncioMagicMock() - ) as mock_term: + with patch.object(VendorDockerVM, "_terminate_container", new=AsyncioMagicMock()) as mock_term: await vm.stop() mock_term.assert_called_once_with(graceful=False) @@ -880,8 +886,7 @@ async def test_terminate_container_uses_env_timeout(compute_project, manager): await vm._terminate_container(graceful=True) - manager.http_query.assert_called_once_with( - "POST", "containers/e90e34656842/stop", params={"t": 120}, timeout=150) + manager.http_query.assert_called_once_with("POST", "containers/e90e34656842/stop", params={"t": 120}, timeout=150) @pytest.mark.asyncio @@ -890,13 +895,11 @@ async def test_create_reparse_refreshes_env_knobs(compute_project, manager): not on the next project reload: create() re-parses the vendor knobs.""" response = _create_response(None, entrypoint=["/init"]) - vm = _make_vm(compute_project, manager, - environment="GNS3_SKIP_INIT=1\nGNS3_STOP_TIMEOUT=120") + vm = _make_vm(compute_project, manager, environment="GNS3_SKIP_INIT=1\nGNS3_STOP_TIMEOUT=120") assert vm._gns3_init is False and vm._stop_timeout == 120 vm._environment = "GNS3_STOP_TIMEOUT=5" # knob removed + value changed - with asyncio_patch("gns3server.compute.docker.Docker.list_images", - return_value=[{"image": "srlinux"}]): + with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "srlinux"}]): with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response): await vm.create() diff --git a/tests/compute/dynamips/test_dynamips_manager.py b/tests/compute/dynamips/test_dynamips_manager.py index 26fc280ca..d08a10271 100644 --- a/tests/compute/dynamips/test_dynamips_manager.py +++ b/tests/compute/dynamips/test_dynamips_manager.py @@ -104,30 +104,20 @@ async def test_duplicate_node(manager, compute_project): Duplicate dynamips do nothing it's manage outside the filesystem """ - with asyncio_patch('gns3server.compute.dynamips.nodes.c7200.C7200.create'): - source_node = await manager.create_node( - 'R1', - compute_project.id, - str(uuid.uuid4()), - platform="c7200" - ) - destination_node = await manager.create_node( - 'R2', - compute_project.id, - str(uuid.uuid4()), - platform="c7200" - ) + with asyncio_patch("gns3server.compute.dynamips.nodes.c7200.C7200.create"): + source_node = await manager.create_node("R1", compute_project.id, str(uuid.uuid4()), platform="c7200") + destination_node = await manager.create_node("R2", compute_project.id, str(uuid.uuid4()), platform="c7200") destination_node._hypervisor = AsyncioMagicMock() - with open(os.path.join(source_node.working_dir, 'c3600_i1_nvram'), 'w+') as f: + with open(os.path.join(source_node.working_dir, "c3600_i1_nvram"), "w+") as f: f.write("1") - with open(source_node.startup_config_path, 'w+') as f: - f.write('hostname R1\necho TEST') + with open(source_node.startup_config_path, "w+") as f: + f.write("hostname R1\necho TEST") await manager.duplicate_node(source_node.id, destination_node.id) - assert not os.path.exists(os.path.join(destination_node.working_dir, 'c3600_i1_nvram')) + assert not os.path.exists(os.path.join(destination_node.working_dir, "c3600_i1_nvram")) with open(destination_node.startup_config_path) as f: content = f.read() - assert content == '!\nhostname R2\necho TEST' + assert content == "!\nhostname R2\necho TEST" with pytest.raises(DynamipsError): source_node.status = "started" await manager.duplicate_node(source_node.id, destination_node.id) diff --git a/tests/compute/dynamips/test_ethernet_switch.py b/tests/compute/dynamips/test_ethernet_switch.py index cfe49ddeb..c34be1952 100644 --- a/tests/compute/dynamips/test_ethernet_switch.py +++ b/tests/compute/dynamips/test_ethernet_switch.py @@ -28,10 +28,10 @@ def test_mac_command(): node.nios[0].name = "Ethernet0" node.nios[1] = NIOUDP(55, "127.0.0.1", 56) node.nios[1].name = "Ethernet1" - #node._hypervisor.send = AsyncioMagicMock(return_value=["0050.7966.6801 1 Ethernet0", "0050.7966.6802 1 Ethernet1"]) - #console = EthernetSwitchConsole(node) - #assert async_run(console.mac()) == \ + # node._hypervisor.send = AsyncioMagicMock(return_value=["0050.7966.6801 1 Ethernet0", "0050.7966.6802 1 Ethernet1"]) + # console = EthernetSwitchConsole(node) + # assert async_run(console.mac()) == \ # "Port Mac VLAN\n" \ # "Ethernet0 00:50:79:66:68:01 1\n" \ # "Ethernet1 00:50:79:66:68:02 1\n" - #node._hypervisor.send.assert_called_with("ethsw show_mac_addr_table Test") + # node._hypervisor.send.assert_called_with("ethsw show_mac_addr_table Test") diff --git a/tests/compute/iou/test_iou_vm.py b/tests/compute/iou/test_iou_vm.py index a3ec3318a..05ee7de4b 100644 --- a/tests/compute/iou/test_iou_vm.py +++ b/tests/compute/iou/test_iou_vm.py @@ -67,7 +67,7 @@ def fake_iou_bin(images_dir): path = os.path.join(images_dir, "iou.bin") with open(path, "w+") as f: - f.write('\x7fELF\x01\x01\x01') + f.write("\x7fELF\x01\x01\x01") os.chmod(path, stat.S_IREAD | stat.S_IEXEC) return path @@ -103,7 +103,7 @@ async def test_start(vm): mock_process.communicate = AsyncioMagicMock(return_value=(None, None)) await vm.start() assert vm.is_running() - assert vm.command_line == ' '.join(mock_exec.call_args[0]) + assert vm.command_line == " ".join(mock_exec.call_args[0]) assert vm._check_requirements.called assert vm._check_iou_license.called @@ -183,10 +183,10 @@ async def test_rename_nvram_file(vm): It should rename the nvram file to the correct name before launching the VM """ - with open(os.path.join(vm.working_dir, "nvram_0000{}".format(vm.application_id + 1)), 'w+') as f: + with open(os.path.join(vm.working_dir, "nvram_0000{}".format(vm.application_id + 1)), "w+") as f: f.write("1") - with open(os.path.join(vm.working_dir, "vlan.dat-0000{}".format(vm.application_id + 1)), 'w+') as f: + with open(os.path.join(vm.working_dir, "vlan.dat-0000{}".format(vm.application_id + 1)), "w+") as f: f.write("1") vm._rename_nvram_file() @@ -455,14 +455,18 @@ async def test_build_command(vm): def test_get_startup_config(vm): - content = "service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption" + content = ( + "service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption" + ) vm.startup_config = content assert vm.startup_config == content def test_update_startup_config(vm): - content = "service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption" + content = ( + "service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption" + ) vm.startup_config_content = content filepath = os.path.join(vm.working_dir, "startup-config.cfg") assert os.path.exists(filepath) @@ -472,7 +476,9 @@ def test_update_startup_config(vm): def test_update_startup_config_empty(vm): - content = "service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption" + content = ( + "service timestamps debug datetime msec\nservice timestamps log datetime msec\nno service password-encryption" + ) vm.startup_config_content = content filepath = os.path.join(vm.working_dir, "startup-config.cfg") assert os.path.exists(filepath) @@ -496,7 +502,7 @@ def test_change_name(vm): path = os.path.join(vm.working_dir, "startup-config.cfg") vm.name = "world" - with open(path, 'w+') as f: + with open(path, "w+") as f: f.write("hostname world") vm.name = "hello" assert vm.name == "hello" @@ -504,7 +510,7 @@ def test_change_name(vm): assert f.read() == "hostname hello" # support hostname not sync vm.name = "alpha" - with open(path, 'w+') as f: + with open(path, "w+") as f: f.write("no service password-encryption\nhostname beta\nno ip icmp rate-limit unreachable") vm.name = "charlie" assert vm.name == "charlie" @@ -526,13 +532,18 @@ async def test_library_check(vm): @pytest.mark.asyncio async def test_enable_l1_keepalives(vm): - with asyncio_patch("gns3server.utils.asyncio.subprocess_check_output", return_value="***************************************************************\n\n-l Enable Layer 1 keepalive messages\n-u UDP port base for distributed networks\n"): + with asyncio_patch( + "gns3server.utils.asyncio.subprocess_check_output", + return_value="***************************************************************\n\n-l Enable Layer 1 keepalive messages\n-u UDP port base for distributed networks\n", + ): command = ["test"] await vm._enable_l1_keepalives(command) assert command == ["test", "-l"] - with asyncio_patch("gns3server.utils.asyncio.subprocess_check_output", return_value="***************************************************************\n\n-u UDP port base for distributed networks\n"): - + with asyncio_patch( + "gns3server.utils.asyncio.subprocess_check_output", + return_value="***************************************************************\n\n-u UDP port base for distributed networks\n", + ): command = ["test"] with pytest.raises(IOUError): await vm._enable_l1_keepalives(command) @@ -543,7 +554,9 @@ async def test_enable_l1_keepalives(vm): async def test_start_capture(vm, tmpdir, manager, free_console_port): output_file = str(tmpdir / "test.pcap") - nio = manager.create_nio({"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"} + ) await vm.adapter_add_nio_binding(0, 0, nio) await vm.start_capture(0, 0, output_file) assert vm._adapters[0].get_nio(0).capturing @@ -553,7 +566,9 @@ async def test_start_capture(vm, tmpdir, manager, free_console_port): async def test_stop_capture(vm, tmpdir, manager, free_console_port): output_file = str(tmpdir / "test.pcap") - nio = manager.create_nio({"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"} + ) await vm.adapter_add_nio_binding(0, 0, nio) await vm.start_capture(0, 0, output_file) assert vm._adapters[0].get_nio(0).capturing diff --git a/tests/compute/marker/test_marker_manager.py b/tests/compute/marker/test_marker_manager.py index 5917870d7..dcd71c3f9 100644 --- a/tests/compute/marker/test_marker_manager.py +++ b/tests/compute/marker/test_marker_manager.py @@ -26,8 +26,8 @@ from gns3server.compute.marker.marker_listener import MarkerListener # Registry # --------------------------------------------------------------------------- -class TestMarkerRegistry: +class TestMarkerRegistry: def test_register_and_lookup(self): MarkerManager.reset() mgr = MarkerManager.instance() @@ -85,6 +85,7 @@ class TestMarkerRegistry: # MarkerListener parsing # --------------------------------------------------------------------------- + class FakeMarkerManager: def __init__(self): self.events = [] @@ -100,13 +101,10 @@ class FakeMarkerManager: self.events.append((project_id, event)) def register(self, project_id, node_id, filter_name, link_id, tag): - self._entries[(node_id, filter_name)] = { - "project_id": project_id, "link_id": link_id, "tag": tag - } + self._entries[(node_id, filter_name)] = {"project_id": project_id, "link_id": link_id, "tag": tag} class TestMarkerListener: - def test_parses_valid_mark_datagram(self): fmgr = FakeMarkerManager() fmgr.register("p1", "n1", "f1", "l1", tag=7) @@ -193,9 +191,7 @@ class TestMarkerListener: fmgr.register("p", "n", "f", "registry-link", tag=1) lis = MarkerListener(fmgr) lis.connection_made(None) - lis.datagram_received( - b"MARK 3.0 node=n filter=f link=signal-link tag=1 len=42\n", None - ) + lis.datagram_received(b"MARK 3.0 node=n filter=f link=signal-link tag=1 len=42\n", None) assert fmgr.events[0][1]["link_id"] == "signal-link" def test_link_dash_falls_back_to_registry_link(self): @@ -249,9 +245,9 @@ class TestMarkerListener: # UDP round-trip # --------------------------------------------------------------------------- + @pytest.mark.asyncio class TestMarkerManagerUDP: - async def test_listener_receives_and_dispatches(self): MarkerManager.reset() mgr = MarkerManager.instance() @@ -273,12 +269,8 @@ class TestMarkerManagerUDP: self.transport = transport sp = SendProto() - transport, _ = await loop.create_datagram_endpoint( - lambda: sp, remote_addr=("127.0.0.1", mgr.port) - ) - transport.sendto( - b"MARK 123.456 node=node-rt filter=filt-rt tag=10 len=88\n" - ) + transport, _ = await loop.create_datagram_endpoint(lambda: sp, remote_addr=("127.0.0.1", mgr.port)) + transport.sendto(b"MARK 123.456 node=node-rt filter=filt-rt tag=10 len=88\n") await asyncio.sleep(0.15) transport.close() diff --git a/tests/compute/qemu/test_qcow2.py b/tests/compute/qemu/test_qcow2.py index c75368e9c..dfd86a2e0 100644 --- a/tests/compute/qemu/test_qcow2.py +++ b/tests/compute/qemu/test_qcow2.py @@ -28,10 +28,7 @@ def qemu_img(): We can't use shutil.which because for safety reason we break the PATH to avoid test interacting with real binaries """ - paths = [ - "/usr/bin/qemu-img", - "/usr/local/bin/qemu-img" - ] + paths = ["/usr/bin/qemu-img", "/usr/local/bin/qemu-img"] for path in paths: if os.path.exists(path): return path @@ -60,9 +57,9 @@ def test_invalid_file(): def test_invalid_empty_file(tmpdir): - open(str(tmpdir / 'a'), 'w+').close() + open(str(tmpdir / "a"), "w+").close() with pytest.raises(Qcow2Error): - Qcow2(str(tmpdir / 'a')) + Qcow2(str(tmpdir / "a")) @pytest.mark.skipif(qemu_img() is None, reason="qemu-img is not available") diff --git a/tests/compute/qemu/test_qemu_manager.py b/tests/compute/qemu/test_qemu_manager.py index 663420445..a75bfceaf 100644 --- a/tests/compute/qemu/test_qemu_manager.py +++ b/tests/compute/qemu/test_qemu_manager.py @@ -44,7 +44,10 @@ def fake_qemu_img_binary(tmpdir): @pytest.mark.asyncio async def test_get_qemu_version(): - with asyncio_patch("gns3server.compute.qemu.subprocess_check_output", return_value="QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard"): + with asyncio_patch( + "gns3server.compute.qemu.subprocess_check_output", + return_value="QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard", + ): version = await Qemu.get_qemu_version("/tmp/qemu-test") assert version == "2.2.0" @@ -61,7 +64,10 @@ async def test_binary_list(monkeypatch, tmpdir): f.write("1") os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) - with asyncio_patch("gns3server.compute.qemu.subprocess_check_output", return_value="QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard") as mock: + with asyncio_patch( + "gns3server.compute.qemu.subprocess_check_output", + return_value="QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard", + ) as mock: version = "2.2.0" qemus = await Qemu.binary_list() @@ -121,8 +127,8 @@ async def test_get_kvm_archs_kvm_ok(): with patch("os.path.exists", return_value=True): archs = await Qemu.get_kvm_archs() - if platform.machine() == 'x86_64': - assert archs == ['x86_64', 'i386'] + if platform.machine() == "x86_64": + assert archs == ["x86_64", "i386"] else: assert archs == [platform.machine()] diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index fa16bb560..d5f0842d3 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -102,21 +102,28 @@ async def test_vm(compute_project, manager, fake_qemu_binary): async def test_vm_create(compute_project, manager, fake_qemu_binary): vm = QemuVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, qemu_path=fake_qemu_binary) - fake_img = os.path.join(vm.working_dir, 'hello') - with open(fake_img, 'w+') as f: - f.write('hello') + fake_img = os.path.join(vm.working_dir, "hello") + with open(fake_img, "w+") as f: + f.write("hello") vm._hda_disk_image = fake_img await vm.create() # tests if `create` created md5sums - assert os.path.exists(os.path.join(vm.working_dir, 'hello.md5sum')) + assert os.path.exists(os.path.join(vm.working_dir, "hello.md5sum")) @pytest.mark.asyncio async def test_vm_invalid_qemu_with_platform(compute_project, manager, fake_qemu_binary): - vm = QemuVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, qemu_path="/usr/fake/bin/qemu-system-64", platform="x86_64") + vm = QemuVM( + "test", + "00010203-0405-0607-0809-0a0b0c0d0e0f", + compute_project, + manager, + qemu_path="/usr/fake/bin/qemu-system-64", + platform="x86_64", + ) assert vm.qemu_path == fake_qemu_binary assert vm.platform == "x86_64" @@ -125,7 +132,13 @@ async def test_vm_invalid_qemu_with_platform(compute_project, manager, fake_qemu @pytest.mark.asyncio async def test_vm_invalid_qemu_without_platform(compute_project, manager, fake_qemu_binary): - vm = QemuVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, qemu_path="/usr/fake/bin/qemu-system-x86_64") + vm = QemuVM( + "test", + "00010203-0405-0607-0809-0a0b0c0d0e0f", + compute_project, + manager, + qemu_path="/usr/fake/bin/qemu-system-x86_64", + ) assert vm.qemu_path == fake_qemu_binary assert vm.platform == "x86_64" @@ -149,7 +162,7 @@ async def test_start(vm, running_subprocess_mock): with asyncio_patch("asyncio.create_subprocess_exec", return_value=running_subprocess_mock) as mock: await vm.start() assert vm.is_running() - assert vm.command_line == ' '.join(mock.call_args[0]) + assert vm.command_line == " ".join(mock.call_args[0]) @pytest.mark.asyncio @@ -232,7 +245,9 @@ async def test_suspend(vm, running_subprocess_mock): @pytest.mark.asyncio async def test_add_nio_binding_udp(vm): - nio = Qemu.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) + nio = Qemu.instance().create_nio( + {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}} + ) assert nio.lport == 4242 await vm.adapter_add_nio_binding(0, nio) assert nio.lport == 4242 @@ -241,7 +256,9 @@ async def test_add_nio_binding_udp(vm): @pytest.mark.asyncio async def test_port_remove_nio_binding(vm): - nio = Qemu.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) + nio = Qemu.instance().create_nio( + {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}} + ) await vm.adapter_add_nio_binding(0, nio) await vm.adapter_remove_nio_binding(0) assert vm._ethernet_adapters[0].ports[0] is None @@ -271,9 +288,9 @@ def test_set_qemu_path(vm, tmpdir, fake_qemu_binary): vm.qemu_path = None # Should not crash with unicode characters - path = str(tmpdir / "\u62FF" / "qemu-system-mips") + path = str(tmpdir / "\u62ff" / "qemu-system-mips") - os.makedirs(str(tmpdir / "\u62FF")) + os.makedirs(str(tmpdir / "\u62ff")) # Raise because file doesn't exists with pytest.raises(QemuError): @@ -360,17 +377,38 @@ async def test_disk_options(vm, tmpdir, fake_qemu_img_binary): open(vm._hda_disk_image, "w+").close() open(vm._hdb_disk_image, "w+").close() - with (asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM._find_disk_file_format", return_value="qcow2")): + with asyncio_patch("gns3server.compute.qemu.qemu_vm.QemuVM._find_disk_file_format", return_value="qcow2"): with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: options = await vm._disk_options() assert process.called args, kwargs = process.call_args - assert args == (fake_qemu_img_binary, "create", "-o", "backing_file={}".format(vm._hda_disk_image), "-F", "qcow2", "-f", "qcow2", os.path.join(vm.working_dir, "hda_disk.qcow2")) or \ - args == (fake_qemu_img_binary, "create", "-o", "backing_file={}".format(vm._hdb_disk_image), "-F", "qcow2", "-f", "qcow2", os.path.join(vm.working_dir, "hdb_disk.qcow2")) + assert args == ( + fake_qemu_img_binary, + "create", + "-o", + "backing_file={}".format(vm._hda_disk_image), + "-F", + "qcow2", + "-f", + "qcow2", + os.path.join(vm.working_dir, "hda_disk.qcow2"), + ) or args == ( + fake_qemu_img_binary, + "create", + "-o", + "backing_file={}".format(vm._hdb_disk_image), + "-F", + "qcow2", + "-f", + "qcow2", + os.path.join(vm.working_dir, "hdb_disk.qcow2"), + ) assert options == [ - '-drive', 'file=' + os.path.join(vm.working_dir, "hda_disk.qcow2") + ',if=ide,index=0,media=disk,id=drive0', - '-drive', 'file=' + os.path.join(vm.working_dir, "hdb_disk.qcow2") + ',if=none,index=1,media=disk,id=drive1', + "-drive", + "file=" + os.path.join(vm.working_dir, "hda_disk.qcow2") + ",if=ide,index=0,media=disk,id=drive0", + "-drive", + "file=" + os.path.join(vm.working_dir, "hdb_disk.qcow2") + ",if=none,index=1,media=disk,id=drive1", ] @@ -382,7 +420,7 @@ async def test_cdrom_option(vm, tmpdir, fake_qemu_img_binary): options = await vm._build_command() - assert ' '.join(['-cdrom', str(tmpdir / "test.iso")]) in ' '.join(options) + assert " ".join(["-cdrom", str(tmpdir / "test.iso")]) in " ".join(options) @pytest.mark.asyncio @@ -391,7 +429,7 @@ async def test_bios_option(vm, tmpdir, fake_qemu_img_binary): vm._bios_image = str(tmpdir / "test.img") open(vm._bios_image, "w+").close() options = await vm._build_command() - assert ' '.join(['-bios', str(tmpdir / "test.img")]) in ' '.join(options) + assert " ".join(["-bios", str(tmpdir / "test.img")]) in " ".join(options) @pytest.mark.skipif(sys.platform.startswith("win"), reason="Test not working on Windows") @@ -407,14 +445,16 @@ async def test_uefi_boot_mode_option(vm, tmpdir, images_dir, fake_qemu_img_binar else: ovmf_code_path = os.path.join(images_dir, "OVMF_CODE_4M.fd") with open(ovmf_code_path, "w+") as f: - f.write('1') + f.write("1") ovmf_vars_path = os.path.join(images_dir, "OVMF_VARS_4M.fd") with open(ovmf_vars_path, "w+") as f: - f.write('1') + f.write("1") options = await vm._build_command() - assert ' '.join(["-drive", "if=pflash,format=raw,readonly,file={}".format(ovmf_code_path)]) in ' '.join(options) - assert ' '.join(["-drive", "if=pflash,format=raw,file={}".format(os.path.join(vm.working_dir, "OVMF_VARS_4M.fd"))]) in ' '.join(options) + assert " ".join(["-drive", "if=pflash,format=raw,readonly,file={}".format(ovmf_code_path)]) in " ".join(options) + assert " ".join( + ["-drive", "if=pflash,format=raw,file={}".format(os.path.join(vm.working_dir, "OVMF_VARS_4M.fd"))] + ) in " ".join(options) @pytest.mark.asyncio @@ -429,20 +469,20 @@ async def test_uefi_with_bios_image_already_configured(vm, tmpdir, fake_qemu_img @pytest.mark.asyncio async def test_vnc_option(vm, fake_qemu_img_binary): - vm._console_type = 'vnc' + vm._console_type = "vnc" vm._console = 5905 options = await vm._build_command() - assert '-vnc 127.0.0.1:5' in ' '.join(options) + assert "-vnc 127.0.0.1:5" in " ".join(options) @pytest.mark.asyncio async def test_spice_option(vm, fake_qemu_img_binary): - vm._console_type = 'spice' + vm._console_type = "spice" vm._console = 5905 options = await vm._build_command() - assert '-spice addr=127.0.0.1,port=5905,disable-ticketing' in ' '.join(options) - assert '-vga qxl' in ' '.join(options) + assert "-spice addr=127.0.0.1,port=5905,disable-ticketing" in " ".join(options) + assert "-vga qxl" in " ".join(options) @pytest.mark.asyncio @@ -452,9 +492,10 @@ async def test_tpm_option(vm, tmpdir, fake_qemu_img_binary): tpm_sock = os.path.join(vm.temporary_directory, "swtpm.sock") with patch("os.path.exists", return_value=True) as os_path: options = await vm._build_command() - assert '-chardev socket,id=chrtpm,path={}'.format(tpm_sock) in ' '.join(options) - assert '-tpmdev emulator,id=tpm0,chardev=chrtpm' in ' '.join(options) - assert '-device tpm-tis,tpmdev=tpm0' in ' '.join(options) + assert "-chardev socket,id=chrtpm,path={}".format(tpm_sock) in " ".join(options) + assert "-tpmdev emulator,id=tpm0,chardev=chrtpm" in " ".join(options) + assert "-device tpm-tis,tpmdev=tpm0" in " ".join(options) + @pytest.mark.asyncio async def test_disk_options_multiple_disk(vm, tmpdir, fake_qemu_img_binary): @@ -477,10 +518,14 @@ async def test_disk_options_multiple_disk(vm, tmpdir, fake_qemu_img_binary): options = await vm._disk_options() assert options == [ - '-drive', 'file=' + os.path.join(vm.working_dir, "hda_disk.qcow2") + ',if=ide,index=0,media=disk,id=drive0', - '-drive', 'file=' + os.path.join(vm.working_dir, "hdb_disk.qcow2") + ',if=ide,index=1,media=disk,id=drive1', - '-drive', 'file=' + os.path.join(vm.working_dir, "hdc_disk.qcow2") + ',if=ide,index=2,media=disk,id=drive2', - '-drive', 'file=' + os.path.join(vm.working_dir, "hdd_disk.qcow2") + ',if=ide,index=3,media=disk,id=drive3' + "-drive", + "file=" + os.path.join(vm.working_dir, "hda_disk.qcow2") + ",if=ide,index=0,media=disk,id=drive0", + "-drive", + "file=" + os.path.join(vm.working_dir, "hdb_disk.qcow2") + ",if=ide,index=1,media=disk,id=drive1", + "-drive", + "file=" + os.path.join(vm.working_dir, "hdc_disk.qcow2") + ",if=ide,index=2,media=disk,id=drive2", + "-drive", + "file=" + os.path.join(vm.working_dir, "hdd_disk.qcow2") + ",if=ide,index=3,media=disk,id=drive3", ] @@ -534,7 +579,6 @@ async def test_control_vm_expect_text(vm, running_subprocess_mock): reader = MagicMock() writer = MagicMock() with asyncio_patch("asyncio.open_connection", return_value=(reader, writer)): - future = asyncio.Future() future.set_result(b"epic product") reader.readline.return_value = future @@ -578,7 +622,7 @@ async def test_build_command(vm, fake_qemu_binary): "-netdev", "socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio.rport, nio.lport), "-display", - "none" + "none", ] @@ -648,7 +692,7 @@ async def test_build_command_kvm_2_4(linux_platform, vm, fake_qemu_binary): "e1000,mac={},bus=pci-bridge1,addr=0x00,netdev=gns3-0".format(vm._mac_address), "-netdev", "socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio.rport, nio.lport), - "-nographic" + "-nographic", ] @@ -697,10 +741,12 @@ async def test_build_command_two_adapters(vm, fake_qemu_binary): "-netdev", "socket,id=gns3-0,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio1.rport, nio1.lport), "-device", - "e1000,mac={},bus=pci-bridge1,addr=0x01,netdev=gns3-1".format(int_to_macaddress(macaddress_to_int(vm._mac_address) + 1)), + "e1000,mac={},bus=pci-bridge1,addr=0x01,netdev=gns3-1".format( + int_to_macaddress(macaddress_to_int(vm._mac_address) + 1) + ), "-netdev", "socket,id=gns3-1,udp=127.0.0.1:{},localaddr=127.0.0.1:{}".format(nio2.rport, nio2.lport), - "-nographic" + "-nographic", ] @@ -725,7 +771,6 @@ async def test_build_command_two_adapters_mac_address(vm): mac_1 = int_to_macaddress(macaddress_to_int(vm._mac_address) + 1) assert mac_0[:8] == "00:42:ab" with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - cmd = await vm._build_command() assert "e1000,mac={},bus=pci-bridge1,addr=0x00,netdev=gns3-0".format(mac_0) in cmd assert "e1000,mac={},bus=pci-bridge1,addr=0x01,netdev=gns3-1".format(mac_1) in cmd @@ -747,7 +792,7 @@ async def test_build_command_large_number_of_adapters(vm): cmd = await vm._build_command() # Count if we have 100 e1000 adapters in the command - assert len([l for l in cmd if "e1000" in l ]) == 100 + assert len([l for l in cmd if "e1000" in l]) == 100 assert len(vm._ethernet_adapters) == 100 assert "e1000,mac={},bus=pci-bridge1,addr=0x00,netdev=gns3-0".format(mac_0) in cmd diff --git a/tests/compute/test_base_node.py b/tests/compute/test_base_node.py index 6420d905b..db2418d76 100644 --- a/tests/compute/test_base_node.py +++ b/tests/compute/test_base_node.py @@ -113,7 +113,9 @@ def test_aux(compute_project, manager, port_manager): aux = port_manager.get_free_tcp_port(compute_project) port_manager.release_tcp_port(aux, compute_project) - node = DockerVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, "ubuntu", aux=aux, aux_type="telnet") + node = DockerVM( + "test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, "ubuntu", aux=aux, aux_type="telnet" + ) assert node.aux == aux node.aux = None assert node.aux is None @@ -125,13 +127,17 @@ def test_allocate_aux(compute_project, manager): assert node.aux is None # Docker has an aux port by default - node = DockerVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, "ubuntu", aux_type="telnet") + node = DockerVM( + "test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, "ubuntu", aux_type="telnet" + ) assert node.aux is not None def test_change_aux_port(compute_project, manager, port_manager): - node = DockerVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, "ubuntu", aux_type="telnet") + node = DockerVM( + "test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, "ubuntu", aux_type="telnet" + ) port1 = port_manager.get_free_tcp_port(node.project) port2 = port_manager.get_free_tcp_port(node.project) port_manager.release_tcp_port(port1, node.project) @@ -145,25 +151,20 @@ def test_change_aux_port(compute_project, manager, port_manager): @pytest.mark.asyncio async def test_update_ubridge_udp_connection(node): - filters = { - "latency": [10] - } + filters = {"latency": [10]} snio = NIOUDP(1245, "localhost", 1246) dnio = NIOUDP(1245, "localhost", 1244) dnio.filters = filters with asyncio_patch("gns3server.compute.base_node.BaseNode._ubridge_apply_filters") as mock: - await node.update_ubridge_udp_connection('VPCS-10', snio, dnio) + await node.update_ubridge_udp_connection("VPCS-10", snio, dnio) mock.assert_called_with("VPCS-10", filters) @pytest.mark.asyncio async def test_ubridge_apply_filters(node): - filters = OrderedDict(( - ('latency', [10]), - ('bpf', ["icmp[icmptype] == 8\ntcp src port 53"]) - )) + filters = OrderedDict((("latency", [10]), ("bpf", ["icmp[icmptype] == 8\ntcp src port 53"]))) node._ubridge_send = AsyncioMagicMock() await node._ubridge_apply_filters("VPCS-10", filters) node._ubridge_send.assert_any_call("bridge reset_packet_filters VPCS-10") @@ -173,14 +174,12 @@ async def test_ubridge_apply_filters(node): @pytest.mark.asyncio async def test_ubridge_apply_bpf_filters(node): - filters = { - "bpf": ["icmp[icmptype] == 8\ntcp src port 53"] - } + filters = {"bpf": ["icmp[icmptype] == 8\ntcp src port 53"]} node._ubridge_send = AsyncioMagicMock() await node._ubridge_apply_filters("VPCS-10", filters) node._ubridge_send.assert_any_call("bridge reset_packet_filters VPCS-10") - node._ubridge_send.assert_any_call("bridge add_packet_filter VPCS-10 filter0 bpf \"icmp[icmptype] == 8\"") - node._ubridge_send.assert_any_call("bridge add_packet_filter VPCS-10 filter1 bpf \"tcp src port 53\"") + node._ubridge_send.assert_any_call('bridge add_packet_filter VPCS-10 filter0 bpf "icmp[icmptype] == 8"') + node._ubridge_send.assert_any_call('bridge add_packet_filter VPCS-10 filter1 bpf "tcp src port 53"') @pytest.mark.asyncio @@ -259,8 +258,16 @@ async def test_apply_markers_appends_linktype_for_serial(compute_project, manage node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager) node._ubridge_send = AsyncioMagicMock() nio = NIOUDP(1234, "127.0.0.1", 4321) - nio.markers = {"m": {"bpf": "icmp", "tag": None, "link_id": "L1", - "direction": None, "data_link_type": "DLT_C_HDLC", "enabled": True}} + nio.markers = { + "m": { + "bpf": "icmp", + "tag": None, + "link_id": "L1", + "direction": None, + "data_link_type": "DLT_C_HDLC", + "enabled": True, + } + } with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm: mm.instance.return_value.register = MagicMock() await node._ubridge_apply_markers("VPCS-10", nio) @@ -274,8 +281,16 @@ async def test_apply_markers_omits_linktype_for_ethernet(compute_project, manage node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager) node._ubridge_send = AsyncioMagicMock() nio = NIOUDP(1234, "127.0.0.1", 4321) - nio.markers = {"m": {"bpf": "icmp", "tag": None, "link_id": "L1", - "direction": None, "data_link_type": "DLT_EN10MB", "enabled": True}} + nio.markers = { + "m": { + "bpf": "icmp", + "tag": None, + "link_id": "L1", + "direction": None, + "data_link_type": "DLT_EN10MB", + "enabled": True, + } + } with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm: mm.instance.return_value.register = MagicMock() await node._ubridge_apply_markers("VPCS-10", nio) @@ -334,8 +349,7 @@ async def test_delete_marker_capture_drops_from_nio_markers(compute_project, man # nio.markers and starting the node reinstalls it (empty pcap reappears). node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager) nio = NIOUDP(1234, "127.0.0.1", 4321) - nio.markers = {"m": {"bpf": "icmp", "tag": None, "link_id": "L1", - "direction": None, "enabled": True}} + nio.markers = {"m": {"bpf": "icmp", "tag": None, "link_id": "L1", "direction": None, "enabled": True}} await node.delete_marker_capture("m", "L1", nio) assert "m" not in nio.markers @@ -404,16 +418,20 @@ async def test_apply_markers_rebuilds_changed_bpf(compute_project, manager): node._ubridge_hypervisor = MagicMock() node._ubridge_hypervisor.is_running.return_value = True node._marker_filter_bridges["m", "L1"] = "VPCS-10" - node._marker_specs["m", "L1"] = {"bpf": "icmp", "tag": None, "direction": None, - "data_link_type": None, "enabled": True} + node._marker_specs["m", "L1"] = { + "bpf": "icmp", + "tag": None, + "direction": None, + "data_link_type": None, + "enabled": True, + } nio = NIOUDP(1234, "127.0.0.1", 4321) - nio.markers = {"m": {"bpf": "tcp", "tag": None, "link_id": "L1", - "direction": None, "enabled": True}} + nio.markers = {"m": {"bpf": "tcp", "tag": None, "link_id": "L1", "direction": None, "enabled": True}} with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm: mm.instance.return_value.register = MagicMock() await node._ubridge_apply_markers("VPCS-10", nio) cmds = [c.args[0] for c in node._ubridge_send.call_args_list] - assert any("delete_packet_filter VPCS-10 m" in c for c in cmds) # old removed + assert any("delete_packet_filter VPCS-10 m" in c for c in cmds) # old removed assert any("add_packet_filter VPCS-10 m mark" in c and "tcp" in c for c in cmds) # new added @@ -441,10 +459,10 @@ async def test_apply_markers_preserves_markers_on_other_bridges(compute_project, mm.instance.return_value.unregister = MagicMock() await node._ubridge_apply_markers("VPCS-10", nio) cmds = [c.args[0] for c in node._ubridge_send.call_args_list] - assert any("delete_packet_filter VPCS-10 m" in c for c in cmds) # L1 removed on its bridge - assert not any("VPCS-20" in c for c in cmds) # other bridge untouched + assert any("delete_packet_filter VPCS-10 m" in c for c in cmds) # L1 removed on its bridge + assert not any("VPCS-20" in c for c in cmds) # other bridge untouched assert ("m", "L1") not in node._marker_filter_bridges - assert ("m", "L2") in node._marker_filter_bridges # L2 preserved + assert ("m", "L2") in node._marker_filter_bridges # L2 preserved @pytest.mark.asyncio @@ -467,6 +485,7 @@ class _GoneConsoleWebsocket: def __init__(self): from types import SimpleNamespace + self.client = SimpleNamespace(host="127.0.0.1", port=5000) async def receive(self): @@ -474,12 +493,14 @@ class _GoneConsoleWebsocket: async def send_bytes(self, data): from starlette.websockets import WebSocketDisconnect + raise WebSocketDisconnect(code=1006) @pytest.mark.asyncio async def test_console_websocket_client_disconnect_while_node_output_streams( - compute_project, manager, port_manager, monkeypatch, caplog): + compute_project, manager, port_manager, monkeypatch, caplog +): # regression test: the client disconnects while the node is still streaming # console output. telnet_forward used to let the (empty-str) WebSocketDisconnect # from send_bytes escape, logging a message-less WARNING. @@ -505,10 +526,7 @@ async def test_console_websocket_client_disconnect_while_node_output_streams( assert telnet_writer.close.called assert not [r for r in caplog.records if r.levelno >= logging.WARNING] - assert any( - "has disconnected from compute console WebSocket while node output" in r.message - for r in caplog.records - ) + assert any("has disconnected from compute console WebSocket while node output" in r.message for r in caplog.records) @pytest.mark.asyncio diff --git a/tests/compute/test_manager.py b/tests/compute/test_manager.py index cddbeb3ea..304fa972d 100644 --- a/tests/compute/test_manager.py +++ b/tests/compute/test_manager.py @@ -81,10 +81,10 @@ def test_get_abs_image_path(qemu, tmpdir, config): os.makedirs(str(tmpdir / "QEMU")) path1 = force_unix_path(str(tmpdir / "test1.bin")) - open(path1, 'w+').close() + open(path1, "w+").close() path2 = force_unix_path(str(tmpdir / "QEMU" / "test2.bin")) - open(path2, 'w+').close() + open(path2, "w+").close() config.settings.Server.images_path = str(tmpdir) assert qemu.get_abs_image_path(path1) == path1 @@ -180,21 +180,21 @@ def test_get_relative_image_path(qemu, tmpdir, config): os.makedirs(str(tmpdir / "images1" / "QEMU")) os.makedirs(str(tmpdir / "images1" / "VBOX")) path1 = force_unix_path(str(tmpdir / "images1" / "test1.bin")) - open(path1, 'w+').close() + open(path1, "w+").close() path2 = force_unix_path(str(tmpdir / "images1" / "QEMU" / "test2.bin")) - open(path2, 'w+').close() + open(path2, "w+").close() os.makedirs(str(tmpdir / "images2")) path3 = force_unix_path(str(tmpdir / "images2" / "test3.bin")) - open(path3, 'w+').close() + open(path3, "w+").close() path4 = force_unix_path(str(tmpdir / "test4.bin")) - open(path4, 'w+').close() + open(path4, "w+").close() # The user use an image of another emulator we return the full path path5 = force_unix_path(str(tmpdir / "images1" / "VBOX" / "test5.bin")) - open(path5, 'w+').close() + open(path5, "w+").close() config.settings.Server.images_path = str(tmpdir / "images1") config.settings.Server.additional_images_paths = str(tmpdir / "images2") @@ -221,9 +221,9 @@ async def test_list_images(qemu, tmpdir): f.write("1234567") with patch("gns3server.utils.images.default_images_directory", return_value=str(tmp_images_dir)): - assert sorted(await qemu.list_images(), key=lambda k: k['filename']) == [ + assert sorted(await qemu.list_images(), key=lambda k: k["filename"]) == [ {"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7}, - {"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7} + {"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7}, ] @@ -243,11 +243,15 @@ async def test_list_images_recursives(qemu, tmpdir): f.write("1234567") with patch("gns3server.utils.images.default_images_directory", return_value=str(tmp_images_dir)): - - assert sorted(await qemu.list_images(), key=lambda k: k['filename']) == [ + assert sorted(await qemu.list_images(), key=lambda k: k["filename"]) == [ {"filename": "a.qcow2", "path": "a.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7}, {"filename": "b.qcow2", "path": "b.qcow2", "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7}, - {"filename": "c.qcow2", "path": force_unix_path(os.path.sep.join(["c", "c.qcow2"])), "md5sum": "fcea920f7412b5da7be0cf42b8c93759", "filesize": 7} + { + "filename": "c.qcow2", + "path": force_unix_path(os.path.sep.join(["c", "c.qcow2"])), + "md5sum": "fcea920f7412b5da7be0cf42b8c93759", + "filesize": 7, + }, ] @@ -299,10 +303,10 @@ async def test_duplicate_vpcs(vpcs, compute_project): @pytest.mark.asyncio async def test_duplicate_ethernet_switch(compute_project): - with asyncio_patch('gns3server.compute.dynamips.nodes.ethernet_switch.EthernetSwitch.create'): + with asyncio_patch("gns3server.compute.dynamips.nodes.ethernet_switch.EthernetSwitch.create"): dynamips_manager = Dynamips.instance() source_node_id = str(uuid.uuid4()) - await dynamips_manager.create_node("SW-1", compute_project.id, source_node_id, node_type='ethernet_switch') + await dynamips_manager.create_node("SW-1", compute_project.id, source_node_id, node_type="ethernet_switch") destination_node_id = str(uuid.uuid4()) - await dynamips_manager.create_node("SW-2", compute_project.id, destination_node_id, node_type='ethernet_switch') + await dynamips_manager.create_node("SW-2", compute_project.id, destination_node_id, node_type="ethernet_switch") await dynamips_manager.duplicate_node(source_node_id, destination_node_id) diff --git a/tests/compute/test_notification_manager.py b/tests/compute/test_notification_manager.py index b3173429d..3daea1a8c 100644 --- a/tests/compute/test_notification_manager.py +++ b/tests/compute/test_notification_manager.py @@ -34,7 +34,7 @@ async def test_queue(): notifications.emit("test", {"a": 1}) res = await queue.get(5) - assert res == ('test', {"a": 1}, {}) + assert res == ("test", {"a": 1}, {}) assert len(notifications._listeners) == 0 diff --git a/tests/compute/test_project.py b/tests/compute/test_project.py index 50f967925..7380df23f 100644 --- a/tests/compute/test_project.py +++ b/tests/compute/test_project.py @@ -48,8 +48,8 @@ async def node(compute_project, manager): @pytest.mark.asyncio async def test_affect_uuid(): - p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f') - assert p.id == '00010203-0405-0607-0809-0a0b0c0d0e0f' + p = Project(project_id="00010203-0405-0607-0809-0a0b0c0d0e0f") + assert p.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" @pytest.mark.asyncio @@ -58,14 +58,14 @@ async def test_clean_tmp_directory(): The tmp directory should be clean at project open and close """ - p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f') + p = Project(project_id="00010203-0405-0607-0809-0a0b0c0d0e0f") path = p.tmp_working_directory() os.makedirs(path) await p.close() assert not os.path.exists(path) os.makedirs(path) - p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f') + p = Project(project_id="00010203-0405-0607-0809-0a0b0c0d0e0f") assert not os.path.exists(path) @@ -99,11 +99,7 @@ async def test_variables(): async def test_json(): p = Project(project_id=str(uuid4())) - assert p.asdict() == { - "name": p.name, - "project_id": p.id, - "variables": None - } + assert p.asdict() == {"name": p.name, "project_id": p.id, "variables": None} @pytest.mark.asyncio @@ -111,11 +107,7 @@ async def test_json_with_variables(): variables = [{"name": "VAR1", "value": "VAL1"}] p = Project(project_id=str(uuid4()), variables=variables) - assert p.asdict() == { - "name": p.name, - "project_id": p.id, - "variables": variables - } + assert p.asdict() == {"name": p.name, "project_id": p.id, "variables": variables} @pytest.mark.asyncio @@ -123,7 +115,7 @@ async def test_node_working_directory(node, projects_dir): directory = projects_dir p = Project(project_id=str(uuid4())) - assert p.node_working_directory(node) == os.path.join(directory, p.id, 'project-files', node.module_name, node.id) + assert p.node_working_directory(node) == os.path.join(directory, p.id, "project-files", node.module_name, node.id) assert os.path.exists(p.node_working_directory(node)) @@ -132,7 +124,7 @@ async def test_node_working_path(node, projects_dir): directory = projects_dir p = Project(project_id=str(uuid4())) - assert p.node_working_path(node) == os.path.join(directory, p.id, 'project-files', node.module_name, node.id) + assert p.node_working_path(node) == os.path.join(directory, p.id, "project-files", node.module_name, node.id) # after this execution directory structure should not be created assert not os.path.exists(p.node_working_path(node)) @@ -193,14 +185,8 @@ async def test_list_files(): files = await project.list_files() assert files == [ - { - "path": "test.txt", - "md5sum": "ad0234829205b9033196ba818f7a872b" - }, - { - "path": os.path.join("vm-1", "dynamips", "test.bin"), - "md5sum": "098f6bcd4621d373cade4e832627b4f6" - } + {"path": "test.txt", "md5sum": "ad0234829205b9033196ba818f7a872b"}, + {"path": os.path.join("vm-1", "dynamips", "test.bin"), "md5sum": "098f6bcd4621d373cade4e832627b4f6"}, ] diff --git a/tests/compute/test_project_manager.py b/tests/compute/test_project_manager.py index 1d6b50372..fe52e8e22 100644 --- a/tests/compute/test_project_manager.py +++ b/tests/compute/test_project_manager.py @@ -24,12 +24,12 @@ from gns3server.compute.project_manager import ProjectManager def test_create_project(): pm = ProjectManager.instance() - project = pm.create_project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f') - assert project == pm.get_project('00010203-0405-0607-0809-0a0b0c0d0e0f') + project = pm.create_project(project_id="00010203-0405-0607-0809-0a0b0c0d0e0f") + assert project == pm.get_project("00010203-0405-0607-0809-0a0b0c0d0e0f") def test_project_not_found(): pm = ProjectManager.instance() with pytest.raises(ComputeNotFoundError): - pm.get_project('00010203-0405-0607-0809-000000000000') + pm.get_project("00010203-0405-0607-0809-000000000000") diff --git a/tests/compute/test_statistics.py b/tests/compute/test_statistics.py index 8fb721adc..f7c993674 100644 --- a/tests/compute/test_statistics.py +++ b/tests/compute/test_statistics.py @@ -9,13 +9,15 @@ def test_compute_statistics_preserves_load_average_precision() -> None: swap = SimpleNamespace(total=1024, free=512, used=512, percent=50) disk = SimpleNamespace(total=16384, used=4096, free=12288, percent=25) - with patch("gns3server.api.routes.compute.compute.CpuPercent.get", return_value=10), \ - patch("gns3server.api.routes.compute.compute.get_cpu_model", return_value="Test CPU"), \ - patch("gns3server.api.routes.compute.compute.psutil.virtual_memory", return_value=memory), \ - patch("gns3server.api.routes.compute.compute.psutil.swap_memory", return_value=swap), \ - patch("gns3server.api.routes.compute.compute.psutil.disk_usage", return_value=disk), \ - patch("gns3server.api.routes.compute.compute.psutil.cpu_count", return_value=4), \ - patch("gns3server.api.routes.compute.compute.psutil.getloadavg", return_value=(1.25, 2.5, 3.75)): + with ( + patch("gns3server.api.routes.compute.compute.CpuPercent.get", return_value=10), + patch("gns3server.api.routes.compute.compute.get_cpu_model", return_value="Test CPU"), + patch("gns3server.api.routes.compute.compute.psutil.virtual_memory", return_value=memory), + patch("gns3server.api.routes.compute.compute.psutil.swap_memory", return_value=swap), + patch("gns3server.api.routes.compute.compute.psutil.disk_usage", return_value=disk), + patch("gns3server.api.routes.compute.compute.psutil.cpu_count", return_value=4), + patch("gns3server.api.routes.compute.compute.psutil.getloadavg", return_value=(1.25, 2.5, 3.75)), + ): statistics = compute_statistics() assert statistics["load_average"] == [1.25, 2.5, 3.75] diff --git a/tests/compute/ubridge/test_hypervisor.py b/tests/compute/ubridge/test_hypervisor.py index 7cba76a2c..1ee5a555a 100644 --- a/tests/compute/ubridge/test_hypervisor.py +++ b/tests/compute/ubridge/test_hypervisor.py @@ -50,6 +50,7 @@ def _make(transport, tmp_path, monkeypatch, node_id="abc123", host="127.0.0.1"): # __init__: transport selection # --------------------------------------------------------------------------- + def test_init_unix_creates_socket_dir_and_path(tmp_path, monkeypatch): hyp = _make("unix", tmp_path, monkeypatch, node_id="abc123") @@ -84,6 +85,7 @@ def test_init_tcp_sets_host_port(tmp_path, monkeypatch): # _build_command + endpoint # --------------------------------------------------------------------------- + def test_build_command_unix(tmp_path, monkeypatch): hyp = _make("unix", tmp_path, monkeypatch) @@ -134,6 +136,7 @@ def test_endpoint_tcp(tmp_path, monkeypatch): # stop: AF_UNIX socket cleanup # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_stop_unlinks_unix_socket(tmp_path, monkeypatch): @@ -163,6 +166,7 @@ async def test_stop_tcp_has_no_socket_to_unlink(tmp_path, monkeypatch): # version requirement # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_requires_ubridge_1_2_3(tmp_path, monkeypatch): @@ -194,6 +198,7 @@ async def test_accepts_ubridge_1_2_3(tmp_path, monkeypatch): # start: fail-fast on an immediately-exiting uBridge # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_start_detects_immediate_exit(tmp_path, monkeypatch): # An unsupported flag (e.g. -U on an old ubridge) makes the process exit at @@ -203,8 +208,10 @@ async def test_start_detects_immediate_exit(tmp_path, monkeypatch): proc = MagicMock() proc.pid = 1234 proc.returncode = 2 # already exited - with patch.object(Hypervisor, "_check_ubridge_version", new_callable=AsyncMock), \ - patch("asyncio.create_subprocess_exec", new_callable=AsyncMock, return_value=proc): + with ( + patch.object(Hypervisor, "_check_ubridge_version", new_callable=AsyncMock), + patch("asyncio.create_subprocess_exec", new_callable=AsyncMock, return_value=proc), + ): with pytest.raises(UbridgeError, match="exited immediately"): await hyp.start() @@ -216,7 +223,9 @@ async def test_start_proceeds_when_process_keeps_running(tmp_path, monkeypatch): proc = MagicMock() proc.pid = 1234 proc.returncode = None # still running - with patch.object(Hypervisor, "_check_ubridge_version", new_callable=AsyncMock), \ - patch("asyncio.create_subprocess_exec", new_callable=AsyncMock, return_value=proc): + with ( + patch.object(Hypervisor, "_check_ubridge_version", new_callable=AsyncMock), + patch("asyncio.create_subprocess_exec", new_callable=AsyncMock, return_value=proc), + ): await hyp.start() # must NOT raise assert hyp._process is proc diff --git a/tests/compute/virtualbox/test_virtualbox_manager.py b/tests/compute/virtualbox/test_virtualbox_manager.py index 54d7e9f1f..48315245c 100644 --- a/tests/compute/virtualbox/test_virtualbox_manager.py +++ b/tests/compute/virtualbox/test_virtualbox_manager.py @@ -76,12 +76,14 @@ def test_vboxmanage_path(manager, config, tmpdir): @pytest.mark.asyncio async def test_list_vms(manager): - vm_list = ['"Windows 8.1" {27b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}', - '"Carriage', - 'Return" {27b4d095-ff5f-4ac4-bb9d-5f2c7861c3f3}', - '', - '"" {42b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}', - '"Linux Microcore 4.7.1" {ccd8c50b-c172-457d-99fa-dd69371ede0e}'] + vm_list = [ + '"Windows 8.1" {27b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}', + '"Carriage', + 'Return" {27b4d095-ff5f-4ac4-bb9d-5f2c7861c3f3}', + "", + '"" {42b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}', + '"Linux Microcore 4.7.1" {ccd8c50b-c172-457d-99fa-dd69371ede0e}', + ] async def execute_mock(cmd, args): if cmd == "list": @@ -96,7 +98,4 @@ async def test_list_vms(manager): with asyncio_patch("gns3server.compute.virtualbox.VirtualBox.execute") as mock: mock.side_effect = execute_mock vms = await manager.list_vms() - assert vms == [ - {"vmname": "Windows 8.1", "ram": 512}, - {"vmname": "Linux Microcore 4.7.1", "ram": 256} - ] + assert vms == [{"vmname": "Windows 8.1", "ram": 512}, {"vmname": "Linux Microcore 4.7.1", "ram": 256}] diff --git a/tests/compute/virtualbox/test_virtualbox_vm.py b/tests/compute/virtualbox/test_virtualbox_vm.py index 5303834e3..8bf127be0 100644 --- a/tests/compute/virtualbox/test_virtualbox_vm.py +++ b/tests/compute/virtualbox/test_virtualbox_vm.py @@ -99,7 +99,9 @@ async def test_vm_invalid_virtualbox_api_version(compute_project, manager): @pytest.mark.asyncio async def test_vm_adapter_add_nio_binding_adapter_not_exist(vm, manager, free_console_port): - nio = manager.create_nio({"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"} + ) with pytest.raises(VirtualBoxError): await vm.adapter_add_nio_binding(15, nio) diff --git a/tests/compute/vmware/test_vmware_vm.py b/tests/compute/vmware/test_vmware_vm.py index 631f07e17..61b53b843 100644 --- a/tests/compute/vmware/test_vmware_vm.py +++ b/tests/compute/vmware/test_vmware_vm.py @@ -58,7 +58,9 @@ async def test_json(vm, tmpdir, compute_project): async def test_start_capture(vm, tmpdir, manager, free_console_port): output_file = str(tmpdir / "test.pcap") - nio = manager.create_nio({"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"} + ) vm.adapters = 1 await vm.adapter_add_nio_binding(0, nio) await vm.start_capture(0, output_file) @@ -69,7 +71,9 @@ async def test_start_capture(vm, tmpdir, manager, free_console_port): async def test_stop_capture(vm, tmpdir, manager, free_console_port): output_file = str(tmpdir / "test.pcap") - nio = manager.create_nio({"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"} + ) vm.adapters = 1 await vm.adapter_add_nio_binding(0, nio) await vm.start_capture(0, output_file) diff --git a/tests/compute/vpcs/test_vpcs_vm.py b/tests/compute/vpcs/test_vpcs_vm.py index e85604f2d..65761cbd7 100644 --- a/tests/compute/vpcs/test_vpcs_vm.py +++ b/tests/compute/vpcs/test_vpcs_vm.py @@ -60,7 +60,10 @@ async def test_vm(compute_project, manager): @pytest.mark.asyncio async def test_vm_check_vpcs_version(vm): - with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.subprocess_check_output", return_value="Welcome to Virtual PC Simulator, version 0.9"): + with asyncio_patch( + "gns3server.compute.vpcs.vpcs_vm.subprocess_check_output", + return_value="Welcome to Virtual PC Simulator, version 0.9", + ): await vm._check_vpcs_version() assert vm._vpcs_version == parse_version("0.9") @@ -68,7 +71,10 @@ async def test_vm_check_vpcs_version(vm): @pytest.mark.asyncio async def test_vm_check_vpcs_version_0_6_1(vm): - with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.subprocess_check_output", return_value="Welcome to Virtual PC Simulator, version 0.6.1"): + with asyncio_patch( + "gns3server.compute.vpcs.vpcs_vm.subprocess_check_output", + return_value="Welcome to Virtual PC Simulator, version 0.6.1", + ): await vm._check_vpcs_version() assert vm._vpcs_version == parse_version("0.6.1") @@ -76,9 +82,14 @@ async def test_vm_check_vpcs_version_0_6_1(vm): @pytest.mark.asyncio async def test_vm_invalid_vpcs_version(vm, manager): - with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.subprocess_check_output", return_value="Welcome to Virtual PC Simulator, version 0.1"): + with asyncio_patch( + "gns3server.compute.vpcs.vpcs_vm.subprocess_check_output", + return_value="Welcome to Virtual PC Simulator, version 0.1", + ): with pytest.raises(VPCSError): - nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) + nio = manager.create_nio( + {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}} + ) await vm.port_add_nio_binding(0, nio) await vm._check_vpcs_version() assert vm.name == "test" @@ -110,22 +121,25 @@ async def test_start(vm): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process) as mock_exec: with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.start_wrap_console"): await vm.start() - assert mock_exec.call_args[0] == (vm._vpcs_path(), - '-p', - str(vm._internal_console_port), - '-m', '1', - '-i', - '1', - '-F', - '-R', - '-s', - ANY, - '-c', - ANY, - '-t', - '127.0.0.1') + assert mock_exec.call_args[0] == ( + vm._vpcs_path(), + "-p", + str(vm._internal_console_port), + "-m", + "1", + "-i", + "1", + "-F", + "-R", + "-s", + ANY, + "-c", + ANY, + "-t", + "127.0.0.1", + ) assert vm.is_running() - assert vm.command_line == ' '.join(mock_exec.call_args[0]) + assert vm.command_line == " ".join(mock_exec.call_args[0]) (action, event, kwargs) = await queue.get(1) assert action == "node.updated" assert event == vm @@ -145,22 +159,27 @@ async def test_start_0_6_1(vm): with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.start_wrap_console"): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process) as mock_exec: - nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) + nio = VPCS.instance().create_nio( + {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}} + ) await vm.port_add_nio_binding(0, nio) await vm.start() - assert mock_exec.call_args[0] == (vm._vpcs_path(), - '-p', - str(vm._internal_console_port), - '-m', '1', - '-i', - '1', - '-F', - '-s', - ANY, - '-c', - ANY, - '-t', - '127.0.0.1') + assert mock_exec.call_args[0] == ( + vm._vpcs_path(), + "-p", + str(vm._internal_console_port), + "-m", + "1", + "-i", + "1", + "-F", + "-s", + ANY, + "-c", + ANY, + "-t", + "127.0.0.1", + ) assert vm.is_running() @@ -178,7 +197,9 @@ async def test_stop(vm): with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.start_wrap_console"): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) + nio = VPCS.instance().create_nio( + {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}} + ) await vm.port_add_nio_binding(0, nio) await vm.start() @@ -211,7 +232,9 @@ async def test_reload(vm): with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM._check_requirements", return_value=True): with asyncio_patch("gns3server.compute.vpcs.vpcs_vm.VPCSVM.start_wrap_console"): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) + nio = VPCS.instance().create_nio( + {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}} + ) await vm.port_add_nio_binding(0, nio) await vm.start() assert vm.is_running() @@ -227,7 +250,9 @@ async def test_reload(vm): @pytest.mark.asyncio async def test_add_nio_binding_udp(vm): - nio = VPCS.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) + nio = VPCS.instance().create_nio( + {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}} + ) await vm.port_add_nio_binding(0, nio) assert nio.lport == 4242 @@ -254,7 +279,7 @@ def test_update_startup_script(vm): content = "echo GNS3 VPCS\nip 192.168.1.2\n" vm.startup_script = content - filepath = os.path.join(vm.working_dir, 'startup.vpc') + filepath = os.path.join(vm.working_dir, "startup.vpc") assert os.path.exists(filepath) with open(filepath) as f: assert f.read() == content @@ -290,8 +315,8 @@ def test_get_startup_script_using_default_script(vm): # Reset script file location vm._script_file = None - filepath = os.path.join(vm.working_dir, 'startup.vpc') - with open(filepath, 'wb+') as f: + filepath = os.path.join(vm.working_dir, "startup.vpc") + with open(filepath, "wb+") as f: assert f.write(content.encode("utf-8")) assert vm.startup_script == content @@ -300,16 +325,16 @@ def test_get_startup_script_using_default_script(vm): def test_change_name(vm): - path = os.path.join(vm.working_dir, 'startup.vpc') + path = os.path.join(vm.working_dir, "startup.vpc") vm.name = "world" - with open(path, 'w+') as f: + with open(path, "w+") as f: f.write("set pcname world") vm.name = "hello" assert vm.name == "hello" with open(path) as f: assert f.read() == "set pcname hello" # Support when the name is not sync with config - with open(path, 'w+') as f: + with open(path, "w+") as f: f.write("set pcname alpha") vm.name = "beta" assert vm.name == "beta" diff --git a/tests/conftest.py b/tests/conftest.py index b0994ebb8..17acbb91a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,6 +54,7 @@ def pytest_configure(config): async def app() -> AsyncGenerator[Any, Any]: from gns3server.api.server import app as gns3app + yield gns3app @@ -101,9 +102,9 @@ async def base_client(app: FastAPI, db_session: AsyncSession) -> AsyncGenerator[ app.dependency_overrides[get_db_session] = _get_test_db async with AsyncClient( - base_url="http://test-api", - headers={"Content-Type": "application/json"}, - transport=ASGIWebSocketTransport(app=app) + base_url="http://test-api", + headers={"Content-Type": "application/json"}, + transport=ASGIWebSocketTransport(app=app), ) as async_client: yield async_client @@ -132,12 +133,7 @@ async def test_user(db_session: AsyncSession) -> User: async def test_compute(db_session: AsyncSession) -> Compute: new_compute = schemas.ComputeCreate( - compute_id=uuid.uuid4(), - protocol=Protocol.http, - host="localhost", - port=4242, - user="julien", - password="secure" + compute_id=uuid.uuid4(), protocol=Protocol.http, host="localhost", port=4242, user="julien", password="secure" ) compute_repo = ComputesRepository(db_session) @@ -344,10 +340,41 @@ def on_gns3vm(linux_platform): Mock the hostname to emulate the GNS3 VM """ - with patch("gns3server.utils.interfaces.interfaces", return_value=[ - {"name": "eth0", "special": False, "type": "ethernet", "ip_addresses": [], "status": "up", "speed": 1000, "mtu": 1500, "flags": ["up", "broadcast", "running", "multicast"]}, - {"name": "eth1", "special": False, "type": "ethernet", "ip_addresses": [], "status": "down", "speed": 0, "mtu": 1500, "flags": ["broadcast"]}, - {"name": "virbr0", "special": True, "type": "ethernet", "ip_addresses": [], "status": "up", "speed": 10000, "mtu": 1500, "flags": ["up", "broadcast", "running", "multicast"]}]): + with patch( + "gns3server.utils.interfaces.interfaces", + return_value=[ + { + "name": "eth0", + "special": False, + "type": "ethernet", + "ip_addresses": [], + "status": "up", + "speed": 1000, + "mtu": 1500, + "flags": ["up", "broadcast", "running", "multicast"], + }, + { + "name": "eth1", + "special": False, + "type": "ethernet", + "ip_addresses": [], + "status": "down", + "speed": 0, + "mtu": 1500, + "flags": ["broadcast"], + }, + { + "name": "virbr0", + "special": True, + "type": "ethernet", + "ip_addresses": [], + "status": "up", + "speed": 10000, + "mtu": 1500, + "flags": ["up", "broadcast", "running", "multicast"], + }, + ], + ): with patch("socket.gethostname", return_value="gns3vm"): yield @@ -356,6 +383,7 @@ def on_gns3vm(linux_platform): def ethernet_device(): import psutil + return sorted(psutil.net_if_addrs().keys())[0] @@ -367,7 +395,7 @@ def ubridge_path(config): path = config.settings.Server.ubridge_path os.makedirs(os.path.dirname(path), exist_ok=True) - open(path, 'w+').close() + open(path, "w+").close() return path @@ -384,27 +412,27 @@ def run_around_tests(monkeypatch, config, port_manager): config.settings.Controller.jwt_secret_key = DEFAULT_JWT_SECRET_KEY - secrets_dir = os.path.join(tmppath, 'secrets') + secrets_dir = os.path.join(tmppath, "secrets") os.makedirs(secrets_dir) config.settings.Server.secrets_dir = secrets_dir - projects_dir = os.path.join(tmppath, 'projects') + projects_dir = os.path.join(tmppath, "projects") os.makedirs(projects_dir) config.settings.Server.projects_path = projects_dir - symbols_dir = os.path.join(tmppath, 'symbols') + symbols_dir = os.path.join(tmppath, "symbols") os.makedirs(symbols_dir) config.settings.Server.symbols_path = symbols_dir - images_dir = os.path.join(tmppath, 'images') + images_dir = os.path.join(tmppath, "images") os.makedirs(images_dir) config.settings.Server.images_path = images_dir - appliances_dir = os.path.join(tmppath, 'appliances') + appliances_dir = os.path.join(tmppath, "appliances") os.makedirs(appliances_dir) config.settings.Server.appliances_path = appliances_dir - config.settings.Server.ubridge_path = os.path.join(tmppath, 'bin', 'ubridge') + config.settings.Server.ubridge_path = os.path.join(tmppath, "bin", "ubridge") # Prevent executions of the VM if we forgot to mock something config.settings.VirtualBox.vboxmanage_path = tmppath @@ -424,7 +452,10 @@ def run_around_tests(monkeypatch, config, port_manager): # test body) freezes the patched object into the importing module's namespace # forever, and a closed-over path would then point at a deleted directory in # every later test (order-dependent FileNotFoundError in psutil.disk_usage). - monkeypatch.setattr("gns3server.utils.path.get_default_project_directory", lambda *args: Config.instance().settings.Server.projects_path) + monkeypatch.setattr( + "gns3server.utils.path.get_default_project_directory", + lambda *args: Config.instance().settings.Server.projects_path, + ) # Force sys.platform to the original value. Because it seems not be restored correctly after each test sys.platform = sys.original_platform diff --git a/tests/controller/gns3vm/test_remote_gns3_vm.py b/tests/controller/gns3vm/test_remote_gns3_vm.py index b9a54c0fc..5b475a174 100644 --- a/tests/controller/gns3vm/test_remote_gns3_vm.py +++ b/tests/controller/gns3vm/test_remote_gns3_vm.py @@ -21,6 +21,7 @@ from gns3server.controller.gns3vm.remote_gns3_vm import RemoteGNS3VM from gns3server.controller.gns3vm.gns3_vm_error import GNS3VMError from pydantic import SecretStr + @pytest.fixture def gns3vm(controller): @@ -38,14 +39,16 @@ async def test_list(gns3vm, controller): @pytest.mark.asyncio async def test_start(gns3vm, controller): - await controller.add_compute("r1", - name="R1", - protocol="https", - host="r1.local", - port=8484, - user="hello", - password=SecretStr("world"), - connect=False) + await controller.add_compute( + "r1", + name="R1", + protocol="https", + host="r1.local", + port=8484, + user="hello", + password=SecretStr("world"), + connect=False, + ) gns3vm.vmname = "R1" await gns3vm.start() @@ -60,14 +63,16 @@ async def test_start(gns3vm, controller): @pytest.mark.asyncio async def test_start_invalid_vm(gns3vm, controller): - await controller.add_compute("r1", - connect=False, - name="R1", - protocol="https", - host="r1.local", - port=8484, - user="hello", - password=SecretStr("world")) + await controller.add_compute( + "r1", + connect=False, + name="R1", + protocol="https", + host="r1.local", + port=8484, + user="hello", + password=SecretStr("world"), + ) gns3vm.vmname = "R2" with pytest.raises(GNS3VMError): diff --git a/tests/controller/gns3vm/test_virtualbox_gns3_vm.py b/tests/controller/gns3vm/test_virtualbox_gns3_vm.py index 0a9be84fb..6ae3cc5a6 100644 --- a/tests/controller/gns3vm/test_virtualbox_gns3_vm.py +++ b/tests/controller/gns3vm/test_virtualbox_gns3_vm.py @@ -55,9 +55,11 @@ vcpfps=25 GuestMemoryBalloon=0 """ - with asyncio_patch("gns3server.controller.gns3vm.virtualbox_gns3_vm.VirtualBoxGNS3VM._execute", return_value=showvminfo) as mock: + with asyncio_patch( + "gns3server.controller.gns3vm.virtualbox_gns3_vm.VirtualBoxGNS3VM._execute", return_value=showvminfo + ) as mock: res = await gns3vm._look_for_interface("nat") - mock.assert_called_with('showvminfo', ['GNS3 VM', '--machinereadable']) + mock.assert_called_with("showvminfo", ["GNS3 VM", "--machinereadable"]) assert res == 2 # with asyncio_patch("gns3server.controller.gns3vm.virtualbox_gns3_vm.VirtualBoxGNS3VM._execute") as mock: @@ -71,6 +73,7 @@ GuestMemoryBalloon=0 async def test_cpu_vendor_id(gns3vm): from cpuinfo import get_cpu_info + cpu_info = await wait_run_in_executor(get_cpu_info) - vendor_id = cpu_info.get('vendor_id_raw') + vendor_id = cpu_info.get("vendor_id_raw") assert vendor_id # vendor id should not be empty diff --git a/tests/controller/gns3vm/test_vmware_gns3_vm.py b/tests/controller/gns3vm/test_vmware_gns3_vm.py index d0f661c1c..89d551e75 100644 --- a/tests/controller/gns3vm/test_vmware_gns3_vm.py +++ b/tests/controller/gns3vm/test_vmware_gns3_vm.py @@ -41,20 +41,19 @@ async def test_set_extra_options(gns3vm, vmx_path, windows_platform): gns3vm._vmx_path = vmx_path # when there is not an entry, we modify it - with open(vmx_path, 'w') as f: + with open(vmx_path, "w") as f: f.write("") await gns3vm._set_extra_options() - with open(vmx_path, 'r') as f: + with open(vmx_path, "r") as f: assert f.read() == 'vhv.enable = "TRUE"\n' # when there is an entry, we don't modify it - with open(vmx_path, 'w') as f: + with open(vmx_path, "w") as f: f.write('vhv.enable = "FALSE"\n') await gns3vm._set_extra_options() - with open(vmx_path, 'r') as f: + with open(vmx_path, "r") as f: assert f.read() == 'vhv.enable = "FALSE"\n' - diff --git a/tests/controller/test_appliance.py b/tests/controller/test_appliance.py index 74f5c3aec..969d90d54 100644 --- a/tests/controller/test_appliance.py +++ b/tests/controller/test_appliance.py @@ -54,9 +54,7 @@ XRD_V8 = { {"adapter_number": 0, "port_name": "MgmtEth0/RP0/CPU0/0"}, {"adapter_number": 1, "port_name": "Gi0/0/0/0"}, ], - "extra_configs": [ - {"target": "/firstboot.cfg", "content": "!\nend\n"} - ], + "extra_configs": [{"target": "/firstboot.cfg", "content": "!\nend\n"}], }, } ], @@ -141,8 +139,7 @@ def test_v8_docker_settings_require_image(): appliance = dict( XRD_V8, settings=[ - {"name": "only", "default": True, "template_type": "docker", - "template_properties": {"adapters": 2}}, + {"name": "only", "default": True, "template_type": "docker", "template_properties": {"adapters": 2}}, ], ) with pytest.raises(pydantic.ValidationError): @@ -158,8 +155,12 @@ def test_v8_template_properties_validated_against_template_type(): appliance = dict( XRD_V8, settings=[ - {"name": "only", "default": True, "template_type": "qemu", - "template_properties": {"ram": 512, "boot_priority": "zzz"}}, + { + "name": "only", + "default": True, + "template_type": "qemu", + "template_properties": {"ram": 512, "boot_priority": "zzz"}, + }, ], ) with pytest.raises(pydantic.ValidationError): @@ -170,8 +171,12 @@ def test_v8_qemu_kvm_property_validates(): appliance = dict( XRD_V8, settings=[ - {"name": "only", "default": True, "template_type": "qemu", - "template_properties": {"ram": 512, "kvm": "disable"}}, + { + "name": "only", + "default": True, + "template_type": "qemu", + "template_properties": {"ram": 512, "kvm": "disable"}, + }, ], ) model = ApplianceModel.model_validate(appliance) @@ -184,8 +189,12 @@ def test_v8_qemu_cpu_throttling_range(): qemu template (int, 0-800). """ - settings = {"name": "only", "default": True, "template_type": "qemu", - "template_properties": {"ram": 512, "cpu_throttling": 500}} + settings = { + "name": "only", + "default": True, + "template_type": "qemu", + "template_properties": {"ram": 512, "cpu_throttling": 500}, + } model = ApplianceModel.model_validate(dict(XRD_V8, settings=[settings])) assert model.settings[0].template_properties.cpu_throttling == 500 diff --git a/tests/controller/test_appliance_manager.py b/tests/controller/test_appliance_manager.py index 2df0cedc4..38c343181 100644 --- a/tests/controller/test_appliance_manager.py +++ b/tests/controller/test_appliance_manager.py @@ -75,8 +75,12 @@ async def test_install_docker_version_skips_image_resolution(monkeypatch): _FakeTemplatesService.created = [] appliance_data = _v8_appliance( [ - {"name": "default", "default": True, "template_type": "docker", - "template_properties": {"image": "xrd:latest"}}, + { + "name": "default", + "default": True, + "template_type": "docker", + "template_properties": {"image": "xrd:latest"}, + }, ], versions=[{"name": "1.0", "images": {"image": "xrd:1.0"}}], ) @@ -113,8 +117,12 @@ async def test_install_iou_version_maps_image_to_path(monkeypatch, tmp_path): _FakeTemplatesService.created = [] appliance_data = _v8_appliance( [ - {"name": "default", "default": True, "template_type": "iou", - "template_properties": {"ethernet_adapters": 4, "ram": 256}}, + { + "name": "default", + "default": True, + "template_type": "iou", + "template_properties": {"ethernet_adapters": 4, "ram": 256}, + }, ], versions=[{"name": "15.9", "images": {"image": "i86bi-linux-l3-15.9.bin"}}], ) @@ -141,8 +149,7 @@ async def test_install_iou_version_maps_image_to_path(monkeypatch, tmp_path): async def test_install_version_not_found(monkeypatch): manager = ApplianceManager() appliance_data = _v8_appliance( - [{"name": "only", "default": True, "template_type": "docker", - "template_properties": {"image": "xrd:latest"}}], + [{"name": "only", "default": True, "template_type": "docker", "template_properties": {"image": "xrd:latest"}}], versions=[{"name": "1.0", "images": {"image": "xrd:1.0"}}], ) appliance = Appliance("test.gns3a", appliance_data) diff --git a/tests/controller/test_appliance_to_template.py b/tests/controller/test_appliance_to_template.py index 0ce40cef4..a50338370 100644 --- a/tests/controller/test_appliance_to_template.py +++ b/tests/controller/test_appliance_to_template.py @@ -327,8 +327,7 @@ def test_v8_iou_version_images_mapped_to_path(): "name": "IOU L3", "category": "router", "settings": [ - {"name": "only", "template_type": "iou", - "template_properties": {"ethernet_adapters": 4, "ram": 256}}, + {"name": "only", "template_type": "iou", "template_properties": {"ethernet_adapters": 4, "ram": 256}}, ], } version = {"name": "15.9", "images": {"image": "i86bi-linux-l3-15.9.bin"}} @@ -346,8 +345,7 @@ def test_v8_dynamips_idlepc_from_version(): "name": "Cisco 7200", "category": "router", "settings": [ - {"name": "only", "template_type": "dynamips", - "template_properties": {"ram": 512, "platform": "c7200"}}, + {"name": "only", "template_type": "dynamips", "template_properties": {"ram": 512, "platform": "c7200"}}, ], } version = {"name": "12.4", "idlepc": "0x613080c0", "images": {"image": "c7200.bin"}} @@ -368,8 +366,7 @@ def test_v8_dynamips_settings_idlepc_precedence(): "name": "Cisco 7200", "category": "router", "settings": [ - {"name": "only", "template_type": "dynamips", - "template_properties": {"ram": 512, "idlepc": "0x6142da40"}}, + {"name": "only", "template_type": "dynamips", "template_properties": {"ram": 512, "idlepc": "0x6142da40"}}, ], } version = {"name": "12.4", "idlepc": "0x613080c0", "images": {"image": "c7200.bin"}} @@ -390,8 +387,11 @@ def test_v8_qemu_kvm_disable_forces_accel_tcg(): "name": "QEMU VM", "category": "guest", "settings": [ - {"name": "only", "template_type": "qemu", - "template_properties": {"ram": 512, "options": "-m 512", "kvm": "disable"}}, + { + "name": "only", + "template_type": "qemu", + "template_properties": {"ram": 512, "options": "-m 512", "kvm": "disable"}, + }, ], } @@ -407,8 +407,11 @@ def test_v8_qemu_kvm_allow_keeps_options(): "name": "QEMU VM", "category": "guest", "settings": [ - {"name": "only", "template_type": "qemu", - "template_properties": {"ram": 512, "options": "-m 512", "kvm": "allow"}}, + { + "name": "only", + "template_type": "qemu", + "template_properties": {"ram": 512, "options": "-m 512", "kvm": "allow"}, + }, ], } @@ -429,10 +432,17 @@ def test_v8_no_cross_type_inheritance(): "name": "Mixed", "category": "router", "settings": [ - {"name": "default qemu", "default": True, "template_type": "qemu", - "template_properties": {"ram": 2048, "adapters": 10}}, - {"name": "docker alt", "template_type": "docker", - "template_properties": {"image": "xrd:latest", "adapters": 2}}, + { + "name": "default qemu", + "default": True, + "template_type": "qemu", + "template_properties": {"ram": 2048, "adapters": 10}, + }, + { + "name": "docker alt", + "template_type": "docker", + "template_properties": {"image": "xrd:latest", "adapters": 2}, + }, ], } version = {"name": "1.0", "settings": "docker alt", "images": {"image": "xrd:1.0"}} @@ -456,10 +466,8 @@ def test_v8_multiple_defaults_inherit_first_same_type(): "name": "Multi", "category": "router", "settings": [ - {"name": "default one", "default": True, "template_type": "qemu", - "template_properties": {"ram": 1024}}, - {"name": "default two", "default": True, "template_type": "qemu", - "template_properties": {"ram": 2048}}, + {"name": "default one", "default": True, "template_type": "qemu", "template_properties": {"ram": 1024}}, + {"name": "default two", "default": True, "template_type": "qemu", "template_properties": {"ram": 2048}}, {"name": "alt", "template_type": "qemu", "template_properties": {"cpus": 2}}, ], } @@ -482,8 +490,7 @@ def test_v8_symbol_fallback_uses_effective_category(): "name": "Test", "category": "router", "settings": [ - {"name": "only", "template_type": "qemu", - "template_properties": {"ram": 512, "category": "guest"}}, + {"name": "only", "template_type": "qemu", "template_properties": {"ram": 512, "category": "guest"}}, ], } template = ApplianceToTemplate().new_template(appliance, None, "local") @@ -514,13 +521,16 @@ def test_v8_reserved_keys_cannot_be_overridden(): "name": "Test", "category": "router", "settings": [ - {"name": "only", "template_type": "docker", - "template_properties": { - "image": "xrd:latest", - "template_type": "qemu", - "compute_id": "evil-compute", - "version": "9.9", - }}, + { + "name": "only", + "template_type": "docker", + "template_properties": { + "image": "xrd:latest", + "template_type": "qemu", + "compute_id": "evil-compute", + "version": "9.9", + }, + }, ], } @@ -543,10 +553,8 @@ def test_v8_get_template_type(): "name": "Mixed", "category": "router", "settings": [ - {"name": "default", "default": True, "template_type": "qemu", - "template_properties": {"ram": 512}}, - {"name": "alt", "template_type": "docker", - "template_properties": {"image": "xrd:latest"}}, + {"name": "default", "default": True, "template_type": "qemu", "template_properties": {"ram": 512}}, + {"name": "alt", "template_type": "docker", "template_properties": {"image": "xrd:latest"}}, ], } diff --git a/tests/controller/test_compute.py b/tests/controller/test_compute.py index 671b8e7a0..04ed1d740 100644 --- a/tests/controller/test_compute.py +++ b/tests/controller/test_compute.py @@ -68,9 +68,9 @@ def test_getUrl(controller): def test_get_url(controller): compute = Compute("my_compute_id", protocol="https", host="localhost", port=84, controller=controller) - with patch('gns3server.controller.compute.Compute._getUrl', return_value="returned") as getURL: - assert compute.get_url("/test") == 'returned' - getURL.assert_called_once_with('/test') + with patch("gns3server.controller.compute.Compute._getUrl", return_value="returned") as getURL: + assert compute.get_url("/test") == "returned" + getURL.assert_called_once_with("/test") def test_host_ip(controller): @@ -85,7 +85,9 @@ def test_name(): assert c.name == "https://example.com:84" c = Compute("world", protocol="https", host="example.com", port=84, controller=MagicMock(), name="hello") assert c.name == "hello" - c = Compute("world", protocol="https", host="example.com", port=84, controller=MagicMock(), user="azertyuiopqsdfghjklkm") + c = Compute( + "world", protocol="https", host="example.com", port=84, controller=MagicMock(), user="azertyuiopqsdfghjklkm" + ) assert c.name == "https://azertyuiopq...@example.com:84" @@ -97,7 +99,16 @@ async def test_compute_httpQuery(compute): response.status = 200 await compute.post("/projects", {"a": "b"}) await compute.close() - mock.assert_called_with("POST", "https://example.com:84/v3/compute/projects", headers={'content-type': 'application/json'}, data=b'{"a": "b"}', auth=None, params=None, chunked=None, timeout=120) + mock.assert_called_with( + "POST", + "https://example.com:84/v3/compute/projects", + headers={"content-type": "application/json"}, + data=b'{"a": "b"}', + auth=None, + params=None, + chunked=None, + timeout=120, + ) assert compute._auth is None @@ -112,13 +123,22 @@ async def test_compute_httpQueryAuth(compute): compute.password = SecretStr("toor") await compute.post("/projects", {"a": "b"}) await compute.close() - mock.assert_called_with("POST", "https://example.com:84/v3/compute/projects", headers={'content-type': 'application/json'}, data=b'{"a": "b"}', auth=compute._auth, params=None, chunked=None, timeout=120) + mock.assert_called_with( + "POST", + "https://example.com:84/v3/compute/projects", + headers={"content-type": "application/json"}, + data=b'{"a": "b"}', + auth=compute._auth, + params=None, + chunked=None, + timeout=120, + ) assert compute._auth.login == "root" assert compute._auth.password == "toor" # @pytest.mark.asyncio -#async def test_compute_httpQueryNotConnected(compute, controller): +# async def test_compute_httpQueryNotConnected(compute, controller): # # controller._notification = MagicMock() # compute._connected = False @@ -136,7 +156,7 @@ async def test_compute_httpQueryAuth(compute): # @pytest.mark.asyncio -#async def test_compute_httpQueryNotConnectedGNS3vmNotRunning(compute, controller): +# async def test_compute_httpQueryNotConnectedGNS3vmNotRunning(compute, controller): # """ # We are not connected to the remote and it's a GNS3 VM. So we need to start it # """ @@ -172,7 +192,16 @@ async def test_compute_httpQueryNotConnectedInvalidVersion(compute): with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: with pytest.raises(ControllerError): await compute.post("/projects", {"a": "b"}) - mock.assert_any_call("GET", "https://example.com:84/v3/compute/capabilities", headers={'content-type': 'application/json'}, data=None, auth=None, params=None, chunked=None, timeout=120) + mock.assert_any_call( + "GET", + "https://example.com:84/v3/compute/capabilities", + headers={"content-type": "application/json"}, + data=None, + auth=None, + params=None, + chunked=None, + timeout=120, + ) await compute.close() @@ -181,12 +210,21 @@ async def test_compute_httpQueryNotConnectedNonGNS3Server(compute): compute._connected = False response = AsyncioMagicMock() - response.read = AsyncioMagicMock(return_value=b'Blocked by super antivirus') + response.read = AsyncioMagicMock(return_value=b"Blocked by super antivirus") response.status = 200 with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: with pytest.raises(ControllerError): await compute.post("/projects", {"a": "b"}) - mock.assert_any_call("GET", "https://example.com:84/v3/compute/capabilities", headers={'content-type': 'application/json'}, data=None, auth=None, params=None, chunked=None, timeout=120) + mock.assert_any_call( + "GET", + "https://example.com:84/v3/compute/capabilities", + headers={"content-type": "application/json"}, + data=None, + auth=None, + params=None, + chunked=None, + timeout=120, + ) await compute.close() @@ -195,12 +233,21 @@ async def test_compute_httpQueryNotConnectedNonGNS3Server2(compute): compute._connected = False response = AsyncioMagicMock() - response.read = AsyncioMagicMock(return_value=b'{}') + response.read = AsyncioMagicMock(return_value=b"{}") response.status = 200 with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: with pytest.raises(ControllerError): await compute.post("/projects", {"a": "b"}) - mock.assert_any_call("GET", "https://example.com:84/v3/compute/capabilities", headers={'content-type': 'application/json'}, data=None, auth=None, params=None, chunked=None, timeout=120) + mock.assert_any_call( + "GET", + "https://example.com:84/v3/compute/capabilities", + headers={"content-type": "application/json"}, + data=None, + auth=None, + params=None, + chunked=None, + timeout=120, + ) @pytest.mark.asyncio @@ -234,22 +281,32 @@ async def test_compute_httpQuery_project(compute): response = MagicMock() with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: response.status = 200 - with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + with patch("gns3server.controller.project.Project.emit_controller_notification") as mock_notification: project = Project(name="Test") mock_notification.assert_called() await compute.post("/projects", project) - mock.assert_called_with("POST", "https://example.com:84/v3/compute/projects", headers={'content-type': 'application/json'}, data=json.dumps(project.asdict()), auth=None, params=None, chunked=None, timeout=120) + mock.assert_called_with( + "POST", + "https://example.com:84/v3/compute/projects", + headers={"content-type": "application/json"}, + data=json.dumps(project.asdict()), + auth=None, + params=None, + chunked=None, + timeout=120, + ) await compute.close() + # FIXME: https://github.com/aio-libs/aiohttp/issues/2525 # @pytest.mark.asyncio -#async def test_connectNotification(compute): +# async def test_connectNotification(compute): # # ws_mock = AsyncioMagicMock() # call = 0 # # @pytest.mark.asyncio -#async def receive(): +# async def receive(): # nonlocal call # call += 1 # if call == 1: @@ -282,7 +339,7 @@ async def test_compute_httpQuery_project(compute): # call = 0 # # @pytest.mark.asyncio -#async def receive(): +# async def receive(): # nonlocal call # call += 1 # if call == 1: @@ -306,6 +363,7 @@ async def test_compute_httpQuery_project(compute): # assert args[1]["memory_usage_percent"] == 80.7 # assert args[1]["cpu_usage_percent"] == 35.7 + @pytest.mark.asyncio async def test_json(compute): @@ -322,14 +380,7 @@ async def test_json(compute): "disk_usage_percent": 0, "connected": True, "last_error": None, - "capabilities": { - "version": "", - "platform": "", - "cpus": 0, - "memory": 0, - "disk_size": 0, - "node_types": [] - } + "capabilities": {"version": "", "platform": "", "cpus": 0, "memory": 0, "disk_size": 0, "node_types": []}, } assert compute.asdict(topology_dump=True) == { "compute_id": "my_compute_id", @@ -347,7 +398,9 @@ async def test_downloadFile(project, compute): response.status = 200 with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: await compute.download_file(project, "test/titi") - mock.assert_called_with("GET", "https://example.com:84/v3/compute/projects/{}/files/test/titi".format(project.id), auth=None) + mock.assert_called_with( + "GET", "https://example.com:84/v3/compute/projects/{}/files/test/titi".format(project.id), auth=None + ) await compute.close() @@ -382,7 +435,16 @@ async def test_forward_get(compute): response.status = 200 with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: await compute.forward("GET", "qemu", "images") - mock.assert_called_with("GET", "https://example.com:84/v3/compute/qemu/images", headers={'content-type': 'application/json'}, data=None, auth=None, params=None, chunked=None, timeout=None) + mock.assert_called_with( + "GET", + "https://example.com:84/v3/compute/qemu/images", + headers={"content-type": "application/json"}, + data=None, + auth=None, + params=None, + chunked=None, + timeout=None, + ) await compute.close() @@ -405,7 +467,16 @@ async def test_forward_post(compute): response.status = 200 with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: await compute.forward("POST", "qemu", "img", data={"id": 42}) - mock.assert_called_with("POST", "https://example.com:84/v3/compute/qemu/img", headers={'content-type': 'application/json'}, data=b'{"id": 42}', auth=None, params=None, chunked=None, timeout=None) + mock.assert_called_with( + "POST", + "https://example.com:84/v3/compute/qemu/img", + headers={"content-type": "application/json"}, + data=b'{"id": 42}', + auth=None, + params=None, + chunked=None, + timeout=None, + ) await compute.close() @@ -418,7 +489,16 @@ async def test_list_files(project, compute): response.status = 200 with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: assert await compute.list_files(project) == res - mock.assert_any_call("GET", "https://example.com:84/v3/compute/projects/{}/files".format(project.id), headers={'content-type': 'application/json'}, data=None, auth=None, params=None, chunked=None, timeout=None) + mock.assert_any_call( + "GET", + "https://example.com:84/v3/compute/projects/{}/files".format(project.id), + headers={"content-type": "application/json"}, + data=None, + auth=None, + params=None, + chunked=None, + timeout=None, + ) await compute.close() @@ -432,7 +512,7 @@ async def test_interfaces(compute): "mac_address": "00:50:56:c0:00:63", "name": "vmnet99", "netmask": "255.255.255.0", - "type": "ethernet" + "type": "ethernet", } ] response = AsyncioMagicMock() @@ -440,7 +520,16 @@ async def test_interfaces(compute): response.status = 200 with asyncio_patch("aiohttp.ClientSession.request", return_value=response) as mock: assert await compute.interfaces() == res - mock.assert_any_call("GET", "https://example.com:84/v3/compute/network/interfaces", headers={'content-type': 'application/json'}, data=None, auth=None, params=None, chunked=None, timeout=120) + mock.assert_any_call( + "GET", + "https://example.com:84/v3/compute/network/interfaces", + headers={"content-type": "application/json"}, + data=None, + auth=None, + params=None, + chunked=None, + timeout=120, + ) await compute.close() @@ -449,64 +538,32 @@ async def test_get_ip_on_same_subnet(controller): compute1 = Compute("compute1", host="192.168.1.1", controller=controller) compute1._interfaces_cache = [ - { - "ip_address": "127.0.0.1", - "netmask": "255.255.255.255" - }, - { - "ip_address": "192.168.2.1", - "netmask": "255.255.255.0" - }, - { - "ip_address": "192.168.1.1", - "netmask": "255.255.255.0" - }, + {"ip_address": "127.0.0.1", "netmask": "255.255.255.255"}, + {"ip_address": "192.168.2.1", "netmask": "255.255.255.0"}, + {"ip_address": "192.168.1.1", "netmask": "255.255.255.0"}, ] # Case 1 both host are on the same network compute2 = Compute("compute2", host="192.168.1.2", controller=controller) compute2._interfaces_cache = [ - { - "ip_address": "127.0.0.1", - "netmask": "255.255.255.255" - }, - { - "ip_address": "192.168.2.2", - "netmask": "255.255.255.0" - }, - { - "ip_address": "192.168.1.2", - "netmask": "255.255.255.0" - } + {"ip_address": "127.0.0.1", "netmask": "255.255.255.255"}, + {"ip_address": "192.168.2.2", "netmask": "255.255.255.0"}, + {"ip_address": "192.168.1.2", "netmask": "255.255.255.0"}, ] assert await compute1.get_ip_on_same_subnet(compute2) == ("192.168.1.1", "192.168.1.2") # Case 2 compute2 host is on a different network but a common interface is available compute2 = Compute("compute2", host="127.0.0.1", controller=controller) compute2._interfaces_cache = [ - { - "ip_address": "127.0.0.1", - "netmask": "255.255.255.255" - }, - { - "ip_address": "192.168.4.2", - "netmask": "255.255.255.0" - }, - { - "ip_address": "192.168.1.2", - "netmask": "255.255.255.0" - } + {"ip_address": "127.0.0.1", "netmask": "255.255.255.255"}, + {"ip_address": "192.168.4.2", "netmask": "255.255.255.0"}, + {"ip_address": "192.168.1.2", "netmask": "255.255.255.0"}, ] assert await compute1.get_ip_on_same_subnet(compute2) == ("192.168.1.1", "192.168.1.2") - #No common interface + # No common interface compute2 = Compute("compute2", host="127.0.0.1", controller=controller) - compute2._interfaces_cache = [ - { - "ip_address": "127.0.0.1", - "netmask": "255.255.255.255" - } - ] + compute2._interfaces_cache = [{"ip_address": "127.0.0.1", "netmask": "255.255.255.255"}] with pytest.raises(ValueError): await compute1.get_ip_on_same_subnet(compute2) @@ -514,26 +571,14 @@ async def test_get_ip_on_same_subnet(controller): compute2 = Compute("compute2", host="192.168.1.2", controller=controller) compute1 = Compute("compute1", host="192.168.2.1", controller=controller) compute1._interfaces_cache = [ - { - "ip_address": "127.0.0.1", - "netmask": "255.255.255.255" - }, - { - "ip_address": "169.254.1.1", - "netmask": "255.255.0.0" - }, + {"ip_address": "127.0.0.1", "netmask": "255.255.255.255"}, + {"ip_address": "169.254.1.1", "netmask": "255.255.0.0"}, ] compute2._interfaces_cache = [ - { - "ip_address": "127.0.0.1", - "netmask": "255.255.255.255" - }, - { - "ip_address": "169.254.2.1", - "netmask": "255.255.0.0" - }, + {"ip_address": "127.0.0.1", "netmask": "255.255.255.255"}, + {"ip_address": "169.254.2.1", "netmask": "255.255.0.0"}, ] - assert await compute1.get_ip_on_same_subnet(compute2) == ('192.168.2.1', '192.168.1.2') + assert await compute1.get_ip_on_same_subnet(compute2) == ("192.168.2.1", "192.168.1.2") class FakeWebSocket: @@ -573,7 +618,12 @@ async def test_connect_notification_poison_frame_autoreconnects(compute, monkeyp emit_mock = MagicMock() monkeypatch.setattr(compute._controller.notification, "controller_emit", emit_mock) frames = [ - _text_frame({"action": "ping", "event": {"cpu_usage_percent": 10.0, "memory_usage_percent": 20.0, "disk_usage_percent": 30.0}}), + _text_frame( + { + "action": "ping", + "event": {"cpu_usage_percent": 10.0, "memory_usage_percent": 20.0, "disk_usage_percent": 30.0}, + } + ), _text_frame({"event": {"poison": True}}), # missing "action": raises KeyError in the receive loop ] session = MagicMock() @@ -590,6 +640,7 @@ async def test_connect_notification_poison_frame_autoreconnects(compute, monkeyp async def fake_connect(): compute._reconnect_attempted = True + monkeypatch.setattr(compute, "connect", fake_connect) # must not raise despite the poison frame diff --git a/tests/controller/test_controller.py b/tests/controller/test_controller.py index 412a9b019..18edb22ee 100644 --- a/tests/controller/test_controller.py +++ b/tests/controller/test_controller.py @@ -120,7 +120,9 @@ async def test_load_projects_skip_unexpected_errors(controller, projects_dir): with open(os.path.join(projects_dir, "broken_project", "broken.gns3"), "w+") as f: f.write("") - with asyncio_patch("gns3server.controller.Controller.load_project", side_effect=Exception("boom")) as mock_load_project: + with asyncio_patch( + "gns3server.controller.Controller.load_project", side_effect=Exception("boom") + ) as mock_load_project: await controller.load_projects() mock_load_project.assert_called_with(os.path.join(projects_dir, "broken_project", "broken.gns3"), load=False) @@ -340,16 +342,12 @@ async def test_getProject(controller): @pytest.mark.asyncio async def test_start(controller): - controller.gns3vm.settings = { - "enable": False, - "engine": "vmware", - "vmname": "GNS3 VM" - } + controller.gns3vm.settings = {"enable": False, "engine": "vmware", "vmname": "GNS3 VM"} - #with asyncio_patch("gns3server.controller.compute.Compute.connect") as mock: + # with asyncio_patch("gns3server.controller.compute.Compute.connect") as mock: with asyncio_patch("gns3server.controller.Controller._install_builtin_disks", return_value=[]): await controller.start() - #assert mock.called + # assert mock.called assert len(controller.computes) == 1 # Local compute is created assert controller.computes["local"].name == f"{socket.gethostname()} (controller)" @@ -360,11 +358,7 @@ async def test_start_vm(controller): Start the controller with a GNS3 VM """ - controller.gns3vm.settings = { - "enable": True, - "engine": "vmware", - "vmname": "GNS3 VM" - } + controller.gns3vm.settings = {"enable": True, "engine": "vmware", "vmname": "GNS3 VM"} with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.start") as mock: with asyncio_patch("gns3server.controller.gns3vm.GNS3VM._check_network"): @@ -392,12 +386,7 @@ async def test_stop_vm(controller): Stop GNS3 VM if configured """ - controller.gns3vm.settings = { - "enable": True, - "engine": "vmware", - "when_exit": "stop", - "vmname": "GNS3 VM" - } + controller.gns3vm.settings = {"enable": True, "engine": "vmware", "when_exit": "stop", "vmname": "GNS3 VM"} controller.gns3vm.current_engine().running = True with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.stop") as mock: @@ -411,12 +400,7 @@ async def test_suspend_vm(controller): Suspend GNS3 VM if configured """ - controller.gns3vm.settings = { - "enable": True, - "engine": "vmware", - "when_exit": "suspend", - "vmname": "GNS3 VM" - } + controller.gns3vm.settings = {"enable": True, "engine": "vmware", "when_exit": "suspend", "vmname": "GNS3 VM"} controller.gns3vm.current_engine().running = True with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.suspend") as mock: @@ -430,12 +414,7 @@ async def test_keep_vm(controller): Keep GNS3 VM if configured """ - controller.gns3vm.settings = { - "enable": True, - "engine": "vmware", - "when_exit": "keep", - "vmname": "GNS3 VM" - } + controller.gns3vm.settings = {"enable": True, "engine": "vmware", "when_exit": "keep", "vmname": "GNS3 VM"} controller.gns3vm.current_engine().running = True with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.suspend") as mock: @@ -457,18 +436,18 @@ async def test_get_free_project_name(controller): async def test_install_base_configs(controller, config, tmpdir): config.settings.Server.configs_path = str(tmpdir) - with open(str(tmpdir / 'iou_l2_base_startup-config.txt'), 'w+') as f: - f.write('test') + with open(str(tmpdir / "iou_l2_base_startup-config.txt"), "w+") as f: + f.write("test") await controller._install_base_configs() - assert os.path.exists(str(tmpdir / 'iou_l3_base_startup-config.txt')) + assert os.path.exists(str(tmpdir / "iou_l3_base_startup-config.txt")) # the IOL docker base config ships with the server too (referenced by the # GNS3_IOL_STARTUP_CONFIG knob in the documented template) - assert os.path.exists(str(tmpdir / 'iol-xe-base.txt')) + assert os.path.exists(str(tmpdir / "iol-xe-base.txt")) # Check is the file has not been overwritten - with open(str(tmpdir / 'iou_l2_base_startup-config.txt')) as f: - assert f.read() == 'test' + with open(str(tmpdir / "iou_l2_base_startup-config.txt")) as f: + assert f.read() == "test" @pytest.mark.asyncio @@ -486,8 +465,8 @@ async def test_install_base_configs(controller, config, tmpdir): "empty200G.qcow2", "empty250G.qcow2", "empty500G.qcow2", - "empty1T.qcow2" - ] + "empty1T.qcow2", + ], ) async def test_install_builtin_disks(controller, config, tmpdir, builtin_disk): @@ -500,17 +479,12 @@ async def test_install_builtin_disks(controller, config, tmpdir, builtin_disk): @pytest.mark.asyncio async def test_appliances(controller, config, tmpdir): - my_appliance = { - "name": "My Appliance", - "status": "stable" - } - with open(str(tmpdir / "my_appliance.gns3a"), 'w+') as f: + my_appliance = {"name": "My Appliance", "status": "stable"} + with open(str(tmpdir / "my_appliance.gns3a"), "w+") as f: json.dump(my_appliance, f) # A broken appliance - my_appliance = { - "name": "Broken" - } - with open(str(tmpdir / "my_appliance2.gns3a"), 'w+') as f: + my_appliance = {"name": "Broken"} + with open(str(tmpdir / "my_appliance2.gns3a"), "w+") as f: json.dump(my_appliance, f) config.settings.Server.appliances_path = str(tmpdir) @@ -624,8 +598,12 @@ async def test_find_projects_skips_unreadable_topology(controller): project = await controller.add_project(name="Broken") await project.add_node( - compute, "n1", None, node_type="vpcs", - template_id=str(uuid.uuid4()), properties={"hda_disk_image": "lost.qcow2"}, + compute, + "n1", + None, + node_type="vpcs", + template_id=str(uuid.uuid4()), + properties={"hda_disk_image": "lost.qcow2"}, ) await project.close() os.remove(project.topology_file) diff --git a/tests/controller/test_drawing.py b/tests/controller/test_drawing.py index f8ea464a4..cd7946e10 100644 --- a/tests/controller/test_drawing.py +++ b/tests/controller/test_drawing.py @@ -55,7 +55,7 @@ def test_json(project): "z": i.z, "locked": i.locked, "svg": i.svg, - "rotation": i.rotation + "rotation": i.rotation, } assert i.asdict(topology_dump=True) == { "drawing_id": i.id, @@ -64,7 +64,7 @@ def test_json(project): "z": i.z, "rotation": i.rotation, "locked": i.locked, - "svg": i.svg + "svg": i.svg, } @@ -99,7 +99,7 @@ def test_image_base64(project): If image are embed as base 64 we need to dump them on disk """ - svg = "\n\n" + svg = '\n\n' drawing = Drawing(project, None, svg=svg) assert drawing._svg == "8418154b760b4e8023650e04c4992e24.png" @@ -113,10 +113,12 @@ def test_image_svg(project): Large SVG are dump on disk """ - svg = "\n" + svg = ( + '\n' + ) for i in range(0, 1000): - svg += "" + svg += '' svg += "" drawing = Drawing(project, None, svg=svg) diff --git a/tests/controller/test_export_project.py b/tests/controller/test_export_project.py index f3128e9d8..709f8ad07 100644 --- a/tests/controller/test_export_project.py +++ b/tests/controller/test_export_project.py @@ -59,7 +59,7 @@ async def node(controller, project): @pytest.mark.asyncio async def write_file(path, z): - with open(path, 'wb') as f: + with open(path, "wb") as f: async for chunk in z: f.write(chunk) @@ -85,7 +85,7 @@ async def test_export(tmpdir, project): f.write("AAA") # The .gns3 should be renamed project.gns3 in order to simplify import - with open(os.path.join(path, "test.gns3"), 'w+') as f: + with open(os.path.join(path, "test.gns3"), "w+") as f: data = { "topology": { "computes": [ @@ -94,52 +94,53 @@ async def test_export(tmpdir, project): "host": "127.0.0.1", "name": "Remote 1", "port": 8001, - "protocol": "http" + "protocol": "http", } ], "nodes": [ { "compute_id": "6b7149c8-7d6e-4ca0-ab6b-daa8ab567be0", "node_type": "dynamips", - "properties": { - "image": "test.image" - } + "properties": {"image": "test.image"}, } - ] + ], } } json.dump(data, f) - with open(os.path.join(path, "vm-1", "dynamips", "test"), 'w+') as f: + with open(os.path.join(path, "vm-1", "dynamips", "test"), "w+") as f: f.write("HELLO") - with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), 'w+') as f: + with open(os.path.join(path, "vm-1", "dynamips", "test_log.txt"), "w+") as f: f.write("LOG") os.makedirs(os.path.join(path, "vm-1", "dynamips", "empty-dir")) os.makedirs(os.path.join(path, "project-files", "snapshots")) - with open(os.path.join(path, "project-files", "snapshots", "test"), 'w+') as f: + with open(os.path.join(path, "project-files", "snapshots", "test"), "w+") as f: f.write("WORLD") os.symlink("/tmp/anywhere", os.path.join(path, "vm-1", "dynamips", "symlink")) with aiozipstream.ZipFile() as z: - with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),): + with patch( + "gns3server.compute.Dynamips.get_images_directory", + return_value=str(tmpdir / "IOS"), + ): await export_project(z, project, str(tmpdir), include_images=False) - await write_file(str(tmpdir / 'zipfile.zip'), z) + await write_file(str(tmpdir / "zipfile.zip"), z) - with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + with zipfile.ZipFile(str(tmpdir / "zipfile.zip")) as myzip: with myzip.open("vm-1/dynamips/test") as myfile: content = myfile.read() assert content == b"HELLO" - assert 'test.gns3' not in myzip.namelist() - assert 'project.gns3' in myzip.namelist() - assert 'vm-1/dynamips/empty-dir/' in myzip.namelist() - assert 'project-files/snapshots/test' not in myzip.namelist() - assert 'vm-1/dynamips/test_log.txt' not in myzip.namelist() - assert 'images/IOS/test.image' not in myzip.namelist() + assert "test.gns3" not in myzip.namelist() + assert "project.gns3" in myzip.namelist() + assert "vm-1/dynamips/empty-dir/" in myzip.namelist() + assert "project-files/snapshots/test" not in myzip.namelist() + assert "vm-1/dynamips/test_log.txt" not in myzip.namelist() + assert "images/IOS/test.image" not in myzip.namelist() - assert 'vm-1/dynamips/symlink' in myzip.namelist() - zip_info = myzip.getinfo('vm-1/dynamips/symlink') + assert "vm-1/dynamips/symlink" in myzip.namelist() + zip_info = myzip.getinfo("vm-1/dynamips/symlink") assert stat.S_ISLNK(zip_info.external_attr >> 16) with myzip.open("project.gns3") as myfile: @@ -194,17 +195,9 @@ async def test_export_disallow_running(tmpdir, project, node): path = project.path - topology = { - "topology": { - "nodes": [ - { - "node_type": "dynamips" - } - ] - } - } + topology = {"topology": {"nodes": [{"node_type": "dynamips"}]}} - with open(os.path.join(path, "test.gns3"), 'w+') as f: + with open(os.path.join(path, "test.gns3"), "w+") as f: json.dump(topology, f) node._status = "started" @@ -221,17 +214,9 @@ async def test_export_disallow_some_type(tmpdir, project): path = project.path - topology = { - "topology": { - "nodes": [ - { - "node_type": "vmware" - } - ] - } - } + topology = {"topology": {"nodes": [{"node_type": "vmware"}]}} - with open(os.path.join(path, "test.gns3"), 'w+') as f: + with open(os.path.join(path, "test.gns3"), "w+") as f: json.dump(topology, f) with pytest.raises(ControllerError): @@ -241,19 +226,8 @@ async def test_export_disallow_some_type(tmpdir, project): await export_project(z, project, str(tmpdir), allow_all_nodes=True) # VirtualBox is always disallowed - topology = { - "topology": { - "nodes": [ - { - "node_type": "virtualbox", - "properties": { - "linked_clone": True - } - } - ] - } - } - with open(os.path.join(path, "test.gns3"), 'w+') as f: + topology = {"topology": {"nodes": [{"node_type": "virtualbox", "properties": {"linked_clone": True}}]}} + with open(os.path.join(path, "test.gns3"), "w+") as f: json.dump(topology, f) with pytest.raises(ControllerError): with aiozipstream.ZipFile() as z: @@ -271,30 +245,20 @@ async def test_export_fix_path(tmpdir, project): topology = { "topology": { "nodes": [ - { - "properties": { - "image": "/tmp/c3725-adventerprisek9-mz.124-25d.image" - }, - "node_type": "dynamips" - }, - { - "properties": { - "image": "gns3/webterm:lastest" - }, - "node_type": "docker" - } + {"properties": {"image": "/tmp/c3725-adventerprisek9-mz.124-25d.image"}, "node_type": "dynamips"}, + {"properties": {"image": "gns3/webterm:lastest"}, "node_type": "docker"}, ] } } - with open(os.path.join(path, "test.gns3"), 'w+') as f: + with open(os.path.join(path, "test.gns3"), "w+") as f: json.dump(topology, f) with aiozipstream.ZipFile() as z: await export_project(z, project, str(tmpdir)) - await write_file(str(tmpdir / 'zipfile.zip'), z) + await write_file(str(tmpdir / "zipfile.zip"), z) - with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + with zipfile.ZipFile(str(tmpdir / "zipfile.zip")) as myzip: with myzip.open("project.gns3") as myfile: content = myfile.read().decode() topology = json.loads(content) @@ -313,28 +277,20 @@ async def test_export_with_images(tmpdir, project): with open(str(tmpdir / "IOS" / "test.image"), "w+") as f: f.write("AAA") - topology = { - "topology": { - "nodes": [ - { - "properties": { - "image": "test.image" - }, - "node_type": "dynamips" - } - ] - } - } + topology = {"topology": {"nodes": [{"properties": {"image": "test.image"}, "node_type": "dynamips"}]}} - with open(os.path.join(path, "test.gns3"), 'w+') as f: + with open(os.path.join(path, "test.gns3"), "w+") as f: json.dump(topology, f) with aiozipstream.ZipFile() as z: - with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),): + with patch( + "gns3server.compute.Dynamips.get_images_directory", + return_value=str(tmpdir / "IOS"), + ): await export_project(z, project, str(tmpdir), include_images=True) - await write_file(str(tmpdir / 'zipfile.zip'), z) + await write_file(str(tmpdir / "zipfile.zip"), z) - with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + with zipfile.ZipFile(str(tmpdir / "zipfile.zip")) as myzip: myzip.getinfo("images/IOS/test.image") @@ -345,7 +301,7 @@ async def test_export_keep_compute_ids(tmpdir, project): in the file """ - with open(os.path.join(project.path, "test.gns3"), 'w+') as f: + with open(os.path.join(project.path, "test.gns3"), "w+") as f: data = { "topology": { "computes": [ @@ -354,24 +310,19 @@ async def test_export_keep_compute_ids(tmpdir, project): "host": "127.0.0.1", "name": "Remote 1", "port": 8001, - "protocol": "http" + "protocol": "http", } ], - "nodes": [ - { - "compute_id": "6b7149c8-7d6e-4ca0-ab6b-daa8ab567be0", - "node_type": "vpcs" - } - ] + "nodes": [{"compute_id": "6b7149c8-7d6e-4ca0-ab6b-daa8ab567be0", "node_type": "vpcs"}], } } json.dump(data, f) with aiozipstream.ZipFile() as z: await export_project(z, project, str(tmpdir), keep_compute_ids=True) - await write_file(str(tmpdir / 'zipfile.zip'), z) + await write_file(str(tmpdir / "zipfile.zip"), z) - with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + with zipfile.ZipFile(str(tmpdir / "zipfile.zip")) as myzip: with myzip.open("project.gns3") as myfile: topo = json.loads(myfile.read().decode())["topology"] assert topo["nodes"][0]["compute_id"] == "6b7149c8-7d6e-4ca0-ab6b-daa8ab567be0" @@ -404,36 +355,25 @@ async def test_export_images_from_vm(tmpdir, project): mock_response.status = 200 compute.download_image = AsyncioMagicMock(return_value=mock_response) - project._project_created_on_compute.add(compute) path = project.path os.makedirs(os.path.join(path, "vm-1", "dynamips")) topology = { - "topology": { - "nodes": [ - { - "compute_id": "vm", - "properties": { - "image": "test.image" - }, - "node_type": "dynamips" - } - ] - } + "topology": {"nodes": [{"compute_id": "vm", "properties": {"image": "test.image"}, "node_type": "dynamips"}]} } # The .gns3 should be renamed project.gns3 in order to simplify import - with open(os.path.join(path, "test.gns3"), 'w+') as f: + with open(os.path.join(path, "test.gns3"), "w+") as f: f.write(json.dumps(topology)) with aiozipstream.ZipFile() as z: await export_project(z, project, str(tmpdir), include_images=True) assert compute.list_files.called - await write_file(str(tmpdir / 'zipfile.zip'), z) + await write_file(str(tmpdir / "zipfile.zip"), z) - with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: + with zipfile.ZipFile(str(tmpdir / "zipfile.zip")) as myzip: with myzip.open("vm-1/dynamips/test") as myfile: content = myfile.read() assert content == b"HELLO" @@ -446,7 +386,7 @@ async def test_export_images_from_vm(tmpdir, project): @pytest.mark.asyncio async def test_export_with_ignoring_snapshots(tmpdir, project): - with open(os.path.join(project.path, "test.gns3"), 'w+') as f: + with open(os.path.join(project.path, "test.gns3"), "w+") as f: data = { "topology": { "computes": [ @@ -455,27 +395,22 @@ async def test_export_with_ignoring_snapshots(tmpdir, project): "host": "127.0.0.1", "name": "Remote 1", "port": 8001, - "protocol": "http" + "protocol": "http", } ], - "nodes": [ - { - "compute_id": "6b7149c8-7d6e-4ca0-ab6b-daa8ab567be0", - "node_type": "vpcs" - } - ] + "nodes": [{"compute_id": "6b7149c8-7d6e-4ca0-ab6b-daa8ab567be0", "node_type": "vpcs"}], } } json.dump(data, f) # create snapshot directory - snapshots_dir = os.path.join(project.path, 'snapshots') + snapshots_dir = os.path.join(project.path, "snapshots") os.makedirs(snapshots_dir) - Path(os.path.join(snapshots_dir, 'snap.gns3project')).touch() + Path(os.path.join(snapshots_dir, "snap.gns3project")).touch() with aiozipstream.ZipFile() as z: await export_project(z, project, str(tmpdir), keep_compute_ids=True) - await write_file(str(tmpdir / 'zipfile.zip'), z) + await write_file(str(tmpdir / "zipfile.zip"), z) - with zipfile.ZipFile(str(tmpdir / 'zipfile.zip')) as myzip: - assert not os.path.join('snapshots', 'snap.gns3project') in [f.filename for f in myzip.filelist] + with zipfile.ZipFile(str(tmpdir / "zipfile.zip")) as myzip: + assert not os.path.join("snapshots", "snap.gns3project") in [f.filename for f in myzip.filelist] diff --git a/tests/controller/test_gns3vm.py b/tests/controller/test_gns3vm.py index 041cd44ce..48af40493 100644 --- a/tests/controller/test_gns3vm.py +++ b/tests/controller/test_gns3vm.py @@ -52,10 +52,15 @@ def dummy_gns3vm(controller, dummy_engine): async def test_list(controller): vm = GNS3VM(controller) - with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.list", return_value=[{"vmname": "test", "vmx_path": "test"}]): + with asyncio_patch( + "gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.list", + return_value=[{"vmname": "test", "vmx_path": "test"}], + ): res = await vm.list("vmware") assert res == [{"vmname": "test"}] # Information specific to VMware is stripped - with asyncio_patch("gns3server.controller.gns3vm.virtualbox_gns3_vm.VirtualBoxGNS3VM.list", return_value=[{"vmname": "test"}]): + with asyncio_patch( + "gns3server.controller.gns3vm.virtualbox_gns3_vm.VirtualBoxGNS3VM.list", return_value=[{"vmname": "test"}] + ): res = await vm.list("virtualbox") assert res == [{"vmname": "test"}] with pytest.raises(NotImplementedError): @@ -73,11 +78,7 @@ async def test_json(controller): async def test_update_settings(controller): vm = GNS3VM(controller) - vm.settings = { - "enable": True, - "engine": "vmware", - "vmname": "GNS3 VM" - } + vm.settings = {"enable": True, "engine": "vmware", "vmname": "GNS3 VM"} with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.start"): with asyncio_patch("gns3server.controller.gns3vm.GNS3VM._check_network"): diff --git a/tests/controller/test_import_project.py b/tests/controller/test_import_project.py index 0d5cff67f..b7d7ea042 100644 --- a/tests/controller/test_import_project.py +++ b/tests/controller/test_import_project.py @@ -43,18 +43,17 @@ async def test_import_project(tmpdir, controller): "name": "test", "auto_open": True, "auto_start": True, - "topology": { - }, - "version": "2.0.0" + "topology": {}, + "version": "2.0.0", } - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) - with open(str(tmpdir / "b.png"), 'w+') as f: + with open(str(tmpdir / "b.png"), "w+") as f: f.write("B") zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") myzip.write(str(tmpdir / "b.png"), "b.png") myzip.write(str(tmpdir / "b.png"), "project-files/dynamips/test") @@ -90,19 +89,13 @@ async def test_import_project_override(projects_dir, controller): tmpdir = Path(projects_dir) / "override-location" tmpdir.mkdir(parents=True, exist_ok=True) project_id = str(uuid.uuid4()) - topology = { - "project_id": project_id, - "name": "test", - "topology": { - }, - "version": "2.0.0" - } + topology = {"project_id": project_id, "name": "test", "topology": {}, "version": "2.0.0"} - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") with open(zip_path, "rb") as f: @@ -120,7 +113,7 @@ async def test_import_project_override(projects_dir, controller): async def write_file(path, z): - with open(path, 'wb') as f: + with open(path, "wb") as f: async for chunk in z: f.write(chunk) @@ -136,12 +129,11 @@ def export_project_with_symlink(tmpdir, controller): "name": "test", "auto_open": True, "auto_start": True, - "topology": { - }, - "version": "2.0.0" + "topology": {}, + "version": "2.0.0", } - with open(os.path.join(project.path, "project.gns3"), 'w+') as f: + with open(os.path.join(project.path, "project.gns3"), "w+") as f: json.dump(topology, f) os.makedirs(os.path.join(project.path, "vm1", "dynamips")) @@ -150,7 +142,10 @@ def export_project_with_symlink(tmpdir, controller): zip_path = str(tmpdir / "project.zip") with aiozipstream.ZipFile() as z: - with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),): + with patch( + "gns3server.compute.Dynamips.get_images_directory", + return_value=str(tmpdir / "IOS"), + ): await export_project(z, project, str(tmpdir), include_images=False) await write_file(zip_path, z) @@ -218,19 +213,13 @@ async def test_import_upgrade(tmpdir, controller): """ project_id = str(uuid.uuid4()) - topology = { - "project_id": str(uuid.uuid4()), - "name": "test", - "topology": { - }, - "version": "1.4.2" - } + topology = {"project_id": str(uuid.uuid4()), "name": "test", "topology": {}, "version": "1.4.2"} - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") with open(zip_path, "rb") as f: @@ -245,22 +234,16 @@ async def test_import_upgrade(tmpdir, controller): async def test_import_with_images(config, tmpdir, controller): project_id = str(uuid.uuid4()) - topology = { - "project_id": str(uuid.uuid4()), - "name": "test", - "topology": { - }, - "version": "2.0.0" - } + topology = {"project_id": str(uuid.uuid4()), "name": "test", "topology": {}, "version": "2.0.0"} - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) - with open(str(tmpdir / "test.image"), 'w+') as f: + with open(str(tmpdir / "test.image"), "w+") as f: f.write("B") zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") myzip.write(str(tmpdir / "test.image"), "images/IOS/test.image") @@ -287,27 +270,20 @@ async def test_import_iou_linux_no_vm(linux_platform, tmpdir, controller): "name": "test", "type": "topology", "topology": { - "nodes": [ - { - "compute_id": "local", - "node_type": "iou", - "name": "test", - "properties": {} - } - ], + "nodes": [{"compute_id": "local", "node_type": "iou", "name": "test", "properties": {}}], "links": [], "computes": [], - "drawings": [] + "drawings": [], }, "revision": 5, - "version": "2.0.0" + "version": "2.0.0", } - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") with open(zip_path, "rb") as f: @@ -338,22 +314,22 @@ async def test_import_iou_linux_with_vm(linux_platform, tmpdir, controller): "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "node_type": "iou", "name": "test", - "properties": {} + "properties": {}, } ], "links": [], "computes": [], - "drawings": [] + "drawings": [], }, "revision": 5, - "version": "2.0.0" + "version": "2.0.0", } - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") with open(zip_path, "rb") as f: @@ -384,22 +360,22 @@ async def test_import_nat_non_linux(windows_platform, tmpdir, controller): "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "node_type": "nat", "name": "test", - "properties": {} + "properties": {}, } ], "links": [], "computes": [], - "drawings": [] + "drawings": [], }, "revision": 5, - "version": "2.0.0" + "version": "2.0.0", } - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") with open(zip_path, "rb") as f: @@ -430,41 +406,43 @@ async def test_import_iou_non_linux(windows_platform, tmpdir, controller): "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "node_type": "iou", "name": "test", - "properties": {} + "properties": {}, }, - { - "compute_id": "local", - "node_type": "vpcs", - "name": "test2", - "properties": {} - } + {"compute_id": "local", "node_type": "vpcs", "name": "test2", "properties": {}}, ], "links": [], "computes": [], - "drawings": [] + "drawings": [], }, "revision": 5, - "version": "2.0.0" + "version": "2.0.0", } - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") with open(zip_path, "rb") as f: with asyncio_patch("gns3server.controller.import_project._move_files_to_compute") as mock: project = await import_project(controller, project_id, f) - controller._computes["vm"].post.assert_called_with('/projects', data={'name': 'test', 'project_id': project_id}) + controller._computes["vm"].post.assert_called_with( + "/projects", data={"name": "test", "project_id": project_id} + ) with open(os.path.join(project.path, "test.gns3")) as f: topo = json.load(f) assert topo["topology"]["nodes"][0]["compute_id"] == "vm" assert topo["topology"]["nodes"][1]["compute_id"] == "local" - mock.assert_called_with(controller._computes["vm"], project_id, project.path, os.path.join('project-files', 'iou', topo["topology"]["nodes"][0]['node_id'])) + mock.assert_called_with( + controller._computes["vm"], + project_id, + project.path, + os.path.join("project-files", "iou", topo["topology"]["nodes"][0]["node_id"]), + ) @pytest.mark.asyncio @@ -487,54 +465,40 @@ async def test_import_node_id(linux_platform, tmpdir, controller): "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "node_type": "iou", "name": "test", - "properties": {} + "properties": {}, }, { "compute_id": "local", "node_id": "c3ae286c-c81f-40d9-a2d0-5874b2f2478d", "node_type": "iou", "name": "test2", - "properties": {} - } + "properties": {}, + }, ], "links": [ { "link_id": "b570a150-c09f-47d9-8d32-9ca5b03234d6", "nodes": [ - { - "adapter_number": 0, - "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", - "port_number": 0 - }, - { - "adapter_number": 0, - "node_id": "c3ae286c-c81f-40d9-a2d0-5874b2f2478d", - "port_number": 0 - } - ] + {"adapter_number": 0, "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "port_number": 0}, + {"adapter_number": 0, "node_id": "c3ae286c-c81f-40d9-a2d0-5874b2f2478d", "port_number": 0}, + ], } ], "computes": [], "drawings": [ - { - "drawing_id": "08d665ba-e982-4d54-82b4-aa0c4d5ba6a3", - "rotation": 0, - "x": -210, - "y": -108, - "z": 0 - } - ] + {"drawing_id": "08d665ba-e982-4d54-82b4-aa0c4d5ba6a3", "rotation": 0, "x": -210, "y": -108, "z": 0} + ], }, "revision": 5, - "version": "2.0.0" + "version": "2.0.0", } - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) # Fake .gns3project zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") myzip.writestr("project-files/iou/0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b/startup.cfg", "test") myzip.writestr("project-files/iou/c3ae286c-c81f-40d9-a2d0-5874b2f2478d/startup.cfg", "test") @@ -545,20 +509,32 @@ async def test_import_node_id(linux_platform, tmpdir, controller): with open(os.path.join(project.path, "test.gns3")) as f: topo = json.load(f) # Node id should have change - assert topo["topology"]["nodes"][0]["node_id"] not in ["0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "c3ae286c-c81f-40d9-a2d0-5874b2f2478d"] + assert topo["topology"]["nodes"][0]["node_id"] not in [ + "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", + "c3ae286c-c81f-40d9-a2d0-5874b2f2478d", + ] # Link should have change link = topo["topology"]["links"][0] assert link["link_id"] != "b570a150-c09f-47d9-8d32-9ca5b03234d6" - assert link["nodes"][0]["node_id"] not in ["0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "c3ae286c-c81f-40d9-a2d0-5874b2f2478d"] + assert link["nodes"][0]["node_id"] not in [ + "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", + "c3ae286c-c81f-40d9-a2d0-5874b2f2478d", + ] # Drawing id should change assert topo["topology"]["drawings"][0]["drawing_id"] != "08d665ba-e982-4d54-82b4-aa0c4d5ba6a3" # Node files should have moved to the new node id - assert not os.path.exists(os.path.join(project.path, "project-files", "iou", "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "startup.cfg")) - assert not os.path.exists(os.path.join(project.path, "project-files", "iou", "c3ae286c-c81f-40d9-a2d0-5874b2f2478d", "startup.cfg")) - assert os.path.exists(os.path.join(project.path, "project-files", "iou", topo["topology"]["nodes"][0]["node_id"], "startup.cfg")) + assert not os.path.exists( + os.path.join(project.path, "project-files", "iou", "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "startup.cfg") + ) + assert not os.path.exists( + os.path.join(project.path, "project-files", "iou", "c3ae286c-c81f-40d9-a2d0-5874b2f2478d", "startup.cfg") + ) + assert os.path.exists( + os.path.join(project.path, "project-files", "iou", topo["topology"]["nodes"][0]["node_id"], "startup.cfg") + ) @pytest.mark.asyncio @@ -581,22 +557,22 @@ async def test_import_keep_compute_ids(windows_platform, tmpdir, controller): "node_id": "0fd3dd4d-dc93-4a04-a9b9-7396a9e22e8b", "node_type": "iou", "name": "test", - "properties": {} + "properties": {}, } ], "links": [], "computes": [], - "drawings": [] + "drawings": [], }, "revision": 5, - "version": "2.0.0" + "version": "2.0.0", } - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") with open(zip_path, "rb") as f: @@ -619,8 +595,18 @@ async def test_move_files_to_compute(tmpdir): with asyncio_patch("gns3server.controller.import_project._upload_file") as mock: await _move_files_to_compute(None, project_id, str(tmpdir), os.path.join("project-files", "docker")) - mock.assert_any_call(None, project_id, str(tmpdir / "project-files" / "docker" / "test"), os.path.join("project-files", "docker", "test")) - mock.assert_any_call(None, project_id, str(tmpdir / "project-files" / "docker" / "test2"), os.path.join("project-files", "docker", "test2")) + mock.assert_any_call( + None, + project_id, + str(tmpdir / "project-files" / "docker" / "test"), + os.path.join("project-files", "docker", "test"), + ) + mock.assert_any_call( + None, + project_id, + str(tmpdir / "project-files" / "docker" / "test2"), + os.path.join("project-files", "docker", "test2"), + ) assert not os.path.exists(str(tmpdir / "project-files" / "docker")) @@ -632,19 +618,13 @@ async def test_import_project_name_and_location(projects_dir, controller): tmpdir = Path(projects_dir) project_id = str(uuid.uuid4()) - topology = { - "project_id": str(uuid.uuid4()), - "name": "test", - "topology": { - }, - "version": "2.0.0" - } + topology = {"project_id": str(uuid.uuid4()), "name": "test", "topology": {}, "version": "2.0.0"} - with open(str(tmpdir / "project.gns3"), 'w+') as f: + with open(str(tmpdir / "project.gns3"), "w+") as f: json.dump(topology, f) zip_path = str(tmpdir / "project.zip") - with zipfile.ZipFile(zip_path, 'w') as myzip: + with zipfile.ZipFile(zip_path, "w") as myzip: myzip.write(str(tmpdir / "project.gns3"), "project.gns3") with open(zip_path, "rb") as f: diff --git a/tests/controller/test_link.py b/tests/controller/test_link.py index 0aa29b219..1cfcdb9bf 100644 --- a/tests/controller/test_link.py +++ b/tests/controller/test_link.py @@ -65,10 +65,10 @@ async def test_add_node(project, compute): "port": node1._ports[0], "adapter_number": 0, "port_number": 4, - 'label': { - 'text': '0/4', - 'style': 'font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;' - } + "label": { + "text": "0/4", + "style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;", + }, } ] assert project.dump.called @@ -205,32 +205,32 @@ async def test_json(project, compute): "node_id": node1.id, "adapter_number": 0, "port_number": 4, - 'label': { - 'text': '0/4', - 'style': 'font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;' - } + "label": { + "text": "0/4", + "style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;", + }, }, { "node_id": node2.id, "adapter_number": 1, "port_number": 3, - 'label': { - 'text': '1/3', - 'style': 'font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;' - } - } + "label": { + "text": "1/3", + "style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;", + }, + }, ], "filters": {}, "markers": {}, "show_filters_icon": True, "link_style": {}, "suspend": False, - 'wireshark': False, + "wireshark": False, "link_type": "ethernet", "capturing": False, "capture_file_name": None, "capture_file_path": None, - "capture_compute_id": None + "capture_compute_id": None, } assert link.asdict(topology_dump=True) == { "link_id": link.id, @@ -239,26 +239,26 @@ async def test_json(project, compute): "node_id": node1.id, "adapter_number": 0, "port_number": 4, - 'label': { - 'text': '0/4', - 'style': 'font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;' - } + "label": { + "text": "0/4", + "style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;", + }, }, { "node_id": node2.id, "adapter_number": 1, "port_number": 3, - 'label': { - 'text': '1/3', - 'style': 'font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;' - } - } + "label": { + "text": "1/3", + "style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;", + }, + }, ], "link_style": {}, "filters": {}, "markers": {}, "show_filters_icon": True, - "suspend": False + "suspend": False, } @@ -295,7 +295,6 @@ async def test_default_capture_file_name(project, compute): @pytest.mark.asyncio async def test_start_capture(link): - async def fake_reader(): return AsyncioBytesIO() @@ -355,16 +354,8 @@ async def test_update_filters(project, compute): link.update = AsyncioMagicMock() assert link._created - await link.update_filters({ - "packet_loss": [10], - "delay": [50, 10], - "frequency_drop": [0], - "bpf": [" \n "] - }) - assert link.filters == { - "packet_loss": [10], - "delay": [50, 10] - } + await link.update_filters({"packet_loss": [10], "delay": [50, 10], "frequency_drop": [0], "bpf": [" \n "]}) + assert link.filters == {"packet_loss": [10], "delay": [50, 10]} assert link.update.called @@ -404,14 +395,16 @@ async def test_update_link_style(project, compute): await link.add_node(node1, 0, 4) await link.add_node(node2, 1, 3) - await link.update_link_style({ - "color": "#00ff00", - "width": 3, - "type": 1, - "link_type": "flowchart", - "bezier_curviness": 150, - "flowchart_roundness": 20, - }) + await link.update_link_style( + { + "color": "#00ff00", + "width": 3, + "type": 1, + "link_type": "flowchart", + "bezier_curviness": 150, + "flowchart_roundness": 20, + } + ) assert link.asdict()["link_style"] == { "color": "#00ff00", diff --git a/tests/controller/test_marker.py b/tests/controller/test_marker.py index 7bacef1f2..ec23b5d2d 100644 --- a/tests/controller/test_marker.py +++ b/tests/controller/test_marker.py @@ -99,6 +99,7 @@ async def _make_link(project, port_cls=EthernetPort, node_types=("vpcs", "vpcs") # UDPLink.start_marker # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_start_marker_stores_entry(project): @@ -204,8 +205,9 @@ async def test_start_marker_rejects_duplicate(project): async def test_start_marker_rejects_invalid_bpf(project): link = await _make_link(project) - with patch("gns3server.controller.udp_link.validate_bpf_syntax", - return_value={"valid": False, "error": "bad expression"}): + with patch( + "gns3server.controller.udp_link.validate_bpf_syntax", return_value={"valid": False, "error": "bad expression"} + ): with pytest.raises(ControllerError): await link.start_marker("bad", "not a real bpf") @@ -214,6 +216,7 @@ async def test_start_marker_rejects_invalid_bpf(project): # UDPLink.stop_marker # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_stop_marker_removes(project): @@ -260,6 +263,7 @@ async def test_stop_marker_unknown_raises(project): # UDPLink.update_marker # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_update_marker_preserves_render_hints(project): """A partial update (bpf only) must not reset color/highlight_duration/tag.""" @@ -271,9 +275,9 @@ async def test_update_marker_preserves_render_hints(project): entry = link.markers["m"] assert entry["bpf"] == "tcp port 80" - assert entry["color"] == "#ff5722" # preserved - assert entry["highlight_duration"] == 800 # preserved - assert entry["tag"] == 1 # preserved + assert entry["color"] == "#ff5722" # preserved + assert entry["highlight_duration"] == 800 # preserved + assert entry["tag"] == 1 # preserved @pytest.mark.asyncio @@ -315,6 +319,7 @@ async def test_update_marker_inherited_bypass(project): # Link.inherit_marker + persistence # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_inherit_marker_creates_global_copy(project): @@ -408,8 +413,8 @@ async def test_load_marker_preserves_direction_and_highlight_duration(project): # The link survives (2 attached nodes); pull it back from the project. link = project._links[link_id] entry = link._markers["icmp"] - assert entry["direction"] == "rx" # dropped before the fix - assert entry["highlight_duration"] == 800 # dropped before the fix + assert entry["direction"] == "rx" # dropped before the fix + assert entry["highlight_duration"] == 800 # dropped before the fix assert entry["tag"] == 7 assert entry["color"] == "#ff5722" assert entry["capture_node_id"] == capture_node_id @@ -435,6 +440,7 @@ async def test_asdict_markers_runtime_vs_dump(project): # Project-level marker definitions # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_create_marker_definition_fans_out(project): @@ -553,23 +559,28 @@ async def test_apply_defs_to_new_link(project): async def test_create_marker_definition_validates_bpf_once(project): # A definition validates its BPF once (project layer); the inherited fan-out # to every link must NOT re-validate — no tcpdump subprocess per link. - with patch("gns3server.controller.project.validate_bpf_syntax", - return_value={"valid": True, "error": None}) as proj_val, \ - patch("gns3server.controller.udp_link.validate_bpf_syntax", - return_value={"valid": True, "error": None}) as link_val: + with ( + patch( + "gns3server.controller.project.validate_bpf_syntax", return_value={"valid": True, "error": None} + ) as proj_val, + patch( + "gns3server.controller.udp_link.validate_bpf_syntax", return_value={"valid": True, "error": None} + ) as link_val, + ): await _make_link(project) await _make_link(project) await project.create_marker_definition("arp", "arp") - assert proj_val.call_count == 1 # validated once at the def layer - assert link_val.call_count == 0 # fan-out skipped per-link validation + assert proj_val.call_count == 1 # validated once at the def layer + assert link_val.call_count == 0 # fan-out skipped per-link validation @pytest.mark.asyncio async def test_create_marker_definition_rejects_invalid_bpf(project): - with patch("gns3server.controller.project.validate_bpf_syntax", - return_value={"valid": False, "error": "syntax error"}): + with patch( + "gns3server.controller.project.validate_bpf_syntax", return_value={"valid": False, "error": "syntax error"} + ): with pytest.raises(ControllerError): await project.create_marker_definition("arp", "not a real bpf") assert "arp" not in project.marker_definitions @@ -579,25 +590,30 @@ async def test_create_marker_definition_rejects_invalid_bpf(project): async def test_update_marker_definition_skips_per_link_validation(project): # Updating a def's BPF validates once more (project); the per-link sync # (update_marker with inherited=True) must NOT re-validate. - with patch("gns3server.controller.project.validate_bpf_syntax", - return_value={"valid": True, "error": None}) as proj_val, \ - patch("gns3server.controller.udp_link.validate_bpf_syntax", - return_value={"valid": True, "error": None}) as link_val: + with ( + patch( + "gns3server.controller.project.validate_bpf_syntax", return_value={"valid": True, "error": None} + ) as proj_val, + patch( + "gns3server.controller.udp_link.validate_bpf_syntax", return_value={"valid": True, "error": None} + ) as link_val, + ): await _make_link(project) await _make_link(project) await project.create_marker_definition("arp", "arp") await project.update_marker_definition("arp", bpf="arp or rarp") - assert proj_val.call_count == 2 # once on create, once on update - assert link_val.call_count == 0 # sync skipped per-link validation + assert proj_val.call_count == 2 # once on create, once on update + assert link_val.call_count == 0 # sync skipped per-link validation @pytest.mark.asyncio async def test_start_marker_skips_validation_for_inherited(project): # An inherited marker rides an already-validated definition BPF, so # start_marker must not call validate_bpf_syntax. - with patch("gns3server.controller.udp_link.validate_bpf_syntax", - return_value={"valid": True, "error": None}) as link_val: + with patch( + "gns3server.controller.udp_link.validate_bpf_syntax", return_value={"valid": True, "error": None} + ) as link_val: link = await _make_link(project) await link.inherit_marker("arp", {"bpf": "arp"}) @@ -608,8 +624,9 @@ async def test_start_marker_skips_validation_for_inherited(project): @pytest.mark.asyncio async def test_start_marker_validates_for_private(project): # A private (non-inherited) marker still validates inline. - with patch("gns3server.controller.udp_link.validate_bpf_syntax", - return_value={"valid": True, "error": None}) as link_val: + with patch( + "gns3server.controller.udp_link.validate_bpf_syntax", return_value={"valid": True, "error": None} + ) as link_val: link = await _make_link(project) await link.start_marker("icmp", "icmp") @@ -636,6 +653,7 @@ async def test_markers_aggregation(project): # Direction clear/preserve semantics (sentinel _UNSET vs explicit None) # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_update_marker_clears_direction(project): # Explicit direction=None clears the filter back to "both directions" — @@ -685,6 +703,7 @@ async def test_update_marker_definition_clears_direction(project): # Capture-node routing + capability validation # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_pinned_marker_routes_only_to_chosen_node(project): # The marker rides only the pinned capture node's NIO; the far endpoint sees nothing. @@ -725,6 +744,7 @@ async def test_start_marker_rejects_non_capable_capture_node(project): # Part A/B: enabled reaches uBridge + instant per-filter toggle # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_markers_for_node_keeps_disabled_and_carries_enabled(project): # Part A: a disabled marker is NOT dropped from the NIO payload (so uBridge @@ -790,6 +810,7 @@ async def test_update_marker_ui_only_does_not_push(project): # Per-definition pause/resume (toggle every inherited global-{name} copy) # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_pause_marker_definition_toggles_copies_off(project): with _valid_bpf(): @@ -843,7 +864,7 @@ async def test_create_marker_definition_rejects_directional(project): @pytest.mark.asyncio async def test_create_marker_definition_allows_both(project): - await project.create_marker_definition("arp", "arp") # default both + await project.create_marker_definition("arp", "arp") # default both await project.create_marker_definition("icmp", "icmp", direction=None) assert project.marker_definitions["arp"]["direction"] is None assert project.marker_definitions["icmp"]["direction"] is None @@ -851,7 +872,7 @@ async def test_create_marker_definition_allows_both(project): @pytest.mark.asyncio async def test_update_marker_definition_rejects_directional(project): - await project.create_marker_definition("arp", "arp") # both + await project.create_marker_definition("arp", "arp") # both with pytest.raises(ControllerError): await project.update_marker_definition("arp", direction="tx") # omitted direction and explicit clear-to-both are both fine @@ -872,6 +893,4 @@ async def test_stop_marker_deletes_capture_pcap(project): capture.delete = AsyncioMagicMock() await link.stop_marker("icmp") - capture.delete.assert_called_once_with( - "/adapters/0/ports/0/markers/icmp", params={"link_id": link.id} - ) + capture.delete.assert_called_once_with("/adapters/0/ports/0/markers/icmp", params={"link_id": link.id}) diff --git a/tests/controller/test_marker_replay.py b/tests/controller/test_marker_replay.py index 19888bbd0..e958d3888 100644 --- a/tests/controller/test_marker_replay.py +++ b/tests/controller/test_marker_replay.py @@ -98,8 +98,9 @@ def _icmp_frame(): icmp = bytes([8, 0, 0, 0]) + struct.pack(">HHH", 1, 1, 0) + b"payload12" icmp = icmp[:2] + struct.pack(">H", _cksum(icmp)) + icmp[4:] - ip0 = struct.pack(">BBHHHBBH4s4s", 0x45, 0, 20 + len(icmp), 1, 0, 64, 1, 0, - bytes([10, 0, 0, 1]), bytes([10, 0, 0, 3])) + ip0 = struct.pack( + ">BBHHHBBH4s4s", 0x45, 0, 20 + len(icmp), 1, 0, 64, 1, 0, bytes([10, 0, 0, 1]), bytes([10, 0, 0, 3]) + ) ip = ip0[:10] + struct.pack(">H", _cksum(ip0)) + ip0[12:] return bytes.fromhex("0200000000020200000000010800") + ip + icmp @@ -108,8 +109,9 @@ def _tcp_syn_frame(): """A minimal TCP SYN (10.0.0.1:472 → 10.0.0.3:22).""" tcp = struct.pack(">HHIIBBHHH", 472, 22, 0, 0, 0x50, 0x02, 64240, 0, 0) - ip0 = struct.pack(">BBHHHBBH4s4s", 0x45, 0, 20 + len(tcp), 2, 0, 64, 6, 0, - bytes([10, 0, 0, 1]), bytes([10, 0, 0, 3])) + ip0 = struct.pack( + ">BBHHHBBH4s4s", 0x45, 0, 20 + len(tcp), 2, 0, 64, 6, 0, bytes([10, 0, 0, 1]), bytes([10, 0, 0, 3]) + ) ip = ip0[:10] + struct.pack(">H", _cksum(ip0)) + ip0[12:] tcp = tcp[:16] + struct.pack(">H", _cksum(ip0[12:] + tcp)) + tcp[18:] return bytes.fromhex("0200000000020200000000010800") + ip + tcp @@ -122,10 +124,17 @@ def _fake_project(tmp_path, markers, markers_dir=None): def _marker_entry(tag, enabled=True, node_id="node-1"): - return {"bpf": "icmp", "tag": tag, "enabled": enabled, "color": None, - "highlight_duration": None, "capture_node_id": node_id, - "direction": None, "data_link_type": "DLT_EN10MB", - "node_id": node_id} + return { + "bpf": "icmp", + "tag": tag, + "enabled": enabled, + "color": None, + "highlight_duration": None, + "capture_node_id": node_id, + "direction": None, + "data_link_type": "DLT_EN10MB", + "node_id": node_id, + } def _fake_columns(monkeypatch, mapping): @@ -142,8 +151,7 @@ def _fake_columns(monkeypatch, mapping): def _cols(src="10.0.0.1", dst="10.0.0.3", proto="ICMP", info="Echo (ping) request"): - return {"src": src, "dst": dst, "proto": proto, "info": info, - "bg": "ffffff", "fg": "000000"} + return {"src": src, "dst": dst, "proto": proto, "info": info, "bg": "ffffff", "fg": "000000"} class _FakeSession: @@ -180,17 +188,20 @@ def _pretend_sharkd(monkeypatch): # pcap scanning (engine-free backbone) # --------------------------------------------------------------------------- -class TestScanPcap: +class TestScanPcap: async def test_scans_frames_and_truncated_tail(self, tmp_path): pcap = tmp_path / "a.pcap" - _write_pcap(pcap, [ - (1693472000, 123456, b"x" * 60), - (1693472001, 654321, b"y" * 40), - ]) + _write_pcap( + pcap, + [ + (1693472000, 123456, b"x" * 60), + (1693472001, 654321, b"y" * 40), + ], + ) # Tear the final record in half: a snapshot mid-write must not raise. data = bytearray(pcap.read_bytes()) - pcap.write_bytes(data[:len(data) - 20]) + pcap.write_bytes(data[: len(data) - 20]) frames = scan_pcap_frames(str(pcap)) assert frames == [(1693472000, 123456, 60)] @@ -202,10 +213,13 @@ class TestScanPcap: async def test_read_frame_bytes_offsets(self, tmp_path): pcap = tmp_path / "b.pcap" - _write_pcap(pcap, [ - (100, 0, b"first" + b"0" * 55), # 60 bytes - (200, 0, b"second"), # 6 bytes - ]) + _write_pcap( + pcap, + [ + (100, 0, b"first" + b"0" * 55), # 60 bytes + (200, 0, b"second"), # 6 bytes + ], + ) assert read_frame_bytes(str(pcap), 2) == b"second".hex() assert read_frame_bytes(str(pcap), 1) == (b"first" + b"0" * 55).hex() assert read_frame_bytes(str(pcap), 3) is None @@ -221,18 +235,21 @@ class TestScanPcap: # Tag gate (engine-free — raised before any sharkd work) # --------------------------------------------------------------------------- -class TestGate: +class TestGate: async def test_unknown_tag_404(self, tmp_path): project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=1)}) with pytest.raises(ControllerNotFoundError): await build_timeline(project, tag=7) async def test_gate_409_while_capturing(self, tmp_path): - project = _fake_project(tmp_path, { - "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), - "linkB/icmp": _marker_entry(tag=7, enabled=True, node_id="n2"), - }) + project = _fake_project( + tmp_path, + { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + "linkB/icmp": _marker_entry(tag=7, enabled=True, node_id="n2"), + }, + ) with pytest.raises(ControllerError, match="linkB"): await build_timeline(project, tag=7) @@ -247,26 +264,38 @@ class TestGate: # Timeline assembly (columns injected — no engine needed) # --------------------------------------------------------------------------- -class TestTimeline: +class TestTimeline: async def test_merge_orders_by_ts_with_stable_tiebreak(self, tmp_path, monkeypatch): # Two sources, deliberately interleaved in time, colliding on one µs. - _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ - (1693472000, 500000, b"a" * 60), # t1 sourceA - (1693472002, 000000, b"a" * 60), # t3 sourceA - ]) - _write_pcap(tmp_path / "n2_linkB_icmp.pcap", [ - (1693472001, 000000, b"b" * 60), # t2 sourceB - (1693472002, 000000, b"b" * 60), # t3 sourceB — same µs as t3 sourceA - ]) - _fake_columns(monkeypatch, { - "n1_linkA_icmp.pcap": {1: _cols(), 2: _cols()}, - "n2_linkB_icmp.pcap": {1: _cols(src="10.0.0.2"), 2: _cols(src="10.0.0.2")}, - }) - project = _fake_project(tmp_path, { - "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), - "linkB/icmp": _marker_entry(tag=7, enabled=False, node_id="n2"), - }) + _write_pcap( + tmp_path / "n1_linkA_icmp.pcap", + [ + (1693472000, 500000, b"a" * 60), # t1 sourceA + (1693472002, 000000, b"a" * 60), # t3 sourceA + ], + ) + _write_pcap( + tmp_path / "n2_linkB_icmp.pcap", + [ + (1693472001, 000000, b"b" * 60), # t2 sourceB + (1693472002, 000000, b"b" * 60), # t3 sourceB — same µs as t3 sourceA + ], + ) + _fake_columns( + monkeypatch, + { + "n1_linkA_icmp.pcap": {1: _cols(), 2: _cols()}, + "n2_linkB_icmp.pcap": {1: _cols(src="10.0.0.2"), 2: _cols(src="10.0.0.2")}, + }, + ) + project = _fake_project( + tmp_path, + { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + "linkB/icmp": _marker_entry(tag=7, enabled=False, node_id="n2"), + }, + ) timeline = await build_timeline(project, tag=7) assert timeline["frame_count"] == 4 @@ -306,10 +335,14 @@ class TestTimeline: async def test_frame_list_is_uncapped(self, tmp_path, monkeypatch): # The list is the whole contract — no truncation flag, no buckets, # however many frames the tag holds. - _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ - (1693472000, 0, b"a" * 60), (1693472000, 500000, b"a" * 60), - (1693472001, 0, b"a" * 60), - ]) + _write_pcap( + tmp_path / "n1_linkA_icmp.pcap", + [ + (1693472000, 0, b"a" * 60), + (1693472000, 500000, b"a" * 60), + (1693472001, 0, b"a" * 60), + ], + ) _fake_columns(monkeypatch, {"n1_linkA_icmp.pcap": {1: _cols(), 2: _cols(), 3: _cols()}}) project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) @@ -321,11 +354,14 @@ class TestTimeline: async def test_filter_applies_before_count_and_slice(self, tmp_path, monkeypatch): # Three frames; the injected "matching set" (what a real engine would # return for the filter) contains only frames 1 and 3. - _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ - (1693472000, 0, b"a" * 60), - (1693472001, 0, b"a" * 60), - (1693472002, 0, b"a" * 60), - ]) + _write_pcap( + tmp_path / "n1_linkA_icmp.pcap", + [ + (1693472000, 0, b"a" * 60), + (1693472001, 0, b"a" * 60), + (1693472002, 0, b"a" * 60), + ], + ) _fake_columns(monkeypatch, {"n1_linkA_icmp.pcap": {1: _cols(), 3: _cols(proto="TCP")}}) project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) @@ -346,14 +382,20 @@ class TestLinkFilter: counting/slicing/bucketing; sources stay the full inventory.""" def _project(self, tmp_path, monkeypatch, columns_override=None): - _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ - (1693472000, 500000, b"a" * 60), - (1693472002, 000000, b"a" * 60), - ]) - _write_pcap(tmp_path / "n2_linkB_icmp.pcap", [ - (1693472001, 000000, b"b" * 60), - (1693472002, 000000, b"b" * 60), - ]) + _write_pcap( + tmp_path / "n1_linkA_icmp.pcap", + [ + (1693472000, 500000, b"a" * 60), + (1693472002, 000000, b"a" * 60), + ], + ) + _write_pcap( + tmp_path / "n2_linkB_icmp.pcap", + [ + (1693472001, 000000, b"b" * 60), + (1693472002, 000000, b"b" * 60), + ], + ) async def fake_columns(pcap, filter_expr): if columns_override is not None: @@ -362,10 +404,13 @@ class TestLinkFilter: return {1: _cols(src=src), 2: _cols(src=src)} monkeypatch.setattr(marker_replay, "_columns_for", fake_columns) - return _fake_project(tmp_path, { - "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), - "linkB/icmp": _marker_entry(tag=7, enabled=False, node_id="n2"), - }) + return _fake_project( + tmp_path, + { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + "linkB/icmp": _marker_entry(tag=7, enabled=False, node_id="n2"), + }, + ) async def test_link_narrows_before_count_and_slice(self, tmp_path, monkeypatch): timeline = await build_timeline(self._project(tmp_path, monkeypatch), tag=7, link_id="linkA") @@ -373,9 +418,7 @@ class TestLinkFilter: assert timeline["start"] == "1693472000.500000" assert [f["link_id"] for f in timeline["frames"]] == ["linkA", "linkA"] # sources stay the FULL inventory with engine-free totals. - assert sorted((s["link_id"], s["count"]) for s in timeline["sources"]) == [ - ("linkA", 2), ("linkB", 2) - ] + assert sorted((s["link_id"], s["count"]) for s in timeline["sources"]) == [("linkA", 2), ("linkB", 2)] async def test_unknown_link_is_empty_success(self, tmp_path, monkeypatch): timeline = await build_timeline(self._project(tmp_path, monkeypatch), tag=7, link_id="nope") @@ -403,35 +446,35 @@ class TestLinkFilter: async def test_query_frames_window_over_link_stream(self, tmp_path, monkeypatch): project = self._project(tmp_path, monkeypatch) - result = await query_frames(project, tag=7, ts="1693472002.000000", - window_ms=0, link_id="linkB") + result = await query_frames(project, tag=7, ts="1693472002.000000", window_ms=0, link_id="linkB") assert [f["link_id"] for f in result["frames"]] == ["linkB"] class TestQueryFrames: - def _project(self, tmp_path, monkeypatch): - _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ - (1693472000, 0, b"a" * 60), - (1693472000, 150000, b"a" * 60), - (1693472005, 0, b"a" * 60), - ]) + _write_pcap( + tmp_path / "n1_linkA_icmp.pcap", + [ + (1693472000, 0, b"a" * 60), + (1693472000, 150000, b"a" * 60), + (1693472005, 0, b"a" * 60), + ], + ) _fake_columns(monkeypatch, {"n1_linkA_icmp.pcap": {i: _cols() for i in (1, 2, 3)}}) return _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) async def test_window_inclusive_bounds(self, tmp_path, monkeypatch): - result = await query_frames(self._project(tmp_path, monkeypatch), tag=7, - ts="1693472000.000000", window_ms=150) + result = await query_frames(self._project(tmp_path, monkeypatch), tag=7, ts="1693472000.000000", window_ms=150) assert [f["ts"] for f in result["frames"]] == ["1693472000.000000", "1693472000.150000"] async def test_window_miss_is_empty_success(self, tmp_path, monkeypatch): - result = await query_frames(self._project(tmp_path, monkeypatch), tag=7, - ts="1693472001.000000", window_ms=100) + result = await query_frames(self._project(tmp_path, monkeypatch), tag=7, ts="1693472001.000000", window_ms=100) assert result == {"frames": []} async def test_limit_applies(self, tmp_path, monkeypatch): - result = await query_frames(self._project(tmp_path, monkeypatch), tag=7, - ts="1693472000.000000", window_ms=150, limit=1) + result = await query_frames( + self._project(tmp_path, monkeypatch), tag=7, ts="1693472000.000000", window_ms=150, limit=1 + ) assert len(result["frames"]) == 1 @@ -439,20 +482,30 @@ class TestQueryFrames: # Tree key renaming # --------------------------------------------------------------------------- -class TestRename: +class TestRename: async def test_renames_closed_key_set_and_drops_hf_id(self): node = { - "t": "proto", "l": "Time to Live: 64", "fn": "ip.ttl", - "f": "ip.ttl == 64", "h": [22, 1], "s": None, "g": False, + "t": "proto", + "l": "Time to Live: 64", + "fn": "ip.ttl", + "f": "ip.ttl == 64", + "h": [22, 1], + "s": None, + "g": False, "e": 8472, "n": [{"l": "nested", "h": [23, 2], "n": []}], } renamed = _rename_value(node) assert renamed == { - "element": "proto", "label": "Time to Live: 64", "name": "ip.ttl", - "filter_expr": "ip.ttl == 64", "pos": 22, "size": 1, - "expert": None, "generated": False, + "element": "proto", + "label": "Time to Live: 64", + "name": "ip.ttl", + "filter_expr": "ip.ttl == 64", + "pos": 22, + "size": 1, + "expert": None, + "generated": False, "children": [{"label": "nested", "pos": 23, "size": 2, "children": []}], } @@ -470,8 +523,8 @@ class TestRename: # sharkd sessions + engine-backed behaviour # --------------------------------------------------------------------------- -class TestSessions: +class TestSessions: async def test_missing_sharkd_raises_501_error(self, tmp_path): _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [(1693472000, 0, b"a" * 60)]) project = _fake_project(tmp_path, {"linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1")}) @@ -539,9 +592,7 @@ class TestSessions: pcap = tmp_path / "n1_linkA_icmp.pcap" _write_pcap(pcap, [(1693472000, 0, b"a" * 60)]) with patch.object(manager, "_spawn", side_effect=slow_spawn): - first, second = await asyncio.gather( - manager._acquire(str(pcap)), manager._acquire(str(pcap)) - ) + first, second = await asyncio.gather(manager._acquire(str(pcap)), manager._acquire(str(pcap))) assert first is second # concurrent requests share one spawn assert len(spawns) == 1 await manager.close_all() @@ -553,12 +604,12 @@ class TestSessions: # A process that never answers: the session must die (never serve the # late reply to a later request) instead of staying resident. proc = await asyncio.create_subprocess_exec( - "sleep", "60", - stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, - ) - session = marker_replay._SharkdSession( - str(pcap), str(tmp_path / "scratch"), "unused", proc, os.stat(str(pcap)) + "sleep", + "60", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, ) + session = marker_replay._SharkdSession(str(pcap), str(tmp_path / "scratch"), "unused", proc, os.stat(str(pcap))) with pytest.raises(SharkdError, match="timed out"): await session.rpc("frames", {}) assert session.alive() is False @@ -579,12 +630,12 @@ class TestSessions: pcap = tmp_path / "n1_linkA_icmp.pcap" _write_pcap(pcap, [(1693472000, 0, b"a" * 60)]) proc = await asyncio.create_subprocess_exec( - sys.executable, str(fake), - stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, - ) - session = marker_replay._SharkdSession( - str(pcap), str(tmp_path / "scratch"), "unused", proc, os.stat(str(pcap)) + sys.executable, + str(fake), + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, ) + session = marker_replay._SharkdSession(str(pcap), str(tmp_path / "scratch"), "unused", proc, os.stat(str(pcap))) with pytest.raises(SharkdError, match="id mismatch"): await session.rpc("frames", {}) assert session.alive() is False @@ -635,10 +686,13 @@ class TestSessions: @sharkd_present async def test_real_session_columns_and_filter(self, tmp_path): - _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ - (1693472000, 123456, _icmp_frame()), - (1693472001, 0, _tcp_syn_frame()), - ]) + _write_pcap( + tmp_path / "n1_linkA_icmp.pcap", + [ + (1693472000, 123456, _icmp_frame()), + (1693472001, 0, _tcp_syn_frame()), + ], + ) manager = marker_replay._get_manager() try: columns = await marker_replay._columns_for(str(tmp_path / "n1_linkA_icmp.pcap"), None) @@ -683,47 +737,51 @@ class TestSessions: class TestDecodeFrame: - def _project(self, tmp_path): - _write_pcap(tmp_path / "n1_linkA_icmp.pcap", [ - (1693472000, 123456, _icmp_frame()), - ]) - return _fake_project(tmp_path, { - "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), - }) + _write_pcap( + tmp_path / "n1_linkA_icmp.pcap", + [ + (1693472000, 123456, _icmp_frame()), + ], + ) + return _fake_project( + tmp_path, + { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + }, + ) async def test_ts_mismatch_guard_404(self, tmp_path): project = self._project(tmp_path) with pytest.raises(ControllerNotFoundError, match="rebuilt"): - await decode_frame(project, tag=7, ts="1.000000", - node_id="n1", link_id="linkA", marker="icmp") + await decode_frame(project, tag=7, ts="1.000000", node_id="n1", link_id="linkA", marker="icmp") async def test_unknown_source_404(self, tmp_path): project = self._project(tmp_path) with pytest.raises(ControllerNotFoundError): - await decode_frame(project, tag=7, ts="1693472000.123456", - node_id="nobody", link_id="linkA", marker="icmp") + await decode_frame(project, tag=7, ts="1693472000.123456", node_id="nobody", link_id="linkA", marker="icmp") async def test_explicit_frame_number_out_of_range_404(self, tmp_path): project = self._project(tmp_path) with pytest.raises(ControllerNotFoundError, match="rebuilt"): - await decode_frame(project, tag=7, ts="1693472000.123456", - node_id="n1", link_id="linkA", marker="icmp", frame_number=5) + await decode_frame( + project, tag=7, ts="1693472000.123456", node_id="n1", link_id="linkA", marker="icmp", frame_number=5 + ) async def test_explicit_frame_number_ts_mismatch_404(self, tmp_path): # The frame number must still land on the exact round-tripped ts — # a rebuilt capture cannot be decoded by stale coordinates. project = self._project(tmp_path) with pytest.raises(ControllerNotFoundError, match="rebuilt"): - await decode_frame(project, tag=7, ts="1693472001.000000", - node_id="n1", link_id="linkA", marker="icmp", frame_number=1) + await decode_frame( + project, tag=7, ts="1693472001.000000", node_id="n1", link_id="linkA", marker="icmp", frame_number=1 + ) async def test_hex_read_failure_is_404_not_null_hex(self, tmp_path, monkeypatch): project = self._project(tmp_path) monkeypatch.setattr(marker_replay, "read_frame_bytes", lambda path, n: None) with pytest.raises(ControllerNotFoundError, match="rebuilt"): - await decode_frame(project, tag=7, ts="1693472000.123456", - node_id="n1", link_id="linkA", marker="icmp") + await decode_frame(project, tag=7, ts="1693472000.123456", node_id="n1", link_id="linkA", marker="icmp") @sharkd_present async def test_same_microsecond_frames_disambiguated_by_frame_number(self, tmp_path): @@ -732,16 +790,20 @@ class TestDecodeFrame: pcap = tmp_path / "n1_linkA_icmp.pcap" icmp, tcp = _icmp_frame(), _tcp_syn_frame() _write_pcap(pcap, [(1693472000, 123456, icmp), (1693472000, 123456, tcp)]) - project = _fake_project(tmp_path, { - "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), - }) + project = _fake_project( + tmp_path, + { + "linkA/icmp": _marker_entry(tag=7, enabled=False, node_id="n1"), + }, + ) manager = marker_replay._get_manager() try: - default = await decode_frame(project, tag=7, ts="1693472000.123456", - node_id="n1", link_id="linkA", marker="icmp") - second = await decode_frame(project, tag=7, ts="1693472000.123456", - node_id="n1", link_id="linkA", marker="icmp", - frame_number=2) + default = await decode_frame( + project, tag=7, ts="1693472000.123456", node_id="n1", link_id="linkA", marker="icmp" + ) + second = await decode_frame( + project, tag=7, ts="1693472000.123456", node_id="n1", link_id="linkA", marker="icmp", frame_number=2 + ) finally: await manager.close_all() # Without a frame number: first ts match. @@ -757,8 +819,9 @@ class TestDecodeFrame: manager = marker_replay._get_manager() try: project = self._project(tmp_path) - detail = await decode_frame(project, tag=7, ts="1693472000.123456", - node_id="n1", link_id="linkA", marker="icmp") + detail = await decode_frame( + project, tag=7, ts="1693472000.123456", node_id="n1", link_id="linkA", marker="icmp" + ) finally: await manager.close_all() @@ -789,10 +852,13 @@ class TestDecodeFrame: trees, sharkd's raw key set stays within the census-known keys.""" pcap = tmp_path / "proto_mix.pcap" - _write_pcap(pcap, [ - (1693472000, 100000, _icmp_frame()), - (1693472001, 0, _tcp_syn_frame()), - ]) + _write_pcap( + pcap, + [ + (1693472000, 100000, _icmp_frame()), + (1693472001, 0, _tcp_syn_frame()), + ], + ) manager = marker_replay._get_manager() try: known = set(marker_replay._KEY_RENAME) | {"h", "e"} diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index 8ce0d428b..300d410bd 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -40,11 +40,15 @@ def compute(): @pytest.fixture def node(compute, project): - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="vpcs", - console_type="vnc", - properties={"startup_script": "echo test"}) + node = Node( + project, + compute, + "demo", + node_id=str(uuid.uuid4()), + node_type="vpcs", + console_type="vnc", + properties={"startup_script": "echo test"}, + ) return node @@ -55,22 +59,29 @@ def test_docker_iol_ports_grouped_in_four_port_units(compute, project): the native IOU node type. Plain docker nodes keep the flat eth naming. """ - node = Node(project, compute, "iol", - node_id=str(uuid.uuid4()), - node_type="docker", - properties={"adapters": 2, "environment": "GNS3_IOL_RUNNER=1"}) + node = Node( + project, + compute, + "iol", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"adapters": 2, "environment": "GNS3_IOL_RUNNER=1"}, + ) ports = node.ports assert [p.asdict()["name"] for p in ports] == [ - "Ethernet0/0", "Ethernet0/1", "Ethernet0/2", "Ethernet0/3", - "Ethernet1/0", "Ethernet1/1", "Ethernet1/2", "Ethernet1/3", + "Ethernet0/0", + "Ethernet0/1", + "Ethernet0/2", + "Ethernet0/3", + "Ethernet1/0", + "Ethernet1/1", + "Ethernet1/2", + "Ethernet1/3", ] assert (ports[5].adapter_number, ports[5].port_number) == (1, 1) assert ports[5].short_name == "e1/1" - node = Node(project, compute, "web", - node_id=str(uuid.uuid4()), - node_type="docker", - properties={"adapters": 2}) + node = Node(project, compute, "web", node_id=str(uuid.uuid4()), node_type="docker", properties={"adapters": 2}) assert [p.asdict()["name"] for p in node.ports] == ["eth0", "eth1"] @@ -82,10 +93,14 @@ def test_docker_iol_startup_config_materialized_once(compute, project, monkeypat afterwards the node's configuration lives in its NVRAM on the compute. """ - node = Node(project, compute, "iol", - node_id=str(uuid.uuid4()), - node_type="docker", - properties={"environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=my-iol-config.txt"}) + node = Node( + project, + compute, + "iol", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=my-iol-config.txt"}, + ) monkeypatch.setattr(node, "_base_config_file_content", lambda path: "hostname %h\n") data = node._node_data() @@ -100,10 +115,14 @@ def test_docker_iol_startup_config_materialized_once(compute, project, monkeypat def test_docker_iol_startup_config_missing_file_keeps_knob(compute, project, monkeypatch): - node = Node(project, compute, "iol", - node_id=str(uuid.uuid4()), - node_type="docker", - properties={"environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=gone.txt"}) + node = Node( + project, + compute, + "iol", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=gone.txt"}, + ) monkeypatch.setattr(node, "_base_config_file_content", lambda path: None) data = node._node_data() @@ -132,23 +151,35 @@ def test_name(compute, project): If node use a name template generate names """ - node = Node(project, compute, "PC", - node_id=str(uuid.uuid4()), - node_type="vpcs", - console_type="vnc", - properties={"startup_script": "echo test"}) + node = Node( + project, + compute, + "PC", + node_id=str(uuid.uuid4()), + node_type="vpcs", + console_type="vnc", + properties={"startup_script": "echo test"}, + ) assert node.name == "PC" - node = Node(project, compute, "PC{0}", - node_id=str(uuid.uuid4()), - node_type="vpcs", - console_type="vnc", - properties={"startup_script": "echo test"}) + node = Node( + project, + compute, + "PC{0}", + node_id=str(uuid.uuid4()), + node_type="vpcs", + console_type="vnc", + properties={"startup_script": "echo test"}, + ) assert node.name == "PC1" - node = Node(project, compute, "PC{0}", - node_id=str(uuid.uuid4()), - node_type="vpcs", - console_type="vnc", - properties={"startup_script": "echo test"}) + node = Node( + project, + compute, + "PC{0}", + node_id=str(uuid.uuid4()), + node_type="vpcs", + console_type="vnc", + properties={"startup_script": "echo test"}, + ) assert node.name == "PC2" @@ -158,10 +189,7 @@ def test_vmname(compute, project): field """ - node = Node(project, compute, "PC", - node_id=str(uuid.uuid4()), - node_type="virtualbox", - vmname="test") + node = Node(project, compute, "PC", node_id=str(uuid.uuid4()), node_type="virtualbox", vmname="test") assert node.properties["vmname"] == "test" @@ -169,13 +197,9 @@ def test_empty_properties(compute, project): """ Empty properties need to be ignored """ - node = Node(project, compute, "PC", - node_id=str(uuid.uuid4()), - node_type="virtualbox", - aa="", - bb=None, - category=2, - cc="xx") + node = Node( + project, compute, "PC", node_id=str(uuid.uuid4()), node_type="virtualbox", aa="", bb=None, category=2, cc="xx" + ) assert "aa" not in node.properties assert "bb" not in node.properties assert "cc" in node.properties @@ -188,7 +212,9 @@ async def test_eq(compute, project, node, controller): assert node == Node(project, compute, "demo1", node_id=node.id, node_type="qemu") assert node != "a" assert node != Node(project, compute, "demo2", node_id=str(uuid.uuid4()), node_type="qemu") - assert node != Node(Project(str(uuid.uuid4()), controller=controller), compute, "demo3", node_id=node.id, node_type="qemu") + assert node != Node( + Project(str(uuid.uuid4()), controller=controller), compute, "demo3", node_id=node.id, node_type="qemu" + ) def test_json(node, compute): @@ -233,11 +259,11 @@ def test_json(node, compute): "link_type": "ethernet", "name": "Ethernet0", "port_number": 0, - "short_name": "e0" + "short_name": "e0", } ], "missing_image": False, - "missing_images": [] + "missing_images": [], } assert node.asdict(topology_dump=True) == { @@ -272,9 +298,7 @@ def test_json(node, compute): def test_init_without_uuid(project, compute): - node = Node(project, compute, "demo", - node_type="vpcs", - console_type="vnc") + node = Node(project, compute, "demo", node_type="vpcs", console_type="vnc") assert node.id is not None @@ -287,13 +311,7 @@ async def test_create(node, compute): compute.post = AsyncioMagicMock(return_value=response) assert await node.create() is True - data = { - "console": 2048, - "console_type": "vnc", - "node_id": node.id, - "startup_script": "echo test", - "name": "demo" - } + data = {"console": 2048, "console_type": "vnc", "node_id": node.id, "startup_script": "echo test", "name": "demo"} compute.post.assert_called_with("/projects/{}/vpcs/nodes".format(node.project.id), data=data, timeout=1200) assert node._console == 2048 assert node._properties == {"startup_script": "echo test"} @@ -319,7 +337,7 @@ async def test_create_image_missing(node, compute): node._upload_missing_image = AsyncioMagicMock(return_value=True) assert await node.create() is True - #assert node._upload_missing_image.called is True + # assert node._upload_missing_image.called is True @pytest.mark.asyncio @@ -330,15 +348,19 @@ async def test_create_image_missing_kept_in_degraded_state(project, compute, tmp """ config.settings.Server.images_path = str(tmpdir) - node = Node(project, compute, "r1", - node_id=str(uuid.uuid4()), - node_type="qemu", - properties={"hda_disk_image": "missing.qcow2", "ram": 256}) + node = Node( + project, + compute, + "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"hda_disk_image": "missing.qcow2", "ram": 256}, + ) async def resp(*args, **kwargs): raise ComputeConflictError( "/projects/{}/qemu/nodes".format(project.id), - {"message": "The image is missing", "image": "missing.qcow2", "exception": "ImageMissingError"} + {"message": "The image is missing", "image": "missing.qcow2", "exception": "ImageMissingError"}, ) compute.post = AsyncioMagicMock(side_effect=resp) @@ -346,9 +368,7 @@ async def test_create_image_missing_kept_in_degraded_state(project, compute, tmp assert await node.create(allow_missing_image=True) is False assert node.missing_image is True - assert node.missing_images == [ - {"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"} - ] + assert node.missing_images == [{"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"}] @pytest.mark.asyncio @@ -359,15 +379,19 @@ async def test_create_image_missing_raises_by_default(project, compute, tmpdir, """ config.settings.Server.images_path = str(tmpdir) - node = Node(project, compute, "r1", - node_id=str(uuid.uuid4()), - node_type="qemu", - properties={"hda_disk_image": "missing.qcow2", "ram": 256}) + node = Node( + project, + compute, + "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"hda_disk_image": "missing.qcow2", "ram": 256}, + ) async def resp(*args, **kwargs): raise ComputeConflictError( "/projects/{}/qemu/nodes".format(project.id), - {"message": "The image is missing", "image": "missing.qcow2", "exception": "ImageMissingError"} + {"message": "The image is missing", "image": "missing.qcow2", "exception": "ImageMissingError"}, ) compute.post = AsyncioMagicMock(side_effect=resp) @@ -478,15 +502,19 @@ async def test_create_docker_image_missing_after_failed_pull(project, compute): degraded state instead of aborting the project open. """ - node = Node(project, compute, "web", - node_id=str(uuid.uuid4()), - node_type="docker", - properties={"image": "ghost:latest", "adapters": 1}) + node = Node( + project, + compute, + "web", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"image": "ghost:latest", "adapters": 1}, + ) async def resp(*args, **kwargs): raise ComputeConflictError( "/projects/{}/docker/nodes".format(project.id), - {"message": "The image is missing", "image": "ghost:latest", "exception": "ImageMissingError"} + {"message": "The image is missing", "image": "ghost:latest", "exception": "ImageMissingError"}, ) compute.post = AsyncioMagicMock(side_effect=resp) @@ -496,9 +524,7 @@ async def test_create_docker_image_missing_after_failed_pull(project, compute): assert await node.create(allow_missing_image=True) is False assert node.missing_image is True - assert node.missing_images == [ - {"property": "image", "image": "ghost:latest", "image_type": "docker"} - ] + assert node.missing_images == [{"property": "image", "image": "ghost:latest", "image_type": "docker"}] @pytest.mark.asyncio @@ -507,15 +533,19 @@ async def test_create_docker_image_missing_pull_error_raises_by_default(project, Without allow_missing_image a failed Docker pull is still surfaced. """ - node = Node(project, compute, "web", - node_id=str(uuid.uuid4()), - node_type="docker", - properties={"image": "ghost:latest", "adapters": 1}) + node = Node( + project, + compute, + "web", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"image": "ghost:latest", "adapters": 1}, + ) async def resp(*args, **kwargs): raise ComputeConflictError( "/projects/{}/docker/nodes".format(project.id), - {"message": "The image is missing", "image": "ghost:latest", "exception": "ImageMissingError"} + {"message": "The image is missing", "image": "ghost:latest", "exception": "ImageMissingError"}, ) compute.post = AsyncioMagicMock(side_effect=resp) @@ -534,15 +564,19 @@ def test_compute_missing_images_lists_all_unavailable_slots(project, compute, tm """ config.settings.Server.images_path = str(tmpdir) - node = Node(project, compute, "r1", - node_id=str(uuid.uuid4()), - node_type="qemu", - properties={ - "hda_disk_image": "present.qcow2", - "hdc_disk_image": "missing.qcow2", - "initrd": "missing.initrd", - "ram": 256, - }) + node = Node( + project, + compute, + "r1", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={ + "hda_disk_image": "present.qcow2", + "hdc_disk_image": "missing.qcow2", + "initrd": "missing.initrd", + "ram": 256, + }, + ) # make one image available on the controller os.makedirs(os.path.join(str(tmpdir), "QEMU"), exist_ok=True) with open(os.path.join(str(tmpdir), "QEMU", "present.qcow2"), "w") as f: @@ -556,9 +590,7 @@ def test_compute_missing_images_lists_all_unavailable_slots(project, compute, tm @pytest.mark.asyncio async def test_start_missing_image(node): - node._missing_images = [ - {"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"} - ] + node._missing_images = [{"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"}] with pytest.raises(ControllerError): await node.start() @@ -570,9 +602,7 @@ async def test_update_recreates_missing_image_node(project, compute, node): the missing image state. """ - node._missing_images = [ - {"property": "image", "image": "missing.image", "image_type": "ios"} - ] + node._missing_images = [{"property": "image", "image": "missing.image", "image_type": "ios"}] response = MagicMock() response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) @@ -600,9 +630,7 @@ async def test_update_replaces_qemu_linked_clone_backing_image(project, compute) "hda_disk_image_md5sum": "old-checksum", }, ) - qemu_node._missing_images = [ - {"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"} - ] + qemu_node._missing_images = [{"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"}] response = MagicMock() response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) @@ -627,18 +655,14 @@ async def test_update_replaces_qemu_linked_clone_backing_image(project, compute) async def test_update_missing_image_node_rolls_back_after_create_error(project, compute, node): original_properties = {"image": "missing.image"} node._properties = original_properties.copy() - node._missing_images = [ - {"property": "image", "image": "missing.image", "image_type": "ios"} - ] + node._missing_images = [{"property": "image", "image": "missing.image", "image_type": "ios"}] compute.post = AsyncioMagicMock(side_effect=ControllerError("create failed")) with pytest.raises(ControllerError): await node.update(properties={"image": "invalid.image"}) assert node.properties == original_properties - assert node.missing_images == [ - {"property": "image", "image": "missing.image", "image_type": "ios"} - ] + assert node.missing_images == [{"property": "image", "image": "missing.image", "image_type": "ios"}] @pytest.mark.asyncio @@ -659,9 +683,7 @@ async def test_start_retries_ready_deferred_links(node, project): @pytest.mark.asyncio async def test_stop_missing_image_node_does_not_contact_compute(node, compute): - node._missing_images = [ - {"property": "image", "image": "missing.image", "image_type": "ios"} - ] + node._missing_images = [{"property": "image", "image": "missing.image", "image_type": "ios"}] compute.post = AsyncioMagicMock() await node.stop() @@ -673,8 +695,8 @@ async def test_stop_missing_image_node_does_not_contact_compute(node, compute): async def test_create_base_script(node, config, compute, tmpdir): config.settings.Server.configs_path = str(tmpdir) - with open(str(tmpdir / 'test.txt'), 'w+') as f: - f.write('hostname test') + with open(str(tmpdir / "test.txt"), "w+") as f: + f.write("hostname test") node._properties = {"base_script_file": "test.txt"} node._console = 2048 @@ -689,7 +711,7 @@ async def test_create_base_script(node, config, compute, tmpdir): "console_type": "vnc", "node_id": node.id, "startup_script": "hostname test", - "name": "demo" + "name": "demo", } compute.post.assert_called_with("/projects/{}/vpcs/nodes".format(node.project.id), data=data, timeout=1200) @@ -713,7 +735,9 @@ def test_symbol(node, symbols_dir): assert node.height == 71 assert node.label["x"] is None assert node.label["y"] == -40 - assert node.label["style"] == None#"font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;" + assert ( + node.label["style"] == None + ) # "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;" shutil.copy(os.path.join("gns3server", "symbols", "classic", "cloud.svg"), os.path.join(symbols_dir, "cloud2.svg")) node.symbol = "cloud2.svg" @@ -734,15 +758,14 @@ def test_label_with_default_label_font(node): the correct color """ node.project.controller.settings = { - "GraphicsView": { - "default_label_color": "#ff0000", - "default_label_font": "TypeWriter,10,-1,5,75,0,0,0,0,0" - } + "GraphicsView": {"default_label_color": "#ff0000", "default_label_font": "TypeWriter,10,-1,5,75,0,0,0,0,0"} } node._label = None node.symbol = ":/symbols/dslam.svg" - assert node.label["style"] == None #"font-family: TypeWriter;font-size: 10;font-weight: bold;fill: #ff0000;fill-opacity: 1.0;" + assert ( + node.label["style"] == None + ) # "font-family: TypeWriter;font-size: 10;font-weight: bold;fill: #ff0000;fill-opacity: 1.0;" @pytest.mark.asyncio @@ -755,17 +778,12 @@ async def test_update(node, compute, project, controller): project.dump = MagicMock() await node.update(x=42, console=2048, console_type="vnc", properties={"startup_script": "echo test"}, name="demo") - data = { - "console": 2048, - "console_type": "vnc", - "startup_script": "echo test", - "name": "demo" - } + data = {"console": 2048, "console_type": "vnc", "startup_script": "echo test", "name": "demo"} compute.put.assert_called_with("/projects/{}/vpcs/nodes/{}".format(node.project.id, node.id), data=data) assert node._console == 2048 assert node.x == 42 assert node._properties == {"startup_script": "echo test"} - #controller._notification.emit.assert_called_with("node.updated", node.asdict()) + # controller._notification.emit.assert_called_with("node.updated", node.asdict()) assert project.dump.called @@ -780,12 +798,7 @@ async def test_update_properties(node, compute, controller): controller._notification = AsyncioMagicMock() await node.update(x=42, console=2048, console_type="vnc", properties={"startup_script": "hello world"}, name="demo") - data = { - "console": 2048, - "console_type": "vnc", - "startup_script": "hello world", - "name": "demo" - } + data = {"console": 2048, "console_type": "vnc", "startup_script": "hello world", "name": "demo"} compute.put.assert_called_with("/projects/{}/vpcs/nodes/{}".format(node.project.id, node.id), data=data) assert node._console == 2048 assert node.x == 42 @@ -793,9 +806,9 @@ async def test_update_properties(node, compute, controller): # The notif should contain the old properties because it's the compute that will emit # the correct info - #node_notif = copy.deepcopy(node.asdict()) - #node_notif["properties"]["startup_script"] = "echo test" - #controller._notification.emit.assert_called_with("node.updated", node_notif) + # node_notif = copy.deepcopy(node.asdict()) + # node_notif["properties"]["startup_script"] = "echo test" + # controller._notification.emit.assert_called_with("node.updated", node_notif) @pytest.mark.asyncio @@ -888,8 +901,7 @@ def test_default_credentials_from_template_kwargs(compute, project): default credentials without sending them to the compute. """ - node = Node(project, compute, "test", node_type="vpcs", - default_username="root", default_password="cisco123") + node = Node(project, compute, "test", node_type="vpcs", default_username="root", default_password="cisco123") assert node.default_username == "root" assert node.default_password == "cisco123" assert "default_username" not in node.properties @@ -927,18 +939,20 @@ async def test_start(node, compute): @pytest.mark.asyncio async def test_start_iou(compute, project, controller): - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="iou") + node = Node(project, compute, "demo", node_id=str(uuid.uuid4()), node_type="iou") compute.post = AsyncioMagicMock() # Without licence configured it should raise an error - #with pytest.raises(aiohttp.web.HTTPConflict): + # with pytest.raises(aiohttp.web.HTTPConflict): # async_run(node.start()) controller._iou_license_settings = {"license_check": True, "iourc_content": "aa"} await node.start() - compute.post.assert_called_with("/projects/{}/iou/nodes/{}/start".format(node.project.id, node.id), timeout=240, data={"license_check": True, "iourc_content": "aa"}) + compute.post.assert_called_with( + "/projects/{}/iou/nodes/{}/start".format(node.project.id, node.id), + timeout=240, + data={"license_check": True, "iourc_content": "aa"}, + ) @pytest.mark.asyncio @@ -947,7 +961,9 @@ async def test_stop(node, compute): compute.post = AsyncioMagicMock() await node.stop() - compute.post.assert_called_with("/projects/{}/vpcs/nodes/{}/stop".format(node.project.id, node.id), timeout=240, dont_connect=True) + compute.post.assert_called_with( + "/projects/{}/vpcs/nodes/{}/stop".format(node.project.id, node.id), timeout=240, dont_connect=True + ) @pytest.mark.asyncio @@ -977,12 +993,7 @@ async def test_create_without_console(node, compute): compute.post = AsyncioMagicMock(return_value=response) await node.create() - data = { - "console_type": "vnc", - "node_id": node.id, - "startup_script": "echo test", - "name": "demo" - } + data = {"console_type": "vnc", "node_id": node.id, "startup_script": "echo test", "name": "demo"} compute.post.assert_called_with("/projects/{}/vpcs/nodes".format(node.project.id), data=data, timeout=1200) assert node._console == 2048 assert node._properties == {"test_value": "success", "startup_script": "echo test"} @@ -1017,7 +1028,9 @@ async def test_dynamips_idle_pc(node, compute): response.json = {"idlepc": "0x60606f54"} compute.get = AsyncioMagicMock(return_value=response) await node.dynamips_auto_idlepc() - compute.get.assert_called_with("/projects/{}/dynamips/nodes/{}/auto_idlepc".format(node.project.id, node.id), timeout=240) + compute.get.assert_called_with( + "/projects/{}/dynamips/nodes/{}/auto_idlepc".format(node.project.id, node.id), timeout=240 + ) @pytest.mark.asyncio @@ -1028,18 +1041,24 @@ async def test_dynamips_idlepc_proposals(node, compute): response.json = ["0x60606f54", "0x30ff6f37"] compute.get = AsyncioMagicMock(return_value=response) await node.dynamips_idlepc_proposals() - compute.get.assert_called_with("/projects/{}/dynamips/nodes/{}/idlepc_proposals".format(node.project.id, node.id), timeout=240) + compute.get.assert_called_with( + "/projects/{}/dynamips/nodes/{}/idlepc_proposals".format(node.project.id, node.id), timeout=240 + ) @pytest.mark.asyncio async def test_upload_missing_image(compute, controller, images_dir): project = Project(str(uuid.uuid4()), controller=controller) - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="qemu", - properties={"hda_disk_image": "linux.img"}) - open(os.path.join(images_dir, "linux.img"), 'w+').close() + node = Node( + project, + compute, + "demo", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"hda_disk_image": "linux.img"}, + ) + open(os.path.join(images_dir, "linux.img"), "w+").close() assert await node._upload_missing_image("qemu", "linux.img") is True compute.post.assert_called_with("/qemu/images/linux.img", data=ANY, timeout=None) @@ -1048,10 +1067,14 @@ async def test_upload_missing_image(compute, controller, images_dir): async def test_upload_missing_image_from_nested_directory(compute, controller, images_dir): project = Project(str(uuid.uuid4()), controller=controller) - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="qemu", - properties={"hda_disk_image": "nested.img"}) + node = Node( + project, + compute, + "demo", + node_id=str(uuid.uuid4()), + node_type="qemu", + properties={"hda_disk_image": "nested.img"}, + ) nested_dir = os.path.join(images_dir, "vendor") os.makedirs(nested_dir) open(os.path.join(nested_dir, "nested.img"), "w+").close() @@ -1064,10 +1087,9 @@ async def test_upload_missing_image_from_nested_directory(compute, controller, i async def test_sync_missing_docker_image_from_controller_daemon(compute, controller): project = Project(str(uuid.uuid4()), controller=controller) - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="docker", - properties={"image": "gns3/frr:latest"}) + node = Node( + project, compute, "demo", node_id=str(uuid.uuid4()), node_type="docker", properties={"image": "gns3/frr:latest"} + ) response = MagicMock() response.content = MagicMock() @@ -1082,10 +1104,9 @@ async def test_sync_missing_docker_image_from_controller_daemon(compute, control async def test_sync_missing_docker_image_pull_fallback(compute, controller): project = Project(str(uuid.uuid4()), controller=controller) - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="docker", - properties={"image": "nginx:latest"}) + node = Node( + project, compute, "demo", node_id=str(uuid.uuid4()), node_type="docker", properties={"image": "nginx:latest"} + ) with asyncio_patch("gns3server.compute.docker.Docker.http_query", side_effect=DockerError("404")): assert await node._upload_missing_image("docker", "nginx:latest") is True @@ -1096,10 +1117,14 @@ async def test_sync_missing_docker_image_pull_fallback(compute, controller): async def test_create_docker_node_pins_image_digest(compute, controller): project = Project(str(uuid.uuid4()), controller=controller) - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="docker", - properties={"image": "gns3/frr:latest", "adapters": 1}) + node = Node( + project, + compute, + "demo", + node_id=str(uuid.uuid4()), + node_type="docker", + properties={"image": "gns3/frr:latest", "adapters": 1}, + ) response = MagicMock() response.status = 200 diff --git a/tests/controller/test_node_port_name.py b/tests/controller/test_node_port_name.py index 26c054d23..bfd108197 100644 --- a/tests/controller/test_node_port_name.py +++ b/tests/controller/test_node_port_name.py @@ -33,11 +33,15 @@ def compute(): @pytest.fixture def node(compute, project): - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="qemu", - console_type="vnc", - properties={"startup_script": "echo test"}) + node = Node( + project, + compute, + "demo", + node_id=str(uuid.uuid4()), + node_type="qemu", + console_type="vnc", + properties={"startup_script": "echo test"}, + ) return node @@ -53,7 +57,7 @@ def test_list_ports(node): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", } ] @@ -71,7 +75,7 @@ def test_list_ports_vpcs(node): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", } ] @@ -90,7 +94,7 @@ def test_list_ports_docker(node): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "eth1", @@ -98,8 +102,8 @@ def test_list_ports_docker(node): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 1, - "link_type": "ethernet" - } + "link_type": "ethernet", + }, ] @@ -153,7 +157,7 @@ def test_list_ports_adapters(node): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet1", @@ -161,8 +165,8 @@ def test_list_ports_adapters(node): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 1, - "link_type": "ethernet" - } + "link_type": "ethernet", + }, ] @@ -171,17 +175,8 @@ def test_list_ports_adapters_cloud(project, compute): List port using adapters properties """ - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="cloud") - node.properties["ports_mapping"] = [ - { - "interface": "eth0", - "name": "eth0", - "port_number": 0, - "type": "ethernet" - } - ] + node = Node(project, compute, "demo", node_id=str(uuid.uuid4()), node_type="cloud") + node.properties["ports_mapping"] = [{"interface": "eth0", "name": "eth0", "port_number": 0, "type": "ethernet"}] assert node.asdict()["ports"] == [ { @@ -190,7 +185,7 @@ def test_list_ports_adapters_cloud(project, compute): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", } ] @@ -200,18 +195,10 @@ def test_list_ports_ethernet_hub(project, compute): List port for atm switch """ - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="ethernet_hub") + node = Node(project, compute, "demo", node_id=str(uuid.uuid4()), node_type="ethernet_hub") node.properties["ports_mapping"] = [ - { - "name": "Ethernet0", - "port_number": 0 - }, - { - "name": "Ethernet1", - "port_number": 1 - } + {"name": "Ethernet0", "port_number": 0}, + {"name": "Ethernet1", "port_number": 1}, ] assert node.asdict()["ports"] == [ @@ -221,7 +208,7 @@ def test_list_ports_ethernet_hub(project, compute): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet1", @@ -229,8 +216,8 @@ def test_list_ports_ethernet_hub(project, compute): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 1, "adapter_number": 0, - "link_type": "ethernet" - } + "link_type": "ethernet", + }, ] @@ -239,12 +226,8 @@ def test_list_ports_atm_switch(project, compute): List port for atm switch """ - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="atm_switch") - node.properties["mappings"] = { - "1:0:100": "10:0:200" - } + node = Node(project, compute, "demo", node_id=str(uuid.uuid4()), node_type="atm_switch") + node.properties["mappings"] = {"1:0:100": "10:0:200"} assert node.asdict()["ports"] == [ { @@ -253,7 +236,7 @@ def test_list_ports_atm_switch(project, compute): "data_link_types": {"ATM": "DLT_ATM_RFC1483"}, "port_number": 1, "adapter_number": 0, - "link_type": "serial" + "link_type": "serial", }, { "name": "10", @@ -261,8 +244,8 @@ def test_list_ports_atm_switch(project, compute): "data_link_types": {"ATM": "DLT_ATM_RFC1483"}, "port_number": 10, "adapter_number": 0, - "link_type": "serial" - } + "link_type": "serial", + }, ] @@ -271,13 +254,8 @@ def test_list_ports_frame_relay_switch(project, compute): List port for frame relay switch """ - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="frame_relay_switch") - node.properties["mappings"] = { - "1:101": "10:202", - "2:102": "11:203" - } + node = Node(project, compute, "demo", node_id=str(uuid.uuid4()), node_type="frame_relay_switch") + node.properties["mappings"] = {"1:101": "10:202", "2:102": "11:203"} assert node.asdict()["ports"] == [ { @@ -286,7 +264,7 @@ def test_list_ports_frame_relay_switch(project, compute): "data_link_types": {"Frame Relay": "DLT_FRELAY"}, "port_number": 1, "adapter_number": 0, - "link_type": "serial" + "link_type": "serial", }, { "name": "2", @@ -294,7 +272,7 @@ def test_list_ports_frame_relay_switch(project, compute): "data_link_types": {"Frame Relay": "DLT_FRELAY"}, "port_number": 2, "adapter_number": 0, - "link_type": "serial" + "link_type": "serial", }, { "name": "10", @@ -302,7 +280,7 @@ def test_list_ports_frame_relay_switch(project, compute): "data_link_types": {"Frame Relay": "DLT_FRELAY"}, "port_number": 10, "adapter_number": 0, - "link_type": "serial" + "link_type": "serial", }, { "name": "11", @@ -310,8 +288,8 @@ def test_list_ports_frame_relay_switch(project, compute): "data_link_types": {"Frame Relay": "DLT_FRELAY"}, "port_number": 11, "adapter_number": 0, - "link_type": "serial" - } + "link_type": "serial", + }, ] @@ -320,9 +298,7 @@ def test_list_ports_iou(compute, project): IOU has a special behavior 4 port by adapters """ - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="iou") + node = Node(project, compute, "demo", node_id=str(uuid.uuid4()), node_type="iou") node.properties["serial_adapters"] = 2 node.properties["ethernet_adapters"] = 3 assert node.asdict()["ports"] == [ @@ -332,7 +308,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet0/1", @@ -340,7 +316,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 1, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet0/2", @@ -348,7 +324,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 2, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet0/3", @@ -356,7 +332,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 3, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet1/0", @@ -364,7 +340,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 1, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet1/1", @@ -372,7 +348,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 1, "adapter_number": 1, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet1/2", @@ -380,7 +356,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 2, "adapter_number": 1, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet1/3", @@ -388,7 +364,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 3, "adapter_number": 1, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet2/0", @@ -396,7 +372,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 2, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet2/1", @@ -404,7 +380,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 1, "adapter_number": 2, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet2/2", @@ -412,7 +388,7 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 2, "adapter_number": 2, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Ethernet2/3", @@ -420,104 +396,72 @@ def test_list_ports_iou(compute, project): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 3, "adapter_number": 2, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Serial3/0", "short_name": "s3/0", - "data_link_types": { - "Frame Relay": "DLT_FRELAY", - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL" - }, + "data_link_types": {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"}, "port_number": 0, "adapter_number": 3, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial3/1", "short_name": "s3/1", - "data_link_types": { - "Frame Relay": "DLT_FRELAY", - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL" - }, + "data_link_types": {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"}, "port_number": 1, "adapter_number": 3, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial3/2", "short_name": "s3/2", - "data_link_types": { - "Frame Relay": "DLT_FRELAY", - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL" - }, + "data_link_types": {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"}, "port_number": 2, "adapter_number": 3, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial3/3", "short_name": "s3/3", - "data_link_types": { - "Frame Relay": "DLT_FRELAY", - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL" - }, + "data_link_types": {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"}, "port_number": 3, "adapter_number": 3, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial4/0", "short_name": "s4/0", - "data_link_types": { - "Frame Relay": "DLT_FRELAY", - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL" - }, + "data_link_types": {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"}, "port_number": 0, "adapter_number": 4, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial4/1", "short_name": "s4/1", - "data_link_types": { - "Frame Relay": "DLT_FRELAY", - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL" - }, + "data_link_types": {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"}, "port_number": 1, "adapter_number": 4, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial4/2", "short_name": "s4/2", - "data_link_types": { - "Frame Relay": "DLT_FRELAY", - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL" - }, + "data_link_types": {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"}, "port_number": 2, "adapter_number": 4, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial4/3", "short_name": "s4/3", - "data_link_types": { - "Frame Relay": "DLT_FRELAY", - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL" - }, + "data_link_types": {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"}, "port_number": 3, "adapter_number": 4, - "link_type": "serial" - } + "link_type": "serial", + }, ] @@ -526,9 +470,7 @@ def test_list_ports_dynamips(project, compute): List port for dynamips """ - node = Node(project, compute, "demo", - node_id=str(uuid.uuid4()), - node_type="dynamips") + node = Node(project, compute, "demo", node_id=str(uuid.uuid4()), node_type="dynamips") node.properties["slot0"] = "C7200-IO-FE" node.properties["slot1"] = "GT96100-FE" node.properties["wic0"] = "WIC-2T" @@ -541,7 +483,7 @@ def test_list_ports_dynamips(project, compute): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 0, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "FastEthernet1/0", @@ -549,7 +491,7 @@ def test_list_ports_dynamips(project, compute): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 0, "adapter_number": 1, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "FastEthernet1/1", @@ -557,52 +499,40 @@ def test_list_ports_dynamips(project, compute): "data_link_types": {"Ethernet": "DLT_EN10MB"}, "port_number": 1, "adapter_number": 1, - "link_type": "ethernet" + "link_type": "ethernet", }, { "name": "Serial0/0", "short_name": "s0/0", - "data_link_types": { - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL", - "Frame Relay": "DLT_FRELAY"}, + "data_link_types": {"Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL", "Frame Relay": "DLT_FRELAY"}, "port_number": 16, "adapter_number": 0, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial0/1", "short_name": "s0/1", - "data_link_types": { - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL", - "Frame Relay": "DLT_FRELAY"}, + "data_link_types": {"Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL", "Frame Relay": "DLT_FRELAY"}, "port_number": 17, "adapter_number": 0, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial0/2", "short_name": "s0/2", - "data_link_types": { - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL", - "Frame Relay": "DLT_FRELAY"}, + "data_link_types": {"Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL", "Frame Relay": "DLT_FRELAY"}, "port_number": 32, "adapter_number": 0, - "link_type": "serial" + "link_type": "serial", }, { "name": "Serial0/3", "short_name": "s0/3", - "data_link_types": { - "Cisco HDLC": "DLT_C_HDLC", - "Cisco PPP": "DLT_PPP_SERIAL", - "Frame Relay": "DLT_FRELAY"}, + "data_link_types": {"Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL", "Frame Relay": "DLT_FRELAY"}, "port_number": 33, "adapter_number": 0, - "link_type": "serial" - } + "link_type": "serial", + }, ] diff --git a/tests/controller/test_notification.py b/tests/controller/test_notification.py index 754559154..1369a9b11 100644 --- a/tests/controller/test_notification.py +++ b/tests/controller/test_notification.py @@ -43,10 +43,10 @@ async def test_emit_to_all(controller, project): notif = controller.notification with notif.project_queue(project.id) as queue: assert len(notif._project_listeners[project.id]) == 1 - await queue.get(0.1) # ping - notif.project_emit('test', {}) + await queue.get(0.1) # ping + notif.project_emit("test", {}) msg = await queue.get(5) - assert msg == ('test', {}, {}) + assert msg == ("test", {}, {}) assert len(notif._project_listeners[project.id]) == 0 @@ -62,10 +62,10 @@ async def test_emit_to_project(controller, project): assert len(notif._project_listeners[project.id]) == 1 await queue.get(0.1) # ping # This event has not listener - notif.project_emit('ignore', {"project_id": 42}) - notif.project_emit('test', {"project_id": project.id}) + notif.project_emit("ignore", {"project_id": 42}) + notif.project_emit("test", {"project_id": project.id}) msg = await queue.get(5) - assert msg == ('test', {"project_id": project.id}, {}) + assert msg == ("test", {"project_id": project.id}, {}) assert len(notif._project_listeners[project.id]) == 0 @@ -79,7 +79,7 @@ async def test_dispatch(controller, project): await queue.get(0.1) # ping await notif.dispatch("test", {}, project_id=project.id, compute_id=1) msg = await queue.get(5) - assert msg == ('test', {}, {}) + assert msg == ("test", {}, {}) @pytest.mark.asyncio @@ -91,7 +91,7 @@ async def test_dispatch_ping(controller, project): await queue.get(0.1) # ping await notif.dispatch("ping", {}, project_id=project.id, compute_id=12) msg = await queue.get(5) - assert msg == ('ping', {'compute_id': 12}, {}) + assert msg == ("ping", {"compute_id": 12}, {}) @pytest.mark.asyncio @@ -105,14 +105,12 @@ async def test_dispatch_node_updated(controller, node, project): with notif.project_queue(project.id) as queue: assert len(notif._project_listeners[project.id]) == 1 await queue.get(0.1) # ping - await notif.dispatch("node.updated", { - "node_id": node.id, - "project_id": project.id, - "name": "hello", - "startup_config": "ip 192" - }, + await notif.dispatch( + "node.updated", + {"node_id": node.id, "project_id": project.id, "name": "hello", "startup_config": "ip 192"}, project_id=project.id, - compute_id=1) + compute_id=1, + ) assert node.name == "hello" action, event, _ = await queue.get(5) assert action == "node.updated" @@ -128,18 +126,16 @@ async def test_dispatch_marker_routed_to_marker_channel(controller, project): """ notif = controller.notification - with notif.project_queue(project.id) as project_q, \ - notif.project_marker_queue(project.id) as marker_q: + with notif.project_queue(project.id) as project_q, notif.project_marker_queue(project.id) as marker_q: assert len(notif._project_marker_listeners[project.id]) == 1 await project_q.get(0.1) # consume initial ping await marker_q.get(0.1) # consume initial ping - await notif.dispatch("marker.match", {"link_id": "abc"}, - project_id=project.id, compute_id=1) + await notif.dispatch("marker.match", {"link_id": "abc"}, project_id=project.id, compute_id=1) # marker.match lands on the marker channel... msg = await marker_q.get(5) - assert msg == ('marker.match', {"link_id": "abc"}, {}) + assert msg == ("marker.match", {"link_id": "abc"}, {}) # ...and does NOT land on the main project queue (times out -> ping) msg = await project_q.get(0.1) assert msg[0] == "ping" diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 978a5d4d8..6b8d2651e 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -53,18 +53,18 @@ async def node(controller, project): @pytest.mark.asyncio async def test_affect_uuid(): - with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + with patch("gns3server.controller.project.Project.emit_controller_notification") as mock_notification: p = Project(name="Test") mock_notification.assert_called() assert len(p.id) == 36 - p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f', name="Test 2") - assert p.id == '00010203-0405-0607-0809-0a0b0c0d0e0f' + p = Project(project_id="00010203-0405-0607-0809-0a0b0c0d0e0f", name="Test 2") + assert p.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" @pytest.mark.asyncio async def test_json(): - with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + with patch("gns3server.controller.project.Project.emit_controller_notification") as mock_notification: p = Project(name="Test") mock_notification.assert_called() @@ -89,14 +89,14 @@ async def test_json(): "supplier": None, "variables": None, "marker_definitions": {}, - "created_by": None + "created_by": None, } @pytest.mark.asyncio async def test_created_by(): - with patch('gns3server.controller.project.Project.emit_controller_notification'): + with patch("gns3server.controller.project.Project.emit_controller_notification"): p = Project(name="Test", created_by="admin") assert p.created_by == "admin" @@ -124,7 +124,7 @@ async def test_update_on_compute(controller): project._project_created_on_compute = [compute] project.emit_notification = MagicMock() await project.update(variables=variables) - compute.put.assert_any_call('/projects/{}'.format(project.id), {"variables": variables}) + compute.put.assert_any_call("/projects/{}".format(project.id), {"variables": variables}) @pytest.mark.asyncio @@ -132,7 +132,7 @@ async def test_path(projects_dir): directory = projects_dir with patch("gns3server.utils.path.get_default_project_directory", return_value=directory): - with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + with patch("gns3server.controller.project.Project.emit_controller_notification") as mock_notification: p = Project(project_id=str(uuid4()), name="Test") mock_notification.assert_called() assert p.path == os.path.join(directory, p.id) @@ -153,7 +153,7 @@ def test_path_exist(tmpdir): @pytest.mark.asyncio async def test_init_path(projects_dir): - with patch('gns3server.controller.project.Project.emit_controller_notification') as mock_notification: + with patch("gns3server.controller.project.Project.emit_controller_notification") as mock_notification: project_id = str(uuid4()) p = Project(project_id=project_id, name="Test") mock_notification.assert_called() @@ -164,15 +164,15 @@ async def test_init_path(projects_dir): async def test_changing_path_with_quote_not_allowed(projects_dir): with pytest.raises(ControllerForbiddenError): - with patch('gns3server.controller.project.Project.emit_controller_notification'): + with patch("gns3server.controller.project.Project.emit_controller_notification"): p = Project(project_id=str(uuid4()), name="Test") - p.path = os.path.join(projects_dir, "project\"53") + p.path = os.path.join(projects_dir, 'project"53') @pytest.mark.asyncio async def test_captures_directory(tmpdir): - with patch('gns3server.controller.project.Project.emit_controller_notification'): + with patch("gns3server.controller.project.Project.emit_controller_notification"): p = Project(name="Test") assert p.captures_directory == str(p.path + os.path.sep + "project-files" + os.path.sep + "captures") assert os.path.exists(p.captures_directory) @@ -196,16 +196,19 @@ async def test_add_node_local(controller): node = await project.add_node(compute, "test", None, node_type="vpcs", properties={"startup_script": "test.cfg"}) assert node.id in project._nodes - compute.post.assert_any_call('/projects', data={ - "name": project._name, - "project_id": project._id, - "path": project._path, - }) - compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id), - data={'node_id': node.id, - 'startup_script': 'test.cfg', - 'name': 'test'}, - timeout=1200) + compute.post.assert_any_call( + "/projects", + data={ + "name": project._name, + "project_id": project._id, + "path": project._path, + }, + ) + compute.post.assert_any_call( + "/projects/{}/vpcs/nodes".format(project.id), + data={"node_id": node.id, "startup_script": "test.cfg", "name": "test"}, + timeout=1200, + ) assert compute in project._project_created_on_compute project.emit_notification.assert_any_call("node.created", node.asdict()) @@ -269,13 +272,12 @@ async def test_add_node_non_local(controller): node = await project.add_node(compute, "test", None, node_type="vpcs", properties={"startup_script": "test.cfg"}) - compute.post.assert_any_call('/projects', data={ - "name": project._name, - "project_id": project._id - }) - compute.post.assert_any_call('/projects/{}/vpcs/nodes'.format(project.id), data={'node_id': node.id, - 'startup_script': 'test.cfg', - 'name': 'test'}, timeout=1200) + compute.post.assert_any_call("/projects", data={"name": project._name, "project_id": project._id}) + compute.post.assert_any_call( + "/projects/{}/vpcs/nodes".format(project.id), + data={"node_id": node.id, "startup_script": "test.cfg", "name": "test"}, + timeout=1200, + ) assert compute in project._project_created_on_compute project.emit_notification.assert_any_call("node.created", node.asdict()) @@ -538,7 +540,7 @@ async def test_delete_node(controller): await project.delete_node(node.id) assert node.id not in project._nodes - compute.delete.assert_any_call('/projects/{}/vpcs/nodes/{}'.format(project.id, node.id)) + compute.delete.assert_any_call("/projects/{}/vpcs/nodes/{}".format(project.id, node.id)) project.emit_notification.assert_any_call("node.deleted", node.asdict()) @@ -585,7 +587,7 @@ async def test_delete_node_delete_link(controller): assert node.id not in project._nodes assert link.id not in project._links - compute.delete.assert_any_call('/projects/{}/vpcs/nodes/{}'.format(project.id, node.id)) + compute.delete.assert_any_call("/projects/{}/vpcs/nodes/{}".format(project.id, node.id)) project.emit_notification.assert_any_call("node.deleted", node.asdict()) project.emit_notification.assert_any_call("link.deleted", link.asdict()) @@ -763,10 +765,7 @@ async def test_clean_pictures_and_keep_supplier_logo(project): When a project is close old pictures should be removed """ - project.supplier = { - 'logo': 'logo.png', - 'url': 'http://acme.com' - } + project.supplier = {"logo": "logo.png", "url": "http://acme.com"} drawing = await project.add_drawing() drawing._svg = "test.png" @@ -838,8 +837,8 @@ async def test_dump(projects_dir): directory = projects_dir with patch("gns3server.utils.path.get_default_project_directory", return_value=directory): - with patch('gns3server.controller.project.Project.emit_controller_notification'): - p = Project(project_id='00010203-0405-0607-0809-0a0b0c0d0e0f', name="Test") + with patch("gns3server.controller.project.Project.emit_controller_notification"): + p = Project(project_id="00010203-0405-0607-0809-0a0b0c0d0e0f", name="Test") p.dump() with open(os.path.join(directory, p.id, "Test.gns3")) as f: content = f.read() @@ -849,7 +848,7 @@ async def test_dump(projects_dir): @pytest.mark.asyncio async def test_open_close(controller): - with patch('gns3server.controller.project.Project.emit_controller_notification'): + with patch("gns3server.controller.project.Project.emit_controller_notification"): project = Project(controller=controller, name="Test") assert project.status == "opened" await project.close() @@ -866,13 +865,13 @@ async def test_open_close(controller): @pytest.mark.asyncio async def test_open_auto_start(controller): - with patch('gns3server.controller.project.Project.emit_controller_notification'): + with patch("gns3server.controller.project.Project.emit_controller_notification"): project = Project(controller=controller, name="Test", auto_start=True) assert project.status == "opened" await project.close() - #project.start_all = AsyncioMagicMock() - #await project.open() - #assert project.start_all.called + # project.start_all = AsyncioMagicMock() + # await project.open() + # assert project.start_all.called def test_is_running(project, node): @@ -901,10 +900,14 @@ async def test_duplicate(project, controller): response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - remote_vpcs = await project.add_node(compute, "test", None, node_type="vpcs", properties={"startup_config": "test.cfg"}) + remote_vpcs = await project.add_node( + compute, "test", None, node_type="vpcs", properties={"startup_config": "test.cfg"} + ) # We allow node not allowed for standard import / export - remote_virtualbox = await project.add_node(compute, "test", None, node_type="vmware", properties={"startup_config": "test.cfg"}) + remote_virtualbox = await project.add_node( + compute, "test", None, node_type="vmware", properties={"startup_config": "test.cfg"} + ) new_project = await project.duplicate(name="Hello") assert new_project.id != project.id @@ -1092,13 +1095,21 @@ async def test_node_name(project): response.json = {"console": 2048} compute.post = AsyncioMagicMock(return_value=response) - node = await project.add_node(compute, "test-{0}", None, node_type="vpcs", properties={"startup_config": "test.cfg"}) + node = await project.add_node( + compute, "test-{0}", None, node_type="vpcs", properties={"startup_config": "test.cfg"} + ) assert node.name == "test-1" - node = await project.add_node(compute, "test-{0}", None, node_type="vpcs", properties={"startup_config": "test.cfg"}) + node = await project.add_node( + compute, "test-{0}", None, node_type="vpcs", properties={"startup_config": "test.cfg"} + ) assert node.name == "test-2" - node = await project.add_node(compute, "hello world-{0}", None, node_type="vpcs", properties={"startup_config": "test.cfg"}) + node = await project.add_node( + compute, "hello world-{0}", None, node_type="vpcs", properties={"startup_config": "test.cfg"} + ) assert node.name == "helloworld-1" - node = await project.add_node(compute, "hello world-{0}", None, node_type="vpcs", properties={"startup_config": "test.cfg"}) + node = await project.add_node( + compute, "hello world-{0}", None, node_type="vpcs", properties={"startup_config": "test.cfg"} + ) assert node.name == "helloworld-2" node = await project.add_node(compute, "VPCS-1", None, node_type="vpcs", properties={"startup_config": "test.cfg"}) assert node.name == "VPCS-1" @@ -1119,13 +1130,8 @@ async def test_duplicate_node(project): compute.post = AsyncioMagicMock(return_value=response) original = await project.add_node( - compute, - "test", - None, - node_type="vpcs", - properties={ - "startup_config": "test.cfg" - }) + compute, "test", None, node_type="vpcs", properties={"startup_config": "test.cfg"} + ) new_node = await project.duplicate_node(original, 42, 10, 11) assert new_node.x == 42 @@ -1164,9 +1170,7 @@ async def test_add_node_missing_image_kept_in_degraded_state(controller): ) assert node.missing_image is True assert node.id in project._nodes - assert node.missing_images == [ - {"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"} - ] + assert node.missing_images == [{"property": "hda_disk_image", "image": "missing.qcow2", "image_type": "qemu"}] @pytest.mark.asyncio diff --git a/tests/controller/test_project_open.py b/tests/controller/test_project_open.py index 073fa3877..1d5ce49d9 100644 --- a/tests/controller/test_project_open.py +++ b/tests/controller/test_project_open.py @@ -42,22 +42,16 @@ def demo_topology(): "revision": 5, "topology": { "computes": [ - { - "compute_id": "local", - "host": "127.0.0.1", - "name": "atlantis", - "port": 3080, - "protocol": "http" - } + {"compute_id": "local", "host": "127.0.0.1", "name": "atlantis", "port": 3080, "protocol": "http"} ], "drawings": [ { "drawing_id": "48bdaa23-326a-4de0-bf7d-cc22709689ec", "rotation": 0, - "svg": "", + "svg": '', "x": -226, "y": 57, - "z": 0 + "z": 0, } ], "links": [ @@ -71,10 +65,10 @@ def demo_topology(): "style": "font-family: TypeWriter;font-size: 10;font-weight: bold;fill: #000000;fill-opacity: 1.0;", "text": "Ethernet0", "x": 72, - "y": 32 + "y": 32, }, "node_id": "64ba8408-afbf-4b66-9cdd-1fd854427478", - "port_number": 0 + "port_number": 0, }, { "adapter_number": 0, @@ -83,12 +77,12 @@ def demo_topology(): "style": "font-family: TypeWriter;font-size: 10;font-weight: bold;fill: #000000;fill-opacity: 1.0;", "text": "Ethernet0", "x": -7, - "y": 26 + "y": 26, }, "node_id": "748bcd89-624a-40eb-a8d3-1d2e85c99b51", - "port_number": 0 - } - ] + "port_number": 0, + }, + ], } ], "nodes": [ @@ -102,18 +96,17 @@ def demo_topology(): "style": "font-family: TypeWriter;font-size: 10;font-weight: bold;fill: #000000;fill-opacity: 1.0;", "text": "PC1", "x": 18, - "y": -25 + "y": -25, }, "name": "PC1", "node_id": "64ba8408-afbf-4b66-9cdd-1fd854427478", "node_type": "vpcs", - "properties": { - }, + "properties": {}, "symbol": ":/symbols/computer.svg", "width": 65, "x": -300, "y": -118, - "z": 1 + "z": 1, }, { "compute_id": "vm", @@ -125,28 +118,27 @@ def demo_topology(): "style": "font-family: TypeWriter;font-size: 10;font-weight: bold;fill: #000000;fill-opacity: 1.0;", "text": "PC2", "x": 18, - "y": -25 + "y": -25, }, "name": "PC2", "node_id": "748bcd89-624a-40eb-a8d3-1d2e85c99b51", "node_type": "vpcs", - "properties": { - }, + "properties": {}, "symbol": ":/symbols/computer.svg", "width": 65, "x": -71, "y": -98, - "z": 1 - } - ] + "z": 1, + }, + ], }, "type": "topology", - "version": "2.0.0" + "version": "2.0.0", } # @pytest.mark.asyncio -#async def test_load_project(controller, tmpdir, demo_topology, http_client): +# async def test_load_project(controller, tmpdir, demo_topology, http_client): # # with open(str(tmpdir / "demo.gns3"), "w+") as f: # json.dump(demo_topology, f) @@ -182,14 +174,9 @@ async def test_open(controller, projects_dir): "name": "demo", "project_id": "3c1be6f9-b4ba-4737-b209-63c47c23359f", "revision": 5, - "topology": { - "computes": [], - "drawings": [], - "links": [], - "nodes": [] - }, + "topology": {"computes": [], "drawings": [], "links": [], "nodes": []}, "type": "topology", - "version": "2.0.0" + "version": "2.0.0", } project_dir = os.path.join(projects_dir, "demo") @@ -197,12 +184,14 @@ async def test_open(controller, projects_dir): with open(os.path.join(project_dir, "demo.gns3"), "w+") as f: json.dump(simple_topology, f) - project = Project(name="demo", - project_id="64ba8408-afbf-4b66-9cdd-1fd854427478", - path=project_dir, - controller=controller, - filename="demo.gns3", - status="closed") + project = Project( + name="demo", + project_id="64ba8408-afbf-4b66-9cdd-1fd854427478", + path=project_dir, + controller=controller, + filename="demo.gns3", + status="closed", + ) await project.open() assert project.status == "opened" @@ -212,7 +201,7 @@ async def test_open(controller, projects_dir): # @pytest.mark.asyncio -#async def test_open_missing_compute(controller, tmpdir, demo_topology, http_client): +# async def test_open_missing_compute(controller, tmpdir, demo_topology, http_client): # """ # If a compute is missing the project should not be open and the .gns3 should # be the one before opening the project diff --git a/tests/controller/test_rbac.py b/tests/controller/test_rbac.py index 29fb38f4e..b332f20e0 100644 --- a/tests/controller/test_rbac.py +++ b/tests/controller/test_rbac.py @@ -51,7 +51,6 @@ pytestmark = pytest.mark.asyncio class TestPrivileges: - @pytest.mark.parametrize( "privilege, path, result", ( @@ -69,25 +68,14 @@ class TestPrivileges: ), ) async def test_default_privileges_user_group( - self, - test_user: User, - db_session: AsyncSession, - privilege: str, - path: str, - result: bool + self, test_user: User, db_session: AsyncSession, privilege: str, path: str, result: bool ) -> None: # add an ACE for path if result: group_id = (await UsersRepository(db_session).get_user_group_by_name("Users")).user_group_id role_id = (await RbacRepository(db_session).get_role_by_name("User")).role_id - ace = ACECreate( - path=path, - ace_type="group", - propagate=False, - group_id=str(group_id), - role_id=str(role_id) - ) + ace = ACECreate(path=path, ace_type="group", propagate=False, group_id=str(group_id), role_id=str(role_id)) await RbacRepository(db_session).create_ace(ace) authorized = await RbacRepository(db_session).check_user_has_privilege(test_user.user_id, path, privilege) @@ -122,13 +110,7 @@ class TestPrivileges: # privileges on deeper levels replace those inherited from an upper level. group_id = (await UsersRepository(db_session).get_user_group_by_name("Users")).user_group_id role_id = (await RbacRepository(db_session).get_role_by_name("User")).role_id - ace = ACECreate( - path=path, - ace_type="group", - propagate=False, - group_id=str(group_id), - role_id=str(role_id) - ) + ace = ACECreate(path=path, ace_type="group", propagate=False, group_id=str(group_id), role_id=str(role_id)) await RbacRepository(db_session).create_ace(ace) authorized = await RbacRepository(db_session).check_user_has_privilege(test_user.user_id, path, privilege) @@ -136,7 +118,6 @@ class TestPrivileges: class TestResourcePools: - async def test_resource_pool(self, test_user: User, db_session: AsyncSession): project_id = uuid.uuid4() @@ -157,7 +138,7 @@ class TestResourcePools: ace_type="group", propagate=False, group_id=str(group_id), - role_id=str(role_id) + role_id=str(role_id), ) await RbacRepository(db_session).create_ace(ace) @@ -167,12 +148,7 @@ class TestResourcePools: assert authorized is True async def test_list_projects_in_resource_pool( - self, - app: FastAPI, - controller: Controller, - base_client: AsyncClient, - db_session: AsyncSession, - test_user: User + self, app: FastAPI, controller: Controller, base_client: AsyncClient, db_session: AsyncSession, test_user: User ) -> None: # Clean up any existing ACEs from previous tests @@ -204,16 +180,16 @@ class TestResourcePools: ace_type="group", propagate=False, group_id=str(group_id), - role_id=str(role_id) + role_id=str(role_id), ) await RbacRepository(db_session).create_ace(ace) # Create a new client with test user authentication access_token = auth_service.create_access_token(test_user.username) async with AsyncClient( - base_url="http://test-api", - headers={"Content-Type": "application/json", "Authorization": f"Bearer {access_token}"}, - transport=ASGIWebSocketTransport(app=app) + base_url="http://test-api", + headers={"Content-Type": "application/json", "Authorization": f"Bearer {access_token}"}, + transport=ASGIWebSocketTransport(app=app), ) as user_client: # user should see only uuid2 (from resource pool) response = await user_client.get(app.url_path_for("get_projects")) @@ -228,11 +204,7 @@ class TestResourcePools: # Now give user access to /projects (in addition to resource pool) ace = ACECreate( - path="/projects", - ace_type="group", - propagate=True, - group_id=str(group_id), - role_id=str(role_id) + path="/projects", ace_type="group", propagate=True, group_id=str(group_id), role_id=str(role_id) ) await RbacRepository(db_session).create_ace(ace) @@ -277,66 +249,66 @@ class TestResourcePools: # permissions_in_db = await rbac_repo.get_user_permissions(test_user.user_id) # assert len(permissions_in_db) == 1 # assert permissions_in_db[0].path == f"/projects/{project_id}/*" - # - # response = await authorized_client.get(app.url_path_for("get_projects")) - # assert response.status_code == status.HTTP_200_OK - # projects = response.json() - # assert len(projects) == 1 +# +# response = await authorized_client.get(app.url_path_for("get_projects")) +# assert response.status_code == status.HTTP_200_OK +# projects = response.json() +# assert len(projects) == 1 - # async def test_admin_access_all_projects(self, app: FastAPI, client: AsyncClient): - # - # response = await client.get(app.url_path_for("get_projects")) - # assert response.status_code == status.HTTP_200_OK - # projects = response.json() - # assert len(projects) == 2 - # - # async def test_admin_user_give_permission_on_project( - # self, - # app: FastAPI, - # client: AsyncClient, - # test_user: User - # ): - # - # response = await client.get(app.url_path_for("get_projects")) - # assert response.status_code == status.HTTP_200_OK - # projects = response.json() - # project_id = None - # for project in projects: - # if project["name"] == "Admin project": - # project_id = project["project_id"] - # break - # - # new_permission = { - # "methods": ["GET"], - # "path": f"/projects/{project_id}", - # "action": "ALLOW" - # } - # response = await client.post(app.url_path_for("create_permission"), json=new_permission) - # assert response.status_code == status.HTTP_201_CREATED - # permission_id = response.json()["permission_id"] - # - # response = await client.put( - # app.url_path_for( - # "add_permission_to_user", - # user_id=test_user.user_id, - # permission_id=permission_id - # ) - # ) - # assert response.status_code == status.HTTP_204_NO_CONTENT - # - # async def test_user_access_admin_project( - # self, - # app: FastAPI, - # authorized_client: AsyncClient, - # test_user: User, - # db_session: AsyncSession - # ) -> None: - # - # response = await authorized_client.get(app.url_path_for("get_projects")) - # assert response.status_code == status.HTTP_200_OK - # projects = response.json() - # assert len(projects) == 2 - # +# async def test_admin_access_all_projects(self, app: FastAPI, client: AsyncClient): +# +# response = await client.get(app.url_path_for("get_projects")) +# assert response.status_code == status.HTTP_200_OK +# projects = response.json() +# assert len(projects) == 2 +# +# async def test_admin_user_give_permission_on_project( +# self, +# app: FastAPI, +# client: AsyncClient, +# test_user: User +# ): +# +# response = await client.get(app.url_path_for("get_projects")) +# assert response.status_code == status.HTTP_200_OK +# projects = response.json() +# project_id = None +# for project in projects: +# if project["name"] == "Admin project": +# project_id = project["project_id"] +# break +# +# new_permission = { +# "methods": ["GET"], +# "path": f"/projects/{project_id}", +# "action": "ALLOW" +# } +# response = await client.post(app.url_path_for("create_permission"), json=new_permission) +# assert response.status_code == status.HTTP_201_CREATED +# permission_id = response.json()["permission_id"] +# +# response = await client.put( +# app.url_path_for( +# "add_permission_to_user", +# user_id=test_user.user_id, +# permission_id=permission_id +# ) +# ) +# assert response.status_code == status.HTTP_204_NO_CONTENT +# +# async def test_user_access_admin_project( +# self, +# app: FastAPI, +# authorized_client: AsyncClient, +# test_user: User, +# db_session: AsyncSession +# ) -> None: +# +# response = await authorized_client.get(app.url_path_for("get_projects")) +# assert response.status_code == status.HTTP_200_OK +# projects = response.json() +# assert len(projects) == 2 +# # class TestTemplatesWithRbac: # diff --git a/tests/controller/test_snapshot.py b/tests/controller/test_snapshot.py index 8881b98f1..008d0bb57 100644 --- a/tests/controller/test_snapshot.py +++ b/tests/controller/test_snapshot.py @@ -67,7 +67,7 @@ def test_json(project): "project_id": project.id, "description": "Snapshot 'snapshot_test' taken on 2016-07-26 at 10:04:39", "filename": "snapshot_test_260716_100439.gns3project", - "created_at": 1469527479 + "created_at": 1469527479, } # new style snapshot @@ -78,9 +78,10 @@ def test_json(project): "project_id": project.id, "filename": "snapshot_test2.gns3snapshot", "description": mock.ANY, - "created_at": mock.ANY + "created_at": mock.ANY, } + def test_invalid_snapshot_filename(project): with pytest.raises(ValueError): diff --git a/tests/controller/test_statistics.py b/tests/controller/test_statistics.py index f184a92b8..eaf5c12f0 100644 --- a/tests/controller/test_statistics.py +++ b/tests/controller/test_statistics.py @@ -7,8 +7,10 @@ def test_server_uptime_uses_process_creation_time() -> None: process = MagicMock() process.create_time.return_value = 50.25 - with patch("gns3server.api.routes.controller.controller.time.time", return_value=200.75), \ - patch("gns3server.api.routes.controller.controller.psutil.Process", return_value=process): + with ( + patch("gns3server.api.routes.controller.controller.time.time", return_value=200.75), + patch("gns3server.api.routes.controller.controller.psutil.Process", return_value=process), + ): uptime = get_server_uptime_seconds() assert uptime == 150 diff --git a/tests/controller/test_symbols.py b/tests/controller/test_symbols.py index 17966193d..3856427fb 100644 --- a/tests/controller/test_symbols.py +++ b/tests/controller/test_symbols.py @@ -29,16 +29,16 @@ def test_list(symbols_dir): symbols = Symbols() assert { - 'symbol_id': ':/symbols/classic/firewall.svg', - 'filename': 'firewall.svg', - 'builtin': True, - 'theme': 'Classic' + "symbol_id": ":/symbols/classic/firewall.svg", + "filename": "firewall.svg", + "builtin": True, + "theme": "Classic", } in symbols.list() assert { - 'symbol_id': 'linux.svg', - 'filename': 'linux.svg', - 'builtin': False, - 'theme': 'Custom symbols' + "symbol_id": "linux.svg", + "filename": "linux.svg", + "builtin": False, + "theme": "Custom symbols", } in symbols.list() @@ -46,7 +46,7 @@ def test_get_path(): symbols = Symbols() symbols.theme = "Classic" - assert symbols.get_path(':/symbols/classic/firewall.svg') == get_resource("symbols/classic/firewall.svg") + assert symbols.get_path(":/symbols/classic/firewall.svg") == get_resource("symbols/classic/firewall.svg") def test_get_path_with_themed_symbols(): @@ -62,4 +62,4 @@ def test_get_size(): symbols = Symbols() symbols.theme = "Classic" - assert symbols.get_size(':/symbols/classic/firewall.svg') == (66, 45, 'svg') + assert symbols.get_size(":/symbols/classic/firewall.svg") == (66, 45, "svg") diff --git a/tests/controller/test_topology.py b/tests/controller/test_topology.py index bc87c1ee3..3419aa794 100644 --- a/tests/controller/test_topology.py +++ b/tests/controller/test_topology.py @@ -31,7 +31,7 @@ from gns3server.version import __version__ @pytest.mark.asyncio async def test_project_to_topology_empty(tmpdir): - with patch('gns3server.controller.project.Project.emit_controller_notification'): + with patch("gns3server.controller.project.Project.emit_controller_notification"): project = Project(name="Test") topo = project_to_topology(project) assert topo == { @@ -50,18 +50,13 @@ async def test_project_to_topology_empty(tmpdir): "snap_to_grid": False, "grid_size": 75, "drawing_grid_size": 25, - "topology": { - "nodes": [], - "links": [], - "computes": [], - "drawings": [] - }, + "topology": {"nodes": [], "links": [], "computes": [], "drawings": []}, "type": "topology", "supplier": None, "variables": None, "version": __version__, "marker_definitions": {}, - "created_by": None + "created_by": None, } @@ -94,14 +89,8 @@ async def test_basic_topology(controller): @pytest.mark.asyncio async def test_project_to_topology(controller): - variables = [ - {"name": "TEST1"}, - {"name": "TEST2", "value": "value1"} - ] - supplier = { - 'logo': 'logo.png', - 'url': 'http://example.com' - } + variables = [{"name": "TEST1"}, {"name": "TEST2", "value": "value1"}] + supplier = {"logo": "logo.png", "url": "http://example.com"} project = Project(name="Test", controller=controller) compute = Compute("my_compute", controller) @@ -119,14 +108,10 @@ def test_load_topology(tmpdir): "project_id": "69f26504-7aa3-48aa-9f29-798d44841211", "name": "Test", "revision": GNS3_FILE_FORMAT_REVISION, - "topology": { - "nodes": [], - "links": [], - "computes": [], - "drawings": [] - }, + "topology": {"nodes": [], "links": [], "computes": [], "drawings": []}, "type": "topology", - "version": __version__} + "version": __version__, + } path = str(tmpdir / "test.gns3") with open(path, "w+") as f: @@ -146,9 +131,7 @@ def test_load_topology_file_error_schema_error(tmpdir): path = str(tmpdir / "test.gns3") with open(path, "w+") as f: - json.dump({ - "revision": GNS3_FILE_FORMAT_REVISION - }, f) + json.dump({"revision": GNS3_FILE_FORMAT_REVISION}, f) with pytest.raises(ControllerError): load_topology(path) @@ -163,10 +146,10 @@ def test_load_newer_topology(tmpdir): "project_id": "69f26504-7aa3-48aa-9f29-798d44841211", "name": "Test", "revision": 42, - "topology": { - }, + "topology": {}, "type": "topology", - "version": __version__} + "version": __version__, + } path = str(tmpdir / "test.gns3") with open(path, "w+") as f: @@ -177,23 +160,16 @@ def test_load_newer_topology(tmpdir): def test_load_topology_with_variables(tmpdir): - variables = [ - {"name": "TEST1"}, - {"name": "TEST2", "value": "value1"} - ] + variables = [{"name": "TEST1"}, {"name": "TEST2", "value": "value1"}] data = { "project_id": "69f26504-7aa3-48aa-9f29-798d44841211", "name": "Test", "revision": GNS3_FILE_FORMAT_REVISION, - "topology": { - "nodes": [], - "links": [], - "computes": [], - "drawings": [] - }, + "topology": {"nodes": [], "links": [], "computes": [], "drawings": []}, "variables": variables, "type": "topology", - "version": __version__} + "version": __version__, + } path = str(tmpdir / "test.gns3") with open(path, "w+") as f: @@ -204,23 +180,16 @@ def test_load_topology_with_variables(tmpdir): def test_load_topology_with_supplier(tmpdir): - supplier = { - 'logo': 'logo.png', - 'url': 'http://example.com' - } + supplier = {"logo": "logo.png", "url": "http://example.com"} data = { "project_id": "69f26504-7aa3-48aa-9f29-798d44841211", "name": "Test", "revision": GNS3_FILE_FORMAT_REVISION, - "topology": { - "nodes": [], - "links": [], - "computes": [], - "drawings": [] - }, + "topology": {"nodes": [], "links": [], "computes": [], "drawings": []}, "supplier": supplier, "type": "topology", - "version": __version__} + "version": __version__, + } path = str(tmpdir / "test.gns3") with open(path, "w+") as f: diff --git a/tests/controller/test_udp_link.py b/tests/controller/test_udp_link.py index f8934b1b4..7c433a6fb 100644 --- a/tests/controller/test_udp_link.py +++ b/tests/controller/test_udp_link.py @@ -72,25 +72,33 @@ async def test_create(project): compute2.host = "example.org" await link.add_node(node2, 3, 1) - compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={ - "lport": 1024, - "rhost": "192.168.1.2", - "rport": 2048, - "type": "nio_udp", - "filters": {"delay": [10, 0]}, - "markers": {}, - "suspend": False, - }, timeout=120) + compute1.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), + data={ + "lport": 1024, + "rhost": "192.168.1.2", + "rport": 2048, + "type": "nio_udp", + "filters": {"delay": [10, 0]}, + "markers": {}, + "suspend": False, + }, + timeout=120, + ) - compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={ - "lport": 2048, - "rhost": "192.168.1.1", - "rport": 1024, - "type": "nio_udp", - "filters": {}, - "markers": {}, - "suspend": False, - }, timeout=120) + compute2.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), + data={ + "lport": 2048, + "rhost": "192.168.1.1", + "rport": 1024, + "type": "nio_udp", + "filters": {}, + "markers": {}, + "suspend": False, + }, + timeout=120, + ) @pytest.mark.asyncio @@ -142,27 +150,37 @@ async def test_create_one_side_failure(project): with pytest.raises(ControllerError): await link.add_node(node2, 3, 1) - compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={ - "lport": 1024, - "rhost": "192.168.1.2", - "rport": 2048, - "type": "nio_udp", - "filters": {}, - "markers": {}, - "suspend": False, - }, timeout=120) + compute1.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), + data={ + "lport": 1024, + "rhost": "192.168.1.2", + "rport": 2048, + "type": "nio_udp", + "filters": {}, + "markers": {}, + "suspend": False, + }, + timeout=120, + ) - compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={ - "lport": 2048, - "rhost": "192.168.1.1", - "rport": 1024, - "type": "nio_udp", - "filters": {}, - "markers": {}, - "suspend": False, - }, timeout=120) + compute2.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), + data={ + "lport": 2048, + "rhost": "192.168.1.1", + "rport": 1024, + "type": "nio_udp", + "filters": {}, + "markers": {}, + "suspend": False, + }, + timeout=120, + ) # The link creation has failed we rollback the nio - compute1.delete.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), timeout=120) + compute1.delete.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), timeout=120 + ) @pytest.mark.asyncio @@ -183,8 +201,12 @@ async def test_delete(project): await link.delete() - compute1.delete.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), timeout=120) - compute2.delete.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), timeout=120) + compute1.delete.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), timeout=120 + ) + compute2.delete.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), timeout=120 + ) @pytest.mark.asyncio @@ -286,15 +308,19 @@ async def test_reset(project): assert link.debug_link_data[0]["lport"] != link.debug_link_data[0]["rport"] assert link.debug_link_data[1]["lport"] != link.debug_link_data[1]["rport"] # the committed NIO carries the fresh pair, not the released one - compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={ - "lport": 4096, - "rhost": "192.168.1.2", - "rport": 8192, - "type": "nio_udp", - "filters": {}, - "markers": {}, - "suspend": False, - }, timeout=120) + compute1.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), + data={ + "lport": 4096, + "rhost": "192.168.1.2", + "rport": 8192, + "type": "nio_udp", + "filters": {}, + "markers": {}, + "suspend": False, + }, + timeout=120, + ) @pytest.mark.asyncio @@ -373,15 +399,17 @@ async def test_capture(project): await link.start_capture() assert link.capturing - compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/capture/start".format(project.id, node_vpcs.id), data={ - "capture_file_name": link.default_capture_file_name(), - "data_link_type": "DLT_EN10MB" - }) + compute1.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/capture/start".format(project.id, node_vpcs.id), + data={"capture_file_name": link.default_capture_file_name(), "data_link_type": "DLT_EN10MB"}, + ) await link.stop_capture() assert link.capturing is False - compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/capture/stop".format(project.id, node_vpcs.id)) + compute1.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/capture/stop".format(project.id, node_vpcs.id) + ) @pytest.mark.asyncio @@ -453,40 +481,49 @@ async def test_update(project): compute2.host = "example.org" await link.add_node(node2, 3, 1) - compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={ - "lport": 1024, - "rhost": "192.168.1.2", - "rport": 2048, - "type": "nio_udp", - "suspend": False, - "markers": {}, - "filters": {"delay": [10, 0]} - }, timeout=120) + compute1.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), + data={ + "lport": 1024, + "rhost": "192.168.1.2", + "rport": 2048, + "type": "nio_udp", + "suspend": False, + "markers": {}, + "filters": {"delay": [10, 0]}, + }, + timeout=120, + ) - compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={ - "lport": 2048, - "rhost": "192.168.1.1", - "rport": 1024, - "type": "nio_udp", - "suspend": False, - "markers": {}, - "filters": {} - }, timeout=120) + compute2.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), + data={ + "lport": 2048, + "rhost": "192.168.1.1", + "rport": 1024, + "type": "nio_udp", + "suspend": False, + "markers": {}, + "filters": {}, + }, + timeout=120, + ) assert link.created await link.update_filters({"frequency_drop": [5], "bpf": ["icmp[icmptype] == 8"]}) - compute1.put.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={ - "lport": 1024, - "rhost": "192.168.1.2", - "rport": 2048, - "type": "nio_udp", - "suspend": False, - "markers": {}, - "filters": { - "frequency_drop": [5], - "bpf": ["icmp[icmptype] == 8"] - } - }, timeout=120) + compute1.put.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), + data={ + "lport": 1024, + "rhost": "192.168.1.2", + "rport": 2048, + "type": "nio_udp", + "suspend": False, + "markers": {}, + "filters": {"frequency_drop": [5], "bpf": ["icmp[icmptype] == 8"]}, + }, + timeout=120, + ) @pytest.mark.asyncio @@ -533,8 +570,9 @@ async def test_update_ethernet_switch_nio(project): "type": "nio_udp", "suspend": False, "markers": {}, - "filters": {} - }, timeout=221 + "filters": {}, + }, + timeout=221, ) @@ -585,22 +623,30 @@ async def test_update_suspend(project): compute2.host = "example.org" await link.add_node(node2, 3, 1) - compute1.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), data={ - "lport": 1024, - "rhost": "192.168.1.2", - "rport": 2048, - "type": "nio_udp", - "filters": {"frequency_drop": [-1]}, - "markers": {}, - "suspend": True - }, timeout=120) + compute1.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/0/ports/4/nio".format(project.id, node1.id), + data={ + "lport": 1024, + "rhost": "192.168.1.2", + "rport": 2048, + "type": "nio_udp", + "filters": {"frequency_drop": [-1]}, + "markers": {}, + "suspend": True, + }, + timeout=120, + ) - compute2.post.assert_any_call("/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), data={ - "lport": 2048, - "rhost": "192.168.1.1", - "rport": 1024, - "type": "nio_udp", - "filters": {}, - "markers": {}, - "suspend": True - }, timeout=120) + compute2.post.assert_any_call( + "/projects/{}/vpcs/nodes/{}/adapters/3/ports/1/nio".format(project.id, node2.id), + data={ + "lport": 2048, + "rhost": "192.168.1.1", + "rport": 1024, + "type": "nio_udp", + "filters": {}, + "markers": {}, + "suspend": True, + }, + timeout=120, + ) diff --git a/tests/stress/benchmark_running_server.py b/tests/stress/benchmark_running_server.py index 9ee61ccc4..6400aebe1 100644 --- a/tests/stress/benchmark_running_server.py +++ b/tests/stress/benchmark_running_server.py @@ -32,23 +32,19 @@ async def create_test_projects(base_url, headers, count, prefix="perf_test_proje for i in range(batch_start, batch_end): name = f"{prefix}_{i}_{uuid.uuid4().hex[:8]}" - task = client.post( - f"{base_url}/projects", - headers=headers, - json={"name": name} - ) + task = client.post(f"{base_url}/projects", headers=headers, json={"name": name}) batch_tasks.append(task) results = await asyncio.gather(*batch_tasks, return_exceptions=True) for i, result in enumerate(results, batch_start): - if hasattr(result, 'status_code'): + if hasattr(result, "status_code"): if result.status_code == 201: - print(f" Created project {i+1}/{count}") + print(f" Created project {i + 1}/{count}") else: - print(f" Failed to create project {i+1}: {result.status_code}") + print(f" Failed to create project {i + 1}: {result.status_code}") else: - print(f" Error creating project {i+1}: {result}") + print(f" Error creating project {i + 1}: {result}") async def cleanup_test_projects(base_url, headers, prefix="perf_test_project"): @@ -62,7 +58,7 @@ async def cleanup_test_projects(base_url, headers, prefix="perf_test_project"): if response.status_code == 200: projects = response.json() - test_projects = [p for p in projects if p.get('name', '').startswith(prefix)] + test_projects = [p for p in projects if p.get("name", "").startswith(prefix)] print(f" Found {len(test_projects)} test projects to delete") batch_size = 50 @@ -72,21 +68,18 @@ async def cleanup_test_projects(base_url, headers, prefix="perf_test_project"): batch_tasks = [] for project in test_projects[batch_start:batch_end]: - task = client.delete( - f"{base_url}/projects/{project['project_id']}", - headers=headers - ) + task = client.delete(f"{base_url}/projects/{project['project_id']}", headers=headers) batch_tasks.append(task) results = await asyncio.gather(*batch_tasks, return_exceptions=True) for i, result in enumerate(results, batch_start): project = test_projects[i] - if hasattr(result, 'status_code') and result.status_code == 204: + if hasattr(result, "status_code") and result.status_code == 204: total_deleted += 1 elapsed = time.time() - cleanup_start - print(f" Deleted {total_deleted} projects in {elapsed:.1f}s ({total_deleted/elapsed:.0f} projects/sec)") + print(f" Deleted {total_deleted} projects in {elapsed:.1f}s ({total_deleted / elapsed:.0f} projects/sec)") except Exception as e: print(f"Error during cleanup: {type(e).__name__}: {e}") @@ -95,11 +88,11 @@ async def cleanup_test_projects(base_url, headers, prefix="perf_test_project"): async def benchmark_get_projects(base_url, headers, project_count, iterations=10, prefix="perf_test_project"): """Benchmark GET /projects endpoint""" - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(f"GET /projects Performance Benchmark") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"Server: {base_url}") - print(f"{'='*60}\n") + print(f"{'=' * 60}\n") if project_count > 0: await create_test_projects(base_url, headers, project_count, prefix) @@ -137,11 +130,11 @@ async def benchmark_get_projects(base_url, headers, project_count, iterations=10 projects = response.json() response_times.append(elapsed_ms) project_counts.append(len(projects)) - print(f" Iteration {i+1}: {elapsed_ms:.2f}ms ({len(projects)} projects)") + print(f" Iteration {i + 1}: {elapsed_ms:.2f}ms ({len(projects)} projects)") else: - print(f" Iteration {i+1}: ERROR {response.status_code}") + print(f" Iteration {i + 1}: ERROR {response.status_code}") except Exception as e: - print(f" Iteration {i+1}: Exception - {type(e).__name__}: {str(e)}") + print(f" Iteration {i + 1}: Exception - {type(e).__name__}: {str(e)}") # Cleanup test projects if project_count > 0: @@ -155,17 +148,17 @@ async def benchmark_get_projects(base_url, headers, project_count, iterations=10 median_time = sorted(response_times)[len(response_times) // 2] avg_project_count = sum(project_counts) / len(project_counts) if project_counts else 0 - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") if project_count > 0: print(f"Results for {project_count} projects (created specifically):") else: print(f"Results for existing projects (~{avg_project_count:.0f} per request):") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"Average: {avg_time:.2f}ms") print(f"Median: {median_time:.2f}ms") print(f"Min: {min_time:.2f}ms") print(f"Max: {max_time:.2f}ms") - print(f"{'='*60}") + print(f"{'=' * 60}") # Performance assessment if avg_time < 100: @@ -185,36 +178,43 @@ async def benchmark_get_projects(base_url, headers, project_count, iterations=10 total_seconds = sum(response_times) / 1000 projects_per_second = total_projects / total_seconds print(f"Throughput: {projects_per_second:.1f} projects/sec") - print(f"{'='*60}\n") + print(f"{'=' * 60}\n") return { - 'project_count': project_count, - 'avg_time_ms': avg_time, - 'median_time_ms': median_time, - 'min_time_ms': min_time, - 'max_time_ms': max_time, - 'projects_per_second': projects_per_second if project_counts and sum(response_times) > 0 else 0 + "project_count": project_count, + "avg_time_ms": avg_time, + "median_time_ms": median_time, + "min_time_ms": min_time, + "max_time_ms": max_time, + "projects_per_second": projects_per_second if project_counts and sum(response_times) > 0 else 0, } async def main(): - parser = argparse.ArgumentParser(description='Benchmark GET /projects performance on running server') - parser.add_argument('--host', type=str, default='127.0.0.1', help='GNS3 server host') - parser.add_argument('--port', type=int, default=3080, help='GNS3 server port') - parser.add_argument('--username', type=str, required=True, help='Username for authentication') - parser.add_argument('--password', type=str, required=True, help='Password for authentication') - parser.add_argument('--project-counts', type=int, nargs='+', default=[10, 50, 100], - help='Numbers of projects to test (default: 10 50 100)') - parser.add_argument('--iterations', type=int, default=10, - help='Number of iterations per test (default: 10)') - parser.add_argument('--full-scale', action='store_true', - help='Run full scale test: 10, 50, 100, 200, 500 projects') - parser.add_argument('--no-cleanup', action='store_true', - help='Do not create/cleanup test projects (use existing projects)') - parser.add_argument('--prefix', type=str, default='perf_test_project', - help='Project name prefix for test/cleanup (default: perf_test_project)') - parser.add_argument('--cleanup-only', action='store_true', - help='Only clean up test projects, do not run benchmark') + parser = argparse.ArgumentParser(description="Benchmark GET /projects performance on running server") + parser.add_argument("--host", type=str, default="127.0.0.1", help="GNS3 server host") + parser.add_argument("--port", type=int, default=3080, help="GNS3 server port") + parser.add_argument("--username", type=str, required=True, help="Username for authentication") + parser.add_argument("--password", type=str, required=True, help="Password for authentication") + parser.add_argument( + "--project-counts", + type=int, + nargs="+", + default=[10, 50, 100], + help="Numbers of projects to test (default: 10 50 100)", + ) + parser.add_argument("--iterations", type=int, default=10, help="Number of iterations per test (default: 10)") + parser.add_argument("--full-scale", action="store_true", help="Run full scale test: 10, 50, 100, 200, 500 projects") + parser.add_argument( + "--no-cleanup", action="store_true", help="Do not create/cleanup test projects (use existing projects)" + ) + parser.add_argument( + "--prefix", + type=str, + default="perf_test_project", + help="Project name prefix for test/cleanup (default: perf_test_project)", + ) + parser.add_argument("--cleanup-only", action="store_true", help="Only clean up test projects, do not run benchmark") args = parser.parse_args() @@ -227,8 +227,7 @@ async def main(): try: async with httpx.AsyncClient() as client: login_response = await client.post( - f"{base_url}/access/users/login", - data={"username": args.username, "password": args.password} + f"{base_url}/access/users/login", data={"username": args.username, "password": args.password} ) if login_response.status_code != 200: @@ -274,13 +273,13 @@ async def main(): # Summary if len(results) > 1: - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print("Summary - Performance Scaling") - print(f"{'='*60}") + print(f"{'=' * 60}") print(f"{'Projects':<12} {'Avg (ms)':<12} {'Status':<12}") - print(f"{'-'*40}") + print(f"{'-' * 40}") for r in results: - avg = r['avg_time_ms'] + avg = r["avg_time_ms"] if avg < 100: status = "EXCELLENT" elif avg < 500: @@ -290,7 +289,7 @@ async def main(): else: status = "CRITICAL" print(f"{r['project_count']:<12} {avg:<12.2f} {status:<12}") - print(f"{'='*60}\n") + print(f"{'=' * 60}\n") if __name__ == "__main__": diff --git a/tests/stress/telnet_race_condition_test.py b/tests/stress/telnet_race_condition_test.py index 59960adab..05d245201 100644 --- a/tests/stress/telnet_race_condition_test.py +++ b/tests/stress/telnet_race_condition_test.py @@ -18,10 +18,7 @@ from typing import List, Optional import sys # Configure logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s' -) +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") log = logging.getLogger(__name__) @@ -41,8 +38,7 @@ class TelnetClient: try: log.debug(f"Client {self.client_id}: Connecting to {self.host}:{self.port}...") self.reader, self.writer = await asyncio.wait_for( - asyncio.open_connection(self.host, self.port), - timeout=5.0 + asyncio.open_connection(self.host, self.port), timeout=5.0 ) self.connected = True log.debug(f"Client {self.client_id}: Connected successfully") @@ -57,7 +53,7 @@ class TelnetClient: return False try: - self.writer.write(command.encode() + b'\r\n') + self.writer.write(command.encode() + b"\r\n") await asyncio.wait_for(self.writer.drain(), timeout=2.0) log.debug(f"Client {self.client_id}: Sent command: {command.strip()}") return True @@ -73,7 +69,7 @@ class TelnetClient: try: data = await asyncio.wait_for(self.reader.read(1024), timeout=timeout) if data: - response = data.decode('utf-8', errors='ignore') + response = data.decode("utf-8", errors="ignore") log.debug(f"Client {self.client_id}: Received: {response[:50]}...") return response except asyncio.TimeoutError: @@ -121,7 +117,7 @@ async def rapid_fire_client( max_delay: float = 0.01, receive_before_disconnect: bool = False, immediate_disconnect: bool = True, - device_type: str = "iou-l3" + device_type: str = "iou-l3", ): """ A client that rapidly connects, sends commands, and disconnects. @@ -220,13 +216,7 @@ async def rapid_fire_client( return success_count, fail_count -async def long_lived_client( - client_id: int, - host: str, - port: str, - duration: float, - send_interval: float = 1.0 -): +async def long_lived_client(client_id: int, host: str, port: str, duration: float, send_interval: float = 1.0): """ A long-lived client that stays connected and periodically sends commands. @@ -273,7 +263,7 @@ async def run_stress_test( long_lived_clients: int, iterations_per_client: int, test_duration: float, - device_type: str = "iou-l3" + device_type: str = "iou-l3", ): """ Run the stress test with multiple concurrent clients. @@ -306,13 +296,7 @@ async def run_stress_test( # Start long-lived clients first (simulate web console users) for i in range(long_lived_clients): task = asyncio.create_task( - long_lived_client( - client_id=1000 + i, - host=host, - port=port, - duration=test_duration, - send_interval=2.0 - ) + long_lived_client(client_id=1000 + i, host=host, port=port, duration=test_duration, send_interval=2.0) ) tasks.append(task) await asyncio.sleep(0.1) # Stagger connections @@ -332,10 +316,10 @@ async def run_stress_test( port=port, iterations=iterations_per_client, min_delay=0.001, # 1ms - very fast - max_delay=0.01, # 10ms - still fast + max_delay=0.01, # 10ms - still fast receive_before_disconnect=False, # Don't wait for response immediate_disconnect=True, # Abrupt close - device_type=device_type + device_type=device_type, ) ) rapid_tasks.append(task) @@ -393,62 +377,31 @@ Device Types: iou-l3 - Cisco IOS L3 router (uses show/run/write commands that trigger broadcast) vpcs - VPCS simulator (simple commands) generic - Generic device (basic test commands) - """ + """, ) - parser.add_argument( - '--host', - default='127.0.0.1', - help='Telnet server host (default: 127.0.0.1)' - ) + parser.add_argument("--host", default="127.0.0.1", help="Telnet server host (default: 127.0.0.1)") + + parser.add_argument("--port", type=int, default=2000, help="Telnet server port (default: 2000)") parser.add_argument( - '--port', - type=int, - default=2000, - help='Telnet server port (default: 2000)' + "--rapid-clients", type=int, default=10, help="Number of rapid connect/disconnect clients (default: 10)" ) - parser.add_argument( - '--rapid-clients', - type=int, - default=10, - help='Number of rapid connect/disconnect clients (default: 10)' - ) + parser.add_argument("--long-lived", type=int, default=2, help="Number of long-lived clients (default: 2)") + + parser.add_argument("--iterations", type=int, default=50, help="Iterations per rapid client (default: 50)") + + parser.add_argument("--duration", type=float, default=30.0, help="Test duration in seconds (default: 30.0)") parser.add_argument( - '--long-lived', - type=int, - default=2, - help='Number of long-lived clients (default: 2)' + "--device-type", + default="iou-l3", + choices=["iou-l3", "vpcs", "generic"], + help="Device type for commands (default: iou-l3)", ) - parser.add_argument( - '--iterations', - type=int, - default=50, - help='Iterations per rapid client (default: 50)' - ) - - parser.add_argument( - '--duration', - type=float, - default=30.0, - help='Test duration in seconds (default: 30.0)' - ) - - parser.add_argument( - '--device-type', - default='iou-l3', - choices=['iou-l3', 'vpcs', 'generic'], - help='Device type for commands (default: iou-l3)' - ) - - parser.add_argument( - '--verbose', - action='store_true', - help='Enable verbose logging' - ) + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") args = parser.parse_args() @@ -456,19 +409,21 @@ Device Types: logging.getLogger().setLevel(logging.DEBUG) try: - asyncio.run(run_stress_test( - host=args.host, - port=args.port, - rapid_clients=args.rapid_clients, - long_lived_clients=args.long_lived, - iterations_per_client=args.iterations, - test_duration=args.duration, - device_type=args.device_type - )) + asyncio.run( + run_stress_test( + host=args.host, + port=args.port, + rapid_clients=args.rapid_clients, + long_lived_clients=args.long_lived, + iterations_per_client=args.iterations, + test_duration=args.duration, + device_type=args.device_type, + ) + ) except KeyboardInterrupt: log.info("Test interrupted by user") sys.exit(0) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tests/test_config.py b/tests/test_config.py index 3537ae76c..64cf66d7a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -60,40 +60,28 @@ def write_config(tmpdir, settings): @pytest.mark.parametrize( "setting, value, result", ( - ("allowed_interfaces", "", []), - ("allowed_interfaces", "eth0", ["eth0"]), - ("allowed_interfaces", "eth1,eth2", ["eth1", "eth2"]), - ("additional_images_paths", "", []), - ("additional_images_paths", "/path/to/dir1", ["/path/to/dir1"]), - ("additional_images_paths", "/path/to/dir1;/path/to/dir2", ["/path/to/dir1", "/path/to/dir2"]) - ) + ("allowed_interfaces", "", []), + ("allowed_interfaces", "eth0", ["eth0"]), + ("allowed_interfaces", "eth1,eth2", ["eth1", "eth2"]), + ("additional_images_paths", "", []), + ("additional_images_paths", "/path/to/dir1", ["/path/to/dir1"]), + ("additional_images_paths", "/path/to/dir1;/path/to/dir2", ["/path/to/dir1", "/path/to/dir2"]), + ), ) def test_server_settings_to_list(tmpdir, setting: str, value: str, result: str): - config = load_config(tmpdir, { - "Server": { - setting: value - } - }) + config = load_config(tmpdir, {"Server": {setting: value}}) assert config.settings.model_dump(exclude_unset=True)["Server"][setting] == result def test_reload(tmpdir): - config = load_config(tmpdir, { - "Server": { - "host": "127.0.0.1" - } - }) + config = load_config(tmpdir, {"Server": {"host": "127.0.0.1"}}) assert config.settings.Server.host == "127.0.0.1" - write_config(tmpdir, { - "Server": { - "host": "192.168.1.2" - } - }) + write_config(tmpdir, {"Server": {"host": "192.168.1.2"}}) config.reload() assert config.settings.Server.host == "192.168.1.2" @@ -110,24 +98,24 @@ def test_server_password_hidden(): @pytest.mark.parametrize( "settings, exception_expected", ( - ({"protocol": "https1"}, True), - ({"console_start_port_range": 15000, "console_end_port_range": 20000}, False), - ({"console_start_port_range": 0}, True), - ({"console_start_port_range": 68000}, True), - ({"console_end_port_range": 15000}, False), - ({"console_end_port_range": 0}, True), - ({"console_end_port_range": 68000}, True), - ({"console_start_port_range": 10000, "console_end_port_range": 5000}, True), - ({"vnc_console_start_port_range": 6000}, False), - ({"vnc_console_start_port_range": 1000}, True), - ({"vnc_console_end_port_range": 6000}, False), - ({"vnc_console_end_port_range": 1000}, True), - ({"vnc_console_start_port_range": 7000, "vnc_console_end_port_range": 6000}, True), - ({"enable_ssl": True, "certfile": "/path/to/certfile", "certkey": "/path/to/certkey"}, True), - ({"enable_ssl": True}, True), - ({"enable_ssl": True, "certfile": "/path/to/certfile"}, True), - ({"enable_ssl": True, "certkey": "/path/to/certkey"}, True) - ) + ({"protocol": "https1"}, True), + ({"console_start_port_range": 15000, "console_end_port_range": 20000}, False), + ({"console_start_port_range": 0}, True), + ({"console_start_port_range": 68000}, True), + ({"console_end_port_range": 15000}, False), + ({"console_end_port_range": 0}, True), + ({"console_end_port_range": 68000}, True), + ({"console_start_port_range": 10000, "console_end_port_range": 5000}, True), + ({"vnc_console_start_port_range": 6000}, False), + ({"vnc_console_start_port_range": 1000}, True), + ({"vnc_console_end_port_range": 6000}, False), + ({"vnc_console_end_port_range": 1000}, True), + ({"vnc_console_start_port_range": 7000, "vnc_console_end_port_range": 6000}, True), + ({"enable_ssl": True, "certfile": "/path/to/certfile", "certkey": "/path/to/certkey"}, True), + ({"enable_ssl": True}, True), + ({"enable_ssl": True, "certfile": "/path/to/certfile"}, True), + ({"enable_ssl": True, "certkey": "/path/to/certkey"}, True), + ), ) def test_server_settings(settings: dict, exception_expected: bool): @@ -143,13 +131,13 @@ def test_server_settings(settings: dict, exception_expected: bool): @pytest.mark.parametrize( "settings, exception_expected", ( - ({"vmnet_start_range": 0}, True), - ({"vmnet_start_range": 256}, True), - ({"vmnet_end_range": 0}, True), - ({"vmnet_end_range": 256}, True), - ({"vmnet_start_range": 2, "vmnet_end_range": 10}, False), - ({"vmnet_start_range": 5, "vmnet_end_range": 3}, True) - ) + ({"vmnet_start_range": 0}, True), + ({"vmnet_start_range": 256}, True), + ({"vmnet_end_range": 0}, True), + ({"vmnet_end_range": 256}, True), + ({"vmnet_start_range": 2, "vmnet_end_range": 10}, False), + ({"vmnet_start_range": 5, "vmnet_end_range": 3}, True), + ), ) def test_vmware_settings(settings: dict, exception_expected: bool): @@ -169,15 +157,17 @@ def test_update_config_writes_ini_types(tmpdir): f.write("# a comment\n[Server]\nhost = 127.0.0.1\nfrobnicate = 42\n") config = Config(files=[path]) - changed = config.update_config({ - "Server": { - "port": 3081, - "report_errors": False, - "allowed_interfaces": ["eth0", "eth1"], - "default_symbol_theme": "Classic", - "additional_images_paths": ["/path/to/dir1", "/path/to/dir2"], + changed = config.update_config( + { + "Server": { + "port": 3081, + "report_errors": False, + "allowed_interfaces": ["eth0", "eth1"], + "default_symbol_theme": "Classic", + "additional_images_paths": ["/path/to/dir1", "/path/to/dir2"], + } } - }) + ) parsed = configparser.ConfigParser() parsed.read(path) diff --git a/tests/test_server.py b/tests/test_server.py index 14829349f..da4abd306 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -33,12 +33,12 @@ def test_locale_check(): except: # Locale is not available on the server return Server._locale_check() - assert locale.getlocale() == ('fr_FR', 'UTF-8') + assert locale.getlocale() == ("fr_FR", "UTF-8") def parse_arguments(server, argv): - parser, args= parse(argv) + parser, args = parse(argv) return server._load_config_and_set_defaults(parser, args, argv) @@ -87,7 +87,7 @@ def test_parse_arguments(capsys, config, tmpdir): assert parse_arguments(server, []).port == 8003 assert parse_arguments(server, ["--ssl"]).ssl - assert parse_arguments(server,[]).ssl is False + assert parse_arguments(server, []).ssl is False with tempfile.NamedTemporaryFile(dir=str(tmpdir)) as f: server_config.certfile = f.name server_config.certkey = f.name @@ -95,28 +95,28 @@ def test_parse_arguments(capsys, config, tmpdir): assert parse_arguments(server, []).ssl assert parse_arguments(server, ["--certfile", "bla"]).certfile == "bla" - assert parse_arguments(server,["--certkey", "blu"]).certkey == "blu" + assert parse_arguments(server, ["--certkey", "blu"]).certkey == "blu" - assert parse_arguments(server,["-L"]).local - assert parse_arguments(server,["--local"]).local + assert parse_arguments(server, ["-L"]).local + assert parse_arguments(server, ["--local"]).local server_config.local = False - assert parse_arguments(server,[]).local is False + assert parse_arguments(server, []).local is False server_config.local = True - assert parse_arguments(server,[]).local + assert parse_arguments(server, []).local - assert parse_arguments(server,["-A"]).allow - assert parse_arguments(server,["--allow"]).allow - assert parse_arguments(server,[]).allow is False + assert parse_arguments(server, ["-A"]).allow + assert parse_arguments(server, ["--allow"]).allow + assert parse_arguments(server, []).allow is False server_config.allow_remote_console = True - assert parse_arguments(server,[]).allow + assert parse_arguments(server, []).allow - assert parse_arguments(server,["-q"]).quiet - assert parse_arguments(server,["--quiet"]).quiet - assert parse_arguments(server,[]).quiet is False + assert parse_arguments(server, ["-q"]).quiet + assert parse_arguments(server, ["--quiet"]).quiet + assert parse_arguments(server, []).quiet is False - assert parse_arguments(server,["-d"]).debug - assert parse_arguments(server,["--debug"]).debug - assert parse_arguments(server,[]).debug is False + assert parse_arguments(server, ["-d"]).debug + assert parse_arguments(server, ["--debug"]).debug + assert parse_arguments(server, []).debug is False def test_set_config_with_args(tmpdir): @@ -126,18 +126,23 @@ def test_set_config_with_args(tmpdir): with tempfile.NamedTemporaryFile(dir=str(tmpdir)) as f: certfile = f.name certkey = f.name - args = parse_arguments(server, ["--host", - "192.168.1.1", - "--local", - "--allow", - "--port", - "8001", - "--ssl", - "--certfile", - certfile, - "--certkey", - certkey, - "--debug"]) + args = parse_arguments( + server, + [ + "--host", + "192.168.1.1", + "--local", + "--allow", + "--port", + "8001", + "--ssl", + "--certfile", + certfile, + "--certkey", + certkey, + "--debug", + ], + ) server._set_config_defaults_from_command_line(args) server_config = config.settings.Server diff --git a/tests/test_topologies.py b/tests/test_topologies.py index 1033a5651..e763d47b9 100644 --- a/tests/test_topologies.py +++ b/tests/test_topologies.py @@ -67,7 +67,9 @@ def test_convert(directory, tmpdir): assert work_topology if "revision" not in before_topology or before_topology["revision"] < GNS3_FILE_FORMAT_REVISION: - assert os.path.exists(os.path.join(work_directory, gns3_file + ".backup{}".format(before_topology.get("revision", 0)))) + assert os.path.exists( + os.path.join(work_directory, gns3_file + ".backup{}".format(before_topology.get("revision", 0))) + ) # We should have the same file in after directory and the work directory for root, dirs, files in os.walk(after_directory): @@ -80,10 +82,14 @@ def test_convert(directory, tmpdir): if file_path.endswith(".gns3project"): size = os.stat(file_path).st_size other_size = os.stat(os.path.join(os.path.join(root, file))).st_size - assert size in range(other_size - 100, other_size + 100), "File {} is different".format(os.path.join(directory, file)) + assert size in range(other_size - 100, other_size + 100), "File {} is different".format( + os.path.join(directory, file) + ) # For non .gns3 file we check if the file are the same elif not file_path.endswith(".gns3"): - assert os.stat(file_path).st_size == os.stat(os.path.join(os.path.join(root, file))).st_size, "File {} is different".format(os.path.join(directory, file)) + assert os.stat(file_path).st_size == os.stat(os.path.join(os.path.join(root, file))).st_size, ( + "File {} is different".format(os.path.join(directory, file)) + ) # Check if we don't have unexpected file in work directory for root, dirs, files in os.walk(work_directory): @@ -109,7 +115,13 @@ def compare_dict(path, source, reference): for key in sorted(reference.keys()): val = reference[key] assert key in source, "{} is missing in {}".format(key, source) - if isinstance(val, str) or isinstance(val, float) or isinstance(val, int) or isinstance(val, bool) or val is None: + if ( + isinstance(val, str) + or isinstance(val, float) + or isinstance(val, int) + or isinstance(val, bool) + or val is None + ): if val == "ANYSTR": pass elif val == "ANYUUID" and len(source[key]) == 36: @@ -125,7 +137,9 @@ def compare_dict(path, source, reference): elif isinstance(val, dict): compare_dict(path + key + "/", source[key], val) elif isinstance(val, list): - assert len(val) == len(source[key]), "Not enough value in {} ({}/{}) it shoud be {} not {}".format(key, len(val), len(source[key]), val, source[key]) + assert len(val) == len(source[key]), "Not enough value in {} ({}/{}) it shoud be {} not {}".format( + key, len(val), len(source[key]), val, source[key] + ) for idx, element in enumerate(source[key]): if isinstance(element, dict): compare_dict(path + key + "/", element, val[idx]) diff --git a/tests/test_utils.py b/tests/test_utils.py index 2839fb587..9662d9956 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -36,16 +36,16 @@ def test_int_to_macaddress(): def test_parse_version(): - assert parse_version('1') == ('000001', '00000', '000000', 'final') - assert parse_version('1.3') == ('000001', '000003', '000000', 'final') - assert parse_version('1.3.dev3') == ('000001', '000003', '000000', 'dev', '000003') - assert parse_version('1.3a1') == ('000001', '000003', '000000', 'a', '000001') - assert parse_version('1.3rc1') == ('000001', '000003', '000000', 'c', '000001') + assert parse_version("1") == ("000001", "00000", "000000", "final") + assert parse_version("1.3") == ("000001", "000003", "000000", "final") + assert parse_version("1.3.dev3") == ("000001", "000003", "000000", "dev", "000003") + assert parse_version("1.3a1") == ("000001", "000003", "000000", "a", "000001") + assert parse_version("1.3rc1") == ("000001", "000003", "000000", "c", "000001") - assert parse_version('1.2.3') > parse_version('1.2.2') - assert parse_version('1.3') > parse_version('1.2.2') - assert parse_version('1.3') > parse_version('1.3alpha1') - assert parse_version('1.3') > parse_version('1.3rc1') - assert parse_version('1.3rc1') > parse_version('1.3alpha3') - assert parse_version('1.3dev1') > parse_version('1.3rc1') - assert parse_version('1.2.3') > parse_version('1.2') + assert parse_version("1.2.3") > parse_version("1.2.2") + assert parse_version("1.3") > parse_version("1.2.2") + assert parse_version("1.3") > parse_version("1.3alpha1") + assert parse_version("1.3") > parse_version("1.3rc1") + assert parse_version("1.3rc1") > parse_version("1.3alpha3") + assert parse_version("1.3dev1") > parse_version("1.3rc1") + assert parse_version("1.2.3") > parse_version("1.2") diff --git a/tests/utils.py b/tests/utils.py index 6e4ade7d0..5e640e478 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -22,7 +22,6 @@ import unittest.mock class _asyncio_patch: - """ A wrapper around python patch supporting asyncio. Like the original patch you can use it as context @@ -52,11 +51,13 @@ class _asyncio_patch: def asyncio_patch(function, *args, **kwargs): return _asyncio_patch(function, *args, **kwargs) + # monkey patch MagicMock, # FIXME: would probably be better to use asynctest or pytest.asyncio to test our asyncio code async def async_magic(): pass + unittest.mock.MagicMock.__await__ = lambda x: async_magic().__await__() diff --git a/tests/utils/asyncio/test_embed_shell.py b/tests/utils/asyncio/test_embed_shell.py index 706292c34..930151e10 100644 --- a/tests/utils/asyncio/test_embed_shell.py +++ b/tests/utils/asyncio/test_embed_shell.py @@ -17,9 +17,9 @@ import asyncio -#from gns3server.utils.asyncio.embed_shell import EmbedShell +# from gns3server.utils.asyncio.embed_shell import EmbedShell -#FIXME: this is broken with recent Python >= 3.6 +# FIXME: this is broken with recent Python >= 3.6 # def test_embed_shell_help(async_run): # class Application(EmbedShell): # diff --git a/tests/utils/asyncio/test_ssh_server.py b/tests/utils/asyncio/test_ssh_server.py index 2a9ca5a7a..7f6d094a9 100644 --- a/tests/utils/asyncio/test_ssh_server.py +++ b/tests/utils/asyncio/test_ssh_server.py @@ -11,6 +11,7 @@ from gns3server.utils.asyncio.ssh_server import AsyncioSSHServer # Helpers # --------------------------------------------------------------------------- + class DummyUpstreamWriter: def __init__(self): self.buffer = bytearray() @@ -49,15 +50,14 @@ async def _wait_for_session_count(server, expected_count, timeout=2): if last_count == expected_count: return await asyncio.sleep(0.05) - assert last_count == expected_count, ( - f"Expected {expected_count} sessions, got {last_count} after {timeout}s" - ) + assert last_count == expected_count, f"Expected {expected_count} sessions, got {last_count} after {timeout}s" # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_ssh_server_forwards_client_input_to_upstream_writer(): """Data typed by a connected SSH client must reach the upstream writer.""" diff --git a/tests/utils/asyncio/test_telnet_server.py b/tests/utils/asyncio/test_telnet_server.py index d91fb91c9..08c2d8acd 100644 --- a/tests/utils/asyncio/test_telnet_server.py +++ b/tests/utils/asyncio/test_telnet_server.py @@ -113,9 +113,7 @@ async def test_telnet_server_naws_callback_uses_negotiated_dimensions(): client_writer = None try: - _, client_writer = await telnetlib3.open_connection( - "127.0.0.1", port, encoding=False, cols=132, rows=44 - ) + _, client_writer = await telnetlib3.open_connection("127.0.0.1", port, encoding=False, cols=132, rows=44) await asyncio.wait_for(callback_event.wait(), timeout=2) assert callback_result == {"columns": 132, "rows": 44} diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py index cb59bfe39..f196d65c5 100644 --- a/tests/utils/test_asyncio.py +++ b/tests/utils/test_asyncio.py @@ -22,7 +22,6 @@ import pytest from gns3server.utils.asyncio import wait_run_in_executor, subprocess_check_output, locking - @pytest.mark.asyncio async def test_wait_run_in_executor(): @@ -59,7 +58,6 @@ async def test_lock_decorator(): """ class TestLock: - def __init__(self): self._test_val = 0 @@ -72,4 +70,9 @@ async def test_lock_decorator(): i = TestLock() res = set(await asyncio.gather(i.method_to_lock(), i.method_to_lock())) - assert res == set((0, 1,)) # We use a set to test this to avoid order issue + assert res == set( + ( + 0, + 1, + ) + ) # We use a set to test this to avoid order issue diff --git a/tests/utils/test_file_watcher.py b/tests/utils/test_file_watcher.py index 003d6162c..83b9761ec 100644 --- a/tests/utils/test_file_watcher.py +++ b/tests/utils/test_file_watcher.py @@ -23,7 +23,7 @@ from unittest.mock import MagicMock from gns3server.utils.file_watcher import FileWatcher -@pytest.mark.parametrize("strategy", ['mtime', 'hash']) +@pytest.mark.parametrize("strategy", ["mtime", "hash"]) @pytest.mark.asyncio async def test_file_watcher(tmpdir, strategy): @@ -38,7 +38,7 @@ async def test_file_watcher(tmpdir, strategy): callback.assert_called_with(str(file)) -@pytest.mark.parametrize("strategy", ['mtime', 'hash']) +@pytest.mark.parametrize("strategy", ["mtime", "hash"]) @pytest.mark.asyncio async def test_file_watcher_not_existing(tmpdir, strategy): @@ -52,7 +52,7 @@ async def test_file_watcher_not_existing(tmpdir, strategy): callback.assert_called_with(str(file)) -@pytest.mark.parametrize("strategy", ['mtime', 'hash']) +@pytest.mark.parametrize("strategy", ["mtime", "hash"]) @pytest.mark.asyncio async def test_file_watcher_list(tmpdir, strategy): diff --git a/tests/utils/test_images.py b/tests/utils/test_images.py index 9b0cabaac..7fa9a86c1 100644 --- a/tests/utils/test_images.py +++ b/tests/utils/test_images.py @@ -49,48 +49,48 @@ def test_images_directories(tmpdir, config): def test_md5sum(tmpdir): - fake_img = str(tmpdir / 'hello载') + fake_img = str(tmpdir / "hello载") - with open(fake_img, 'w+') as f: - f.write('hello') + with open(fake_img, "w+") as f: + f.write("hello") - assert md5sum(fake_img) == '5d41402abc4b2a76b9719d911017c592' - with open(str(tmpdir / 'hello载.md5sum')) as f: - assert f.read() == '5d41402abc4b2a76b9719d911017c592' + assert md5sum(fake_img) == "5d41402abc4b2a76b9719d911017c592" + with open(str(tmpdir / "hello载.md5sum")) as f: + assert f.read() == "5d41402abc4b2a76b9719d911017c592" def test_md5sum_stopped_event(tmpdir): - fake_img = str(tmpdir / 'hello_stopped') - with open(fake_img, 'w+') as f: - f.write('hello') + fake_img = str(tmpdir / "hello_stopped") + with open(fake_img, "w+") as f: + f.write("hello") event = threading.Event() event.set() assert md5sum(fake_img, stopped_event=event) is None - assert not os.path.exists(str(tmpdir / 'hello_stopped.md5sum')) + assert not os.path.exists(str(tmpdir / "hello_stopped.md5sum")) def test_md5sum_existing_digest(tmpdir): - fake_img = str(tmpdir / 'hello') + fake_img = str(tmpdir / "hello") - with open(fake_img, 'w+') as f: - f.write('hello') + with open(fake_img, "w+") as f: + f.write("hello") - with open(str(tmpdir / 'hello.md5sum'), 'w+') as f: - f.write('aaaaa02abc4b2a76b9719d911017c592') + with open(str(tmpdir / "hello.md5sum"), "w+") as f: + f.write("aaaaa02abc4b2a76b9719d911017c592") - assert md5sum(fake_img) == 'aaaaa02abc4b2a76b9719d911017c592' + assert md5sum(fake_img) == "aaaaa02abc4b2a76b9719d911017c592" def test_md5sum_existing_digest_but_missing_image(tmpdir): - fake_img = str(tmpdir / 'hello') + fake_img = str(tmpdir / "hello") - with open(str(tmpdir / 'hello.md5sum'), 'w+') as f: - f.write('aaaaa02abc4b2a76b9719d911017c592') + with open(str(tmpdir / "hello.md5sum"), "w+") as f: + f.write("aaaaa02abc4b2a76b9719d911017c592") assert md5sum(fake_img) is None @@ -102,13 +102,13 @@ def test_md5sum_none(tmpdir): def test_remove_checksum(tmpdir): - with open(str(tmpdir / 'hello.md5sum'), 'w+') as f: - f.write('aaaaa02abc4b2a76b9719d911017c592') - remove_checksum(str(tmpdir / 'hello')) + with open(str(tmpdir / "hello.md5sum"), "w+") as f: + f.write("aaaaa02abc4b2a76b9719d911017c592") + remove_checksum(str(tmpdir / "hello")) - assert not os.path.exists(str(tmpdir / 'hello.md5sum')) + assert not os.path.exists(str(tmpdir / "hello.md5sum")) - remove_checksum(str(tmpdir / 'not_exists')) + remove_checksum(str(tmpdir / "not_exists")) @pytest.mark.asyncio @@ -116,34 +116,33 @@ async def test_list_images(tmpdir, config): # IOS image in the images directory ios_image_1 = tmpdir / "images1" / "IOS" / "ios_image_1.image" - ios_image_1.write(b'\x7fELF\x01\x02\x01', ensure=True) + ios_image_1.write(b"\x7fELF\x01\x02\x01", ensure=True) ios_image_1 = force_unix_path(str(ios_image_1)) # IOS image in an additional images path ios_image_2 = tmpdir / "images2" / "ios_image_2.image" - ios_image_2.write(b'\x7fELF\x01\x02\x01', ensure=True) + ios_image_2.write(b"\x7fELF\x01\x02\x01", ensure=True) ios_image_2 = force_unix_path(str(ios_image_2)) # Not a valid elf file not_elf_file = tmpdir / "images1" / "IOS" / "not_elf.image" - not_elf_file.write(b'NOTANELF', ensure=True) + not_elf_file.write(b"NOTANELF", ensure=True) not_elf_file = force_unix_path(str(not_elf_file)) # Invalid image because it is very small small_file = tmpdir / "images1" / "too_small.image" - small_file.write(b'1', ensure=True) + small_file.write(b"1", ensure=True) if sys.platform.startswith("linux"): # 64-bit IOU image iou_image_1 = tmpdir / "images1" / "IOU" / "iou64.bin" - iou_image_1.write(b'\x7fELF\x02\x01\x01', ensure=True) + iou_image_1.write(b"\x7fELF\x02\x01\x01", ensure=True) iou_image_1 = force_unix_path(str(iou_image_1)) # 32-bit IOU image iou_image_2 = tmpdir / "images1" / "IOU" / "iou32.bin" - iou_image_2.write(b'\x7fELF\x01\x01\x01', ensure=True) # 32-bit IOU image + iou_image_2.write(b"\x7fELF\x01\x01\x01", ensure=True) # 32-bit IOU image iou_image_2 = force_unix_path(str(iou_image_2)) - # Qemu image qemu_image_1 = tmpdir / "images1" / "QEMU" / "qemu_image.qcow2" qemu_image_1.write("1234567", ensure=True) @@ -151,7 +150,7 @@ async def test_list_images(tmpdir, config): # ELF file inside the Qemu elf_file = tmpdir / "images1" / "QEMU" / "elf_file.bin" - elf_file.write(b'\x7fELF\x02\x01\x01', ensure=True) # ELF file + elf_file.write(b"\x7fELF\x02\x01\x01", ensure=True) # ELF file elf_file = force_unix_path(str(elf_file)) md5sum_file = tmpdir / "images1" / "QEMU" / "image.qcow2.md5sum" @@ -161,42 +160,32 @@ async def test_list_images(tmpdir, config): config.settings.Server.images_path = str(tmpdir / "images1") config.settings.Server.additional_images_paths = "/tmp/null24564;" + str(tmpdir / "images2") - assert sorted(await list_images("dynamips"), key=lambda k: k['filename']) == [ + assert sorted(await list_images("dynamips"), key=lambda k: k["filename"]) == [ { - 'filename': 'ios_image_1.image', - 'filesize': 7, - 'md5sum': 'b0d5aa897d937aced5a6b1046e8f7e2e', - 'path': 'ios_image_1.image' + "filename": "ios_image_1.image", + "filesize": 7, + "md5sum": "b0d5aa897d937aced5a6b1046e8f7e2e", + "path": "ios_image_1.image", }, { - 'filename': 'ios_image_2.image', - 'filesize': 7, - 'md5sum': 'b0d5aa897d937aced5a6b1046e8f7e2e', - 'path': str(ios_image_2) - } + "filename": "ios_image_2.image", + "filesize": 7, + "md5sum": "b0d5aa897d937aced5a6b1046e8f7e2e", + "path": str(ios_image_2), + }, ] if sys.platform.startswith("linux"): - assert sorted(await list_images("iou"), key=lambda k: k['filename']) == [ - { - 'filename': 'iou32.bin', - 'filesize': 7, - 'md5sum': 'e573e8f5c93c6c00783f20c7a170aa6c', - 'path': 'iou32.bin' - }, - { - 'filename': 'iou64.bin', - 'filesize': 7, - 'md5sum': 'c73626d23469519894d58bc98bee9655', - 'path': 'iou64.bin' - } + assert sorted(await list_images("iou"), key=lambda k: k["filename"]) == [ + {"filename": "iou32.bin", "filesize": 7, "md5sum": "e573e8f5c93c6c00783f20c7a170aa6c", "path": "iou32.bin"}, + {"filename": "iou64.bin", "filesize": 7, "md5sum": "c73626d23469519894d58bc98bee9655", "path": "iou64.bin"}, ] assert await list_images("qemu") == [ { - 'filename': 'qemu_image.qcow2', - 'filesize': 7, - 'md5sum': 'fcea920f7412b5da7be0cf42b8c93759', - 'path': 'qemu_image.qcow2' + "filename": "qemu_image.qcow2", + "filesize": 7, + "md5sum": "fcea920f7412b5da7be0cf42b8c93759", + "path": "qemu_image.qcow2", } ] diff --git a/tests/utils/test_packet_filter_validation.py b/tests/utils/test_packet_filter_validation.py index d15ae1fc3..a5ba0cbe8 100644 --- a/tests/utils/test_packet_filter_validation.py +++ b/tests/utils/test_packet_filter_validation.py @@ -7,7 +7,7 @@ from gns3server.utils.packet_filter_validation import ( validate_filter_parameters, validate_all_filters, filter_inactive_filters, - FilterValidationError + FilterValidationError, ) @@ -143,17 +143,14 @@ class TestPacketFilterValidation: def test_validate_all_filters(self): """Test validating multiple filters at once.""" - filters = { - "frequency_drop": [10], - "delay": [100, 50] - } + filters = {"frequency_drop": [10], "delay": [100, 50]} validate_all_filters(filters) # Should not raise def test_validate_all_filters_with_invalid(self): """Test validate_all_filters with invalid filter.""" filters = { "frequency_drop": [10], - "packet_loss": [150] # Invalid: over 100% + "packet_loss": [150], # Invalid: over 100% } with pytest.raises(FilterValidationError): validate_all_filters(filters) @@ -242,16 +239,13 @@ class TestFilterInactiveFilters: def test_filter_inactive_multiple_filters_mixed(self): """Test multiple filters with mixed active/inactive states.""" filters = { - "delay": [0, 0], # Disabled: [0, 0] - "packet_loss": [0], # Disabled: 0% - "corrupt": [2], # Active: 2% - "frequency_drop": [10] # Active: every 10th packet + "delay": [0, 0], # Disabled: [0, 0] + "packet_loss": [0], # Disabled: 0% + "corrupt": [2], # Active: 2% + "frequency_drop": [10], # Active: every 10th packet } result = filter_inactive_filters(filters) - assert result == { - "corrupt": [2], - "frequency_drop": [10] - } + assert result == {"corrupt": [2], "frequency_drop": [10]} def test_filter_inactive_empty_filters(self): """Test empty filters dictionary.""" @@ -262,4 +256,4 @@ class TestFilterInactiveFilters: def test_filter_inactive_none_filters(self): """Test None filters.""" result = filter_inactive_filters(None) - assert result == {} \ No newline at end of file + assert result == {} From ef579dfa8f0a65124cc4944b5ffd1569afe2fbf5 Mon Sep 17 00:00:00 2001 From: Mark Paronyan Date: Sun, 27 Sep 2026 16:07:44 +0300 Subject: [PATCH 52/60] ci: no flake8 checks for now, ruff covers them --- justfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/justfile b/justfile index 43528fd1c..b07fefc81 100644 --- a/justfile +++ b/justfile @@ -20,8 +20,6 @@ format: lint: python -m ruff check --exit-non-zero-on-fix python -m ruff format --check --diff - python -m flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - python -m flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics [group('dev')] type-check: From 5c686b0c2095ae7c382023d9c7c902096a9ceb06 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 27 Sep 2026 17:13:34 +0200 Subject: [PATCH 53/60] Revert changes to rbac-user-isolation-roadmap.md --- .../roadmap/rbac-user-isolation-roadmap.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/gns3-copilot/roadmap/rbac-user-isolation-roadmap.md b/docs/gns3-copilot/roadmap/rbac-user-isolation-roadmap.md index 0712c90fa..447279d07 100644 --- a/docs/gns3-copilot/roadmap/rbac-user-isolation-roadmap.md +++ b/docs/gns3-copilot/roadmap/rbac-user-isolation-roadmap.md @@ -556,20 +556,20 @@ Server: **API Endpoints:** ```python # Configuration (admin only) -POST / v3 / access / config / email -GET / v3 / access / config / email -PUT / v3 / access / config / email +POST /v3/access/config/email +GET /v3/access/config/email +PUT /v3/access/config/email # Test email (admin only) -POST / v3 / access / config / email / test +POST /v3/access/config/email/test # Password reset (public) -POST / v3 / access / users / password / reset / request -POST / v3 / access / users / password / reset / confirm +POST /v3/access/users/password/reset/request +POST /v3/access/users/password/reset/confirm # Email verification (public) -POST / v3 / access / users / verify / email -POST / v3 / access / users / verify / confirm +POST /v3/access/users/verify/email +POST /v3/access/users/verify/confirm ``` ### Database Schema From 45a0f596c885a283d097fee19020936b3b32d301 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 27 Sep 2026 17:52:40 +0200 Subject: [PATCH 54/60] Ignore Markdown files --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5d4286bcd..21c8a9ba6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,9 @@ packages = ["gns3server"] [tool.ruff] target-version = "py310" line-length = 120 +extend-exclude = [ + "*.md", +] [tool.ruff.lint] select = [ From 544890c6b401540c13b2876104a6318fb387a64c Mon Sep 17 00:00:00 2001 From: Mark Paronyan Date: Sun, 27 Sep 2026 19:09:25 +0300 Subject: [PATCH 55/60] chore: delete mac and win reqs --- mac-requirements.txt | 1 - win-requirements.txt | 4 ---- 2 files changed, 5 deletions(-) delete mode 100644 mac-requirements.txt delete mode 100644 win-requirements.txt diff --git a/mac-requirements.txt b/mac-requirements.txt deleted file mode 100644 index bc04b4960..000000000 --- a/mac-requirements.txt +++ /dev/null @@ -1 +0,0 @@ --r requirements.txt diff --git a/win-requirements.txt b/win-requirements.txt deleted file mode 100644 index 1cc7ba1e2..000000000 --- a/win-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ --r requirements.txt - -pywin32==311 -wmi==1.5.1 From 76e85b552a5b7e6479f3ba11cc699e4f089691dd Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 27 Sep 2026 18:51:14 +0200 Subject: [PATCH 56/60] chore(deps): update dependencies --- requirements.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index b41c599ee..5b89bf4a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,23 @@ # GNS3 Server Core Dependencies -uvicorn==0.52.4 -pydantic==2.13.4 +uvicorn==0.54.0 +pydantic==2.13.5 fastapi==0.141.1 python-multipart==0.0.32 websockets==16.1.1 # version 16.1.1 is the last version that supports Python 3.10 aiohttp>=3.14.3,<3.15 aiofiles>=25.1.0,<26.0 Jinja2>=3.1.6,<3.2 -sentry-sdk>=2.68.0,<3 # optional dependency +sentry-sdk>=2.70.0,<3 # optional dependency psutil>=7.2.2 async-timeout>=5.0.1,<5.1; python_version < '3.11' # this library has effectively been upstreamed into Python 3.11+ distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 greenlet==3.5.3; python_version >= '3.13' # necessary to run sqlalchemy on Python >= 3.13 -sqlalchemy==2.0.52 +sqlalchemy==2.1.1 aiosqlite==0.22.1 -alembic==1.19.1 +alembic==1.20 bcrypt==5.0.0 -joserfc==1.7.4 +joserfc==1.7.5 email-validator==2.3.0 watchdog==6.0.0 zstandard==0.25.0 @@ -25,11 +25,11 @@ platformdirs>=2.4.0 # fastmcp-slim >=3.4 requires >=4.0.0; upper bound removed truststore>=0.10.4; python_version >= '3.10' # Shared dependencies (also used by AI Copilot) -telnetlib3==5.0.0 +telnetlib3==5.0.1,<6 asyncssh>=2.24.0,<3 typing-extensions>=4.16.0 requests>=2.34.2 -urllib3>=2.7.0 +urllib3>=2.8.0 # ============================================================================== # File type detection From 52f2774c053fe2d81a1e3708e06c7525193bb1d9 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 27 Sep 2026 18:55:33 +0200 Subject: [PATCH 57/60] sqlalchemy v2.0.54 is the last version that supports Python 3.10 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5b89bf4a1..15330d785 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ async-timeout>=5.0.1,<5.1; python_version < '3.11' # this library has effective distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 greenlet==3.5.3; python_version >= '3.13' # necessary to run sqlalchemy on Python >= 3.13 -sqlalchemy==2.1.1 +sqlalchemy==2.0.54 # version 2.0.54 is the last version that supports Python 3.10 aiosqlite==0.22.1 alembic==1.20 bcrypt==5.0.0 From b96774d1f70faa4d64ac4d365e6b2aaa12509b1e Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 27 Sep 2026 18:59:42 +0200 Subject: [PATCH 58/60] Sync appliances --- .../asterfusion-vAsterNOS-VPP.gns3a | 18 +- gns3server/appliances/cisco-iol-l2.gns3a | 35 ++++ gns3server/appliances/cisco-iol-xe.gns3a | 35 ++++ gns3server/appliances/cisco-iosv.gns3a | 14 ++ gns3server/appliances/cisco-nxosv9k.gns3a | 14 ++ .../appliances/cisco-xrd-control-plane.gns3a | 140 ++++++++++++++ gns3server/appliances/freebsd.gns3a | 17 +- gns3server/appliances/infix.gns3a | 18 +- gns3server/appliances/ostinato.gns3a | 26 +++ gns3server/appliances/srlinux.gns3a | 180 ++++++++++++++++++ gns3server/appliances/westermo-weos.gns3a | 15 +- 11 files changed, 506 insertions(+), 6 deletions(-) create mode 100644 gns3server/appliances/cisco-iol-l2.gns3a create mode 100644 gns3server/appliances/cisco-iol-xe.gns3a create mode 100644 gns3server/appliances/cisco-xrd-control-plane.gns3a create mode 100644 gns3server/appliances/srlinux.gns3a diff --git a/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a index 86016b329..62037f4bf 100644 --- a/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a +++ b/gns3server/appliances/asterfusion-vAsterNOS-VPP.gns3a @@ -13,9 +13,17 @@ "maintainer_email": "bd@cloudswit.ch", "availability": "with-registration", "images": [ + { + "filename": "AsterNOS-VPP_V6.1-R0104P02_x86.img", + "version": "6.1_R0104P02", + "md5sum": "a02926586176559b9ead106baa934ce0", + "filesize": 2584018944, + "download_url": "https://asternos.dev/api/file/db840750-01fd-4d1f-9e35-efa3ce30193f", + "compression": "gzip" + }, { "filename": "AsterNOS-VPP_V6.1-R0101P02_x86.img", - "version": "6.1", + "version": "6.1_R0101P02", "md5sum": "55834c3e8849ab226144d79ef76acf81", "filesize": 1318201015, "download_url": "https://asternos.dev/api/file/b2c9fd83-a9d3-46f3-9676-6b49dd481d6c", @@ -33,7 +41,13 @@ }, "versions": [ { - "name": "6.1", + "name": "6.1_R0104P02", + "images": { + "hda_disk_image": "AsterNOS-VPP_V6.1-R0104P02_x86.img" + } + }, + { + "name": "6.1_R0101P02", "images": { "hda_disk_image": "AsterNOS-VPP_V6.1-R0101P02_x86.img" } diff --git a/gns3server/appliances/cisco-iol-l2.gns3a b/gns3server/appliances/cisco-iol-l2.gns3a new file mode 100644 index 000000000..08b6001d7 --- /dev/null +++ b/gns3server/appliances/cisco-iol-l2.gns3a @@ -0,0 +1,35 @@ +{ + "appliance_id": "329a24ea-9476-470a-bedd-6a79c1dd9867", + "name": "Cisco IOL-L2", + "category": "multilayer_switch", + "description": "Cisco IOL-L2 is the containerized Layer-2 switch image from Cisco Modeling Labs: the real IOS code in a scratch image driven by CML's iol-runner. This appliance runs the unmodified image inside a GNS3 Docker node — the console opens directly in the IOS CLI and links are wired through unix-socket NIO instead of a TAP interface.", + "vendor_name": "Cisco", + "vendor_url": "https://www.cisco.com/", + "documentation_url": "https://developer.cisco.com/docs/modeling-labs/", + "product_name": "Cisco IOL-L2", + "product_url": "https://developer.cisco.com/modeling-labs/", + "registry_version": 8, + "status": "experimental", + "availability": "service-contract", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "usage": "**Server requirement:** a GNS3 server with IOL Docker (iol-runner) support — selected by the `GNS3_IOL_RUNNER` environment marker. This is not in stock gns3-server releases yet.\n\nThe image is not on Docker Hub: it ships with Cisco Modeling Labs and requires a CML account. Load it on the GNS3 server and make sure the tag matches (`iol-xe/ioll2-xe:17-18-02`), or edit the template image afterwards:\n\n```bash\ndocker load -i # or pull from Cisco's registry with your CML account\ndocker tag iol-xe/ioll2-xe:17-18-02\n```\n\n**Other versions:** Docker appliances have no per-version selector in the `.gns3a` format — load the version you want and change the template's image field (templates are version-agnostic: same env vars and volumes).\n\nNodes boot straight into a minimal base config (hostname = node name, `no ip domain lookup`, console timeouts off — from `iol-xe-base.txt`, installed by the server) with no initial setup dialog. `write memory` persists across stop/start; to re-seed a node from the base config, delete its NVRAM or recreate the node.\n\nThe console is the IOS CLI (telnet, PID 1 stdio). Keystroke echo follows the IOS console poll cadence (~50 ms) — paste long commands instead of typing them. Interface names are IOL-style `Ethernet0/0`; one adapter is a 4-port unit (8 units max), change the count while the node is stopped. MAC addresses are derived from the node's application ID (`aabb.cc…`) and cannot be set. If you cap `memory`, keep it at IOL memory + ~512 MB headroom (default 2048 MB + no cap).", + "netmiko_device_type": "cisco_ios_telnet", + "symbol": ":/symbols/multilayer_switch.svg", + "settings": [ + { + "name": "Default template settings", + "default": true, + "template_type": "docker", + "template_properties": { + "adapters": 4, + "image": "iol-xe/ioll2-xe:17-18-02", + "console_type": "telnet", + "environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=iol-xe-base.txt", + "extra_volumes": [ + "/config" + ] + } + } + ] +} diff --git a/gns3server/appliances/cisco-iol-xe.gns3a b/gns3server/appliances/cisco-iol-xe.gns3a new file mode 100644 index 000000000..02a4316d2 --- /dev/null +++ b/gns3server/appliances/cisco-iol-xe.gns3a @@ -0,0 +1,35 @@ +{ + "appliance_id": "8890703c-4a26-42aa-a755-7a096e879866", + "name": "Cisco IOL-XE", + "category": "router", + "description": "Cisco IOS-XE IOL is the containerized IOS-XE router image from Cisco Modeling Labs: the real IOS-XE code (a scratch image driven by CML's iol-runner). This appliance runs the unmodified image inside a GNS3 Docker node — the console opens directly in the IOS-XE CLI and links are wired through unix-socket NIO instead of a TAP interface.", + "vendor_name": "Cisco", + "vendor_url": "https://www.cisco.com/", + "documentation_url": "https://developer.cisco.com/docs/modeling-labs/", + "product_name": "Cisco IOL-XE", + "product_url": "https://developer.cisco.com/modeling-labs/", + "registry_version": 8, + "status": "experimental", + "availability": "service-contract", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "usage": "**Server requirement:** a GNS3 server with IOL Docker (iol-runner) support — selected by the `GNS3_IOL_RUNNER` environment marker. This is not in stock gns3-server releases yet.\n\nThe image is not on Docker Hub: it ships with Cisco Modeling Labs and requires a CML account. Load it on the GNS3 server and make sure the tag matches (`iol-xe/iol-xe:17-18-02`), or edit the template image afterwards:\n\n```bash\ndocker load -i # or pull from Cisco's registry with your CML account\ndocker tag iol-xe/iol-xe:17-18-02\n```\n\n**Other versions:** Docker appliances have no per-version selector in the `.gns3a` format — load the version you want and change the template's image field (templates are version-agnostic: same env vars and volumes).\n\nNodes boot straight into a minimal base config (hostname = node name, `no ip domain lookup`, console timeouts off — from `iol-xe-base.txt`, installed by the server) with no initial setup dialog. `write memory` persists across stop/start; to re-seed a node from the base config, delete its NVRAM or recreate the node.\n\nThe console is the IOS-XE CLI (telnet, PID 1 stdio). Keystroke echo follows the IOS console poll cadence (~50 ms) — paste long commands instead of typing them. Interface names are IOL-style `Ethernet0/0`, not `GigabitEthernet0/0`; one adapter is a 4-port unit (8 units max), change the count while the node is stopped. MAC addresses are derived from the node's application ID (`aabb.cc…`) and cannot be set. If you cap `memory`, keep it at IOL memory + ~512 MB headroom (default 2048 MB + no cap).", + "netmiko_device_type": "cisco_ios_telnet", + "symbol": ":/symbols/router.svg", + "settings": [ + { + "name": "Default template settings", + "default": true, + "template_type": "docker", + "template_properties": { + "adapters": 2, + "image": "iol-xe/iol-xe:17-18-02", + "console_type": "telnet", + "environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=iol-xe-base.txt", + "extra_volumes": [ + "/config" + ] + } + } + ] +} diff --git a/gns3server/appliances/cisco-iosv.gns3a b/gns3server/appliances/cisco-iosv.gns3a index 4ddc6cbc5..7c56b19c7 100644 --- a/gns3server/appliances/cisco-iosv.gns3a +++ b/gns3server/appliances/cisco-iosv.gns3a @@ -26,6 +26,13 @@ "kvm": "require" }, "images": [ + { + "filename": "vios-adventerprisek9-m.spa.159-3.m12.qcow2", + "version": "15.9(3)M12", + "md5sum": "61d2d7cf16c8a3efa01db6545dcecdfd", + "filesize": 57326080, + "download_url": "https://u.cisco.com/my-learning/my-account" + }, { "filename": "vios-adventerprisek9-m.spa.159-3.m10.qcow2", "version": "15.9(3)M10", @@ -120,6 +127,13 @@ } ], "versions": [ + { + "name": "15.9(3)M12", + "images": { + "hda_disk_image": "vios-adventerprisek9-m.spa.159-3.m12.qcow2", + "hdb_disk_image": "IOSv_startup_config.img" + } + }, { "name": "15.9(3)M10", "images": { diff --git a/gns3server/appliances/cisco-nxosv9k.gns3a b/gns3server/appliances/cisco-nxosv9k.gns3a index 20d96baef..1302d184b 100644 --- a/gns3server/appliances/cisco-nxosv9k.gns3a +++ b/gns3server/appliances/cisco-nxosv9k.gns3a @@ -27,6 +27,13 @@ "kvm": "require" }, "images": [ + { + "filename": "nexus9300v64.10.6.2.F.qcow2", + "version": "9300v 10.6.2.F", + "md5sum": "f11113080fea3dc87ab498436c4e5199", + "filesize": 2976382976, + "download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/10.6(2)" + }, { "filename": "nexus9500v64.10.5.5.M.qcow2", "version": "9500v 10.5.5.M", @@ -94,6 +101,13 @@ } ], "versions": [ + { + "name": "9300v 10.6.2.F", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "nexus9300v64.10.6.2.F.qcow2" + } + }, { "name": "9500v 10.5.5.M", "images": { diff --git a/gns3server/appliances/cisco-xrd-control-plane.gns3a b/gns3server/appliances/cisco-xrd-control-plane.gns3a new file mode 100644 index 000000000..b7dc79349 --- /dev/null +++ b/gns3server/appliances/cisco-xrd-control-plane.gns3a @@ -0,0 +1,140 @@ +{ + "appliance_id": "e4a3a5fe-3a13-521b-abd1-ab9483e83aa2", + "name": "XRd Control Plane", + "category": "router", + "description": "Cisco IOS XRd Control Plane runs the full IOS XR control plane (BGP, OSPF, IS-IS, MPLS, segment routing, NETCONF/YANG, gNMI) as a Docker container with a Linux-kernel data plane — designed for control-plane labs and automation, not forwarding performance.\n\nThis appliance runs the unmodified Cisco image inside a GNS3 Docker node. The Web console opens directly in the IOS XR CLI (xr_cli via the docker_exec console).", + "vendor_name": "Cisco", + "vendor_url": "https://www.cisco.com/", + "documentation_url": "https://xrdocs.io/virtual-routing/", + "product_name": "XRd Control Plane", + "product_url": "https://www.cisco.com/c/en/us/products/collateral/routers/ios-xrd/datasheet-c78-744298.html", + "registry_version": 8, + "status": "experimental", + "availability": "service-contract", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "usage": "The official image must be loaded on the GNS3 server first (it is not on Docker Hub). The tag must be `ios-xr/xrd-control-plane:`, or edit the template image afterwards:\n\n```bash\ndocker load -i xrd-control-plane-.tar.gz # from Cisco with a valid account\n```\n\n**Other versions (25.x, 26.x, ...):** the appliance pins one tested image tag, and Docker appliances have no per-version selector in the `.gns3a` format — just load the tarball you want and change the template's image field to `ios-xr/xrd-control-plane:` (templates are version-agnostic here: same env vars, volumes and interface mapping).\n\n**Host preparation** (once, XRd requirements — GNS3 warns about these at Docker connect):\n\n```bash\nsudo sysctl -w fs.inotify.max_user_instances=64000 fs.inotify.max_user_watches=524288 fs.file-max=1000000\nsudo modprobe fuse\n# persist them under /etc/sysctl.d/\n```\n\nEach node uses about **4 GB RAM** and takes 2-3 minutes to boot.\n\n**Default login:** `admin` / `cisco` (created by the injected first-boot config).\n\nThe first-boot config only applies while the XR config storage is empty; to re-seed it, delete and recreate the node. Committed configuration persists across stop/start (via `/xr-storage`).\n\nData interfaces need an address and `no shutdown` before they pass traffic.", + "netmiko_device_type": "cisco_xr_telnet", + "symbol": ":/symbols/router.svg", + "settings": [ + { + "name": "Default template settings", + "default": true, + "template_type": "docker", + "template_properties": { + "adapters": 24, + "image": "ios-xr/xrd-control-plane:24.4.1", + "console_type": "docker_exec", + "environment": "GNS3_SKIP_INIT=1\nGNS3_CONSOLE_CMD=/pkg/bin/xr_cli.sh\nGNS3_CONSOLE_RESIZE=0\nGNS3_MASK_UDEV=1\nGNS3_SHM_SIZE=1024\nGNS3_DEVICES=/dev/fuse\nGNS3_STOP_TIMEOUT=40\nXR_FIRST_BOOT_CONFIG=/firstboot.cfg\nXR_MGMT_INTERFACES=linux:eth0,xr_name=Mg0/RP0/CPU0/0,chksum,snoop_v4,snoop_v6\nXR_INTERFACES=linux:eth1,xr_name=Gi0/0/0/0;linux:eth2,xr_name=Gi0/0/0/1;linux:eth3,xr_name=Gi0/0/0/2;linux:eth4,xr_name=Gi0/0/0/3;linux:eth5,xr_name=Gi0/0/0/4;linux:eth6,xr_name=Gi0/0/0/5;linux:eth7,xr_name=Gi0/0/0/6;linux:eth8,xr_name=Gi0/0/0/7;linux:eth9,xr_name=Gi0/0/0/8;linux:eth10,xr_name=Gi0/0/0/9;linux:eth11,xr_name=Gi0/0/0/10;linux:eth12,xr_name=Gi0/0/0/11;linux:eth13,xr_name=Gi0/0/0/12;linux:eth14,xr_name=Gi0/0/0/13;linux:eth15,xr_name=Gi0/0/0/14;linux:eth16,xr_name=Gi0/0/0/15;linux:eth17,xr_name=Gi0/0/0/16;linux:eth18,xr_name=Gi0/0/0/17;linux:eth19,xr_name=Gi0/0/0/18;linux:eth20,xr_name=Gi0/0/0/19;linux:eth21,xr_name=Gi0/0/0/20;linux:eth22,xr_name=Gi0/0/0/21;linux:eth23,xr_name=Gi0/0/0/22", + "extra_volumes": [ + "/xr-storage", + "/xr-storage-shadow" + ], + "extra_configs": [ + { + "target": "/firstboot.cfg", + "content": "!\nusername admin\n group root-lr\n secret cisco\n!\nend\n" + } + ], + "custom_adapters": [ + { + "adapter_number": 0, + "port_name": "MgmtEth0/RP0/CPU0/0" + }, + { + "adapter_number": 1, + "port_name": "Gi0/0/0/0" + }, + { + "adapter_number": 2, + "port_name": "Gi0/0/0/1" + }, + { + "adapter_number": 3, + "port_name": "Gi0/0/0/2" + }, + { + "adapter_number": 4, + "port_name": "Gi0/0/0/3" + }, + { + "adapter_number": 5, + "port_name": "Gi0/0/0/4" + }, + { + "adapter_number": 6, + "port_name": "Gi0/0/0/5" + }, + { + "adapter_number": 7, + "port_name": "Gi0/0/0/6" + }, + { + "adapter_number": 8, + "port_name": "Gi0/0/0/7" + }, + { + "adapter_number": 9, + "port_name": "Gi0/0/0/8" + }, + { + "adapter_number": 10, + "port_name": "Gi0/0/0/9" + }, + { + "adapter_number": 11, + "port_name": "Gi0/0/0/10" + }, + { + "adapter_number": 12, + "port_name": "Gi0/0/0/11" + }, + { + "adapter_number": 13, + "port_name": "Gi0/0/0/12" + }, + { + "adapter_number": 14, + "port_name": "Gi0/0/0/13" + }, + { + "adapter_number": 15, + "port_name": "Gi0/0/0/14" + }, + { + "adapter_number": 16, + "port_name": "Gi0/0/0/15" + }, + { + "adapter_number": 17, + "port_name": "Gi0/0/0/16" + }, + { + "adapter_number": 18, + "port_name": "Gi0/0/0/17" + }, + { + "adapter_number": 19, + "port_name": "Gi0/0/0/18" + }, + { + "adapter_number": 20, + "port_name": "Gi0/0/0/19" + }, + { + "adapter_number": 21, + "port_name": "Gi0/0/0/20" + }, + { + "adapter_number": 22, + "port_name": "Gi0/0/0/21" + }, + { + "adapter_number": 23, + "port_name": "Gi0/0/0/22" + } + ] + } + } + ] +} diff --git a/gns3server/appliances/freebsd.gns3a b/gns3server/appliances/freebsd.gns3a index 2f937568d..608e5e741 100644 --- a/gns3server/appliances/freebsd.gns3a +++ b/gns3server/appliances/freebsd.gns3a @@ -24,13 +24,22 @@ "kvm": "require" }, "images": [ + { + "filename": "FreeBSD-15.0-RELEASE-amd64-zfs.qcow2", + "version": "15.0-zfs", + "md5sum": "d3109107e34336081eb88665674cbca4", + "filesize": 2854944768, + "download_url": "https://www.freebsd.org/where.html", + "direct_download_url": "https://download.freebsd.org/releases/VM-IMAGES/15.0-RELEASE/amd64/Latest/FreeBSD-15.0-RELEASE-amd64-ufs.qcow2.xz", + "compression": "xz" + }, { "filename": "FreeBSD-14.0-RELEASE-amd64.qcow2", "version": "14.0", "md5sum": "87b988eaa4a8aaabea1c10649c98b3f0", "filesize": 3506569216, "download_url": "https://www.freebsd.org/where.html", - "direct_download_url": "https://download.freebsd.org/releases/VM-IMAGES/14.0-RELEASE/amd64/Latest/FreeBSD-14.0-RELEASE-amd64.qcow2.xz", + "direct_download_url": "https://archive.freebsd.org/old-releases/VM-IMAGES/14.0-RELEASE/amd64/Latest/FreeBSD-14.0-RELEASE-amd64.qcow2.xz", "compression": "xz" }, { @@ -53,6 +62,12 @@ } ], "versions": [ + { + "name": "15.0-zfs", + "images": { + "hda_disk_image": "FreeBSD-15.0-RELEASE-amd64-zfs.qcow2" + } + }, { "name": "14.0", "images": { diff --git a/gns3server/appliances/infix.gns3a b/gns3server/appliances/infix.gns3a index 481cba0fd..54bed43f6 100644 --- a/gns3server/appliances/infix.gns3a +++ b/gns3server/appliances/infix.gns3a @@ -2,7 +2,7 @@ "appliance_id": "4d351078-c6f5-444c-ab30-0ef20e3d8c53", "name": "Infix", "category": "router", - "description": "Infix OS is a free Linux-based operating system with made-easy management using NETCONF, RESTCONF, or the built-in command line interface (CLI). It is suitable for both end-devices with a single Ethernet port and switches/routers with multiple ports. Immutable by design, extensions are possible using Docker containers.", + "description": "Infix OS is a free Linux-based operating system with made-easy management using the built-in web interface, NETCONF, RESTCONF, or the command line interface (CLI). It is suitable for both end-devices with a single Ethernet port and switches/routers with multiple ports. Immutable by design, extensions are possible using Docker containers.", "vendor_name": "KernelKit", "vendor_url": "https://github.com/kernelkit", "vendor_logo_url": "https://kernelkit.org/assets/img/jack.png", @@ -146,9 +146,23 @@ "md5sum": "79ca8bd8534bbaa1af0ab874a49d6f4c", "version": "26.06.0", "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.06.0/infix-x86_64-v26.06.0.qcow2" + }, + { + "filename": "infix-x86_64-v26.08.0.qcow2", + "filesize": 364118016, + "md5sum": "0bcf79a0dc88df0e340e43bcf62e8d40", + "version": "26.08.0", + "direct_download_url": "https://github.com/kernelkit/infix/releases/download/v26.08.0/infix-x86_64-v26.08.0.qcow2" } ], "versions": [ + { + "name": "26.08.0", + "images": { + "bios_image": "OVMF-edk2-stable202305.fd", + "hda_disk_image": "infix-x86_64-v26.08.0.qcow2" + } + }, { "name": "26.06.0", "images": { @@ -255,4 +269,4 @@ } } ] -} \ No newline at end of file +} diff --git a/gns3server/appliances/ostinato.gns3a b/gns3server/appliances/ostinato.gns3a index 190c68617..b1d00759f 100644 --- a/gns3server/appliances/ostinato.gns3a +++ b/gns3server/appliances/ostinato.gns3a @@ -30,6 +30,20 @@ "options": "-vga std -usbdevice tablet" }, "images": [ + { + "version": "2.1.0", + "filename": "ostinato-lab-2.1.0-1.qcow2", + "filesize": 177537024, + "md5sum": "ec1e3d202a3a110bf669b551dc9b5af3", + "download_url": "https://ostinato.org/pricing/gns3" + }, + { + "version": "2.0.0", + "filename": "ostinato-lab-2.0.0-1.qcow2", + "filesize": 176816128, + "md5sum": "a9bd35b3a61560abface695d7952d335", + "download_url": "https://ostinato.org/pricing/gns3" + }, { "version": "1.3.0", "filename": "ostinatostd-1.3.0-1.qcow2", @@ -53,6 +67,18 @@ } ], "versions": [ + { + "name": "2.1.0", + "images": { + "hda_disk_image": "ostinato-lab-2.1.0-1.qcow2" + } + }, + { + "name": "2.0.0", + "images": { + "hda_disk_image": "ostinato-lab-2.0.0-1.qcow2" + } + }, { "name": "1.3.0", "images": { diff --git a/gns3server/appliances/srlinux.gns3a b/gns3server/appliances/srlinux.gns3a new file mode 100644 index 000000000..05025786d --- /dev/null +++ b/gns3server/appliances/srlinux.gns3a @@ -0,0 +1,180 @@ +{ + "appliance_id": "ffa79bec-d4fd-5842-89b4-e08f8fe21bf9", + "name": "SRLinux", + "category": "router", + "description": "Nokia SR Linux is the open, modular, and deeply programmable network operating system running on Nokia data center routers. It exposes a full TUI CLI, a gNMI/json-rpc management plane, and supports BGP, OSPF, IS-IS, MPLS, EVPN, segment routing and more.\n\nThis Docker image runs SR Linux unmodified inside a GNS3 Docker node. The Web console opens directly in the native sr_cli TUI (via the docker_exec console).", + "vendor_name": "Nokia", + "vendor_url": "https://www.nokia.com/ip-networks/service-router-linux-NOS/", + "documentation_url": "https://documentation.nokia.com/srlinux/", + "product_name": "SR Linux", + "product_url": "https://www.nokia.com/ip-networks/service-router-linux-NOS/", + "registry_version": 8, + "status": "experimental", + "availability": "free", + "maintainer": "GNS3 Team", + "maintainer_email": "developers@gns3.net", + "usage": "The Web console opens directly in the SR Linux CLI (`sr_cli`).\n\nData interfaces default to `admin-state disable`; enable them before use:\n\n```\nenter candidate\ninterface ethernet-1/1\n admin-state enable\ncommit now\n```\n\nTo make a data interface carry traffic, also bind it to a network-instance (default VRF `default`). The first port (mgmt0) is the management interface.\n\nSave the running config with `save config` (persisted across node stop/start via `/etc/opt/srlinux`).\n\nThe CLI auto-logs out after 300s idle; just reopen the console, or disable it:\n\n```\nenter candidate\n/system cli idle-timeout disable\ncommit now\n```", + "netmiko_device_type": "nokia_sros_telnet", + "symbol": ":/symbols/affinity/circle/blue/router_cloud.svg", + "settings": [ + { + "name": "Default template settings", + "default": true, + "template_type": "docker", + "template_properties": { + "adapters": 35, + "image": "ghcr.io/nokia/srlinux:latest", + "console_type": "docker_exec", + "start_command": "sudo -E bash -c 'touch /.dockerenv && /opt/srlinux/bin/sr_linux'", + "environment": "GNS3_SKIP_INIT=1\nGNS3_INTERFACE_NAMES=mgmt0,e1-1,e1-2,e1-3,e1-4,e1-5,e1-6,e1-7,e1-8,e1-9,e1-10,e1-11,e1-12,e1-13,e1-14,e1-15,e1-16,e1-17,e1-18,e1-19,e1-20,e1-21,e1-22,e1-23,e1-24,e1-25,e1-26,e1-27,e1-28,e1-29,e1-30,e1-31,e1-32,e1-33,e1-34\nGNS3_CONSOLE_CMD=/opt/srlinux/bin/sr_cli", + "extra_volumes": [ + "/etc/opt/srlinux", + "/var/log/srlinux", + "/opt/srlinux/appmgr" + ], + "custom_adapters": [ + { + "adapter_number": 0, + "port_name": "mgmt0" + }, + { + "adapter_number": 1, + "port_name": "e1-1" + }, + { + "adapter_number": 2, + "port_name": "e1-2" + }, + { + "adapter_number": 3, + "port_name": "e1-3" + }, + { + "adapter_number": 4, + "port_name": "e1-4" + }, + { + "adapter_number": 5, + "port_name": "e1-5" + }, + { + "adapter_number": 6, + "port_name": "e1-6" + }, + { + "adapter_number": 7, + "port_name": "e1-7" + }, + { + "adapter_number": 8, + "port_name": "e1-8" + }, + { + "adapter_number": 9, + "port_name": "e1-9" + }, + { + "adapter_number": 10, + "port_name": "e1-10" + }, + { + "adapter_number": 11, + "port_name": "e1-11" + }, + { + "adapter_number": 12, + "port_name": "e1-12" + }, + { + "adapter_number": 13, + "port_name": "e1-13" + }, + { + "adapter_number": 14, + "port_name": "e1-14" + }, + { + "adapter_number": 15, + "port_name": "e1-15" + }, + { + "adapter_number": 16, + "port_name": "e1-16" + }, + { + "adapter_number": 17, + "port_name": "e1-17" + }, + { + "adapter_number": 18, + "port_name": "e1-18" + }, + { + "adapter_number": 19, + "port_name": "e1-19" + }, + { + "adapter_number": 20, + "port_name": "e1-20" + }, + { + "adapter_number": 21, + "port_name": "e1-21" + }, + { + "adapter_number": 22, + "port_name": "e1-22" + }, + { + "adapter_number": 23, + "port_name": "e1-23" + }, + { + "adapter_number": 24, + "port_name": "e1-24" + }, + { + "adapter_number": 25, + "port_name": "e1-25" + }, + { + "adapter_number": 26, + "port_name": "e1-26" + }, + { + "adapter_number": 27, + "port_name": "e1-27" + }, + { + "adapter_number": 28, + "port_name": "e1-28" + }, + { + "adapter_number": 29, + "port_name": "e1-29" + }, + { + "adapter_number": 30, + "port_name": "e1-30" + }, + { + "adapter_number": 31, + "port_name": "e1-31" + }, + { + "adapter_number": 32, + "port_name": "e1-32" + }, + { + "adapter_number": 33, + "port_name": "e1-33" + }, + { + "adapter_number": 34, + "port_name": "e1-34" + } + ] + } + } + ] +} diff --git a/gns3server/appliances/westermo-weos.gns3a b/gns3server/appliances/westermo-weos.gns3a index dbcf3a7eb..ee6244267 100644 --- a/gns3server/appliances/westermo-weos.gns3a +++ b/gns3server/appliances/westermo-weos.gns3a @@ -14,7 +14,7 @@ "availability": "free", "maintainer": "Westermo", "maintainer_email": "info@westermo.com", - "usage": "\nFactory Configuration\n=====================\nAll ports are in VLAN1 untagged. The system will acquire a link-local\nIPv4 address (169.254.0.0/16) and will also request an address using\nDHCP. mDNS is enabled and the device is reachable under these names\n(where XX-XX-XX corresponds to the last three octets in the device's\nMAC address): zero-XX-XX-XX.local, zero.local, and weos.local.\n\nManagement\n==========\nAccessible via console, ssh, and http/https.\n\nUsername: admin\nPassword: admin\n\nUpgrade\n=======\nTo upgrade a device, simply download the new version and replace the\nHDA (Primary Master) image under the HDD tab in the configuration pane\nof the device.\n", + "usage": "All ports are in VLAN1 and untagged. The system will acquire a link-local IPv4 address (169.254.0.0/16) and will also request an address using DHCP. mDNS is enabled and the device is reachable under these names (where XX-XX-XX corresponds to the last three octets in the device's MAC address): zero-XX-XX-XX.local, zero.local, and weos.local. Management: Accessible via console, ssh, and http/https. Username: admin Password: admin.", "symbol": ":/symbols/classic/router.svg", "port_name_format": "eth{0}", "linked_clone": true, @@ -38,6 +38,13 @@ "filesize": 17825792, "direct_download_url": "https://dropzone.westermo.com/file.aspx?id=e6a7676d-85a7-4374-8961-c68aacb74921" }, + { + "filename": "WeOS-zero-5.30.0.disk", + "version": "5.30.0", + "filesize": 81788928, + "md5sum": "a0eb21084bfa2b39ea42c2e5b59a3eb1", + "direct_download_url": "https://dropzone.westermo.com/file.aspx?id=ee83b507-927d-4421-a1d5-fa60f7b00328" + }, { "filename": "WeOS-zero-5.29.0.disk", "version": "5.29.0", @@ -62,6 +69,12 @@ ], "versions": [ { + "name": "5.30.0", + "images": { + "hda_disk_image": "WeOS-zero-5.30.0.disk", + "hdb_disk_image": "Config-zero-1.0.0.disk" + } + }, { "name": "5.29.0", "images": { "hda_disk_image": "WeOS-zero-5.29.0.disk", From 3f2afb0e4adf0b9fb075af2d78615eb777d0d126 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 27 Sep 2026 19:10:11 +0200 Subject: [PATCH 59/60] Bundle web-ui v3.1.0a6 --- gns3server/static/web-ui/chunk-3DUTSYEG.js | 2945 +++++++++++ gns3server/static/web-ui/chunk-3PDLDM3Z.js | 1 - gns3server/static/web-ui/chunk-ERB75MEN.js | 685 --- gns3server/static/web-ui/chunk-ETQHA42W.js | 2 + gns3server/static/web-ui/chunk-EXPJ4N52.js | 2825 ----------- gns3server/static/web-ui/chunk-G7NALUVX.js | 1028 ++++ gns3server/static/web-ui/chunk-JVPSP3B2.js | 1 - gns3server/static/web-ui/chunk-TYGV4UPE.js | 1049 ---- gns3server/static/web-ui/chunk-UX44Z5PA.js | 1 + gns3server/static/web-ui/chunk-VB5QSUFX.js | 649 +++ gns3server/static/web-ui/index.html | 4 +- gns3server/static/web-ui/main-D46BBTLD.js | 4592 +++++++++++++++++ gns3server/static/web-ui/main-WF2BVN4Q.js | 4611 ------------------ gns3server/static/web-ui/styles-2FY4FSR3.css | 1 + gns3server/static/web-ui/styles-MAIEK22J.css | 1 - 15 files changed, 9220 insertions(+), 9175 deletions(-) create mode 100644 gns3server/static/web-ui/chunk-3DUTSYEG.js delete mode 100644 gns3server/static/web-ui/chunk-3PDLDM3Z.js delete mode 100644 gns3server/static/web-ui/chunk-ERB75MEN.js create mode 100644 gns3server/static/web-ui/chunk-ETQHA42W.js delete mode 100644 gns3server/static/web-ui/chunk-EXPJ4N52.js create mode 100644 gns3server/static/web-ui/chunk-G7NALUVX.js delete mode 100644 gns3server/static/web-ui/chunk-JVPSP3B2.js delete mode 100644 gns3server/static/web-ui/chunk-TYGV4UPE.js create mode 100644 gns3server/static/web-ui/chunk-UX44Z5PA.js create mode 100644 gns3server/static/web-ui/chunk-VB5QSUFX.js create mode 100644 gns3server/static/web-ui/main-D46BBTLD.js delete mode 100644 gns3server/static/web-ui/main-WF2BVN4Q.js create mode 100644 gns3server/static/web-ui/styles-2FY4FSR3.css delete mode 100644 gns3server/static/web-ui/styles-MAIEK22J.css diff --git a/gns3server/static/web-ui/chunk-3DUTSYEG.js b/gns3server/static/web-ui/chunk-3DUTSYEG.js new file mode 100644 index 000000000..654f10045 --- /dev/null +++ b/gns3server/static/web-ui/chunk-3DUTSYEG.js @@ -0,0 +1,2945 @@ +import{$ as P,$a as O,$c as xe,$e as dt,A as xn,Ab as Bn,Ae as ui,Bb as Vt,Bd as gt,Be as Qt,C as kn,Cb as we,Cc as Gn,Cd as Zn,Ce as fi,D as Cn,Db as Gt,Dd as Jn,De as Be,Ea as d,Eb as p,Ed as et,F as G,Fa as ce,Fb as de,Fd as $t,Fe as pi,G as Z,Ga as Ie,Gb as $,Gd as Ut,Ha as Fn,Hb as se,Hd as Ne,He as q,Ia as ae,Ib as Qe,Jd as ei,Ka as On,L as wn,La as He,Ld as ti,M as $e,Ma as M,Md as ni,Na as B,Oa as I,Od as ii,Oe as bi,R as ut,Ra as _e,Rc as De,S as rt,Sa as ge,Sb as U,T as Tn,Ta as pt,Td as ai,Te as _i,U as Mn,Ua as An,Uc as Wn,Ue as gi,V as te,Va as Ln,Vc as Yn,W as Ae,Wa as Pn,Wc as $n,We as Kt,X as x,Xa as T,Xc as Wt,Xe as he,Y as In,Za as F,Ze as Re,_ as Sn,_c as Ee,_e as vi,a as S,aa as N,ac as zn,af as yi,b as le,ba as C,bb as Bt,be as oi,bf as xt,ca as J,cb as zt,cf as tt,da as r,db as Ht,dc as ye,eb as E,ec as Ke,ef as xi,fb as c,fd as j,ff as ki,g as re,gb as u,gd as Yt,h as Oe,ha as ne,hb as A,hd as Un,hf as Ci,i as f,ia as ie,ib as ke,ic as st,id as lt,j as Ye,ja as Ue,jb as Pe,ka as Dn,kb as jt,kc as Hn,kd as Xe,la as X,le as Ve,ma as be,mc as Y,md as K,me as ri,ne as si,o as vn,oa as g,ob as Se,oc as _,od as ct,oe as li,pa as z,pb as Ce,pc as Te,pd as je,pe as ci,qb as k,qc as jn,qd as qn,qe as qt,r as ze,ra as Q,rb as bt,rd as Qn,re as di,sa as En,sb as y,sd as Kn,ta as qe,tb as H,td as Ze,ua as Le,ub as w,ud as Xn,ue as vt,va as D,vb as ve,vd as me,ve as mi,w as pe,wa as ft,wb as W,wc as Vn,we as hi,xa as Rn,xb as m,xd as Je,xe as yt,y as yn,yb as h,yd as _t,zb as Nn}from"./chunk-G7NALUVX.js";var Da=["tooltip"],Ea=20;var Ra=new C("mat-tooltip-scroll-strategy",{providedIn:"root",factory:()=>{let i=r(X);return()=>Je(i,{scrollThrottle:Ea})}}),Fa=new C("mat-tooltip-default-options",{providedIn:"root",factory:()=>({showDelay:0,hideDelay:0,touchendHideDelay:1500})});var wi="tooltip-panel",Oa={passive:!0},Aa=8,La=8,Pa=24,Na=200,Ba=(()=>{class i{_elementRef=r(D);_ngZone=r(z);_platform=r(Ee);_ariaDescriber=r(fi);_focusMonitor=r(Ve);_dir=r(xe);_injector=r(X);_viewContainerRef=r(He);_mediaMatcher=r(li);_document=r(be);_renderer=r(ae);_animationsDisabled=q();_defaultOptions=r(Fa,{optional:!0});_overlayRef=null;_tooltipInstance=null;_overlayPanelClass;_portal;_position="below";_positionAtOrigin=!1;_disabled=!1;_tooltipClass;_viewInitialized=!1;_pointerExitEventsInitialized=!1;_tooltipComponent=Ti;_viewportMargin=8;_currentPosition;_cssClassPrefix="mat-mdc";_ariaDescriptionPending=!1;_dirSubscribed=!1;get position(){return this._position}set position(e){e!==this._position&&(this._position=e,this._overlayRef&&(this._updatePosition(this._overlayRef),this._tooltipInstance?.show(0),this._overlayRef.updatePosition()))}get positionAtOrigin(){return this._positionAtOrigin}set positionAtOrigin(e){this._positionAtOrigin=Be(e),this._detach(),this._overlayRef=null}get disabled(){return this._disabled}set disabled(e){let t=Be(e);this._disabled!==t&&(this._disabled=t,t?this.hide(0):this._setupPointerEnterEventsIfNeeded(),this._syncAriaDescription(this.message))}get showDelay(){return this._showDelay}set showDelay(e){this._showDelay=Wt(e)}_showDelay;get hideDelay(){return this._hideDelay}set hideDelay(e){this._hideDelay=Wt(e),this._tooltipInstance&&(this._tooltipInstance._mouseLeaveHideDelay=this._hideDelay)}_hideDelay;touchGestures="auto";get message(){return this._message}set message(e){let t=this._message;this._message=e!=null?String(e).trim():"",!this._message&&this._isTooltipVisible()?this.hide(0):(this._setupPointerEnterEventsIfNeeded(),this._updateTooltipMessage()),this._syncAriaDescription(t)}_message="";get tooltipClass(){return this._tooltipClass}set tooltipClass(e){this._tooltipClass=e,this._tooltipInstance&&this._setTooltipClass(this._tooltipClass)}_eventCleanups=[];_touchstartTimeout=null;_destroyed=new f;_isDestroyed=!1;constructor(){let e=this._defaultOptions;e&&(this._showDelay=e.showDelay,this._hideDelay=e.hideDelay,e.position&&(this.position=e.position),e.positionAtOrigin&&(this.positionAtOrigin=e.positionAtOrigin),e.touchGestures&&(this.touchGestures=e.touchGestures),e.tooltipClass&&(this.tooltipClass=e.tooltipClass)),this._viewportMargin=Aa}ngAfterViewInit(){this._viewInitialized=!0,this._setupPointerEnterEventsIfNeeded(),this._focusMonitor.monitor(this._elementRef).pipe(x(this._destroyed)).subscribe(e=>{e?e==="keyboard"&&this._ngZone.run(()=>this.show()):this._ngZone.run(()=>this.hide(0))})}ngOnDestroy(){let e=this._elementRef.nativeElement;this._touchstartTimeout&&clearTimeout(this._touchstartTimeout),this._overlayRef&&(this._overlayRef.dispose(),this._tooltipInstance=null),this._eventCleanups.forEach(t=>t()),this._eventCleanups.length=0,this._destroyed.next(),this._destroyed.complete(),this._isDestroyed=!0,this._ariaDescriber.removeDescription(e,this.message,"tooltip"),this._focusMonitor.stopMonitoring(e)}show(e=this.showDelay,t){if(this.disabled||!this.message||this._isTooltipVisible()){this._tooltipInstance?._cancelPendingAnimations();return}let n=this._createOverlay(t);this._detach(),this._portal=this._portal||new ct(this._tooltipComponent,this._viewContainerRef);let a=this._tooltipInstance=n.attach(this._portal).instance;a._triggerElement=this._elementRef.nativeElement,a._mouseLeaveHideDelay=this._hideDelay,a.afterHidden().pipe(x(this._destroyed)).subscribe(()=>this._detach()),this._setTooltipClass(this._tooltipClass),this._updateTooltipMessage(),a.show(e)}hide(e=this.hideDelay){let t=this._tooltipInstance;t&&(t.isVisible()?t.hide(e):(t._cancelPendingAnimations(),this._detach()))}toggle(e){this._isTooltipVisible()?this.hide():this.show(void 0,e)}_isTooltipVisible(){return!!this._tooltipInstance&&this._tooltipInstance.isVisible()}_createOverlay(e){if(this._overlayRef){let o=this._overlayRef.getConfig().positionStrategy;if((!this.positionAtOrigin||!e)&&o._origin instanceof D)return this._overlayRef;this._detach()}let t=this._injector.get(Yt).getAncestorScrollContainers(this._elementRef),n=`${this._cssClassPrefix}-${wi}`,a=gt(this._injector,this.positionAtOrigin?e||this._elementRef:this._elementRef).withTransformOriginOn(`.${this._cssClassPrefix}-tooltip`).withFlexibleDimensions(!1).withViewportMargin(this._viewportMargin).withScrollableContainers(t).withPopoverLocation("global");return a.positionChanges.pipe(x(this._destroyed)).subscribe(o=>{this._updateCurrentPositionClass(o.connectionPair),this._tooltipInstance&&o.scrollableViewProperties.isOverlayClipped&&this._tooltipInstance.isVisible()&&this._ngZone.run(()=>this.hide(0))}),this._overlayRef=et(this._injector,{direction:this._dir,positionStrategy:a,panelClass:this._overlayPanelClass?[...this._overlayPanelClass,n]:n,scrollStrategy:this._injector.get(Ra)(),disableAnimations:this._animationsDisabled,eventPredicate:this._overlayEventPredicate}),this._updatePosition(this._overlayRef),this._overlayRef.detachments().pipe(x(this._destroyed)).subscribe(()=>this._detach()),this._overlayRef.outsidePointerEvents().pipe(x(this._destroyed)).subscribe(()=>this._tooltipInstance?._handleBodyInteraction()),this._overlayRef.keydownEvents().pipe(x(this._destroyed)).subscribe(o=>{o.preventDefault(),o.stopPropagation(),this._ngZone.run(()=>this.hide(0))}),this._defaultOptions?.disableTooltipInteractivity&&this._overlayRef.addPanelClass(`${this._cssClassPrefix}-tooltip-panel-non-interactive`),this._dirSubscribed||(this._dirSubscribed=!0,this._dir.change.pipe(x(this._destroyed)).subscribe(()=>{this._overlayRef&&this._updatePosition(this._overlayRef)})),this._overlayRef}_detach(){this._overlayRef&&this._overlayRef.hasAttached()&&this._overlayRef.detach(),this._tooltipInstance=null}_updatePosition(e){let t=e.getConfig().positionStrategy,n=this._getOrigin(),a=this._getOverlayPosition();t.withPositions([this._addOffset(S(S({},n.main),a.main)),this._addOffset(S(S({},n.fallback),a.fallback))])}_addOffset(e){let t=La,n=!this._dir||this._dir.value=="ltr";return e.originY==="top"?e.offsetY=-t:e.originY==="bottom"?e.offsetY=t:e.originX==="start"?e.offsetX=n?-t:t:e.originX==="end"&&(e.offsetX=n?t:-t),e}_getOrigin(){let e=!this._dir||this._dir.value=="ltr",t=this.position,n;t=="above"||t=="below"?n={originX:"center",originY:t=="above"?"top":"bottom"}:t=="before"||t=="left"&&e||t=="right"&&!e?n={originX:"start",originY:"center"}:(t=="after"||t=="right"&&e||t=="left"&&!e)&&(n={originX:"end",originY:"center"});let{x:a,y:o}=this._invertPosition(n.originX,n.originY);return{main:n,fallback:{originX:a,originY:o}}}_getOverlayPosition(){let e=!this._dir||this._dir.value=="ltr",t=this.position,n;t=="above"?n={overlayX:"center",overlayY:"bottom"}:t=="below"?n={overlayX:"center",overlayY:"top"}:t=="before"||t=="left"&&e||t=="right"&&!e?n={overlayX:"end",overlayY:"center"}:(t=="after"||t=="right"&&e||t=="left"&&!e)&&(n={overlayX:"start",overlayY:"center"});let{x:a,y:o}=this._invertPosition(n.overlayX,n.overlayY);return{main:n,fallback:{overlayX:a,overlayY:o}}}_updateTooltipMessage(){this._tooltipInstance&&(this._tooltipInstance.message=this.message,this._tooltipInstance._markForCheck(),ce(()=>{this._tooltipInstance&&this._overlayRef.updatePosition()},{injector:this._injector}))}_setTooltipClass(e){this._tooltipInstance&&(this._tooltipInstance.tooltipClass=e instanceof Set?Array.from(e):e,this._tooltipInstance._markForCheck())}_invertPosition(e,t){return this.position==="above"||this.position==="below"?t==="top"?t="bottom":t==="bottom"&&(t="top"):e==="end"?e="start":e==="start"&&(e="end"),{x:e,y:t}}_updateCurrentPositionClass(e){let{overlayY:t,originX:n,originY:a}=e,o;if(t==="center"?this._dir&&this._dir.value==="rtl"?o=n==="end"?"left":"right":o=n==="start"?"left":"right":o=t==="bottom"&&a==="top"?"above":"below",o!==this._currentPosition){let l=this._overlayRef;if(l){let v=`${this._cssClassPrefix}-${wi}-`;l.removePanelClass(v+this._currentPosition),l.addPanelClass(v+o)}this._currentPosition=o}}_setupPointerEnterEventsIfNeeded(){this._disabled||!this.message||!this._viewInitialized||this._eventCleanups.length||(this._isTouchPlatform()?this.touchGestures!=="off"&&(this._disableNativeGesturesIfNecessary(),this._addListener("touchstart",e=>{let t=e.targetTouches?.[0],n=t?{x:t.clientX,y:t.clientY}:void 0;this._setupPointerExitEventsIfNeeded(),this._touchstartTimeout&&clearTimeout(this._touchstartTimeout);let a=500;this._touchstartTimeout=setTimeout(()=>{this._touchstartTimeout=null,this.show(void 0,n)},this._defaultOptions?.touchLongPressShowDelay??a)})):this._addListener("mouseenter",e=>{this._setupPointerExitEventsIfNeeded();let t;e.x!==void 0&&e.y!==void 0&&(t=e),this.show(void 0,t)}))}_setupPointerExitEventsIfNeeded(){if(!this._pointerExitEventsInitialized){if(this._pointerExitEventsInitialized=!0,!this._isTouchPlatform())this._addListener("mouseleave",e=>{let t=e.relatedTarget;(!t||!this._overlayRef?.overlayElement.contains(t))&&this.hide()}),this._addListener("wheel",e=>{if(this._isTooltipVisible()){let t=this._document.elementFromPoint(e.clientX,e.clientY),n=this._elementRef.nativeElement;t!==n&&!n.contains(t)&&this.hide()}});else if(this.touchGestures!=="off"){this._disableNativeGesturesIfNecessary();let e=()=>{this._touchstartTimeout&&clearTimeout(this._touchstartTimeout),this.hide(this._defaultOptions?.touchendHideDelay)};this._addListener("touchend",e),this._addListener("touchcancel",e)}}}_addListener(e,t){this._eventCleanups.push(this._renderer.listen(this._elementRef.nativeElement,e,t,Oa))}_isTouchPlatform(){let e=this._defaultOptions?.detectHoverCapability;return typeof e=="function"?!e():this._platform.IOS||this._platform.ANDROID?!0:this._platform.isBrowser?!!e&&this._mediaMatcher.matchMedia("(any-hover: none)").matches:!1}_disableNativeGesturesIfNecessary(){let e=this.touchGestures;if(e!=="off"){let t=this._elementRef.nativeElement,n=t.style;(e==="on"||t.nodeName!=="INPUT"&&t.nodeName!=="TEXTAREA")&&(n.userSelect=n.msUserSelect=n.webkitUserSelect=n.MozUserSelect="none"),(e==="on"||!t.draggable)&&(n.webkitUserDrag="none"),n.touchAction="none",n.webkitTapHighlightColor="transparent"}}_syncAriaDescription(e){this._ariaDescriptionPending||(this._ariaDescriptionPending=!0,this._ariaDescriber.removeDescription(this._elementRef.nativeElement,e,"tooltip"),this._isDestroyed||ce({write:()=>{this._ariaDescriptionPending=!1,this.message&&!this.disabled&&this._ariaDescriber.describe(this._elementRef.nativeElement,this.message,"tooltip")}},{injector:this._injector}))}_overlayEventPredicate=e=>e.type==="keydown"?this._isTooltipVisible()&&e.keyCode===27&&!me(e):!0;static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["","matTooltip",""]],hostAttrs:[1,"mat-mdc-tooltip-trigger"],hostVars:2,hostBindings:function(t,n){t&2&&p("mat-mdc-tooltip-disabled",n.disabled)},inputs:{position:[0,"matTooltipPosition","position"],positionAtOrigin:[0,"matTooltipPositionAtOrigin","positionAtOrigin"],disabled:[0,"matTooltipDisabled","disabled"],showDelay:[0,"matTooltipShowDelay","showDelay"],hideDelay:[0,"matTooltipHideDelay","hideDelay"],touchGestures:[0,"matTooltipTouchGestures","touchGestures"],message:[0,"matTooltip","message"],tooltipClass:[0,"matTooltipClass","tooltipClass"]},exportAs:["matTooltip"]})}return i})(),Ti=(()=>{class i{_changeDetectorRef=r(Y);_elementRef=r(D);_isMultiline=!1;message;tooltipClass;_showTimeoutId;_hideTimeoutId;_triggerElement;_mouseLeaveHideDelay;_animationsDisabled=q();_tooltip;_closeOnInteraction=!1;_isVisible=!1;_onHide=new f;_showAnimation="mat-mdc-tooltip-show";_hideAnimation="mat-mdc-tooltip-hide";constructor(){}show(e){this._hideTimeoutId!=null&&clearTimeout(this._hideTimeoutId),this._showTimeoutId=setTimeout(()=>{this._toggleVisibility(!0),this._showTimeoutId=void 0},e)}hide(e){this._showTimeoutId!=null&&clearTimeout(this._showTimeoutId),this._hideTimeoutId=setTimeout(()=>{this._toggleVisibility(!1),this._hideTimeoutId=void 0},e)}afterHidden(){return this._onHide}isVisible(){return this._isVisible}ngOnDestroy(){this._cancelPendingAnimations(),this._onHide.complete(),this._triggerElement=null}_handleBodyInteraction(){this._closeOnInteraction&&this.hide(0)}_markForCheck(){this._changeDetectorRef.markForCheck()}_handleMouseLeave({relatedTarget:e}){(!e||!this._triggerElement.contains(e))&&(this.isVisible()?this.hide(this._mouseLeaveHideDelay):this._finalizeAnimation(!1))}_onShow(){this._isMultiline=this._isTooltipMultiline(),this._markForCheck()}_isTooltipMultiline(){let e=this._elementRef.nativeElement.getBoundingClientRect();return e.height>Pa&&e.width>=Na}_handleAnimationEnd({animationName:e}){(e===this._showAnimation||e===this._hideAnimation)&&this._finalizeAnimation(e===this._showAnimation)}_cancelPendingAnimations(){this._showTimeoutId!=null&&clearTimeout(this._showTimeoutId),this._hideTimeoutId!=null&&clearTimeout(this._hideTimeoutId),this._showTimeoutId=this._hideTimeoutId=void 0}_finalizeAnimation(e){e?this._closeOnInteraction=!0:this.isVisible()||this._onHide.next()}_toggleVisibility(e){let t=this._tooltip.nativeElement,n=this._showAnimation,a=this._hideAnimation;if(t.classList.remove(e?a:n),t.classList.add(e?n:a),this._isVisible!==e&&(this._isVisible=e,this._changeDetectorRef.markForCheck()),e&&!this._animationsDisabled&&typeof getComputedStyle=="function"){let o=getComputedStyle(t);(o.getPropertyValue("animation-duration")==="0s"||o.getPropertyValue("animation-name")==="none")&&(this._animationsDisabled=!0)}e&&this._onShow(),this._animationsDisabled&&(t.classList.add("_mat-animation-noopable"),this._finalizeAnimation(e))}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-tooltip-component"]],viewQuery:function(t,n){if(t&1&&W(Da,7),t&2){let a;m(a=h())&&(n._tooltip=a.first)}},hostAttrs:["aria-hidden","true"],hostBindings:function(t,n){t&1&&k("mouseleave",function(o){return n._handleMouseLeave(o)})},decls:4,vars:5,consts:[["tooltip",""],[1,"mdc-tooltip","mat-mdc-tooltip",3,"animationend"],[1,"mat-mdc-tooltip-surface","mdc-tooltip__surface"]],template:function(t,n){t&1&&(ke(0,"div",1,0),bt("animationend",function(o){return n._handleAnimationEnd(o)}),ke(2,"div",2),$(3),Pe()()),t&2&&(de(n.tooltipClass),p("mdc-tooltip--multiline",n._isMultiline),d(3),se(n.message))},styles:[`.mat-mdc-tooltip { + position: relative; + transform: scale(0); + display: inline-flex; +} +.mat-mdc-tooltip::before { + content: ""; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; + position: absolute; +} +.mat-mdc-tooltip-panel-below .mat-mdc-tooltip::before { + top: -8px; +} +.mat-mdc-tooltip-panel-above .mat-mdc-tooltip::before { + bottom: -8px; +} +.mat-mdc-tooltip-panel-right .mat-mdc-tooltip::before { + left: -8px; +} +.mat-mdc-tooltip-panel-left .mat-mdc-tooltip::before { + right: -8px; +} +.mat-mdc-tooltip._mat-animation-noopable { + animation: none; + transform: scale(1); +} + +.mat-mdc-tooltip-surface { + word-break: normal; + overflow-wrap: anywhere; + padding: 4px 8px; + min-width: 40px; + max-width: 200px; + min-height: 24px; + max-height: 40vh; + box-sizing: border-box; + overflow: hidden; + text-align: center; + will-change: transform, opacity; + background-color: var(--mat-tooltip-container-color, var(--mat-sys-inverse-surface)); + color: var(--mat-tooltip-supporting-text-color, var(--mat-sys-inverse-on-surface)); + border-radius: var(--mat-tooltip-container-shape, var(--mat-sys-corner-extra-small)); + font-family: var(--mat-tooltip-supporting-text-font, var(--mat-sys-body-small-font)); + font-size: var(--mat-tooltip-supporting-text-size, var(--mat-sys-body-small-size)); + font-weight: var(--mat-tooltip-supporting-text-weight, var(--mat-sys-body-small-weight)); + line-height: var(--mat-tooltip-supporting-text-line-height, var(--mat-sys-body-small-line-height)); + letter-spacing: var(--mat-tooltip-supporting-text-tracking, var(--mat-sys-body-small-tracking)); +} +.mat-mdc-tooltip-surface::before { + position: absolute; + box-sizing: border-box; + width: 100%; + height: 100%; + top: 0; + left: 0; + border: 1px solid transparent; + border-radius: inherit; + content: ""; + pointer-events: none; +} +.mdc-tooltip--multiline .mat-mdc-tooltip-surface { + text-align: left; +} +[dir=rtl] .mdc-tooltip--multiline .mat-mdc-tooltip-surface { + text-align: right; +} + +.mat-mdc-tooltip-panel { + line-height: normal; +} +.mat-mdc-tooltip-panel.mat-mdc-tooltip-panel-non-interactive { + pointer-events: none; +} + +@keyframes mat-mdc-tooltip-show { + 0% { + opacity: 0; + transform: scale(0.8); + } + 100% { + opacity: 1; + transform: scale(1); + } +} +@keyframes mat-mdc-tooltip-hide { + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0.8); + } +} +.mat-mdc-tooltip-show { + animation: mat-mdc-tooltip-show 150ms cubic-bezier(0, 0, 0.2, 1) forwards; +} + +.mat-mdc-tooltip-hide { + animation: mat-mdc-tooltip-hide 75ms cubic-bezier(0.4, 0, 1, 1) forwards; +} +`],encapsulation:2,changeDetection:0})}return i})();var _s=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[mi,Ne,j,Xe]})}return i})();var za=["mat-menu-item",""],Ha=[[["mat-icon"],["","matMenuItemIcon",""]],"*"],ja=["mat-icon, [matMenuItemIcon]","*"];function Va(i,s){i&1&&(Ue(),c(0,"svg",2),A(1,"polygon",3),u())}var Ga=["*"];function Wa(i,s){if(i&1){let e=Se();ke(0,"div",0),bt("click",function(){ne(e);let n=y();return ie(n.closed.emit("click"))})("animationstart",function(n){ne(e);let a=y();return ie(a._onAnimationStart(n.animationName))})("animationend",function(n){ne(e);let a=y();return ie(a._onAnimationDone(n.animationName))})("animationcancel",function(n){ne(e);let a=y();return ie(a._onAnimationDone(n.animationName))}),ke(1,"div",1),w(2),Pe()()}if(i&2){let e=y();de(e._classList),p("mat-menu-panel-animations-disabled",e._animationsDisabled)("mat-menu-panel-exit-animation",e._panelAnimationState==="void")("mat-menu-panel-animating",e._isAnimating()),Ce("id",e.panelId),T("aria-label",e.ariaLabel||null)("aria-labelledby",e.ariaLabelledby||null)("aria-describedby",e.ariaDescribedby||null)}}var Jt=new C("MAT_MENU_PANEL"),Zt=(()=>{class i{_elementRef=r(D);_document=r(be);_focusMonitor=r(Ve);_parentMenu=r(Jt,{optional:!0});_changeDetectorRef=r(Y);role="menuitem";disabled=!1;disableRipple=!1;_hovered=new f;_focused=new f;_highlighted=!1;_triggersSubmenu=!1;constructor(){r(De).load(Re),this._parentMenu?.addItem?.(this)}focus(e,t){this._focusMonitor&&e?this._focusMonitor.focusVia(this._getHostElement(),e,t):this._getHostElement().focus(t),this._focused.next(this)}ngAfterViewInit(){this._focusMonitor&&this._focusMonitor.monitor(this._elementRef,!1)}ngOnDestroy(){this._focusMonitor&&this._focusMonitor.stopMonitoring(this._elementRef),this._parentMenu&&this._parentMenu.removeItem&&this._parentMenu.removeItem(this),this._hovered.complete(),this._focused.complete()}_getTabIndex(){return this.disabled?"-1":"0"}_getHostElement(){return this._elementRef.nativeElement}_checkDisabled(e){this.disabled&&(e.preventDefault(),e.stopPropagation())}_handleMouseEnter(){this._hovered.next(this)}getLabel(){let e=this._elementRef.nativeElement.cloneNode(!0),t=e.querySelectorAll("mat-icon, .material-icons");for(let n=0;n{class i{_template=r(Ie);_appRef=r(Ln);_injector=r(X);_viewContainerRef=r(He);_document=r(be);_changeDetectorRef=r(Y);_portal;_outlet;_attached=new f;constructor(){}attach(e={}){this._portal||(this._portal=new je(this._template,this._viewContainerRef)),this.detach(),this._outlet||(this._outlet=new Qn(this._document.createElement("div"),this._appRef,this._injector));let t=this._template.elementRef.nativeElement;t.parentNode.insertBefore(this._outlet.outletElement,t),this._changeDetectorRef.markForCheck(),this._portal.attach(this._outlet,e),this._attached.next()}detach(){this._portal?.isAttached&&this._portal.detach()}ngOnDestroy(){this.detach(),this._outlet?.dispose()}static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["ng-template","matMenuContent",""]],features:[U([{provide:Mi,useExisting:i}])]})}return i})(),Ya=new C("mat-menu-default-options",{providedIn:"root",factory:()=>({overlapTrigger:!1,xPosition:"after",yPosition:"below",backdropClass:"cdk-overlay-transparent-backdrop"})}),Xt="_mat-menu-enter",kt="_mat-menu-exit",Ct=(()=>{class i{_elementRef=r(D);_changeDetectorRef=r(Y);_injector=r(X);_keyManager;_xPosition;_yPosition;_firstItemFocusRef;_exitFallbackTimeout;_animationsDisabled=q();_allItems;_directDescendantItems=new ft;_classList={};_panelAnimationState="void";_animationDone=new f;_isAnimating=Q(!1);parentMenu;direction;overlayPanelClass;backdropClass;ariaLabel;ariaLabelledby;ariaDescribedby;get xPosition(){return this._xPosition}set xPosition(e){this._xPosition=e,this.setPositionClasses()}get yPosition(){return this._yPosition}set yPosition(e){this._yPosition=e,this.setPositionClasses()}templateRef;items;lazyContent;overlapTrigger=!1;hasBackdrop;set panelClass(e){let t=this._previousPanelClass,n=S({},this._classList);t&&t.length&&t.split(" ").forEach(a=>{n[a]=!1}),this._previousPanelClass=e,e&&e.length&&(e.split(" ").forEach(a=>{n[a]=!0}),this._elementRef.nativeElement.className=""),this._classList=n}_previousPanelClass;get classList(){return this.panelClass}set classList(e){this.panelClass=e}closed=new g;close=this.closed;panelId=r(K).getId("mat-menu-panel-");constructor(){let e=r(Ya);this.overlayPanelClass=e.overlayPanelClass||"",this._xPosition=e.xPosition,this._yPosition=e.yPosition,this.backdropClass=e.backdropClass,this.overlapTrigger=e.overlapTrigger,this.hasBackdrop=e.hasBackdrop}ngOnInit(){this.setPositionClasses()}ngAfterContentInit(){this._updateDirectDescendants(),this._keyManager=new yt(this._directDescendantItems).withWrap().withTypeAhead().withHomeAndEnd(),this._keyManager.tabOut.subscribe(()=>this.closed.emit("tab")),this._directDescendantItems.changes.pipe(te(this._directDescendantItems),Ae(e=>G(...e.map(t=>t._focused)))).subscribe(e=>this._keyManager.updateActiveItem(e)),this._directDescendantItems.changes.subscribe(e=>{let t=this._keyManager;if(this._panelAnimationState==="enter"&&t.activeItem?._hasFocus()){let n=e.toArray(),a=Math.max(0,Math.min(n.length-1,t.activeItemIndex||0));n[a]&&!n[a].disabled?t.setActiveItem(a):t.setNextItemActive()}})}ngOnDestroy(){this._keyManager?.destroy(),this._directDescendantItems.destroy(),this.closed.complete(),this._firstItemFocusRef?.destroy(),clearTimeout(this._exitFallbackTimeout)}_hovered(){return this._directDescendantItems.changes.pipe(te(this._directDescendantItems),Ae(t=>G(...t.map(n=>n._hovered))))}addItem(e){}removeItem(e){}_handleKeydown(e){let t=e.keyCode,n=this._keyManager;switch(t){case 27:me(e)||(e.preventDefault(),this.closed.emit("keydown"));break;case 37:this.parentMenu&&this.direction==="ltr"&&this.closed.emit("keydown");break;case 39:this.parentMenu&&this.direction==="rtl"&&this.closed.emit("keydown");break;default:(t===38||t===40)&&n.setFocusOrigin("keyboard"),n.onKeydown(e);return}}focusFirstItem(e="program"){this._firstItemFocusRef?.destroy(),this._firstItemFocusRef=ce(()=>{let t=this._resolvePanel();if(!t||!t.contains(document.activeElement)){let n=this._keyManager;n.setFocusOrigin(e).setFirstItemActive(),!n.activeItem&&t&&t.focus()}},{injector:this._injector})}resetActiveItem(){this._keyManager.setActiveItem(-1)}setElevation(e){}setPositionClasses(e=this.xPosition,t=this.yPosition){this._classList=le(S({},this._classList),{"mat-menu-before":e==="before","mat-menu-after":e==="after","mat-menu-above":t==="above","mat-menu-below":t==="below"}),this._changeDetectorRef.markForCheck()}_onAnimationDone(e){let t=e===kt;(t||e===Xt)&&(t&&(clearTimeout(this._exitFallbackTimeout),this._exitFallbackTimeout=void 0),this._animationDone.next(t?"void":"enter"),this._isAnimating.set(!1))}_onAnimationStart(e){(e===Xt||e===kt)&&this._isAnimating.set(!0)}_setIsOpen(e){if(this._panelAnimationState=e?"enter":"void",e){if(this._keyManager.activeItemIndex===0){let t=this._resolvePanel();t&&(t.scrollTop=0)}}else this._animationsDisabled||(this._exitFallbackTimeout=setTimeout(()=>this._onAnimationDone(kt),200));this._animationsDisabled&&setTimeout(()=>{this._onAnimationDone(e?Xt:kt)}),this._changeDetectorRef.markForCheck()}_updateDirectDescendants(){this._allItems.changes.pipe(te(this._allItems)).subscribe(e=>{this._directDescendantItems.reset(e.filter(t=>t._parentMenu===this)),this._directDescendantItems.notifyOnChanges()})}_resolvePanel(){let e=null;return this._directDescendantItems.length&&(e=this._directDescendantItems.first._getHostElement().closest('[role="menu"]')),e}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-menu"]],contentQueries:function(t,n,a){if(t&1&&ve(a,Mi,5)(a,Zt,5)(a,Zt,4),t&2){let o;m(o=h())&&(n.lazyContent=o.first),m(o=h())&&(n._allItems=o),m(o=h())&&(n.items=o)}},viewQuery:function(t,n){if(t&1&&W(Ie,5),t&2){let a;m(a=h())&&(n.templateRef=a.first)}},hostVars:3,hostBindings:function(t,n){t&2&&T("aria-label",null)("aria-labelledby",null)("aria-describedby",null)},inputs:{backdropClass:"backdropClass",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],ariaDescribedby:[0,"aria-describedby","ariaDescribedby"],xPosition:"xPosition",yPosition:"yPosition",overlapTrigger:[2,"overlapTrigger","overlapTrigger",_],hasBackdrop:[2,"hasBackdrop","hasBackdrop",e=>e==null?null:_(e)],panelClass:[0,"class","panelClass"],classList:"classList"},outputs:{closed:"closed",close:"close"},exportAs:["matMenu"],features:[U([{provide:Jt,useExisting:i}])],ngContentSelectors:Ga,decls:1,vars:0,consts:[["tabindex","-1","role","menu",1,"mat-mdc-menu-panel",3,"click","animationstart","animationend","animationcancel","id"],[1,"mat-mdc-menu-content"]],template:function(t,n){t&1&&(H(),pt(0,Wa,3,12,"ng-template"))},styles:[`mat-menu { + display: none; +} + +.mat-mdc-menu-content { + margin: 0; + padding: 8px 0; + outline: 0; +} +.mat-mdc-menu-content, +.mat-mdc-menu-content .mat-mdc-menu-item .mat-mdc-menu-item-text { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + flex: 1; + white-space: normal; + font-family: var(--mat-menu-item-label-text-font, var(--mat-sys-label-large-font)); + line-height: var(--mat-menu-item-label-text-line-height, var(--mat-sys-label-large-line-height)); + font-size: var(--mat-menu-item-label-text-size, var(--mat-sys-label-large-size)); + letter-spacing: var(--mat-menu-item-label-text-tracking, var(--mat-sys-label-large-tracking)); + font-weight: var(--mat-menu-item-label-text-weight, var(--mat-sys-label-large-weight)); +} + +@keyframes _mat-menu-enter { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes _mat-menu-exit { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +.mat-mdc-menu-panel { + min-width: 112px; + max-width: 280px; + overflow: auto; + box-sizing: border-box; + outline: 0; + animation: _mat-menu-enter 120ms cubic-bezier(0, 0, 0.2, 1); + border-radius: var(--mat-menu-container-shape, var(--mat-sys-corner-extra-small)); + background-color: var(--mat-menu-container-color, var(--mat-sys-surface-container)); + box-shadow: var(--mat-menu-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12)); + will-change: transform, opacity; +} +.mat-mdc-menu-panel.mat-menu-panel-exit-animation { + animation: _mat-menu-exit 100ms 25ms linear forwards; +} +.mat-mdc-menu-panel.mat-menu-panel-animations-disabled { + animation: none; +} +.mat-mdc-menu-panel.mat-menu-panel-animating { + pointer-events: none; +} +.mat-mdc-menu-panel.mat-menu-panel-animating:has(.mat-mdc-menu-content:empty) { + display: none; +} +@media (forced-colors: active) { + .mat-mdc-menu-panel { + outline: solid 1px; + } +} +.mat-mdc-menu-panel .mat-divider { + border-top-color: var(--mat-menu-divider-color, var(--mat-sys-surface-variant)); + margin-bottom: var(--mat-menu-divider-bottom-spacing, 8px); + margin-top: var(--mat-menu-divider-top-spacing, 8px); +} + +.mat-mdc-menu-item { + display: flex; + position: relative; + align-items: center; + justify-content: flex-start; + overflow: hidden; + padding: 0; + cursor: pointer; + width: 100%; + text-align: left; + box-sizing: border-box; + color: inherit; + font-size: inherit; + background: none; + text-decoration: none; + margin: 0; + min-height: 48px; + padding-left: var(--mat-menu-item-leading-spacing, 12px); + padding-right: var(--mat-menu-item-trailing-spacing, 12px); + -webkit-user-select: none; + user-select: none; + cursor: pointer; + outline: none; + border: none; + -webkit-tap-highlight-color: transparent; +} +.mat-mdc-menu-item::-moz-focus-inner { + border: 0; +} +[dir=rtl] .mat-mdc-menu-item { + padding-left: var(--mat-menu-item-trailing-spacing, 12px); + padding-right: var(--mat-menu-item-leading-spacing, 12px); +} +.mat-mdc-menu-item:has(.material-icons, mat-icon, [matButtonIcon]) { + padding-left: var(--mat-menu-item-with-icon-leading-spacing, 12px); + padding-right: var(--mat-menu-item-with-icon-trailing-spacing, 12px); +} +[dir=rtl] .mat-mdc-menu-item:has(.material-icons, mat-icon, [matButtonIcon]) { + padding-left: var(--mat-menu-item-with-icon-trailing-spacing, 12px); + padding-right: var(--mat-menu-item-with-icon-leading-spacing, 12px); +} +.mat-mdc-menu-item, .mat-mdc-menu-item:visited, .mat-mdc-menu-item:link { + color: var(--mat-menu-item-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-menu-item .mat-icon-no-color, +.mat-mdc-menu-item .mat-mdc-menu-submenu-icon { + color: var(--mat-menu-item-icon-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-menu-item[disabled] { + cursor: default; + opacity: 0.38; +} +.mat-mdc-menu-item[disabled]::after { + display: block; + position: absolute; + content: ""; + top: 0; + left: 0; + bottom: 0; + right: 0; +} +.mat-mdc-menu-item:focus { + outline: 0; +} +.mat-mdc-menu-item .mat-icon { + flex-shrink: 0; + margin-right: var(--mat-menu-item-spacing, 12px); + height: var(--mat-menu-item-icon-size, 24px); + width: var(--mat-menu-item-icon-size, 24px); +} +[dir=rtl] .mat-mdc-menu-item { + text-align: right; +} +[dir=rtl] .mat-mdc-menu-item .mat-icon { + margin-right: 0; + margin-left: var(--mat-menu-item-spacing, 12px); +} +.mat-mdc-menu-item:not([disabled]):hover { + background-color: var(--mat-menu-item-hover-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent)); +} +.mat-mdc-menu-item:not([disabled]).cdk-program-focused, .mat-mdc-menu-item:not([disabled]).cdk-keyboard-focused, .mat-mdc-menu-item:not([disabled]).mat-mdc-menu-item-highlighted { + background-color: var(--mat-menu-item-focus-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent)); +} +@media (forced-colors: active) { + .mat-mdc-menu-item { + margin-top: 1px; + } +} + +.mat-mdc-menu-submenu-icon { + width: var(--mat-menu-item-icon-size, 24px); + height: 10px; + fill: currentColor; + padding-left: var(--mat-menu-item-spacing, 12px); +} +[dir=rtl] .mat-mdc-menu-submenu-icon { + padding-right: var(--mat-menu-item-spacing, 12px); + padding-left: 0; +} +[dir=rtl] .mat-mdc-menu-submenu-icon polygon { + transform: scaleX(-1); + transform-origin: center; +} +@media (forced-colors: active) { + .mat-mdc-menu-submenu-icon { + fill: CanvasText; + } +} + +.mat-mdc-menu-item .mat-mdc-menu-ripple { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + pointer-events: none; +} +`],encapsulation:2,changeDetection:0})}return i})(),$a=new C("mat-menu-scroll-strategy",{providedIn:"root",factory:()=>{let i=r(X);return()=>Je(i)}});var nt=new WeakMap,Ua=(()=>{class i{_canHaveBackdrop;_element=r(D);_viewContainerRef=r(He);_menuItemInstance=r(Zt,{optional:!0,self:!0});_dir=r(xe,{optional:!0});_focusMonitor=r(Ve);_ngZone=r(z);_injector=r(X);_scrollStrategy=r($a);_changeDetectorRef=r(Y);_animationsDisabled=q();_portal;_overlayRef=null;_menuOpen=!1;_closingActionsSubscription=re.EMPTY;_menuCloseSubscription=re.EMPTY;_pendingRemoval;_parentMaterialMenu;_parentInnerPadding;_openedBy=void 0;get _menu(){return this._menuInternal}set _menu(e){e!==this._menuInternal&&(this._menuInternal=e,this._menuCloseSubscription.unsubscribe(),e&&(this._parentMaterialMenu,this._menuCloseSubscription=e.close.subscribe(t=>{this._destroyMenu(t),(t==="click"||t==="tab")&&this._parentMaterialMenu&&this._parentMaterialMenu.closed.emit(t)})),this._menuItemInstance?._setTriggersSubmenu(this._triggersSubmenu()))}_menuInternal=null;constructor(e){this._canHaveBackdrop=e;let t=r(Jt,{optional:!0});this._parentMaterialMenu=t instanceof Ct?t:void 0}ngOnDestroy(){this._menu&&this._ownsMenu(this._menu)&&nt.delete(this._menu),this._pendingRemoval?.unsubscribe(),this._menuCloseSubscription.unsubscribe(),this._closingActionsSubscription.unsubscribe(),this._overlayRef&&(this._overlayRef.dispose(),this._overlayRef=null)}get menuOpen(){return this._menuOpen}get dir(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}_triggersSubmenu(){return!!(this._menuItemInstance&&this._parentMaterialMenu&&this._menu)}_closeMenu(){this._menu?.close.emit()}_openMenu(e){if(this._triggerIsAriaDisabled())return;let t=this._menu;if(this._menuOpen||!t)return;this._pendingRemoval?.unsubscribe();let n=nt.get(t);nt.set(t,this),n&&n!==this&&n._closeMenu();let a=this._createOverlay(t),o=a.getConfig(),l=o.positionStrategy;this._setPosition(t,l),this._canHaveBackdrop?o.hasBackdrop=t.hasBackdrop==null?!this._triggersSubmenu():t.hasBackdrop:o.hasBackdrop=t.hasBackdrop??!1,a.hasAttached()||(a.attach(this._getPortal(t)),t.lazyContent?.attach(this.menuData)),this._closingActionsSubscription=this._menuClosingActions().subscribe(()=>this._closeMenu()),t.parentMenu=this._triggersSubmenu()?this._parentMaterialMenu:void 0,t.direction=this.dir,e&&t.focusFirstItem(this._openedBy||"program"),this._setIsMenuOpen(!0),t instanceof Ct&&(t._setIsOpen(!0),t._directDescendantItems.changes.pipe(x(t.close)).subscribe(()=>{l.withLockedPosition(!1).reapplyLastPosition(),l.withLockedPosition(!0)}))}focus(e,t){this._focusMonitor&&e?this._focusMonitor.focusVia(this._element,e,t):this._element.nativeElement.focus(t)}_destroyMenu(e){let t=this._overlayRef,n=this._menu;!t||!this.menuOpen||(this._closingActionsSubscription.unsubscribe(),this._pendingRemoval?.unsubscribe(),n instanceof Ct&&this._ownsMenu(n)?(this._pendingRemoval=n._animationDone.pipe($e(1)).subscribe(()=>{t.detach(),nt.has(n)||n.lazyContent?.detach()}),n._setIsOpen(!1)):(t.detach(),n?.lazyContent?.detach()),n&&this._ownsMenu(n)&&nt.delete(n),this.restoreFocus&&(e==="keydown"||!this._openedBy||!this._triggersSubmenu())&&this.focus(this._openedBy),this._openedBy=void 0,this._setIsMenuOpen(!1))}_setIsMenuOpen(e){e!==this._menuOpen&&(this._menuOpen=e,this._menuOpen?this.menuOpened.emit():this.menuClosed.emit(),this._triggersSubmenu()&&this._menuItemInstance._setHighlighted(e),this._changeDetectorRef.markForCheck())}_createOverlay(e){if(!this._overlayRef){let t=this._getOverlayConfig(e);this._subscribeToPositions(e,t.positionStrategy),this._overlayRef=et(this._injector,t),this._overlayRef.keydownEvents().subscribe(n=>{this._menu instanceof Ct&&this._menu._handleKeydown(n)})}return this._overlayRef}_getOverlayConfig(e){return new _t({positionStrategy:gt(this._injector,this._getOverlayOrigin()).withLockedPosition().withGrowAfterOpen().withTransformOriginOn(".mat-menu-panel, .mat-mdc-menu-panel"),backdropClass:e.backdropClass||"cdk-overlay-transparent-backdrop",panelClass:e.overlayPanelClass,scrollStrategy:this._scrollStrategy(),direction:this._dir||"ltr",disableAnimations:this._animationsDisabled})}_subscribeToPositions(e,t){e.setPositionClasses&&t.positionChanges.subscribe(n=>{this._ngZone.run(()=>{let a=n.connectionPair.overlayX==="start"?"after":"before",o=n.connectionPair.overlayY==="top"?"below":"above";e.setPositionClasses(a,o)})})}_setPosition(e,t){let[n,a]=e.xPosition==="before"?["end","start"]:["start","end"],[o,l]=e.yPosition==="above"?["bottom","top"]:["top","bottom"],[v,b]=[o,l],[V,ue]=[n,a],fe=0;if(this._triggersSubmenu()){if(ue=n=e.xPosition==="before"?"start":"end",a=V=n==="end"?"start":"end",this._parentMaterialMenu){if(this._parentInnerPadding==null){let R=this._parentMaterialMenu.items.first;this._parentInnerPadding=R?R._getHostElement().offsetTop:0}fe=o==="bottom"?this._parentInnerPadding:-this._parentInnerPadding}}else e.overlapTrigger||(v=o==="top"?"bottom":"top",b=l==="top"?"bottom":"top");t.withPositions([{originX:n,originY:v,overlayX:V,overlayY:o,offsetY:fe},{originX:a,originY:v,overlayX:ue,overlayY:o,offsetY:fe},{originX:n,originY:b,overlayX:V,overlayY:l,offsetY:-fe},{originX:a,originY:b,overlayX:ue,overlayY:l,offsetY:-fe}])}_menuClosingActions(){let e=this._getOutsideClickStream(this._overlayRef),t=this._overlayRef.detachments(),n=this._parentMaterialMenu?this._parentMaterialMenu.closed:ze(),a=this._parentMaterialMenu?this._parentMaterialMenu._hovered().pipe(Z(o=>this._menuOpen&&o!==this._menuItemInstance)):ze();return G(e,n,a,t)}_getPortal(e){return(!this._portal||this._portal.templateRef!==e.templateRef)&&(this._portal=new je(e.templateRef,this._viewContainerRef)),this._portal}_ownsMenu(e){return nt.get(e)===this}_triggerIsAriaDisabled(){return _(this._element.nativeElement.getAttribute("aria-disabled"))}static \u0275fac=function(t){On()};static \u0275dir=I({type:i})}return i})(),Ys=(()=>{class i extends Ua{_cleanupTouchstart;_hoverSubscription=re.EMPTY;get _deprecatedMatMenuTriggerFor(){return this.menu}set _deprecatedMatMenuTriggerFor(e){this.menu=e}get menu(){return this._menu}set menu(e){this._menu=e}menuData;restoreFocus=!0;menuOpened=new g;onMenuOpen=this.menuOpened;menuClosed=new g;onMenuClose=this.menuClosed;constructor(){super(!0);let e=r(ae);this._cleanupTouchstart=e.listen(this._element.nativeElement,"touchstart",t=>{$n(t)||(this._openedBy="touch")},{passive:!0})}triggersSubmenu(){return super._triggersSubmenu()}toggleMenu(){return this.menuOpen?this.closeMenu():this.openMenu()}openMenu(){this._openMenu(!0)}closeMenu(){this._closeMenu()}updatePosition(){this._overlayRef?.updatePosition()}ngAfterContentInit(){this._handleHover()}ngOnDestroy(){super.ngOnDestroy(),this._cleanupTouchstart(),this._hoverSubscription.unsubscribe()}_getOverlayOrigin(){return this._element}_getOutsideClickStream(e){return e.backdropClick()}_handleMousedown(e){Yn(e)||(this._openedBy=e.button===0?"mouse":void 0,this.triggersSubmenu()&&e.preventDefault())}_handleKeydown(e){let t=e.keyCode;(t===13||t===32)&&(this._openedBy="keyboard"),this.triggersSubmenu()&&(t===39&&this.dir==="ltr"||t===37&&this.dir==="rtl")&&(this._openedBy="keyboard",this.openMenu())}_handleClick(e){this.triggersSubmenu()?(e.stopPropagation(),this.openMenu()):this.toggleMenu()}_handleHover(){this.triggersSubmenu()&&this._parentMaterialMenu&&(this._hoverSubscription=this._parentMaterialMenu._hovered().subscribe(e=>{e===this._menuItemInstance&&!e.disabled&&this._parentMaterialMenu?._panelAnimationState!=="void"&&(this._openedBy="mouse",this._openMenu(!1))}))}static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["","mat-menu-trigger-for",""],["","matMenuTriggerFor",""]],hostAttrs:[1,"mat-mdc-menu-trigger"],hostVars:3,hostBindings:function(t,n){t&1&&k("click",function(o){return n._handleClick(o)})("mousedown",function(o){return n._handleMousedown(o)})("keydown",function(o){return n._handleKeydown(o)}),t&2&&T("aria-haspopup",n.menu?"menu":null)("aria-expanded",n.menuOpen)("aria-controls",n.menuOpen?n.menu==null?null:n.menu.panelId:null)},inputs:{_deprecatedMatMenuTriggerFor:[0,"mat-menu-trigger-for","_deprecatedMatMenuTriggerFor"],menu:[0,"matMenuTriggerFor","menu"],menuData:[0,"matMenuTriggerData","menuData"],restoreFocus:[0,"matMenuTriggerRestoreFocus","restoreFocus"]},outputs:{menuOpened:"menuOpened",onMenuOpen:"onMenuOpen",menuClosed:"menuClosed",onMenuClose:"onMenuClose"},exportAs:["matMenuTrigger"],features:[_e]})}return i})();var $s=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[dt,Ne,j,Xe]})}return i})();var qa=["*",[["mat-option"],["ng-container"]]],Qa=["*","mat-option, ng-container"],Ka=["text"],Xa=[[["mat-icon"]],"*"],Za=["mat-icon","*"];function Ja(i,s){if(i&1&&A(0,"mat-pseudo-checkbox",1),i&2){let e=y();E("disabled",e.disabled)("state",e.selected?"checked":"unchecked")}}function eo(i,s){if(i&1&&A(0,"mat-pseudo-checkbox",3),i&2){let e=y();E("disabled",e.disabled)}}function to(i,s){if(i&1&&(c(0,"span",4),$(1),u()),i&2){let e=y();d(),Qe("(",e.group.label,")")}}var St=new C("MAT_OPTION_PARENT_COMPONENT"),Dt=new C("MatOptgroup"),no=(()=>{class i{label;disabled=!1;_labelId=r(K).getId("mat-optgroup-label-");_inert;constructor(){let e=r(St,{optional:!0});this._inert=e?.inertGroups??!1}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-optgroup"]],hostAttrs:[1,"mat-mdc-optgroup"],hostVars:3,hostBindings:function(t,n){t&2&&T("role",n._inert?null:"group")("aria-disabled",n._inert?null:n.disabled.toString())("aria-labelledby",n._inert?null:n._labelId)},inputs:{label:"label",disabled:[2,"disabled","disabled",_]},exportAs:["matOptgroup"],features:[U([{provide:Dt,useExisting:i}])],ngContentSelectors:Qa,decls:5,vars:4,consts:[["role","presentation",1,"mat-mdc-optgroup-label",3,"id"],[1,"mdc-list-item__primary-text"]],template:function(t,n){t&1&&(H(qa),ke(0,"span",0)(1,"span",1),$(2),w(3),Pe()(),w(4,1)),t&2&&(p("mdc-list-item--disabled",n.disabled),Ce("id",n._labelId),d(2),Qe("",n.label," "))},styles:[`.mat-mdc-optgroup { + color: var(--mat-optgroup-label-text-color, var(--mat-sys-on-surface-variant)); + font-family: var(--mat-optgroup-label-text-font, var(--mat-sys-title-small-font)); + line-height: var(--mat-optgroup-label-text-line-height, var(--mat-sys-title-small-line-height)); + font-size: var(--mat-optgroup-label-text-size, var(--mat-sys-title-small-size)); + letter-spacing: var(--mat-optgroup-label-text-tracking, var(--mat-sys-title-small-tracking)); + font-weight: var(--mat-optgroup-label-text-weight, var(--mat-sys-title-small-weight)); +} + +.mat-mdc-optgroup-label { + display: flex; + position: relative; + align-items: center; + justify-content: flex-start; + overflow: hidden; + min-height: 48px; + padding: 0 16px; + outline: none; +} +.mat-mdc-optgroup-label.mdc-list-item--disabled { + opacity: 0.38; +} +.mat-mdc-optgroup-label .mdc-list-item__primary-text { + font-size: inherit; + font-weight: inherit; + letter-spacing: inherit; + line-height: inherit; + font-family: inherit; + text-decoration: inherit; + text-transform: inherit; + white-space: normal; + color: inherit; +} +`],encapsulation:2,changeDetection:0})}return i})(),en=class{source;isUserInput;constructor(s,e=!1){this.source=s,this.isUserInput=e}},Et=(()=>{class i{_element=r(D);_changeDetectorRef=r(Y);_parent=r(St,{optional:!0});group=r(Dt,{optional:!0});_signalDisableRipple=!1;_selected=!1;_active=!1;_mostRecentViewValue="";get multiple(){return this._parent&&this._parent.multiple}get selected(){return this._selected}value;id=r(K).getId("mat-option-");get disabled(){return this.group&&this.group.disabled||this._disabled()}set disabled(e){this._disabled.set(e)}_disabled=Q(!1);get disableRipple(){return this._signalDisableRipple?this._parent.disableRipple():!!this._parent?.disableRipple}get hideSingleSelectionIndicator(){return!!(this._parent&&this._parent.hideSingleSelectionIndicator)}onSelectionChange=new g;_text;_stateChanges=new f;constructor(){let e=r(De);e.load(Re),e.load(si),this._signalDisableRipple=!!this._parent&&An(this._parent.disableRipple)}get active(){return this._active}get viewValue(){return(this._text?.nativeElement.textContent||"").trim()}select(e=!0){this._selected||(this._selected=!0,this._changeDetectorRef.markForCheck(),e&&this._emitSelectionChangeEvent())}deselect(e=!0){this._selected&&(this._selected=!1,this._changeDetectorRef.markForCheck(),e&&this._emitSelectionChangeEvent())}focus(e,t){let n=this._getHostElement();typeof n.focus=="function"&&n.focus(t)}setActiveStyles(){this._active||(this._active=!0,this._changeDetectorRef.markForCheck())}setInactiveStyles(){this._active&&(this._active=!1,this._changeDetectorRef.markForCheck())}getLabel(){return this.viewValue}_handleKeydown(e){(e.keyCode===13||e.keyCode===32)&&!me(e)&&(this._selectViaInteraction(),e.preventDefault())}_selectViaInteraction(){this.disabled||(this._selected=this.multiple?!this._selected:!0,this._changeDetectorRef.markForCheck(),this._emitSelectionChangeEvent(!0))}_getTabIndex(){return this.disabled?"-1":"0"}_getHostElement(){return this._element.nativeElement}ngAfterViewChecked(){if(this._selected){let e=this.viewValue;e!==this._mostRecentViewValue&&(this._mostRecentViewValue&&this._stateChanges.next(),this._mostRecentViewValue=e)}}ngOnDestroy(){this._stateChanges.complete()}_emitSelectionChangeEvent(e=!1){this.onSelectionChange.emit(new en(this,e))}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-option"]],viewQuery:function(t,n){if(t&1&&W(Ka,7),t&2){let a;m(a=h())&&(n._text=a.first)}},hostAttrs:["role","option",1,"mat-mdc-option","mdc-list-item"],hostVars:11,hostBindings:function(t,n){t&1&&k("click",function(){return n._selectViaInteraction()})("keydown",function(o){return n._handleKeydown(o)}),t&2&&(Ce("id",n.id),T("aria-selected",n.selected)("aria-disabled",n.disabled.toString()),p("mdc-list-item--selected",n.selected)("mat-mdc-option-multiple",n.multiple)("mat-mdc-option-active",n.active)("mdc-list-item--disabled",n.disabled))},inputs:{value:"value",id:"id",disabled:[2,"disabled","disabled",_]},outputs:{onSelectionChange:"onSelectionChange"},exportAs:["matOption"],ngContentSelectors:Za,decls:8,vars:5,consts:[["text",""],["aria-hidden","true",1,"mat-mdc-option-pseudo-checkbox",3,"disabled","state"],[1,"mdc-list-item__primary-text"],["state","checked","aria-hidden","true","appearance","minimal",1,"mat-mdc-option-pseudo-checkbox",3,"disabled"],[1,"cdk-visually-hidden"],["aria-hidden","true","mat-ripple","",1,"mat-mdc-option-ripple","mat-focus-indicator",3,"matRippleTrigger","matRippleDisabled"]],template:function(t,n){t&1&&(H(Xa),F(0,Ja,1,2,"mat-pseudo-checkbox",1),w(1),c(2,"span",2,0),w(4,1),u(),F(5,eo,1,1,"mat-pseudo-checkbox",3),F(6,to,2,1,"span",4),A(7,"div",5)),t&2&&(O(n.multiple?0:-1),d(5),O(!n.multiple&&n.selected&&!n.hideSingleSelectionIndicator?5:-1),d(),O(n.group&&n.group._inert?6:-1),d(),E("matRippleTrigger",n._getHostElement())("matRippleDisabled",n.disabled||n.disableRipple))},dependencies:[Ci,he],styles:[`.mat-mdc-option { + -webkit-user-select: none; + user-select: none; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: flex; + position: relative; + align-items: center; + justify-content: flex-start; + overflow: hidden; + min-height: 48px; + padding: 0 16px; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + color: var(--mat-option-label-text-color, var(--mat-sys-on-surface)); + font-family: var(--mat-option-label-text-font, var(--mat-sys-label-large-font)); + line-height: var(--mat-option-label-text-line-height, var(--mat-sys-label-large-line-height)); + font-size: var(--mat-option-label-text-size, var(--mat-sys-body-large-size)); + letter-spacing: var(--mat-option-label-text-tracking, var(--mat-sys-label-large-tracking)); + font-weight: var(--mat-option-label-text-weight, var(--mat-sys-body-large-weight)); +} +.mat-mdc-option:hover:not(.mdc-list-item--disabled) { + background-color: var(--mat-option-hover-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent)); +} +.mat-mdc-option:focus.mdc-list-item, .mat-mdc-option.mat-mdc-option-active.mdc-list-item { + background-color: var(--mat-option-focus-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent)); + outline: 0; +} +.mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-active, .mat-mdc-option-multiple, :focus, :hover) { + background-color: var(--mat-option-selected-state-layer-color, var(--mat-sys-secondary-container)); +} +.mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-active, .mat-mdc-option-multiple, :focus, :hover) .mdc-list-item__primary-text { + color: var(--mat-option-selected-state-label-text-color, var(--mat-sys-on-secondary-container)); +} +.mat-mdc-option .mat-pseudo-checkbox { + --mat-pseudo-checkbox-minimal-selected-checkmark-color: var(--mat-option-selected-state-label-text-color, var(--mat-sys-on-secondary-container)); +} +.mat-mdc-option.mdc-list-item { + align-items: center; + background: transparent; +} +.mat-mdc-option.mdc-list-item--disabled { + cursor: default; + pointer-events: none; +} +.mat-mdc-option.mdc-list-item--disabled .mat-mdc-option-pseudo-checkbox, .mat-mdc-option.mdc-list-item--disabled .mdc-list-item__primary-text, .mat-mdc-option.mdc-list-item--disabled > mat-icon { + opacity: 0.38; +} +.mat-mdc-optgroup .mat-mdc-option:not(.mat-mdc-option-multiple) { + padding-left: 32px; +} +[dir=rtl] .mat-mdc-optgroup .mat-mdc-option:not(.mat-mdc-option-multiple) { + padding-left: 16px; + padding-right: 32px; +} +.mat-mdc-option .mat-icon, +.mat-mdc-option .mat-pseudo-checkbox-full { + margin-right: 16px; + flex-shrink: 0; +} +[dir=rtl] .mat-mdc-option .mat-icon, +[dir=rtl] .mat-mdc-option .mat-pseudo-checkbox-full { + margin-right: 0; + margin-left: 16px; +} +.mat-mdc-option .mat-pseudo-checkbox-minimal { + margin-left: 16px; + flex-shrink: 0; +} +[dir=rtl] .mat-mdc-option .mat-pseudo-checkbox-minimal { + margin-right: 16px; + margin-left: 0; +} +.mat-mdc-option .mat-mdc-option-ripple { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + pointer-events: none; +} +.mat-mdc-option .mdc-list-item__primary-text { + white-space: normal; + font-size: inherit; + font-weight: inherit; + letter-spacing: inherit; + line-height: inherit; + font-family: inherit; + text-decoration: inherit; + text-transform: inherit; + margin-right: auto; +} +[dir=rtl] .mat-mdc-option .mdc-list-item__primary-text { + margin-right: 0; + margin-left: auto; +} +@media (forced-colors: active) { + .mat-mdc-option.mdc-list-item--selected:not(:has(.mat-mdc-option-pseudo-checkbox))::after { + content: ""; + position: absolute; + top: 50%; + right: 16px; + transform: translateY(-50%); + width: 10px; + height: 0; + border-bottom: solid 10px; + border-radius: 10px; + } + [dir=rtl] .mat-mdc-option.mdc-list-item--selected:not(:has(.mat-mdc-option-pseudo-checkbox))::after { + right: auto; + left: 16px; + } +} + +.mat-mdc-option-multiple { + --mat-list-list-item-selected-container-color: var(--mat-list-list-item-container-color, transparent); +} + +.mat-mdc-option-active .mat-focus-indicator::before { + content: ""; +} +`],encapsulation:2,changeDetection:0})}return i})();function Ii(i,s,e){if(e.length){let t=s.toArray(),n=e.toArray(),a=0;for(let o=0;oe+t?Math.max(0,i-t+s):e}var Di=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[j]})}return i})();var Rt=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[dt,Di,Et,j]})}return i})();var tn=class{_box;_destroyed=new f;_resizeSubject=new f;_resizeObserver;_elementObservables=new Map;constructor(s){this._box=s,typeof ResizeObserver<"u"&&(this._resizeObserver=new ResizeObserver(e=>this._resizeSubject.next(e)))}observe(s){return this._elementObservables.has(s)||this._elementObservables.set(s,new Oe(e=>{let t=this._resizeSubject.subscribe(e);return this._resizeObserver?.observe(s,{box:this._box}),()=>{this._resizeObserver?.unobserve(s),t.unsubscribe(),this._elementObservables.delete(s)}}).pipe(Z(e=>e.some(t=>t.target===s)),Tn({bufferSize:1,refCount:!0}),x(this._destroyed))),this._elementObservables.get(s)}destroy(){this._destroyed.next(),this._destroyed.complete(),this._resizeSubject.complete(),this._elementObservables.clear()}},Ft=(()=>{class i{_cleanupErrorListener;_observers=new Map;_ngZone=r(z);constructor(){typeof ResizeObserver<"u"}ngOnDestroy(){for(let[,e]of this._observers)e.destroy();this._observers.clear(),this._cleanupErrorListener?.()}observe(e,t){let n=t?.box||"content-box";return this._observers.has(n)||this._observers.set(n,new tn(n)),this._observers.get(n).observe(e)}static \u0275fac=function(t){return new(t||i)};static \u0275prov=P({token:i,factory:i.\u0275fac,providedIn:"root"})}return i})();var io=["notch"],ao=["matFormFieldNotchedOutline",""],oo=["*"],Ei=["iconPrefixContainer"],Ri=["textPrefixContainer"],Fi=["iconSuffixContainer"],Oi=["textSuffixContainer"],ro=["textField"],so=["*",[["mat-label"]],[["","matPrefix",""],["","matIconPrefix",""]],[["","matTextPrefix",""]],[["","matTextSuffix",""]],[["","matSuffix",""],["","matIconSuffix",""]],[["mat-error"],["","matError",""]],[["mat-hint",3,"align","end"]],[["mat-hint","align","end"]]],lo=["*","mat-label","[matPrefix], [matIconPrefix]","[matTextPrefix]","[matTextSuffix]","[matSuffix], [matIconSuffix]","mat-error, [matError]","mat-hint:not([align='end'])","mat-hint[align='end']"];function co(i,s){i&1&&A(0,"span",21)}function mo(i,s){if(i&1&&(c(0,"label",20),w(1,1),F(2,co,1,0,"span",21),u()),i&2){let e=y(2);E("floating",e._shouldLabelFloat())("monitorResize",e._hasOutline())("id",e._labelId),T("for",e._control.disableAutomaticLabeling?null:e._control.id),d(2),O(!e.hideRequiredMarker&&e._control.required?2:-1)}}function ho(i,s){if(i&1&&F(0,mo,3,5,"label",20),i&2){let e=y();O(e._hasFloatingLabel()?0:-1)}}function uo(i,s){i&1&&A(0,"div",7)}function fo(i,s){}function po(i,s){if(i&1&&ge(0,fo,0,0,"ng-template",13),i&2){y(2);let e=we(1);E("ngTemplateOutlet",e)}}function bo(i,s){if(i&1&&(c(0,"div",9),F(1,po,1,1,null,13),u()),i&2){let e=y();E("matFormFieldNotchedOutlineOpen",e._shouldLabelFloat()),d(),O(e._forceDisplayInfixLabel()?-1:1)}}function _o(i,s){i&1&&(c(0,"div",10,2),w(2,2),u())}function go(i,s){i&1&&(c(0,"div",11,3),w(2,3),u())}function vo(i,s){}function yo(i,s){if(i&1&&ge(0,vo,0,0,"ng-template",13),i&2){y();let e=we(1);E("ngTemplateOutlet",e)}}function xo(i,s){i&1&&(c(0,"div",14,4),w(2,4),u())}function ko(i,s){i&1&&(c(0,"div",15,5),w(2,5),u())}function Co(i,s){i&1&&A(0,"div",16)}function wo(i,s){i&1&&(c(0,"div",18),w(1,6),u())}function To(i,s){if(i&1&&(c(0,"mat-hint",22),$(1),u()),i&2){let e=y(2);E("id",e._hintLabelId),d(),se(e.hintLabel)}}function Mo(i,s){if(i&1&&(c(0,"div",19),F(1,To,2,2,"mat-hint",22),w(2,7),A(3,"div",23),w(4,8),u()),i&2){let e=y();d(),O(e.hintLabel?1:-1)}}var nn=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["mat-label"]]})}return i})(),Hi=new C("MatError"),Io=(()=>{class i{id=r(K).getId("mat-mdc-error-");constructor(){}static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["mat-error"],["","matError",""]],hostAttrs:[1,"mat-mdc-form-field-error","mat-mdc-form-field-bottom-align"],hostVars:1,hostBindings:function(t,n){t&2&&Ce("id",n.id)},inputs:{id:"id"},features:[U([{provide:Hi,useExisting:i}])]})}return i})(),an=(()=>{class i{align="start";id=r(K).getId("mat-mdc-hint-");static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["mat-hint"]],hostAttrs:[1,"mat-mdc-form-field-hint","mat-mdc-form-field-bottom-align"],hostVars:4,hostBindings:function(t,n){t&2&&(Ce("id",n.id),T("align",null),p("mat-mdc-form-field-hint-end",n.align==="end"))},inputs:{align:"align",id:"id"}})}return i})(),ji=new C("MatPrefix"),So=(()=>{class i{set _isTextSelector(e){this._isText=!0}_isText=!1;static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["","matPrefix",""],["","matIconPrefix",""],["","matTextPrefix",""]],inputs:{_isTextSelector:[0,"matTextPrefix","_isTextSelector"]},features:[U([{provide:ji,useExisting:i}])]})}return i})(),Vi=new C("MatSuffix"),Do=(()=>{class i{set _isTextSelector(e){this._isText=!0}_isText=!1;static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["","matSuffix",""],["","matIconSuffix",""],["","matTextSuffix",""]],inputs:{_isTextSelector:[0,"matTextSuffix","_isTextSelector"]},features:[U([{provide:Vi,useExisting:i}])]})}return i})(),Gi=new C("FloatingLabelParent"),Ai=(()=>{class i{_elementRef=r(D);get floating(){return this._floating}set floating(e){this._floating=e,this.monitorResize&&this._handleResize()}_floating=!1;get monitorResize(){return this._monitorResize}set monitorResize(e){this._monitorResize=e,this._monitorResize?this._subscribeToResize():this._resizeSubscription.unsubscribe()}_monitorResize=!1;_resizeObserver=r(Ft);_ngZone=r(z);_parent=r(Gi);_resizeSubscription=new re;constructor(){}ngOnDestroy(){this._resizeSubscription.unsubscribe()}getWidth(){return Eo(this._elementRef.nativeElement)}get element(){return this._elementRef.nativeElement}_handleResize(){setTimeout(()=>this._parent._handleLabelResized())}_subscribeToResize(){this._resizeSubscription.unsubscribe(),this._ngZone.runOutsideAngular(()=>{this._resizeSubscription=this._resizeObserver.observe(this._elementRef.nativeElement,{box:"border-box"}).subscribe(()=>this._handleResize())})}static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["label","matFormFieldFloatingLabel",""]],hostAttrs:[1,"mdc-floating-label","mat-mdc-floating-label"],hostVars:2,hostBindings:function(t,n){t&2&&p("mdc-floating-label--float-above",n.floating)},inputs:{floating:"floating",monitorResize:"monitorResize"}})}return i})();function Eo(i){let s=i;if(s.offsetParent!==null)return s.scrollWidth;let e=s.cloneNode(!0);e.style.setProperty("position","absolute"),e.style.setProperty("transform","translate(-9999px, -9999px)"),document.documentElement.appendChild(e);let t=e.scrollWidth;return e.remove(),t}var Li="mdc-line-ripple--active",Ot="mdc-line-ripple--deactivating",Pi=(()=>{class i{_elementRef=r(D);_cleanupTransitionEnd;constructor(){let e=r(z),t=r(ae);e.runOutsideAngular(()=>{this._cleanupTransitionEnd=t.listen(this._elementRef.nativeElement,"transitionend",this._handleTransitionEnd)})}activate(){let e=this._elementRef.nativeElement.classList;e.remove(Ot),e.add(Li)}deactivate(){this._elementRef.nativeElement.classList.add(Ot)}_handleTransitionEnd=e=>{let t=this._elementRef.nativeElement.classList,n=t.contains(Ot);e.propertyName==="opacity"&&n&&t.remove(Li,Ot)};ngOnDestroy(){this._cleanupTransitionEnd()}static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["div","matFormFieldLineRipple",""]],hostAttrs:[1,"mdc-line-ripple"]})}return i})(),Ni=(()=>{class i{_elementRef=r(D);_ngZone=r(z);open=!1;_notch;ngAfterViewInit(){let e=this._elementRef.nativeElement,t=e.querySelector(".mdc-floating-label");t?(e.classList.add("mdc-notched-outline--upgraded"),typeof requestAnimationFrame=="function"&&(t.style.transitionDuration="0s",this._ngZone.runOutsideAngular(()=>{requestAnimationFrame(()=>t.style.transitionDuration="")}))):e.classList.add("mdc-notched-outline--no-label")}_setNotchWidth(e){let t=this._notch.nativeElement;!this.open||!e?t.style.width="":t.style.width=`calc(${e}px * var(--mat-mdc-form-field-floating-label-scale, 0.75) + 9px)`}_setMaxWidth(e){this._notch.nativeElement.style.setProperty("--mat-form-field-notch-max-width",`calc(100% - ${e}px)`)}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["div","matFormFieldNotchedOutline",""]],viewQuery:function(t,n){if(t&1&&W(io,5),t&2){let a;m(a=h())&&(n._notch=a.first)}},hostAttrs:[1,"mdc-notched-outline"],hostVars:2,hostBindings:function(t,n){t&2&&p("mdc-notched-outline--notched",n.open)},inputs:{open:[0,"matFormFieldNotchedOutlineOpen","open"]},attrs:ao,ngContentSelectors:oo,decls:5,vars:0,consts:[["notch",""],[1,"mat-mdc-notch-piece","mdc-notched-outline__leading"],[1,"mat-mdc-notch-piece","mdc-notched-outline__notch"],[1,"mat-mdc-notch-piece","mdc-notched-outline__trailing"]],template:function(t,n){t&1&&(H(),jt(0,"div",1),ke(1,"div",2,0),w(3),Pe(),jt(4,"div",3))},encapsulation:2,changeDetection:0})}return i})(),on=(()=>{class i{value=null;stateChanges;id;placeholder;ngControl=null;focused=!1;empty=!1;shouldLabelFloat=!1;required=!1;disabled=!1;errorState=!1;controlType;autofilled;userAriaDescribedBy;disableAutomaticLabeling;describedByIds;static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i})}return i})();var rn=new C("MatFormField"),Ro=new C("MAT_FORM_FIELD_DEFAULT_OPTIONS"),Bi="fill",Fo="auto",zi="fixed",Oo="translateY(-50%)",Wi=(()=>{class i{_elementRef=r(D);_changeDetectorRef=r(Y);_platform=r(Ee);_idGenerator=r(K);_ngZone=r(z);_defaults=r(Ro,{optional:!0});_currentDirection;_textField;_iconPrefixContainer;_textPrefixContainer;_iconSuffixContainer;_textSuffixContainer;_floatingLabel;_notchedOutline;_lineRipple;_iconPrefixContainerSignal=st("iconPrefixContainer");_textPrefixContainerSignal=st("textPrefixContainer");_iconSuffixContainerSignal=st("iconSuffixContainer");_textSuffixContainerSignal=st("textSuffixContainer");_prefixSuffixContainers=ye(()=>[this._iconPrefixContainerSignal(),this._textPrefixContainerSignal(),this._iconSuffixContainerSignal(),this._textSuffixContainerSignal()].map(e=>e?.nativeElement).filter(e=>e!==void 0));_formFieldControl;_prefixChildren;_suffixChildren;_errorChildren;_hintChildren;_labelChild=Hn(nn);get hideRequiredMarker(){return this._hideRequiredMarker}set hideRequiredMarker(e){this._hideRequiredMarker=Be(e)}_hideRequiredMarker=!1;color="primary";get floatLabel(){return this._floatLabel||this._defaults?.floatLabel||Fo}set floatLabel(e){e!==this._floatLabel&&(this._floatLabel=e,this._changeDetectorRef.markForCheck())}_floatLabel;get appearance(){return this._appearanceSignal()}set appearance(e){let t=e||this._defaults?.appearance||Bi;this._appearanceSignal.set(t)}_appearanceSignal=Q(Bi);get subscriptSizing(){return this._subscriptSizing||this._defaults?.subscriptSizing||zi}set subscriptSizing(e){this._subscriptSizing=e||this._defaults?.subscriptSizing||zi}_subscriptSizing=null;get hintLabel(){return this._hintLabel}set hintLabel(e){this._hintLabel=e,this._processHints()}_hintLabel="";_hasIconPrefix=!1;_hasTextPrefix=!1;_hasIconSuffix=!1;_hasTextSuffix=!1;_labelId=this._idGenerator.getId("mat-mdc-form-field-label-");_hintLabelId=this._idGenerator.getId("mat-mdc-hint-");_describedByIds;get _control(){return this._explicitFormFieldControl||this._formFieldControl}set _control(e){this._explicitFormFieldControl=e}_destroyed=new f;_isFocused=null;_explicitFormFieldControl;_previousControl=null;_previousControlValidatorFn=null;_stateChanges;_valueChanges;_describedByChanges;_outlineLabelOffsetResizeObserver=null;_animationsDisabled=q();constructor(){let e=this._defaults,t=r(xe);e&&(e.appearance&&(this.appearance=e.appearance),this._hideRequiredMarker=!!e?.hideRequiredMarker,e.color&&(this.color=e.color)),En(()=>this._currentDirection=t.valueSignal()),this._syncOutlineLabelOffset()}ngAfterViewInit(){this._updateFocusState(),this._animationsDisabled||this._ngZone.runOutsideAngular(()=>{setTimeout(()=>{this._elementRef.nativeElement.classList.add("mat-form-field-animations-enabled")},300)}),this._changeDetectorRef.detectChanges()}ngAfterContentInit(){this._assertFormFieldControl(),this._initializeSubscript(),this._initializePrefixAndSuffix()}ngAfterContentChecked(){this._assertFormFieldControl(),this._control!==this._previousControl&&(this._initializeControl(this._previousControl),this._control.ngControl&&this._control.ngControl.control&&(this._previousControlValidatorFn=this._control.ngControl.control.validator),this._previousControl=this._control),this._control.ngControl&&this._control.ngControl.control&&this._control.ngControl.control.validator!==this._previousControlValidatorFn&&this._changeDetectorRef.markForCheck()}ngOnDestroy(){this._outlineLabelOffsetResizeObserver?.disconnect(),this._stateChanges?.unsubscribe(),this._valueChanges?.unsubscribe(),this._describedByChanges?.unsubscribe(),this._destroyed.next(),this._destroyed.complete()}getLabelId=ye(()=>this._hasFloatingLabel()?this._labelId:null);getConnectedOverlayOrigin(){return this._textField||this._elementRef}_animateAndLockLabel(){this._hasFloatingLabel()&&(this.floatLabel="always")}_initializeControl(e){let t=this._control,n="mat-mdc-form-field-type-";e&&this._elementRef.nativeElement.classList.remove(n+e.controlType),t.controlType&&this._elementRef.nativeElement.classList.add(n+t.controlType),this._stateChanges?.unsubscribe(),this._stateChanges=t.stateChanges.subscribe(()=>{this._updateFocusState(),this._changeDetectorRef.markForCheck()}),this._describedByChanges?.unsubscribe(),this._describedByChanges=t.stateChanges.pipe(te([void 0,void 0]),pe(()=>[t.errorState,t.userAriaDescribedBy]),ut(),Z(([[a,o],[l,v]])=>a!==l||o!==v)).subscribe(()=>this._syncDescribedByIds()),this._valueChanges?.unsubscribe(),t.ngControl&&t.ngControl.valueChanges&&(this._valueChanges=t.ngControl.valueChanges.pipe(x(this._destroyed)).subscribe(()=>this._changeDetectorRef.markForCheck()))}_checkPrefixAndSuffixTypes(){this._hasIconPrefix=!!this._prefixChildren.find(e=>!e._isText),this._hasTextPrefix=!!this._prefixChildren.find(e=>e._isText),this._hasIconSuffix=!!this._suffixChildren.find(e=>!e._isText),this._hasTextSuffix=!!this._suffixChildren.find(e=>e._isText)}_initializePrefixAndSuffix(){this._checkPrefixAndSuffixTypes(),G(this._prefixChildren.changes,this._suffixChildren.changes).subscribe(()=>{this._checkPrefixAndSuffixTypes(),this._changeDetectorRef.markForCheck()})}_initializeSubscript(){this._hintChildren.changes.subscribe(()=>{this._processHints(),this._changeDetectorRef.markForCheck()}),this._errorChildren.changes.subscribe(()=>{this._syncDescribedByIds(),this._changeDetectorRef.markForCheck()}),this._validateHints(),this._syncDescribedByIds()}_assertFormFieldControl(){this._control}_updateFocusState(){let e=this._control.focused;e&&!this._isFocused?(this._isFocused=!0,this._lineRipple?.activate()):!e&&(this._isFocused||this._isFocused===null)&&(this._isFocused=!1,this._lineRipple?.deactivate()),this._elementRef.nativeElement.classList.toggle("mat-focused",e),this._textField?.nativeElement.classList.toggle("mdc-text-field--focused",e)}_syncOutlineLabelOffset(){jn({earlyRead:()=>{if(this._appearanceSignal()!=="outline")return this._outlineLabelOffsetResizeObserver?.disconnect(),null;if(globalThis.ResizeObserver){this._outlineLabelOffsetResizeObserver||=new globalThis.ResizeObserver(()=>{this._writeOutlinedLabelStyles(this._getOutlinedLabelOffset())});for(let e of this._prefixSuffixContainers())this._outlineLabelOffsetResizeObserver.observe(e,{box:"border-box"})}return this._getOutlinedLabelOffset()},write:e=>this._writeOutlinedLabelStyles(e())})}_shouldAlwaysFloat(){return this.floatLabel==="always"}_hasOutline(){return this.appearance==="outline"}_forceDisplayInfixLabel(){return!this._platform.isBrowser&&this._prefixChildren.length&&!this._shouldLabelFloat()}_hasFloatingLabel=ye(()=>!!this._labelChild());_shouldLabelFloat(){return this._hasFloatingLabel()?this._control.shouldLabelFloat||this._shouldAlwaysFloat():!1}_shouldForward(e){let t=this._control?this._control.ngControl:null;return t&&t[e]}_getSubscriptMessageType(){return this._errorChildren&&this._errorChildren.length>0&&this._control.errorState?"error":"hint"}_handleLabelResized(){this._refreshOutlineNotchWidth()}_refreshOutlineNotchWidth(){!this._hasOutline()||!this._floatingLabel||!this._shouldLabelFloat()?this._notchedOutline?._setNotchWidth(0):this._notchedOutline?._setNotchWidth(this._floatingLabel.getWidth())}_processHints(){this._validateHints(),this._syncDescribedByIds()}_validateHints(){this._hintChildren}_syncDescribedByIds(){if(this._control){let e=[];if(this._control.userAriaDescribedBy&&typeof this._control.userAriaDescribedBy=="string"&&e.push(...this._control.userAriaDescribedBy.split(" ")),this._getSubscriptMessageType()==="hint"){let a=this._hintChildren?this._hintChildren.find(l=>l.align==="start"):null,o=this._hintChildren?this._hintChildren.find(l=>l.align==="end"):null;a?e.push(a.id):this._hintLabel&&e.push(this._hintLabelId),o&&e.push(o.id)}else this._errorChildren&&e.push(...this._errorChildren.map(a=>a.id));let t=this._control.describedByIds,n;if(t){let a=this._describedByIds||e;n=e.concat(t.filter(o=>o&&!a.includes(o)))}else n=e;this._control.setDescribedByIds(n),this._describedByIds=e}}_getOutlinedLabelOffset(){if(!this._hasOutline()||!this._floatingLabel)return null;if(!this._iconPrefixContainer&&!this._textPrefixContainer)return["",null];if(!this._isAttachedToDom())return null;let e=this._iconPrefixContainer?.nativeElement,t=this._textPrefixContainer?.nativeElement,n=this._iconSuffixContainer?.nativeElement,a=this._textSuffixContainer?.nativeElement,o=e?.getBoundingClientRect().width??0,l=t?.getBoundingClientRect().width??0,v=n?.getBoundingClientRect().width??0,b=a?.getBoundingClientRect().width??0,V=this._currentDirection==="rtl"?"-1":"1",ue=`${o+l}px`,R=`calc(${V} * (${ue} + var(--mat-mdc-form-field-label-offset-x, 0px)))`,oe=`var(--mat-mdc-form-field-label-transform, ${Oo} translateX(${R}))`,ht=o+l+v+b;return[oe,ht]}_writeOutlinedLabelStyles(e){if(e!==null){let[t,n]=e;this._floatingLabel&&(this._floatingLabel.element.style.transform=t),n!==null&&this._notchedOutline?._setMaxWidth(n)}}_isAttachedToDom(){let e=this._elementRef.nativeElement;if(e.getRootNode){let t=e.getRootNode();return t&&t!==e}return document.documentElement.contains(e)}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-form-field"]],contentQueries:function(t,n,a){if(t&1&&(Nn(a,n._labelChild,nn,5),ve(a,on,5)(a,ji,5)(a,Vi,5)(a,Hi,5)(a,an,5)),t&2){Vt();let o;m(o=h())&&(n._formFieldControl=o.first),m(o=h())&&(n._prefixChildren=o),m(o=h())&&(n._suffixChildren=o),m(o=h())&&(n._errorChildren=o),m(o=h())&&(n._hintChildren=o)}},viewQuery:function(t,n){if(t&1&&(Bn(n._iconPrefixContainerSignal,Ei,5)(n._textPrefixContainerSignal,Ri,5)(n._iconSuffixContainerSignal,Fi,5)(n._textSuffixContainerSignal,Oi,5),W(ro,5)(Ei,5)(Ri,5)(Fi,5)(Oi,5)(Ai,5)(Ni,5)(Pi,5)),t&2){Vt(4);let a;m(a=h())&&(n._textField=a.first),m(a=h())&&(n._iconPrefixContainer=a.first),m(a=h())&&(n._textPrefixContainer=a.first),m(a=h())&&(n._iconSuffixContainer=a.first),m(a=h())&&(n._textSuffixContainer=a.first),m(a=h())&&(n._floatingLabel=a.first),m(a=h())&&(n._notchedOutline=a.first),m(a=h())&&(n._lineRipple=a.first)}},hostAttrs:[1,"mat-mdc-form-field"],hostVars:38,hostBindings:function(t,n){t&2&&p("mat-mdc-form-field-label-always-float",n._shouldAlwaysFloat())("mat-mdc-form-field-has-icon-prefix",n._hasIconPrefix)("mat-mdc-form-field-has-icon-suffix",n._hasIconSuffix)("mat-form-field-invalid",n._control.errorState)("mat-form-field-disabled",n._control.disabled)("mat-form-field-autofilled",n._control.autofilled)("mat-form-field-appearance-fill",n.appearance=="fill")("mat-form-field-appearance-outline",n.appearance=="outline")("mat-form-field-hide-placeholder",n._hasFloatingLabel()&&!n._shouldLabelFloat())("mat-primary",n.color!=="accent"&&n.color!=="warn")("mat-accent",n.color==="accent")("mat-warn",n.color==="warn")("ng-untouched",n._shouldForward("untouched"))("ng-touched",n._shouldForward("touched"))("ng-pristine",n._shouldForward("pristine"))("ng-dirty",n._shouldForward("dirty"))("ng-valid",n._shouldForward("valid"))("ng-invalid",n._shouldForward("invalid"))("ng-pending",n._shouldForward("pending"))},inputs:{hideRequiredMarker:"hideRequiredMarker",color:"color",floatLabel:"floatLabel",appearance:"appearance",subscriptSizing:"subscriptSizing",hintLabel:"hintLabel"},exportAs:["matFormField"],features:[U([{provide:rn,useExisting:i},{provide:Gi,useExisting:i}])],ngContentSelectors:lo,decls:18,vars:21,consts:[["labelTemplate",""],["textField",""],["iconPrefixContainer",""],["textPrefixContainer",""],["textSuffixContainer",""],["iconSuffixContainer",""],[1,"mat-mdc-text-field-wrapper","mdc-text-field",3,"click"],[1,"mat-mdc-form-field-focus-overlay"],[1,"mat-mdc-form-field-flex"],["matFormFieldNotchedOutline","",3,"matFormFieldNotchedOutlineOpen"],[1,"mat-mdc-form-field-icon-prefix"],[1,"mat-mdc-form-field-text-prefix"],[1,"mat-mdc-form-field-infix"],[3,"ngTemplateOutlet"],[1,"mat-mdc-form-field-text-suffix"],[1,"mat-mdc-form-field-icon-suffix"],["matFormFieldLineRipple",""],["aria-atomic","true","aria-live","polite",1,"mat-mdc-form-field-subscript-wrapper","mat-mdc-form-field-bottom-align"],[1,"mat-mdc-form-field-error-wrapper"],[1,"mat-mdc-form-field-hint-wrapper"],["matFormFieldFloatingLabel","",3,"floating","monitorResize","id"],["aria-hidden","true",1,"mat-mdc-form-field-required-marker","mdc-floating-label--required"],[3,"id"],[1,"mat-mdc-form-field-hint-spacer"]],template:function(t,n){if(t&1&&(H(so),ge(0,ho,1,1,"ng-template",null,0,zn),c(2,"div",6,1),k("click",function(o){return n._control.onContainerClick(o)}),F(4,uo,1,0,"div",7),c(5,"div",8),F(6,bo,2,2,"div",9),F(7,_o,3,0,"div",10),F(8,go,3,0,"div",11),c(9,"div",12),F(10,yo,1,1,null,13),w(11),u(),F(12,xo,3,0,"div",14),F(13,ko,3,0,"div",15),u(),F(14,Co,1,0,"div",16),u(),c(15,"div",17),F(16,wo,2,0,"div",18)(17,Mo,5,1,"div",19),u()),t&2){let a;d(2),p("mdc-text-field--filled",!n._hasOutline())("mdc-text-field--outlined",n._hasOutline())("mdc-text-field--no-label",!n._hasFloatingLabel())("mdc-text-field--disabled",n._control.disabled)("mdc-text-field--invalid",n._control.errorState),d(2),O(!n._hasOutline()&&!n._control.disabled?4:-1),d(2),O(n._hasOutline()?6:-1),d(),O(n._hasIconPrefix?7:-1),d(),O(n._hasTextPrefix?8:-1),d(2),O(!n._hasOutline()||n._forceDisplayInfixLabel()?10:-1),d(2),O(n._hasTextSuffix?12:-1),d(),O(n._hasIconSuffix?13:-1),d(),O(n._hasOutline()?-1:14),d(),p("mat-mdc-form-field-subscript-dynamic-size",n.subscriptSizing==="dynamic");let o=n._getSubscriptMessageType();d(),O((a=o)==="error"?16:a==="hint"?17:-1)}},dependencies:[Ai,Ni,Vn,Pi,an],styles:[`.mdc-text-field { + display: inline-flex; + align-items: baseline; + padding: 0 16px; + position: relative; + box-sizing: border-box; + overflow: hidden; + will-change: opacity, transform, color; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.mdc-text-field__input { + width: 100%; + min-width: 0; + border: none; + border-radius: 0; + background: none; + padding: 0; + -moz-appearance: none; + -webkit-appearance: none; + height: 28px; +} +.mdc-text-field__input::-webkit-calendar-picker-indicator, .mdc-text-field__input::-webkit-search-cancel-button { + display: none; +} +.mdc-text-field__input::-ms-clear { + display: none; +} +.mdc-text-field__input:focus { + outline: none; +} +.mdc-text-field__input:invalid { + box-shadow: none; +} +.mdc-text-field__input::placeholder { + opacity: 0; +} +.mdc-text-field__input::-moz-placeholder { + opacity: 0; +} +.mdc-text-field__input::-webkit-input-placeholder { + opacity: 0; +} +.mdc-text-field__input:-ms-input-placeholder { + opacity: 0; +} +.mdc-text-field--no-label .mdc-text-field__input::placeholder, .mdc-text-field--focused .mdc-text-field__input::placeholder { + opacity: 1; +} +.mdc-text-field--no-label .mdc-text-field__input::-moz-placeholder, .mdc-text-field--focused .mdc-text-field__input::-moz-placeholder { + opacity: 1; +} +.mdc-text-field--no-label .mdc-text-field__input::-webkit-input-placeholder, .mdc-text-field--focused .mdc-text-field__input::-webkit-input-placeholder { + opacity: 1; +} +.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder, .mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder { + opacity: 1; +} +.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive::placeholder { + opacity: 0; +} +.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive::-moz-placeholder { + opacity: 0; +} +.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive::-webkit-input-placeholder { + opacity: 0; +} +.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive:-ms-input-placeholder { + opacity: 0; +} +.mdc-text-field--outlined .mdc-text-field__input, .mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input { + height: 100%; +} +.mdc-text-field--outlined .mdc-text-field__input { + display: flex; + border: none !important; + background-color: transparent; +} +.mdc-text-field--disabled .mdc-text-field__input { + pointer-events: auto; +} +.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input { + color: var(--mat-form-field-filled-input-text-color, var(--mat-sys-on-surface)); + caret-color: var(--mat-form-field-filled-caret-color, var(--mat-sys-primary)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder { + color: var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input::-moz-placeholder { + color: var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input::-webkit-input-placeholder { + color: var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder { + color: var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input { + color: var(--mat-form-field-outlined-input-text-color, var(--mat-sys-on-surface)); + caret-color: var(--mat-form-field-outlined-caret-color, var(--mat-sys-primary)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder { + color: var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input::-moz-placeholder { + color: var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input::-webkit-input-placeholder { + color: var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder { + color: var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--filled.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__input { + caret-color: var(--mat-form-field-filled-error-caret-color, var(--mat-sys-error)); +} +.mdc-text-field--outlined.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__input { + caret-color: var(--mat-form-field-outlined-error-caret-color, var(--mat-sys-error)); +} +.mdc-text-field--filled.mdc-text-field--disabled .mdc-text-field__input { + color: var(--mat-form-field-filled-disabled-input-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mdc-text-field--outlined.mdc-text-field--disabled .mdc-text-field__input { + color: var(--mat-form-field-outlined-disabled-input-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +@media (forced-colors: active) { + .mdc-text-field--disabled .mdc-text-field__input { + background-color: Window; + } +} + +.mdc-text-field--filled { + height: 56px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + border-top-left-radius: var(--mat-form-field-filled-container-shape, var(--mat-sys-corner-extra-small)); + border-top-right-radius: var(--mat-form-field-filled-container-shape, var(--mat-sys-corner-extra-small)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled) { + background-color: var(--mat-form-field-filled-container-color, var(--mat-sys-surface-variant)); +} +.mdc-text-field--filled.mdc-text-field--disabled { + background-color: var(--mat-form-field-filled-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 4%, transparent)); +} + +.mdc-text-field--outlined { + height: 56px; + overflow: visible; + padding-right: max(16px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))); + padding-left: max(16px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)) + 4px); +} +[dir=rtl] .mdc-text-field--outlined { + padding-right: max(16px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)) + 4px); + padding-left: max(16px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))); +} + +.mdc-floating-label { + position: absolute; + left: 0; + transform-origin: left top; + line-height: 1.15rem; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + cursor: text; + overflow: hidden; + will-change: transform; +} +[dir=rtl] .mdc-floating-label { + right: 0; + left: auto; + transform-origin: right top; + text-align: right; +} +.mdc-text-field .mdc-floating-label { + top: 50%; + transform: translateY(-50%); + pointer-events: none; +} +.mdc-notched-outline .mdc-floating-label { + display: inline-block; + position: relative; + max-width: 100%; +} +.mdc-text-field--outlined .mdc-floating-label { + left: 4px; + right: auto; +} +[dir=rtl] .mdc-text-field--outlined .mdc-floating-label { + left: auto; + right: 4px; +} +.mdc-text-field--filled .mdc-floating-label { + left: 16px; + right: auto; +} +[dir=rtl] .mdc-text-field--filled .mdc-floating-label { + left: auto; + right: 16px; +} +.mdc-text-field--disabled .mdc-floating-label { + cursor: default; +} +@media (forced-colors: active) { + .mdc-text-field--disabled .mdc-floating-label { + z-index: 1; + } +} +.mdc-text-field--filled.mdc-text-field--no-label .mdc-floating-label { + display: none; +} +.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-floating-label { + color: var(--mat-form-field-filled-label-text-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-floating-label { + color: var(--mat-form-field-filled-focus-label-text-color, var(--mat-sys-primary)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-floating-label { + color: var(--mat-form-field-filled-hover-label-text-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--filled.mdc-text-field--disabled .mdc-floating-label { + color: var(--mat-form-field-filled-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid .mdc-floating-label { + color: var(--mat-form-field-filled-error-label-text-color, var(--mat-sys-error)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid.mdc-text-field--focused .mdc-floating-label { + color: var(--mat-form-field-filled-error-focus-label-text-color, var(--mat-sys-error)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-floating-label { + color: var(--mat-form-field-filled-error-hover-label-text-color, var(--mat-sys-on-error-container)); +} +.mdc-text-field--filled .mdc-floating-label { + font-family: var(--mat-form-field-filled-label-text-font, var(--mat-sys-body-large-font)); + font-size: var(--mat-form-field-filled-label-text-size, var(--mat-sys-body-large-size)); + font-weight: var(--mat-form-field-filled-label-text-weight, var(--mat-sys-body-large-weight)); + letter-spacing: var(--mat-form-field-filled-label-text-tracking, var(--mat-sys-body-large-tracking)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-floating-label { + color: var(--mat-form-field-outlined-label-text-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-floating-label { + color: var(--mat-form-field-outlined-focus-label-text-color, var(--mat-sys-primary)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-floating-label { + color: var(--mat-form-field-outlined-hover-label-text-color, var(--mat-sys-on-surface)); +} +.mdc-text-field--outlined.mdc-text-field--disabled .mdc-floating-label { + color: var(--mat-form-field-outlined-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid .mdc-floating-label { + color: var(--mat-form-field-outlined-error-label-text-color, var(--mat-sys-error)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid.mdc-text-field--focused .mdc-floating-label { + color: var(--mat-form-field-outlined-error-focus-label-text-color, var(--mat-sys-error)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-floating-label { + color: var(--mat-form-field-outlined-error-hover-label-text-color, var(--mat-sys-on-error-container)); +} +.mdc-text-field--outlined .mdc-floating-label { + font-family: var(--mat-form-field-outlined-label-text-font, var(--mat-sys-body-large-font)); + font-size: var(--mat-form-field-outlined-label-text-size, var(--mat-sys-body-large-size)); + font-weight: var(--mat-form-field-outlined-label-text-weight, var(--mat-sys-body-large-weight)); + letter-spacing: var(--mat-form-field-outlined-label-text-tracking, var(--mat-sys-body-large-tracking)); +} + +.mdc-floating-label--float-above { + cursor: auto; + transform: translateY(-106%) scale(0.75); +} +.mdc-text-field--filled .mdc-floating-label--float-above { + transform: translateY(-106%) scale(0.75); +} +.mdc-text-field--outlined .mdc-floating-label--float-above { + transform: translateY(-37.25px) scale(1); + font-size: 0.75rem; +} +.mdc-notched-outline .mdc-floating-label--float-above { + text-overflow: clip; +} +.mdc-notched-outline--upgraded .mdc-floating-label--float-above { + max-width: 133.3333333333%; +} +.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above, .mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above { + transform: translateY(-34.75px) scale(0.75); +} +.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above, .mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above { + font-size: 1rem; +} + +.mdc-floating-label--required:not(.mdc-floating-label--hide-required-marker)::after { + margin-left: 1px; + margin-right: 0; + content: "*"; +} +[dir=rtl] .mdc-floating-label--required:not(.mdc-floating-label--hide-required-marker)::after { + margin-left: 0; + margin-right: 1px; +} + +.mdc-notched-outline { + display: flex; + position: absolute; + top: 0; + right: 0; + left: 0; + box-sizing: border-box; + width: 100%; + max-width: 100%; + height: 100%; + text-align: left; + pointer-events: none; +} +[dir=rtl] .mdc-notched-outline { + text-align: right; +} +.mdc-text-field--outlined .mdc-notched-outline { + z-index: 1; +} + +.mat-mdc-notch-piece { + box-sizing: border-box; + height: 100%; + pointer-events: none; + border: none; + border-top: 1px solid; + border-bottom: 1px solid; +} +.mdc-text-field--focused .mat-mdc-notch-piece { + border-width: 2px; +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mat-mdc-notch-piece { + border-color: var(--mat-form-field-outlined-outline-color, var(--mat-sys-outline)); + border-width: var(--mat-form-field-outlined-outline-width, 1px); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mat-mdc-notch-piece { + border-color: var(--mat-form-field-outlined-hover-outline-color, var(--mat-sys-on-surface)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mat-mdc-notch-piece { + border-color: var(--mat-form-field-outlined-focus-outline-color, var(--mat-sys-primary)); +} +.mdc-text-field--outlined.mdc-text-field--disabled .mat-mdc-notch-piece { + border-color: var(--mat-form-field-outlined-disabled-outline-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid .mat-mdc-notch-piece { + border-color: var(--mat-form-field-outlined-error-outline-color, var(--mat-sys-error)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--focused):hover .mdc-notched-outline .mat-mdc-notch-piece { + border-color: var(--mat-form-field-outlined-error-hover-outline-color, var(--mat-sys-on-error-container)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid.mdc-text-field--focused .mat-mdc-notch-piece { + border-color: var(--mat-form-field-outlined-error-focus-outline-color, var(--mat-sys-error)); +} +.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline .mat-mdc-notch-piece { + border-width: var(--mat-form-field-outlined-focus-outline-width, 2px); +} + +.mdc-notched-outline__leading { + border-left: 1px solid; + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); + border-bottom-left-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); +} +.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading { + width: max(12px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))); +} +[dir=rtl] .mdc-notched-outline__leading { + border-left: none; + border-right: 1px solid; + border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-top-right-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); + border-bottom-right-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); +} + +.mdc-notched-outline__trailing { + flex-grow: 1; + border-left: none; + border-right: 1px solid; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); + border-bottom-right-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); +} +[dir=rtl] .mdc-notched-outline__trailing { + border-left: 1px solid; + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); + border-bottom-left-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); +} + +.mdc-notched-outline__notch { + flex: 0 0 auto; + width: auto; +} +.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__notch { + max-width: min(var(--mat-form-field-notch-max-width, 100%), calc(100% - max(12px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))) * 2)); +} +.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch { + max-width: min(100%, calc(100% - max(12px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))) * 2)); +} +.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch { + padding-top: 1px; +} +.mdc-text-field--focused.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch { + padding-top: 2px; +} +.mdc-notched-outline--notched .mdc-notched-outline__notch { + padding-left: 0; + padding-right: 8px; + border-top: none; +} +[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch { + padding-left: 8px; + padding-right: 0; +} +.mdc-notched-outline--no-label .mdc-notched-outline__notch { + display: none; +} + +.mdc-line-ripple::before, .mdc-line-ripple::after { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + border-bottom-style: solid; + content: ""; +} +.mdc-line-ripple::before { + z-index: 1; + border-bottom-width: var(--mat-form-field-filled-active-indicator-height, 1px); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before { + border-bottom-color: var(--mat-form-field-filled-active-indicator-color, var(--mat-sys-on-surface-variant)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-line-ripple::before { + border-bottom-color: var(--mat-form-field-filled-hover-active-indicator-color, var(--mat-sys-on-surface)); +} +.mdc-text-field--filled.mdc-text-field--disabled .mdc-line-ripple::before { + border-bottom-color: var(--mat-form-field-filled-disabled-active-indicator-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid .mdc-line-ripple::before { + border-bottom-color: var(--mat-form-field-filled-error-active-indicator-color, var(--mat-sys-error)); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--focused):hover .mdc-line-ripple::before { + border-bottom-color: var(--mat-form-field-filled-error-hover-active-indicator-color, var(--mat-sys-on-error-container)); +} +.mdc-line-ripple::after { + transform: scaleX(0); + opacity: 0; + z-index: 2; +} +.mdc-text-field--filled .mdc-line-ripple::after { + border-bottom-width: var(--mat-form-field-filled-focus-active-indicator-height, 2px); +} +.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after { + border-bottom-color: var(--mat-form-field-filled-focus-active-indicator-color, var(--mat-sys-primary)); +} +.mdc-text-field--filled.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::after { + border-bottom-color: var(--mat-form-field-filled-error-focus-active-indicator-color, var(--mat-sys-error)); +} + +.mdc-line-ripple--active::after { + transform: scaleX(1); + opacity: 1; +} + +.mdc-line-ripple--deactivating::after { + opacity: 0; +} + +.mdc-text-field--disabled { + pointer-events: none; +} + +.mat-mdc-form-field-textarea-control { + vertical-align: middle; + resize: vertical; + box-sizing: border-box; + height: auto; + margin: 0; + padding: 0; + border: none; + overflow: auto; +} + +.mat-mdc-form-field-input-control.mat-mdc-form-field-input-control { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font: inherit; + letter-spacing: inherit; + text-decoration: inherit; + text-transform: inherit; + border: none; +} + +.mat-mdc-form-field .mat-mdc-floating-label.mdc-floating-label { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + line-height: normal; + pointer-events: all; + will-change: auto; +} + +.mat-mdc-form-field:not(.mat-form-field-disabled) .mat-mdc-floating-label.mdc-floating-label { + cursor: inherit; +} + +.mdc-text-field--no-label:not(.mdc-text-field--textarea) .mat-mdc-form-field-input-control.mdc-text-field__input, +.mat-mdc-text-field-wrapper .mat-mdc-form-field-input-control { + height: auto; +} + +.mat-mdc-text-field-wrapper .mat-mdc-form-field-input-control.mdc-text-field__input[type=color] { + height: 23px; +} + +.mat-mdc-text-field-wrapper { + height: auto; + flex: auto; + will-change: auto; +} + +.mat-mdc-form-field-has-icon-prefix .mat-mdc-text-field-wrapper { + padding-left: 0; + --mat-mdc-form-field-label-offset-x: -16px; +} + +.mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { + padding-right: 0; +} + +[dir=rtl] .mat-mdc-text-field-wrapper { + padding-left: 16px; + padding-right: 16px; +} +[dir=rtl] .mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { + padding-left: 0; +} +[dir=rtl] .mat-mdc-form-field-has-icon-prefix .mat-mdc-text-field-wrapper { + padding-right: 0; +} + +.mat-form-field-disabled .mdc-text-field__input::placeholder { + color: var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-form-field-disabled .mdc-text-field__input::-moz-placeholder { + color: var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-form-field-disabled .mdc-text-field__input::-webkit-input-placeholder { + color: var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-form-field-disabled .mdc-text-field__input:-ms-input-placeholder { + color: var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} + +.mat-mdc-form-field-label-always-float .mdc-text-field__input::placeholder { + transition-delay: 40ms; + transition-duration: 110ms; + opacity: 1; +} + +.mat-mdc-text-field-wrapper .mat-mdc-form-field-infix .mat-mdc-floating-label { + left: auto; + right: auto; +} + +.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-text-field__input { + display: inline-block; +} + +.mat-mdc-form-field .mat-mdc-text-field-wrapper.mdc-text-field .mdc-notched-outline__notch { + padding-top: 0; +} + +.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field .mdc-notched-outline__notch { + border-left: 1px solid transparent; +} + +[dir=rtl] .mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field .mdc-notched-outline__notch { + border-left: none; + border-right: 1px solid transparent; +} + +.mat-mdc-form-field-infix { + min-height: var(--mat-form-field-container-height, 56px); + padding-top: var(--mat-form-field-filled-with-label-container-padding-top, 24px); + padding-bottom: var(--mat-form-field-filled-with-label-container-padding-bottom, 8px); +} +.mdc-text-field--outlined .mat-mdc-form-field-infix, .mdc-text-field--no-label .mat-mdc-form-field-infix { + padding-top: var(--mat-form-field-container-vertical-padding, 16px); + padding-bottom: var(--mat-form-field-container-vertical-padding, 16px); +} + +.mat-mdc-text-field-wrapper .mat-mdc-form-field-flex .mat-mdc-floating-label { + top: calc(var(--mat-form-field-container-height, 56px) / 2); +} + +.mdc-text-field--filled .mat-mdc-floating-label { + display: var(--mat-form-field-filled-label-display, block); +} + +.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above { + --mat-mdc-form-field-label-transform: translateY(calc(calc(6.75px + var(--mat-form-field-container-height, 56px) / 2) * -1)) + scale(var(--mat-mdc-form-field-floating-label-scale, 0.75)); + transform: var(--mat-mdc-form-field-label-transform); +} + +@keyframes _mat-form-field-subscript-animation { + from { + opacity: 0; + transform: translateY(-5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +.mat-mdc-form-field-subscript-wrapper { + box-sizing: border-box; + width: 100%; + position: relative; +} + +.mat-mdc-form-field-hint-wrapper, +.mat-mdc-form-field-error-wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 0 16px; + opacity: 1; + transform: translateY(0); + animation: _mat-form-field-subscript-animation 0ms cubic-bezier(0.55, 0, 0.55, 0.2); +} + +.mat-mdc-form-field-subscript-dynamic-size .mat-mdc-form-field-hint-wrapper, +.mat-mdc-form-field-subscript-dynamic-size .mat-mdc-form-field-error-wrapper { + position: static; +} + +.mat-mdc-form-field-bottom-align::before { + content: ""; + display: inline-block; + height: 16px; +} + +.mat-mdc-form-field-bottom-align.mat-mdc-form-field-subscript-dynamic-size::before { + content: unset; +} + +.mat-mdc-form-field-hint-end { + order: 1; +} + +.mat-mdc-form-field-hint-wrapper { + display: flex; +} + +.mat-mdc-form-field-hint-spacer { + flex: 1 0 1em; +} + +.mat-mdc-form-field-error { + display: block; + color: var(--mat-form-field-error-text-color, var(--mat-sys-error)); +} + +.mat-mdc-form-field-subscript-wrapper, +.mat-mdc-form-field-bottom-align::before { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-family: var(--mat-form-field-subscript-text-font, var(--mat-sys-body-small-font)); + line-height: var(--mat-form-field-subscript-text-line-height, var(--mat-sys-body-small-line-height)); + font-size: var(--mat-form-field-subscript-text-size, var(--mat-sys-body-small-size)); + letter-spacing: var(--mat-form-field-subscript-text-tracking, var(--mat-sys-body-small-tracking)); + font-weight: var(--mat-form-field-subscript-text-weight, var(--mat-sys-body-small-weight)); +} + +.mat-mdc-form-field-focus-overlay { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + opacity: 0; + pointer-events: none; + background-color: var(--mat-form-field-state-layer-color, var(--mat-sys-on-surface)); +} +.mat-mdc-text-field-wrapper:hover .mat-mdc-form-field-focus-overlay { + opacity: var(--mat-form-field-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); +} +.mat-mdc-form-field.mat-focused .mat-mdc-form-field-focus-overlay { + opacity: var(--mat-form-field-focus-state-layer-opacity, 0); +} + +select.mat-mdc-form-field-input-control { + -moz-appearance: none; + -webkit-appearance: none; + background-color: transparent; + display: inline-flex; + box-sizing: border-box; +} +select.mat-mdc-form-field-input-control:not(:disabled) { + cursor: pointer; +} +select.mat-mdc-form-field-input-control:not(.mat-mdc-native-select-inline) option { + color: var(--mat-form-field-select-option-text-color, var(--mat-sys-neutral10)); +} +select.mat-mdc-form-field-input-control:not(.mat-mdc-native-select-inline) option:disabled { + color: var(--mat-form-field-select-disabled-option-text-color, color-mix(in srgb, var(--mat-sys-neutral10) 38%, transparent)); +} + +.mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-infix::after { + content: ""; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid; + position: absolute; + right: 0; + top: 50%; + margin-top: -2.5px; + pointer-events: none; + color: var(--mat-form-field-enabled-select-arrow-color, var(--mat-sys-on-surface-variant)); +} +[dir=rtl] .mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-infix::after { + right: auto; + left: 0; +} +.mat-mdc-form-field-type-mat-native-select.mat-focused .mat-mdc-form-field-infix::after { + color: var(--mat-form-field-focus-select-arrow-color, var(--mat-sys-primary)); +} +.mat-mdc-form-field-type-mat-native-select.mat-form-field-disabled .mat-mdc-form-field-infix::after { + color: var(--mat-form-field-disabled-select-arrow-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-input-control { + padding-right: 15px; +} +[dir=rtl] .mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-input-control { + padding-right: 0; + padding-left: 15px; +} + +@media (forced-colors: active) { + .mat-form-field-appearance-fill .mat-mdc-text-field-wrapper { + outline: solid 1px; + } +} +@media (forced-colors: active) { + .mat-form-field-appearance-fill.mat-form-field-disabled .mat-mdc-text-field-wrapper { + outline-color: GrayText; + } +} + +@media (forced-colors: active) { + .mat-form-field-appearance-fill.mat-focused .mat-mdc-text-field-wrapper { + outline: dashed 3px; + } +} + +@media (forced-colors: active) { + .mat-mdc-form-field.mat-focused .mdc-notched-outline { + border: dashed 3px; + } +} + +.mat-mdc-form-field-input-control[type=date], .mat-mdc-form-field-input-control[type=datetime], .mat-mdc-form-field-input-control[type=datetime-local], .mat-mdc-form-field-input-control[type=month], .mat-mdc-form-field-input-control[type=week], .mat-mdc-form-field-input-control[type=time] { + line-height: 1; +} +.mat-mdc-form-field-input-control::-webkit-datetime-edit { + line-height: 1; + padding: 0; + margin-bottom: -2px; +} + +.mat-mdc-form-field { + --mat-mdc-form-field-floating-label-scale: 0.75; + display: inline-flex; + flex-direction: column; + min-width: 0; + text-align: left; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-family: var(--mat-form-field-container-text-font, var(--mat-sys-body-large-font)); + line-height: var(--mat-form-field-container-text-line-height, var(--mat-sys-body-large-line-height)); + font-size: var(--mat-form-field-container-text-size, var(--mat-sys-body-large-size)); + letter-spacing: var(--mat-form-field-container-text-tracking, var(--mat-sys-body-large-tracking)); + font-weight: var(--mat-form-field-container-text-weight, var(--mat-sys-body-large-weight)); +} +.mat-mdc-form-field .mdc-text-field--outlined .mdc-floating-label--float-above { + font-size: calc(var(--mat-form-field-outlined-label-text-populated-size) * var(--mat-mdc-form-field-floating-label-scale)); +} +.mat-mdc-form-field .mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above { + font-size: var(--mat-form-field-outlined-label-text-populated-size); +} +[dir=rtl] .mat-mdc-form-field { + text-align: right; +} + +.mat-mdc-form-field-flex { + display: inline-flex; + align-items: baseline; + box-sizing: border-box; + width: 100%; +} + +.mat-mdc-text-field-wrapper { + width: 100%; + z-index: 0; +} + +.mat-mdc-form-field-icon-prefix, +.mat-mdc-form-field-icon-suffix { + align-self: center; + line-height: 0; + pointer-events: auto; + position: relative; + z-index: 1; +} +.mat-mdc-form-field-icon-prefix > .mat-icon, +.mat-mdc-form-field-icon-suffix > .mat-icon { + padding: 0 12px; + box-sizing: content-box; +} + +.mat-mdc-form-field-icon-prefix { + color: var(--mat-form-field-leading-icon-color, var(--mat-sys-on-surface-variant)); +} +.mat-form-field-disabled .mat-mdc-form-field-icon-prefix { + color: var(--mat-form-field-disabled-leading-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} + +.mat-mdc-form-field-icon-suffix { + color: var(--mat-form-field-trailing-icon-color, var(--mat-sys-on-surface-variant)); +} +.mat-form-field-disabled .mat-mdc-form-field-icon-suffix { + color: var(--mat-form-field-disabled-trailing-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-form-field-invalid .mat-mdc-form-field-icon-suffix { + color: var(--mat-form-field-error-trailing-icon-color, var(--mat-sys-error)); +} +.mat-form-field-invalid:not(.mat-focused):not(.mat-form-field-disabled) .mat-mdc-text-field-wrapper:hover .mat-mdc-form-field-icon-suffix { + color: var(--mat-form-field-error-hover-trailing-icon-color, var(--mat-sys-on-error-container)); +} +.mat-form-field-invalid.mat-focused .mat-mdc-text-field-wrapper .mat-mdc-form-field-icon-suffix { + color: var(--mat-form-field-error-focus-trailing-icon-color, var(--mat-sys-error)); +} + +.mat-mdc-form-field-icon-prefix, +[dir=rtl] .mat-mdc-form-field-icon-suffix { + padding: 0 4px 0 0; +} + +.mat-mdc-form-field-icon-suffix, +[dir=rtl] .mat-mdc-form-field-icon-prefix { + padding: 0 0 0 4px; +} + +.mat-mdc-form-field-subscript-wrapper .mat-icon, +.mat-mdc-form-field label .mat-icon { + width: 1em; + height: 1em; + font-size: inherit; +} + +.mat-mdc-form-field-infix { + flex: auto; + min-width: 0; + width: 180px; + position: relative; + box-sizing: border-box; +} +.mat-mdc-form-field-infix:has(textarea[cols]) { + width: auto; +} + +.mat-mdc-form-field .mdc-notched-outline__notch { + margin-left: -1px; + -webkit-clip-path: inset(-9em -999em -9em 1px); + clip-path: inset(-9em -999em -9em 1px); +} +[dir=rtl] .mat-mdc-form-field .mdc-notched-outline__notch { + margin-left: 0; + margin-right: -1px; + -webkit-clip-path: inset(-9em 1px -9em -999em); + clip-path: inset(-9em 1px -9em -999em); +} + +.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-floating-label { + transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1), color 150ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input { + transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input::placeholder { + transition: opacity 67ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input::-moz-placeholder { + transition: opacity 67ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input::-webkit-input-placeholder { + transition: opacity 67ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input:-ms-input-placeholder { + transition: opacity 67ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input::placeholder, .mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input::placeholder { + transition-delay: 40ms; + transition-duration: 110ms; +} +.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input::-moz-placeholder, .mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input::-moz-placeholder { + transition-delay: 40ms; + transition-duration: 110ms; +} +.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input::-webkit-input-placeholder, .mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input::-webkit-input-placeholder { + transition-delay: 40ms; + transition-duration: 110ms; +} +.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder, .mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder { + transition-delay: 40ms; + transition-duration: 110ms; +} +.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field--filled:not(.mdc-ripple-upgraded):focus .mdc-text-field__ripple::before { + transition-duration: 75ms; +} +.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-line-ripple::after { + transition: transform 180ms cubic-bezier(0.4, 0, 0.2, 1), opacity 180ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-mdc-form-field.mat-form-field-animations-enabled .mat-mdc-form-field-hint-wrapper, +.mat-mdc-form-field.mat-form-field-animations-enabled .mat-mdc-form-field-error-wrapper { + animation-duration: 300ms; +} + +.mdc-notched-outline .mdc-floating-label { + max-width: calc(100% + 1px); +} + +.mdc-notched-outline--upgraded .mdc-floating-label--float-above { + max-width: calc(133.3333333333% + 1px); +} +`],encapsulation:2,changeDetection:0})}return i})();var Yi=(()=>{class i{isErrorState(e,t){return!!(e&&e.invalid&&(e.touched||t&&t.submitted))}static \u0275fac=function(t){return new(t||i)};static \u0275prov=P({token:i,factory:i.\u0275fac,providedIn:"root"})}return i})();var At=class{_defaultMatcher;ngControl;_parentFormGroup;_parentForm;_stateChanges;errorState=!1;matcher;constructor(s,e,t,n,a){this._defaultMatcher=s,this.ngControl=e,this._parentFormGroup=t,this._parentForm=n,this._stateChanges=a}updateErrorState(){let s=this.errorState,e=this._parentFormGroup||this._parentForm,t=this.matcher||this._defaultMatcher,n=this.ngControl?this.ngControl.control:null,a=t?.isErrorState(n,e)??!1;a!==s&&(this.errorState=a,this._stateChanges.next())}};var $i=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[di,Wi,j]})}return i})();var Lo=["trigger"],Po=["panel"],No=[[["mat-select-trigger"]],"*"],Bo=["mat-select-trigger","*"];function zo(i,s){if(i&1&&(c(0,"span",4),$(1),u()),i&2){let e=y();d(),se(e.placeholder)}}function Ho(i,s){i&1&&w(0)}function jo(i,s){if(i&1&&(c(0,"span",11),$(1),u()),i&2){let e=y(2);d(),se(e.triggerValue)}}function Vo(i,s){if(i&1&&(c(0,"span",5),F(1,Ho,1,0)(2,jo,2,1,"span",11),u()),i&2){let e=y();d(),O(e.customTrigger?1:2)}}function Go(i,s){if(i&1){let e=Se();c(0,"div",12,1),k("keydown",function(n){ne(e);let a=y();return ie(a._handleKeydown(n))}),w(2,1),u()}if(i&2){let e=y();de(e.panelClass),p("mat-select-panel-animations-enabled",!e._animationsDisabled)("mat-primary",(e._parentFormField==null?null:e._parentFormField.color)==="primary")("mat-accent",(e._parentFormField==null?null:e._parentFormField.color)==="accent")("mat-warn",(e._parentFormField==null?null:e._parentFormField.color)==="warn")("mat-undefined",!(e._parentFormField!=null&&e._parentFormField.color)),T("id",e.id+"-panel")("aria-multiselectable",e.multiple)("aria-label",e.ariaLabel||null)("aria-labelledby",e._getPanelAriaLabelledby())}}var Wo=new C("mat-select-scroll-strategy",{providedIn:"root",factory:()=>{let i=r(X);return()=>Je(i)}}),Yo=new C("MAT_SELECT_CONFIG"),Ui=new C("MatSelectTrigger"),sn=class{source;value;constructor(s,e){this.source=s,this.value=e}},Cc=(()=>{class i{_viewportRuler=r(lt);_changeDetectorRef=r(Y);_elementRef=r(D);_dir=r(xe,{optional:!0});_idGenerator=r(K);_renderer=r(ae);_parentFormField=r(rn,{optional:!0});ngControl=r(ii,{self:!0,optional:!0});_liveAnnouncer=r(vt);_defaultOptions=r(Yo,{optional:!0});_animationsDisabled=q();_popoverLocation;_initialized=new f;_cleanupDetach;options;optionGroups;customTrigger;_positions=[{originX:"start",originY:"bottom",overlayX:"start",overlayY:"top"},{originX:"end",originY:"bottom",overlayX:"end",overlayY:"top"},{originX:"start",originY:"top",overlayX:"start",overlayY:"bottom",panelClass:"mat-mdc-select-panel-above"},{originX:"end",originY:"top",overlayX:"end",overlayY:"bottom",panelClass:"mat-mdc-select-panel-above"}];_scrollOptionIntoView(e){let t=this.options.toArray()[e];if(t){let n=this.panel.nativeElement,a=Ii(e,this.options,this.optionGroups),o=t._getHostElement();e===0&&a===1?n.scrollTop=0:n.scrollTop=Si(o.offsetTop,o.offsetHeight,n.scrollTop,n.offsetHeight)}}_positioningSettled(){this._scrollOptionIntoView(this._keyManager.activeItemIndex||0)}_getChangeEvent(e){return new sn(this,e)}_scrollStrategyFactory=r(Wo);_panelOpen=!1;_compareWith=(e,t)=>e===t;_uid=this._idGenerator.getId("mat-select-");_triggerAriaLabelledBy=null;_previousControl;_destroy=new f;_errorStateTracker;stateChanges=new f;disableAutomaticLabeling=!0;userAriaDescribedBy;_selectionModel;_keyManager;_preferredOverlayOrigin;_overlayWidth;_onChange=()=>{};_onTouched=()=>{};_valueId=this._idGenerator.getId("mat-select-value-");_scrollStrategy;_overlayPanelClass=this._defaultOptions?.overlayPanelClass||"";get focused(){return this._focused||this._panelOpen}_focused=!1;controlType="mat-select";trigger;panel;_overlayDir;panelClass;disabled=!1;get disableRipple(){return this._disableRipple()}set disableRipple(e){this._disableRipple.set(e)}_disableRipple=Q(!1);tabIndex=0;get hideSingleSelectionIndicator(){return this._hideSingleSelectionIndicator}set hideSingleSelectionIndicator(e){this._hideSingleSelectionIndicator=e,this._syncParentProperties()}_hideSingleSelectionIndicator=this._defaultOptions?.hideSingleSelectionIndicator??!1;get placeholder(){return this._placeholder}set placeholder(e){this._placeholder=e,this.stateChanges.next()}_placeholder;get required(){return this._required??this.ngControl?.control?.hasValidator(ni.required)??!1}set required(e){this._required=e,this.stateChanges.next()}_required;get multiple(){return this._multiple}set multiple(e){this._selectionModel,this._multiple=e}_multiple=!1;disableOptionCentering=this._defaultOptions?.disableOptionCentering??!1;get compareWith(){return this._compareWith}set compareWith(e){this._compareWith=e,this._selectionModel&&this._initializeSelection()}get value(){return this._value}set value(e){this._assignValue(e)&&this._onChange(e)}_value;ariaLabel="";ariaLabelledby;get errorStateMatcher(){return this._errorStateTracker.matcher}set errorStateMatcher(e){this._errorStateTracker.matcher=e}typeaheadDebounceInterval;sortComparator;get id(){return this._id}set id(e){this._id=e||this._uid,this.stateChanges.next()}_id;get errorState(){return this._errorStateTracker.errorState}set errorState(e){this._errorStateTracker.errorState=e}panelWidth=this._defaultOptions&&typeof this._defaultOptions.panelWidth<"u"?this._defaultOptions.panelWidth:"auto";canSelectNullableOptions=this._defaultOptions?.canSelectNullableOptions??!1;optionSelectionChanges=xn(()=>{let e=this.options;return e?e.changes.pipe(te(e),Ae(()=>G(...e.map(t=>t.onSelectionChange)))):this._initialized.pipe(Ae(()=>this.optionSelectionChanges))});openedChange=new g;_openedStream=this.openedChange.pipe(Z(e=>e),pe(()=>{}));_closedStream=this.openedChange.pipe(Z(e=>!e),pe(()=>{}));selectionChange=new g;valueChange=new g;constructor(){let e=r(Yi),t=r(ai,{optional:!0}),n=r(oi,{optional:!0}),a=r(new Ke("tabindex"),{optional:!0}),o=r(Jn,{optional:!0});this.ngControl&&(this.ngControl.valueAccessor=this),this._defaultOptions?.typeaheadDebounceInterval!=null&&(this.typeaheadDebounceInterval=this._defaultOptions.typeaheadDebounceInterval),this._errorStateTracker=new At(e,this.ngControl,n,t,this.stateChanges),this._scrollStrategy=this._scrollStrategyFactory(),this.tabIndex=a==null?0:parseInt(a)||0,this._popoverLocation=o?.usePopover===!1?null:"inline",this.id=this.id}ngOnInit(){this._selectionModel=new ki(this.multiple),this.stateChanges.next(),this._viewportRuler.change().pipe(x(this._destroy)).subscribe(()=>{this.panelOpen&&(this._overlayWidth=this._getOverlayWidth(this._preferredOverlayOrigin),this._changeDetectorRef.detectChanges())})}ngAfterContentInit(){this._initialized.next(),this._initialized.complete(),this._initKeyManager(),this._selectionModel.changed.pipe(x(this._destroy)).subscribe(e=>{e.added.forEach(t=>t.select()),e.removed.forEach(t=>t.deselect())}),this.options.changes.pipe(te(null),x(this._destroy)).subscribe(()=>{this._resetOptions(),this._initializeSelection()})}ngDoCheck(){let e=this._getTriggerAriaLabelledby(),t=this.ngControl;if(e!==this._triggerAriaLabelledBy){let n=this._elementRef.nativeElement;this._triggerAriaLabelledBy=e,e?n.setAttribute("aria-labelledby",e):n.removeAttribute("aria-labelledby")}t&&(this._previousControl!==t.control&&(this._previousControl!==void 0&&t.disabled!==null&&t.disabled!==this.disabled&&(this.disabled=t.disabled),this._previousControl=t.control),this.updateErrorState())}ngOnChanges(e){(e.disabled||e.userAriaDescribedBy)&&this.stateChanges.next(),e.typeaheadDebounceInterval&&this._keyManager&&this._keyManager.withTypeAhead(this.typeaheadDebounceInterval),e.panelClass&&this.panelClass instanceof Set&&(this.panelClass=Array.from(this.panelClass))}ngOnDestroy(){this._cleanupDetach?.(),this._keyManager?.destroy(),this._destroy.next(),this._destroy.complete(),this.stateChanges.complete(),this._clearFromModal()}toggle(){this.panelOpen?this.close():this.open()}open(){this._canOpen()&&(this._parentFormField&&(this._preferredOverlayOrigin=this._parentFormField.getConnectedOverlayOrigin()),this._cleanupDetach?.(),this._overlayWidth=this._getOverlayWidth(this._preferredOverlayOrigin),this._applyModalPanelOwnership(),this._panelOpen=!0,this._overlayDir.positionChange.pipe($e(1)).subscribe(()=>{this._changeDetectorRef.detectChanges(),this._positioningSettled()}),this._overlayDir.attachOverlay(),this._keyManager.withHorizontalOrientation(null),this._highlightCorrectOption(),this._changeDetectorRef.markForCheck(),this.stateChanges.next(),Promise.resolve().then(()=>this.openedChange.emit(!0)))}_trackedModal=null;_applyModalPanelOwnership(){let e=this._elementRef.nativeElement.closest('body > .cdk-overlay-container [aria-modal="true"]');if(!e)return;let t=`${this.id}-panel`;this._trackedModal&&Qt(this._trackedModal,"aria-owns",t),ui(e,"aria-owns",t),this._trackedModal=e}_clearFromModal(){if(!this._trackedModal)return;let e=`${this.id}-panel`;Qt(this._trackedModal,"aria-owns",e),this._trackedModal=null}close(){this._panelOpen&&(this._panelOpen=!1,this._exitAndDetach(),this._keyManager.withHorizontalOrientation(this._isRtl()?"rtl":"ltr"),this._changeDetectorRef.markForCheck(),this._onTouched(),this.stateChanges.next(),Promise.resolve().then(()=>this.openedChange.emit(!1)))}_exitAndDetach(){if(this._animationsDisabled||!this.panel){this._detachOverlay();return}this._cleanupDetach?.(),this._cleanupDetach=()=>{t(),clearTimeout(n),this._cleanupDetach=void 0};let e=this.panel.nativeElement,t=this._renderer.listen(e,"animationend",a=>{a.animationName==="_mat-select-exit"&&(this._cleanupDetach?.(),this._detachOverlay())}),n=setTimeout(()=>{this._cleanupDetach?.(),this._detachOverlay()},200);e.classList.add("mat-select-panel-exit")}_detachOverlay(){this._overlayDir.detachOverlay(),this._changeDetectorRef.markForCheck()}writeValue(e){this._assignValue(e)}registerOnChange(e){this._onChange=e}registerOnTouched(e){this._onTouched=e}setDisabledState(e){this.disabled=e,this._changeDetectorRef.markForCheck(),this.stateChanges.next()}get panelOpen(){return this._panelOpen}get selected(){return this.multiple?this._selectionModel?.selected||[]:this._selectionModel?.selected[0]}get triggerValue(){if(this.empty)return"";if(this._multiple){let e=this._selectionModel.selected.map(t=>t.viewValue);return this._isRtl()&&e.reverse(),e.join(", ")}return this._selectionModel.selected[0].viewValue}updateErrorState(){this._errorStateTracker.updateErrorState()}_isRtl(){return this._dir?this._dir.value==="rtl":!1}_handleKeydown(e){this.disabled||(this.panelOpen?this._handleOpenKeydown(e):this._handleClosedKeydown(e))}_handleClosedKeydown(e){let t=e.keyCode,n=t===40||t===38||t===37||t===39,a=t===13||t===32,o=this._keyManager;if(!o.isTyping()&&a&&!me(e)||(this.multiple||e.altKey)&&n)e.preventDefault(),this.open();else if(!this.multiple){let l=this.selected;o.onKeydown(e);let v=this.selected;v&&l!==v&&this._liveAnnouncer.announce(v.viewValue,1e4)}}_handleOpenKeydown(e){let t=this._keyManager,n=e.keyCode,a=n===40||n===38,o=t.isTyping();if(a&&e.altKey)e.preventDefault(),this.close();else if(!o&&(n===13||n===32)&&t.activeItem&&!me(e))e.preventDefault(),t.activeItem._selectViaInteraction();else if(!o&&this._multiple&&n===65&&e.ctrlKey){e.preventDefault();let l=this.options.some(v=>!v.disabled&&!v.selected);this.options.forEach(v=>{v.disabled||(l?v.select():v.deselect())})}else{let l=t.activeItemIndex;t.onKeydown(e),this._multiple&&a&&e.shiftKey&&t.activeItem&&t.activeItemIndex!==l&&t.activeItem._selectViaInteraction()}}_handleOverlayKeydown(e){e.keyCode===27&&!me(e)&&(e.preventDefault(),this.close())}_onFocus(){this.disabled||(this._focused=!0,this.stateChanges.next())}_onBlur(){this._focused=!1,this._keyManager?.cancelTypeahead(),!this.disabled&&!this.panelOpen&&(this._onTouched(),this._changeDetectorRef.markForCheck(),this.stateChanges.next())}get empty(){return!this._selectionModel||this._selectionModel.isEmpty()}_initializeSelection(){Promise.resolve().then(()=>{this.ngControl&&(this._value=this.ngControl.value),this._setSelectionByValue(this._value),this.stateChanges.next()})}_setSelectionByValue(e){if(this.options.forEach(t=>t.setInactiveStyles()),this._selectionModel.clear(),this.multiple&&e)Array.isArray(e),e.forEach(t=>this._selectOptionByValue(t)),this._sortValues();else{let t=this._selectOptionByValue(e);t?this._keyManager.updateActiveItem(t):this.panelOpen||this._keyManager.updateActiveItem(-1)}this._changeDetectorRef.markForCheck()}_selectOptionByValue(e){let t=this.options.find(n=>{if(this._selectionModel.isSelected(n))return!1;try{return(n.value!=null||this.canSelectNullableOptions)&&this._compareWith(n.value,e)}catch{return!1}});return t&&this._selectionModel.select(t),t}_assignValue(e){return e!==this._value||this._multiple&&Array.isArray(e)?(this.options&&this._setSelectionByValue(e),this._value=e,!0):!1}_skipPredicate=e=>this.panelOpen?!1:e.disabled;_getOverlayWidth(e){return this.panelWidth==="auto"?(e instanceof $t?e.elementRef:e||this._elementRef).nativeElement.getBoundingClientRect().width:this.panelWidth===null?"":this.panelWidth}_syncParentProperties(){if(this.options)for(let e of this.options)e._changeDetectorRef.markForCheck()}_initKeyManager(){this._keyManager=new hi(this.options).withTypeAhead(this.typeaheadDebounceInterval).withVerticalOrientation().withHorizontalOrientation(this._isRtl()?"rtl":"ltr").withHomeAndEnd().withPageUpDown().withAllowedModifierKeys(["shiftKey"]).skipPredicate(this._skipPredicate),this._keyManager.tabOut.subscribe(()=>{this.panelOpen&&(!this.multiple&&this._keyManager.activeItem&&this._keyManager.activeItem._selectViaInteraction(),this.focus(),this.close())}),this._keyManager.change.subscribe(()=>{this._panelOpen&&this.panel?this._scrollOptionIntoView(this._keyManager.activeItemIndex||0):!this._panelOpen&&!this.multiple&&this._keyManager.activeItem&&this._keyManager.activeItem._selectViaInteraction()})}_resetOptions(){let e=G(this.options.changes,this._destroy);this.optionSelectionChanges.pipe(x(e)).subscribe(t=>{this._onSelect(t.source,t.isUserInput),t.isUserInput&&!this.multiple&&this._panelOpen&&(this.close(),this.focus())}),G(...this.options.map(t=>t._stateChanges)).pipe(x(e)).subscribe(()=>{this._changeDetectorRef.detectChanges(),this.stateChanges.next()})}_onSelect(e,t){let n=this._selectionModel.isSelected(e);!this.canSelectNullableOptions&&e.value==null&&!this._multiple?(e.deselect(),this._selectionModel.clear(),this.value!=null&&this._propagateChanges(e.value)):(n!==e.selected&&(e.selected?this._selectionModel.select(e):this._selectionModel.deselect(e)),t&&this._keyManager.setActiveItem(e),this.multiple&&(this._sortValues(),t&&this.focus())),n!==this._selectionModel.isSelected(e)&&this._propagateChanges(),this.stateChanges.next()}_sortValues(){if(this.multiple){let e=this.options.toArray();this._selectionModel.sort((t,n)=>this.sortComparator?this.sortComparator(t,n,e):e.indexOf(t)-e.indexOf(n)),this.stateChanges.next()}}_propagateChanges(e){let t;this.multiple?t=this.selected.map(n=>n.value):t=this.selected?this.selected.value:e,this._value=t,this.valueChange.emit(t),this._onChange(t),this.selectionChange.emit(this._getChangeEvent(t)),this._changeDetectorRef.markForCheck()}_highlightCorrectOption(){if(this._keyManager)if(this.empty){let e=-1;for(let t=0;t0&&!!this._overlayDir}focus(e){this._elementRef.nativeElement.focus(e)}_getPanelAriaLabelledby(){if(this.ariaLabel)return null;let e=this._parentFormField?.getLabelId()||null,t=e?e+" ":"";return this.ariaLabelledby?t+this.ariaLabelledby:e}_getAriaActiveDescendant(){return this.panelOpen&&this._keyManager&&this._keyManager.activeItem?this._keyManager.activeItem.id:null}_getTriggerAriaLabelledby(){if(this.ariaLabel)return null;let e=this._parentFormField?.getLabelId()||"";return this.ariaLabelledby&&(e+=" "+this.ariaLabelledby),e||(e=this._valueId),e}get describedByIds(){return this._elementRef.nativeElement.getAttribute("aria-describedby")?.split(" ")||[]}setDescribedByIds(e){let t=this._elementRef.nativeElement;e.length?t.setAttribute("aria-describedby",e.join(" ")):t.removeAttribute("aria-describedby")}onContainerClick(e){let t=Wn(e);t&&(t.tagName==="MAT-OPTION"||t.classList.contains("cdk-overlay-backdrop")||t.closest(".mat-mdc-select-panel"))||(this.focus(),this.open())}get shouldLabelFloat(){return this.panelOpen||!this.empty||this.focused&&!!this.placeholder}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-select"]],contentQueries:function(t,n,a){if(t&1&&ve(a,Ui,5)(a,Et,5)(a,Dt,5),t&2){let o;m(o=h())&&(n.customTrigger=o.first),m(o=h())&&(n.options=o),m(o=h())&&(n.optionGroups=o)}},viewQuery:function(t,n){if(t&1&&W(Lo,5)(Po,5)(Ut,5),t&2){let a;m(a=h())&&(n.trigger=a.first),m(a=h())&&(n.panel=a.first),m(a=h())&&(n._overlayDir=a.first)}},hostAttrs:["role","combobox","aria-haspopup","listbox",1,"mat-mdc-select"],hostVars:21,hostBindings:function(t,n){t&1&&k("keydown",function(o){return n._handleKeydown(o)})("focus",function(){return n._onFocus()})("blur",function(){return n._onBlur()}),t&2&&(T("id",n.id)("tabindex",n.disabled?-1:n.tabIndex)("aria-controls",n.panelOpen?n.id+"-panel":null)("aria-expanded",n.panelOpen)("aria-label",n.ariaLabel||null)("aria-required",n.required.toString())("aria-disabled",n.disabled.toString())("aria-invalid",n.errorState)("aria-activedescendant",n._getAriaActiveDescendant()),p("mat-mdc-select-disabled",n.disabled)("mat-mdc-select-invalid",n.errorState)("mat-mdc-select-required",n.required)("mat-mdc-select-empty",n.empty)("mat-mdc-select-multiple",n.multiple)("mat-select-open",n.panelOpen))},inputs:{userAriaDescribedBy:[0,"aria-describedby","userAriaDescribedBy"],panelClass:"panelClass",disabled:[2,"disabled","disabled",_],disableRipple:[2,"disableRipple","disableRipple",_],tabIndex:[2,"tabIndex","tabIndex",e=>e==null?0:Te(e)],hideSingleSelectionIndicator:[2,"hideSingleSelectionIndicator","hideSingleSelectionIndicator",_],placeholder:"placeholder",required:[2,"required","required",_],multiple:[2,"multiple","multiple",_],disableOptionCentering:[2,"disableOptionCentering","disableOptionCentering",_],compareWith:"compareWith",value:"value",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],errorStateMatcher:"errorStateMatcher",typeaheadDebounceInterval:[2,"typeaheadDebounceInterval","typeaheadDebounceInterval",Te],sortComparator:"sortComparator",id:"id",panelWidth:"panelWidth",canSelectNullableOptions:[2,"canSelectNullableOptions","canSelectNullableOptions",_]},outputs:{openedChange:"openedChange",_openedStream:"opened",_closedStream:"closed",selectionChange:"selectionChange",valueChange:"valueChange"},exportAs:["matSelect"],features:[U([{provide:on,useExisting:i},{provide:St,useExisting:i}]),qe],ngContentSelectors:Bo,decls:11,vars:10,consts:[["fallbackOverlayOrigin","cdkOverlayOrigin","trigger",""],["panel",""],["cdk-overlay-origin","",1,"mat-mdc-select-trigger",3,"click"],[1,"mat-mdc-select-value"],[1,"mat-mdc-select-placeholder","mat-mdc-select-min-line"],[1,"mat-mdc-select-value-text"],[1,"mat-mdc-select-arrow-wrapper"],[1,"mat-mdc-select-arrow"],["viewBox","0 0 24 24","width","24px","height","24px","focusable","false","aria-hidden","true"],["d","M7 10l5 5 5-5z"],["cdk-connected-overlay","","cdkConnectedOverlayHasBackdrop","","cdkConnectedOverlayBackdropClass","cdk-overlay-transparent-backdrop",3,"detach","backdropClick","overlayKeydown","cdkConnectedOverlayDisableClose","cdkConnectedOverlayPanelClass","cdkConnectedOverlayScrollStrategy","cdkConnectedOverlayOrigin","cdkConnectedOverlayPositions","cdkConnectedOverlayWidth","cdkConnectedOverlayFlexibleDimensions","cdkConnectedOverlayUsePopover"],[1,"mat-mdc-select-min-line"],["role","listbox","tabindex","-1",1,"mat-mdc-select-panel","mdc-menu-surface","mdc-menu-surface--open",3,"keydown"]],template:function(t,n){if(t&1&&(H(No),c(0,"div",2,0),k("click",function(){return n.open()}),c(3,"div",3),F(4,zo,2,1,"span",4)(5,Vo,3,1,"span",5),u(),c(6,"div",6)(7,"div",7),Ue(),c(8,"svg",8),A(9,"path",9),u()()()(),ge(10,Go,3,16,"ng-template",10),k("detach",function(){return n.close()})("backdropClick",function(){return n.close()})("overlayKeydown",function(o){return n._handleOverlayKeydown(o)})),t&2){let a=we(1);d(3),T("id",n._valueId),d(),O(n.empty?4:5),d(6),E("cdkConnectedOverlayDisableClose",!0)("cdkConnectedOverlayPanelClass",n._overlayPanelClass)("cdkConnectedOverlayScrollStrategy",n._scrollStrategy)("cdkConnectedOverlayOrigin",n._preferredOverlayOrigin||a)("cdkConnectedOverlayPositions",n._positions)("cdkConnectedOverlayWidth",n._overlayWidth)("cdkConnectedOverlayFlexibleDimensions",!0)("cdkConnectedOverlayUsePopover",n._popoverLocation)}},dependencies:[$t,Ut],styles:[`@keyframes _mat-select-enter { + from { + opacity: 0; + transform: scaleY(0.8); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes _mat-select-exit { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +.mat-mdc-select { + display: inline-block; + width: 100%; + outline: none; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + color: var(--mat-select-enabled-trigger-text-color, var(--mat-sys-on-surface)); + font-family: var(--mat-select-trigger-text-font, var(--mat-sys-body-large-font)); + line-height: var(--mat-select-trigger-text-line-height, var(--mat-sys-body-large-line-height)); + font-size: var(--mat-select-trigger-text-size, var(--mat-sys-body-large-size)); + font-weight: var(--mat-select-trigger-text-weight, var(--mat-sys-body-large-weight)); + letter-spacing: var(--mat-select-trigger-text-tracking, var(--mat-sys-body-large-tracking)); +} + +div.mat-mdc-select-panel { + box-shadow: var(--mat-select-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12)); +} + +.mat-mdc-select-disabled { + color: var(--mat-select-disabled-trigger-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-mdc-select-disabled .mat-mdc-select-placeholder { + color: var(--mat-select-disabled-trigger-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} + +.mat-mdc-select-trigger { + display: inline-flex; + align-items: center; + cursor: pointer; + position: relative; + box-sizing: border-box; + width: 100%; +} +.mat-mdc-select-disabled .mat-mdc-select-trigger { + -webkit-user-select: none; + user-select: none; + cursor: default; +} + +.mat-mdc-select-value { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mat-mdc-select-value-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mat-mdc-select-arrow-wrapper { + height: 24px; + flex-shrink: 0; + display: inline-flex; + align-items: center; +} +.mat-form-field-appearance-fill .mdc-text-field--no-label .mat-mdc-select-arrow-wrapper { + transform: none; +} + +.mat-mdc-form-field .mat-mdc-select.mat-mdc-select-invalid .mat-mdc-select-arrow, +.mat-form-field-invalid:not(.mat-form-field-disabled) .mat-mdc-form-field-infix::after { + color: var(--mat-select-invalid-arrow-color, var(--mat-sys-error)); +} + +.mat-mdc-select-arrow { + width: 10px; + height: 5px; + position: relative; + color: var(--mat-select-enabled-arrow-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-form-field.mat-focused .mat-mdc-select-arrow { + color: var(--mat-select-focused-arrow-color, var(--mat-sys-primary)); +} +.mat-mdc-form-field .mat-mdc-select.mat-mdc-select-disabled .mat-mdc-select-arrow { + color: var(--mat-select-disabled-arrow-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-select-open .mat-mdc-select-arrow { + transform: rotate(180deg); +} +.mat-form-field-animations-enabled .mat-mdc-select-arrow { + transition: transform 80ms linear; +} +.mat-mdc-select-arrow svg { + fill: currentColor; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} +@media (forced-colors: active) { + .mat-mdc-select-arrow svg { + fill: CanvasText; + } + .mat-mdc-select-disabled .mat-mdc-select-arrow svg { + fill: GrayText; + } +} + +div.mat-mdc-select-panel { + width: 100%; + max-height: 275px; + outline: 0; + overflow: auto; + padding: 8px 0; + box-sizing: border-box; + transform-origin: top center; + border-radius: 0 0 4px 4px; + position: relative; + background-color: var(--mat-select-panel-background-color, var(--mat-sys-surface-container)); +} +.mat-mdc-select-panel-above div.mat-mdc-select-panel { + border-radius: 4px 4px 0 0; + transform-origin: bottom center; +} +@media (forced-colors: active) { + div.mat-mdc-select-panel { + outline: solid 1px; + } +} + +.mat-select-panel-animations-enabled { + animation: _mat-select-enter 120ms cubic-bezier(0, 0, 0.2, 1); +} +.mat-select-panel-animations-enabled.mat-select-panel-exit { + animation: _mat-select-exit 100ms linear; +} + +.mat-mdc-select-placeholder { + transition: color 400ms 133.3333333333ms cubic-bezier(0.25, 0.8, 0.25, 1); + color: var(--mat-select-placeholder-text-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-form-field:not(.mat-form-field-animations-enabled) .mat-mdc-select-placeholder, ._mat-animation-noopable .mat-mdc-select-placeholder { + transition: none; +} +.mat-form-field-hide-placeholder .mat-mdc-select-placeholder { + color: transparent; + -webkit-text-fill-color: transparent; + transition: none; + display: block; +} + +.mat-mdc-form-field-type-mat-select:not(.mat-form-field-disabled) .mat-mdc-text-field-wrapper { + cursor: pointer; +} +.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-fill .mat-mdc-floating-label { + max-width: calc(100% - 18px); +} +.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-fill .mdc-floating-label--float-above { + max-width: calc(100% / 0.75 - 24px); +} +.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-outline .mdc-notched-outline__notch { + max-width: calc(100% - 60px); +} +.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-outline .mdc-text-field--label-floating .mdc-notched-outline__notch { + max-width: calc(100% - 24px); +} + +.mat-mdc-select-min-line:empty::before { + content: " "; + white-space: pre; + width: 1px; + display: inline-block; + visibility: hidden; +} + +.mat-form-field-appearance-fill .mat-mdc-select-arrow-wrapper { + transform: var(--mat-select-arrow-transform, translateY(-8px)); +} +`],encapsulation:2,changeDetection:0})}return i})(),wc=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["mat-select-trigger"]],features:[U([{provide:Ui,useExisting:i}])]})}return i})(),Tc=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[Ne,Rt,j,Xe,$i,Rt]})}return i})();var qi=(()=>{class i{httpController;constructor(e){this.httpController=e}getComputes(e){return this.httpController.get(e,"/computes")}getCompute(e,t){return this.httpController.get(e,`/computes/${t}`)}getNetworkInterfaces(e,t){return this.httpController.get(e,`/computes/${t}/network/interfaces`)}createCompute(e,t){return this.httpController.post(e,"/computes",t)}updateCompute(e,t,n){return this.httpController.put(e,`/computes/${t}`,n)}deleteCompute(e,t){return this.httpController.delete(e,`/computes/${t}`)}connectCompute(e,t){return this.httpController.post(e,`/computes/${t}/connect`,null)}getStatistics(e){return this.httpController.get(e,"/statistics")}static \u0275fac=function(t){return new(t||i)(J(xi))};static \u0275prov=P({token:i,factory:i.\u0275fac})}return i})();var Fc=(()=>{class i{computeService;WS_LIVENESS_TIMEOUT_MS=15e3;WS_LIVENESS_CHECK_MS=5e3;ws=null;currentController=null;intentionalClose=!1;reconnectTimer=null;reconnectAttempt=0;livenessTimer=null;lastWsMessageAt=0;computeNotificationEmitter=new g;computeCache=new Map;projectNotificationEmitter=new g;serverSettingsNotificationEmitter=new g;wsReconnected=new g;computeCacheUpdated=new g;constructor(e){this.computeService=e}notificationsPath(e){let t="ws";return e.protocol==="https:"&&(t="wss"),`${t}://${e.host}:${e.port}/${tt.current_version}/notifications/ws?token=${e.authToken}`}projectNotificationsPath(e,t){let n="ws";return e.protocol==="https:"&&(n="wss"),`${n}://${e.host}:${e.port}/${tt.current_version}/projects/${t}/notifications/ws?token=${e.authToken}`}markerNotificationsPath(e,t){let n="ws";return e.protocol==="https:"&&(n="wss"),`${n}://${e.host}:${e.port}/${tt.current_version}/projects/${t}/notifications/markers/ws?token=${e.authToken}`}connectToComputeNotifications(e){this.ws&&this.currentController===e||(this.disconnect(),this.currentController=e,this.openComputeNotificationsWs())}openComputeNotificationsWs(){this.currentController&&(this.intentionalClose=!1,this.ws=new WebSocket(this.notificationsPath(this.currentController)),this.ws.onopen=()=>{let e=this.reconnectAttempt>0;this.reconnectAttempt=0,this.startLivenessCheck(),e&&(this.resyncComputesAfterReconnect(),this.wsReconnected.emit())},this.ws.onmessage=e=>{this.lastWsMessageAt=Date.now();let t=JSON.parse(e.data);this.handleMessage(t)},this.ws.onerror=()=>{console.error("Compute notifications WebSocket error")},this.ws.onclose=()=>{this.ws=null,this.stopLivenessCheck(),!this.intentionalClose&&this.scheduleReconnect()})}scheduleReconnect(){if(this.intentionalClose||!this.currentController)return;let e=this.reconnectAttempt++,t=Math.min(3e4,1e3*2**e),n=Math.round(t+Math.random()*.25*t);this.reconnectTimer=setTimeout(()=>this.openComputeNotificationsWs(),n)}resyncComputesAfterReconnect(){this.currentController&&this.computeService.getComputes(this.currentController).subscribe({next:e=>this.setInitialComputes(e),error:e=>console.warn("Compute resync after WS reconnect failed",e)})}startLivenessCheck(){this.stopLivenessCheck(),this.lastWsMessageAt=Date.now(),this.livenessTimer=setInterval(()=>{Date.now()-this.lastWsMessageAt>this.WS_LIVENESS_TIMEOUT_MS&&(console.warn("Compute notifications WS liveness timeout \u2014 no message received, forcing reconnect"),this.stopLivenessCheck(),this.ws?.close())},this.WS_LIVENESS_CHECK_MS)}stopLivenessCheck(){this.livenessTimer&&(clearInterval(this.livenessTimer),this.livenessTimer=null)}disconnect(){this.intentionalClose=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.stopLivenessCheck(),this.reconnectAttempt=0,this.ws&&(this.ws.close(),this.ws=null),this.currentController=null,this.computeCache.clear()}getCachedComputes(){return Array.from(this.computeCache.values())}hasCachedData(){return this.computeCache.size>0}setInitialComputes(e){this.computeCache.clear(),e.forEach(t=>{this.computeCache.set(t.compute_id,t)}),this.computeCacheUpdated.emit(this.getCachedComputes())}handleMessage(e){switch(e.action){case"compute.created":this.computeCache.set(e.event.compute_id,e.event),this.computeNotificationEmitter.emit(e),this.computeCacheUpdated.emit(this.getCachedComputes());break;case"compute.updated":this.computeCache.set(e.event.compute_id,e.event),this.computeNotificationEmitter.emit(e),this.computeCacheUpdated.emit(this.getCachedComputes());break;case"compute.deleted":this.computeCache.delete(e.event.compute_id),this.computeNotificationEmitter.emit(e),this.computeCacheUpdated.emit(this.getCachedComputes());break;case"project.created":case"project.opened":case"project.closed":case"project.updated":case"project.deleted":this.projectNotificationEmitter.emit(e);break;case"settings.updated":this.serverSettingsNotificationEmitter.emit(e);break}}static \u0275fac=function(t){return new(t||i)(J(qi))};static \u0275prov=P({token:i,factory:i.\u0275fac})}return i})();function Uo(i,s){if(i&1){let e=Se();c(0,"div",1)(1,"button",2),k("click",function(){ne(e);let n=y();return ie(n.action())}),$(2),u()()}if(i&2){let e=y();d(2),Qe(" ",e.data.action," ")}}var qo=["label"];function Qo(i,s){}var Ko=Math.pow(2,31)-1,Ge=class{_overlayRef;instance;containerInstance;_afterDismissed=new f;_afterOpened=new f;_onAction=new f;_durationTimeoutId;_dismissedByAction=!1;constructor(s,e){this._overlayRef=e,this.containerInstance=s,s._onExit.subscribe(()=>this._finishDismiss())}dismiss(){this._afterDismissed.closed||this.containerInstance.exit(),clearTimeout(this._durationTimeoutId)}dismissWithAction(){this._onAction.closed||(this._dismissedByAction=!0,this._onAction.next(),this._onAction.complete(),this.dismiss()),clearTimeout(this._durationTimeoutId)}closeWithAction(){this.dismissWithAction()}_dismissAfter(s){this._durationTimeoutId=setTimeout(()=>this.dismiss(),Math.min(s,Ko))}_open(){this._afterOpened.closed||(this._afterOpened.next(),this._afterOpened.complete())}_finishDismiss(){this._overlayRef.dispose(),this._onAction.closed||this._onAction.complete(),this._afterDismissed.next({dismissedByAction:this._dismissedByAction}),this._afterDismissed.complete(),this._dismissedByAction=!1}afterDismissed(){return this._afterDismissed}afterOpened(){return this.containerInstance._onEnter}onAction(){return this._onAction}},Lt=new C("MatSnackBarData"),it=class{politeness="polite";announcementMessage="";viewContainerRef;duration=0;panelClass;direction;data=null;horizontalPosition="center";verticalPosition="bottom"},Xo=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["","matSnackBarLabel",""]],hostAttrs:[1,"mat-mdc-snack-bar-label","mdc-snackbar__label"]})}return i})(),Zo=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["","matSnackBarActions",""]],hostAttrs:[1,"mat-mdc-snack-bar-actions","mdc-snackbar__actions"]})}return i})(),Jo=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["","matSnackBarAction",""]],hostAttrs:[1,"mat-mdc-snack-bar-action","mdc-snackbar__action"]})}return i})(),Qi=(()=>{class i{snackBarRef=r(Ge);data=r(Lt);constructor(){}action(){this.snackBarRef.dismissWithAction()}get hasAction(){return!!this.data.action}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["simple-snack-bar"]],hostAttrs:[1,"mat-mdc-simple-snack-bar"],exportAs:["matSnackBar"],decls:3,vars:2,consts:[["matSnackBarLabel",""],["matSnackBarActions",""],["matButton","","matSnackBarAction","",3,"click"]],template:function(t,n){t&1&&(c(0,"div",0),$(1),u(),F(2,Uo,3,1,"div",1)),t&2&&(d(),Qe(" ",n.data.message,` +`),d(),O(n.hasAction?2:-1))},dependencies:[yi,Xo,Zo,Jo],styles:[`.mat-mdc-simple-snack-bar { + display: flex; +} +.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-label { + max-height: 50vh; + overflow: auto; +} +`],encapsulation:2,changeDetection:0})}return i})(),ln="_mat-snack-bar-enter",cn="_mat-snack-bar-exit",er=(()=>{class i extends qn{_ngZone=r(z);_elementRef=r(D);_changeDetectorRef=r(Y);_platform=r(Ee);_animationsDisabled=q();snackBarConfig=r(it);_document=r(be);_trackedModals=new Set;_enterFallback;_exitFallback;_injector=r(X);_announceDelay=150;_announceTimeoutId;_destroyed=!1;_portalOutlet;_onAnnounce=new f;_onExit=new f;_onEnter=new f;_animationState="void";_live;_label;_role;_liveElementId=r(K).getId("mat-snack-bar-container-live-");constructor(){super();let e=this.snackBarConfig;e.politeness==="assertive"&&!e.announcementMessage?this._live="assertive":e.politeness==="off"?this._live="off":this._live="polite",this._platform.FIREFOX&&(this._live==="polite"&&(this._role="status"),this._live==="assertive"&&(this._role="alert"))}attachComponentPortal(e){this._assertNotAttached();let t=this._portalOutlet.attachComponentPortal(e);return this._afterPortalAttached(),t}attachTemplatePortal(e){this._assertNotAttached();let t=this._portalOutlet.attachTemplatePortal(e);return this._afterPortalAttached(),t}attachDomPortal=e=>{this._assertNotAttached();let t=this._portalOutlet.attachDomPortal(e);return this._afterPortalAttached(),t};onAnimationEnd(e){e===cn?this._completeExit():e===ln&&(clearTimeout(this._enterFallback),this._ngZone.run(()=>{this._onEnter.next(),this._onEnter.complete()}))}enter(){this._destroyed||(this._animationState="visible",this._changeDetectorRef.markForCheck(),this._changeDetectorRef.detectChanges(),this._screenReaderAnnounce(),this._animationsDisabled?ce(()=>{this._ngZone.run(()=>queueMicrotask(()=>this.onAnimationEnd(ln)))},{injector:this._injector}):(clearTimeout(this._enterFallback),this._enterFallback=setTimeout(()=>{this._elementRef.nativeElement.classList.add("mat-snack-bar-fallback-visible"),this.onAnimationEnd(ln)},200)))}exit(){return this._destroyed?ze(void 0):(this._ngZone.run(()=>{this._animationState="hidden",this._changeDetectorRef.markForCheck(),this._elementRef.nativeElement.setAttribute("mat-exit",""),clearTimeout(this._announceTimeoutId),this._animationsDisabled?ce(()=>{this._ngZone.run(()=>queueMicrotask(()=>this.onAnimationEnd(cn)))},{injector:this._injector}):(clearTimeout(this._exitFallback),this._exitFallback=setTimeout(()=>this.onAnimationEnd(cn),200))}),this._onExit)}ngOnDestroy(){this._destroyed=!0,this._clearFromModals(),this._completeExit()}_completeExit(){clearTimeout(this._exitFallback),queueMicrotask(()=>{this._onExit.next(),this._onExit.complete()})}_afterPortalAttached(){let e=this._elementRef.nativeElement,t=this.snackBarConfig.panelClass;t&&(Array.isArray(t)?t.forEach(o=>e.classList.add(o)):e.classList.add(t)),this._exposeToModals();let n=this._label.nativeElement,a="mdc-snackbar__label";n.classList.toggle(a,!n.querySelector(`.${a}`))}_exposeToModals(){let e=this._liveElementId,t=this._document.querySelectorAll('body > .cdk-overlay-container [aria-modal="true"]');for(let n=0;n{let t=e.getAttribute("aria-owns");if(t){let n=t.replace(this._liveElementId,"").trim();n.length>0?e.setAttribute("aria-owns",n):e.removeAttribute("aria-owns")}}),this._trackedModals.clear()}_assertNotAttached(){this._portalOutlet.hasAttached()}_screenReaderAnnounce(){this._announceTimeoutId||this._ngZone.runOutsideAngular(()=>{this._announceTimeoutId=setTimeout(()=>{if(this._destroyed)return;let e=this._elementRef.nativeElement,t=e.querySelector("[aria-hidden]"),n=e.querySelector("[aria-live]");if(t&&n){let a=null;this._platform.isBrowser&&document.activeElement instanceof HTMLElement&&t.contains(document.activeElement)&&(a=document.activeElement),t.removeAttribute("aria-hidden"),n.appendChild(t),a?.focus(),this._onAnnounce.next(),this._onAnnounce.complete()}},this._announceDelay)})}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-snack-bar-container"]],viewQuery:function(t,n){if(t&1&&W(Ze,7)(qo,7),t&2){let a;m(a=h())&&(n._portalOutlet=a.first),m(a=h())&&(n._label=a.first)}},hostAttrs:[1,"mdc-snackbar","mat-mdc-snack-bar-container"],hostVars:6,hostBindings:function(t,n){t&1&&k("animationend",function(o){return n.onAnimationEnd(o.animationName)})("animationcancel",function(o){return n.onAnimationEnd(o.animationName)}),t&2&&p("mat-snack-bar-container-enter",n._animationState==="visible")("mat-snack-bar-container-exit",n._animationState==="hidden")("mat-snack-bar-container-animations-enabled",!n._animationsDisabled)},features:[_e],decls:6,vars:3,consts:[["label",""],[1,"mdc-snackbar__surface","mat-mdc-snackbar-surface"],[1,"mat-mdc-snack-bar-label"],["aria-hidden","true"],["cdkPortalOutlet",""]],template:function(t,n){t&1&&(c(0,"div",1)(1,"div",2,0)(3,"div",3),ge(4,Qo,0,0,"ng-template",4),u(),A(5,"div"),u()()),t&2&&(d(5),T("aria-live",n._live)("role",n._role)("id",n._liveElementId))},dependencies:[Ze],styles:[`@keyframes _mat-snack-bar-enter { + from { + transform: scale(0.8); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} +@keyframes _mat-snack-bar-exit { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +.mat-mdc-snack-bar-container { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + margin: 8px; +} +.mat-mdc-snack-bar-handset .mat-mdc-snack-bar-container { + width: 100vw; +} + +.mat-snack-bar-container-animations-enabled { + opacity: 0; +} +.mat-snack-bar-container-animations-enabled.mat-snack-bar-fallback-visible { + opacity: 1; +} +.mat-snack-bar-container-animations-enabled.mat-snack-bar-container-enter { + animation: _mat-snack-bar-enter 150ms cubic-bezier(0, 0, 0.2, 1) forwards; +} +.mat-snack-bar-container-animations-enabled.mat-snack-bar-container-exit { + animation: _mat-snack-bar-exit 75ms cubic-bezier(0.4, 0, 1, 1) forwards; +} + +.mat-mdc-snackbar-surface { + box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); + display: flex; + align-items: center; + justify-content: flex-start; + box-sizing: border-box; + padding-left: 0; + padding-right: 8px; +} +[dir=rtl] .mat-mdc-snackbar-surface { + padding-right: 0; + padding-left: 8px; +} +.mat-mdc-snack-bar-container .mat-mdc-snackbar-surface { + min-width: 344px; + max-width: 672px; +} +.mat-mdc-snack-bar-handset .mat-mdc-snackbar-surface { + width: 100%; + min-width: 0; +} +@media (forced-colors: active) { + .mat-mdc-snackbar-surface { + outline: solid 1px; + } +} +.mat-mdc-snack-bar-container .mat-mdc-snackbar-surface { + color: var(--mat-snack-bar-supporting-text-color, var(--mat-sys-inverse-on-surface)); + border-radius: var(--mat-snack-bar-container-shape, var(--mat-sys-corner-extra-small)); + background-color: var(--mat-snack-bar-container-color, var(--mat-sys-inverse-surface)); +} + +.mdc-snackbar__label { + width: 100%; + flex-grow: 1; + box-sizing: border-box; + margin: 0; + padding: 14px 8px 14px 16px; +} +[dir=rtl] .mdc-snackbar__label { + padding-left: 8px; + padding-right: 16px; +} +.mat-mdc-snack-bar-container .mdc-snackbar__label { + font-family: var(--mat-snack-bar-supporting-text-font, var(--mat-sys-body-medium-font)); + font-size: var(--mat-snack-bar-supporting-text-size, var(--mat-sys-body-medium-size)); + font-weight: var(--mat-snack-bar-supporting-text-weight, var(--mat-sys-body-medium-weight)); + line-height: var(--mat-snack-bar-supporting-text-line-height, var(--mat-sys-body-medium-line-height)); +} + +.mat-mdc-snack-bar-actions { + display: flex; + flex-shrink: 0; + align-items: center; + box-sizing: border-box; +} + +.mat-mdc-snack-bar-handset, +.mat-mdc-snack-bar-container, +.mat-mdc-snack-bar-label { + flex: 1 1 auto; +} + +.mat-mdc-snack-bar-container .mat-mdc-button.mat-mdc-snack-bar-action:not(:disabled).mat-unthemed { + color: var(--mat-snack-bar-button-color, var(--mat-sys-inverse-primary)); +} +.mat-mdc-snack-bar-container .mat-mdc-button.mat-mdc-snack-bar-action:not(:disabled) { + --mat-button-text-state-layer-color: currentColor; + --mat-button-text-ripple-color: currentColor; +} +.mat-mdc-snack-bar-container .mat-mdc-button.mat-mdc-snack-bar-action:not(:disabled) .mat-ripple-element { + opacity: 0.1; +} +`],encapsulation:2})}return i})(),tr=new C("mat-snack-bar-default-options",{providedIn:"root",factory:()=>new it}),dn=(()=>{class i{_live=r(vt);_injector=r(X);_breakpointObserver=r(ci);_parentSnackBar=r(i,{optional:!0,skipSelf:!0});_defaultConfig=r(tr);_animationsDisabled=q();_snackBarRefAtThisLevel=null;simpleSnackBarComponent=Qi;snackBarContainerComponent=er;handsetCssClass="mat-mdc-snack-bar-handset";get _openedSnackBarRef(){let e=this._parentSnackBar;return e?e._openedSnackBarRef:this._snackBarRefAtThisLevel}set _openedSnackBarRef(e){this._parentSnackBar?this._parentSnackBar._openedSnackBarRef=e:this._snackBarRefAtThisLevel=e}constructor(){}openFromComponent(e,t){return this._attach(e,t)}openFromTemplate(e,t){return this._attach(e,t)}open(e,t="",n){let a=S(S({},this._defaultConfig),n);return a.data={message:e,action:t},a.announcementMessage===e&&(a.announcementMessage=void 0),this.openFromComponent(this.simpleSnackBarComponent,a)}dismiss(){this._openedSnackBarRef&&this._openedSnackBarRef.dismiss()}ngOnDestroy(){this._snackBarRefAtThisLevel&&this._snackBarRefAtThisLevel.dismiss()}_attachSnackBarContainer(e,t){let n=t&&t.viewContainerRef&&t.viewContainerRef.injector,a=X.create({parent:n||this._injector,providers:[{provide:it,useValue:t}]}),o=new ct(this.snackBarContainerComponent,t.viewContainerRef,a),l=e.attach(o);return l.instance.snackBarConfig=t,l.instance}_attach(e,t){let n=S(S(S({},new it),this._defaultConfig),t),a=this._createOverlay(n),o=this._attachSnackBarContainer(a,n),l=new Ge(o,a);if(e instanceof Ie){let v=new je(e,null,{$implicit:n.data,snackBarRef:l});l.instance=o.attachTemplatePortal(v)}else{let v=this._createInjector(n,l),b=new ct(e,void 0,v),V=o.attachComponentPortal(b);l.instance=V.instance}return this._breakpointObserver.observe(pi.HandsetPortrait).pipe(x(a.detachments())).subscribe(v=>{a.overlayElement.classList.toggle(this.handsetCssClass,v.matches)}),n.announcementMessage&&o._onAnnounce.subscribe(()=>{this._live.announce(n.announcementMessage,n.politeness)}),this._animateSnackBar(l,n),this._openedSnackBarRef=l,this._openedSnackBarRef}_animateSnackBar(e,t){e.afterDismissed().subscribe(()=>{this._openedSnackBarRef==e&&(this._openedSnackBarRef=null),t.announcementMessage&&this._live.clear()}),t.duration&&t.duration>0&&e.afterOpened().subscribe(()=>e._dismissAfter(t.duration)),this._openedSnackBarRef?(this._openedSnackBarRef.afterDismissed().subscribe(()=>{e.containerInstance.enter()}),this._openedSnackBarRef.dismiss()):e.containerInstance.enter()}_createOverlay(e){let t=new _t;t.direction=e.direction;let n=Zn(this._injector),a=e.direction==="rtl",o=e.horizontalPosition==="left"||e.horizontalPosition==="start"&&!a||e.horizontalPosition==="end"&&a,l=!o&&e.horizontalPosition!=="center";return o?n.left("0"):l?n.right("0"):n.centerHorizontally(),e.verticalPosition==="top"?n.top("0"):n.bottom("0"),t.positionStrategy=n,t.disableAnimations=this._animationsDisabled,et(this._injector,t)}_createInjector(e,t){let n=e&&e.viewContainerRef&&e.viewContainerRef.injector;return X.create({parent:n||this._injector,providers:[{provide:Ge,useValue:t},{provide:Lt,useValue:e.data}]})}static \u0275fac=function(t){return new(t||i)};static \u0275prov=P({token:i,factory:i.\u0275fac,providedIn:"root"})}return i})();var Zc=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({providers:[dn],imports:[Ne,Xn,xt,Qi,j]})}return i})();var Ki={success:{icon:"check_circle",label:"Completed"},info:{icon:"info",label:"Activity"},warning:{icon:"warning",label:"Warning"},error:{icon:"error",label:"Error"}};function Xi(i){return Ki[i].icon}function Zi(i){return Ki[i].label}var Ji=(()=>{class i{snackBarRef=r(Ge);data=r(Lt);icon=Xi;label=Zi;dismiss(){this.snackBarRef.dismiss()}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["app-notification-toast"]],decls:11,vars:11,consts:[[1,"notification-toast"],["aria-hidden","true",1,"notification-toast__status"],[1,"notification-toast__content"],["mat-icon-button","","type","button","aria-label","Dismiss notification",3,"click"]],template:function(t,n){t&1&&(c(0,"div",0)(1,"mat-icon",1),$(2),u(),c(3,"div",2)(4,"strong"),$(5),u(),c(6,"span"),$(7),u()(),c(8,"button",3),k("click",function(){return n.dismiss()}),c(9,"mat-icon"),$(10,"close"),u()()()),t&2&&(p("notification-toast--success",n.data.notification.kind==="success")("notification-toast--info",n.data.notification.kind==="info")("notification-toast--warning",n.data.notification.kind==="warning")("notification-toast--error",n.data.notification.kind==="error"),d(2),se(n.icon(n.data.notification.kind)),d(3),se(n.label(n.data.notification.kind)),d(2),se(n.data.notification.message))},dependencies:[xt,vi,gi,_i],styles:["[_nghost-%COMP%]{display:block}.notification-toast[_ngcontent-%COMP%]{--notification-accent: var(--mat-sys-primary);min-width:min(380px,100vw - 32px);max-width:min(460px,100vw - 32px);padding:var(--gns3-density-space-3);display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:var(--gns3-density-space-3);box-sizing:border-box;color:var(--mat-sys-on-surface);background:var(--mat-sys-surface-container-high);border:1px solid color-mix(in srgb,var(--notification-accent) 45%,var(--mat-sys-outline-variant));border-left:4px solid var(--notification-accent);border-radius:var(--gns3-density-radius-md);box-shadow:0 12px 32px color-mix(in srgb,var(--mat-sys-shadow) 24%,transparent)}.notification-toast--success[_ngcontent-%COMP%]{--notification-accent: var(--custom-mat-sys-success)}.notification-toast--warning[_ngcontent-%COMP%]{--notification-accent: var(--mat-sys-tertiary)}.notification-toast--error[_ngcontent-%COMP%]{--notification-accent: var(--mat-sys-error)}.notification-toast[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{width:var(--gns3-density-control-height-sm);height:var(--gns3-density-control-height-sm)}.notification-toast__status[_ngcontent-%COMP%]{color:var(--notification-accent)}.notification-toast__content[_ngcontent-%COMP%]{min-width:0;display:flex;flex-direction:column;gap:var(--gns3-density-space-1)}.notification-toast__content[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font-size:14px;font-weight:650}.notification-toast__content[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{overflow-wrap:anywhere;color:var(--mat-sys-on-surface-variant);font-size:13px;line-height:1.4;white-space:pre-line}@media(max-width:480px){.notification-toast[_ngcontent-%COMP%]{min-width:calc(100vw - 24px);max-width:calc(100vw - 24px)}}"],changeDetection:0})}return i})();var ea="gns3-notification-history",ta=100,ir=864e13,na=(()=>{class i{sequence=0;notificationState=Q(this.restore());panelOpenState=Q(!1);notifications=this.notificationState.asReadonly();panelOpen=this.panelOpenState.asReadonly();unreadCount=ye(()=>this.notificationState().filter(e=>!e.read).length);errorCount=ye(()=>this.notificationState().filter(e=>e.kind==="error").length);warningCount=ye(()=>this.notificationState().filter(e=>e.kind==="warning").length);activityCount=ye(()=>this.notificationState().filter(e=>e.kind==="success"||e.kind==="info").length);add(e,t){let n={id:this.createId(),kind:e,message:this.normalizeMessage(t),createdAt:Date.now(),read:!1};return this.update([n,...this.notificationState()].slice(0,ta)),n}togglePanel(){this.panelOpenState.update(e=>!e)}closePanel(){this.panelOpenState.set(!1)}markAsRead(e){this.notificationState().some(t=>t.id===e&&!t.read)&&this.update(this.notificationState().map(t=>t.id===e?le(S({},t),{read:!0}):t))}markAllAsRead(){this.unreadCount()!==0&&this.update(this.notificationState().map(e=>le(S({},e),{read:!0})))}remove(e){this.update(this.notificationState().filter(t=>t.id!==e))}clear(){this.update([])}createId(){return typeof globalThis.crypto?.randomUUID=="function"?globalThis.crypto.randomUUID():`${Date.now()}-${this.sequence++}-${Math.random().toString(36).slice(2)}`}normalizeMessage(e){if(typeof e=="string")return e;if(e instanceof Error)return e.message;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}update(e){this.notificationState.set(e);try{localStorage.setItem(ea,JSON.stringify(e))}catch{}}restore(){try{let e=JSON.parse(localStorage.getItem(ea)||"[]");return Array.isArray(e)?e.filter(t=>typeof t?.id=="string"&&typeof t?.message=="string"&&typeof t?.createdAt=="number"&&Number.isFinite(t.createdAt)&&t.createdAt>=0&&t.createdAt<=ir&&typeof t?.read=="boolean"&&["success","info","warning","error"].includes(t?.kind)).slice(0,ta):[]}catch{return[]}}static \u0275fac=function(t){return new(t||i)};static \u0275prov=P({token:i,factory:i.\u0275fac,providedIn:"root"})}return i})();var ia=(()=>{class i{snackbar;notificationCenter;snackBarConfigForSuccess={duration:4e3,panelClass:["gns3-toast-panel"],horizontalPosition:"center",verticalPosition:"bottom"};snackBarConfigForWarning={duration:4e3,panelClass:["gns3-toast-panel"],horizontalPosition:"center",verticalPosition:"bottom",politeness:"assertive"};snackBarConfigForError={duration:1e4,panelClass:["gns3-toast-panel"],horizontalPosition:"center",verticalPosition:"bottom",politeness:"assertive"};snackBarConfigForInfo={duration:4e3,panelClass:["gns3-toast-panel"],horizontalPosition:"center",verticalPosition:"bottom"};constructor(e,t){this.snackbar=e,this.notificationCenter=t}error(e,t={}){console.error(e),this.show("error",e,this.snackBarConfigForError,t)}warning(e,t={}){this.show("warning",e,this.snackBarConfigForWarning,t)}success(e,t={}){this.show("success",e,this.snackBarConfigForSuccess,t)}info(e,t={}){this.show("info",e,this.snackBarConfigForInfo,t)}show(e,t,n,a){let o=this.notificationCenter.add(e,t);a.showToast===!1||this.notificationCenter.panelOpen()||this.snackbar.openFromComponent(Ji,le(S({},n),{data:{notification:o}}))}static \u0275fac=function(t){return new(t||i)(J(dn),J(na))};static \u0275prov=P({token:i,factory:i.\u0275fac})}return i})();var _d=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[j]})}return i})();var rr=["mat-internal-form-field",""],sr=["*"],aa=(()=>{class i{labelPosition="after";static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["div","mat-internal-form-field",""]],hostAttrs:[1,"mdc-form-field","mat-internal-form-field"],hostVars:2,hostBindings:function(t,n){t&2&&p("mdc-form-field--align-end",n.labelPosition==="before")},inputs:{labelPosition:"labelPosition"},attrs:rr,ngContentSelectors:sr,decls:1,vars:0,template:function(t,n){t&1&&(H(),w(0))},styles:[`.mat-internal-form-field { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-flex; + align-items: center; + vertical-align: middle; +} +.mat-internal-form-field > label { + margin-left: 0; + margin-right: auto; + padding-left: 4px; + padding-right: 0; + order: 0; +} +[dir=rtl] .mat-internal-form-field > label { + margin-left: auto; + margin-right: 0; + padding-left: 0; + padding-right: 4px; +} + +.mdc-form-field--align-end > label { + margin-left: auto; + margin-right: 0; + padding-left: 0; + padding-right: 4px; + order: -1; +} +[dir=rtl] .mdc-form-field--align-end .mdc-form-field--align-end label { + margin-left: 0; + margin-right: auto; + padding-left: 4px; + padding-right: 0; +} +`],encapsulation:2,changeDetection:0})}return i})();var lr=["input"],cr=["label"],dr=["*"],mn={color:"accent",clickAction:"check-indeterminate",disabledInteractive:!1},mr=new C("mat-checkbox-default-options",{providedIn:"root",factory:()=>mn}),ee=(function(i){return i[i.Init=0]="Init",i[i.Checked=1]="Checked",i[i.Unchecked=2]="Unchecked",i[i.Indeterminate=3]="Indeterminate",i})(ee||{}),hn=class{source;checked},hr=(()=>{class i{_elementRef=r(D);_changeDetectorRef=r(Y);_ngZone=r(z);_animationsDisabled=q();_options=r(mr,{optional:!0});focus(){this._inputElement.nativeElement.focus()}_createChangeEvent(e){let t=new hn;return t.source=this,t.checked=e,t}_getAnimationTargetElement(){return this._inputElement?.nativeElement}_animationClasses={uncheckedToChecked:"mdc-checkbox--anim-unchecked-checked",uncheckedToIndeterminate:"mdc-checkbox--anim-unchecked-indeterminate",checkedToUnchecked:"mdc-checkbox--anim-checked-unchecked",checkedToIndeterminate:"mdc-checkbox--anim-checked-indeterminate",indeterminateToChecked:"mdc-checkbox--anim-indeterminate-checked",indeterminateToUnchecked:"mdc-checkbox--anim-indeterminate-unchecked"};ariaLabel="";ariaLabelledby=null;ariaDescribedby;ariaExpanded;ariaControls;ariaOwns;_uniqueId;id;get inputId(){return`${this.id||this._uniqueId}-input`}required=!1;labelPosition="after";name=null;change=new g;indeterminateChange=new g;value;disableRipple=!1;_inputElement;_labelElement;tabIndex;color;disabledInteractive;_onTouched=()=>{};_currentAnimationClass="";_currentCheckState=ee.Init;_controlValueAccessorChangeFn=()=>{};_validatorChangeFn=()=>{};constructor(){r(De).load(Re);let e=r(new Ke("tabindex"),{optional:!0});this._options=this._options||mn,this.color=this._options.color||mn.color,this.tabIndex=e==null?0:parseInt(e)||0,this.id=this._uniqueId=r(K).getId("mat-mdc-checkbox-"),this.disabledInteractive=this._options?.disabledInteractive??!1}ngOnChanges(e){e.required&&this._validatorChangeFn()}ngAfterViewInit(){this._syncIndeterminate(this.indeterminate)}get checked(){return this._checked}set checked(e){e!=this.checked&&(this._checked=e,this._changeDetectorRef.markForCheck())}_checked=!1;get disabled(){return this._disabled}set disabled(e){e!==this.disabled&&(this._disabled=e,this._changeDetectorRef.markForCheck())}_disabled=!1;get indeterminate(){return this._indeterminate()}set indeterminate(e){let t=e!=this._indeterminate();this._indeterminate.set(e),t&&(e?this._transitionCheckState(ee.Indeterminate):this._transitionCheckState(this.checked?ee.Checked:ee.Unchecked),this.indeterminateChange.emit(e)),this._syncIndeterminate(e)}_indeterminate=Q(!1);_isRippleDisabled(){return this.disableRipple||this.disabled}_onLabelTextChange(){this._changeDetectorRef.detectChanges()}writeValue(e){this.checked=!!e}registerOnChange(e){this._controlValueAccessorChangeFn=e}registerOnTouched(e){this._onTouched=e}setDisabledState(e){this.disabled=e}validate(e){return this.required&&e.value!==!0?{required:!0}:null}registerOnValidatorChange(e){this._validatorChangeFn=e}_transitionCheckState(e){let t=this._currentCheckState,n=this._getAnimationTargetElement();if(!(t===e||!n)&&(this._currentAnimationClass&&n.classList.remove(this._currentAnimationClass),this._currentAnimationClass=this._getAnimationClassForCheckStateTransition(t,e),this._currentCheckState=e,this._currentAnimationClass.length>0)){n.classList.add(this._currentAnimationClass);let a=this._currentAnimationClass;this._ngZone.runOutsideAngular(()=>{setTimeout(()=>{n.classList.remove(a)},1e3)})}}_emitChangeEvent(){this._controlValueAccessorChangeFn(this.checked),this.change.emit(this._createChangeEvent(this.checked)),this._inputElement&&(this._inputElement.nativeElement.checked=this.checked)}toggle(){this.checked=!this.checked,this._controlValueAccessorChangeFn(this.checked)}_handleInputClick(){let e=this._options?.clickAction;!this.disabled&&e!=="noop"?(this.indeterminate&&e!=="check"&&Promise.resolve().then(()=>{this._indeterminate.set(!1),this.indeterminateChange.emit(!1)}),this._checked=!this._checked,this._transitionCheckState(this._checked?ee.Checked:ee.Unchecked),this._emitChangeEvent()):(this.disabled&&this.disabledInteractive||!this.disabled&&e==="noop")&&(this._inputElement.nativeElement.checked=this.checked,this._inputElement.nativeElement.indeterminate=this.indeterminate)}_onInteractionEvent(e){e.stopPropagation()}_onBlur(){Promise.resolve().then(()=>{this._onTouched(),this._changeDetectorRef.markForCheck()})}_getAnimationClassForCheckStateTransition(e,t){if(this._animationsDisabled)return"";switch(e){case ee.Init:if(t===ee.Checked)return this._animationClasses.uncheckedToChecked;if(t==ee.Indeterminate)return this._checked?this._animationClasses.checkedToIndeterminate:this._animationClasses.uncheckedToIndeterminate;break;case ee.Unchecked:return t===ee.Checked?this._animationClasses.uncheckedToChecked:this._animationClasses.uncheckedToIndeterminate;case ee.Checked:return t===ee.Unchecked?this._animationClasses.checkedToUnchecked:this._animationClasses.checkedToIndeterminate;case ee.Indeterminate:return t===ee.Checked?this._animationClasses.indeterminateToChecked:this._animationClasses.indeterminateToUnchecked}return""}_syncIndeterminate(e){let t=this._inputElement;t&&(t.nativeElement.indeterminate=e)}_onInputClick(){this._handleInputClick()}_onTouchTargetClick(){this._handleInputClick(),this.disabled||this._inputElement.nativeElement.focus()}_preventBubblingFromLabel(e){e.target&&this._labelElement.nativeElement.contains(e.target)&&e.stopPropagation()}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-checkbox"]],viewQuery:function(t,n){if(t&1&&W(lr,5)(cr,5),t&2){let a;m(a=h())&&(n._inputElement=a.first),m(a=h())&&(n._labelElement=a.first)}},hostAttrs:[1,"mat-mdc-checkbox"],hostVars:16,hostBindings:function(t,n){t&2&&(Ce("id",n.id),T("tabindex",null)("aria-label",null)("aria-labelledby",null),de(n.color?"mat-"+n.color:"mat-accent"),p("_mat-animation-noopable",n._animationsDisabled)("mdc-checkbox--disabled",n.disabled)("mat-mdc-checkbox-disabled",n.disabled)("mat-mdc-checkbox-checked",n.checked)("mat-mdc-checkbox-disabled-interactive",n.disabledInteractive))},inputs:{ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],ariaDescribedby:[0,"aria-describedby","ariaDescribedby"],ariaExpanded:[2,"aria-expanded","ariaExpanded",_],ariaControls:[0,"aria-controls","ariaControls"],ariaOwns:[0,"aria-owns","ariaOwns"],id:"id",required:[2,"required","required",_],labelPosition:"labelPosition",name:"name",value:"value",disableRipple:[2,"disableRipple","disableRipple",_],tabIndex:[2,"tabIndex","tabIndex",e=>e==null?void 0:Te(e)],color:"color",disabledInteractive:[2,"disabledInteractive","disabledInteractive",_],checked:[2,"checked","checked",_],disabled:[2,"disabled","disabled",_],indeterminate:[2,"indeterminate","indeterminate",_]},outputs:{change:"change",indeterminateChange:"indeterminateChange"},exportAs:["matCheckbox"],features:[U([{provide:ei,useExisting:Sn(()=>i),multi:!0},{provide:ti,useExisting:i,multi:!0}]),qe],ngContentSelectors:dr,decls:15,vars:23,consts:[["checkbox",""],["input",""],["label",""],["mat-internal-form-field","",3,"click","labelPosition"],[1,"mdc-checkbox"],["aria-hidden","true",1,"mat-mdc-checkbox-touch-target",3,"click"],["type","checkbox",1,"mdc-checkbox__native-control",3,"blur","click","change","checked","indeterminate","disabled","id","required","tabIndex"],["aria-hidden","true",1,"mdc-checkbox__ripple"],["aria-hidden","true",1,"mdc-checkbox__background"],["focusable","false","viewBox","0 0 24 24",1,"mdc-checkbox__checkmark"],["fill","none","d","M1.73,12.91 8.1,19.28 22.79,4.59",1,"mdc-checkbox__checkmark-path"],[1,"mdc-checkbox__mixedmark"],["mat-ripple","","aria-hidden","true",1,"mat-mdc-checkbox-ripple","mat-focus-indicator",3,"matRippleTrigger","matRippleDisabled","matRippleCentered"],[1,"mdc-label",3,"for"]],template:function(t,n){if(t&1&&(H(),c(0,"div",3),k("click",function(o){return n._preventBubblingFromLabel(o)}),c(1,"div",4,0)(3,"div",5),k("click",function(){return n._onTouchTargetClick()}),u(),c(4,"input",6,1),k("blur",function(){return n._onBlur()})("click",function(){return n._onInputClick()})("change",function(o){return n._onInteractionEvent(o)}),u(),A(6,"div",7),c(7,"div",8),Ue(),c(8,"svg",9),A(9,"path",10),u(),Dn(),A(10,"div",11),u(),A(11,"div",12),u(),c(12,"label",13,2),w(14),u()()),t&2){let a=we(2);E("labelPosition",n.labelPosition),d(4),p("mdc-checkbox--selected",n.checked),E("checked",n.checked)("indeterminate",n.indeterminate)("disabled",n.disabled&&!n.disabledInteractive)("id",n.inputId)("required",n.required)("tabIndex",n.disabled&&!n.disabledInteractive?-1:n.tabIndex),T("aria-label",n.ariaLabel||null)("aria-labelledby",n.ariaLabelledby)("aria-describedby",n.ariaDescribedby)("aria-checked",n.indeterminate?"mixed":null)("aria-controls",n.ariaControls)("aria-disabled",n.disabled&&n.disabledInteractive?!0:null)("aria-expanded",n.ariaExpanded)("aria-owns",n.ariaOwns)("name",n.name)("value",n.value),d(7),E("matRippleTrigger",a)("matRippleDisabled",n.disableRipple||n.disabled)("matRippleCentered",!0),d(),E("for",n.inputId)}},dependencies:[he,aa],styles:[`.mdc-checkbox { + display: inline-block; + position: relative; + flex: 0 0 18px; + box-sizing: content-box; + width: 18px; + height: 18px; + line-height: 0; + white-space: nowrap; + cursor: pointer; + vertical-align: bottom; + padding: calc((var(--mat-checkbox-state-layer-size, 40px) - 18px) / 2); + margin: calc((var(--mat-checkbox-state-layer-size, 40px) - var(--mat-checkbox-state-layer-size, 40px)) / 2); +} +.mdc-checkbox:hover > .mdc-checkbox__ripple { + opacity: var(--mat-checkbox-unselected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); + background-color: var(--mat-checkbox-unselected-hover-state-layer-color, var(--mat-sys-on-surface)); +} +.mdc-checkbox:hover > .mat-mdc-checkbox-ripple > .mat-ripple-element { + background-color: var(--mat-checkbox-unselected-hover-state-layer-color, var(--mat-sys-on-surface)); +} +.mdc-checkbox .mdc-checkbox__native-control:focus + .mdc-checkbox__ripple { + opacity: var(--mat-checkbox-unselected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); + background-color: var(--mat-checkbox-unselected-focus-state-layer-color, var(--mat-sys-on-surface)); +} +.mdc-checkbox .mdc-checkbox__native-control:focus ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + background-color: var(--mat-checkbox-unselected-focus-state-layer-color, var(--mat-sys-on-surface)); +} +.mdc-checkbox:active > .mdc-checkbox__native-control + .mdc-checkbox__ripple { + opacity: var(--mat-checkbox-unselected-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); + background-color: var(--mat-checkbox-unselected-pressed-state-layer-color, var(--mat-sys-primary)); +} +.mdc-checkbox:active > .mdc-checkbox__native-control ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + background-color: var(--mat-checkbox-unselected-pressed-state-layer-color, var(--mat-sys-primary)); +} +.mdc-checkbox:hover > .mdc-checkbox__native-control:checked + .mdc-checkbox__ripple { + opacity: var(--mat-checkbox-selected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); + background-color: var(--mat-checkbox-selected-hover-state-layer-color, var(--mat-sys-primary)); +} +.mdc-checkbox:hover > .mdc-checkbox__native-control:checked ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + background-color: var(--mat-checkbox-selected-hover-state-layer-color, var(--mat-sys-primary)); +} +.mdc-checkbox .mdc-checkbox__native-control:focus:checked + .mdc-checkbox__ripple { + opacity: var(--mat-checkbox-selected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); + background-color: var(--mat-checkbox-selected-focus-state-layer-color, var(--mat-sys-primary)); +} +.mdc-checkbox .mdc-checkbox__native-control:focus:checked ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + background-color: var(--mat-checkbox-selected-focus-state-layer-color, var(--mat-sys-primary)); +} +.mdc-checkbox:active > .mdc-checkbox__native-control:checked + .mdc-checkbox__ripple { + opacity: var(--mat-checkbox-selected-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); + background-color: var(--mat-checkbox-selected-pressed-state-layer-color, var(--mat-sys-on-surface)); +} +.mdc-checkbox:active > .mdc-checkbox__native-control:checked ~ .mat-mdc-checkbox-ripple .mat-ripple-element { + background-color: var(--mat-checkbox-selected-pressed-state-layer-color, var(--mat-sys-on-surface)); +} +.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox .mdc-checkbox__native-control ~ .mat-mdc-checkbox-ripple .mat-ripple-element, +.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox .mdc-checkbox__native-control + .mdc-checkbox__ripple { + background-color: var(--mat-checkbox-unselected-hover-state-layer-color, var(--mat-sys-on-surface)); +} +.mdc-checkbox .mdc-checkbox__native-control { + position: absolute; + margin: 0; + padding: 0; + opacity: 0; + cursor: inherit; + z-index: 1; + width: var(--mat-checkbox-state-layer-size, 40px); + height: var(--mat-checkbox-state-layer-size, 40px); + top: calc((var(--mat-checkbox-state-layer-size, 40px) - var(--mat-checkbox-state-layer-size, 40px)) / 2); + right: calc((var(--mat-checkbox-state-layer-size, 40px) - var(--mat-checkbox-state-layer-size, 40px)) / 2); + left: calc((var(--mat-checkbox-state-layer-size, 40px) - var(--mat-checkbox-state-layer-size, 40px)) / 2); +} + +.mdc-checkbox--disabled { + cursor: default; + pointer-events: none; +} + +.mdc-checkbox__background { + display: inline-flex; + position: absolute; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: 18px; + height: 18px; + border: 2px solid currentColor; + border-radius: 2px; + background-color: transparent; + pointer-events: none; + will-change: background-color, border-color; + transition: background-color 90ms cubic-bezier(0.4, 0, 0.6, 1), border-color 90ms cubic-bezier(0.4, 0, 0.6, 1); + -webkit-print-color-adjust: exact; + color-adjust: exact; + border-color: var(--mat-checkbox-unselected-icon-color, var(--mat-sys-on-surface-variant)); + top: calc((var(--mat-checkbox-state-layer-size, 40px) - 18px) / 2); + left: calc((var(--mat-checkbox-state-layer-size, 40px) - 18px) / 2); +} + +.mdc-checkbox__native-control:enabled:checked ~ .mdc-checkbox__background, +.mdc-checkbox__native-control:enabled:indeterminate ~ .mdc-checkbox__background { + border-color: var(--mat-checkbox-selected-icon-color, var(--mat-sys-primary)); + background-color: var(--mat-checkbox-selected-icon-color, var(--mat-sys-primary)); +} + +.mdc-checkbox--disabled .mdc-checkbox__background { + border-color: var(--mat-checkbox-disabled-unselected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +@media (forced-colors: active) { + .mdc-checkbox--disabled .mdc-checkbox__background { + border-color: GrayText; + } +} + +.mdc-checkbox__native-control:disabled:checked ~ .mdc-checkbox__background, +.mdc-checkbox__native-control:disabled:indeterminate ~ .mdc-checkbox__background { + background-color: var(--mat-checkbox-disabled-selected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); + border-color: transparent; +} +@media (forced-colors: active) { + .mdc-checkbox__native-control:disabled:checked ~ .mdc-checkbox__background, + .mdc-checkbox__native-control:disabled:indeterminate ~ .mdc-checkbox__background { + border-color: GrayText; + } +} + +.mdc-checkbox:hover > .mdc-checkbox__native-control:not(:checked) ~ .mdc-checkbox__background, +.mdc-checkbox:hover > .mdc-checkbox__native-control:not(:indeterminate) ~ .mdc-checkbox__background { + border-color: var(--mat-checkbox-unselected-hover-icon-color, var(--mat-sys-on-surface)); + background-color: transparent; +} + +.mdc-checkbox:hover > .mdc-checkbox__native-control:checked ~ .mdc-checkbox__background, +.mdc-checkbox:hover > .mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background { + border-color: var(--mat-checkbox-selected-hover-icon-color, var(--mat-sys-primary)); + background-color: var(--mat-checkbox-selected-hover-icon-color, var(--mat-sys-primary)); +} + +.mdc-checkbox__native-control:focus:focus:not(:checked) ~ .mdc-checkbox__background, +.mdc-checkbox__native-control:focus:focus:not(:indeterminate) ~ .mdc-checkbox__background { + border-color: var(--mat-checkbox-unselected-focus-icon-color, var(--mat-sys-on-surface)); +} + +.mdc-checkbox__native-control:focus:focus:checked ~ .mdc-checkbox__background, +.mdc-checkbox__native-control:focus:focus:indeterminate ~ .mdc-checkbox__background { + border-color: var(--mat-checkbox-selected-focus-icon-color, var(--mat-sys-primary)); + background-color: var(--mat-checkbox-selected-focus-icon-color, var(--mat-sys-primary)); +} + +.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox:hover > .mdc-checkbox__native-control ~ .mdc-checkbox__background, +.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox .mdc-checkbox__native-control:focus ~ .mdc-checkbox__background, +.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__background { + border-color: var(--mat-checkbox-disabled-unselected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +@media (forced-colors: active) { + .mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox:hover > .mdc-checkbox__native-control ~ .mdc-checkbox__background, + .mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox .mdc-checkbox__native-control:focus ~ .mdc-checkbox__background, + .mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__background { + border-color: GrayText; + } +} +.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__native-control:checked ~ .mdc-checkbox__background, +.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background { + background-color: var(--mat-checkbox-disabled-selected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); + border-color: transparent; +} + +.mdc-checkbox__checkmark { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + opacity: 0; + transition: opacity 180ms cubic-bezier(0.4, 0, 0.6, 1); + color: var(--mat-checkbox-selected-checkmark-color, var(--mat-sys-on-primary)); +} +@media (forced-colors: active) { + .mdc-checkbox__checkmark { + color: CanvasText; + } +} + +.mdc-checkbox--disabled .mdc-checkbox__checkmark, .mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__checkmark { + color: var(--mat-checkbox-disabled-selected-checkmark-color, var(--mat-sys-surface)); +} +@media (forced-colors: active) { + .mdc-checkbox--disabled .mdc-checkbox__checkmark, .mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__checkmark { + color: GrayText; + } +} + +.mdc-checkbox__checkmark-path { + transition: stroke-dashoffset 180ms cubic-bezier(0.4, 0, 0.6, 1); + stroke: currentColor; + stroke-width: 3.12px; + stroke-dashoffset: 29.7833385; + stroke-dasharray: 29.7833385; +} + +.mdc-checkbox__mixedmark { + width: 100%; + height: 0; + transform: scaleX(0) rotate(0deg); + border-width: 1px; + border-style: solid; + opacity: 0; + transition: opacity 90ms cubic-bezier(0.4, 0, 0.6, 1), transform 90ms cubic-bezier(0.4, 0, 0.6, 1); + border-color: var(--mat-checkbox-selected-checkmark-color, var(--mat-sys-on-primary)); +} +@media (forced-colors: active) { + .mdc-checkbox__mixedmark { + margin: 0 1px; + } +} + +.mdc-checkbox--disabled .mdc-checkbox__mixedmark, .mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__mixedmark { + border-color: var(--mat-checkbox-disabled-selected-checkmark-color, var(--mat-sys-surface)); +} +@media (forced-colors: active) { + .mdc-checkbox--disabled .mdc-checkbox__mixedmark, .mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive .mdc-checkbox__mixedmark { + border-color: GrayText; + } +} + +.mdc-checkbox--anim-unchecked-checked .mdc-checkbox__background, +.mdc-checkbox--anim-unchecked-indeterminate .mdc-checkbox__background, +.mdc-checkbox--anim-checked-unchecked .mdc-checkbox__background, +.mdc-checkbox--anim-indeterminate-unchecked .mdc-checkbox__background { + animation-duration: 180ms; + animation-timing-function: linear; +} + +.mdc-checkbox--anim-unchecked-checked .mdc-checkbox__checkmark-path { + animation: mdc-checkbox-unchecked-checked-checkmark-path 180ms linear; + transition: none; +} + +.mdc-checkbox--anim-unchecked-indeterminate .mdc-checkbox__mixedmark { + animation: mdc-checkbox-unchecked-indeterminate-mixedmark 90ms linear; + transition: none; +} + +.mdc-checkbox--anim-checked-unchecked .mdc-checkbox__checkmark-path { + animation: mdc-checkbox-checked-unchecked-checkmark-path 90ms linear; + transition: none; +} + +.mdc-checkbox--anim-checked-indeterminate .mdc-checkbox__checkmark { + animation: mdc-checkbox-checked-indeterminate-checkmark 90ms linear; + transition: none; +} +.mdc-checkbox--anim-checked-indeterminate .mdc-checkbox__mixedmark { + animation: mdc-checkbox-checked-indeterminate-mixedmark 90ms linear; + transition: none; +} + +.mdc-checkbox--anim-indeterminate-checked .mdc-checkbox__checkmark { + animation: mdc-checkbox-indeterminate-checked-checkmark 500ms linear; + transition: none; +} +.mdc-checkbox--anim-indeterminate-checked .mdc-checkbox__mixedmark { + animation: mdc-checkbox-indeterminate-checked-mixedmark 500ms linear; + transition: none; +} + +.mdc-checkbox--anim-indeterminate-unchecked .mdc-checkbox__mixedmark { + animation: mdc-checkbox-indeterminate-unchecked-mixedmark 300ms linear; + transition: none; +} + +.mdc-checkbox__native-control:checked ~ .mdc-checkbox__background, +.mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background { + transition: border-color 90ms cubic-bezier(0, 0, 0.2, 1), background-color 90ms cubic-bezier(0, 0, 0.2, 1); +} +.mdc-checkbox__native-control:checked ~ .mdc-checkbox__background > .mdc-checkbox__checkmark > .mdc-checkbox__checkmark-path, +.mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background > .mdc-checkbox__checkmark > .mdc-checkbox__checkmark-path { + stroke-dashoffset: 0; +} + +.mdc-checkbox__native-control:checked ~ .mdc-checkbox__background > .mdc-checkbox__checkmark { + transition: opacity 180ms cubic-bezier(0, 0, 0.2, 1), transform 180ms cubic-bezier(0, 0, 0.2, 1); + opacity: 1; +} +.mdc-checkbox__native-control:checked ~ .mdc-checkbox__background > .mdc-checkbox__mixedmark { + transform: scaleX(1) rotate(-45deg); +} + +.mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background > .mdc-checkbox__checkmark { + transform: rotate(45deg); + opacity: 0; + transition: opacity 90ms cubic-bezier(0.4, 0, 0.6, 1), transform 90ms cubic-bezier(0.4, 0, 0.6, 1); +} +.mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background > .mdc-checkbox__mixedmark { + transform: scaleX(1) rotate(0deg); + opacity: 1; +} + +@keyframes mdc-checkbox-unchecked-checked-checkmark-path { + 0%, 50% { + stroke-dashoffset: 29.7833385; + } + 50% { + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } + 100% { + stroke-dashoffset: 0; + } +} +@keyframes mdc-checkbox-unchecked-indeterminate-mixedmark { + 0%, 68.2% { + transform: scaleX(0); + } + 68.2% { + animation-timing-function: cubic-bezier(0, 0, 0, 1); + } + 100% { + transform: scaleX(1); + } +} +@keyframes mdc-checkbox-checked-unchecked-checkmark-path { + from { + animation-timing-function: cubic-bezier(0.4, 0, 1, 1); + opacity: 1; + stroke-dashoffset: 0; + } + to { + opacity: 0; + stroke-dashoffset: -29.7833385; + } +} +@keyframes mdc-checkbox-checked-indeterminate-checkmark { + from { + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + transform: rotate(0deg); + opacity: 1; + } + to { + transform: rotate(45deg); + opacity: 0; + } +} +@keyframes mdc-checkbox-indeterminate-checked-checkmark { + from { + animation-timing-function: cubic-bezier(0.14, 0, 0, 1); + transform: rotate(45deg); + opacity: 0; + } + to { + transform: rotate(360deg); + opacity: 1; + } +} +@keyframes mdc-checkbox-checked-indeterminate-mixedmark { + from { + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + transform: rotate(-45deg); + opacity: 0; + } + to { + transform: rotate(0deg); + opacity: 1; + } +} +@keyframes mdc-checkbox-indeterminate-checked-mixedmark { + from { + animation-timing-function: cubic-bezier(0.14, 0, 0, 1); + transform: rotate(0deg); + opacity: 1; + } + to { + transform: rotate(315deg); + opacity: 0; + } +} +@keyframes mdc-checkbox-indeterminate-unchecked-mixedmark { + 0% { + animation-timing-function: linear; + transform: scaleX(1); + opacity: 1; + } + 32.8%, 100% { + transform: scaleX(0); + opacity: 0; + } +} +.mat-mdc-checkbox { + display: inline-block; + position: relative; + -webkit-tap-highlight-color: transparent; +} +.mat-mdc-checkbox._mat-animation-noopable > .mat-internal-form-field > .mdc-checkbox > .mat-mdc-checkbox-touch-target, +.mat-mdc-checkbox._mat-animation-noopable > .mat-internal-form-field > .mdc-checkbox > .mdc-checkbox__native-control, +.mat-mdc-checkbox._mat-animation-noopable > .mat-internal-form-field > .mdc-checkbox > .mdc-checkbox__ripple, +.mat-mdc-checkbox._mat-animation-noopable > .mat-internal-form-field > .mdc-checkbox > .mat-mdc-checkbox-ripple::before, +.mat-mdc-checkbox._mat-animation-noopable > .mat-internal-form-field > .mdc-checkbox > .mdc-checkbox__background, +.mat-mdc-checkbox._mat-animation-noopable > .mat-internal-form-field > .mdc-checkbox > .mdc-checkbox__background > .mdc-checkbox__checkmark, +.mat-mdc-checkbox._mat-animation-noopable > .mat-internal-form-field > .mdc-checkbox > .mdc-checkbox__background > .mdc-checkbox__checkmark > .mdc-checkbox__checkmark-path, +.mat-mdc-checkbox._mat-animation-noopable > .mat-internal-form-field > .mdc-checkbox > .mdc-checkbox__background > .mdc-checkbox__mixedmark { + transition: none !important; + animation: none !important; +} +.mat-mdc-checkbox label { + cursor: pointer; +} +.mat-mdc-checkbox .mat-internal-form-field { + color: var(--mat-checkbox-label-text-color, var(--mat-sys-on-surface)); + font-family: var(--mat-checkbox-label-text-font, var(--mat-sys-body-medium-font)); + line-height: var(--mat-checkbox-label-text-line-height, var(--mat-sys-body-medium-line-height)); + font-size: var(--mat-checkbox-label-text-size, var(--mat-sys-body-medium-size)); + letter-spacing: var(--mat-checkbox-label-text-tracking, var(--mat-sys-body-medium-tracking)); + font-weight: var(--mat-checkbox-label-text-weight, var(--mat-sys-body-medium-weight)); +} +.mat-mdc-checkbox.mat-mdc-checkbox-disabled.mat-mdc-checkbox-disabled-interactive { + pointer-events: auto; +} +.mat-mdc-checkbox.mat-mdc-checkbox-disabled.mat-mdc-checkbox-disabled-interactive input { + cursor: default; +} +.mat-mdc-checkbox.mat-mdc-checkbox-disabled label { + cursor: default; + color: var(--mat-checkbox-disabled-label-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +@media (forced-colors: active) { + .mat-mdc-checkbox.mat-mdc-checkbox-disabled label { + color: GrayText; + } +} +.mat-mdc-checkbox label:empty { + display: none; +} +.mat-mdc-checkbox .mdc-checkbox__ripple { + opacity: 0; +} + +.mat-mdc-checkbox .mat-mdc-checkbox-ripple, +.mdc-checkbox__ripple { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + border-radius: 50%; + pointer-events: none; +} +.mat-mdc-checkbox .mat-mdc-checkbox-ripple:not(:empty), +.mdc-checkbox__ripple:not(:empty) { + transform: translateZ(0); +} + +.mat-mdc-checkbox-ripple .mat-ripple-element { + opacity: 0.1; +} + +.mat-mdc-checkbox-touch-target { + position: absolute; + top: 50%; + left: 50%; + height: var(--mat-checkbox-touch-target-size, 48px); + width: var(--mat-checkbox-touch-target-size, 48px); + transform: translate(-50%, -50%); + display: var(--mat-checkbox-touch-target-display, block); +} + +.mat-mdc-checkbox .mat-mdc-checkbox-ripple::before { + border-radius: 50%; +} + +.mdc-checkbox__native-control:focus-visible ~ .mat-focus-indicator::before { + content: ""; +} +`],encapsulation:2,changeDetection:0})}return i})(),Yd=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[hr,j]})}return i})();var at=["*"];function ur(i,s){i&1&&w(0)}var la=["tabListContainer"],ca=["tabList"],da=["tabListInner"],ma=["nextPaginator"],ha=["previousPaginator"],fr=["content"];function pr(i,s){}var br=["tabBodyWrapper"],_r=["tabHeader"];function gr(i,s){}function vr(i,s){if(i&1&&ge(0,gr,0,0,"ng-template",12),i&2){let e=y().$implicit;E("cdkPortalOutlet",e.templateLabel)}}function yr(i,s){if(i&1&&$(0),i&2){let e=y().$implicit;se(e.textLabel)}}function xr(i,s){if(i&1){let e=Se();c(0,"div",7,2),k("click",function(){let n=ne(e),a=n.$implicit,o=n.$index,l=y(),v=we(1);return ie(l._handleClick(a,v,o))})("cdkFocusChange",function(n){let a=ne(e).$index,o=y();return ie(o._tabFocusChanged(n,a))}),A(2,"span",8)(3,"div",9),c(4,"span",10)(5,"span",11),F(6,vr,1,1,null,12)(7,yr,1,1),u()()()}if(i&2){let e=s.$implicit,t=s.$index,n=we(1),a=y();de(e.labelClass),p("mdc-tab--active",a.selectedIndex===t),E("id",a._getTabLabelId(e,t))("disabled",e.disabled)("fitInkBarToContent",a.fitInkBarToContent),T("tabIndex",a._getTabIndex(t))("aria-posinset",t+1)("aria-setsize",a._tabs.length)("aria-controls",a._getTabContentId(t))("aria-selected",a.selectedIndex===t)("aria-label",e.ariaLabel||null)("aria-labelledby",!e.ariaLabel&&e.ariaLabelledby?e.ariaLabelledby:null),d(3),E("matRippleTrigger",n)("matRippleDisabled",e.disabled||a.disableRipple),d(3),O(e.templateLabel?6:7)}}function kr(i,s){i&1&&w(0)}function Cr(i,s){if(i&1){let e=Se();c(0,"mat-tab-body",13),k("_onCentered",function(){ne(e);let n=y();return ie(n._removeTabBodyWrapperHeight())})("_onCentering",function(n){ne(e);let a=y();return ie(a._setTabBodyWrapperHeight(n))})("_beforeCentering",function(n){ne(e);let a=y();return ie(a._bodyCentered(n))}),u()}if(i&2){let e=s.$implicit,t=s.$index,n=y();de(e.bodyClass),E("id",n._getTabContentId(t))("content",e.content)("position",e.position)("animationDuration",n.animationDuration)("preserveContent",n.preserveContent),T("tabindex",n.contentTabIndex!=null&&n.selectedIndex===t?n.contentTabIndex:null)("aria-labelledby",n._getTabLabelId(e,t))("aria-hidden",n.selectedIndex!==t)}}var wr=["mat-tab-nav-bar",""],Tr=["mat-tab-link",""],Mr=new C("MatTabContent"),Ir=(()=>{class i{template=r(Ie);constructor(){}static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["","matTabContent",""]],features:[U([{provide:Mr,useExisting:i}])]})}return i})(),Sr=new C("MatTabLabel"),ua=new C("MAT_TAB"),Dr=(()=>{class i extends Kn{_closestTab=r(ua,{optional:!0});static \u0275fac=(()=>{let e;return function(n){return(e||(e=Le(i)))(n||i)}})();static \u0275dir=I({type:i,selectors:[["","mat-tab-label",""],["","matTabLabel",""]],features:[U([{provide:Sr,useExisting:i}]),_e]})}return i})(),fa=new C("MAT_TAB_GROUP"),Er=(()=>{class i{_viewContainerRef=r(He);_closestTabGroup=r(fa,{optional:!0});disabled=!1;get templateLabel(){return this._templateLabel}set templateLabel(e){this._setTemplateLabelInput(e)}_templateLabel;_explicitContent=void 0;_implicitContent;textLabel="";ariaLabel;ariaLabelledby;labelClass;bodyClass;id=null;_contentPortal=null;get content(){return this._contentPortal}_stateChanges=new f;position=null;origin=null;isActive=!1;constructor(){r(De).load(Re)}ngOnChanges(e){(e.hasOwnProperty("textLabel")||e.hasOwnProperty("disabled"))&&this._stateChanges.next()}ngOnDestroy(){this._stateChanges.complete()}ngOnInit(){this._contentPortal=new je(this._explicitContent||this._implicitContent,this._viewContainerRef)}_setTemplateLabelInput(e){e&&e._closestTab===this&&(this._templateLabel=e)}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-tab"]],contentQueries:function(t,n,a){if(t&1&&ve(a,Dr,5)(a,Ir,7,Ie),t&2){let o;m(o=h())&&(n.templateLabel=o.first),m(o=h())&&(n._explicitContent=o.first)}},viewQuery:function(t,n){if(t&1&&W(Ie,7),t&2){let a;m(a=h())&&(n._implicitContent=a.first)}},hostAttrs:["hidden",""],hostVars:1,hostBindings:function(t,n){t&2&&T("id",null)},inputs:{disabled:[2,"disabled","disabled",_],textLabel:[0,"label","textLabel"],ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],labelClass:"labelClass",bodyClass:"bodyClass",id:"id"},exportAs:["matTab"],features:[U([{provide:ua,useExisting:i}]),qe],ngContentSelectors:at,decls:1,vars:0,template:function(t,n){t&1&&(H(),pt(0,ur,1,0,"ng-template"))},encapsulation:2})}return i})(),un="mdc-tab-indicator--active",oa="mdc-tab-indicator--no-transition",Pt=class{_items;_currentItem;constructor(s){this._items=s}hide(){this._items.forEach(s=>s.deactivateInkBar()),this._currentItem=void 0}alignToElement(s){let e=this._items.find(n=>n.elementRef.nativeElement===s),t=this._currentItem;if(e!==t&&(t?.deactivateInkBar(),e)){let n=t?.elementRef.nativeElement.getBoundingClientRect?.();e.activateInkBar(n),this._currentItem=e}}},pa=(()=>{class i{_elementRef=r(D);_inkBarElement=null;_inkBarContentElement=null;_fitToContent=!1;get fitInkBarToContent(){return this._fitToContent}set fitInkBarToContent(e){this._fitToContent!==e&&(this._fitToContent=e,this._inkBarElement&&this._appendInkBarElement())}activateInkBar(e){let t=this._elementRef.nativeElement;if(!e||!t.getBoundingClientRect||!this._inkBarContentElement){t.classList.add(un);return}let n=t.getBoundingClientRect(),a=e.width/n.width,o=e.left-n.left;t.classList.add(oa),this._inkBarContentElement.style.setProperty("transform",`translateX(${o}px) scaleX(${a})`),t.getBoundingClientRect(),t.classList.remove(oa),t.classList.add(un),this._inkBarContentElement.style.setProperty("transform","")}deactivateInkBar(){this._elementRef.nativeElement.classList.remove(un)}ngOnInit(){this._createInkBarElement()}ngOnDestroy(){this._inkBarElement?.remove(),this._inkBarElement=this._inkBarContentElement=null}_createInkBarElement(){let e=this._elementRef.nativeElement.ownerDocument||document,t=this._inkBarElement=e.createElement("span"),n=this._inkBarContentElement=e.createElement("span");t.className="mdc-tab-indicator",n.className="mdc-tab-indicator__content mdc-tab-indicator__content--underline",t.appendChild(this._inkBarContentElement),this._appendInkBarElement()}_appendInkBarElement(){this._inkBarElement;let e=this._fitToContent?this._elementRef.nativeElement.querySelector(".mdc-tab__content"):this._elementRef.nativeElement;e.appendChild(this._inkBarElement)}static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,inputs:{fitInkBarToContent:[2,"fitInkBarToContent","fitInkBarToContent",_]}})}return i})();var ba=(()=>{class i extends pa{elementRef=r(D);disabled=!1;focus(){this.elementRef.nativeElement.focus()}getOffsetLeft(){return this.elementRef.nativeElement.offsetLeft}getOffsetWidth(){return this.elementRef.nativeElement.offsetWidth}static \u0275fac=(()=>{let e;return function(n){return(e||(e=Le(i)))(n||i)}})();static \u0275dir=I({type:i,selectors:[["","matTabLabelWrapper",""]],hostVars:3,hostBindings:function(t,n){t&2&&(T("aria-disabled",!!n.disabled),p("mat-mdc-tab-disabled",n.disabled))},inputs:{disabled:[2,"disabled","disabled",_]},features:[_e]})}return i})(),ra={passive:!0},Rr=650,Fr=100,_a=(()=>{class i{_elementRef=r(D);_changeDetectorRef=r(Y);_viewportRuler=r(lt);_dir=r(xe,{optional:!0});_ngZone=r(z);_platform=r(Ee);_sharedResizeObserver=r(Ft);_injector=r(X);_renderer=r(ae);_animationsDisabled=q();_eventCleanups;_scrollDistance=0;_selectedIndexChanged=!1;_destroyed=new f;_showPaginationControls=!1;_disableScrollAfter=!0;_disableScrollBefore=!0;_tabLabelCount;_scrollDistanceChanged=!1;_keyManager;_currentTextContent;_stopScrolling=new f;disablePagination=!1;get selectedIndex(){return this._selectedIndex}set selectedIndex(e){let t=isNaN(e)?0:e;this._selectedIndex!=t&&(this._selectedIndexChanged=!0,this._selectedIndex=t,this._keyManager&&this._keyManager.updateActiveItem(t))}_selectedIndex=0;selectFocusedIndex=new g;indexFocused=new g;constructor(){this._eventCleanups=this._ngZone.runOutsideAngular(()=>[this._renderer.listen(this._elementRef.nativeElement,"mouseleave",()=>this._stopInterval())])}ngAfterViewInit(){this._eventCleanups.push(this._renderer.listen(this._previousPaginator.nativeElement,"touchstart",()=>this._handlePaginatorPress("before"),ra),this._renderer.listen(this._nextPaginator.nativeElement,"touchstart",()=>this._handlePaginatorPress("after"),ra))}ngAfterContentInit(){let e=this._dir?this._dir.change:ze("ltr"),t=this._sharedResizeObserver.observe(this._elementRef.nativeElement).pipe(wn(32),x(this._destroyed)),n=this._viewportRuler.change(150).pipe(x(this._destroyed)),a=()=>{this.updatePagination(),this._alignInkBarToSelectedTab()};this._keyManager=new yt(this._items).withHorizontalOrientation(this._getLayoutDirection()).withHomeAndEnd().withWrap().skipPredicate(()=>!1),this._keyManager.updateActiveItem(Math.max(this._selectedIndex,0)),ce(a,{injector:this._injector}),G(e,n,t,this._items.changes,this._itemsResized()).pipe(x(this._destroyed)).subscribe(()=>{this._ngZone.run(()=>{Promise.resolve().then(()=>{this._scrollDistance=Math.max(0,Math.min(this._getMaxScrollDistance(),this._scrollDistance)),a()})}),this._keyManager?.withHorizontalOrientation(this._getLayoutDirection())}),this._keyManager.change.subscribe(o=>{this.indexFocused.emit(o),this._setTabFocus(o)})}_itemsResized(){return typeof ResizeObserver!="function"?vn:this._items.changes.pipe(te(this._items),Ae(e=>new Oe(t=>this._ngZone.runOutsideAngular(()=>{let n=new ResizeObserver(a=>t.next(a));return e.forEach(a=>n.observe(a.elementRef.nativeElement)),()=>{n.disconnect()}}))),Mn(1),Z(e=>e.some(t=>t.contentRect.width>0&&t.contentRect.height>0)))}ngAfterContentChecked(){this._tabLabelCount!=this._items.length&&(this.updatePagination(),this._tabLabelCount=this._items.length,this._changeDetectorRef.markForCheck()),this._selectedIndexChanged&&(this._scrollToLabel(this._selectedIndex),this._checkScrollingControls(),this._alignInkBarToSelectedTab(),this._selectedIndexChanged=!1,this._changeDetectorRef.markForCheck()),this._scrollDistanceChanged&&(this._updateTabScrollPosition(),this._scrollDistanceChanged=!1,this._changeDetectorRef.markForCheck())}ngOnDestroy(){this._eventCleanups.forEach(e=>e()),this._keyManager?.destroy(),this._destroyed.next(),this._destroyed.complete(),this._stopScrolling.complete()}_handleKeydown(e){if(!me(e))switch(e.keyCode){case 13:case 32:if(this.focusIndex!==this.selectedIndex){let t=this._items.get(this.focusIndex);t&&!t.disabled&&(this.selectFocusedIndex.emit(this.focusIndex),this._itemSelected(e))}break;default:this._keyManager?.onKeydown(e)}}_onContentChanges(){let e=this._elementRef.nativeElement.textContent;e!==this._currentTextContent&&(this._currentTextContent=e||"",this._ngZone.run(()=>{this.updatePagination(),this._alignInkBarToSelectedTab(),this._changeDetectorRef.markForCheck()}))}updatePagination(){this._checkPaginationEnabled(),this._checkScrollingControls(),this._updateTabScrollPosition()}get focusIndex(){return this._keyManager?this._keyManager.activeItemIndex:0}set focusIndex(e){!this._isValidIndex(e)||this.focusIndex===e||!this._keyManager||this._keyManager.setActiveItem(e)}_isValidIndex(e){return this._items?!!this._items.toArray()[e]:!0}_setTabFocus(e){if(this._showPaginationControls&&this._scrollToLabel(e),this._items&&this._items.length){this._items.toArray()[e].focus();let t=this._tabListContainer.nativeElement;this._getLayoutDirection()=="ltr"?t.scrollLeft=0:t.scrollLeft=t.scrollWidth-t.offsetWidth}}_getLayoutDirection(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}_updateTabScrollPosition(){if(this.disablePagination)return;let e=this.scrollDistance,t=this._getLayoutDirection()==="ltr"?-e:e;this._tabList.nativeElement.style.transform=`translateX(${Math.round(t)}px)`,(this._platform.TRIDENT||this._platform.EDGE)&&(this._tabListContainer.nativeElement.scrollLeft=0)}get scrollDistance(){return this._scrollDistance}set scrollDistance(e){this._scrollTo(e)}_scrollHeader(e){let t=this._tabListContainer.nativeElement.offsetWidth,n=(e=="before"?-1:1)*t/3;return this._scrollTo(this._scrollDistance+n)}_handlePaginatorClick(e){this._stopInterval(),this._scrollHeader(e)}_scrollToLabel(e){if(this.disablePagination)return;let t=this._items?this._items.toArray()[e]:null;if(!t)return;let n=this._tabListContainer.nativeElement.offsetWidth,{offsetLeft:a,offsetWidth:o}=t.elementRef.nativeElement,l,v;this._getLayoutDirection()=="ltr"?(l=a,v=l+o):(v=this._tabListInner.nativeElement.offsetWidth-a,l=v-o);let b=this.scrollDistance,V=this.scrollDistance+n;lV&&(this.scrollDistance+=Math.min(v-V,l-b))}_checkPaginationEnabled(){if(this.disablePagination)this._showPaginationControls=!1;else{let e=this._tabListInner.nativeElement.scrollWidth,t=this._elementRef.nativeElement.offsetWidth,n=e-t>=5;n||(this.scrollDistance=0),n!==this._showPaginationControls&&(this._showPaginationControls=n,this._changeDetectorRef.markForCheck())}}_checkScrollingControls(){this.disablePagination?this._disableScrollAfter=this._disableScrollBefore=!0:(this._disableScrollBefore=this.scrollDistance==0,this._disableScrollAfter=this.scrollDistance==this._getMaxScrollDistance(),this._changeDetectorRef.markForCheck())}_getMaxScrollDistance(){let e=this._tabListInner.nativeElement.scrollWidth,t=this._tabListContainer.nativeElement.offsetWidth;return e-t||0}_alignInkBarToSelectedTab(){let e=this._items&&this._items.length?this._items.toArray()[this.selectedIndex]:null,t=e?e.elementRef.nativeElement:null;t?this._inkBar.alignToElement(t):this._inkBar.hide()}_stopInterval(){this._stopScrolling.next()}_handlePaginatorPress(e,t){t&&t.button!=null&&t.button!==0||(this._stopInterval(),Cn(Rr,Fr).pipe(x(G(this._stopScrolling,this._destroyed))).subscribe(()=>{let{maxScrollDistance:n,distance:a}=this._scrollHeader(e);(a===0||a>=n)&&this._stopInterval()}))}_scrollTo(e){if(this.disablePagination)return{maxScrollDistance:0,distance:0};let t=this._getMaxScrollDistance();return this._scrollDistance=Math.max(0,Math.min(t,e)),this._scrollDistanceChanged=!0,this._checkScrollingControls(),{maxScrollDistance:t,distance:this._scrollDistance}}static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,inputs:{disablePagination:[2,"disablePagination","disablePagination",_],selectedIndex:[2,"selectedIndex","selectedIndex",Te]},outputs:{selectFocusedIndex:"selectFocusedIndex",indexFocused:"indexFocused"}})}return i})(),Or=(()=>{class i extends _a{_items;_tabListContainer;_tabList;_tabListInner;_nextPaginator;_previousPaginator;_inkBar;ariaLabel;ariaLabelledby;disableRipple=!1;ngAfterContentInit(){this._inkBar=new Pt(this._items),super.ngAfterContentInit()}_itemSelected(e){e.preventDefault()}static \u0275fac=(()=>{let e;return function(n){return(e||(e=Le(i)))(n||i)}})();static \u0275cmp=M({type:i,selectors:[["mat-tab-header"]],contentQueries:function(t,n,a){if(t&1&&ve(a,ba,4),t&2){let o;m(o=h())&&(n._items=o)}},viewQuery:function(t,n){if(t&1&&W(la,7)(ca,7)(da,7)(ma,5)(ha,5),t&2){let a;m(a=h())&&(n._tabListContainer=a.first),m(a=h())&&(n._tabList=a.first),m(a=h())&&(n._tabListInner=a.first),m(a=h())&&(n._nextPaginator=a.first),m(a=h())&&(n._previousPaginator=a.first)}},hostAttrs:[1,"mat-mdc-tab-header"],hostVars:4,hostBindings:function(t,n){t&2&&p("mat-mdc-tab-header-pagination-controls-enabled",n._showPaginationControls)("mat-mdc-tab-header-rtl",n._getLayoutDirection()=="rtl")},inputs:{ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],disableRipple:[2,"disableRipple","disableRipple",_]},features:[_e],ngContentSelectors:at,decls:13,vars:10,consts:[["previousPaginator",""],["tabListContainer",""],["tabList",""],["tabListInner",""],["nextPaginator",""],["mat-ripple","",1,"mat-mdc-tab-header-pagination","mat-mdc-tab-header-pagination-before",3,"click","mousedown","touchend","matRippleDisabled"],[1,"mat-mdc-tab-header-pagination-chevron"],[1,"mat-mdc-tab-label-container",3,"keydown"],["role","tablist",1,"mat-mdc-tab-list",3,"cdkObserveContent"],[1,"mat-mdc-tab-labels"],["mat-ripple","",1,"mat-mdc-tab-header-pagination","mat-mdc-tab-header-pagination-after",3,"mousedown","click","touchend","matRippleDisabled"]],template:function(t,n){t&1&&(H(),c(0,"div",5,0),k("click",function(){return n._handlePaginatorClick("before")})("mousedown",function(o){return n._handlePaginatorPress("before",o)})("touchend",function(){return n._stopInterval()}),A(2,"div",6),u(),c(3,"div",7,1),k("keydown",function(o){return n._handleKeydown(o)}),c(5,"div",8,2),k("cdkObserveContent",function(){return n._onContentChanges()}),c(7,"div",9,3),w(9),u()()(),c(10,"div",10,4),k("mousedown",function(o){return n._handlePaginatorPress("after",o)})("click",function(){return n._handlePaginatorClick("after")})("touchend",function(){return n._stopInterval()}),A(12,"div",6),u()),t&2&&(p("mat-mdc-tab-header-pagination-disabled",n._disableScrollBefore),E("matRippleDisabled",n._disableScrollBefore||n.disableRipple),d(3),p("_mat-animation-noopable",n._animationsDisabled),d(2),T("aria-label",n.ariaLabel||null)("aria-labelledby",n.ariaLabelledby||null),d(5),p("mat-mdc-tab-header-pagination-disabled",n._disableScrollAfter),E("matRippleDisabled",n._disableScrollAfter||n.disableRipple))},dependencies:[he,qt],styles:[`.mat-mdc-tab-header { + display: flex; + overflow: hidden; + position: relative; + flex-shrink: 0; +} + +.mdc-tab-indicator .mdc-tab-indicator__content { + transition-duration: var(--mat-tab-animation-duration, 250ms); +} + +.mat-mdc-tab-header-pagination { + -webkit-user-select: none; + user-select: none; + position: relative; + display: none; + justify-content: center; + align-items: center; + min-width: 32px; + cursor: pointer; + z-index: 2; + -webkit-tap-highlight-color: transparent; + touch-action: none; + box-sizing: content-box; + outline: 0; +} +.mat-mdc-tab-header-pagination::-moz-focus-inner { + border: 0; +} +.mat-mdc-tab-header-pagination .mat-ripple-element { + opacity: 0.12; + background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-header-pagination-controls-enabled .mat-mdc-tab-header-pagination { + display: flex; +} + +.mat-mdc-tab-header-pagination-before, +.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-after { + padding-left: 4px; +} +.mat-mdc-tab-header-pagination-before .mat-mdc-tab-header-pagination-chevron, +.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-after .mat-mdc-tab-header-pagination-chevron { + transform: rotate(-135deg); +} + +.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-before, +.mat-mdc-tab-header-pagination-after { + padding-right: 4px; +} +.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-before .mat-mdc-tab-header-pagination-chevron, +.mat-mdc-tab-header-pagination-after .mat-mdc-tab-header-pagination-chevron { + transform: rotate(45deg); +} + +.mat-mdc-tab-header-pagination-chevron { + border-style: solid; + border-width: 2px 2px 0 0; + height: 8px; + width: 8px; + border-color: var(--mat-tab-pagination-icon-color, var(--mat-sys-on-surface)); +} + +.mat-mdc-tab-header-pagination-disabled { + box-shadow: none; + cursor: default; + pointer-events: none; +} +.mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron { + opacity: 0.4; +} + +.mat-mdc-tab-list { + flex-grow: 1; + position: relative; + transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1); +} +._mat-animation-noopable .mat-mdc-tab-list { + transition: none; +} + +.mat-mdc-tab-label-container { + display: flex; + flex-grow: 1; + overflow: hidden; + z-index: 1; + border-bottom-style: solid; + border-bottom-width: var(--mat-tab-divider-height, 1px); + border-bottom-color: var(--mat-tab-divider-color, var(--mat-sys-surface-variant)); +} +.mat-mdc-tab-group-inverted-header .mat-mdc-tab-label-container { + border-bottom: none; + border-top-style: solid; + border-top-width: var(--mat-tab-divider-height, 1px); + border-top-color: var(--mat-tab-divider-color, var(--mat-sys-surface-variant)); +} + +.mat-mdc-tab-labels { + display: flex; + flex: 1 0 auto; +} +[mat-align-tabs=center] > .mat-mdc-tab-header .mat-mdc-tab-labels { + justify-content: center; +} +[mat-align-tabs=end] > .mat-mdc-tab-header .mat-mdc-tab-labels { + justify-content: flex-end; +} +.cdk-drop-list .mat-mdc-tab-labels, .mat-mdc-tab-labels.cdk-drop-list { + min-height: var(--mat-tab-container-height, 48px); +} + +.mat-mdc-tab::before { + margin: 5px; +} +@media (forced-colors: active) { + .mat-mdc-tab[aria-disabled=true] { + color: GrayText; + } +} +`],encapsulation:2})}return i})(),ga=new C("MAT_TABS_CONFIG"),sa=(()=>{class i extends Ze{_host=r(fn);_ngZone=r(z);_centeringSub=re.EMPTY;_leavingSub=re.EMPTY;constructor(){super()}ngOnInit(){super.ngOnInit(),this._centeringSub=this._host._beforeCentering.pipe(te(this._host._isCenterPosition())).subscribe(e=>{this._host._content&&e&&!this.hasAttached()&&this._ngZone.run(()=>{Promise.resolve().then(),this.attach(this._host._content)})}),this._leavingSub=this._host._afterLeavingCenter.subscribe(()=>{this._host.preserveContent||this._ngZone.run(()=>this.detach())})}ngOnDestroy(){super.ngOnDestroy(),this._centeringSub.unsubscribe(),this._leavingSub.unsubscribe()}static \u0275fac=function(t){return new(t||i)};static \u0275dir=I({type:i,selectors:[["","matTabBodyHost",""]],features:[_e]})}return i})(),fn=(()=>{class i{_elementRef=r(D);_dir=r(xe,{optional:!0});_ngZone=r(z);_injector=r(X);_renderer=r(ae);_diAnimationsDisabled=q();_eventCleanups;_initialized=!1;_fallbackTimer;_positionIndex;_dirChangeSubscription=re.EMPTY;_position;_previousPosition;_onCentering=new g;_beforeCentering=new g;_afterLeavingCenter=new g;_onCentered=new g(!0);_portalHost;_contentElement;_content;animationDuration="500ms";preserveContent=!1;set position(e){this._positionIndex=e,this._computePositionAnimationState()}constructor(){if(this._dir){let e=r(Y);this._dirChangeSubscription=this._dir.change.subscribe(t=>{this._computePositionAnimationState(t),e.markForCheck()})}}ngOnInit(){this._bindTransitionEvents(),this._position==="center"&&(this._setActiveClass(!0),ce(()=>this._onCentering.emit(this._elementRef.nativeElement.clientHeight),{injector:this._injector})),this._initialized=!0}ngOnDestroy(){clearTimeout(this._fallbackTimer),this._eventCleanups?.forEach(e=>e()),this._dirChangeSubscription.unsubscribe()}_bindTransitionEvents(){this._ngZone.runOutsideAngular(()=>{let e=this._elementRef.nativeElement,t=n=>{n.target===this._contentElement?.nativeElement&&(this._elementRef.nativeElement.classList.remove("mat-tab-body-animating"),n.type==="transitionend"&&this._transitionDone())};this._eventCleanups=[this._renderer.listen(e,"transitionstart",n=>{n.target===this._contentElement?.nativeElement&&(this._elementRef.nativeElement.classList.add("mat-tab-body-animating"),this._transitionStarted())}),this._renderer.listen(e,"transitionend",t),this._renderer.listen(e,"transitioncancel",t)]})}_transitionStarted(){clearTimeout(this._fallbackTimer);let e=this._position==="center";this._beforeCentering.emit(e),e&&this._onCentering.emit(this._elementRef.nativeElement.clientHeight)}_transitionDone(){this._position==="center"?this._onCentered.emit():this._previousPosition==="center"&&this._afterLeavingCenter.emit()}_setActiveClass(e){this._elementRef.nativeElement.classList.toggle("mat-mdc-tab-body-active",e)}_getLayoutDirection(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}_isCenterPosition(){return this._positionIndex===0}_computePositionAnimationState(e=this._getLayoutDirection()){this._previousPosition=this._position,this._positionIndex<0?this._position=e=="ltr"?"left":"right":this._positionIndex>0?this._position=e=="ltr"?"right":"left":this._position="center",this._animationsDisabled()?this._simulateTransitionEvents():this._initialized&&(this._position==="center"||this._previousPosition==="center")&&(clearTimeout(this._fallbackTimer),this._fallbackTimer=this._ngZone.runOutsideAngular(()=>setTimeout(()=>this._simulateTransitionEvents(),100)))}_simulateTransitionEvents(){this._transitionStarted(),ce(()=>this._transitionDone(),{injector:this._injector})}_animationsDisabled(){return this._diAnimationsDisabled||this.animationDuration==="0ms"||this.animationDuration==="0s"}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-tab-body"]],viewQuery:function(t,n){if(t&1&&W(sa,5)(fr,5),t&2){let a;m(a=h())&&(n._portalHost=a.first),m(a=h())&&(n._contentElement=a.first)}},hostAttrs:[1,"mat-mdc-tab-body"],hostVars:1,hostBindings:function(t,n){t&2&&T("inert",n._position==="center"?null:"")},inputs:{_content:[0,"content","_content"],animationDuration:"animationDuration",preserveContent:"preserveContent",position:"position"},outputs:{_onCentering:"_onCentering",_beforeCentering:"_beforeCentering",_onCentered:"_onCentered"},decls:3,vars:6,consts:[["content",""],["cdkScrollable","",1,"mat-mdc-tab-body-content"],["matTabBodyHost",""]],template:function(t,n){t&1&&(c(0,"div",1,0),ge(2,pr,0,0,"ng-template",2),u()),t&2&&p("mat-tab-body-content-left",n._position==="left")("mat-tab-body-content-right",n._position==="right")("mat-tab-body-content-can-animate",n._position==="center"||n._previousPosition==="center")},dependencies:[sa,Un],styles:[`.mat-mdc-tab-body { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + display: block; + overflow: hidden; + outline: 0; + flex-basis: 100%; +} +.mat-mdc-tab-body.mat-mdc-tab-body-active { + position: relative; + overflow-x: hidden; + overflow-y: auto; + z-index: 1; + flex-grow: 1; +} +.mat-mdc-tab-group.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body.mat-mdc-tab-body-active { + overflow-y: hidden; +} + +.mat-mdc-tab-body-content { + height: 100%; + overflow: auto; + transform: none; + visibility: hidden; +} +.mat-tab-body-animating > .mat-mdc-tab-body-content, .mat-mdc-tab-body-active > .mat-mdc-tab-body-content { + visibility: visible; +} +.mat-tab-body-animating > .mat-mdc-tab-body-content { + min-height: 1px; +} +.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body-content { + overflow: hidden; +} + +.mat-tab-body-content-can-animate { + transition: transform var(--mat-tab-animation-duration) 1ms cubic-bezier(0.35, 0, 0.25, 1); +} +.mat-mdc-tab-body-wrapper._mat-animation-noopable .mat-tab-body-content-can-animate { + transition: none; +} + +.mat-tab-body-content-left { + transform: translate3d(-100%, 0, 0); +} + +.mat-tab-body-content-right { + transform: translate3d(100%, 0, 0); +} +`],encapsulation:2})}return i})(),ym=(()=>{class i{_elementRef=r(D);_changeDetectorRef=r(Y);_ngZone=r(z);_tabsSubscription=re.EMPTY;_tabLabelSubscription=re.EMPTY;_tabBodySubscription=re.EMPTY;_diAnimationsDisabled=q();_allTabs;_tabBodies;_tabBodyWrapper;_tabHeader;_tabs=new ft;_indexToSelect=0;_lastFocusedTabIndex=null;_tabBodyWrapperHeight=0;color;get fitInkBarToContent(){return this._fitInkBarToContent}set fitInkBarToContent(e){this._fitInkBarToContent=e,this._changeDetectorRef.markForCheck()}_fitInkBarToContent=!1;stretchTabs=!0;alignTabs=null;dynamicHeight=!1;get selectedIndex(){return this._selectedIndex}set selectedIndex(e){this._indexToSelect=isNaN(e)?null:e}_selectedIndex=null;headerPosition="above";get animationDuration(){return this._animationDuration}set animationDuration(e){let t=e+"";this._animationDuration=/^\d+$/.test(t)?e+"ms":t}_animationDuration;get contentTabIndex(){return this._contentTabIndex}set contentTabIndex(e){this._contentTabIndex=isNaN(e)?null:e}_contentTabIndex=null;disablePagination=!1;disableRipple=!1;preserveContent=!1;get backgroundColor(){return this._backgroundColor}set backgroundColor(e){let t=this._elementRef.nativeElement.classList;t.remove("mat-tabs-with-background",`mat-background-${this.backgroundColor}`),e&&t.add("mat-tabs-with-background",`mat-background-${e}`),this._backgroundColor=e}_backgroundColor;ariaLabel;ariaLabelledby;selectedIndexChange=new g;focusChange=new g;animationDone=new g;selectedTabChange=new g(!0);_groupId;_isServer=!r(Ee).isBrowser;constructor(){let e=r(ga,{optional:!0});this._groupId=r(K).getId("mat-tab-group-"),this.animationDuration=e&&e.animationDuration?e.animationDuration:"500ms",this.disablePagination=e&&e.disablePagination!=null?e.disablePagination:!1,this.dynamicHeight=e&&e.dynamicHeight!=null?e.dynamicHeight:!1,e?.contentTabIndex!=null&&(this.contentTabIndex=e.contentTabIndex),this.preserveContent=!!e?.preserveContent,this.fitInkBarToContent=e&&e.fitInkBarToContent!=null?e.fitInkBarToContent:!1,this.stretchTabs=e&&e.stretchTabs!=null?e.stretchTabs:!0,this.alignTabs=e&&e.alignTabs!=null?e.alignTabs:null}ngAfterContentChecked(){let e=this._indexToSelect=this._clampTabIndex(this._indexToSelect);if(this._selectedIndex!=e){let t=this._selectedIndex==null;if(!t){this.selectedTabChange.emit(this._createChangeEvent(e));let n=this._tabBodyWrapper.nativeElement;n.style.minHeight=n.clientHeight+"px"}Promise.resolve().then(()=>{this._tabs.forEach((n,a)=>n.isActive=a===e),t||(this.selectedIndexChange.emit(e),this._tabBodyWrapper.nativeElement.style.minHeight="")})}this._tabs.forEach((t,n)=>{t.position=n-e,this._selectedIndex!=null&&t.position==0&&!t.origin&&(t.origin=e-this._selectedIndex)}),this._selectedIndex!==e&&(this._selectedIndex=e,this._lastFocusedTabIndex=null,this._changeDetectorRef.markForCheck())}ngAfterContentInit(){this._subscribeToAllTabChanges(),this._subscribeToTabLabels(),this._tabsSubscription=this._tabs.changes.subscribe(()=>{let e=this._clampTabIndex(this._indexToSelect);if(e===this._selectedIndex){let t=this._tabs.toArray(),n;for(let a=0;a{t[e].isActive=!0,this.selectedTabChange.emit(this._createChangeEvent(e))})}this._changeDetectorRef.markForCheck()})}ngAfterViewInit(){this._tabBodySubscription=this._tabBodies.changes.subscribe(()=>this._bodyCentered(!0))}_subscribeToAllTabChanges(){this._allTabs.changes.pipe(te(this._allTabs)).subscribe(e=>{this._tabs.reset(e.filter(t=>t._closestTabGroup===this||!t._closestTabGroup)),this._tabs.notifyOnChanges()})}ngOnDestroy(){this._tabs.destroy(),this._tabsSubscription.unsubscribe(),this._tabLabelSubscription.unsubscribe(),this._tabBodySubscription.unsubscribe()}realignInkBar(){this._tabHeader&&this._tabHeader._alignInkBarToSelectedTab()}updatePagination(){this._tabHeader&&this._tabHeader.updatePagination()}focusTab(e){let t=this._tabHeader;t&&(t.focusIndex=e)}_focusChanged(e){this._lastFocusedTabIndex=e,this.focusChange.emit(this._createChangeEvent(e))}_createChangeEvent(e){let t=new pn;return t.index=e,this._tabs&&this._tabs.length&&(t.tab=this._tabs.toArray()[e]),t}_subscribeToTabLabels(){this._tabLabelSubscription&&this._tabLabelSubscription.unsubscribe(),this._tabLabelSubscription=G(...this._tabs.map(e=>e._stateChanges)).subscribe(()=>this._changeDetectorRef.markForCheck())}_clampTabIndex(e){return Math.min(this._tabs.length-1,Math.max(e||0,0))}_getTabLabelId(e,t){return e.id||`${this._groupId}-label-${t}`}_getTabContentId(e){return`${this._groupId}-content-${e}`}_setTabBodyWrapperHeight(e){if(!this.dynamicHeight||!this._tabBodyWrapperHeight){this._tabBodyWrapperHeight=e;return}let t=this._tabBodyWrapper.nativeElement;t.style.height=this._tabBodyWrapperHeight+"px",this._tabBodyWrapper.nativeElement.offsetHeight&&(t.style.height=e+"px")}_removeTabBodyWrapperHeight(){let e=this._tabBodyWrapper.nativeElement;this._tabBodyWrapperHeight=e.clientHeight,e.style.height="",this._ngZone.run(()=>this.animationDone.emit())}_handleClick(e,t,n){t.focusIndex=n,e.disabled||(this.selectedIndex=n)}_getTabIndex(e){let t=this._lastFocusedTabIndex??this.selectedIndex;return e===t?0:-1}_tabFocusChanged(e,t){e&&e!=="mouse"&&e!=="touch"&&(this._tabHeader.focusIndex=t)}_bodyCentered(e){e&&this._tabBodies?.forEach((t,n)=>t._setActiveClass(n===this._selectedIndex))}_animationsDisabled(){return this._diAnimationsDisabled||this.animationDuration==="0"||this.animationDuration==="0ms"}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-tab-group"]],contentQueries:function(t,n,a){if(t&1&&ve(a,Er,5),t&2){let o;m(o=h())&&(n._allTabs=o)}},viewQuery:function(t,n){if(t&1&&W(br,5)(_r,5)(fn,5),t&2){let a;m(a=h())&&(n._tabBodyWrapper=a.first),m(a=h())&&(n._tabHeader=a.first),m(a=h())&&(n._tabBodies=a)}},hostAttrs:[1,"mat-mdc-tab-group"],hostVars:11,hostBindings:function(t,n){t&2&&(T("mat-align-tabs",n.alignTabs),de("mat-"+(n.color||"primary")),Gt("--mat-tab-animation-duration",n.animationDuration),p("mat-mdc-tab-group-dynamic-height",n.dynamicHeight)("mat-mdc-tab-group-inverted-header",n.headerPosition==="below")("mat-mdc-tab-group-stretch-tabs",n.stretchTabs))},inputs:{color:"color",fitInkBarToContent:[2,"fitInkBarToContent","fitInkBarToContent",_],stretchTabs:[2,"mat-stretch-tabs","stretchTabs",_],alignTabs:[0,"mat-align-tabs","alignTabs"],dynamicHeight:[2,"dynamicHeight","dynamicHeight",_],selectedIndex:[2,"selectedIndex","selectedIndex",Te],headerPosition:"headerPosition",animationDuration:"animationDuration",contentTabIndex:[2,"contentTabIndex","contentTabIndex",Te],disablePagination:[2,"disablePagination","disablePagination",_],disableRipple:[2,"disableRipple","disableRipple",_],preserveContent:[2,"preserveContent","preserveContent",_],backgroundColor:"backgroundColor",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"]},outputs:{selectedIndexChange:"selectedIndexChange",focusChange:"focusChange",animationDone:"animationDone",selectedTabChange:"selectedTabChange"},exportAs:["matTabGroup"],features:[U([{provide:fa,useExisting:i}])],ngContentSelectors:at,decls:9,vars:8,consts:[["tabHeader",""],["tabBodyWrapper",""],["tabNode",""],[3,"indexFocused","selectFocusedIndex","selectedIndex","disableRipple","disablePagination","aria-label","aria-labelledby"],["role","tab","matTabLabelWrapper","","cdkMonitorElementFocus","",1,"mdc-tab","mat-mdc-tab","mat-focus-indicator",3,"id","mdc-tab--active","class","disabled","fitInkBarToContent"],[1,"mat-mdc-tab-body-wrapper"],["role","tabpanel",3,"id","class","content","position","animationDuration","preserveContent"],["role","tab","matTabLabelWrapper","","cdkMonitorElementFocus","",1,"mdc-tab","mat-mdc-tab","mat-focus-indicator",3,"click","cdkFocusChange","id","disabled","fitInkBarToContent"],[1,"mdc-tab__ripple"],["mat-ripple","",1,"mat-mdc-tab-ripple",3,"matRippleTrigger","matRippleDisabled"],[1,"mdc-tab__content"],[1,"mdc-tab__text-label"],[3,"cdkPortalOutlet"],["role","tabpanel",3,"_onCentered","_onCentering","_beforeCentering","id","content","position","animationDuration","preserveContent"]],template:function(t,n){t&1&&(H(),c(0,"mat-tab-header",3,0),k("indexFocused",function(o){return n._focusChanged(o)})("selectFocusedIndex",function(o){return n.selectedIndex=o}),zt(2,xr,8,17,"div",4,Bt),u(),F(4,kr,1,0),c(5,"div",5,1),zt(7,Cr,1,10,"mat-tab-body",6,Bt),u()),t&2&&(E("selectedIndex",n.selectedIndex||0)("disableRipple",n.disableRipple)("disablePagination",n.disablePagination),Pn("aria-label",n.ariaLabel)("aria-labelledby",n.ariaLabelledby),d(2),Ht(n._tabs),d(2),O(n._isServer?4:-1),d(),p("_mat-animation-noopable",n._animationsDisabled()),d(2),Ht(n._tabs))},dependencies:[Or,ba,ri,he,Ze,fn],styles:[`.mdc-tab { + min-width: 90px; + padding: 0 24px; + display: flex; + flex: 1 0 auto; + justify-content: center; + box-sizing: border-box; + border: none; + outline: none; + text-align: center; + white-space: nowrap; + cursor: pointer; + z-index: 1; + touch-action: manipulation; +} + +.mdc-tab__content { + display: flex; + align-items: center; + justify-content: center; + height: inherit; + pointer-events: none; +} + +.mdc-tab__text-label { + transition: 150ms color linear; + display: inline-block; + line-height: 1; + z-index: 2; +} + +.mdc-tab--active .mdc-tab__text-label { + transition-delay: 100ms; +} + +._mat-animation-noopable .mdc-tab__text-label { + transition: none; +} + +.mdc-tab-indicator { + display: flex; + position: absolute; + top: 0; + left: 0; + justify-content: center; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; +} + +.mdc-tab-indicator__content { + transition: var(--mat-tab-animation-duration, 250ms) transform cubic-bezier(0.4, 0, 0.2, 1); + transform-origin: left; + opacity: 0; +} + +.mdc-tab-indicator__content--underline { + align-self: flex-end; + box-sizing: border-box; + width: 100%; + border-top-style: solid; +} + +.mdc-tab-indicator--active .mdc-tab-indicator__content { + opacity: 1; +} + +._mat-animation-noopable .mdc-tab-indicator__content, .mdc-tab-indicator--no-transition .mdc-tab-indicator__content { + transition: none; +} + +.mat-mdc-tab-ripple.mat-mdc-tab-ripple { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + pointer-events: none; +} + +.mat-mdc-tab { + -webkit-tap-highlight-color: transparent; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-decoration: none; + background: none; + height: var(--mat-tab-container-height, 48px); + font-family: var(--mat-tab-label-text-font, var(--mat-sys-title-small-font)); + font-size: var(--mat-tab-label-text-size, var(--mat-sys-title-small-size)); + letter-spacing: var(--mat-tab-label-text-tracking, var(--mat-sys-title-small-tracking)); + line-height: var(--mat-tab-label-text-line-height, var(--mat-sys-title-small-line-height)); + font-weight: var(--mat-tab-label-text-weight, var(--mat-sys-title-small-weight)); +} +.mat-mdc-tab.mdc-tab { + flex-grow: 0; +} +.mat-mdc-tab .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-active-indicator-color, var(--mat-sys-primary)); + border-top-width: var(--mat-tab-active-indicator-height, 2px); + border-radius: var(--mat-tab-active-indicator-shape, 0); +} +.mat-mdc-tab:hover .mdc-tab__text-label { + color: var(--mat-tab-inactive-hover-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab:focus .mdc-tab__text-label { + color: var(--mat-tab-inactive-focus-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab.mdc-tab--active .mdc-tab__text-label { + color: var(--mat-tab-active-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab.mdc-tab--active .mdc-tab__ripple::before, +.mat-mdc-tab.mdc-tab--active .mat-ripple-element { + background-color: var(--mat-tab-active-ripple-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab.mdc-tab--active:hover .mdc-tab__text-label { + color: var(--mat-tab-active-hover-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab.mdc-tab--active:hover .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-active-hover-indicator-color, var(--mat-sys-primary)); +} +.mat-mdc-tab.mdc-tab--active:focus .mdc-tab__text-label { + color: var(--mat-tab-active-focus-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab.mdc-tab--active:focus .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-active-focus-indicator-color, var(--mat-sys-primary)); +} +.mat-mdc-tab.mat-mdc-tab-disabled { + opacity: 0.4; + pointer-events: none; +} +.mat-mdc-tab.mat-mdc-tab-disabled .mdc-tab__content { + pointer-events: none; +} +.mat-mdc-tab.mat-mdc-tab-disabled .mdc-tab__ripple::before, +.mat-mdc-tab.mat-mdc-tab-disabled .mat-ripple-element { + background-color: var(--mat-tab-disabled-ripple-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-tab .mdc-tab__ripple::before { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + pointer-events: none; + background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab .mdc-tab__text-label { + color: var(--mat-tab-inactive-label-text-color, var(--mat-sys-on-surface)); + display: inline-flex; + align-items: center; +} +.mat-mdc-tab .mdc-tab__content { + position: relative; + pointer-events: auto; +} +.mat-mdc-tab:hover .mdc-tab__ripple::before { + opacity: 0.04; +} +.mat-mdc-tab.cdk-program-focused .mdc-tab__ripple::before, .mat-mdc-tab.cdk-keyboard-focused .mdc-tab__ripple::before { + opacity: 0.12; +} +.mat-mdc-tab .mat-ripple-element { + opacity: 0.12; + background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-group.mat-mdc-tab-group-stretch-tabs > .mat-mdc-tab-header .mat-mdc-tab { + flex-grow: 1; +} + +.mat-mdc-tab-group { + display: flex; + flex-direction: column; + max-width: 100%; +} +.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination { + background-color: var(--mat-tab-background-color); +} +.mat-mdc-tab-group.mat-tabs-with-background.mat-primary > .mat-mdc-tab-header .mat-mdc-tab .mdc-tab__text-label { + color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-group.mat-tabs-with-background.mat-primary > .mat-mdc-tab-header .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-group.mat-tabs-with-background:not(.mat-primary) > .mat-mdc-tab-header .mat-mdc-tab:not(.mdc-tab--active) .mdc-tab__text-label { + color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-group.mat-tabs-with-background:not(.mat-primary) > .mat-mdc-tab-header .mat-mdc-tab:not(.mdc-tab--active) .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mat-mdc-tab-header-pagination-chevron, +.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mat-focus-indicator::before, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron, +.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-focus-indicator::before { + border-color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mat-ripple-element, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mdc-tab__ripple::before, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-ripple-element, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mdc-tab__ripple::before { + background-color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mat-mdc-tab-header-pagination-chevron, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron { + color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-group.mat-mdc-tab-group-inverted-header { + flex-direction: column-reverse; +} +.mat-mdc-tab-group.mat-mdc-tab-group-inverted-header .mdc-tab-indicator__content--underline { + align-self: flex-start; +} + +.mat-mdc-tab-body-wrapper { + position: relative; + overflow: hidden; + display: flex; + transition: height 500ms cubic-bezier(0.35, 0, 0.25, 1); +} +.mat-mdc-tab-body-wrapper._mat-animation-noopable { + transition: none !important; + animation: none !important; +} +`],encapsulation:2})}return i})(),pn=class{index;tab},Ar=(()=>{class i extends _a{_focusedItem=Q(null);get fitInkBarToContent(){return this._fitInkBarToContent.value}set fitInkBarToContent(e){this._fitInkBarToContent.next(e),this._changeDetectorRef.markForCheck()}_fitInkBarToContent=new Ye(!1);stretchTabs=!0;get animationDuration(){return this._animationDuration}set animationDuration(e){let t=e+"";this._animationDuration=/^\d+$/.test(t)?e+"ms":t}_animationDuration;_items;get backgroundColor(){return this._backgroundColor}set backgroundColor(e){let t=this._elementRef.nativeElement.classList;t.remove("mat-tabs-with-background",`mat-background-${this.backgroundColor}`),e&&t.add("mat-tabs-with-background",`mat-background-${e}`),this._backgroundColor=e}_backgroundColor;get disableRipple(){return this._disableRipple()}set disableRipple(e){this._disableRipple.set(e)}_disableRipple=Q(!1);color="primary";tabPanel;_tabListContainer;_tabList;_tabListInner;_nextPaginator;_previousPaginator;_inkBar;constructor(){let e=r(ga,{optional:!0});super(),this.disablePagination=e&&e.disablePagination!=null?e.disablePagination:!1,this.fitInkBarToContent=e&&e.fitInkBarToContent!=null?e.fitInkBarToContent:!1,this.stretchTabs=e&&e.stretchTabs!=null?e.stretchTabs:!0}_itemSelected(){}ngAfterContentInit(){this._inkBar=new Pt(this._items),this._items.changes.pipe(te(null),x(this._destroyed)).subscribe(()=>this.updateActiveLink()),super.ngAfterContentInit(),this._keyManager.change.pipe(te(null),x(this._destroyed)).subscribe(()=>this._focusedItem.set(this._keyManager?.activeItem||null))}ngAfterViewInit(){this.tabPanel,super.ngAfterViewInit()}updateActiveLink(){if(!this._items)return;let e=this._items.toArray();for(let t=0;t .mat-mdc-tab-link-container .mat-mdc-tab-links { + justify-content: center; +} +[mat-align-tabs=end] > .mat-mdc-tab-link-container .mat-mdc-tab-links { + justify-content: flex-end; +} +.cdk-drop-list .mat-mdc-tab-links, .mat-mdc-tab-links.cdk-drop-list { + min-height: var(--mat-tab-container-height, 48px); +} + +.mat-mdc-tab-link-container { + display: flex; + flex-grow: 1; + overflow: hidden; + z-index: 1; + border-bottom-style: solid; + border-bottom-width: var(--mat-tab-divider-height, 1px); + border-bottom-color: var(--mat-tab-divider-color, var(--mat-sys-surface-variant)); +} + +.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination { + background-color: var(--mat-tab-background-color); +} +.mat-mdc-tab-nav-bar.mat-tabs-with-background.mat-primary > .mat-mdc-tab-link-container .mat-mdc-tab-link .mdc-tab__text-label { + color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-nav-bar.mat-tabs-with-background.mat-primary > .mat-mdc-tab-link-container .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-nav-bar.mat-tabs-with-background:not(.mat-primary) > .mat-mdc-tab-link-container .mat-mdc-tab-link:not(.mdc-tab--active) .mdc-tab__text-label { + color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-nav-bar.mat-tabs-with-background:not(.mat-primary) > .mat-mdc-tab-link-container .mat-mdc-tab-link:not(.mdc-tab--active) .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mat-mdc-tab-header-pagination-chevron, +.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mat-focus-indicator::before, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron, +.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-focus-indicator::before { + border-color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mat-ripple-element, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mdc-tab__ripple::before, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-ripple-element, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mdc-tab__ripple::before { + background-color: var(--mat-tab-foreground-color); +} +.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mat-mdc-tab-header-pagination-chevron, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron { + color: var(--mat-tab-foreground-color); +} +`],encapsulation:2})}return i})(),Lr=(()=>{class i extends pa{_tabNavBar=r(Ar);elementRef=r(D);_focusMonitor=r(Ve);_destroyed=new f;_isActive=!1;_tabIndex=ye(()=>this._tabNavBar._focusedItem()===this?this.tabIndex:-1);get active(){return this._isActive}set active(e){e!==this._isActive&&(this._isActive=e,this._tabNavBar.updateActiveLink())}disabled=!1;get disableRipple(){return this._disableRipple()}set disableRipple(e){this._disableRipple.set(e)}_disableRipple=Q(!1);tabIndex=0;rippleConfig;get rippleDisabled(){return this.disabled||this.disableRipple||this._tabNavBar.disableRipple||!!this.rippleConfig.disabled}id=r(K).getId("mat-tab-link-");constructor(){super(),r(De).load(Re);let e=r(Kt,{optional:!0}),t=r(new Ke("tabindex"),{optional:!0});this.rippleConfig=e||{},this.tabIndex=t==null?0:parseInt(t)||0,q()&&(this.rippleConfig.animation={enterDuration:0,exitDuration:0}),this._tabNavBar._fitInkBarToContent.pipe(x(this._destroyed)).subscribe(n=>{this.fitInkBarToContent=n})}focus(){this.elementRef.nativeElement.focus()}ngAfterViewInit(){this._focusMonitor.monitor(this.elementRef)}ngOnDestroy(){this._destroyed.next(),this._destroyed.complete(),super.ngOnDestroy(),this._focusMonitor.stopMonitoring(this.elementRef)}_handleFocus(){this._tabNavBar.focusIndex=this._tabNavBar._items.toArray().indexOf(this)}_handleKeydown(e){(e.keyCode===32||e.keyCode===13)&&(this.disabled?e.preventDefault():this._tabNavBar.tabPanel&&(e.keyCode===32&&e.preventDefault(),this.elementRef.nativeElement.click()))}_getAriaControls(){return this._tabNavBar.tabPanel?this._tabNavBar.tabPanel?.id:this.elementRef.nativeElement.getAttribute("aria-controls")}_getAriaSelected(){return this._tabNavBar.tabPanel?this.active?"true":"false":this.elementRef.nativeElement.getAttribute("aria-selected")}_getAriaCurrent(){return this.active&&!this._tabNavBar.tabPanel?"page":null}_getRole(){return this._tabNavBar.tabPanel?"tab":this.elementRef.nativeElement.getAttribute("role")}static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["","mat-tab-link",""],["","matTabLink",""]],hostAttrs:[1,"mdc-tab","mat-mdc-tab-link","mat-focus-indicator"],hostVars:11,hostBindings:function(t,n){t&1&&k("focus",function(){return n._handleFocus()})("keydown",function(o){return n._handleKeydown(o)}),t&2&&(T("aria-controls",n._getAriaControls())("aria-current",n._getAriaCurrent())("aria-disabled",n.disabled)("aria-selected",n._getAriaSelected())("id",n.id)("tabIndex",n._tabIndex())("role",n._getRole()),p("mat-mdc-tab-disabled",n.disabled)("mdc-tab--active",n.active))},inputs:{active:[2,"active","active",_],disabled:[2,"disabled","disabled",_],disableRipple:[2,"disableRipple","disableRipple",_],tabIndex:[2,"tabIndex","tabIndex",e=>e==null?0:Te(e)],id:"id"},exportAs:["matTabLink"],features:[_e],attrs:Tr,ngContentSelectors:at,decls:5,vars:2,consts:[[1,"mdc-tab__ripple"],["mat-ripple","",1,"mat-mdc-tab-ripple",3,"matRippleTrigger","matRippleDisabled"],[1,"mdc-tab__content"],[1,"mdc-tab__text-label"]],template:function(t,n){t&1&&(H(),A(0,"span",0)(1,"div",1),c(2,"span",2)(3,"span",3),w(4),u()()),t&2&&(d(),E("matRippleTrigger",n.elementRef.nativeElement)("matRippleDisabled",n.rippleDisabled))},dependencies:[he],styles:[`.mat-mdc-tab-link { + -webkit-tap-highlight-color: transparent; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-decoration: none; + background: none; + height: var(--mat-tab-container-height, 48px); + font-family: var(--mat-tab-label-text-font, var(--mat-sys-title-small-font)); + font-size: var(--mat-tab-label-text-size, var(--mat-sys-title-small-size)); + letter-spacing: var(--mat-tab-label-text-tracking, var(--mat-sys-title-small-tracking)); + line-height: var(--mat-tab-label-text-line-height, var(--mat-sys-title-small-line-height)); + font-weight: var(--mat-tab-label-text-weight, var(--mat-sys-title-small-weight)); +} +.mat-mdc-tab-link.mdc-tab { + flex-grow: 0; +} +.mat-mdc-tab-link .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-active-indicator-color, var(--mat-sys-primary)); + border-top-width: var(--mat-tab-active-indicator-height, 2px); + border-radius: var(--mat-tab-active-indicator-shape, 0); +} +.mat-mdc-tab-link:hover .mdc-tab__text-label { + color: var(--mat-tab-inactive-hover-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-link:focus .mdc-tab__text-label { + color: var(--mat-tab-inactive-focus-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-link.mdc-tab--active .mdc-tab__text-label { + color: var(--mat-tab-active-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-link.mdc-tab--active .mdc-tab__ripple::before, +.mat-mdc-tab-link.mdc-tab--active .mat-ripple-element { + background-color: var(--mat-tab-active-ripple-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-link.mdc-tab--active:hover .mdc-tab__text-label { + color: var(--mat-tab-active-hover-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-link.mdc-tab--active:hover .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-active-hover-indicator-color, var(--mat-sys-primary)); +} +.mat-mdc-tab-link.mdc-tab--active:focus .mdc-tab__text-label { + color: var(--mat-tab-active-focus-label-text-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-link.mdc-tab--active:focus .mdc-tab-indicator__content--underline { + border-color: var(--mat-tab-active-focus-indicator-color, var(--mat-sys-primary)); +} +.mat-mdc-tab-link.mat-mdc-tab-disabled { + opacity: 0.4; + pointer-events: none; +} +.mat-mdc-tab-link.mat-mdc-tab-disabled .mdc-tab__content { + pointer-events: none; +} +.mat-mdc-tab-link.mat-mdc-tab-disabled .mdc-tab__ripple::before, +.mat-mdc-tab-link.mat-mdc-tab-disabled .mat-ripple-element { + background-color: var(--mat-tab-disabled-ripple-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-tab-link .mdc-tab__ripple::before { + content: ""; + display: block; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + pointer-events: none; + background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-link .mdc-tab__text-label { + color: var(--mat-tab-inactive-label-text-color, var(--mat-sys-on-surface)); + display: inline-flex; + align-items: center; +} +.mat-mdc-tab-link .mdc-tab__content { + position: relative; + pointer-events: auto; +} +.mat-mdc-tab-link:hover .mdc-tab__ripple::before { + opacity: 0.04; +} +.mat-mdc-tab-link.cdk-program-focused .mdc-tab__ripple::before, .mat-mdc-tab-link.cdk-keyboard-focused .mdc-tab__ripple::before { + opacity: 0.12; +} +.mat-mdc-tab-link .mat-ripple-element { + opacity: 0.12; + background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); +} +.mat-mdc-tab-header.mat-mdc-tab-nav-bar-stretch-tabs .mat-mdc-tab-link { + flex-grow: 1; +} +.mat-mdc-tab-link::before { + margin: 5px; +} + +@media (max-width: 599px) { + .mat-mdc-tab-link { + min-width: 72px; + } +} +`],encapsulation:2,changeDetection:0})}return i})(),xm=(()=>{class i{id=r(K).getId("mat-tab-nav-panel-");_activeTabId;static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-tab-nav-panel"]],hostAttrs:["role","tabpanel",1,"mat-mdc-tab-nav-panel"],hostVars:2,hostBindings:function(t,n){t&2&&T("aria-labelledby",n._activeTabId)("id",n.id)},inputs:{id:"id"},exportAs:["matTabNavPanel"],ngContentSelectors:at,decls:1,vars:0,template:function(t,n){t&1&&(H(),w(0))},encapsulation:2,changeDetection:0})}return i})(),km=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[j]})}return i})();var Pr=new C("DEFAULT_THEME_TOKEN",{providedIn:"root",factory:()=>"indigo-pink"}),va=(()=>{class i{document;_darkMode$=new Ye(!1);darkMode$=this._darkMode$.asObservable();themeChanged=new g;mapThemeChanged=new g;currentTheme="indigo-pink";currentMapTheme="auto";savedTheme="indigo-pink";savedMapTheme="auto";availableThemes=[{key:"deeppurple-amber",label:"Deep Purple & Amber",type:"light",primaryColor:"#6750A4"},{key:"indigo-pink",label:"Indigo & Pink",type:"light",primaryColor:"#3F51B5"},{key:"magenta-violet",label:"Magenta & Violet",type:"light",primaryColor:"#D81B60"},{key:"rose-red",label:"Rose & Red",type:"light",primaryColor:"#E91E63"},{key:"pink-bluegrey",label:"Pink & Bluegrey",type:"dark",primaryColor:"#E91E63"},{key:"purple-green",label:"Purple & Green",type:"dark",primaryColor:"#7E57C2"},{key:"azure-blue",label:"Azure & Blue",type:"dark",primaryColor:"#0078D4"},{key:"cyan-orange",label:"Cyan & Orange",type:"dark",primaryColor:"#00B7C3"}];availableMapBackgrounds=[{key:"auto",label:"Follow global theme",background:"",textColor:"",type:"light"},{key:"light-1",label:"Cyan Sky",background:"radial-gradient(ellipse at 20% 20%, #B2EBF2 0%, #E0F7FA 70%)",textColor:"#006064",type:"light"},{key:"light-2",label:"Blue Sky",background:"radial-gradient(ellipse at 20% 20%, #BBDEFB 0%, #E3F2FD 70%)",textColor:"#1565C0",type:"light"},{key:"light-3",label:"Cloud Gray",background:"radial-gradient(ellipse at 20% 20%, #F5F5F5 0%, #FAFAFA 70%)",textColor:"#424242",type:"light"},{key:"light-4",label:"Lavender",background:"radial-gradient(ellipse at 20% 20%, #E1BEE7 0%, #F3E5F5 70%)",textColor:"#4A148C",type:"light"},{key:"dark-1",label:"Deep Cyan",background:"linear-gradient(135deg, #006064 0%, #00838F 50%, #006064 100%)",textColor:"#FFFFFF",type:"dark"},{key:"dark-2",label:"Deep Blue",background:"linear-gradient(135deg, #1565C0 0%, #1976D2 50%, #1565C0 100%)",textColor:"#FFFFFF",type:"dark"},{key:"dark-3",label:"Charcoal",background:"linear-gradient(135deg, #424242 0%, #616161 50%, #424242 100%)",textColor:"#FFFFFF",type:"dark"},{key:"dark-4",label:"Deep Purple",background:"linear-gradient(135deg, #4A148C 0%, #6A1B9A 50%, #4A148C 100%)",textColor:"#FFFFFF",type:"dark"}];constructor(e,t){this.document=e;let n=localStorage.getItem("theme");this.currentTheme=n||t,this.savedTheme=this.currentTheme;let a=localStorage.getItem("mapTheme");this.currentMapTheme=a||"auto",this.savedMapTheme=this.currentMapTheme,this.applyTheme(this.currentTheme)}getCurrentTheme(){return this.currentTheme}getThemeType(){return this.isDarkTheme(this.currentTheme)?"dark":"light"}getActualMapTheme(){if(this.savedMapTheme==="auto")return this.getThemeType();let e=this.availableMapBackgrounds.find(t=>t.key===this.savedMapTheme);return e?e.type:"light"}getActualTheme(){return this.getThemeType()}isDarkTheme(e){return e==="pink-bluegrey"||e==="purple-green"||e==="azure-blue"||e==="cyan-orange"}setTheme(e){this.currentTheme!==e&&(this.currentTheme=e,this.savedTheme=e,this.applyTheme(e),this.saveThemePreference(e),this.themeChanged.emit(e),this.currentMapTheme==="auto"&&this.mapThemeChanged.emit(e),this._darkMode$.next(this.isDarkTheme(e)))}toggleTheme(){let e=this.getThemeType(),t;e==="dark"?t=this.availableThemes.find(n=>n.type==="light")?.key||"deeppurple-amber":t=this.availableThemes.find(n=>n.type==="dark")?.key||"pink-bluegrey",this.setTheme(t)}setDarkMode(e){let t=e?"pink-bluegrey":"indigo-pink";this.setTheme(t)}setMapTheme(e){this.currentMapTheme=e,this.savedMapTheme=e,localStorage.setItem("mapTheme",e),this.mapThemeChanged.emit(this.getActualMapTheme())}restoreTheme(){let e=localStorage.getItem("theme");e&&this.availableThemes.some(t=>t.key===e)&&this.setTheme(e)}applyTheme(e){let t=this.document.documentElement;t.classList.remove("theme-deeppurple-amber","theme-indigo-pink","theme-magenta-violet","theme-rose-red","theme-pink-bluegrey","theme-purple-green","theme-azure-blue","theme-cyan-orange"),t.classList.add(`theme-${e}`)}saveThemePreference(e){localStorage.setItem("theme",e)}isDarkMode(){return this.isDarkTheme(this.currentTheme)}isLightMode(){return!this.isDarkTheme(this.currentTheme)}getCanvasLabelColor(){return this.getActualMapTheme()==="dark"?"#FFFFFF":"#000000"}getCanvasLinkColor(){return this.getActualMapTheme()==="dark"?"#FFFFFF":"#000000"}static \u0275fac=function(t){return new(t||i)(J(be),J(Pr))};static \u0275prov=P({token:i,factory:i.\u0275fac,providedIn:"root"})}return i})();var ya=(()=>{class i{themeService;symbolScalingSubject=new f;isScrollDisabled=new f;isMapLocked=new f;isTopologySummaryVisible=!0;isLogConsoleVisible=!1;isLayerNumberVisible=!1;isItemLockStatusVisible=!1;logConsoleSubject=new f;mapRenderedEmitter=new g;openReadme;showInterfaceLabels=!0;integrateLinkLabelsToLinks=!0;openConsolesInWidget=!1;defaultSceneWidth;defaultSceneHeight;defaultGridSize;defaultDrawingGridSize;defaultShowGrid;defaultSnapToGrid;defaultLabelStyle;defaultNoteStyle;defaultLinkStyle;static FONT_FAMILIES=["TypeWriter","Noto Sans","Arial","Courier New","Times New Roman","Helvetica","Verdana","Georgia","Comic Sans MS"];static FONT_WEIGHTS=["normal","bold"];static LINK_STYLE_NAMES=["Invisible","Solid","Dash","Dot","Dash Dot","Dash Dot Dot"];static LINK_TYPES=[{label:"Straight",value:"straight"},{label:"Bezier",value:"bezier"},{label:"Flowchart",value:"flowchart"},{label:"StateMachine",value:"statemachine"},{label:"Freeform",value:"freeform"}];static DEFAULT_SCENE_WIDTH=2e3;static DEFAULT_SCENE_HEIGHT=1e3;static DEFAULT_GRID_SIZE=75;static DEFAULT_DRAWING_GRID_SIZE=25;static DEFAULT_SHOW_GRID=!1;static DEFAULT_SNAP_TO_GRID=!1;defaultLabelStyleFallback;defaultNoteStyleFallback;defaultLinkStyleFallback;static SCENE_WIDTH_KEY="defaultSceneWidth";static SCENE_HEIGHT_KEY="defaultSceneHeight";static GRID_SIZE_KEY="defaultGridSize";static DRAWING_GRID_SIZE_KEY="defaultDrawingGridSize";static SHOW_GRID_KEY="defaultShowGrid";static SNAP_TO_GRID_KEY="defaultSnapToGrid";static LABEL_STYLE_KEY="defaultLabelStyle";static NOTE_STYLE_KEY="defaultNoteStyle";static LINK_STYLE_KEY="defaultLinkStyle";constructor(e){this.themeService=e,this.defaultLabelStyleFallback={fontFamily:"TypeWriter",fontSize:10,fontWeight:"bold",color:this.themeService.getCanvasLabelColor()},this.defaultNoteStyleFallback={fontFamily:"Noto Sans",fontSize:11,fontWeight:"bold",color:this.themeService.getCanvasLabelColor()},this.defaultLinkStyleFallback={color:this.themeService.getCanvasLinkColor(),width:2,type:1,link_type:"straight"},this.isLayerNumberVisible=localStorage.getItem("layersVisibility")==="true",localStorage.getItem("integrateLinkLabelsToLinks")&&(this.integrateLinkLabelsToLinks=localStorage.getItem("integrateLinkLabelsToLinks")==="true"),localStorage.getItem("openConsolesInWidget")&&(this.openConsolesInWidget=localStorage.getItem("openConsolesInWidget")==="true"),localStorage.getItem("symbolScaling")||localStorage.setItem("symbolScaling","true"),localStorage.getItem("openReadme")?this.openReadme=localStorage.getItem("openReadme")==="true":localStorage.setItem("openReadme","false"),localStorage.getItem("showInterfaceLabels")?this.showInterfaceLabels=localStorage.getItem("showInterfaceLabels")==="true":localStorage.setItem("showInterfaceLabels","true"),this.defaultSceneWidth=this.readNumber(i.SCENE_WIDTH_KEY,i.DEFAULT_SCENE_WIDTH),this.defaultSceneHeight=this.readNumber(i.SCENE_HEIGHT_KEY,i.DEFAULT_SCENE_HEIGHT),this.defaultGridSize=this.readNumber(i.GRID_SIZE_KEY,i.DEFAULT_GRID_SIZE),this.defaultDrawingGridSize=this.readNumber(i.DRAWING_GRID_SIZE_KEY,i.DEFAULT_DRAWING_GRID_SIZE),this.defaultShowGrid=this.readBoolean(i.SHOW_GRID_KEY,i.DEFAULT_SHOW_GRID),this.defaultSnapToGrid=this.readBoolean(i.SNAP_TO_GRID_KEY,i.DEFAULT_SNAP_TO_GRID),this.defaultLabelStyle=this.readTextStyle(i.LABEL_STYLE_KEY,this.defaultLabelStyleFallback),this.defaultNoteStyle=this.readTextStyle(i.NOTE_STYLE_KEY,this.defaultNoteStyleFallback),this.defaultLinkStyle=this.readLinkStyle(i.LINK_STYLE_KEY,this.defaultLinkStyleFallback)}readNumber(e,t){let n=localStorage.getItem(e);if(n===null)return t;let a=parseInt(n,10);return Number.isFinite(a)&&a>=0?a:t}readBoolean(e,t){let n=localStorage.getItem(e);return n===null?t:n==="true"}readTextStyle(e,t){let n=localStorage.getItem(e);if(!n)return S({},t);try{return this.normalizeTextStyle(JSON.parse(n),t)}catch{return S({},t)}}readLinkStyle(e,t){let n=localStorage.getItem(e);if(!n)return S({},t);try{return this.normalizeLinkStyle(JSON.parse(n),t)}catch{return S({},t)}}normalizeTextStyle(e,t){let n=typeof e=="object"&&e!==null?e:{},a=Number(n.fontSize);return{fontFamily:i.FONT_FAMILIES.includes(n.fontFamily)?n.fontFamily:t.fontFamily,fontSize:Number.isFinite(a)?Math.min(200,Math.max(1,Math.round(a*2)/2)):t.fontSize,fontWeight:i.FONT_WEIGHTS.includes(n.fontWeight)?n.fontWeight:t.fontWeight,color:this.normalizeColor(n.color,t.color)}}normalizeLinkStyle(e,t){let n=typeof e=="object"&&e!==null?e:{},a=Number(n.width),o=Number(n.type),l=i.LINK_TYPES.map(v=>v.value);return{color:this.normalizeColor(n.color,t.color),width:Number.isFinite(a)?Math.min(20,Math.max(1,Math.round(a))):t.width,type:Number.isInteger(o)&&o>=0&&o=0?Math.round(e):i.DEFAULT_SCENE_WIDTH;this.defaultSceneWidth=t,localStorage.setItem(i.SCENE_WIDTH_KEY,String(t))}getDefaultSceneHeight(){return this.defaultSceneHeight}setDefaultSceneHeight(e){let t=Number.isFinite(e)&&e>=0?Math.round(e):i.DEFAULT_SCENE_HEIGHT;this.defaultSceneHeight=t,localStorage.setItem(i.SCENE_HEIGHT_KEY,String(t))}getDefaultGridSize(){return this.defaultGridSize}setDefaultGridSize(e){let t=Number.isFinite(e)&&e>=0?Math.round(e):i.DEFAULT_GRID_SIZE;this.defaultGridSize=t,localStorage.setItem(i.GRID_SIZE_KEY,String(t))}getDefaultDrawingGridSize(){return this.defaultDrawingGridSize}setDefaultDrawingGridSize(e){let t=Number.isFinite(e)&&e>=0?Math.round(e):i.DEFAULT_DRAWING_GRID_SIZE;this.defaultDrawingGridSize=t,localStorage.setItem(i.DRAWING_GRID_SIZE_KEY,String(t))}getDefaultShowGrid(){return this.defaultShowGrid}setDefaultShowGrid(e){this.defaultShowGrid=e,localStorage.setItem(i.SHOW_GRID_KEY,e?"true":"false")}getDefaultSnapToGrid(){return this.defaultSnapToGrid}setDefaultSnapToGrid(e){this.defaultSnapToGrid=e,localStorage.setItem(i.SNAP_TO_GRID_KEY,e?"true":"false")}getDefaultLabelStyle(){return le(S({},this.defaultLabelStyle),{color:this.hasDefaultLabelStyle()?this.defaultLabelStyle.color:this.themeService.getCanvasLabelColor()})}hasDefaultLabelStyle(){return localStorage.getItem(i.LABEL_STYLE_KEY)!==null}setDefaultLabelStyle(e){this.defaultLabelStyle=this.normalizeTextStyle(e,this.defaultLabelStyleFallback),localStorage.setItem(i.LABEL_STYLE_KEY,JSON.stringify(this.defaultLabelStyle))}getDefaultNoteStyle(){return le(S({},this.defaultNoteStyle),{color:this.hasDefaultNoteStyle()?this.defaultNoteStyle.color:this.themeService.getCanvasLabelColor()})}hasDefaultNoteStyle(){return localStorage.getItem(i.NOTE_STYLE_KEY)!==null}setDefaultNoteStyle(e){this.defaultNoteStyle=this.normalizeTextStyle(e,this.defaultNoteStyleFallback),localStorage.setItem(i.NOTE_STYLE_KEY,JSON.stringify(this.defaultNoteStyle))}getDefaultLinkStyle(){return le(S({},this.defaultLinkStyle),{color:this.hasDefaultLinkStyle()?this.defaultLinkStyle.color:this.themeService.getCanvasLinkColor()})}hasDefaultLinkStyle(){return localStorage.getItem(i.LINK_STYLE_KEY)!==null}setDefaultLinkStyle(e){this.defaultLinkStyle=this.normalizeLinkStyle(e,this.defaultLinkStyleFallback),localStorage.setItem(i.LINK_STYLE_KEY,JSON.stringify(this.defaultLinkStyle))}static \u0275fac=function(t){return new(t||i)(J(va))};static \u0275prov=P({token:i,factory:i.\u0275fac,providedIn:"root"})}return i})();var ot=class{data=[];keyIndex=new Map;dataChange=new Ye([]);itemUpdated=new f;getItems(){return this.data}add(s){if(this.findIndex(s)>=0){this.update(s);return}this.data=[...this.data,s],this.keyIndex.set(this.getItemKey(s),this.data.length-1),this.dataChange.next(this.data)}set(s){let e=[];for(let t of s||[]){let n=this.getItemKey(t),a=this.keyIndex.get(n);e.push(a!==void 0?Object.assign(this.data[a],t):t)}this.data=e,this.reindex(),this.dataChange.next(this.data)}get(s){let e=this.keyIndex.get(s);if(e!==void 0)return this.data[e]}update(s){let e=this.findIndex(s);if(e>=0){let t=Object.assign(this.data[e],s);this.data=[...this.data],this.data[e]=t,this.dataChange.next(this.data),this.itemUpdated.next(t)}}remove(s){let e=this.findIndex(s);e>=0&&(this.data=this.data.filter((t,n)=>n!==e),this.reindex(),this.dataChange.next(this.data))}applyBatch(s,e){if(s.length>0){let t=this.data.length;this.data=[...this.data,...s];for(let n=0;n0){let t=new Set(e.map(n=>this.getItemKey(n)));this.data=this.data.filter(n=>!t.has(this.getItemKey(n))),this.reindex()}else s.length===0&&(this.data=[...this.data]);this.dataChange.next(this.data)}get changes(){return this.dataChange}get itemChanged(){return this.itemUpdated}clear(){this.data=[],this.keyIndex.clear(),this.dataChange.next(this.data)}reindex(){this.keyIndex.clear();for(let s=0;s{class i extends ot{getItemKey(e){return e.link_id}static \u0275fac=(()=>{let e;return function(n){return(e||(e=Le(i)))(n||i)}})();static \u0275prov=P({token:i,factory:i.\u0275fac})}return i})();var zm=(()=>{class i extends ot{getItemKey(e){return e.node_id}static \u0275fac=(()=>{let e;return function(n){return(e||(e=Le(i)))(n||i)}})();static \u0275prov=P({token:i,factory:i.\u0275fac})}return i})();var $m=(()=>{class i{router;toasterService;mapSettingsService;nodeConsoleTrigger=new g;closeNodeConsoleTrigger=new f;consoleResized=new f;openConsoles=0;defaultConsoleWidth=720;defaultConsoleHeight=408;defaultNumberOfColumns=80;defaultNumberOfRows=24;lastNumberOfColumns;lastNumberOfRows;constructor(e,t,n){this.router=e,this.toasterService=t,this.mapSettingsService=n}getNumberOfColumns(){return this.lastNumberOfColumns}getNumberOfRows(){return this.lastNumberOfRows}setNumberOfColumns(e){this.lastNumberOfColumns=e}setNumberOfRows(e){this.lastNumberOfRows=e}openConsoleForNode(e){this.nodeConsoleTrigger.emit(e)}closeConsoleForNode(e){this.closeNodeConsoleTrigger.next(e)}resizeTerminal(e){this.consoleResized.next(e)}getLineWidth(){return this.defaultConsoleWidth/this.defaultNumberOfColumns}getLineHeight(){return this.defaultConsoleHeight/this.defaultNumberOfRows}getUrl(e,t){let n="ws";return e.protocol==="https:"&&(n="wss"),`${n}://${e.host}:${e.port}/${tt.current_version}/projects/${t.project_id}/nodes/${t.node_id}/console/ws?token=${e.authToken}`}sendTerminalSize(e,t,n){e&&e.readyState===WebSocket.OPEN&&e.send(new Blob([JSON.stringify({cols:t,rows:n})]))}openConsolesForAllNodesInWidget(e){let t="Please start the following nodes if you want to open consoles for them: ",n=0;e.forEach(a=>{a.console_type!=="none"&&(a.status==="started"?(this.mapSettingsService.logConsoleSubject.next(!0),setTimeout(()=>{this.openConsoleForNode(a)},500)):(n++,t+=a.name+" "))}),n>0&&this.toasterService.error(t)}openConsolesForAllNodesInNewTabs(e){let t="Please start the following nodes if you want to open consoles in tabs for them: ",n=0;e.forEach(a=>{if(a.console_type==="telnet"||a.console_type==="ssh"||a.console_type==="docker_exec")if(a.status==="started"){let o=this.router.url.split("/"),l=`/static/web-ui/${o[1]}/${o[2]}/${o[3]}/${o[4]}/nodes/${a.node_id}`;window.open(l)}else n++,t+=a.name+" "}),n>0&&this.toasterService.error(t)}static \u0275fac=function(t){return new(t||i)(J(bi),J(ia),J(ya))};static \u0275prov=P({token:i,factory:i.\u0275fac})}return i})();var Km=(()=>{class i{get vertical(){return this._vertical}set vertical(e){this._vertical=Be(e)}_vertical=!1;get inset(){return this._inset}set inset(e){this._inset=Be(e)}_inset=!1;static \u0275fac=function(t){return new(t||i)};static \u0275cmp=M({type:i,selectors:[["mat-divider"]],hostAttrs:["role","separator",1,"mat-divider"],hostVars:7,hostBindings:function(t,n){t&2&&(T("aria-orientation",n.vertical?"vertical":"horizontal"),p("mat-divider-vertical",n.vertical)("mat-divider-horizontal",!n.vertical)("mat-divider-inset",n.inset))},inputs:{vertical:"vertical",inset:"inset"},decls:0,vars:0,template:function(t,n){},styles:[`.mat-divider { + display: block; + margin: 0; + border-top-style: solid; + border-top-color: var(--mat-divider-color, var(--mat-sys-outline-variant)); + border-top-width: var(--mat-divider-width, 1px); +} +.mat-divider.mat-divider-vertical { + border-top: 0; + border-right-style: solid; + border-right-color: var(--mat-divider-color, var(--mat-sys-outline-variant)); + border-right-width: var(--mat-divider-width, 1px); +} +.mat-divider.mat-divider-inset { + margin-left: 80px; +} +[dir=rtl] .mat-divider.mat-divider-inset { + margin-left: auto; + margin-right: 80px; +} +`],encapsulation:2,changeDetection:0})}return i})(),Xm=(()=>{class i{static \u0275fac=function(t){return new(t||i)};static \u0275mod=B({type:i});static \u0275inj=N({imports:[j]})}return i})();var We=typeof window>"u"?!1:"ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0;function zr(i){let s=i.cloneNode(!0),e=s.querySelectorAll("[id]"),t=i.nodeName.toLowerCase();return s.removeAttribute("id"),e.forEach(n=>{n.removeAttribute("id")}),t==="canvas"?Ca(i,s):(t==="input"||t==="select"||t==="textarea")&&ka(i,s),xa("canvas",i,s,Ca),xa("input, textarea, select",i,s,ka),s}function xa(i,s,e,t){let n=s.querySelectorAll(i);if(n.length){let a=e.querySelectorAll(i);for(let o=0;on[l]).find(l=>!!l);if(o&&o.includes("translate")&&(e=o.replace(/.*translate3?d?\((-?[0-9]*)px, (-?[0-9]*)px.*/,"$1"),t=o.replace(/.*translate3?d?\((-?[0-9]*)px, (-?[0-9]*)px.*/,"$2")),s==="absolute")return{height:i.nativeElement.offsetHeight,width:i.nativeElement.offsetWidth,top:i.nativeElement.offsetTop-t,bottom:i.nativeElement.offsetHeight+i.nativeElement.offsetTop-t,left:i.nativeElement.offsetLeft-e,right:i.nativeElement.offsetWidth+i.nativeElement.offsetLeft-e};{let l=i.nativeElement.getBoundingClientRect();return{height:l.height,width:l.width,top:l.top-t,bottom:l.bottom-t,left:l.left-e,right:l.right-e,scrollTop:i.nativeElement.scrollTop,scrollLeft:i.nativeElement.scrollLeft}}}var Ta=Object.freeze({topLeft:"nw-resize",topRight:"ne-resize",bottomLeft:"sw-resize",bottomRight:"se-resize",leftOrRight:"col-resize",topOrBottom:"row-resize"});function Ma(i,s){return i.left&&i.top?s.topLeft:i.right&&i.top?s.topRight:i.left&&i.bottom?s.bottomLeft:i.right&&i.bottom?s.bottomRight:i.left||i.right?s.leftOrRight:i.top||i.bottom?s.topOrBottom:""}function Nt({edges:i,initialRectangle:s,newRectangle:e}){let t={};return Object.keys(i).forEach(n=>{t[n]=(e[n]||0)-(s[n]||0)}),t}var Ia="resize-active",Vr="resize-ghost-element",Gr=50,Wr=(()=>{class i{constructor(){this.enableGhostResize=!1,this.resizeSnapGrid={},this.resizeCursors=Ta,this.ghostElementPositioning="fixed",this.allowNegativeResizes=!1,this.mouseMoveThrottleMS=Gr,this.resizeStart=new g,this.resizing=new g,this.resizeEnd=new g,this.mouseup=new f,this.mousedown=new f,this.mousemove=new f,this.destroy$=new f,this.platformId=r(Rn),this.renderer=r(ae),this.elm=r(D),this.zone=r(z),this.rendererFactory=r(Fn),this.document=r(be),this.pointerEventListeners=bn.getInstance(this.rendererFactory.createRenderer(this.document,null),this.zone)}ngOnInit(){let e=G(this.pointerEventListeners.pointerDown,this.mousedown),t=G(this.pointerEventListeners.pointerMove,this.mousemove).pipe(In(({event:b})=>{a&&b.cancelable&&!Sa(b)&&b.preventDefault()}),rt()),n=G(this.pointerEventListeners.pointerUp,this.mouseup),a,o=()=>{a&&a.clonedNode&&(this.elm.nativeElement.parentElement.removeChild(a.clonedNode),this.renderer.setStyle(this.elm.nativeElement,"visibility","inherit"))},l=()=>S(S({},Ta),this.resizeCursors);e.pipe(yn(b=>{function V(R){return{clientX:R.clientX-b.clientX,clientY:R.clientY-b.clientY}}let ue=()=>{let R={x:1,y:1};return a&&(this.resizeSnapGrid.left&&a.edges.left?R.x=+this.resizeSnapGrid.left:this.resizeSnapGrid.right&&a.edges.right&&(R.x=+this.resizeSnapGrid.right),this.resizeSnapGrid.top&&a.edges.top?R.y=+this.resizeSnapGrid.top:this.resizeSnapGrid.bottom&&a.edges.bottom&&(R.y=+this.resizeSnapGrid.bottom)),R};function fe(R,oe){return{x:Math.ceil(R.clientX/oe.x),y:Math.ceil(R.clientY/oe.y)}}return G(t.pipe($e(1)).pipe(pe(R=>[,R])),t.pipe(ut())).pipe(pe(([R,oe])=>[R&&V(R),V(oe)])).pipe(Z(([R,oe])=>{if(!R)return!0;let ht=ue(),_n=fe(R,ht),gn=fe(oe,ht);return _n.x!==gn.x||_n.y!==gn.y})).pipe(pe(([,R])=>{let oe=ue();return{clientX:Math.round(R.clientX/oe.x)*oe.x,clientY:Math.round(R.clientY/oe.y)*oe.y}})).pipe(x(G(n,e)))})).pipe(Z(()=>!!a)).pipe(pe(({clientX:b,clientY:V})=>wa(a.startingRect,a.edges,b,V))).pipe(Z(b=>this.allowNegativeResizes||!!(b.height&&b.width&&b.height>0&&b.width>0))).pipe(Z(b=>this.validateResize?this.validateResize({rectangle:b,edges:Nt({edges:a.edges,initialRectangle:a.startingRect,newRectangle:b})}):!0),x(this.destroy$)).subscribe(b=>{a&&a.clonedNode&&(this.renderer.setStyle(a.clonedNode,"height",`${b.height}px`),this.renderer.setStyle(a.clonedNode,"width",`${b.width}px`),this.renderer.setStyle(a.clonedNode,"top",`${b.top}px`),this.renderer.setStyle(a.clonedNode,"left",`${b.left}px`)),this.resizing.observers.length>0&&this.zone.run(()=>{this.resizing.emit({edges:Nt({edges:a.edges,initialRectangle:a.startingRect,newRectangle:b}),rectangle:b})}),a.currentRect=b}),e.pipe(pe(({edges:b})=>b||{}),Z(b=>Object.keys(b).length>0),x(this.destroy$)).subscribe(b=>{a&&o();let V=jr(this.elm,this.ghostElementPositioning);a={edges:b,startingRect:V,currentRect:V};let ue=l(),fe=Ma(a.edges,ue);this.renderer.setStyle(document.body,"cursor",fe),this.setElementClass(this.elm,Ia,!0),this.enableGhostResize&&(a.clonedNode=zr(this.elm.nativeElement),this.elm.nativeElement.parentElement.appendChild(a.clonedNode),this.renderer.setStyle(this.elm.nativeElement,"visibility","hidden"),this.renderer.setStyle(a.clonedNode,"position",this.ghostElementPositioning),this.renderer.setStyle(a.clonedNode,"left",`${a.startingRect.left}px`),this.renderer.setStyle(a.clonedNode,"top",`${a.startingRect.top}px`),this.renderer.setStyle(a.clonedNode,"height",`${a.startingRect.height}px`),this.renderer.setStyle(a.clonedNode,"width",`${a.startingRect.width}px`),this.renderer.setStyle(a.clonedNode,"cursor",Ma(a.edges,ue)),this.renderer.addClass(a.clonedNode,Vr),a.clonedNode.scrollTop=a.startingRect.scrollTop,a.clonedNode.scrollLeft=a.startingRect.scrollLeft),this.resizeStart.observers.length>0&&this.zone.run(()=>{this.resizeStart.emit({edges:Nt({edges:b,initialRectangle:V,newRectangle:V}),rectangle:wa(V,{},0,0)})})}),n.pipe(x(this.destroy$)).subscribe(()=>{a&&(this.renderer.removeClass(this.elm.nativeElement,Ia),this.renderer.setStyle(document.body,"cursor",""),this.renderer.setStyle(this.elm.nativeElement,"cursor",""),this.resizeEnd.observers.length>0&&this.zone.run(()=>{this.resizeEnd.emit({edges:Nt({edges:a.edges,initialRectangle:a.startingRect,newRectangle:a.currentRect}),rectangle:a.currentRect})}),o(),a=null)})}ngOnDestroy(){Gn(this.platformId)&&this.renderer.setStyle(document.body,"cursor",""),this.mousedown.complete(),this.mouseup.complete(),this.mousemove.complete(),this.destroy$.next()}setElementClass(e,t,n){n?this.renderer.addClass(e.nativeElement,t):this.renderer.removeClass(e.nativeElement,t)}static{this.\u0275fac=function(t){return new(t||i)}}static{this.\u0275dir=I({type:i,selectors:[["","mwlResizable",""]],inputs:{validateResize:"validateResize",enableGhostResize:"enableGhostResize",resizeSnapGrid:"resizeSnapGrid",resizeCursors:"resizeCursors",ghostElementPositioning:"ghostElementPositioning",allowNegativeResizes:"allowNegativeResizes",mouseMoveThrottleMS:"mouseMoveThrottleMS"},outputs:{resizeStart:"resizeStart",resizing:"resizing",resizeEnd:"resizeEnd"},exportAs:["mwlResizable"]})}}return i})(),bn=class i{static getInstance(s,e){return i.instance||(i.instance=new i(s,e)),i.instance}constructor(s,e){this.pointerDown=new Oe(t=>{let n,a;return e.runOutsideAngular(()=>{n=s.listen("document","mousedown",o=>{t.next({clientX:o.clientX,clientY:o.clientY,event:o})}),We&&(a=s.listen("document","touchstart",o=>{t.next({clientX:o.touches[0].clientX,clientY:o.touches[0].clientY,event:o})}))}),()=>{n(),We&&a()}}).pipe(rt()),this.pointerMove=new Oe(t=>{let n,a;return e.runOutsideAngular(()=>{n=s.listen("document","mousemove",o=>{t.next({clientX:o.clientX,clientY:o.clientY,event:o})}),We&&(a=s.listen("document","touchmove",o=>{t.next({clientX:o.targetTouches[0].clientX,clientY:o.targetTouches[0].clientY,event:o})}))}),()=>{n(),We&&a()}}).pipe(rt()),this.pointerUp=new Oe(t=>{let n,a,o;return e.runOutsideAngular(()=>{n=s.listen("document","mouseup",l=>{t.next({clientX:l.clientX,clientY:l.clientY,event:l})}),We&&(a=s.listen("document","touchend",l=>{t.next({clientX:l.changedTouches[0].clientX,clientY:l.changedTouches[0].clientY,event:l})}),o=s.listen("document","touchcancel",l=>{t.next({clientX:l.changedTouches[0].clientX,clientY:l.changedTouches[0].clientY,event:l})}))}),()=>{n(),We&&(a(),o())}}).pipe(rt())}},sh=(()=>{class i{constructor(){this.renderer=r(ae),this.element=r(D),this.zone=r(z),this.resizableDirective=r(Wr,{optional:!0}),this.resizeEdges={},this.eventListeners={},this.destroy$=new f}ngOnInit(){this.zone.runOutsideAngular(()=>{this.listenOnTheHost("mousedown").subscribe(e=>{this.onMousedown(e,e.clientX,e.clientY)}),this.listenOnTheHost("mouseup").subscribe(e=>{this.onMouseup(e.clientX,e.clientY)}),We&&(this.listenOnTheHost("touchstart").subscribe(e=>{this.onMousedown(e,e.touches[0].clientX,e.touches[0].clientY)}),G(this.listenOnTheHost("touchend"),this.listenOnTheHost("touchcancel")).subscribe(e=>{this.onMouseup(e.changedTouches[0].clientX,e.changedTouches[0].clientY)}))})}ngOnDestroy(){this.destroy$.next(),this.unsubscribeEventListeners()}onMousedown(e,t,n){e.cancelable&&!Sa(e)&&e.preventDefault(),this.eventListeners.touchmove||(this.eventListeners.touchmove=this.renderer.listen(this.element.nativeElement,"touchmove",a=>{this.onMousemove(a,a.targetTouches[0].clientX,a.targetTouches[0].clientY)})),this.eventListeners.mousemove||(this.eventListeners.mousemove=this.renderer.listen(this.element.nativeElement,"mousemove",a=>{this.onMousemove(a,a.clientX,a.clientY)})),this.resizable.mousedown.next({clientX:t,clientY:n,edges:this.resizeEdges})}onMouseup(e,t){this.unsubscribeEventListeners(),this.resizable.mouseup.next({clientX:e,clientY:t,edges:this.resizeEdges})}get resizable(){return this.resizableDirective||this.resizableContainer}onMousemove(e,t,n){this.resizable.mousemove.next({clientX:t,clientY:n,edges:this.resizeEdges,event:e})}unsubscribeEventListeners(){Object.keys(this.eventListeners).forEach(e=>{this.eventListeners[e](),delete this.eventListeners[e]})}listenOnTheHost(e){return kn(this.element.nativeElement,e).pipe(x(this.destroy$))}static{this.\u0275fac=function(t){return new(t||i)}}static{this.\u0275dir=I({type:i,selectors:[["","mwlResizeHandle",""]],inputs:{resizeEdges:"resizeEdges",resizableContainer:"resizableContainer"}})}}return i})(),lh=(()=>{class i{static{this.\u0275fac=function(t){return new(t||i)}}static{this.\u0275mod=B({type:i})}static{this.\u0275inj=N({})}}return i})();var uh=(()=>{class i{_entries=Q([]);entries=this._entries.asReadonly();getSnapshot(){return this._entries()}rebuildAll(e){let t=[];for(let n of e)this.collect(n,t);this.setEntries(t)}rebuildFromAggregate(e){let t=[];for(let[n,a]of Object.entries(e)){let o=n.indexOf("/"),l=o>=0?n.slice(o+1):n;t.push({name:l,linkId:a.link_id,bpf:a.bpf,tag:a.tag??null,color:a.color})}this.setEntries(t)}reconcileLink(e){let t=this._entries().filter(n=>n.linkId!==e.link_id);this.collect(e,t),this.setEntries(t)}removeLink(e){let t=this._entries().filter(n=>n.linkId!==e);this.setEntries(t)}reset(){this._entries.set([])}collect(e,t){if(e.markers)for(let[n,a]of Object.entries(e.markers))t.push({name:n,linkId:e.link_id,bpf:a.bpf,tag:a.tag,color:a.color})}setEntries(e){Yr(this._entries(),e)||this._entries.set(e)}static \u0275fac=function(t){return new(t||i)};static \u0275prov=P({token:i,factory:i.\u0275fac})}return i})();function Yr(i,s){if(i.length!==s.length)return!1;let e=n=>`${n.linkId}\0${n.name}\0${n.bpf}\0${n.tag??""}\0${n.color??""}`,t=new Set(i.map(e));for(let n of s)if(!t.has(e(n)))return!1;return!0}export{Ba as a,_s as b,Zt as c,Ws as d,Ct as e,Ys as f,$s as g,nn as h,Io as i,an as j,So as k,Do as l,on as m,rn as n,Wi as o,$i as p,Yi as q,At as r,St as s,Dt as t,no as u,en as v,Et as w,Ii as x,Si as y,Di as z,Rt as A,Cc as B,wc as C,Tc as D,qi as E,Fc as F,Ge as G,Lt as H,dn as I,Zc as J,Xi as K,Zi as L,na as M,ia as N,_d as O,aa as P,va as Q,hr as R,Yd as S,Ir as T,Dr as U,Er as V,ym as W,Ar as X,Lr as Y,xm as Z,km as _,ya as $,ot as aa,Pm as ba,zm as ca,uh as da,$m as ea,Km as fa,Xm as ga,Wr as ha,sh as ia,lh as ja}; diff --git a/gns3server/static/web-ui/chunk-3PDLDM3Z.js b/gns3server/static/web-ui/chunk-3PDLDM3Z.js deleted file mode 100644 index 2c11c1d96..000000000 --- a/gns3server/static/web-ui/chunk-3PDLDM3Z.js +++ /dev/null @@ -1 +0,0 @@ -import{b as V,d as A,e as z,l as $,m as R,p as L,q as G,r as W,s as Y,t as X,u as U,v as q,x as H}from"./chunk-ERB75MEN.js";import{$a as d,Ea as l,Eb as v,Gb as a,Hb as x,Lb as k,Ma as M,Mb as T,Nb as D,X as w,Ya as j,Za as m,Ze as B,_e as N,a as b,cb as E,da as C,db as I,eb as h,fb as e,ga as p,gb as n,ha as u,hb as P,i as S,jc as O,ob as _,qa as g,qb as f,sb as s,yc as F}from"./chunk-TYGV4UPE.js";function J(o,c){if(o&1){let t=_();e(0,"div",3)(1,"mat-icon",6),a(2,"warning"),n(),e(3,"p",7),a(4,"Ready to inject a network fault?"),n(),e(5,"div",8)(6,"p",9),a(7,"Number of faults to inject"),n(),e(8,"mat-button-toggle-group",10),D("valueChange",function(i){p(t);let y=s();return T(y.faultType,i)||(y.faultType=i),u(i)}),e(9,"mat-button-toggle",11),a(10,"1"),n(),e(11,"mat-button-toggle",11),a(12,"2"),n(),e(13,"mat-button-toggle",11),a(14,"3"),n(),e(15,"mat-button-toggle",11),a(16,"Random"),n()()(),e(17,"p",12),a(18," This will inject a simulated network fault into your topology for troubleshooting practice. Make sure you have saved your current work. "),n(),e(19,"p",12),a(20," The fault injection process will analyze your topology, select an appropriate fault, and apply it automatically. You'll be able to see the details in AI Chat. "),n()()}if(o&2){let t=s();l(8),k("value",t.faultType),l(),h("value",1),l(2),h("value",2),l(2),h("value",3),l(2),h("value","random")}}function K(o,c){if(o&1&&(e(0,"p",17),a(1),n(),e(2,"p",18),a(3,"Agent is working"),n()),o&2){let t=s(2);l(),x(t.currentStep()||"Injecting fault...")}}function Q(o,c){if(o&1&&(e(0,"div",19)(1,"mat-icon",20),a(2),n(),e(3,"div",21)(4,"p",22),a(5),n(),e(6,"p",23),a(7,"Check the AI Chat panel for detailed execution history and tool results."),n()()()),o&2){let t=s(2);v("success",t.completionStatus()==="success")("error",t.completionStatus()==="error")("aborted",t.completionStatus()==="aborted"),l(2),x(t.completionStatus()==="success"?"check_circle":t.completionStatus()==="aborted"?"cancel":"error"),l(3),x(t.completionTitle())}}function Z(o,c){if(o&1&&(e(0,"div",26)(1,"mat-icon"),a(2),n(),e(3,"div",27)(4,"p",28),a(5),n()()()),o&2){let t=c.$implicit,r=s(3);v("success",t.type==="success")("error",t.type==="error"),l(2),x(r.getEventIcon(t.type)),l(3),x(t.message)}}function tt(o,c){if(o&1&&(e(0,"div",16)(1,"h3"),a(2,"Progress"),n(),e(3,"div",24),E(4,Z,6,6,"div",25,j().trackByEventId,!0),n()()),o&2){let t=s(2);l(4),I(t.displayedEvents())}}function et(o,c){if(o&1&&(e(0,"div",13),P(1,"img",14),n(),m(2,K,4,1),m(3,Q,8,8,"div",15),m(4,tt,6,0,"div",16)),o&2){let t=s();v("injecting",t.isInjecting()),l(2),d(t.isInjecting()?2:-1),l(),d(t.completed()&&!t.isInjecting()?3:-1),l(),d(t.isInjecting()&&t.displayedEvents().length>0?4:-1)}}function nt(o,c){if(o&1){let t=_();e(0,"button",29),f("click",function(){p(t);let i=s();return u(i.onCancelConfirm())}),a(1,"Cancel"),n(),e(2,"button",30),f("click",function(){p(t);let i=s();return u(i.onConfirmInject())}),e(3,"mat-icon"),a(4,"bug_report"),n(),e(5,"span"),a(6,"Confirm & Inject"),n()()}}function it(o,c){if(o&1){let t=_();e(0,"button",29),f("click",function(){p(t);let i=s();return u(i.onAbort())}),a(1,"Abort"),n()}}function ot(o,c){if(o&1){let t=_();e(0,"button",29),f("click",function(){p(t);let i=s();return u(i.onCancel())}),a(1,"Cancel"),n(),e(2,"button",30),f("click",function(){p(t);let i=s();return u(i.onInject())}),e(3,"mat-icon"),a(4,"bug_report"),n(),e(5,"span"),a(6,"Inject Fault"),n()()}}function at(o,c){if(o&1){let t=_();e(0,"button",29),f("click",function(){p(t);let i=s();return u(i.onViewDetails())}),e(1,"mat-icon"),a(2,"chat"),n(),e(3,"span"),a(4,"View in AI Chat"),n()(),e(5,"button",31),f("click",function(){p(t);let i=s();return u(i.onClose())}),e(6,"mat-icon"),a(7,"check"),n(),e(8,"span"),a(9,"Done"),n()()}}var bt=(()=>{class o{dialogRef=C($);data=C(R);aiChatService=C(H);controller=this.data.controller;project=this.data.project;isInjecting=g(!1);completed=g(!1);showConfirm=g(!1);faultType=O(1);faceState=g("idle");completionStatus=g("success");completionTitle=g("");currentStep=g("");eventsBuffer=[];displayedEvents=g([]);firstToolCallProcessed=!1;destroy$=new S;static MAX_DISPLAYED_EVENTS=3;ngOnInit(){this.faceState.set("idle")}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}onInject(){this.showConfirm.set(!0)}onCancelConfirm(){this.showConfirm.set(!1)}onConfirmInject(){if(this.isInjecting())return;this.showConfirm.set(!1),this.isInjecting.set(!0),this.completed.set(!1),this.faceState.set("injecting"),this.eventsBuffer=[],this.displayedEvents.set([]),this.firstToolCallProcessed=!1;let r=`Inject ${this.faultType()==="random"?"random":String(this.faultType())} network fault(s) for troubleshooting practice`;this.aiChatService.injectFault(this.controller,this.project.project_id,r).pipe(w(this.destroy$)).subscribe({next:i=>{this.handleFaultEvent(i)},error:i=>{this.handleError(i)},complete:()=>{}})}handleFaultEvent(t){if(console.log("Fault injection event:",t),!(t.type==="heartbeat"||t.type==="content"))switch(t.type){case"tool_call":if(t.tool_call&&!this.firstToolCallProcessed){let r=t.tool_call.function.name;this.currentStep.set(`Preparing: ${r}`),this.addEvent({type:"tool_call",message:`Preparing: ${r}`}),this.firstToolCallProcessed=!0}break;case"tool_start":t.tool_name&&(this.currentStep.set(`Executing: ${t.tool_name}`),this.addEvent({type:"info",message:`Executing: ${t.tool_name}`}));break;case"tool_end":t.tool_name&&this.addEvent({type:"success",message:`Completed: ${t.tool_name}`});break;case"error":this.handleError(t);break;case"done":this.isInjecting.set(!1),this.completed.set(!0),this.faceState.set("success"),this.completionStatus.set("success"),this.completionTitle.set("Fault injected successfully!"),this.currentStep.set("");break}}handleError(t){console.error("Fault injection error:",t);let r=t?.error?.message||t?.message||t?.error||"Failed to inject fault";this.addEvent({type:"error",message:"Error injecting fault",details:r}),this.isInjecting.set(!1),this.completed.set(!0),this.faceState.set("error"),this.completionStatus.set("error"),this.completionTitle.set("Failed to inject fault"),this.currentStep.set("")}addEvent(t){let r=b({id:`event_${Date.now()}_${Math.random().toString(36).substring(2,11)}`,timestamp:new Date().toISOString()},t);this.eventsBuffer.push(r),this.eventsBuffer.length>o.MAX_DISPLAYED_EVENTS&&this.eventsBuffer.shift(),this.displayedEvents.set([...this.eventsBuffer])}getEventIcon(t){switch(t){case"info":return"info";case"tool_call":return"build";case"success":return"check_circle";case"error":return"error";default:return"info"}}onAbort(){this.isInjecting.set(!1),this.completed.set(!0),this.faceState.set("aborted"),this.completionStatus.set("aborted"),this.completionTitle.set("Fault injection aborted"),this.currentStep.set(""),this.destroy$.next(),this.addEvent({type:"info",message:"Fault injection aborted by user"})}onViewDetails(){this.dialogRef.close({success:this.completionStatus()==="success",openAIChat:!0})}onCancel(){this.isInjecting()||this.dialogRef.close(null)}onClose(){this.dialogRef.close({success:this.completionStatus()==="success",events:this.displayedEvents()})}trackByEventId(t,r){return r.id}static \u0275fac=function(r){return new(r||o)};static \u0275cmp=M({type:o,selectors:[["app-fault-injection-dialog"]],inputs:{faultType:[1,"faultType"]},outputs:{faultType:"faultTypeChange"},decls:13,vars:6,consts:[["mat-dialog-title","",1,"fault-injection-title"],[1,"fault-injection-icon"],["mat-dialog-content","",1,"fault-injection-content"],[1,"confirm-panel"],["mat-dialog-actions","","align","end"],["mat-button",""],[1,"confirm-panel__icon"],[1,"confirm-panel__title"],[1,"fault-type-selector"],[1,"fault-type-selector__label"],["hideSingleSelectionIndicator","true",1,"fault-type-selector__group",3,"valueChange","value"],[1,"fault-type-btn",3,"value"],[1,"confirm-panel__desc"],[1,"animation-area"],["src","assets/gns3_icon.svg","alt","GNS3",1,"gns3-logo"],[1,"completion-message",3,"success","error","aborted"],[1,"events-section"],[1,"status-main"],[1,"status-sub"],[1,"completion-message"],[1,"completion-icon"],[1,"completion-content"],[1,"completion-title"],[1,"completion-text"],[1,"events-list"],[1,"event",3,"success","error"],[1,"event"],[1,"event-content"],[1,"event-msg"],["mat-button","",3,"click"],["mat-raised-button","","color","warn",3,"click"],["mat-raised-button","","color","primary",3,"click"]],template:function(r,i){r&1&&(e(0,"h2",0)(1,"mat-icon",1),a(2,"bug_report"),n(),e(3,"span"),a(4,"Fault Injection"),n()(),e(5,"div",2),m(6,J,21,5,"div",3),m(7,et,5,5),n(),e(8,"div",4),m(9,nt,7,0),m(10,it,2,0,"button",5),m(11,ot,7,0),m(12,at,10,0),n()),r&2&&(l(6),d(i.showConfirm()?6:-1),l(),d(!i.showConfirm()||i.isInjecting()||i.completed()?7:-1),l(2),d(i.showConfirm()?9:-1),l(),d(i.isInjecting()?10:-1),l(),d(!i.showConfirm()&&!i.isInjecting()&&!i.completed()?11:-1),l(),d(i.completed()&&!i.isInjecting()?12:-1))},dependencies:[F,Y,L,W,G,N,B,z,A,V,q,X,U],styles:[".fault-injection-title[_ngcontent-%COMP%]{display:flex;align-items:center;gap:12px}.fault-injection-icon[_ngcontent-%COMP%]{font-size:24px;width:24px;height:24px;color:var(--mat-sys-error)}.fault-injection-content[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:24px}.confirm-panel[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;gap:16px;padding:32px 24px;text-align:center}.confirm-panel__icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;color:var(--mat-sys-error)}.confirm-panel__title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0;color:var(--mat-sys-on-surface)}.confirm-panel__desc[_ngcontent-%COMP%]{font-size:14px;margin:0;color:var(--mat-sys-on-surface-variant);line-height:1.6;text-align:left;width:100%}.fault-type-selector[_ngcontent-%COMP%]{width:100%;max-width:360px}.fault-type-selector__label[_ngcontent-%COMP%]{font-size:12px;font-weight:500;margin:0 0 8px;text-align:left;color:var(--mat-sys-on-surface-variant)}.fault-type-selector__group[_ngcontent-%COMP%]{display:flex;gap:6px;width:100%;border:none;border-radius:0}.fault-type-btn[_ngcontent-%COMP%]{flex:1;min-width:0;height:36px;font-size:13px;font-weight:500;border:1px solid var(--mat-sys-outline-variant);border-radius:8px;color:var(--mat-sys-on-surface);display:flex;align-items:center;justify-content:center}.fault-type-btn.mat-button-toggle-checked[_ngcontent-%COMP%]{border-color:var(--mat-sys-primary);background:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);font-weight:600}.animation-area[_ngcontent-%COMP%]{position:relative;display:flex;align-items:center;justify-content:center;padding:32px;background:var(--mat-sys-surface-container-low);border-radius:12px;min-height:200px}.animation-area.injecting[_ngcontent-%COMP%]{background:color-mix(in srgb,var(--mat-sys-error-container) 30%,var(--mat-sys-surface-container-low))}.gns3-logo[_ngcontent-%COMP%]{width:120px;height:120px}.status-main[_ngcontent-%COMP%]{font-size:18px;font-weight:500;margin:0;text-align:center;color:var(--mat-sys-on-surface)}.status-sub[_ngcontent-%COMP%]{font-size:14px;margin:0;text-align:center;color:var(--mat-sys-on-surface-variant)}.completion-message[_ngcontent-%COMP%]{display:flex;gap:16px;padding:20px;background:var(--mat-sys-surface-container-low);border-radius:12px;border-left:4px solid var(--mat-sys-primary)}.completion-message.success[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-primary);background:var(--mat-sys-primary-container-low)}.completion-message.error[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-error);background:var(--mat-sys-error-container-low)}.completion-message.aborted[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-outline);background:var(--mat-sys-surface-container-high)}.completion-icon[_ngcontent-%COMP%]{font-size:32px;width:32px;height:32px;flex-shrink:0;color:var(--mat-sys-primary)}.completion-message.error[_ngcontent-%COMP%] .completion-icon[_ngcontent-%COMP%]{color:var(--mat-sys-error)}.completion-message.aborted[_ngcontent-%COMP%] .completion-icon[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface-variant)}.completion-content[_ngcontent-%COMP%]{flex:1}.completion-title[_ngcontent-%COMP%]{font-size:18px;font-weight:500;margin:0 0 8px;color:var(--mat-sys-on-surface)}.completion-text[_ngcontent-%COMP%]{font-size:14px;margin:0;color:var(--mat-sys-on-surface-variant);line-height:1.5}.events-section[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:12px}.events-section[_ngcontent-%COMP%] h3[_ngcontent-%COMP%]{font-size:16px;font-weight:500;margin:0;color:var(--mat-sys-on-surface)}.events-list[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:8px}.event[_ngcontent-%COMP%]{display:flex;gap:12px;padding:12px 16px;background:var(--mat-sys-surface-container-low);border-radius:8px;border-left:3px solid var(--mat-sys-outline)}.event.success[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-primary);background:var(--mat-sys-primary-container-low)}.event.error[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-error);background:var(--mat-sys-error-container-low)}.event[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:20px;width:20px;height:20px;flex-shrink:0;color:var(--mat-sys-on-surface-variant)}.event-content[_ngcontent-%COMP%]{flex:1;min-width:0}.event-msg[_ngcontent-%COMP%]{font-size:14px;font-weight:500;margin:0 0 4px;color:var(--mat-sys-on-surface);word-wrap:break-word}.event-details[_ngcontent-%COMP%]{font-size:12px;margin:0;color:var(--mat-sys-on-surface-variant);word-wrap:break-word}"],changeDetection:0})}return o})();export{bt as FaultInjectionDialogComponent}; diff --git a/gns3server/static/web-ui/chunk-ERB75MEN.js b/gns3server/static/web-ui/chunk-ERB75MEN.js deleted file mode 100644 index bbc0b1527..000000000 --- a/gns3server/static/web-ui/chunk-ERB75MEN.js +++ /dev/null @@ -1,685 +0,0 @@ -import{$ as D,$a as fe,Bd as Mt,Cb as kt,D as ie,Db as wt,Dc as Ce,Dd as Oe,E as rt,Ea as k,Eb as O,Fa as ue,Fb as Tt,G as f,Ga as me,Gd as Ft,Ge as Pe,He as ut,Ia as ge,Id as Me,Jc as At,L as N,Ma as A,Na as E,Nc as Se,Oa as L,Oc as Ie,P as ae,Qa as he,Qb as be,Qc as Et,Ra as Z,S as re,Sa as Y,Uc as $t,Ue as Le,V as pt,We as Be,Xa as m,Xc as ke,Y as ft,Yc as lt,Ye as je,Za as pe,_ as se,_b as ve,a as M,aa as x,b as Xt,ba as h,bf as Nt,ca as S,cc as Dt,cd as j,cf as ze,da as l,eb as B,ef as Ge,fb as v,fd as we,g as te,gb as w,h as Ht,hb as X,i as T,ia as Ut,j as ee,ja as le,k as Vt,ka as F,kc as st,ke as Rt,la as J,ld as z,mc as y,na as P,nb as yt,nc as xt,nd as Qt,ne as ot,oa as ce,od as Te,p as Q,pa as _t,pb as Wt,pd as De,q as g,qa as bt,qb as tt,sa as de,sb as Ct,sd as ct,se as Fe,t as ne,ta as vt,tb as St,td as dt,te as Re,u as q,ua as I,ub as It,uc as ye,ud as nt,vb as _e,vd as Ot,ve as Ne,wb as et,xb as U,xd as xe,y as ht,ya as K,yb as W,yd as Ae,z as oe,zd as Ee}from"./chunk-TYGV4UPE.js";var dn=["determinateSpinner"];function un(o,s){if(o&1&&(Ut(),v(0,"svg",11),X(1,"circle",12),w()),o&2){let t=Ct();m("viewBox",t._viewBox()),k(),wt("stroke-dasharray",t._strokeCircumference(),"px")("stroke-dashoffset",t._strokeCircumference()/2,"px")("stroke-width",t._circleStrokeWidth(),"%"),m("r",t._circleRadius())}}var mn=new h("mat-progress-spinner-default-options",{providedIn:"root",factory:()=>({diameter:He})}),He=100,gn=10,io=(()=>{class o{_elementRef=l(I);_noopAnimations;get color(){return this._color||this._defaultColor}set color(t){this._color=t}_color;_defaultColor="primary";_determinateCircle;constructor(){let t=l(mn),e=Pe(),n=this._elementRef.nativeElement;this._noopAnimations=e==="di-disabled"&&!!t&&!t._forceAnimations,this.mode=n.nodeName.toLowerCase()==="mat-spinner"?"indeterminate":"determinate",!this._noopAnimations&&e==="reduced-motion"&&n.classList.add("mat-progress-spinner-reduced-motion"),t&&(t.color&&(this.color=this._defaultColor=t.color),t.diameter&&(this.diameter=t.diameter),t.strokeWidth&&(this.strokeWidth=t.strokeWidth))}mode;get value(){return this.mode==="determinate"?this._value:0}set value(t){this._value=Math.max(0,Math.min(100,t||0))}_value=0;get diameter(){return this._diameter}set diameter(t){this._diameter=t||0}_diameter=He;get strokeWidth(){return this._strokeWidth??this.diameter/10}set strokeWidth(t){this._strokeWidth=t||0}_strokeWidth;_circleRadius(){return(this.diameter-gn)/2}_viewBox(){let t=this._circleRadius()*2+this.strokeWidth;return`0 0 ${t} ${t}`}_strokeCircumference(){return 2*Math.PI*this._circleRadius()}_strokeDashOffset(){return this.mode==="determinate"?this._strokeCircumference()*(100-this._value)/100:null}_circleStrokeWidth(){return this.strokeWidth/this.diameter*100}static \u0275fac=function(e){return new(e||o)};static \u0275cmp=A({type:o,selectors:[["mat-progress-spinner"],["mat-spinner"]],viewQuery:function(e,n){if(e&1&&et(dn,5),e&2){let i;U(i=W())&&(n._determinateCircle=i.first)}},hostAttrs:["role","progressbar","tabindex","-1",1,"mat-mdc-progress-spinner","mdc-circular-progress"],hostVars:18,hostBindings:function(e,n){e&2&&(m("aria-valuemin",0)("aria-valuemax",100)("aria-valuenow",n.mode==="determinate"?n.value:null)("mode",n.mode),Tt("mat-"+n.color),wt("width",n.diameter,"px")("height",n.diameter,"px")("--mat-progress-spinner-size",n.diameter+"px")("--mat-progress-spinner-active-indicator-width",n.diameter+"px"),O("_mat-animation-noopable",n._noopAnimations)("mdc-circular-progress--indeterminate",n.mode==="indeterminate"))},inputs:{color:"color",mode:"mode",value:[2,"value","value",xt],diameter:[2,"diameter","diameter",xt],strokeWidth:[2,"strokeWidth","strokeWidth",xt]},exportAs:["matProgressSpinner"],decls:14,vars:11,consts:[["circle",""],["determinateSpinner",""],["aria-hidden","true",1,"mdc-circular-progress__determinate-container"],["xmlns","http://www.w3.org/2000/svg","focusable","false",1,"mdc-circular-progress__determinate-circle-graphic"],["cx","50%","cy","50%",1,"mdc-circular-progress__determinate-circle"],["aria-hidden","true",1,"mdc-circular-progress__indeterminate-container"],[1,"mdc-circular-progress__spinner-layer"],[1,"mdc-circular-progress__circle-clipper","mdc-circular-progress__circle-left"],[3,"ngTemplateOutlet"],[1,"mdc-circular-progress__gap-patch"],[1,"mdc-circular-progress__circle-clipper","mdc-circular-progress__circle-right"],["xmlns","http://www.w3.org/2000/svg","focusable","false",1,"mdc-circular-progress__indeterminate-circle-graphic"],["cx","50%","cy","50%"]],template:function(e,n){if(e&1&&(Y(0,un,2,8,"ng-template",null,0,ve),v(2,"div",2,1),Ut(),v(4,"svg",3),X(5,"circle",4),w()(),le(),v(6,"div",5)(7,"div",6)(8,"div",7),yt(9,8),w(),v(10,"div",9),yt(11,8),w(),v(12,"div",10),yt(13,8),w()()()),e&2){let i=kt(1);k(4),m("viewBox",n._viewBox()),k(),wt("stroke-dasharray",n._strokeCircumference(),"px")("stroke-dashoffset",n._strokeDashOffset(),"px")("stroke-width",n._circleStrokeWidth(),"%"),m("r",n._circleRadius()),k(4),B("ngTemplateOutlet",i),k(2),B("ngTemplateOutlet",i),k(2),B("ngTemplateOutlet",i)}},dependencies:[ye],styles:[`.mat-mdc-progress-spinner { - --mat-progress-spinner-animation-multiplier: 1; - display: block; - overflow: hidden; - line-height: 0; - position: relative; - direction: ltr; - transition: opacity 250ms cubic-bezier(0.4, 0, 0.6, 1); -} -.mat-mdc-progress-spinner circle { - stroke-width: var(--mat-progress-spinner-active-indicator-width, 4px); -} -.mat-mdc-progress-spinner._mat-animation-noopable, .mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__determinate-circle { - transition: none !important; -} -.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__indeterminate-circle-graphic, -.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__spinner-layer, -.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__indeterminate-container { - animation: none !important; -} -.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__indeterminate-container circle { - stroke-dasharray: 0 !important; -} -@media (forced-colors: active) { - .mat-mdc-progress-spinner .mdc-circular-progress__indeterminate-circle-graphic, - .mat-mdc-progress-spinner .mdc-circular-progress__determinate-circle { - stroke: currentColor; - stroke: CanvasText; - } -} - -.mat-progress-spinner-reduced-motion { - --mat-progress-spinner-animation-multiplier: 1.25; -} - -.mdc-circular-progress__determinate-container, -.mdc-circular-progress__indeterminate-circle-graphic, -.mdc-circular-progress__indeterminate-container, -.mdc-circular-progress__spinner-layer { - position: absolute; - width: 100%; - height: 100%; -} - -.mdc-circular-progress__determinate-container { - transform: rotate(-90deg); -} -.mdc-circular-progress--indeterminate .mdc-circular-progress__determinate-container { - opacity: 0; -} - -.mdc-circular-progress__indeterminate-container { - font-size: 0; - letter-spacing: 0; - white-space: nowrap; - opacity: 0; -} -.mdc-circular-progress--indeterminate .mdc-circular-progress__indeterminate-container { - opacity: 1; - animation: mdc-circular-progress-container-rotate calc(1568.2352941176ms * var(--mat-progress-spinner-animation-multiplier)) linear infinite; -} - -.mdc-circular-progress__determinate-circle-graphic, -.mdc-circular-progress__indeterminate-circle-graphic { - fill: transparent; -} - -.mat-mdc-progress-spinner .mdc-circular-progress__determinate-circle, -.mat-mdc-progress-spinner .mdc-circular-progress__indeterminate-circle-graphic { - stroke: var(--mat-progress-spinner-active-indicator-color, var(--mat-sys-primary)); -} -@media (forced-colors: active) { - .mat-mdc-progress-spinner .mdc-circular-progress__determinate-circle, - .mat-mdc-progress-spinner .mdc-circular-progress__indeterminate-circle-graphic { - stroke: CanvasText; - } -} - -.mdc-circular-progress__determinate-circle { - transition: stroke-dashoffset 500ms cubic-bezier(0, 0, 0.2, 1); -} - -.mdc-circular-progress__gap-patch { - position: absolute; - top: 0; - left: 47.5%; - box-sizing: border-box; - width: 5%; - height: 100%; - overflow: hidden; -} - -.mdc-circular-progress__gap-patch .mdc-circular-progress__indeterminate-circle-graphic { - left: -900%; - width: 2000%; - transform: rotate(180deg); -} -.mdc-circular-progress__circle-clipper .mdc-circular-progress__indeterminate-circle-graphic { - width: 200%; -} -.mdc-circular-progress__circle-right .mdc-circular-progress__indeterminate-circle-graphic { - left: -100%; -} -.mdc-circular-progress--indeterminate .mdc-circular-progress__circle-left .mdc-circular-progress__indeterminate-circle-graphic { - animation: mdc-circular-progress-left-spin calc(1333ms * var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both; -} -.mdc-circular-progress--indeterminate .mdc-circular-progress__circle-right .mdc-circular-progress__indeterminate-circle-graphic { - animation: mdc-circular-progress-right-spin calc(1333ms * var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both; -} - -.mdc-circular-progress__circle-clipper { - display: inline-flex; - position: relative; - width: 50%; - height: 100%; - overflow: hidden; -} - -.mdc-circular-progress--indeterminate .mdc-circular-progress__spinner-layer { - animation: mdc-circular-progress-spinner-layer-rotate calc(5332ms * var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both; -} - -@keyframes mdc-circular-progress-container-rotate { - to { - transform: rotate(360deg); - } -} -@keyframes mdc-circular-progress-spinner-layer-rotate { - 12.5% { - transform: rotate(135deg); - } - 25% { - transform: rotate(270deg); - } - 37.5% { - transform: rotate(405deg); - } - 50% { - transform: rotate(540deg); - } - 62.5% { - transform: rotate(675deg); - } - 75% { - transform: rotate(810deg); - } - 87.5% { - transform: rotate(945deg); - } - 100% { - transform: rotate(1080deg); - } -} -@keyframes mdc-circular-progress-left-spin { - from { - transform: rotate(265deg); - } - 50% { - transform: rotate(130deg); - } - to { - transform: rotate(265deg); - } -} -@keyframes mdc-circular-progress-right-spin { - from { - transform: rotate(-265deg); - } - 50% { - transform: rotate(-130deg); - } - to { - transform: rotate(-265deg); - } -} -`],encapsulation:2,changeDetection:0})}return o})();var ao=(()=>{class o{static \u0275fac=function(e){return new(e||o)};static \u0275mod=E({type:o});static \u0275inj=x({imports:[j]})}return o})();function Ve(o){return Error(`Unable to find icon with the name "${o}"`)}function pn(){return Error("Could not find HttpClient for use with Angular Material icons. Please add provideHttpClient() to your providers.")}function Ue(o){return Error(`The URL provided to MatIconRegistry was not trusted as a resource URL via Angular's DomSanitizer. Attempted URL was "${o}".`)}function We(o){return Error(`The literal provided to MatIconRegistry was not trusted as safe HTML by Angular's DomSanitizer. Attempted literal was "${o}".`)}var R=class{url;svgText;options;svgElement=null;constructor(s,t,e){this.url=s,this.svgText=t,this.options=e}},Qe=(()=>{class o{_httpClient;_sanitizer;_errorHandler;_document;_svgIconConfigs=new Map;_iconSetConfigs=new Map;_cachedIconsByUrl=new Map;_inProgressUrlFetches=new Map;_fontCssClassesByAlias=new Map;_resolvers=[];_defaultFontSetClass=["material-icons","mat-ligature-font"];constructor(t,e,n,i){this._httpClient=t,this._sanitizer=e,this._errorHandler=i,this._document=n}addSvgIcon(t,e,n){return this.addSvgIconInNamespace("",t,e,n)}addSvgIconLiteral(t,e,n){return this.addSvgIconLiteralInNamespace("",t,e,n)}addSvgIconInNamespace(t,e,n,i){return this._addSvgIconConfig(t,e,new R(n,null,i))}addSvgIconResolver(t){return this._resolvers.push(t),this}addSvgIconLiteralInNamespace(t,e,n,i){let a=this._sanitizer.sanitize(K.HTML,n);if(!a)throw We(n);let r=ot(a);return this._addSvgIconConfig(t,e,new R("",r,i))}addSvgIconSet(t,e){return this.addSvgIconSetInNamespace("",t,e)}addSvgIconSetLiteral(t,e){return this.addSvgIconSetLiteralInNamespace("",t,e)}addSvgIconSetInNamespace(t,e,n){return this._addSvgIconSetConfig(t,new R(e,null,n))}addSvgIconSetLiteralInNamespace(t,e,n){let i=this._sanitizer.sanitize(K.HTML,e);if(!i)throw We(e);let a=ot(i);return this._addSvgIconSetConfig(t,new R("",a,n))}registerFontClassAlias(t,e=t){return this._fontCssClassesByAlias.set(t,e),this}classNameForFontAlias(t){return this._fontCssClassesByAlias.get(t)||t}setDefaultFontSetClass(...t){return this._defaultFontSetClass=t,this}getDefaultFontSetClass(){return this._defaultFontSetClass}getSvgIconFromUrl(t){let e=this._sanitizer.sanitize(K.RESOURCE_URL,t);if(!e)throw Ue(t);let n=this._cachedIconsByUrl.get(e);return n?Q(Pt(n)):this._loadSvgIconFromConfig(new R(t,null)).pipe(ft(i=>this._cachedIconsByUrl.set(e,i)),q(i=>Pt(i)))}getNamedSvgIcon(t,e=""){let n=$e(e,t),i=this._svgIconConfigs.get(n);if(i)return this._getSvgFromConfig(i);if(i=this._getIconConfigFromResolvers(e,t),i)return this._svgIconConfigs.set(n,i),this._getSvgFromConfig(i);let a=this._iconSetConfigs.get(e);return a?this._getSvgFromIconSetConfigs(t,a):g(Ve(n))}ngOnDestroy(){this._resolvers=[],this._svgIconConfigs.clear(),this._iconSetConfigs.clear(),this._cachedIconsByUrl.clear()}_getSvgFromConfig(t){return t.svgText?Q(Pt(this._svgElementFromConfig(t))):this._loadSvgIconFromConfig(t).pipe(q(e=>Pt(e)))}_getSvgFromIconSetConfigs(t,e){let n=this._extractIconWithNameFromAnySet(t,e);if(n)return Q(n);let i=e.filter(a=>!a.svgText).map(a=>this._loadSvgIconSetFromConfig(a).pipe(f(r=>{let c=`Loading icon set URL: ${this._sanitizer.sanitize(K.RESOURCE_URL,a.url)} failed: ${r.message}`;return this._errorHandler.handleError(new Error(c)),Q(null)})));return oe(i).pipe(q(()=>{let a=this._extractIconWithNameFromAnySet(t,e);if(!a)throw Ve(t);return a}))}_extractIconWithNameFromAnySet(t,e){for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.svgText&&i.svgText.toString().indexOf(t)>-1){let a=this._svgElementFromConfig(i),r=this._extractSvgIconFromSet(a,t,i.options);if(r)return r}}return null}_loadSvgIconFromConfig(t){return this._fetchIcon(t).pipe(ft(e=>t.svgText=e),q(()=>this._svgElementFromConfig(t)))}_loadSvgIconSetFromConfig(t){return t.svgText?Q(null):this._fetchIcon(t).pipe(ft(e=>t.svgText=e))}_extractSvgIconFromSet(t,e,n){let i=t.querySelector(`[id="${e}"]`);if(!i)return null;let a=i.cloneNode(!0);if(a.removeAttribute("id"),a.nodeName.toLowerCase()==="svg")return this._setSvgAttributes(a,n);if(a.nodeName.toLowerCase()==="symbol")return this._setSvgAttributes(this._toSvgElement(a),n);let r=this._svgElementFromString(ot(""));return r.appendChild(a),this._setSvgAttributes(r,n)}_svgElementFromString(t){let e=this._document.createElement("DIV");e.innerHTML=t;let n=e.querySelector("svg");if(!n)throw Error(" tag not found");return n}_toSvgElement(t){let e=this._svgElementFromString(ot("")),n=t.attributes;for(let i=0;iot(c)),ae(()=>this._inProgressUrlFetches.delete(a)),re());return this._inProgressUrlFetches.set(a,d),d}_addSvgIconConfig(t,e,n){return this._svgIconConfigs.set($e(t,e),n),this}_addSvgIconSetConfig(t,e){let n=this._iconSetConfigs.get(t);return n?n.push(e):this._iconSetConfigs.set(t,[e]),this}_svgElementFromConfig(t){if(!t.svgElement){let e=this._svgElementFromString(t.svgText);this._setSvgAttributes(e,t.options),t.svgElement=e}return t.svgElement}_getIconConfigFromResolvers(t,e){for(let n=0;n{let o=l(J),s=o?o.location:null;return{getPathname:()=>s?s.pathname+s.search:""}}}),qe=["clip-path","color-profile","src","cursor","fill","filter","marker","marker-start","marker-mid","marker-end","mask","stroke"],yn=qe.map(o=>`[${o}]`).join(", "),Cn=/^url\(['"]?#(.*?)['"]?\)$/,Do=(()=>{class o{_elementRef=l(I);_iconRegistry=l(Qe);_location=l(vn);_errorHandler=l(_t);_defaultColor;get color(){return this._color||this._defaultColor}set color(t){this._color=t}_color;inline=!1;get svgIcon(){return this._svgIcon}set svgIcon(t){t!==this._svgIcon&&(t?this._updateSvgIcon(t):this._svgIcon&&this._clearSvgElement(),this._svgIcon=t)}_svgIcon;get fontSet(){return this._fontSet}set fontSet(t){let e=this._cleanupFontValue(t);e!==this._fontSet&&(this._fontSet=e,this._updateFontIconClasses())}_fontSet;get fontIcon(){return this._fontIcon}set fontIcon(t){let e=this._cleanupFontValue(t);e!==this._fontIcon&&(this._fontIcon=e,this._updateFontIconClasses())}_fontIcon;_previousFontSetClass=[];_previousFontIconClass;_svgName=null;_svgNamespace=null;_previousPath;_elementsWithExternalReferences;_currentIconFetch=te.EMPTY;constructor(){let t=l(new Dt("aria-hidden"),{optional:!0}),e=l(bn,{optional:!0});e&&(e.color&&(this.color=this._defaultColor=e.color),e.fontSet&&(this.fontSet=e.fontSet)),t||this._elementRef.nativeElement.setAttribute("aria-hidden","true")}_splitIconName(t){if(!t)return["",""];let e=t.split(":");switch(e.length){case 1:return["",e[0]];case 2:return e;default:throw Error(`Invalid icon name: "${t}"`)}}ngOnInit(){this._updateFontIconClasses()}ngAfterViewChecked(){let t=this._elementsWithExternalReferences;if(t&&t.size){let e=this._location.getPathname();e!==this._previousPath&&(this._previousPath=e,this._prependPathToReferences(e))}}ngOnDestroy(){this._currentIconFetch.unsubscribe(),this._elementsWithExternalReferences&&this._elementsWithExternalReferences.clear()}_usingFontIcon(){return!this.svgIcon}_setSvgElement(t){this._clearSvgElement();let e=this._location.getPathname();this._previousPath=e,this._cacheChildrenWithExternalReferences(t),this._prependPathToReferences(e),this._elementRef.nativeElement.appendChild(t)}_clearSvgElement(){let t=this._elementRef.nativeElement,e=t.childNodes.length;for(this._elementsWithExternalReferences&&this._elementsWithExternalReferences.clear();e--;){let n=t.childNodes[e];(n.nodeType!==1||n.nodeName.toLowerCase()==="svg")&&n.remove()}}_updateFontIconClasses(){if(!this._usingFontIcon())return;let t=this._elementRef.nativeElement,e=(this.fontSet?this._iconRegistry.classNameForFontAlias(this.fontSet).split(/ +/):this._iconRegistry.getDefaultFontSetClass()).filter(n=>n.length>0);this._previousFontSetClass.forEach(n=>t.classList.remove(n)),e.forEach(n=>t.classList.add(n)),this._previousFontSetClass=e,this.fontIcon!==this._previousFontIconClass&&!e.includes("mat-ligature-font")&&(this._previousFontIconClass&&t.classList.remove(this._previousFontIconClass),this.fontIcon&&t.classList.add(this.fontIcon),this._previousFontIconClass=this.fontIcon)}_cleanupFontValue(t){return typeof t=="string"?t.trim().split(" ")[0]:t}_prependPathToReferences(t){let e=this._elementsWithExternalReferences;e&&e.forEach((n,i)=>{n.forEach(a=>{i.setAttribute(a.name,`url('${t}#${a.value}')`)})})}_cacheChildrenWithExternalReferences(t){let e=t.querySelectorAll(yn),n=this._elementsWithExternalReferences=this._elementsWithExternalReferences||new Map;for(let i=0;i{let r=e[i],d=r.getAttribute(a),c=d?d.match(Cn):null;if(c){let C=n.get(r);C||(C=[],n.set(r,C)),C.push({name:a,value:c[1]})}})}_updateSvgIcon(t){if(this._svgNamespace=null,this._svgName=null,this._currentIconFetch.unsubscribe(),t){let[e,n]=this._splitIconName(t);e&&(this._svgNamespace=e),n&&(this._svgName=n),this._currentIconFetch=this._iconRegistry.getNamedSvgIcon(n,e).pipe(N(1)).subscribe(i=>this._setSvgElement(i),i=>{let a=`Error retrieving icon ${e}:${n}! ${i.message}`;this._errorHandler.handleError(new Error(a))})}}static \u0275fac=function(e){return new(e||o)};static \u0275cmp=A({type:o,selectors:[["mat-icon"]],hostAttrs:["role","img",1,"mat-icon","notranslate"],hostVars:10,hostBindings:function(e,n){e&2&&(m("data-mat-icon-type",n._usingFontIcon()?"font":"svg")("data-mat-icon-name",n._svgName||n.fontIcon)("data-mat-icon-namespace",n._svgNamespace||n.fontSet)("fontIcon",n._usingFontIcon()?n.fontIcon:null),Tt(n.color?"mat-"+n.color:""),O("mat-icon-inline",n.inline)("mat-icon-no-color",n.color!=="primary"&&n.color!=="accent"&&n.color!=="warn"))},inputs:{color:"color",inline:[2,"inline","inline",y],svgIcon:"svgIcon",fontSet:"fontSet",fontIcon:"fontIcon"},exportAs:["matIcon"],ngContentSelectors:_n,decls:1,vars:0,template:function(e,n){e&1&&(St(),It(0))},styles:[`mat-icon, mat-icon.mat-primary, mat-icon.mat-accent, mat-icon.mat-warn { - color: var(--mat-icon-color, inherit); -} - -.mat-icon { - -webkit-user-select: none; - user-select: none; - background-repeat: no-repeat; - display: inline-block; - fill: currentColor; - height: 24px; - width: 24px; - overflow: hidden; -} -.mat-icon.mat-icon-inline { - font-size: inherit; - height: inherit; - line-height: inherit; - width: inherit; -} -.mat-icon.mat-ligature-font[fontIcon]::before { - content: attr(fontIcon); -} - -[dir=rtl] .mat-icon-rtl-mirror { - transform: scale(-1, 1); -} - -.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-prefix .mat-icon, -.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-suffix .mat-icon { - display: block; -} -.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-prefix .mat-icon-button .mat-icon, -.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-suffix .mat-icon-button .mat-icon { - margin: auto; -} -`],encapsulation:2,changeDetection:0})}return o})(),xo=(()=>{class o{static \u0275fac=function(e){return new(e||o)};static \u0275mod=E({type:o});static \u0275inj=x({imports:[j]})}return o})();function Sn(o,s){}var G=class{viewContainerRef;injector;id;role="dialog";panelClass="";hasBackdrop=!0;backdropClass="";disableClose=!1;closePredicate;width="";height="";minWidth;minHeight;maxWidth;maxHeight;positionStrategy;data=null;direction;ariaDescribedBy=null;ariaLabelledBy=null;ariaLabel=null;ariaModal=!1;autoFocus="first-tabbable";restoreFocus=!0;scrollStrategy;closeOnNavigation=!0;closeOnDestroy=!0;closeOnOverlayDetachments=!0;disableAnimations=!1;providers;container;templateContext};var Jt=(()=>{class o extends De{_elementRef=l(I);_focusTrapFactory=l(Re);_config;_interactivityChecker=l(Fe);_ngZone=l(ce);_focusMonitor=l(Rt);_renderer=l(ge);_changeDetectorRef=l(st);_injector=l(F);_platform=l(ke);_document=l(J);_portalOutlet;_focusTrapped=new T;_focusTrap=null;_elementFocusedBeforeDialogWasOpened=null;_closeInteractionType=null;_ariaLabelledByQueue=[];_isDestroyed=!1;constructor(){super(),this._config=l(G,{optional:!0})||new G,this._config.ariaLabelledBy&&this._ariaLabelledByQueue.push(this._config.ariaLabelledBy)}_addAriaLabelledBy(t){this._ariaLabelledByQueue.push(t),this._changeDetectorRef.markForCheck()}_removeAriaLabelledBy(t){let e=this._ariaLabelledByQueue.indexOf(t);e>-1&&(this._ariaLabelledByQueue.splice(e,1),this._changeDetectorRef.markForCheck())}_contentAttached(){this._initializeFocusTrap(),this._captureInitialFocus()}_captureInitialFocus(){this._trapFocus()}ngOnDestroy(){this._focusTrapped.complete(),this._isDestroyed=!0,this._restoreFocus()}attachComponentPortal(t){this._portalOutlet.hasAttached();let e=this._portalOutlet.attachComponentPortal(t);return this._contentAttached(),e}attachTemplatePortal(t){this._portalOutlet.hasAttached();let e=this._portalOutlet.attachTemplatePortal(t);return this._contentAttached(),e}attachDomPortal=t=>{this._portalOutlet.hasAttached();let e=this._portalOutlet.attachDomPortal(t);return this._contentAttached(),e};_recaptureFocus(){this._containsFocus()||this._trapFocus()}_forceFocus(t,e){this._interactivityChecker.isFocusable(t)||(t.tabIndex=-1,this._ngZone.runOutsideAngular(()=>{let n=()=>{i(),a(),t.removeAttribute("tabindex")},i=this._renderer.listen(t,"blur",n),a=this._renderer.listen(t,"mousedown",n)})),t.focus(e)}_focusByCssSelector(t,e){let n=this._elementRef.nativeElement.querySelector(t);n&&this._forceFocus(n,e)}_trapFocus(t){this._isDestroyed||ue(()=>{let e=this._elementRef.nativeElement;switch(this._config.autoFocus){case!1:case"dialog":this._containsFocus()||e.focus(t);break;case!0:case"first-tabbable":this._focusTrap?.focusInitialElement(t)||this._focusDialogContainer(t);break;case"first-heading":this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]',t);break;default:this._focusByCssSelector(this._config.autoFocus,t);break}this._focusTrapped.next()},{injector:this._injector})}_restoreFocus(){let t=this._config.restoreFocus,e=null;if(typeof t=="string"?e=this._document.querySelector(t):typeof t=="boolean"?e=t?this._elementFocusedBeforeDialogWasOpened:null:t&&(e=t),this._config.restoreFocus&&e&&typeof e.focus=="function"){let n=Et(),i=this._elementRef.nativeElement;(!n||n===this._document.body||n===i||i.contains(n))&&(this._focusMonitor?(this._focusMonitor.focusVia(e,this._closeInteractionType),this._closeInteractionType=null):e.focus())}this._focusTrap&&this._focusTrap.destroy()}_focusDialogContainer(t){this._elementRef.nativeElement.focus?.(t)}_containsFocus(){let t=this._elementRef.nativeElement,e=Et();return t===e||t.contains(e)}_initializeFocusTrap(){this._platform.isBrowser&&(this._focusTrap=this._focusTrapFactory.create(this._elementRef.nativeElement),this._document&&(this._elementFocusedBeforeDialogWasOpened=Et()))}static \u0275fac=function(e){return new(e||o)};static \u0275cmp=A({type:o,selectors:[["cdk-dialog-container"]],viewQuery:function(e,n){if(e&1&&et(ct,7),e&2){let i;U(i=W())&&(n._portalOutlet=i.first)}},hostAttrs:["tabindex","-1",1,"cdk-dialog-container"],hostVars:6,hostBindings:function(e,n){e&2&&m("id",n._config.id||null)("role",n._config.role)("aria-modal",n._config.ariaModal)("aria-labelledby",n._config.ariaLabel?null:n._ariaLabelledByQueue[0])("aria-label",n._config.ariaLabel)("aria-describedby",n._config.ariaDescribedBy||null)},features:[Z],decls:1,vars:0,consts:[["cdkPortalOutlet",""]],template:function(e,n){e&1&&Y(0,Sn,0,0,"ng-template",0)},dependencies:[ct],styles:[`.cdk-dialog-container { - display: block; - width: 100%; - height: 100%; - min-height: inherit; - max-height: inherit; -} -`],encapsulation:2})}return o})(),mt=class{overlayRef;config;componentInstance=null;componentRef=null;containerInstance;disableClose;closed=new T;backdropClick;keydownEvents;outsidePointerEvents;id;_detachSubscription;constructor(s,t){this.overlayRef=s,this.config=t,this.disableClose=t.disableClose,this.backdropClick=s.backdropClick(),this.keydownEvents=s.keydownEvents(),this.outsidePointerEvents=s.outsidePointerEvents(),this.id=t.id,this.keydownEvents.subscribe(e=>{e.keyCode===27&&!this.disableClose&&!nt(e)&&(e.preventDefault(),this.close(void 0,{focusOrigin:"keyboard"}))}),this.backdropClick.subscribe(()=>{!this.disableClose&&this._canClose()?this.close(void 0,{focusOrigin:"mouse"}):this.containerInstance._recaptureFocus?.()}),this._detachSubscription=s.detachments().subscribe(()=>{t.closeOnOverlayDetachments!==!1&&this.close()})}close(s,t){if(this._canClose(s)){let e=this.closed;this.containerInstance._closeInteractionType=t?.focusOrigin||"program",this._detachSubscription.unsubscribe(),this.overlayRef.dispose(),e.next(s),e.complete(),this.componentInstance=this.containerInstance=null}}updatePosition(){return this.overlayRef.updatePosition(),this}updateSize(s="",t=""){return this.overlayRef.updateSize({width:s,height:t}),this}addPanelClass(s){return this.overlayRef.addPanelClass(s),this}removePanelClass(s){return this.overlayRef.removePanelClass(s),this}_canClose(s){let t=this.config;return!!this.containerInstance&&(!t.closePredicate||t.closePredicate(s,t,this.componentInstance))}},In=new h("DialogScrollStrategy",{providedIn:"root",factory:()=>{let o=l(F);return()=>Ot(o)}}),kn=new h("DialogData"),wn=new h("DefaultDialogConfig");function Tn(o){let s=bt(o),t=new P;return{valueSignal:s,get value(){return s()},change:t,ngOnDestroy(){t.complete()}}}var Kt=(()=>{class o{_injector=l(F);_defaultOptions=l(wn,{optional:!0});_parentDialog=l(o,{optional:!0,skipSelf:!0});_overlayContainer=l(Ae);_idGenerator=l(z);_openDialogsAtThisLevel=[];_afterAllClosedAtThisLevel=new T;_afterOpenedAtThisLevel=new T;_ariaHiddenElements=new Map;_scrollStrategy=l(In);get openDialogs(){return this._parentDialog?this._parentDialog.openDialogs:this._openDialogsAtThisLevel}get afterOpened(){return this._parentDialog?this._parentDialog.afterOpened:this._afterOpenedAtThisLevel}afterAllClosed=ht(()=>this.openDialogs.length?this._getAfterAllClosed():this._getAfterAllClosed().pipe(pt(void 0)));constructor(){}open(t,e){let n=this._defaultOptions||new G;e=M(M({},n),e),e.id=e.id||this._idGenerator.getId("cdk-dialog-"),e.id&&this.getDialogById(e.id);let i=this._getOverlayConfig(e),a=Oe(this._injector,i),r=new mt(a,e),d=this._attachContainer(a,r,e);if(r.containerInstance=d,!this.openDialogs.length){let c=this._overlayContainer.getContainerElement();d._focusTrapped?d._focusTrapped.pipe(N(1)).subscribe(()=>{this._hideNonDialogContentFromAssistiveTechnology(c)}):this._hideNonDialogContentFromAssistiveTechnology(c)}return this._attachDialogContent(t,r,d,e),this.openDialogs.push(r),r.closed.subscribe(()=>this._removeOpenDialog(r,!0)),this.afterOpened.next(r),r}closeAll(){qt(this.openDialogs,t=>t.close())}getDialogById(t){return this.openDialogs.find(e=>e.id===t)}ngOnDestroy(){qt(this._openDialogsAtThisLevel,t=>{t.config.closeOnDestroy===!1&&this._removeOpenDialog(t,!1)}),qt(this._openDialogsAtThisLevel,t=>t.close()),this._afterAllClosedAtThisLevel.complete(),this._afterOpenedAtThisLevel.complete(),this._openDialogsAtThisLevel=[]}_getOverlayConfig(t){let e=new xe({positionStrategy:t.positionStrategy||Mt().centerHorizontally().centerVertically(),scrollStrategy:t.scrollStrategy||this._scrollStrategy(),panelClass:t.panelClass,hasBackdrop:t.hasBackdrop,direction:t.direction,minWidth:t.minWidth,minHeight:t.minHeight,maxWidth:t.maxWidth,maxHeight:t.maxHeight,width:t.width,height:t.height,disposeOnNavigation:t.closeOnNavigation,disableAnimations:t.disableAnimations});return t.backdropClass&&(e.backdropClass=t.backdropClass),e}_attachContainer(t,e,n){let i=n.injector||n.viewContainerRef?.injector,a=[{provide:G,useValue:n},{provide:mt,useValue:e},{provide:Ee,useValue:t}],r;n.container?typeof n.container=="function"?r=n.container:(r=n.container.type,a.push(...n.container.providers(n))):r=Jt;let d=new Qt(r,n.viewContainerRef,F.create({parent:i||this._injector,providers:a}));return t.attach(d).instance}_attachDialogContent(t,e,n,i){if(t instanceof me){let a=this._createInjector(i,e,n,void 0),r={$implicit:i.data,dialogRef:e};i.templateContext&&(r=M(M({},r),typeof i.templateContext=="function"?i.templateContext():i.templateContext)),n.attachTemplatePortal(new Te(t,null,r,a))}else{let a=this._createInjector(i,e,n,this._injector),r=n.attachComponentPortal(new Qt(t,i.viewContainerRef,a));e.componentRef=r,e.componentInstance=r.instance}}_createInjector(t,e,n,i){let a=t.injector||t.viewContainerRef?.injector,r=[{provide:kn,useValue:t.data},{provide:mt,useValue:e}];return t.providers&&(typeof t.providers=="function"?r.push(...t.providers(e,t,n)):r.push(...t.providers)),t.direction&&(!a||!a.get(lt,null,{optional:!0}))&&r.push({provide:lt,useValue:Tn(t.direction)}),F.create({parent:a||i,providers:r})}_removeOpenDialog(t,e){let n=this.openDialogs.indexOf(t);n>-1&&(this.openDialogs.splice(n,1),this.openDialogs.length||(this._ariaHiddenElements.forEach((i,a)=>{i?a.setAttribute("aria-hidden",i):a.removeAttribute("aria-hidden")}),this._ariaHiddenElements.clear(),e&&this._getAfterAllClosed().next()))}_hideNonDialogContentFromAssistiveTechnology(t){if(t.parentElement){let e=t.parentElement.children;for(let n=e.length-1;n>-1;n--){let i=e[n];i!==t&&i.nodeName!=="SCRIPT"&&i.nodeName!=="STYLE"&&!i.hasAttribute("aria-live")&&!i.hasAttribute("popover")&&(this._ariaHiddenElements.set(i,i.getAttribute("aria-hidden")),i.setAttribute("aria-hidden","true"))}}}_getAfterAllClosed(){let t=this._parentDialog;return t?t._getAfterAllClosed():this._afterAllClosedAtThisLevel}static \u0275fac=function(e){return new(e||o)};static \u0275prov=D({token:o,factory:o.\u0275fac,providedIn:"root"})}return o})();function qt(o,s){let t=o.length;for(;t--;)s(o[t])}var Ke=(()=>{class o{static \u0275fac=function(e){return new(e||o)};static \u0275mod=E({type:o});static \u0275inj=x({providers:[Kt],imports:[Ft,dt,Ne,dt]})}return o})();function Dn(o,s){}var Bt=class{viewContainerRef;injector;id;role="dialog";panelClass="";hasBackdrop=!0;backdropClass="";disableClose=!1;closePredicate;width="";height="";minWidth;minHeight;maxWidth;maxHeight;position;data=null;direction;ariaDescribedBy=null;ariaLabelledBy=null;ariaLabel=null;ariaModal=!1;autoFocus="first-tabbable";restoreFocus=!0;delayFocusTrap=!0;scrollStrategy;closeOnNavigation=!0;enterAnimationDuration;exitAnimationDuration},Zt="mdc-dialog--open",Ze="mdc-dialog--opening",Ye="mdc-dialog--closing",xn=150,An=75,En=(()=>{class o extends Jt{_animationStateChanged=new P;_animationsEnabled=!ut();_actionSectionCount=0;_hostElement=this._elementRef.nativeElement;_enterAnimationDuration=this._animationsEnabled?tn(this._config.enterAnimationDuration)??xn:0;_exitAnimationDuration=this._animationsEnabled?tn(this._config.exitAnimationDuration)??An:0;_animationTimer=null;_contentAttached(){super._contentAttached(),this._startOpenAnimation()}_startOpenAnimation(){this._animationStateChanged.emit({state:"opening",totalTime:this._enterAnimationDuration}),this._animationsEnabled?(this._hostElement.style.setProperty(Xe,`${this._enterAnimationDuration}ms`),this._requestAnimationFrame(()=>this._hostElement.classList.add(Ze,Zt)),this._waitForAnimationToComplete(this._enterAnimationDuration,this._finishDialogOpen)):(this._hostElement.classList.add(Zt),Promise.resolve().then(()=>this._finishDialogOpen()))}_startExitAnimation(){this._animationStateChanged.emit({state:"closing",totalTime:this._exitAnimationDuration}),this._hostElement.classList.remove(Zt),this._animationsEnabled?(this._hostElement.style.setProperty(Xe,`${this._exitAnimationDuration}ms`),this._requestAnimationFrame(()=>this._hostElement.classList.add(Ye)),this._waitForAnimationToComplete(this._exitAnimationDuration,this._finishDialogClose)):Promise.resolve().then(()=>this._finishDialogClose())}_updateActionSectionCount(t){this._actionSectionCount+=t,this._changeDetectorRef.markForCheck()}_finishDialogOpen=()=>{this._clearAnimationClasses(),this._openAnimationDone(this._enterAnimationDuration)};_finishDialogClose=()=>{this._clearAnimationClasses(),this._animationStateChanged.emit({state:"closed",totalTime:this._exitAnimationDuration})};_clearAnimationClasses(){this._hostElement.classList.remove(Ze,Ye)}_waitForAnimationToComplete(t,e){this._animationTimer!==null&&clearTimeout(this._animationTimer),this._animationTimer=setTimeout(e,t)}_requestAnimationFrame(t){this._ngZone.runOutsideAngular(()=>{typeof requestAnimationFrame=="function"?requestAnimationFrame(t):t()})}_captureInitialFocus(){this._config.delayFocusTrap||this._trapFocus()}_openAnimationDone(t){this._config.delayFocusTrap&&this._trapFocus(),this._animationStateChanged.next({state:"opened",totalTime:t})}ngOnDestroy(){super.ngOnDestroy(),this._animationTimer!==null&&clearTimeout(this._animationTimer)}attachComponentPortal(t){let e=super.attachComponentPortal(t);return e.location.nativeElement.classList.add("mat-mdc-dialog-component-host"),e}static \u0275fac=(()=>{let t;return function(n){return(t||(t=vt(o)))(n||o)}})();static \u0275cmp=A({type:o,selectors:[["mat-dialog-container"]],hostAttrs:["tabindex","-1",1,"mat-mdc-dialog-container","mdc-dialog"],hostVars:10,hostBindings:function(e,n){e&2&&(Wt("id",n._config.id),m("aria-modal",n._config.ariaModal)("role",n._config.role)("aria-labelledby",n._config.ariaLabel?null:n._ariaLabelledByQueue[0])("aria-label",n._config.ariaLabel)("aria-describedby",n._config.ariaDescribedBy||null),O("_mat-animation-noopable",!n._animationsEnabled)("mat-mdc-dialog-container-with-actions",n._actionSectionCount>0))},features:[Z],decls:3,vars:0,consts:[[1,"mat-mdc-dialog-inner-container","mdc-dialog__container"],[1,"mat-mdc-dialog-surface","mdc-dialog__surface"],["cdkPortalOutlet",""]],template:function(e,n){e&1&&(v(0,"div",0)(1,"div",1),Y(2,Dn,0,0,"ng-template",2),w()())},dependencies:[ct],styles:[`.mat-mdc-dialog-container { - width: 100%; - height: 100%; - display: block; - box-sizing: border-box; - max-height: inherit; - min-height: inherit; - min-width: inherit; - max-width: inherit; - outline: 0; -} - -.cdk-overlay-pane.mat-mdc-dialog-panel { - max-width: var(--mat-dialog-container-max-width, 560px); - min-width: var(--mat-dialog-container-min-width, 280px); -} -@media (max-width: 599px) { - .cdk-overlay-pane.mat-mdc-dialog-panel { - max-width: var(--mat-dialog-container-small-max-width, calc(100vw - 32px)); - } -} - -.mat-mdc-dialog-inner-container { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-around; - box-sizing: border-box; - height: 100%; - opacity: 0; - transition: opacity linear var(--mat-dialog-transition-duration, 0ms); - max-height: inherit; - min-height: inherit; - min-width: inherit; - max-width: inherit; -} -.mdc-dialog--closing .mat-mdc-dialog-inner-container { - transition: opacity 75ms linear; - transform: none; -} -.mdc-dialog--open .mat-mdc-dialog-inner-container { - opacity: 1; -} -._mat-animation-noopable .mat-mdc-dialog-inner-container { - transition: none; -} - -.mat-mdc-dialog-surface { - display: flex; - flex-direction: column; - flex-grow: 0; - flex-shrink: 0; - box-sizing: border-box; - width: 100%; - height: 100%; - position: relative; - overflow-y: auto; - outline: 0; - transform: scale(0.8); - transition: transform var(--mat-dialog-transition-duration, 0ms) cubic-bezier(0, 0, 0.2, 1); - max-height: inherit; - min-height: inherit; - min-width: inherit; - max-width: inherit; - box-shadow: var(--mat-dialog-container-elevation-shadow, none); - border-radius: var(--mat-dialog-container-shape, var(--mat-sys-corner-extra-large, 4px)); - background-color: var(--mat-dialog-container-color, var(--mat-sys-surface, white)); -} -[dir=rtl] .mat-mdc-dialog-surface { - text-align: right; -} -.mdc-dialog--open .mat-mdc-dialog-surface, .mdc-dialog--closing .mat-mdc-dialog-surface { - transform: none; -} -._mat-animation-noopable .mat-mdc-dialog-surface { - transition: none; -} -.mat-mdc-dialog-surface::before { - position: absolute; - box-sizing: border-box; - width: 100%; - height: 100%; - top: 0; - left: 0; - border: 2px solid transparent; - border-radius: inherit; - content: ""; - pointer-events: none; -} - -.mat-mdc-dialog-title { - display: block; - position: relative; - flex-shrink: 0; - box-sizing: border-box; - margin: 0 0 1px; - padding: var(--mat-dialog-headline-padding, 6px 24px 13px); -} -.mat-mdc-dialog-title::before { - display: inline-block; - width: 0; - height: 40px; - content: ""; - vertical-align: 0; -} -[dir=rtl] .mat-mdc-dialog-title { - text-align: right; -} -.mat-mdc-dialog-container .mat-mdc-dialog-title { - color: var(--mat-dialog-subhead-color, var(--mat-sys-on-surface, rgba(0, 0, 0, 0.87))); - font-family: var(--mat-dialog-subhead-font, var(--mat-sys-headline-small-font, inherit)); - line-height: var(--mat-dialog-subhead-line-height, var(--mat-sys-headline-small-line-height, 1.5rem)); - font-size: var(--mat-dialog-subhead-size, var(--mat-sys-headline-small-size, 1rem)); - font-weight: var(--mat-dialog-subhead-weight, var(--mat-sys-headline-small-weight, 400)); - letter-spacing: var(--mat-dialog-subhead-tracking, var(--mat-sys-headline-small-tracking, 0.03125em)); -} - -.mat-mdc-dialog-content { - display: block; - flex-grow: 1; - box-sizing: border-box; - margin: 0; - overflow: auto; - max-height: 65vh; -} -.mat-mdc-dialog-content > :first-child { - margin-top: 0; -} -.mat-mdc-dialog-content > :last-child { - margin-bottom: 0; -} -.mat-mdc-dialog-container .mat-mdc-dialog-content { - color: var(--mat-dialog-supporting-text-color, var(--mat-sys-on-surface-variant, rgba(0, 0, 0, 0.6))); - font-family: var(--mat-dialog-supporting-text-font, var(--mat-sys-body-medium-font, inherit)); - line-height: var(--mat-dialog-supporting-text-line-height, var(--mat-sys-body-medium-line-height, 1.5rem)); - font-size: var(--mat-dialog-supporting-text-size, var(--mat-sys-body-medium-size, 1rem)); - font-weight: var(--mat-dialog-supporting-text-weight, var(--mat-sys-body-medium-weight, 400)); - letter-spacing: var(--mat-dialog-supporting-text-tracking, var(--mat-sys-body-medium-tracking, 0.03125em)); -} -.mat-mdc-dialog-container .mat-mdc-dialog-content { - padding: var(--mat-dialog-content-padding, 20px 24px); -} -.mat-mdc-dialog-container-with-actions .mat-mdc-dialog-content { - padding: var(--mat-dialog-with-actions-content-padding, 20px 24px 0); -} -.mat-mdc-dialog-container .mat-mdc-dialog-title + .mat-mdc-dialog-content { - padding-top: 0; -} - -.mat-mdc-dialog-actions { - display: flex; - position: relative; - flex-shrink: 0; - flex-wrap: wrap; - align-items: center; - box-sizing: border-box; - min-height: 52px; - margin: 0; - border-top: 1px solid transparent; - padding: var(--mat-dialog-actions-padding, 16px 24px); - justify-content: var(--mat-dialog-actions-alignment, flex-end); -} -@media (forced-colors: active) { - .mat-mdc-dialog-actions { - border-top-color: CanvasText; - } -} -.mat-mdc-dialog-actions.mat-mdc-dialog-actions-align-start, .mat-mdc-dialog-actions[align=start] { - justify-content: start; -} -.mat-mdc-dialog-actions.mat-mdc-dialog-actions-align-center, .mat-mdc-dialog-actions[align=center] { - justify-content: center; -} -.mat-mdc-dialog-actions.mat-mdc-dialog-actions-align-end, .mat-mdc-dialog-actions[align=end] { - justify-content: flex-end; -} -.mat-mdc-dialog-actions .mat-button-base + .mat-button-base, -.mat-mdc-dialog-actions .mat-mdc-button-base + .mat-mdc-button-base { - margin-left: 8px; -} -[dir=rtl] .mat-mdc-dialog-actions .mat-button-base + .mat-button-base, -[dir=rtl] .mat-mdc-dialog-actions .mat-mdc-button-base + .mat-mdc-button-base { - margin-left: 0; - margin-right: 8px; -} - -.mat-mdc-dialog-component-host { - display: contents; -} -`],encapsulation:2})}return o})(),Xe="--mat-dialog-transition-duration";function tn(o){return o==null?null:typeof o=="number"?o:o.endsWith("ms")?$t(o.substring(0,o.length-2)):o.endsWith("s")?$t(o.substring(0,o.length-1))*1e3:o==="0"?0:null}var Lt=(function(o){return o[o.OPEN=0]="OPEN",o[o.CLOSING=1]="CLOSING",o[o.CLOSED=2]="CLOSED",o})(Lt||{}),gt=class{_ref;_config;_containerInstance;componentInstance;componentRef=null;disableClose;id;_afterOpened=new Vt(1);_beforeClosed=new Vt(1);_result;_closeFallbackTimeout;_state=Lt.OPEN;_closeInteractionType;constructor(s,t,e){this._ref=s,this._config=t,this._containerInstance=e,this.disableClose=t.disableClose,this.id=s.id,s.addPanelClass("mat-mdc-dialog-panel"),e._animationStateChanged.pipe(rt(n=>n.state==="opened"),N(1)).subscribe(()=>{this._afterOpened.next(),this._afterOpened.complete()}),e._animationStateChanged.pipe(rt(n=>n.state==="closed"),N(1)).subscribe(()=>{clearTimeout(this._closeFallbackTimeout),this._finishDialogClose()}),s.overlayRef.detachments().subscribe(()=>{this._beforeClosed.next(this._result),this._beforeClosed.complete(),this._finishDialogClose()}),ie(this.backdropClick(),this.keydownEvents().pipe(rt(n=>n.keyCode===27&&!this.disableClose&&!nt(n)))).subscribe(n=>{this.disableClose||(n.preventDefault(),en(this,n.type==="keydown"?"keyboard":"mouse"))})}close(s){let t=this._config.closePredicate;t&&!t(s,this._config,this.componentInstance)||(this._result=s,this._containerInstance._animationStateChanged.pipe(rt(e=>e.state==="closing"),N(1)).subscribe(e=>{this._beforeClosed.next(s),this._beforeClosed.complete(),this._ref.overlayRef.detachBackdrop(),this._closeFallbackTimeout=setTimeout(()=>this._finishDialogClose(),e.totalTime+100)}),this._state=Lt.CLOSING,this._containerInstance._startExitAnimation())}afterOpened(){return this._afterOpened}afterClosed(){return this._ref.closed}beforeClosed(){return this._beforeClosed}backdropClick(){return this._ref.backdropClick}keydownEvents(){return this._ref.keydownEvents}updatePosition(s){let t=this._ref.config.positionStrategy;return s&&(s.left||s.right)?s.left?t.left(s.left):t.right(s.right):t.centerHorizontally(),s&&(s.top||s.bottom)?s.top?t.top(s.top):t.bottom(s.bottom):t.centerVertically(),this._ref.updatePosition(),this}updateSize(s="",t=""){return this._ref.updateSize(s,t),this}addPanelClass(s){return this._ref.addPanelClass(s),this}removePanelClass(s){return this._ref.removePanelClass(s),this}getState(){return this._state}_finishDialogClose(){this._state=Lt.CLOSED,this._ref.close(this._result,{focusOrigin:this._closeInteractionType}),this.componentInstance=null}};function en(o,s,t){return o._closeInteractionType=s,o.close(t)}var On=new h("MatMdcDialogData"),Mn=new h("mat-mdc-dialog-default-options"),Fn=new h("mat-mdc-dialog-scroll-strategy",{providedIn:"root",factory:()=>{let o=l(F);return()=>Ot(o)}}),Yt=(()=>{class o{_defaultOptions=l(Mn,{optional:!0});_scrollStrategy=l(Fn);_parentDialog=l(o,{optional:!0,skipSelf:!0});_idGenerator=l(z);_injector=l(F);_dialog=l(Kt);_animationsDisabled=ut();_openDialogsAtThisLevel=[];_afterAllClosedAtThisLevel=new T;_afterOpenedAtThisLevel=new T;dialogConfigClass=Bt;_dialogRefConstructor;_dialogContainerType;_dialogDataToken;get openDialogs(){return this._parentDialog?this._parentDialog.openDialogs:this._openDialogsAtThisLevel}get afterOpened(){return this._parentDialog?this._parentDialog.afterOpened:this._afterOpenedAtThisLevel}_getAfterAllClosed(){let t=this._parentDialog;return t?t._getAfterAllClosed():this._afterAllClosedAtThisLevel}afterAllClosed=ht(()=>this.openDialogs.length?this._getAfterAllClosed():this._getAfterAllClosed().pipe(pt(void 0)));constructor(){this._dialogRefConstructor=gt,this._dialogContainerType=En,this._dialogDataToken=On}open(t,e){let n;e=M(M({},this._defaultOptions||new Bt),e),e.id=e.id||this._idGenerator.getId("mat-mdc-dialog-"),e.scrollStrategy=e.scrollStrategy||this._scrollStrategy();let i=this._dialog.open(t,Xt(M({},e),{positionStrategy:Mt(this._injector).centerHorizontally().centerVertically(),disableClose:!0,closePredicate:void 0,closeOnDestroy:!1,closeOnOverlayDetachments:!1,disableAnimations:this._animationsDisabled||e.enterAnimationDuration?.toLocaleString()==="0"||e.exitAnimationDuration?.toString()==="0",container:{type:this._dialogContainerType,providers:()=>[{provide:this.dialogConfigClass,useValue:e},{provide:G,useValue:e}]},templateContext:()=>({dialogRef:n}),providers:(a,r,d)=>(n=new this._dialogRefConstructor(a,e,d),n.updatePosition(e?.position),[{provide:this._dialogContainerType,useValue:d},{provide:this._dialogDataToken,useValue:r.data},{provide:this._dialogRefConstructor,useValue:n}])}));return n.componentRef=i.componentRef,n.componentInstance=i.componentInstance,this.openDialogs.push(n),this.afterOpened.next(n),n.afterClosed().subscribe(()=>{let a=this.openDialogs.indexOf(n);a>-1&&(this.openDialogs.splice(a,1),this.openDialogs.length||this._getAfterAllClosed().next())}),n}closeAll(){this._closeDialogs(this.openDialogs)}getDialogById(t){return this.openDialogs.find(e=>e.id===t)}ngOnDestroy(){this._closeDialogs(this._openDialogsAtThisLevel),this._afterAllClosedAtThisLevel.complete(),this._afterOpenedAtThisLevel.complete()}_closeDialogs(t){let e=t.length;for(;e--;)t[e].close()}static \u0275fac=function(e){return new(e||o)};static \u0275prov=D({token:o,factory:o.\u0275fac,providedIn:"root"})}return o})(),pi=(()=>{class o{dialogRef=l(gt,{optional:!0});_elementRef=l(I);_dialog=l(Yt);ariaLabel;type="button";dialogResult;_matDialogClose;constructor(){}ngOnInit(){this.dialogRef||(this.dialogRef=on(this._elementRef,this._dialog.openDialogs))}ngOnChanges(t){let e=t._matDialogClose||t._matDialogCloseResult;e&&(this.dialogResult=e.currentValue)}_onButtonClick(t){en(this.dialogRef,t.screenX===0&&t.screenY===0?"keyboard":"mouse",this.dialogResult)}static \u0275fac=function(e){return new(e||o)};static \u0275dir=L({type:o,selectors:[["","mat-dialog-close",""],["","matDialogClose",""]],hostVars:2,hostBindings:function(e,n){e&1&&tt("click",function(a){return n._onButtonClick(a)}),e&2&&m("aria-label",n.ariaLabel||null)("type",n.type)},inputs:{ariaLabel:[0,"aria-label","ariaLabel"],type:"type",dialogResult:[0,"mat-dialog-close","dialogResult"],_matDialogClose:[0,"matDialogClose","_matDialogClose"]},exportAs:["matDialogClose"],features:[de]})}return o})(),nn=(()=>{class o{_dialogRef=l(gt,{optional:!0});_elementRef=l(I);_dialog=l(Yt);constructor(){}ngOnInit(){this._dialogRef||(this._dialogRef=on(this._elementRef,this._dialog.openDialogs)),this._dialogRef&&Promise.resolve().then(()=>{this._onAdd()})}ngOnDestroy(){this._dialogRef?._containerInstance&&Promise.resolve().then(()=>{this._onRemove()})}static \u0275fac=function(e){return new(e||o)};static \u0275dir=L({type:o})}return o})(),fi=(()=>{class o extends nn{id=l(z).getId("mat-mdc-dialog-title-");_onAdd(){this._dialogRef._containerInstance?._addAriaLabelledBy?.(this.id)}_onRemove(){this._dialogRef?._containerInstance?._removeAriaLabelledBy?.(this.id)}static \u0275fac=(()=>{let t;return function(n){return(t||(t=vt(o)))(n||o)}})();static \u0275dir=L({type:o,selectors:[["","mat-dialog-title",""],["","matDialogTitle",""]],hostAttrs:[1,"mat-mdc-dialog-title","mdc-dialog__title"],hostVars:1,hostBindings:function(e,n){e&2&&Wt("id",n.id)},inputs:{id:"id"},exportAs:["matDialogTitle"],features:[Z]})}return o})(),_i=(()=>{class o{static \u0275fac=function(e){return new(e||o)};static \u0275dir=L({type:o,selectors:[["","mat-dialog-content",""],["mat-dialog-content"],["","matDialogContent",""]],hostAttrs:[1,"mat-mdc-dialog-content","mdc-dialog__content"],features:[he([we])]})}return o})(),bi=(()=>{class o extends nn{align;_onAdd(){this._dialogRef._containerInstance?._updateActionSectionCount?.(1)}_onRemove(){this._dialogRef._containerInstance?._updateActionSectionCount?.(-1)}static \u0275fac=(()=>{let t;return function(n){return(t||(t=vt(o)))(n||o)}})();static \u0275dir=L({type:o,selectors:[["","mat-dialog-actions",""],["mat-dialog-actions"],["","matDialogActions",""]],hostAttrs:[1,"mat-mdc-dialog-actions","mdc-dialog__actions"],hostVars:6,hostBindings:function(e,n){e&2&&O("mat-mdc-dialog-actions-align-start",n.align==="start")("mat-mdc-dialog-actions-align-center",n.align==="center")("mat-mdc-dialog-actions-align-end",n.align==="end")},inputs:{align:"align"},features:[Z]})}return o})();function on(o,s){let t=o.nativeElement.parentElement;for(;t&&!t.classList.contains("mat-mdc-dialog-container");)t=t.parentElement;return t?s.find(e=>e.id===t.id):null}var vi=(()=>{class o{static \u0275fac=function(e){return new(e||o)};static \u0275mod=E({type:o});static \u0275inj=x({providers:[Yt],imports:[Ke,Ft,dt,j]})}return o})();var zn=["button"],Gn=["*"];function Hn(o,s){if(o&1&&(v(0,"div",2),X(1,"mat-pseudo-checkbox",6),w()),o&2){let t=Ct();k(),B("disabled",t.disabled)}}var an=new h("MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS",{providedIn:"root",factory:()=>({hideSingleSelectionIndicator:!1,hideMultipleSelectionIndicator:!1,disabledInteractive:!1})}),rn=new h("MatButtonToggleGroup"),Vn={provide:Me,useExisting:se(()=>Un),multi:!0},jt=class{source;value;constructor(s,t){this.source=s,this.value=t}},Un=(()=>{class o{_changeDetector=l(st);_dir=l(lt,{optional:!0});_multiple=!1;_disabled=!1;_disabledInteractive=!1;_selectionModel;_rawValue;_controlValueAccessorChangeFn=()=>{};_onTouched=()=>{};_buttonToggles;appearance;get name(){return this._name}set name(t){this._name=t,this._markButtonsForCheck()}_name=l(z).getId("mat-button-toggle-group-");vertical=!1;get value(){let t=this._selectionModel?this._selectionModel.selected:[];return this.multiple?t.map(e=>e.value):t[0]?t[0].value:void 0}set value(t){this._setSelectionByValue(t),this.valueChange.emit(this.value)}valueChange=new P;get selected(){let t=this._selectionModel?this._selectionModel.selected:[];return this.multiple?t:t[0]||null}get multiple(){return this._multiple}set multiple(t){this._multiple=t,this._markButtonsForCheck()}get disabled(){return this._disabled}set disabled(t){this._disabled=t,this._markButtonsForCheck()}get disabledInteractive(){return this._disabledInteractive}set disabledInteractive(t){this._disabledInteractive=t,this._markButtonsForCheck()}get dir(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}change=new P;get hideSingleSelectionIndicator(){return this._hideSingleSelectionIndicator}set hideSingleSelectionIndicator(t){this._hideSingleSelectionIndicator=t,this._markButtonsForCheck()}_hideSingleSelectionIndicator;get hideMultipleSelectionIndicator(){return this._hideMultipleSelectionIndicator}set hideMultipleSelectionIndicator(t){this._hideMultipleSelectionIndicator=t,this._markButtonsForCheck()}_hideMultipleSelectionIndicator;constructor(){let t=l(an,{optional:!0});this.appearance=t&&t.appearance?t.appearance:"standard",this._hideSingleSelectionIndicator=t?.hideSingleSelectionIndicator??!1,this._hideMultipleSelectionIndicator=t?.hideMultipleSelectionIndicator??!1}ngOnInit(){this._selectionModel=new ze(this.multiple,void 0,!1)}ngAfterContentInit(){this._selectionModel.select(...this._buttonToggles.filter(t=>t.checked)),this.multiple||this._initializeTabIndex()}writeValue(t){this.value=t,this._changeDetector.markForCheck()}registerOnChange(t){this._controlValueAccessorChangeFn=t}registerOnTouched(t){this._onTouched=t}setDisabledState(t){this.disabled=t}_keydown(t){if(this.multiple||this.disabled||nt(t))return;let n=t.target.id,i=this._buttonToggles.toArray().findIndex(r=>r.buttonId===n),a=null;switch(t.keyCode){case 32:case 13:a=this._buttonToggles.get(i)||null;break;case 38:a=this._getNextButton(i,-1);break;case 37:a=this._getNextButton(i,this.dir==="ltr"?-1:1);break;case 40:a=this._getNextButton(i,1);break;case 39:a=this._getNextButton(i,this.dir==="ltr"?1:-1);break;default:return}a&&(t.preventDefault(),a._onButtonClick(),a.focus())}_emitChangeEvent(t){let e=new jt(t,this.value);this._rawValue=e.value,this._controlValueAccessorChangeFn(e.value),this.change.emit(e)}_syncButtonToggle(t,e,n=!1,i=!1){!this.multiple&&this.selected&&!t.checked&&(this.selected.checked=!1),this._selectionModel?e?this._selectionModel.select(t):this._selectionModel.deselect(t):i=!0,i?Promise.resolve().then(()=>this._updateModelValue(t,n)):this._updateModelValue(t,n)}_isSelected(t){return this._selectionModel&&this._selectionModel.isSelected(t)}_isPrechecked(t){return typeof this._rawValue>"u"?!1:this.multiple&&Array.isArray(this._rawValue)?this._rawValue.some(e=>t.value!=null&&e===t.value):t.value===this._rawValue}_initializeTabIndex(){if(this._buttonToggles.forEach(t=>{t.tabIndex=-1}),this.selected)this.selected.tabIndex=0;else for(let t=0;tthis._selectValue(n,e))):(this._clearSelection(),this._selectValue(t,e)),!this.multiple&&e.every(n=>n.tabIndex===-1)){for(let n of e)if(!n.disabled){n.tabIndex=0;break}}}_clearSelection(){this._selectionModel.clear(),this._buttonToggles.forEach(t=>{t.checked=!1,this.multiple||(t.tabIndex=-1)})}_selectValue(t,e){for(let n of e)if(n.value===t){n.checked=!0,this._selectionModel.select(n),this.multiple||(n.tabIndex=0);break}}_updateModelValue(t,e){e&&this._emitChangeEvent(t),this.valueChange.emit(this.value)}_markButtonsForCheck(){this._buttonToggles?.forEach(t=>t._markForCheck())}static \u0275fac=function(e){return new(e||o)};static \u0275dir=L({type:o,selectors:[["mat-button-toggle-group"]],contentQueries:function(e,n,i){if(e&1&&_e(i,sn,5),e&2){let a;U(a=W())&&(n._buttonToggles=a)}},hostAttrs:[1,"mat-button-toggle-group"],hostVars:6,hostBindings:function(e,n){e&1&&tt("keydown",function(a){return n._keydown(a)}),e&2&&(m("role",n.multiple?"group":"radiogroup")("aria-disabled",n.disabled),O("mat-button-toggle-vertical",n.vertical)("mat-button-toggle-group-appearance-standard",n.appearance==="standard"))},inputs:{appearance:"appearance",name:"name",vertical:[2,"vertical","vertical",y],value:"value",multiple:[2,"multiple","multiple",y],disabled:[2,"disabled","disabled",y],disabledInteractive:[2,"disabledInteractive","disabledInteractive",y],hideSingleSelectionIndicator:[2,"hideSingleSelectionIndicator","hideSingleSelectionIndicator",y],hideMultipleSelectionIndicator:[2,"hideMultipleSelectionIndicator","hideMultipleSelectionIndicator",y]},outputs:{valueChange:"valueChange",change:"change"},exportAs:["matButtonToggleGroup"],features:[be([Vn,{provide:rn,useExisting:o}])]})}return o})(),sn=(()=>{class o{_changeDetectorRef=l(st);_elementRef=l(I);_focusMonitor=l(Rt);_idGenerator=l(z);_animationDisabled=ut();_checked=!1;ariaLabel;ariaLabelledby=null;_buttonElement;buttonToggleGroup;get buttonId(){return`${this.id}-button`}id;name;value;get tabIndex(){return this._tabIndex()}set tabIndex(t){this._tabIndex.set(t)}_tabIndex;disableRipple=!1;get appearance(){return this.buttonToggleGroup?this.buttonToggleGroup.appearance:this._appearance}set appearance(t){this._appearance=t}_appearance;get checked(){return this.buttonToggleGroup?this.buttonToggleGroup._isSelected(this):this._checked}set checked(t){t!==this._checked&&(this._checked=t,this.buttonToggleGroup&&this.buttonToggleGroup._syncButtonToggle(this,this._checked),this._changeDetectorRef.markForCheck())}get disabled(){return this._disabled||this.buttonToggleGroup&&this.buttonToggleGroup.disabled}set disabled(t){this._disabled=t}_disabled=!1;get disabledInteractive(){return this._disabledInteractive||this.buttonToggleGroup!==null&&this.buttonToggleGroup.disabledInteractive}set disabledInteractive(t){this._disabledInteractive=t}_disabledInteractive;change=new P;constructor(){l(Ie).load(Be);let t=l(rn,{optional:!0}),e=l(new Dt("tabindex"),{optional:!0})||"",n=l(an,{optional:!0});this._tabIndex=bt(parseInt(e)||0),this.buttonToggleGroup=t,this._appearance=n&&n.appearance?n.appearance:"standard",this._disabledInteractive=n?.disabledInteractive??!1}ngOnInit(){let t=this.buttonToggleGroup;this.id=this.id||this._idGenerator.getId("mat-button-toggle-"),t&&(t._isPrechecked(this)?this.checked=!0:t._isSelected(this)!==this._checked&&t._syncButtonToggle(this,this._checked))}ngAfterViewInit(){this._animationDisabled||this._elementRef.nativeElement.classList.add("mat-button-toggle-animations-enabled"),this._focusMonitor.monitor(this._elementRef,!0)}ngOnDestroy(){let t=this.buttonToggleGroup;this._focusMonitor.stopMonitoring(this._elementRef),t&&t._isSelected(this)&&t._syncButtonToggle(this,!1,!1,!0)}focus(t){this._buttonElement.nativeElement.focus(t)}_onButtonClick(){if(this.disabled)return;let t=this.isSingleSelector()?!0:!this._checked;if(t!==this._checked&&(this._checked=t,this.buttonToggleGroup&&(this.buttonToggleGroup._syncButtonToggle(this,this._checked,!0),this.buttonToggleGroup._onTouched())),this.isSingleSelector()){let e=this.buttonToggleGroup._buttonToggles.find(n=>n.tabIndex===0);e&&(e.tabIndex=-1),this.tabIndex=0}this.change.emit(new jt(this,this.value))}_markForCheck(){this._changeDetectorRef.markForCheck()}_getButtonName(){return this.isSingleSelector()?this.buttonToggleGroup.name:this.name||null}isSingleSelector(){return this.buttonToggleGroup&&!this.buttonToggleGroup.multiple}static \u0275fac=function(e){return new(e||o)};static \u0275cmp=A({type:o,selectors:[["mat-button-toggle"]],viewQuery:function(e,n){if(e&1&&et(zn,5),e&2){let i;U(i=W())&&(n._buttonElement=i.first)}},hostAttrs:["role","presentation",1,"mat-button-toggle"],hostVars:14,hostBindings:function(e,n){e&1&&tt("focus",function(){return n.focus()}),e&2&&(m("aria-label",null)("aria-labelledby",null)("id",n.id)("name",null),O("mat-button-toggle-standalone",!n.buttonToggleGroup)("mat-button-toggle-checked",n.checked)("mat-button-toggle-disabled",n.disabled)("mat-button-toggle-disabled-interactive",n.disabledInteractive)("mat-button-toggle-appearance-standard",n.appearance==="standard"))},inputs:{ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],id:"id",name:"name",value:"value",tabIndex:"tabIndex",disableRipple:[2,"disableRipple","disableRipple",y],appearance:"appearance",checked:[2,"checked","checked",y],disabled:[2,"disabled","disabled",y],disabledInteractive:[2,"disabledInteractive","disabledInteractive",y]},outputs:{change:"change"},exportAs:["matButtonToggle"],ngContentSelectors:Gn,decls:7,vars:13,consts:[["button",""],["type","button",1,"mat-button-toggle-button","mat-focus-indicator",3,"click","id","disabled"],[1,"mat-button-toggle-checkbox-wrapper"],[1,"mat-button-toggle-label-content"],[1,"mat-button-toggle-focus-overlay"],["matRipple","",1,"mat-button-toggle-ripple",3,"matRippleTrigger","matRippleDisabled"],["state","checked","aria-hidden","true","appearance","minimal",3,"disabled"]],template:function(e,n){if(e&1&&(St(),v(0,"button",1,0),tt("click",function(){return n._onButtonClick()}),pe(2,Hn,2,1,"div",2),v(3,"span",3),It(4),w()(),X(5,"span",4)(6,"span",5)),e&2){let i=kt(1);B("id",n.buttonId)("disabled",n.disabled&&!n.disabledInteractive||null),m("role",n.isSingleSelector()?"radio":"button")("tabindex",n.disabled&&!n.disabledInteractive?-1:n.tabIndex)("aria-pressed",n.isSingleSelector()?null:n.checked)("aria-checked",n.isSingleSelector()?n.checked:null)("name",n._getButtonName())("aria-label",n.ariaLabel)("aria-labelledby",n.ariaLabelledby)("aria-disabled",n.disabled&&n.disabledInteractive?"true":null),k(2),fe(n.buttonToggleGroup&&(!n.buttonToggleGroup.multiple&&!n.buttonToggleGroup.hideSingleSelectionIndicator||n.buttonToggleGroup.multiple&&!n.buttonToggleGroup.hideMultipleSelectionIndicator)?2:-1),k(4),B("matRippleTrigger",i)("matRippleDisabled",n.disableRipple||n.disabled)}},dependencies:[Le,Ge],styles:[`.mat-button-toggle-standalone, -.mat-button-toggle-group { - position: relative; - display: inline-flex; - flex-direction: row; - white-space: nowrap; - overflow: hidden; - -webkit-tap-highlight-color: transparent; - border-radius: var(--mat-button-toggle-legacy-shape); - transform: translateZ(0); -} -.mat-button-toggle-standalone:not([class*=mat-elevation-z]), -.mat-button-toggle-group:not([class*=mat-elevation-z]) { - box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); -} -@media (forced-colors: active) { - .mat-button-toggle-standalone, - .mat-button-toggle-group { - outline: solid 1px; - } -} - -.mat-button-toggle-standalone.mat-button-toggle-appearance-standard, -.mat-button-toggle-group-appearance-standard { - border-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); - border: solid 1px var(--mat-button-toggle-divider-color, var(--mat-sys-outline)); -} -.mat-button-toggle-standalone.mat-button-toggle-appearance-standard .mat-pseudo-checkbox, -.mat-button-toggle-group-appearance-standard .mat-pseudo-checkbox { - --mat-pseudo-checkbox-minimal-selected-checkmark-color: var(--mat-button-toggle-selected-state-text-color, var(--mat-sys-on-secondary-container)); -} -.mat-button-toggle-standalone.mat-button-toggle-appearance-standard:not([class*=mat-elevation-z]), -.mat-button-toggle-group-appearance-standard:not([class*=mat-elevation-z]) { - box-shadow: none; -} -@media (forced-colors: active) { - .mat-button-toggle-standalone.mat-button-toggle-appearance-standard, - .mat-button-toggle-group-appearance-standard { - outline: 0; - } -} - -.mat-button-toggle-vertical { - flex-direction: column; -} -.mat-button-toggle-vertical .mat-button-toggle-label-content { - display: block; -} - -.mat-button-toggle { - white-space: nowrap; - position: relative; - color: var(--mat-button-toggle-legacy-text-color); - font-family: var(--mat-button-toggle-legacy-label-text-font); - font-size: var(--mat-button-toggle-legacy-label-text-size); - line-height: var(--mat-button-toggle-legacy-label-text-line-height); - font-weight: var(--mat-button-toggle-legacy-label-text-weight); - letter-spacing: var(--mat-button-toggle-legacy-label-text-tracking); - --mat-pseudo-checkbox-minimal-selected-checkmark-color: var(--mat-button-toggle-legacy-selected-state-text-color); -} -.mat-button-toggle.cdk-keyboard-focused .mat-button-toggle-focus-overlay { - opacity: var(--mat-button-toggle-legacy-focus-state-layer-opacity); -} -.mat-button-toggle .mat-icon svg { - vertical-align: top; -} - -.mat-button-toggle-checkbox-wrapper { - display: inline-block; - justify-content: flex-start; - align-items: center; - width: 0; - height: 18px; - line-height: 18px; - overflow: hidden; - box-sizing: border-box; - position: absolute; - top: 50%; - left: 16px; - transform: translate3d(0, -50%, 0); -} -[dir=rtl] .mat-button-toggle-checkbox-wrapper { - left: auto; - right: 16px; -} -.mat-button-toggle-appearance-standard .mat-button-toggle-checkbox-wrapper { - left: 12px; -} -[dir=rtl] .mat-button-toggle-appearance-standard .mat-button-toggle-checkbox-wrapper { - left: auto; - right: 12px; -} -.mat-button-toggle-checked .mat-button-toggle-checkbox-wrapper { - width: 18px; -} -.mat-button-toggle-animations-enabled .mat-button-toggle-checkbox-wrapper { - transition: width 150ms 45ms cubic-bezier(0.4, 0, 0.2, 1); -} -.mat-button-toggle-vertical .mat-button-toggle-checkbox-wrapper { - transition: none; -} - -.mat-button-toggle-checked { - color: var(--mat-button-toggle-legacy-selected-state-text-color); - background-color: var(--mat-button-toggle-legacy-selected-state-background-color); -} - -.mat-button-toggle-disabled { - pointer-events: none; - color: var(--mat-button-toggle-legacy-disabled-state-text-color); - background-color: var(--mat-button-toggle-legacy-disabled-state-background-color); - --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: var(--mat-button-toggle-legacy-disabled-state-text-color); -} -.mat-button-toggle-disabled.mat-button-toggle-checked { - background-color: var(--mat-button-toggle-legacy-disabled-selected-state-background-color); -} - -.mat-button-toggle-disabled-interactive { - pointer-events: auto; -} - -.mat-button-toggle-appearance-standard { - color: var(--mat-button-toggle-text-color, var(--mat-sys-on-surface)); - background-color: var(--mat-button-toggle-background-color, transparent); - font-family: var(--mat-button-toggle-label-text-font, var(--mat-sys-label-large-font)); - font-size: var(--mat-button-toggle-label-text-size, var(--mat-sys-label-large-size)); - line-height: var(--mat-button-toggle-label-text-line-height, var(--mat-sys-label-large-line-height)); - font-weight: var(--mat-button-toggle-label-text-weight, var(--mat-sys-label-large-weight)); - letter-spacing: var(--mat-button-toggle-label-text-tracking, var(--mat-sys-label-large-tracking)); -} -.mat-button-toggle-group-appearance-standard .mat-button-toggle-appearance-standard + .mat-button-toggle-appearance-standard { - border-left: solid 1px var(--mat-button-toggle-divider-color, var(--mat-sys-outline)); -} -[dir=rtl] .mat-button-toggle-group-appearance-standard .mat-button-toggle-appearance-standard + .mat-button-toggle-appearance-standard { - border-left: none; - border-right: solid 1px var(--mat-button-toggle-divider-color, var(--mat-sys-outline)); -} -.mat-button-toggle-group-appearance-standard.mat-button-toggle-vertical .mat-button-toggle-appearance-standard + .mat-button-toggle-appearance-standard { - border-left: none; - border-right: none; - border-top: solid 1px var(--mat-button-toggle-divider-color, var(--mat-sys-outline)); -} -.mat-button-toggle-appearance-standard.mat-button-toggle-checked { - color: var(--mat-button-toggle-selected-state-text-color, var(--mat-sys-on-secondary-container)); - background-color: var(--mat-button-toggle-selected-state-background-color, var(--mat-sys-secondary-container)); -} -.mat-button-toggle-appearance-standard.mat-button-toggle-disabled { - color: var(--mat-button-toggle-disabled-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); - background-color: var(--mat-button-toggle-disabled-state-background-color, transparent); -} -.mat-button-toggle-appearance-standard.mat-button-toggle-disabled .mat-pseudo-checkbox { - --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: var(--mat-button-toggle-disabled-selected-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-button-toggle-appearance-standard.mat-button-toggle-disabled.mat-button-toggle-checked { - color: var(--mat-button-toggle-disabled-selected-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); - background-color: var(--mat-button-toggle-disabled-selected-state-background-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); -} -.mat-button-toggle-appearance-standard .mat-button-toggle-focus-overlay { - background-color: var(--mat-button-toggle-state-layer-color, var(--mat-sys-on-surface)); -} -.mat-button-toggle-appearance-standard:hover .mat-button-toggle-focus-overlay { - opacity: var(--mat-button-toggle-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); -} -.mat-button-toggle-appearance-standard.cdk-keyboard-focused .mat-button-toggle-focus-overlay { - opacity: var(--mat-button-toggle-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); -} -@media (hover: none) { - .mat-button-toggle-appearance-standard:hover .mat-button-toggle-focus-overlay { - display: none; - } -} - -.mat-button-toggle-label-content { - -webkit-user-select: none; - user-select: none; - display: inline-block; - padding: 0 16px; - line-height: var(--mat-button-toggle-legacy-height); - position: relative; -} -.mat-button-toggle-appearance-standard .mat-button-toggle-label-content { - padding: 0 12px; - line-height: var(--mat-button-toggle-height, 40px); -} - -.mat-button-toggle-label-content > * { - vertical-align: middle; -} - -.mat-button-toggle-focus-overlay { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - border-radius: inherit; - pointer-events: none; - opacity: 0; - background-color: var(--mat-button-toggle-legacy-state-layer-color); -} - -@media (forced-colors: active) { - .mat-button-toggle-checked .mat-button-toggle-focus-overlay { - border-bottom: solid 500px; - opacity: 0.5; - height: 0; - } - .mat-button-toggle-checked:hover .mat-button-toggle-focus-overlay { - opacity: 0.6; - } - .mat-button-toggle-checked.mat-button-toggle-appearance-standard .mat-button-toggle-focus-overlay { - border-bottom: solid 500px; - } -} -.mat-button-toggle .mat-button-toggle-ripple { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - pointer-events: none; -} - -.mat-button-toggle-button { - border: 0; - background: none; - color: inherit; - padding: 0; - margin: 0; - font: inherit; - outline: none; - width: 100%; - cursor: pointer; -} -.mat-button-toggle-animations-enabled .mat-button-toggle-button { - transition: padding 150ms 45ms cubic-bezier(0.4, 0, 0.2, 1); -} -.mat-button-toggle-vertical .mat-button-toggle-button { - transition: none; -} -.mat-button-toggle-disabled .mat-button-toggle-button { - cursor: default; -} -.mat-button-toggle-button::-moz-focus-inner { - border: 0; -} -.mat-button-toggle-checked .mat-button-toggle-button:has(.mat-button-toggle-checkbox-wrapper) { - padding-left: 30px; -} -[dir=rtl] .mat-button-toggle-checked .mat-button-toggle-button:has(.mat-button-toggle-checkbox-wrapper) { - padding-left: 0; - padding-right: 30px; -} - -.mat-button-toggle-standalone.mat-button-toggle-appearance-standard { - --mat-focus-indicator-border-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); -} - -.mat-button-toggle-group-appearance-standard:not(.mat-button-toggle-vertical) .mat-button-toggle:last-of-type .mat-button-toggle-button::before { - border-top-right-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); - border-bottom-right-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); -} -.mat-button-toggle-group-appearance-standard:not(.mat-button-toggle-vertical) .mat-button-toggle:first-of-type .mat-button-toggle-button::before { - border-top-left-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); - border-bottom-left-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); -} - -.mat-button-toggle-group-appearance-standard.mat-button-toggle-vertical .mat-button-toggle:last-of-type .mat-button-toggle-button::before { - border-bottom-right-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); - border-bottom-left-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); -} -.mat-button-toggle-group-appearance-standard.mat-button-toggle-vertical .mat-button-toggle:first-of-type .mat-button-toggle-button::before { - border-top-right-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); - border-top-left-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); -} -`],encapsulation:2,changeDetection:0})}return o})(),Vi=(()=>{class o{static \u0275fac=function(e){return new(e||o)};static \u0275mod=E({type:o});static \u0275inj=x({imports:[je,sn,j]})}return o})();function Wn(o){return o.metadata&&(typeof o.metadata=="string"?JSON.parse(o.metadata):o.metadata).copilot_mode||null}function Wi(o){return Wn(o)==="troubleshooting_injection"}var it=(function(o){return o.NETWORK_ERROR="NETWORK_ERROR",o.PROJECT_NOT_OPENED="PROJECT_NOT_OPENED",o.LLM_NOT_CONFIGURED="LLM_NOT_CONFIGURED",o.SESSION_NOT_FOUND="SESSION_NOT_FOUND",o.UNAUTHORIZED="UNAUTHORIZED",o.UNKNOWN_ERROR="UNKNOWN_ERROR",o})(it||{});var zt=class{authToken;id;name;location;host;port;path;ubridge_path;status="stopped";protocol;username;password;tokenExpired=!1};var ln=(()=>{class o{httpController;controllerIds=[];serviceInitialized=new T;isServiceInitialized;constructor(t){this.httpController=t,this.controllerIds=this.getcontrollerIds(),this.isServiceInitialized=!0,this.serviceInitialized.next(this.isServiceInitialized)}getcontrollerIds(){let t=localStorage.getItem("controllerIds");if(t?.length>0){let e=t.split(",");return[...new Set(e)].filter(n=>n&&n.trim().length>0)}return[]}updatecontrollerIds(){localStorage.removeItem("controllerIds"),localStorage.setItem("controllerIds",this.controllerIds.toString())}get(t){let e=JSON.parse(localStorage.getItem(`controller-${t}`));return new Promise(i=>{i(e)})}create(t){if(this.findAllSync().some(r=>r.name===t.name))return Promise.reject(new Error(`Controller with name "${t.name}" already exists`));let n=this.controllerIds.map(r=>parseInt(r.replace("controller-",""),10)).filter(r=>!isNaN(r)),i=n.length>0?Math.max(...n):0;return t.id=i+1,localStorage.setItem(`controller-${t.id}`,JSON.stringify(t)),this.controllerIds.push(`controller-${t.id}`),this.updatecontrollerIds(),new Promise(r=>{r(t)})}findAllSync(){let t=[];return this.controllerIds.forEach(e=>{let n=localStorage.getItem(e);n&&t.push(JSON.parse(n))}),t}isControllerNameTaken(t){return this.findAllSync().some(n=>n.name===t)}update(t){return localStorage.removeItem(`controller-${t.id}`),localStorage.setItem(`controller-${t.id}`,JSON.stringify(t)),new Promise(n=>{n(t)})}findAll(){return new Promise(e=>{let n=[];this.controllerIds.forEach(i=>{let a=localStorage.getItem(i);if(a){let r=JSON.parse(a);n.push(r)}}),e(n)})}delete(t){return localStorage.removeItem(`controller-${t.id}`),this.controllerIds=this.controllerIds.filter(n=>n!==`controller-${t.id}`),this.updatecontrollerIds(),new Promise(n=>{n(t.id)})}getControllerUrl(t){return`${t.protocol}//${t.host}:${t.port}/`}checkControllerVersion(t){return this.httpController.get(t,"/version").pipe(ne(5e3),f(e=>{if(e.name==="TimeoutError"){let n=new Error("Connection timeout");return g(()=>n)}return g(()=>e)}))}getLocalController(t,e){return new Promise((i,a)=>{this.findAll().then(r=>{let d=r.find(c=>c.location==="bundled");if(d)d.host=t,d.port=e,d.protocol=location.protocol,this.update(d).then(c=>{i(c)},a);else{let c=new zt;c.name="local",c.host=t,c.port=e,c.location="bundled",c.protocol=location.protocol,this.create(c).then(C=>{i(C)},a)}},a)})}static \u0275fac=function(e){return new(e||o)(S(Nt))};static \u0275prov=D({token:o,factory:o.\u0275fac})}return o})();var ea=(()=>{class o{http;httpController;controllerService;currentProjectId=null;currentSessionId=null;isStreaming=new ee(!1);constructor(t,e,n){this.http=t,this.httpController=e,this.controllerService=n}injectFault(t,e,n){let i=`${this.getControllerUrl(t)}/v3/copilot/projects/${e}/chat/inject`,a=this.getAuthHeaders(t),r={"Content-Type":"application/json"};return a.keys().forEach(d=>{let c=a.get(d);c&&(r[d]=c)}),new Ht(d=>(fetch(i,{method:"POST",headers:r,body:JSON.stringify({message:n})}).then(async c=>{if(!c.ok){let u=`HTTP error! status: ${c.status}`;try{let b=await c.json();b.message&&(u=b.message)}catch{c.statusText&&(u=c.statusText)}let _=new Error(u);throw _.status=c.status,_.statusText=c.statusText,_.error={message:u},_}if(!c.body)throw new Error("Response body is null");let C=c.body.getReader(),Gt=new TextDecoder,H="";(async()=>{try{for(;;){let{done:u,value:_}=await C.read();if(u){d.complete();break}H+=Gt.decode(_,{stream:!0});let b=H.split(` -`);H=b.pop()||"";for(let at of b)if(at.startsWith("data: ")){let V=at.slice(6).trim();if(V)try{let p=JSON.parse(V);if(p.type==="heartbeat")continue;if(d.next(p),p.type==="done"||p.type==="error"){d.complete();break}}catch(p){console.error("Failed to parse SSE data:",V,p)}}if(d.closed)break}}catch(u){console.error("Stream processing error:",u),d.error(u)}finally{C.cancel()}})()}).catch(c=>{console.error("Fetch error:",c),d.error(c)}),()=>{})).pipe(f(d=>g(()=>d)))}streamChat(t,e,n){this.currentProjectId=e,this.isStreaming.next(!0);let i=`${this.getControllerUrl(t)}/v3/copilot/projects/${e}/chat/stream`,a=this.getAuthHeaders(t),r={"Content-Type":"application/json"};return a.keys().forEach(d=>{let c=a.get(d);c&&(r[d]=c)}),new Ht(d=>(fetch(i,{method:"POST",headers:r,body:JSON.stringify(n)}).then(async c=>{if(!c.ok){let u=`HTTP error! status: ${c.status}`;try{let b=await c.json();b.message&&(u=b.message)}catch{c.statusText&&(u=c.statusText)}let _=new Error(u);throw _.status=c.status,_.statusText=c.statusText,_.error={message:u},_}if(!c.body)throw new Error("Response body is null");let C=c.body.getReader(),Gt=new TextDecoder,H="";(async()=>{try{for(;;){let{done:u,value:_}=await C.read();if(u){d.complete();break}H+=Gt.decode(_,{stream:!0});let b=H.split(` -`);H=b.pop()||"";for(let at of b)if(at.startsWith("data: ")){let V=at.slice(6).trim();if(V)try{let p=JSON.parse(V);if(p.type==="heartbeat")continue;if(p.session_id&&(this.currentSessionId=p.session_id),d.next(p),p.type==="done"||p.type==="error"){d.complete();break}}catch(p){console.error("Failed to parse SSE data:",V,p)}}if(d.closed)break}}catch(u){console.error("Stream processing error:",u),d.error(u)}finally{C.cancel()}})()}).catch(c=>{console.error("Fetch error:",c),d.error(c)}).finally(()=>{this.isStreaming.next(!1)}),()=>{this.isStreaming.next(!1)})).pipe(f(d=>(this.isStreaming.next(!1),g(()=>d))))}getSessions(t,e){return this.httpController.get(t,`/copilot/projects/${e}/chat/sessions`).pipe(f(n=>(console.error("Failed to get sessions:",n),g(()=>n))))}getSessionHistory(t,e,n,i=100){let a=i?{limit:i}:void 0;return this.httpController.get(t,`/copilot/projects/${e}/chat/sessions/${n}/history`).pipe(f(r=>(console.error("Failed to get session history:",r),g(()=>r))))}renameSession(t,e,n,i){let a={title:i};return this.httpController.patch(t,`/copilot/projects/${e}/chat/sessions/${n}`,a).pipe(f(r=>(console.error("Failed to rename session:",r),g(()=>r))))}deleteSession(t,e,n){return this.httpController.delete(t,`/copilot/projects/${e}/chat/sessions/${n}`).pipe(f(i=>(console.error("Failed to delete session:",i),g(()=>i))))}pinSession(t,e,n){return this.httpController.put(t,`/copilot/projects/${e}/chat/sessions/${n}/pin`,null).pipe(f(i=>(console.error("Failed to pin session:",i),g(()=>i))))}unpinSession(t,e,n){return this.httpController.delete(t,`/copilot/projects/${e}/chat/sessions/${n}/pin`).pipe(f(i=>(console.error("Failed to unpin session:",i),g(()=>i))))}getStreamingState(){return this.isStreaming.asObservable()}abortChat(t,e,n){return this.httpController.post(t,`/copilot/projects/${e}/chat/sessions/${n}/abort`,null).pipe(f(i=>(console.error("Failed to abort chat:",i),g(()=>i))))}getCurrentSessionId(){return this.currentSessionId}resetCurrentSession(){this.currentSessionId=null,this.currentProjectId=null}reloadSkills(t){return this.httpController.post(t,"/copilot/reload/skills",null).pipe(f(e=>(console.error("Failed to reload skills:",e),g(()=>e))))}getControllerUrl(t){return`${t.protocol==="https:"?"https":"http"}://${t.host}:${t.port}`}getAuthHeaders(t){let e=new Ce;if(t.authToken)return e.set("Authorization",`Bearer ${t.authToken}`);if(t.username&&t.password){let n=btoa(`${t.username}:${t.password}`);return e.set("Authorization",`Basic ${n}`)}return e}createChatError(t){return t.status===401?{type:it.UNAUTHORIZED,message:"Unauthorized access",details:t}:t.status===404?{type:it.SESSION_NOT_FOUND,message:"Session not found",details:t}:t.message&&t.message.includes("fetch")?{type:it.NETWORK_ERROR,message:"Network connection failed",details:t}:{type:it.UNKNOWN_ERROR,message:t.message||"Unknown error",details:t}}static \u0275fac=function(e){return new(e||o)(S(At),S(Nt),S(ln))};static \u0275prov=D({token:o,factory:o.\u0275fac,providedIn:"root"})}return o})();export{io as a,ao as b,Qe as c,Do as d,xo as e,zt as f,ln as g,Jt as h,kn as i,Kt as j,Ke as k,gt as l,On as m,Yt as n,pi as o,fi as p,_i as q,bi as r,vi as s,Un as t,sn as u,Vi as v,Wi as w,ea as x}; diff --git a/gns3server/static/web-ui/chunk-ETQHA42W.js b/gns3server/static/web-ui/chunk-ETQHA42W.js new file mode 100644 index 000000000..d7a67d727 --- /dev/null +++ b/gns3server/static/web-ui/chunk-ETQHA42W.js @@ -0,0 +1,2 @@ +import{$ as ke,A as _e,B as ye,D as fe,E as xe,F as Ce,N as be,R as ve,S as Me,U as Se,V as Pe,W as Oe,_ as we,a as V,b as H,ba as ze,c as me,ca as Ee,da as Te,e as pe,ea as Fe,f as ge,fa as Ie,g as ue,ga as Ne,ha as De,ia as $e,ja as We,w as he}from"./chunk-3DUTSYEG.js";import{$a as S,Aa as X,Ac as re,Bc as I,Ca as q,Cb as B,Da as A,Db as K,Ea as s,Eb as F,Gb as r,Hb as _,Ib as ee,Jb as Q,Kd as ae,Ma as z,Pd as se,Qb as te,Sa as E,Tb as v,Te as N,Ue as D,Wa as J,Wd as le,Xa as T,Yb as U,Za as M,_b as G,_e as $,a as j,af as de,b as L,bf as W,cb as w,da as h,db as k,dc as ne,eb as d,fb as n,gb as i,ha as x,hb as b,ia as C,je as ce,ma as Z,mc as oe,oa as O,ob as P,qb as p,sb as u,vc as ie}from"./chunk-G7NALUVX.js";var Qe=(a,m)=>m.name;function Ue(a,m){if(a&1&&(n(0,"div",5),b(1,"span",6),n(2,"span",7)(3,"strong"),r(4),i(),n(5,"span"),r(6),i()()()),a&2){let e=m.$implicit;d("matTooltip",e.bpf),s(),K("background",e.color),F("marker-legend__swatch--default",!e.color),s(3),_(e.name),s(2),_(e.bpf)}}function Ge(a,m){if(a&1&&(n(0,"div",3),w(1,Ue,7,7,"div",5,Qe),i()),a&2){let e=u();s(),k(e.entries())}}function Ye(a,m){if(a&1){let e=P();n(0,"div",4)(1,"mat-icon"),r(2,"insights"),i(),n(3,"span"),r(4,"No markers defined"),i(),n(5,"button",8),p("click",function(){x(e);let t=u();return C(t.onClick())}),r(6,"Open marker manager"),i()()}}var Ve=(()=>{class a{registry=h(Te);openMarkerManager=new O;entries=ne(()=>{let e=this.registry.entries(),o=new Map;for(let l of e)o.has(l.name)||o.set(l.name,{bpf:l.bpf,color:l.color});let t=[];for(let[l,c]of o)t.push({name:l,bpf:c.bpf,color:c.color});return t});onClick(){this.openMarkerManager.emit()}static \u0275fac=function(o){return new(o||a)};static \u0275cmp=z({type:a,selectors:[["app-marker-legend"]],outputs:{openMarkerManager:"openMarkerManager"},decls:9,vars:2,consts:[[1,"marker-legend"],[1,"marker-legend__header"],["mat-icon-button","","type","button","aria-label","Open marker manager","matTooltip","Open marker manager","matTooltipPosition","left",1,"marker-legend__manage",3,"click"],[1,"marker-legend__list"],[1,"marker-legend__empty"],["matTooltipPosition","above",1,"marker-legend__row",3,"matTooltip"],[1,"marker-legend__swatch"],[1,"marker-legend__identity"],["mat-button","","type","button",3,"click"]],template:function(o,t){o&1&&(n(0,"div",0)(1,"div",1)(2,"span"),r(3),i(),n(4,"button",2),p("click",function(){return t.onClick()}),n(5,"mat-icon"),r(6,"settings"),i()()(),M(7,Ge,3,0,"div",3)(8,Ye,7,0,"div",4),i()),o&2&&(s(3),ee("",t.entries().length," markers"),s(4),S(t.entries().length>0?7:8))},dependencies:[I,H,V,D,N,W,de,$],styles:[".marker-legend[_ngcontent-%COMP%]{display:flex;flex:1 1 auto;min-height:0;flex-direction:column}.marker-legend__header[_ngcontent-%COMP%]{display:flex;min-height:var(--topology-control-height);align-items:center;justify-content:space-between;padding-inline:var(--gns3-density-space-3);color:var(--mat-sys-on-surface-variant);font:var(--mat-sys-label-medium);font-weight:600}.marker-legend__manage[_ngcontent-%COMP%]{width:var(--topology-control-height);height:var(--topology-control-height)}.marker-legend__list[_ngcontent-%COMP%]{min-height:0;flex:1 1 auto;overflow:auto;overflow-x:hidden}.marker-legend__row[_ngcontent-%COMP%]{display:flex;min-height:var(--topology-row-height);box-sizing:border-box;align-items:center;gap:var(--gns3-density-space-2);padding-inline:var(--gns3-density-space-3);border-bottom:1px solid var(--mat-sys-outline-variant);cursor:default}.marker-legend__row[_ngcontent-%COMP%]:hover{background:var(--mat-sys-surface-container)}.marker-legend__swatch[_ngcontent-%COMP%]{width:12px;height:12px;flex:0 0 12px;border-radius:3px}.marker-legend__swatch--default[_ngcontent-%COMP%]{background-color:var(--mat-sys-primary)}.marker-legend__identity[_ngcontent-%COMP%]{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.marker-legend__identity[_ngcontent-%COMP%] strong[_ngcontent-%COMP%], .marker-legend__identity[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.marker-legend__identity[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font:var(--mat-sys-body-small);font-weight:600;color:var(--mat-sys-on-surface)}.marker-legend__identity[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface-variant);font:var(--mat-sys-label-small)}.marker-legend__empty[_ngcontent-%COMP%]{display:flex;flex:1 1 auto;flex-direction:column;align-items:center;justify-content:center;gap:var(--gns3-density-space-2);color:var(--mat-sys-on-surface-variant)}.marker-legend__empty[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface-variant)}.marker-legend__empty[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{font:var(--mat-sys-body-small)}"],changeDetection:0})}return a})();var Ze=()=>({top:!0}),Xe=()=>({right:!0}),qe=()=>({bottom:!0}),Je=()=>({left:!0}),Ke=()=>({top:!0,left:!0}),et=()=>({top:!0,right:!0}),tt=()=>({bottom:!0,left:!0}),nt=()=>({bottom:!0,right:!0}),ot=(a,m)=>m.node_id,it=(a,m)=>m.compute_id;function rt(a,m){if(a&1){let e=P();n(0,"div",37),p("mousemove",function(t){x(e);let l=u();return C(l.dragWidget(t))},A)("mouseup",function(){x(e);let t=u();return C(t.toggleDragging(!1))},A),i()}}function at(a,m){a&1&&(n(0,"mat-icon"),r(1,"map"),i(),n(2,"span"),r(3,"Topology"),i())}function st(a,m){if(a&1&&(n(0,"span",21),r(1),i()),a&2){let e=u();s(),_(e.activeFilterCount)}}function lt(a,m){if(a&1&&b(0,"img",42),a&2){let e=u().$implicit;d("src",e.symbol_url,X)}}function ct(a,m){a&1&&(n(0,"mat-icon"),r(1,"dns"),i())}function dt(a,m){if(a&1){let e=P();n(0,"div",38),p("click",function(){let t=x(e).$implicit,l=u();return C(l.selectNode(t))})("keydown.enter",function(){let t=x(e).$implicit,l=u();return C(l.selectNode(t))}),n(1,"div",39),b(2,"span",40),n(3,"span",41),M(4,lt,1,1,"img",42)(5,ct,2,0,"mat-icon"),i(),n(6,"span",43)(7,"strong"),r(8),i()()(),n(9,"span",44),r(10),i(),n(11,"span",45),r(12),i(),n(13,"button",46),p("click",function(t){let l=x(e).$implicit;return u().selectNode(l),C(t.stopPropagation())}),n(14,"mat-icon"),r(15,"more_horiz"),i()()()}if(a&2){let e=m.$implicit,o=u(),t=B(60);F("node-row--selected",(o.selectedNode==null?null:o.selectedNode.node_id)===e.node_id),s(2),d("matTooltip",o.getStatusLabel(e.status)),T("data-status",e.status)("aria-label",o.getStatusLabel(e.status)),s(2),S(e.symbol_url?4:5),s(4),_(e.name),s(2),_(o.hasConsole(e)?e.console_type:"none"),s(2),_(o.hasConsole(e)?e.console:"-"),s(),J("aria-label",te("Actions for ",e.name)),d("matMenuTriggerFor",t)}}function mt(a,m){a&1&&(n(0,"div",32)(1,"mat-icon"),r(2,"search_off"),i(),n(3,"span"),r(4,"No nodes match the current filters"),i()())}function pt(a,m){a&1&&(n(0,"mat-icon"),r(1,"insights"),i(),n(2,"span"),r(3,"Markers"),i())}function gt(a,m){a&1&&(n(0,"mat-icon"),r(1,"dns"),i(),n(2,"span"),r(3,"Computes"),i())}function ut(a,m){if(a&1&&(n(0,"div",36),b(1,"span",47),n(2,"span",48)(3,"strong"),r(4),i(),n(5,"span"),r(6),i()(),n(7,"span",49)(8,"span"),r(9,"CPU"),i(),n(10,"strong"),r(11),U(12,"number"),i()(),n(13,"span",49)(14,"span"),r(15,"Memory"),i(),n(16,"strong"),r(17),U(18,"number"),i()()()),a&2){let e=m.$implicit,o=u();d("matTooltip",o.getComputeTooltip(e)),s(),T("data-status",e.connected?"started":"stopped"),s(3),_(e.name||e.compute_id),s(2),Q("",e.host,":",e.port),s(5),_(e.cpu_usage_percent!==null&&e.cpu_usage_percent!==void 0?G(12,7,e.cpu_usage_percent,"1.0-1")+"%":"-"),s(6),_(e.memory_usage_percent!==null&&e.memory_usage_percent!==void 0?G(18,10,e.memory_usage_percent,"1.0-1")+"%":"-")}}function ht(a,m){a&1&&(n(0,"div",32)(1,"mat-icon"),r(2,"dns"),i(),n(3,"span"),r(4,"No computes available"),i()())}var an=(()=>{class a{nodesDataSource=h(Ee);computeService=h(xe);linksDataSource=h(ze);notificationService=h(Ce);toasterService=h(be);nodeConsoleService=h(Fe);mapSettingsService=h(ke);cd=h(oe);document=h(Z);controller;project;openWebConsoleInline=new O;openMarkerManager=new O;computesInitialized=!1;style={};subscriptions=[];nodes=[];filteredNodes=[];sortingOrder="asc";startedStatusFilterEnabled=!1;suspendedStatusFilterEnabled=!1;stoppedStatusFilterEnabled=!1;captureFilterEnabled=!1;packetFilterEnabled=!1;computes=[];searchQuery="";selectedNode=null;selectedTabIndex=0;isDraggingEnabled=!1;ngOnInit(){this.subscriptions.push(this.nodesDataSource.changes.subscribe(e=>{let o=this.selectedNode?.node_id;this.nodes=e.map(t=>this.normalizeConsoleHost(t)),this.selectedNode=o?this.nodes.find(t=>t.node_id===o)??null:null,this.applyFilters(),this.cd.markForCheck()})),this.initializeComputes(),this.revertPosition()}initializeComputes(){!this.controller||this.computesInitialized||(this.computesInitialized=!0,this.subscriptions.push(this.notificationService.computeCacheUpdated.subscribe(e=>{this.computes=e,this.cd.markForCheck()})),this.subscriptions.push(this.computeService.getComputes(this.controller).subscribe({next:e=>{this.computes=e,this.notificationService.setInitialComputes(e),this.cd.markForCheck()},error:e=>{let o=e.error?.message||e.message||"Failed to load computes";this.toasterService.error(o),this.cd.markForCheck()}})))}normalizeConsoleHost(e){return(e.console_host==="0.0.0.0"||e.console_host==="0:0:0:0:0:0:0:0"||e.console_host==="::")&&this.controller?.host?L(j({},e),{console_host:this.controller.host}):e}revertPosition(){let e=this.document.defaultView?.innerWidth??1280,o=this.document.defaultView?.innerHeight??800;if(e<=720){this.style={position:"fixed",top:"56px",left:"0px",width:`${e}px`,height:`${Math.max(0,o-56)}px`};return}let t=localStorage.getItem("leftPosition"),l=localStorage.getItem("rightPosition"),c=localStorage.getItem("topPosition"),g=localStorage.getItem("widthOfWidget"),R=localStorage.getItem("heightOfWidget");if(c){let f=Math.min(Math.max(Number(g)||720,400),e),Y=Math.min(Math.max(Number(R)||680,420),o),He=Math.min(Math.max(Number(c)||0,0),Math.max(0,o-Y)),Re=t!==null?{left:`${Math.min(Math.max(Number(t)||0,0),Math.max(0,e-f))}px`}:{right:`${Math.min(Math.max(Number(l)||0,0),Math.max(0,e-f))}px`};this.style=L(j({position:"fixed"},Re),{top:`${He}px`,width:`${f}px`,height:`${Y}px`})}else{let y=this.document.documentElement.dataset.density==="compact";this.style={top:y?"56px":"68px",right:y?"8px":"16px",width:"720px",height:"680px"}}}onViewportResize(){this.revertPosition()}toggleDragging(e){this.isDraggingEnabled=e}startDraggingFromTabStrip(e){let o=e.target;e.button===0&&o.closest(".mat-mdc-tab-header")&&this.toggleDragging(!0)}dragWidget(e){let o=Number(e.movementX),t=Number(e.movementY),l=Number(this.style.width.split("px")[0]),c=Number(this.style.height.split("px")[0]),g=this.document.defaultView?.innerWidth??1280,R=this.document.defaultView?.innerHeight??800,y=this.clamp(Number(this.style.top.split("px")[0])+t,0,R-c);if(this.style.left){let f=this.clamp(Number(this.style.left.split("px")[0])+o,0,g-l);this.style={position:"fixed",left:`${f}px`,top:`${y}px`,width:`${l}px`,height:`${c}px`},localStorage.setItem("leftPosition",f.toString()),localStorage.removeItem("rightPosition"),localStorage.setItem("topPosition",y.toString()),localStorage.setItem("widthOfWidget",l.toString()),localStorage.setItem("heightOfWidget",c.toString())}else{let f=this.clamp(Number(this.style.right.split("px")[0])-o,0,g-l);this.style={position:"fixed",right:`${f}px`,top:`${y}px`,width:`${l}px`,height:`${c}px`},localStorage.setItem("rightPosition",f.toString()),localStorage.removeItem("leftPosition"),localStorage.setItem("topPosition",y.toString()),localStorage.setItem("widthOfWidget",l.toString()),localStorage.setItem("heightOfWidget",c.toString())}}clamp(e,o,t){return Math.min(Math.max(e,o),Math.max(o,t))}validate(e){return!(e.rectangle.width&&e.rectangle.height&&(e.rectangle.width<400||e.rectangle.height<420))}onResizeEnd(e){this.style={position:"fixed",left:`${e.rectangle.left}px`,top:`${e.rectangle.top}px`,width:`${e.rectangle.width}px`,height:`${e.rectangle.height}px`},localStorage.setItem("leftPosition",e.rectangle.left.toString()),localStorage.removeItem("rightPosition"),localStorage.setItem("topPosition",e.rectangle.top.toString()),localStorage.setItem("widthOfWidget",e.rectangle.width.toString()),localStorage.setItem("heightOfWidget",e.rectangle.height.toString())}onTabSelectionChange(e){this.selectedTabIndex=e}compareAsc(e,o){return e.name.localeCompare(o.name)}compareDesc(e,o){return o.name.localeCompare(e.name)}ngOnDestroy(){this.subscriptions.forEach(e=>e.unsubscribe())}setSortingOrder(){this.applyFilters()}setSearchQuery(e){this.searchQuery=e,this.applyFilters()}applyStatusFilter(e){e==="started"?this.startedStatusFilterEnabled=!this.startedStatusFilterEnabled:e==="stopped"?this.stoppedStatusFilterEnabled=!this.stoppedStatusFilterEnabled:e==="suspended"&&(this.suspendedStatusFilterEnabled=!this.suspendedStatusFilterEnabled),this.applyFilters()}applyCaptureFilter(e){e==="capture"?this.captureFilterEnabled=!this.captureFilterEnabled:e==="packet"&&(this.packetFilterEnabled=!this.packetFilterEnabled),this.applyFilters()}applyFilters(){let e=[];this.startedStatusFilterEnabled&&(e=e.concat(this.nodes.filter(t=>t.status==="started"))),this.stoppedStatusFilterEnabled&&(e=e.concat(this.nodes.filter(t=>t.status==="stopped"))),this.suspendedStatusFilterEnabled&&(e=e.concat(this.nodes.filter(t=>t.status==="suspended"))),!this.startedStatusFilterEnabled&&!this.stoppedStatusFilterEnabled&&!this.suspendedStatusFilterEnabled&&(e=e.concat(this.nodes)),this.captureFilterEnabled&&(e=this.checkCapturing(e)),this.packetFilterEnabled&&(e=this.checkPacketFilters(e));let o=this.searchQuery.trim().toLocaleLowerCase();o&&(e=e.filter(t=>t.name.toLocaleLowerCase().includes(o))),this.sortingOrder==="asc"?this.filteredNodes=[...e].sort(this.compareAsc):this.filteredNodes=[...e].sort(this.compareDesc)}checkCapturing(e){let o=this.linksDataSource.getItems(),t=new Set;return o.forEach(l=>{l.capturing&&l.nodes.forEach(c=>{t.add(c.node_id)})}),e.filter(l=>t.has(l.node_id))}checkPacketFilters(e){let o=this.linksDataSource.getItems(),t=new Set;return o.forEach(l=>{Object.values(l.filters??{}).some(c=>this.hasActiveFilterValue(c))&&l.nodes.forEach(c=>{t.add(c.node_id)})}),e.filter(l=>t.has(l.node_id))}selectNode(e){this.selectedNode=e}hasConsole(e){return e.console!==null&&e.console!==void 0&&!!e.console_type&&e.console_type!=="none"}canOpenConsole(e){return!e||e.status!=="started"?!1:e.console_type==="telnet"||e.console_type==="ssh"||e.console_type==="vnc"||e.console_type?.startsWith("http")===!0}openConsole(e=this.selectedNode){if(e){if(e.status!=="started"){this.toasterService.error("To open console please start the node");return}if(e.console_type==="vnc"||e.console_type&&e.console_type.startsWith("http")){this.openWebConsoleInline.emit({node:e,controller:this.controller,project:this.project});return}if(e.console_type!=="telnet"&&e.console_type!=="ssh"){this.toasterService.error(`Console type '${e.console_type||"none"}' is not supported in the embedded console.`);return}this.mapSettingsService.logConsoleSubject.next(!0),this.nodeConsoleService.openConsoleForNode(e)}}getStatusLabel(e){return e==="started"?"Running":e==="stopped"?"Stopped":e==="suspended"?"Suspended":e||"Unknown"}get activeFilterCount(){return[this.startedStatusFilterEnabled,this.stoppedStatusFilterEnabled,this.suspendedStatusFilterEnabled,this.captureFilterEnabled,this.packetFilterEnabled].filter(Boolean).length}get connectedComputeCount(){return this.computes.filter(e=>e.connected).length}hasActiveFilterValue(e){return Array.isArray(e)?e.some(o=>o!=null&&o!==""&&o!==0):e!=null&&e!==""&&e!==0&&e!==!1}getComputeTooltip(e){return e?[`Name: ${e.name||"N/A"}`,`Host: ${e.host}:${e.port}`,`Connected: ${e.connected?"Yes":"No"}`,e.cpu_usage_percent!=null?`CPU: ${e.cpu_usage_percent.toFixed(1)}%`:null,e.memory_usage_percent!=null?`Memory: ${e.memory_usage_percent.toFixed(1)}%`:null].filter(Boolean).join(` +`):""}static \u0275fac=function(o){return new(o||a)};static \u0275cmp=z({type:a,selectors:[["app-topology-summary"]],hostBindings:function(o,t){o&1&&p("resize",function(){return t.onViewportResize()},q)},inputs:{controller:"controller",project:"project"},outputs:{openWebConsoleInline:"openWebConsoleInline",openMarkerManager:"openMarkerManager"},decls:80,vars:36,consts:[["filterMenu","matMenu"],["nodeActionsMenu","matMenu"],["mwlResizable","","aria-label","Topology map",1,"topology-inspector",3,"resizeStart","resizeEnd","ngStyle","validateResize","enableGhostResize"],["mwlResizeHandle","",1,"resize-handle","resize-handle-top",3,"resizeEdges"],["mwlResizeHandle","",1,"resize-handle","resize-handle-right",3,"resizeEdges"],["mwlResizeHandle","",1,"resize-handle","resize-handle-bottom",3,"resizeEdges"],["mwlResizeHandle","",1,"resize-handle","resize-handle-left",3,"resizeEdges"],["mwlResizeHandle","",1,"resize-handle","resize-handle-top-left",3,"resizeEdges"],["mwlResizeHandle","",1,"resize-handle","resize-handle-top-right",3,"resizeEdges"],["mwlResizeHandle","",1,"resize-handle","resize-handle-bottom-left",3,"resizeEdges"],["mwlResizeHandle","",1,"resize-handle","resize-handle-bottom-right",3,"resizeEdges"],["animationDuration","0ms",1,"inspector-tabs",3,"selectedIndexChange","mousedown","selectedIndex"],["mat-tab-label",""],[1,"tab-content"],[1,"node-toolbar"],[1,"search-control"],["type","search","aria-label","Filter nodes","placeholder","Filter nodes",3,"ngModelChange","ngModel"],["aria-label","Sort nodes",1,"sort-control",3,"selectionChange","value"],["value","asc"],["value","desc"],["mat-icon-button","","type","button",1,"filter-button",3,"matMenuTriggerFor"],[1,"filter-button__count"],[1,"topology-filter-menu"],[1,"filter-menu",3,"click"],[1,"filter-menu__label"],[3,"change","checked"],["role","table","aria-label","Project nodes",1,"node-table"],[1,"node-table__body"],["role","row",1,"node-table__header"],["role","columnheader"],["role","columnheader","aria-label","Actions"],["role","row","tabindex","0",1,"node-row",3,"node-row--selected"],[1,"empty-state"],["mat-menu-item","",3,"click","disabled"],[3,"openMarkerManager"],[1,"compute-list"],["matTooltipPosition","above",1,"compute-row",3,"matTooltip"],[3,"mousemove","mouseup"],["role","row","tabindex","0",1,"node-row",3,"click","keydown.enter"],["role","cell",1,"node-cell"],[1,"status-dot",3,"matTooltip"],[1,"node-symbol"],["alt","",3,"src"],[1,"node-identity"],["role","cell",1,"protocol-cell"],["role","cell",1,"port-cell"],["mat-icon-button","","type","button",1,"row-action",3,"click","aria-label","matMenuTriggerFor"],[1,"status-dot"],[1,"compute-identity"],[1,"compute-metric"]],template:function(o,t){if(o&1){let l=P();M(0,rt,1,0,"div"),n(1,"section",2),p("resizeStart",function(){return t.toggleDragging(!1)})("resizeEnd",function(g){return t.onResizeEnd(g)}),b(2,"div",3)(3,"div",4)(4,"div",5)(5,"div",6)(6,"div",7)(7,"div",8)(8,"div",9)(9,"div",10),n(10,"mat-tab-group",11),p("selectedIndexChange",function(g){return t.onTabSelectionChange(g)})("mousedown",function(g){return t.startDraggingFromTabStrip(g)}),n(11,"mat-tab"),E(12,at,4,0,"ng-template",12),n(13,"div",13)(14,"div",14)(15,"div",15)(16,"mat-icon"),r(17,"search"),i(),n(18,"input",16),p("ngModelChange",function(g){return t.setSearchQuery(g)}),i()(),n(19,"mat-select",17),p("selectionChange",function(g){return x(l),t.sortingOrder=g.value,C(t.setSortingOrder())}),n(20,"mat-option",18),r(21,"Name (A-Z)"),i(),n(22,"mat-option",19),r(23,"Name (Z-A)"),i()(),n(24,"button",20)(25,"mat-icon"),r(26,"filter_alt"),i(),M(27,st,2,1,"span",21),i()(),n(28,"mat-menu",22,0)(30,"div",23),p("click",function(g){return g.stopPropagation()}),n(31,"span",24),r(32,"Status"),i(),n(33,"mat-checkbox",25),p("change",function(){return t.applyStatusFilter("started")}),r(34," Running "),i(),n(35,"mat-checkbox",25),p("change",function(){return t.applyStatusFilter("stopped")}),r(36," Stopped "),i(),n(37,"mat-checkbox",25),p("change",function(){return t.applyStatusFilter("suspended")}),r(38," Suspended "),i(),b(39,"mat-divider"),n(40,"span",24),r(41,"Links"),i(),n(42,"mat-checkbox",25),p("change",function(){return t.applyCaptureFilter("capture")}),r(43," Active capture "),i(),n(44,"mat-checkbox",25),p("change",function(){return t.applyCaptureFilter("packet")}),r(45," Packet filter "),i()()(),n(46,"div",26)(47,"div",27)(48,"div",28)(49,"span",29),r(50,"Node"),i(),n(51,"span",29),r(52,"Protocol"),i(),n(53,"span",29),r(54,"Port"),i(),b(55,"span",30),i(),w(56,dt,16,12,"div",31,ot,!1,mt,5,0,"div",32),i()(),n(59,"mat-menu",null,1)(61,"button",33),p("click",function(){return t.openConsole()}),n(62,"mat-icon"),r(63,"terminal"),i(),n(64,"span"),r(65,"Open console"),i()()()()(),n(66,"mat-tab"),E(67,pt,4,0,"ng-template",12),n(68,"div",13)(69,"app-marker-legend",34),p("openMarkerManager",function(){return t.openMarkerManager.emit()}),i()()(),n(70,"mat-tab"),E(71,gt,4,0,"ng-template",12),n(72,"div",13)(73,"div")(74,"span"),r(75),i()(),n(76,"div",35),w(77,ut,19,13,"div",36,it,!1,ht,5,0,"div",32),i()()()()()}if(o&2){let l=B(29);S(t.isDraggingEnabled?0:-1),s(),d("ngStyle",t.style)("validateResize",t.validate)("enableGhostResize",!0),s(),d("resizeEdges",v(28,Ze)),s(),d("resizeEdges",v(29,Xe)),s(),d("resizeEdges",v(30,qe)),s(),d("resizeEdges",v(31,Je)),s(),d("resizeEdges",v(32,Ke)),s(),d("resizeEdges",v(33,et)),s(),d("resizeEdges",v(34,tt)),s(),d("resizeEdges",v(35,nt)),s(),d("selectedIndex",t.selectedTabIndex),s(8),d("ngModel",t.searchQuery),s(),d("value",t.sortingOrder),s(5),d("matMenuTriggerFor",l),T("aria-label",t.activeFilterCount?t.activeFilterCount+" active node filters":"Filter nodes"),s(3),S(t.activeFilterCount?27:-1),s(6),d("checked",t.startedStatusFilterEnabled),s(2),d("checked",t.stoppedStatusFilterEnabled),s(2),d("checked",t.suspendedStatusFilterEnabled),s(5),d("checked",t.captureFilterEnabled),s(2),d("checked",t.packetFilterEnabled),s(12),k(t.filteredNodes),s(5),d("disabled",!t.canOpenConsole(t.selectedNode)),s(14),Q("",t.computes.length," computes \xB7 ",t.connectedComputeCount," connected"),s(2),k(t.computes)}},dependencies:[I,ie,ce,ae,se,le,we,Se,Pe,Oe,fe,ye,he,_e,Ne,Ie,H,V,D,N,W,$,ue,pe,me,ge,Me,ve,We,De,$e,Ve,re],styles:["[_nghost-%COMP%]{--topology-row-height: 46px;--topology-row-action-size: 32px;--topology-symbol-size: 30px;--topology-control-height: var(--gns3-density-control-height)}.topology-inspector[_ngcontent-%COMP%]{position:fixed;z-index:900;display:flex;min-width:400px;max-width:calc(100vw - 32px);min-height:420px;max-height:calc(100vh - 84px);box-sizing:border-box;flex-direction:column;overflow:hidden;border:1px solid var(--mat-sys-outline-variant);border-radius:var(--gns3-density-radius-sm);color:var(--mat-sys-on-surface);background:var(--mat-sys-surface-container-low);box-shadow:0 16px 40px color-mix(in srgb,var(--mat-sys-shadow) 22%,transparent);container-name:topology-panel;container-type:inline-size}.resize-handle[_ngcontent-%COMP%]{position:absolute;z-index:10;background:transparent}.resize-handle.resize-handle-top[_ngcontent-%COMP%], .resize-handle.resize-handle-bottom[_ngcontent-%COMP%]{left:12px;right:12px;height:10px;cursor:ns-resize}.resize-handle.resize-handle-top[_ngcontent-%COMP%]{top:-4px}.resize-handle.resize-handle-bottom[_ngcontent-%COMP%]{bottom:-4px}.resize-handle.resize-handle-left[_ngcontent-%COMP%], .resize-handle.resize-handle-right[_ngcontent-%COMP%]{top:12px;bottom:12px;width:10px;cursor:ew-resize}.resize-handle.resize-handle-left[_ngcontent-%COMP%]{left:-4px}.resize-handle.resize-handle-right[_ngcontent-%COMP%]{right:-4px}.resize-handle.resize-handle-top-left[_ngcontent-%COMP%], .resize-handle.resize-handle-top-right[_ngcontent-%COMP%], .resize-handle.resize-handle-bottom-left[_ngcontent-%COMP%], .resize-handle.resize-handle-bottom-right[_ngcontent-%COMP%]{width:18px;height:18px}.resize-handle.resize-handle-top-left[_ngcontent-%COMP%], .resize-handle.resize-handle-bottom-right[_ngcontent-%COMP%]{cursor:nwse-resize}.resize-handle.resize-handle-top-right[_ngcontent-%COMP%], .resize-handle.resize-handle-bottom-left[_ngcontent-%COMP%]{cursor:nesw-resize}.resize-handle.resize-handle-top-left[_ngcontent-%COMP%]{top:-5px;left:-5px}.resize-handle.resize-handle-top-right[_ngcontent-%COMP%]{top:-5px;right:-5px}.resize-handle.resize-handle-bottom-left[_ngcontent-%COMP%]{bottom:-5px;left:-5px}.resize-handle.resize-handle-bottom-right[_ngcontent-%COMP%]{right:-5px;bottom:-5px}.node-cell[_ngcontent-%COMP%], .node-identity[_ngcontent-%COMP%], .compute-identity[_ngcontent-%COMP%], .compute-row[_ngcontent-%COMP%]{min-width:0}.inspector-tabs[_ngcontent-%COMP%]{min-height:0;flex:1 1 auto}.tab-content[_ngcontent-%COMP%]{display:flex;min-height:0;height:100%;box-sizing:border-box;flex-direction:column;gap:var(--gns3-density-space-2);padding:var(--gns3-density-space-2) var(--gns3-density-space-3)}.tab-content[_ngcontent-%COMP%] app-marker-legend[_ngcontent-%COMP%]{display:flex;flex:1 1 auto;min-height:0;margin:calc(-1 * var(--gns3-density-space-2)) calc(-1 * var(--gns3-density-space-3))}.node-toolbar[_ngcontent-%COMP%]{display:grid;min-width:0;grid-template-columns:minmax(180px,1fr) minmax(150px,180px) var(--topology-control-height);align-items:center;justify-content:start;gap:var(--gns3-density-space-2)}.search-control[_ngcontent-%COMP%], .sort-control[_ngcontent-%COMP%], .filter-button[_ngcontent-%COMP%]{height:var(--topology-control-height);box-sizing:border-box}.search-control[_ngcontent-%COMP%]{display:flex;min-width:0;align-items:center;gap:var(--gns3-density-space-2);padding-inline:var(--gns3-density-space-3);border:1px solid var(--mat-sys-outline);border-radius:var(--gns3-density-radius-sm);background:var(--mat-sys-surface-container-lowest)}.search-control[_ngcontent-%COMP%]:focus-within{border-color:var(--mat-sys-primary);outline:1px solid var(--mat-sys-primary)}.search-control[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{flex:0 0 auto;color:var(--mat-sys-on-surface-variant)}.search-control[_ngcontent-%COMP%] input[_ngcontent-%COMP%]{min-width:0;width:100%;border:0;outline:0;color:var(--mat-sys-on-surface);background:transparent;font:var(--mat-sys-body-medium);letter-spacing:0}.search-control[_ngcontent-%COMP%] input[_ngcontent-%COMP%]::placeholder{color:var(--mat-sys-on-surface-variant)}.filter-button[_ngcontent-%COMP%]{position:relative;display:flex;width:var(--topology-control-height);min-width:0;align-items:center;justify-content:center;border:1px solid var(--mat-sys-outline);padding:0}.filter-button[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{margin:0;line-height:normal}.filter-button__count[_ngcontent-%COMP%]{position:absolute;top:2px;right:2px;display:grid;min-width:16px;height:16px;box-sizing:border-box;place-items:center;padding-inline:3px;border-radius:8px;color:var(--mat-sys-on-primary);background:var(--mat-sys-primary);font:var(--mat-sys-label-small)}.sort-control[_ngcontent-%COMP%]{display:flex;align-items:center;border:1px solid var(--mat-sys-outline);border-radius:var(--gns3-density-radius-sm);background:var(--mat-sys-surface-container-lowest)}.sort-control[_ngcontent-%COMP%]{padding-inline:var(--gns3-density-space-3)}.filter-menu[_ngcontent-%COMP%]{display:grid;min-width:220px;gap:var(--gns3-density-space-1);padding:var(--gns3-density-space-2) var(--gns3-density-space-3)}.filter-menu__label[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface-variant);font:var(--mat-sys-label-small)}.node-table[_ngcontent-%COMP%]{display:flex;min-height:0;flex:1 1 auto;flex-direction:column;overflow:hidden;border:1px solid var(--mat-sys-outline-variant);border-radius:var(--gns3-density-radius-sm);background:var(--mat-sys-surface-container-lowest)}.node-table__header[_ngcontent-%COMP%], .node-row[_ngcontent-%COMP%]{display:grid;grid-template-columns:minmax(190px,260px) 82px 64px minmax(0,1fr) 36px;align-items:center}.node-table__header[_ngcontent-%COMP%] > [_ngcontent-%COMP%]:last-child, .row-action[_ngcontent-%COMP%]{grid-column:5}.node-table__header[_ngcontent-%COMP%]{position:sticky;top:0;z-index:1;min-height:32px;padding-inline:var(--gns3-density-space-3) var(--gns3-density-space-1);border-bottom:1px solid var(--mat-sys-outline-variant);color:var(--mat-sys-on-surface-variant);background:var(--mat-sys-surface-container);font:var(--mat-sys-label-medium);font-weight:600}.node-table__body[_ngcontent-%COMP%], .compute-list[_ngcontent-%COMP%]{min-height:0;flex:1 1 auto;overflow:auto;overflow-x:hidden}.node-row[_ngcontent-%COMP%]{min-height:var(--topology-row-height);box-sizing:border-box;padding-inline:var(--gns3-density-space-3) var(--gns3-density-space-1);border-bottom:1px solid var(--mat-sys-outline-variant);outline:0;cursor:pointer}.node-row[_ngcontent-%COMP%]:hover, .node-row[_ngcontent-%COMP%]:focus-visible{background:var(--mat-sys-surface-container)}.node-row--selected[_ngcontent-%COMP%]{background:var(--mat-sys-primary-container);box-shadow:inset 3px 0 var(--mat-sys-primary)}.node-cell[_ngcontent-%COMP%]{display:flex;align-items:center;gap:var(--gns3-density-space-2);overflow:hidden}.status-dot[_ngcontent-%COMP%], .compute-row[_ngcontent-%COMP%] .status-dot[_ngcontent-%COMP%]{display:inline-block;width:12px;height:12px;flex:0 0 12px;border-radius:50%;background:var(--mat-sys-outline)}.status-dot[data-status=started][_ngcontent-%COMP%], .compute-row[_ngcontent-%COMP%] .status-dot[data-status=started][_ngcontent-%COMP%]{background:var(--custom-mat-sys-success)}.status-dot[data-status=stopped][_ngcontent-%COMP%], .compute-row[_ngcontent-%COMP%] .status-dot[data-status=stopped][_ngcontent-%COMP%]{background:var(--mat-sys-error)}.status-dot[data-status=suspended][_ngcontent-%COMP%], .compute-row[_ngcontent-%COMP%] .status-dot[data-status=suspended][_ngcontent-%COMP%]{background:var(--mat-sys-secondary)}.node-symbol[_ngcontent-%COMP%]{display:grid;width:var(--topology-symbol-size);height:var(--topology-symbol-size);flex:0 0 var(--topology-symbol-size);place-items:center}.node-symbol[_ngcontent-%COMP%] img[_ngcontent-%COMP%]{display:block;max-width:100%;max-height:100%;object-fit:contain}.node-symbol[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{color:var(--mat-sys-primary)}.node-identity[_ngcontent-%COMP%], .compute-identity[_ngcontent-%COMP%]{display:flex;flex:1 1 auto;flex-direction:column;overflow:hidden}.node-identity[_ngcontent-%COMP%] strong[_ngcontent-%COMP%], .node-identity[_ngcontent-%COMP%] span[_ngcontent-%COMP%], .compute-identity[_ngcontent-%COMP%] strong[_ngcontent-%COMP%], .compute-identity[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.node-identity[_ngcontent-%COMP%] strong[_ngcontent-%COMP%], .compute-identity[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font:var(--mat-sys-body-small);font-weight:600}.node-identity[_ngcontent-%COMP%] span[_ngcontent-%COMP%], .compute-identity[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface-variant);font:var(--mat-sys-label-small)}.protocol-cell[_ngcontent-%COMP%], .port-cell[_ngcontent-%COMP%]{width:100%;min-width:0;box-sizing:border-box;overflow:hidden;justify-self:start;color:var(--mat-sys-on-surface-variant);font:var(--mat-sys-label-medium);text-align:left;text-overflow:ellipsis;white-space:nowrap}.node-table__header[_ngcontent-%COMP%] span[_ngcontent-%COMP%]:nth-child(2), .node-table__header[_ngcontent-%COMP%] span[_ngcontent-%COMP%]:nth-child(3){width:100%;justify-self:start;text-align:left}.row-action[_ngcontent-%COMP%]{--mat-icon-button-state-layer-size: var(--topology-row-action-size);display:inline-flex;width:var(--topology-row-action-size);height:var(--topology-row-action-size);align-self:center;align-items:center;justify-self:center;justify-content:center;padding:0;line-height:1}.row-action[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{display:block;margin:0;line-height:24px}.empty-state[_ngcontent-%COMP%]{display:flex;align-items:center;justify-content:center;gap:var(--gns3-density-space-2);color:var(--mat-sys-on-surface-variant);font:var(--mat-sys-body-medium)}.compute-list[_ngcontent-%COMP%]{border:1px solid var(--mat-sys-outline-variant);border-radius:var(--gns3-density-radius-sm);background:var(--mat-sys-surface-container-lowest)}.compute-row[_ngcontent-%COMP%]{display:grid;width:100%;min-height:var(--topology-row-height);box-sizing:border-box;grid-template-columns:12px minmax(120px,1fr) minmax(54px,76px) minmax(64px,88px);align-items:center;gap:var(--gns3-density-space-2);padding-inline:var(--gns3-density-space-3);border-bottom:1px solid var(--mat-sys-outline-variant)}.compute-metric[_ngcontent-%COMP%]{display:flex;min-width:0;flex-direction:column;align-items:flex-end;overflow:hidden}.compute-metric[_ngcontent-%COMP%] span[_ngcontent-%COMP%], .compute-metric[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.compute-metric[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface-variant);font:var(--mat-sys-label-small)}.compute-metric[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font:var(--mat-sys-body-small);font-weight:600}@container topology-panel (max-width: 640px){.node-toolbar[_ngcontent-%COMP%]{grid-template-columns:minmax(0,1fr) var(--topology-control-height)}.sort-control[_ngcontent-%COMP%]{display:none}.node-table__header[_ngcontent-%COMP%], .node-row[_ngcontent-%COMP%]{grid-template-columns:minmax(160px,220px) 72px 54px minmax(0,1fr) 32px}}@container topology-panel (max-width: 440px){.node-table__header[_ngcontent-%COMP%], .node-row[_ngcontent-%COMP%]{grid-template-columns:minmax(140px,1fr) 68px 52px 30px}.node-table__header[_ngcontent-%COMP%] > [_ngcontent-%COMP%]:last-child, .row-action[_ngcontent-%COMP%]{grid-column:4}}@media(max-width:720px){.topology-inspector[_ngcontent-%COMP%]{min-width:0;min-height:0;max-width:none;max-height:none;border-radius:0}.resize-handle[_ngcontent-%COMP%]{display:none}}html[data-density=compact][_nghost-%COMP%], html[data-density=compact] [_nghost-%COMP%]{--topology-row-height: 38px;--topology-row-action-size: 28px;--topology-symbol-size: 24px}html[data-density=compact][_nghost-%COMP%] .topology-inspector[_ngcontent-%COMP%], html[data-density=compact] [_nghost-%COMP%] .topology-inspector[_ngcontent-%COMP%]{max-height:calc(100vh - 64px)}html[data-density=compact][_nghost-%COMP%] .node-table__header[_ngcontent-%COMP%], html[data-density=compact] [_nghost-%COMP%] .node-table__header[_ngcontent-%COMP%]{min-height:28px}"],changeDetection:0})}return a})();export{an as TopologySummaryComponent}; diff --git a/gns3server/static/web-ui/chunk-EXPJ4N52.js b/gns3server/static/web-ui/chunk-EXPJ4N52.js deleted file mode 100644 index 5d95f3b08..000000000 --- a/gns3server/static/web-ui/chunk-EXPJ4N52.js +++ /dev/null @@ -1,2825 +0,0 @@ -import{$ as P,$a as v,$e as _t,Ab as ci,Ad as Ei,Ae as Wi,B as Ke,Bb as we,Bd as Fi,Be as Ie,Cb as ht,Cd as Oi,Ce as Qi,D as J,Da as ye,Db as Ce,Dd as se,De as K,E as dt,Ea as c,Eb as _,Ed as Me,Fa as rt,Fb as bt,Fd as Se,Fe as Gi,Ga as Dt,Gb as E,Gd as Pt,Hb as pt,He as Q,Ia as st,Ib as ft,Jb as di,K as Ze,Kb as mi,L as Xe,La as ee,Lb as hi,Ld as Ri,Ma as C,Mb as pi,Na as N,Nb as fi,Nd as Ai,Oa as x,Oc as wt,Qb as $,R as Je,Ra as q,Rb as ui,Rc as wi,Sa as tt,Sb as _i,Sd as Li,Se as De,T as ti,Ta as ai,Te as Yt,U as ei,Ua as oi,Uc as Gt,Ue as vt,V as nt,W as $t,Wa as ri,We as Ct,X as I,Xa as T,Xc as ot,Yc as ut,Ye as Kt,Za as g,Ze as qi,_b as Te,_e as Ui,a as X,aa as z,ae as Pi,ba as w,bb as xe,bc as At,bf as me,ca as at,cb as Et,cc as ie,cd as j,cf as Yi,da as r,db as Ft,eb as D,ed as Ci,ef as Ki,fb as l,fd as Ti,g as it,ga as R,gb as d,gc as Qt,gd as ne,h as Jt,ha as A,hb as M,i as k,ia as mt,ib as kt,ic as bi,j as St,jb as Ot,jd as ae,ka as G,kb as ke,kc as W,ke as le,la as It,ld as U,le as Bi,mc as S,me as zi,n as Ue,na as F,nc as gt,nd as qt,oa as H,ob as lt,oc as gi,od as oe,oe as Ni,p as te,pb as Rt,pd as Mi,pe as ji,qa as Z,qb as u,qc as vi,qe as Ut,ra as ii,rb as si,rd as Si,re as ce,sa as Wt,sb as p,sd as Lt,ta as Y,tb as B,tc as yi,td as Ii,u as Ht,ua as O,ub as y,uc as xi,ud as ct,ue as de,va as ni,vb as et,ve as Vi,wb as V,wd as re,we as Hi,xb as m,xd as Di,xe as $i,y as Ye,yb as h,yc as ki,zb as li}from"./chunk-TYGV4UPE.js";var Ee=class{_box;_destroyed=new k;_resizeSubject=new k;_resizeObserver;_elementObservables=new Map;constructor(s){this._box=s,typeof ResizeObserver<"u"&&(this._resizeObserver=new ResizeObserver(t=>this._resizeSubject.next(t)))}observe(s){return this._elementObservables.has(s)||this._elementObservables.set(s,new Jt(t=>{let e=this._resizeSubject.subscribe(t);return this._resizeObserver?.observe(s,{box:this._box}),()=>{this._resizeObserver?.unobserve(s),e.unsubscribe(),this._elementObservables.delete(s)}}).pipe(dt(t=>t.some(e=>e.target===s)),ti({bufferSize:1,refCount:!0}),I(this._destroyed))),this._elementObservables.get(s)}destroy(){this._destroyed.next(),this._destroyed.complete(),this._resizeSubject.complete(),this._elementObservables.clear()}},he=(()=>{class n{_cleanupErrorListener;_observers=new Map;_ngZone=r(H);constructor(){typeof ResizeObserver<"u"}ngOnDestroy(){for(let[,t]of this._observers)t.destroy();this._observers.clear(),this._cleanupErrorListener?.()}observe(t,e){let i=e?.box||"content-box";return this._observers.has(i)||this._observers.set(i,new Ee(i)),this._observers.get(i).observe(t)}static \u0275fac=function(e){return new(e||n)};static \u0275prov=P({token:n,factory:n.\u0275fac,providedIn:"root"})}return n})();var Bt=["*"];function ra(n,s){n&1&&y(0)}var tn=["tabListContainer"],en=["tabList"],nn=["tabListInner"],an=["nextPaginator"],on=["previousPaginator"],sa=["content"];function la(n,s){}var ca=["tabBodyWrapper"],da=["tabHeader"];function ma(n,s){}function ha(n,s){if(n&1&&tt(0,ma,0,0,"ng-template",12),n&2){let t=p().$implicit;D("cdkPortalOutlet",t.templateLabel)}}function pa(n,s){if(n&1&&E(0),n&2){let t=p().$implicit;pt(t.textLabel)}}function fa(n,s){if(n&1){let t=lt();l(0,"div",7,2),u("click",function(){let i=R(t),a=i.$implicit,o=i.$index,f=p(),b=ht(1);return A(f._handleClick(a,b,o))})("cdkFocusChange",function(i){let a=R(t).$index,o=p();return A(o._tabFocusChanged(i,a))}),M(2,"span",8)(3,"div",9),l(4,"span",10)(5,"span",11),g(6,ha,1,1,null,12)(7,pa,1,1),d()()()}if(n&2){let t=s.$implicit,e=s.$index,i=ht(1),a=p();bt(t.labelClass),_("mdc-tab--active",a.selectedIndex===e),D("id",a._getTabLabelId(t,e))("disabled",t.disabled)("fitInkBarToContent",a.fitInkBarToContent),T("tabIndex",a._getTabIndex(e))("aria-posinset",e+1)("aria-setsize",a._tabs.length)("aria-controls",a._getTabContentId(e))("aria-selected",a.selectedIndex===e)("aria-label",t.ariaLabel||null)("aria-labelledby",!t.ariaLabel&&t.ariaLabelledby?t.ariaLabelledby:null),c(3),D("matRippleTrigger",i)("matRippleDisabled",t.disabled||a.disableRipple),c(3),v(t.templateLabel?6:7)}}function ua(n,s){n&1&&y(0)}function _a(n,s){if(n&1){let t=lt();l(0,"mat-tab-body",13),u("_onCentered",function(){R(t);let i=p();return A(i._removeTabBodyWrapperHeight())})("_onCentering",function(i){R(t);let a=p();return A(a._setTabBodyWrapperHeight(i))})("_beforeCentering",function(i){R(t);let a=p();return A(a._bodyCentered(i))}),d()}if(n&2){let t=s.$implicit,e=s.$index,i=p();bt(t.bodyClass),D("id",i._getTabContentId(e))("content",t.content)("position",t.position)("animationDuration",i.animationDuration)("preserveContent",i.preserveContent),T("tabindex",i.contentTabIndex!=null&&i.selectedIndex===e?i.contentTabIndex:null)("aria-labelledby",i._getTabLabelId(t,e))("aria-hidden",i.selectedIndex!==e)}}var ba=["mat-tab-nav-bar",""],ga=["mat-tab-link",""],va=new w("MatTabContent"),ya=(()=>{class n{template=r(Dt);constructor(){}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matTabContent",""]],features:[$([{provide:va,useExisting:n}])]})}return n})(),xa=new w("MatTabLabel"),rn=new w("MAT_TAB"),ka=(()=>{class n extends Si{_closestTab=r(rn,{optional:!0});static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275dir=x({type:n,selectors:[["","mat-tab-label",""],["","matTabLabel",""]],features:[$([{provide:xa,useExisting:n}]),q]})}return n})(),sn=new w("MAT_TAB_GROUP"),Ae=(()=>{class n{_viewContainerRef=r(ee);_closestTabGroup=r(sn,{optional:!0});disabled=!1;get templateLabel(){return this._templateLabel}set templateLabel(t){this._setTemplateLabelInput(t)}_templateLabel;_explicitContent=void 0;_implicitContent;textLabel="";ariaLabel;ariaLabelledby;labelClass;bodyClass;id=null;_contentPortal=null;get content(){return this._contentPortal}_stateChanges=new k;position=null;origin=null;isActive=!1;constructor(){r(wt).load(Ct)}ngOnChanges(t){(t.hasOwnProperty("textLabel")||t.hasOwnProperty("disabled"))&&this._stateChanges.next()}ngOnDestroy(){this._stateChanges.complete()}ngOnInit(){this._contentPortal=new oe(this._explicitContent||this._implicitContent,this._viewContainerRef)}_setTemplateLabelInput(t){t&&t._closestTab===this&&(this._templateLabel=t)}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-tab"]],contentQueries:function(e,i,a){if(e&1&&et(a,ka,5)(a,ya,7,Dt),e&2){let o;m(o=h())&&(i.templateLabel=o.first),m(o=h())&&(i._explicitContent=o.first)}},viewQuery:function(e,i){if(e&1&&V(Dt,7),e&2){let a;m(a=h())&&(i._implicitContent=a.first)}},hostAttrs:["hidden",""],hostVars:1,hostBindings:function(e,i){e&2&&T("id",null)},inputs:{disabled:[2,"disabled","disabled",S],textLabel:[0,"label","textLabel"],ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],labelClass:"labelClass",bodyClass:"bodyClass",id:"id"},exportAs:["matTab"],features:[$([{provide:rn,useExisting:n}]),Wt],ngContentSelectors:Bt,decls:1,vars:0,template:function(e,i){e&1&&(B(),ai(0,ra,1,0,"ng-template"))},encapsulation:2})}return n})(),Fe="mdc-tab-indicator--active",Zi="mdc-tab-indicator--no-transition",pe=class{_items;_currentItem;constructor(s){this._items=s}hide(){this._items.forEach(s=>s.deactivateInkBar()),this._currentItem=void 0}alignToElement(s){let t=this._items.find(i=>i.elementRef.nativeElement===s),e=this._currentItem;if(t!==e&&(e?.deactivateInkBar(),t)){let i=e?.elementRef.nativeElement.getBoundingClientRect?.();t.activateInkBar(i),this._currentItem=t}}},ln=(()=>{class n{_elementRef=r(O);_inkBarElement=null;_inkBarContentElement=null;_fitToContent=!1;get fitInkBarToContent(){return this._fitToContent}set fitInkBarToContent(t){this._fitToContent!==t&&(this._fitToContent=t,this._inkBarElement&&this._appendInkBarElement())}activateInkBar(t){let e=this._elementRef.nativeElement;if(!t||!e.getBoundingClientRect||!this._inkBarContentElement){e.classList.add(Fe);return}let i=e.getBoundingClientRect(),a=t.width/i.width,o=t.left-i.left;e.classList.add(Zi),this._inkBarContentElement.style.setProperty("transform",`translateX(${o}px) scaleX(${a})`),e.getBoundingClientRect(),e.classList.remove(Zi),e.classList.add(Fe),this._inkBarContentElement.style.setProperty("transform","")}deactivateInkBar(){this._elementRef.nativeElement.classList.remove(Fe)}ngOnInit(){this._createInkBarElement()}ngOnDestroy(){this._inkBarElement?.remove(),this._inkBarElement=this._inkBarContentElement=null}_createInkBarElement(){let t=this._elementRef.nativeElement.ownerDocument||document,e=this._inkBarElement=t.createElement("span"),i=this._inkBarContentElement=t.createElement("span");e.className="mdc-tab-indicator",i.className="mdc-tab-indicator__content mdc-tab-indicator__content--underline",e.appendChild(this._inkBarContentElement),this._appendInkBarElement()}_appendInkBarElement(){this._inkBarElement;let t=this._fitToContent?this._elementRef.nativeElement.querySelector(".mdc-tab__content"):this._elementRef.nativeElement;t.appendChild(this._inkBarElement)}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,inputs:{fitInkBarToContent:[2,"fitInkBarToContent","fitInkBarToContent",S]}})}return n})();var cn=(()=>{class n extends ln{elementRef=r(O);disabled=!1;focus(){this.elementRef.nativeElement.focus()}getOffsetLeft(){return this.elementRef.nativeElement.offsetLeft}getOffsetWidth(){return this.elementRef.nativeElement.offsetWidth}static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275dir=x({type:n,selectors:[["","matTabLabelWrapper",""]],hostVars:3,hostBindings:function(e,i){e&2&&(T("aria-disabled",!!i.disabled),_("mat-mdc-tab-disabled",i.disabled))},inputs:{disabled:[2,"disabled","disabled",S]},features:[q]})}return n})(),Xi={passive:!0},wa=650,Ca=100,dn=(()=>{class n{_elementRef=r(O);_changeDetectorRef=r(W);_viewportRuler=r(ne);_dir=r(ut,{optional:!0});_ngZone=r(H);_platform=r(ot);_sharedResizeObserver=r(he);_injector=r(G);_renderer=r(st);_animationsDisabled=Q();_eventCleanups;_scrollDistance=0;_selectedIndexChanged=!1;_destroyed=new k;_showPaginationControls=!1;_disableScrollAfter=!0;_disableScrollBefore=!0;_tabLabelCount;_scrollDistanceChanged=!1;_keyManager;_currentTextContent;_stopScrolling=new k;disablePagination=!1;get selectedIndex(){return this._selectedIndex}set selectedIndex(t){let e=isNaN(t)?0:t;this._selectedIndex!=e&&(this._selectedIndexChanged=!0,this._selectedIndex=e,this._keyManager&&this._keyManager.updateActiveItem(e))}_selectedIndex=0;selectFocusedIndex=new F;indexFocused=new F;constructor(){this._eventCleanups=this._ngZone.runOutsideAngular(()=>[this._renderer.listen(this._elementRef.nativeElement,"mouseleave",()=>this._stopInterval())])}ngAfterViewInit(){this._eventCleanups.push(this._renderer.listen(this._previousPaginator.nativeElement,"touchstart",()=>this._handlePaginatorPress("before"),Xi),this._renderer.listen(this._nextPaginator.nativeElement,"touchstart",()=>this._handlePaginatorPress("after"),Xi))}ngAfterContentInit(){let t=this._dir?this._dir.change:te("ltr"),e=this._sharedResizeObserver.observe(this._elementRef.nativeElement).pipe(Ze(32),I(this._destroyed)),i=this._viewportRuler.change(150).pipe(I(this._destroyed)),a=()=>{this.updatePagination(),this._alignInkBarToSelectedTab()};this._keyManager=new $i(this._items).withHorizontalOrientation(this._getLayoutDirection()).withHomeAndEnd().withWrap().skipPredicate(()=>!1),this._keyManager.updateActiveItem(Math.max(this._selectedIndex,0)),rt(a,{injector:this._injector}),J(t,i,e,this._items.changes,this._itemsResized()).pipe(I(this._destroyed)).subscribe(()=>{this._ngZone.run(()=>{Promise.resolve().then(()=>{this._scrollDistance=Math.max(0,Math.min(this._getMaxScrollDistance(),this._scrollDistance)),a()})}),this._keyManager?.withHorizontalOrientation(this._getLayoutDirection())}),this._keyManager.change.subscribe(o=>{this.indexFocused.emit(o),this._setTabFocus(o)})}_itemsResized(){return typeof ResizeObserver!="function"?Ue:this._items.changes.pipe(nt(this._items),$t(t=>new Jt(e=>this._ngZone.runOutsideAngular(()=>{let i=new ResizeObserver(a=>e.next(a));return t.forEach(a=>i.observe(a.elementRef.nativeElement)),()=>{i.disconnect()}}))),ei(1),dt(t=>t.some(e=>e.contentRect.width>0&&e.contentRect.height>0)))}ngAfterContentChecked(){this._tabLabelCount!=this._items.length&&(this.updatePagination(),this._tabLabelCount=this._items.length,this._changeDetectorRef.markForCheck()),this._selectedIndexChanged&&(this._scrollToLabel(this._selectedIndex),this._checkScrollingControls(),this._alignInkBarToSelectedTab(),this._selectedIndexChanged=!1,this._changeDetectorRef.markForCheck()),this._scrollDistanceChanged&&(this._updateTabScrollPosition(),this._scrollDistanceChanged=!1,this._changeDetectorRef.markForCheck())}ngOnDestroy(){this._eventCleanups.forEach(t=>t()),this._keyManager?.destroy(),this._destroyed.next(),this._destroyed.complete(),this._stopScrolling.complete()}_handleKeydown(t){if(!ct(t))switch(t.keyCode){case 13:case 32:if(this.focusIndex!==this.selectedIndex){let e=this._items.get(this.focusIndex);e&&!e.disabled&&(this.selectFocusedIndex.emit(this.focusIndex),this._itemSelected(t))}break;default:this._keyManager?.onKeydown(t)}}_onContentChanges(){let t=this._elementRef.nativeElement.textContent;t!==this._currentTextContent&&(this._currentTextContent=t||"",this._ngZone.run(()=>{this.updatePagination(),this._alignInkBarToSelectedTab(),this._changeDetectorRef.markForCheck()}))}updatePagination(){this._checkPaginationEnabled(),this._checkScrollingControls(),this._updateTabScrollPosition()}get focusIndex(){return this._keyManager?this._keyManager.activeItemIndex:0}set focusIndex(t){!this._isValidIndex(t)||this.focusIndex===t||!this._keyManager||this._keyManager.setActiveItem(t)}_isValidIndex(t){return this._items?!!this._items.toArray()[t]:!0}_setTabFocus(t){if(this._showPaginationControls&&this._scrollToLabel(t),this._items&&this._items.length){this._items.toArray()[t].focus();let e=this._tabListContainer.nativeElement;this._getLayoutDirection()=="ltr"?e.scrollLeft=0:e.scrollLeft=e.scrollWidth-e.offsetWidth}}_getLayoutDirection(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}_updateTabScrollPosition(){if(this.disablePagination)return;let t=this.scrollDistance,e=this._getLayoutDirection()==="ltr"?-t:t;this._tabList.nativeElement.style.transform=`translateX(${Math.round(e)}px)`,(this._platform.TRIDENT||this._platform.EDGE)&&(this._tabListContainer.nativeElement.scrollLeft=0)}get scrollDistance(){return this._scrollDistance}set scrollDistance(t){this._scrollTo(t)}_scrollHeader(t){let e=this._tabListContainer.nativeElement.offsetWidth,i=(t=="before"?-1:1)*e/3;return this._scrollTo(this._scrollDistance+i)}_handlePaginatorClick(t){this._stopInterval(),this._scrollHeader(t)}_scrollToLabel(t){if(this.disablePagination)return;let e=this._items?this._items.toArray()[t]:null;if(!e)return;let i=this._tabListContainer.nativeElement.offsetWidth,{offsetLeft:a,offsetWidth:o}=e.elementRef.nativeElement,f,b;this._getLayoutDirection()=="ltr"?(f=a,b=f+o):(b=this._tabListInner.nativeElement.offsetWidth-a,f=b-o);let xt=this.scrollDistance,Mt=this.scrollDistance+i;fMt&&(this.scrollDistance+=Math.min(b-Mt,f-xt))}_checkPaginationEnabled(){if(this.disablePagination)this._showPaginationControls=!1;else{let t=this._tabListInner.nativeElement.scrollWidth,e=this._elementRef.nativeElement.offsetWidth,i=t-e>=5;i||(this.scrollDistance=0),i!==this._showPaginationControls&&(this._showPaginationControls=i,this._changeDetectorRef.markForCheck())}}_checkScrollingControls(){this.disablePagination?this._disableScrollAfter=this._disableScrollBefore=!0:(this._disableScrollBefore=this.scrollDistance==0,this._disableScrollAfter=this.scrollDistance==this._getMaxScrollDistance(),this._changeDetectorRef.markForCheck())}_getMaxScrollDistance(){let t=this._tabListInner.nativeElement.scrollWidth,e=this._tabListContainer.nativeElement.offsetWidth;return t-e||0}_alignInkBarToSelectedTab(){let t=this._items&&this._items.length?this._items.toArray()[this.selectedIndex]:null,e=t?t.elementRef.nativeElement:null;e?this._inkBar.alignToElement(e):this._inkBar.hide()}_stopInterval(){this._stopScrolling.next()}_handlePaginatorPress(t,e){e&&e.button!=null&&e.button!==0||(this._stopInterval(),Ke(wa,Ca).pipe(I(J(this._stopScrolling,this._destroyed))).subscribe(()=>{let{maxScrollDistance:i,distance:a}=this._scrollHeader(t);(a===0||a>=i)&&this._stopInterval()}))}_scrollTo(t){if(this.disablePagination)return{maxScrollDistance:0,distance:0};let e=this._getMaxScrollDistance();return this._scrollDistance=Math.max(0,Math.min(e,t)),this._scrollDistanceChanged=!0,this._checkScrollingControls(),{maxScrollDistance:e,distance:this._scrollDistance}}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,inputs:{disablePagination:[2,"disablePagination","disablePagination",S],selectedIndex:[2,"selectedIndex","selectedIndex",gt]},outputs:{selectFocusedIndex:"selectFocusedIndex",indexFocused:"indexFocused"}})}return n})(),Ta=(()=>{class n extends dn{_items;_tabListContainer;_tabList;_tabListInner;_nextPaginator;_previousPaginator;_inkBar;ariaLabel;ariaLabelledby;disableRipple=!1;ngAfterContentInit(){this._inkBar=new pe(this._items),super.ngAfterContentInit()}_itemSelected(t){t.preventDefault()}static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275cmp=C({type:n,selectors:[["mat-tab-header"]],contentQueries:function(e,i,a){if(e&1&&et(a,cn,4),e&2){let o;m(o=h())&&(i._items=o)}},viewQuery:function(e,i){if(e&1&&V(tn,7)(en,7)(nn,7)(an,5)(on,5),e&2){let a;m(a=h())&&(i._tabListContainer=a.first),m(a=h())&&(i._tabList=a.first),m(a=h())&&(i._tabListInner=a.first),m(a=h())&&(i._nextPaginator=a.first),m(a=h())&&(i._previousPaginator=a.first)}},hostAttrs:[1,"mat-mdc-tab-header"],hostVars:4,hostBindings:function(e,i){e&2&&_("mat-mdc-tab-header-pagination-controls-enabled",i._showPaginationControls)("mat-mdc-tab-header-rtl",i._getLayoutDirection()=="rtl")},inputs:{ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],disableRipple:[2,"disableRipple","disableRipple",S]},features:[q],ngContentSelectors:Bt,decls:13,vars:10,consts:[["previousPaginator",""],["tabListContainer",""],["tabList",""],["tabListInner",""],["nextPaginator",""],["mat-ripple","",1,"mat-mdc-tab-header-pagination","mat-mdc-tab-header-pagination-before",3,"click","mousedown","touchend","matRippleDisabled"],[1,"mat-mdc-tab-header-pagination-chevron"],[1,"mat-mdc-tab-label-container",3,"keydown"],["role","tablist",1,"mat-mdc-tab-list",3,"cdkObserveContent"],[1,"mat-mdc-tab-labels"],["mat-ripple","",1,"mat-mdc-tab-header-pagination","mat-mdc-tab-header-pagination-after",3,"mousedown","click","touchend","matRippleDisabled"]],template:function(e,i){e&1&&(B(),l(0,"div",5,0),u("click",function(){return i._handlePaginatorClick("before")})("mousedown",function(o){return i._handlePaginatorPress("before",o)})("touchend",function(){return i._stopInterval()}),M(2,"div",6),d(),l(3,"div",7,1),u("keydown",function(o){return i._handleKeydown(o)}),l(5,"div",8,2),u("cdkObserveContent",function(){return i._onContentChanges()}),l(7,"div",9,3),y(9),d()()(),l(10,"div",10,4),u("mousedown",function(o){return i._handlePaginatorPress("after",o)})("click",function(){return i._handlePaginatorClick("after")})("touchend",function(){return i._stopInterval()}),M(12,"div",6),d()),e&2&&(_("mat-mdc-tab-header-pagination-disabled",i._disableScrollBefore),D("matRippleDisabled",i._disableScrollBefore||i.disableRipple),c(3),_("_mat-animation-noopable",i._animationsDisabled),c(2),T("aria-label",i.ariaLabel||null)("aria-labelledby",i.ariaLabelledby||null),c(5),_("mat-mdc-tab-header-pagination-disabled",i._disableScrollAfter),D("matRippleDisabled",i._disableScrollAfter||i.disableRipple))},dependencies:[vt,Ut],styles:[`.mat-mdc-tab-header { - display: flex; - overflow: hidden; - position: relative; - flex-shrink: 0; -} - -.mdc-tab-indicator .mdc-tab-indicator__content { - transition-duration: var(--mat-tab-animation-duration, 250ms); -} - -.mat-mdc-tab-header-pagination { - -webkit-user-select: none; - user-select: none; - position: relative; - display: none; - justify-content: center; - align-items: center; - min-width: 32px; - cursor: pointer; - z-index: 2; - -webkit-tap-highlight-color: transparent; - touch-action: none; - box-sizing: content-box; - outline: 0; -} -.mat-mdc-tab-header-pagination::-moz-focus-inner { - border: 0; -} -.mat-mdc-tab-header-pagination .mat-ripple-element { - opacity: 0.12; - background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-header-pagination-controls-enabled .mat-mdc-tab-header-pagination { - display: flex; -} - -.mat-mdc-tab-header-pagination-before, -.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-after { - padding-left: 4px; -} -.mat-mdc-tab-header-pagination-before .mat-mdc-tab-header-pagination-chevron, -.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-after .mat-mdc-tab-header-pagination-chevron { - transform: rotate(-135deg); -} - -.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-before, -.mat-mdc-tab-header-pagination-after { - padding-right: 4px; -} -.mat-mdc-tab-header-rtl .mat-mdc-tab-header-pagination-before .mat-mdc-tab-header-pagination-chevron, -.mat-mdc-tab-header-pagination-after .mat-mdc-tab-header-pagination-chevron { - transform: rotate(45deg); -} - -.mat-mdc-tab-header-pagination-chevron { - border-style: solid; - border-width: 2px 2px 0 0; - height: 8px; - width: 8px; - border-color: var(--mat-tab-pagination-icon-color, var(--mat-sys-on-surface)); -} - -.mat-mdc-tab-header-pagination-disabled { - box-shadow: none; - cursor: default; - pointer-events: none; -} -.mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron { - opacity: 0.4; -} - -.mat-mdc-tab-list { - flex-grow: 1; - position: relative; - transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1); -} -._mat-animation-noopable .mat-mdc-tab-list { - transition: none; -} - -.mat-mdc-tab-label-container { - display: flex; - flex-grow: 1; - overflow: hidden; - z-index: 1; - border-bottom-style: solid; - border-bottom-width: var(--mat-tab-divider-height, 1px); - border-bottom-color: var(--mat-tab-divider-color, var(--mat-sys-surface-variant)); -} -.mat-mdc-tab-group-inverted-header .mat-mdc-tab-label-container { - border-bottom: none; - border-top-style: solid; - border-top-width: var(--mat-tab-divider-height, 1px); - border-top-color: var(--mat-tab-divider-color, var(--mat-sys-surface-variant)); -} - -.mat-mdc-tab-labels { - display: flex; - flex: 1 0 auto; -} -[mat-align-tabs=center] > .mat-mdc-tab-header .mat-mdc-tab-labels { - justify-content: center; -} -[mat-align-tabs=end] > .mat-mdc-tab-header .mat-mdc-tab-labels { - justify-content: flex-end; -} -.cdk-drop-list .mat-mdc-tab-labels, .mat-mdc-tab-labels.cdk-drop-list { - min-height: var(--mat-tab-container-height, 48px); -} - -.mat-mdc-tab::before { - margin: 5px; -} -@media (forced-colors: active) { - .mat-mdc-tab[aria-disabled=true] { - color: GrayText; - } -} -`],encapsulation:2})}return n})(),mn=new w("MAT_TABS_CONFIG"),Ji=(()=>{class n extends Lt{_host=r(Oe);_ngZone=r(H);_centeringSub=it.EMPTY;_leavingSub=it.EMPTY;constructor(){super()}ngOnInit(){super.ngOnInit(),this._centeringSub=this._host._beforeCentering.pipe(nt(this._host._isCenterPosition())).subscribe(t=>{this._host._content&&t&&!this.hasAttached()&&this._ngZone.run(()=>{Promise.resolve().then(),this.attach(this._host._content)})}),this._leavingSub=this._host._afterLeavingCenter.subscribe(()=>{this._host.preserveContent||this._ngZone.run(()=>this.detach())})}ngOnDestroy(){super.ngOnDestroy(),this._centeringSub.unsubscribe(),this._leavingSub.unsubscribe()}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matTabBodyHost",""]],features:[q]})}return n})(),Oe=(()=>{class n{_elementRef=r(O);_dir=r(ut,{optional:!0});_ngZone=r(H);_injector=r(G);_renderer=r(st);_diAnimationsDisabled=Q();_eventCleanups;_initialized=!1;_fallbackTimer;_positionIndex;_dirChangeSubscription=it.EMPTY;_position;_previousPosition;_onCentering=new F;_beforeCentering=new F;_afterLeavingCenter=new F;_onCentered=new F(!0);_portalHost;_contentElement;_content;animationDuration="500ms";preserveContent=!1;set position(t){this._positionIndex=t,this._computePositionAnimationState()}constructor(){if(this._dir){let t=r(W);this._dirChangeSubscription=this._dir.change.subscribe(e=>{this._computePositionAnimationState(e),t.markForCheck()})}}ngOnInit(){this._bindTransitionEvents(),this._position==="center"&&(this._setActiveClass(!0),rt(()=>this._onCentering.emit(this._elementRef.nativeElement.clientHeight),{injector:this._injector})),this._initialized=!0}ngOnDestroy(){clearTimeout(this._fallbackTimer),this._eventCleanups?.forEach(t=>t()),this._dirChangeSubscription.unsubscribe()}_bindTransitionEvents(){this._ngZone.runOutsideAngular(()=>{let t=this._elementRef.nativeElement,e=i=>{i.target===this._contentElement?.nativeElement&&(this._elementRef.nativeElement.classList.remove("mat-tab-body-animating"),i.type==="transitionend"&&this._transitionDone())};this._eventCleanups=[this._renderer.listen(t,"transitionstart",i=>{i.target===this._contentElement?.nativeElement&&(this._elementRef.nativeElement.classList.add("mat-tab-body-animating"),this._transitionStarted())}),this._renderer.listen(t,"transitionend",e),this._renderer.listen(t,"transitioncancel",e)]})}_transitionStarted(){clearTimeout(this._fallbackTimer);let t=this._position==="center";this._beforeCentering.emit(t),t&&this._onCentering.emit(this._elementRef.nativeElement.clientHeight)}_transitionDone(){this._position==="center"?this._onCentered.emit():this._previousPosition==="center"&&this._afterLeavingCenter.emit()}_setActiveClass(t){this._elementRef.nativeElement.classList.toggle("mat-mdc-tab-body-active",t)}_getLayoutDirection(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}_isCenterPosition(){return this._positionIndex===0}_computePositionAnimationState(t=this._getLayoutDirection()){this._previousPosition=this._position,this._positionIndex<0?this._position=t=="ltr"?"left":"right":this._positionIndex>0?this._position=t=="ltr"?"right":"left":this._position="center",this._animationsDisabled()?this._simulateTransitionEvents():this._initialized&&(this._position==="center"||this._previousPosition==="center")&&(clearTimeout(this._fallbackTimer),this._fallbackTimer=this._ngZone.runOutsideAngular(()=>setTimeout(()=>this._simulateTransitionEvents(),100)))}_simulateTransitionEvents(){this._transitionStarted(),rt(()=>this._transitionDone(),{injector:this._injector})}_animationsDisabled(){return this._diAnimationsDisabled||this.animationDuration==="0ms"||this.animationDuration==="0s"}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-tab-body"]],viewQuery:function(e,i){if(e&1&&V(Ji,5)(sa,5),e&2){let a;m(a=h())&&(i._portalHost=a.first),m(a=h())&&(i._contentElement=a.first)}},hostAttrs:[1,"mat-mdc-tab-body"],hostVars:1,hostBindings:function(e,i){e&2&&T("inert",i._position==="center"?null:"")},inputs:{_content:[0,"content","_content"],animationDuration:"animationDuration",preserveContent:"preserveContent",position:"position"},outputs:{_onCentering:"_onCentering",_beforeCentering:"_beforeCentering",_onCentered:"_onCentered"},decls:3,vars:6,consts:[["content",""],["cdkScrollable","",1,"mat-mdc-tab-body-content"],["matTabBodyHost",""]],template:function(e,i){e&1&&(l(0,"div",1,0),tt(2,la,0,0,"ng-template",2),d()),e&2&&_("mat-tab-body-content-left",i._position==="left")("mat-tab-body-content-right",i._position==="right")("mat-tab-body-content-can-animate",i._position==="center"||i._previousPosition==="center")},dependencies:[Ji,Ti],styles:[`.mat-mdc-tab-body { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - display: block; - overflow: hidden; - outline: 0; - flex-basis: 100%; -} -.mat-mdc-tab-body.mat-mdc-tab-body-active { - position: relative; - overflow-x: hidden; - overflow-y: auto; - z-index: 1; - flex-grow: 1; -} -.mat-mdc-tab-group.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body.mat-mdc-tab-body-active { - overflow-y: hidden; -} - -.mat-mdc-tab-body-content { - height: 100%; - overflow: auto; - transform: none; - visibility: hidden; -} -.mat-tab-body-animating > .mat-mdc-tab-body-content, .mat-mdc-tab-body-active > .mat-mdc-tab-body-content { - visibility: visible; -} -.mat-tab-body-animating > .mat-mdc-tab-body-content { - min-height: 1px; -} -.mat-mdc-tab-group-dynamic-height .mat-mdc-tab-body-content { - overflow: hidden; -} - -.mat-tab-body-content-can-animate { - transition: transform var(--mat-tab-animation-duration) 1ms cubic-bezier(0.35, 0, 0.25, 1); -} -.mat-mdc-tab-body-wrapper._mat-animation-noopable .mat-tab-body-content-can-animate { - transition: none; -} - -.mat-tab-body-content-left { - transform: translate3d(-100%, 0, 0); -} - -.mat-tab-body-content-right { - transform: translate3d(100%, 0, 0); -} -`],encapsulation:2})}return n})(),hn=(()=>{class n{_elementRef=r(O);_changeDetectorRef=r(W);_ngZone=r(H);_tabsSubscription=it.EMPTY;_tabLabelSubscription=it.EMPTY;_tabBodySubscription=it.EMPTY;_diAnimationsDisabled=Q();_allTabs;_tabBodies;_tabBodyWrapper;_tabHeader;_tabs=new ni;_indexToSelect=0;_lastFocusedTabIndex=null;_tabBodyWrapperHeight=0;color;get fitInkBarToContent(){return this._fitInkBarToContent}set fitInkBarToContent(t){this._fitInkBarToContent=t,this._changeDetectorRef.markForCheck()}_fitInkBarToContent=!1;stretchTabs=!0;alignTabs=null;dynamicHeight=!1;get selectedIndex(){return this._selectedIndex}set selectedIndex(t){this._indexToSelect=isNaN(t)?null:t}_selectedIndex=null;headerPosition="above";get animationDuration(){return this._animationDuration}set animationDuration(t){let e=t+"";this._animationDuration=/^\d+$/.test(e)?t+"ms":e}_animationDuration;get contentTabIndex(){return this._contentTabIndex}set contentTabIndex(t){this._contentTabIndex=isNaN(t)?null:t}_contentTabIndex=null;disablePagination=!1;disableRipple=!1;preserveContent=!1;get backgroundColor(){return this._backgroundColor}set backgroundColor(t){let e=this._elementRef.nativeElement.classList;e.remove("mat-tabs-with-background",`mat-background-${this.backgroundColor}`),t&&e.add("mat-tabs-with-background",`mat-background-${t}`),this._backgroundColor=t}_backgroundColor;ariaLabel;ariaLabelledby;selectedIndexChange=new F;focusChange=new F;animationDone=new F;selectedTabChange=new F(!0);_groupId;_isServer=!r(ot).isBrowser;constructor(){let t=r(mn,{optional:!0});this._groupId=r(U).getId("mat-tab-group-"),this.animationDuration=t&&t.animationDuration?t.animationDuration:"500ms",this.disablePagination=t&&t.disablePagination!=null?t.disablePagination:!1,this.dynamicHeight=t&&t.dynamicHeight!=null?t.dynamicHeight:!1,t?.contentTabIndex!=null&&(this.contentTabIndex=t.contentTabIndex),this.preserveContent=!!t?.preserveContent,this.fitInkBarToContent=t&&t.fitInkBarToContent!=null?t.fitInkBarToContent:!1,this.stretchTabs=t&&t.stretchTabs!=null?t.stretchTabs:!0,this.alignTabs=t&&t.alignTabs!=null?t.alignTabs:null}ngAfterContentChecked(){let t=this._indexToSelect=this._clampTabIndex(this._indexToSelect);if(this._selectedIndex!=t){let e=this._selectedIndex==null;if(!e){this.selectedTabChange.emit(this._createChangeEvent(t));let i=this._tabBodyWrapper.nativeElement;i.style.minHeight=i.clientHeight+"px"}Promise.resolve().then(()=>{this._tabs.forEach((i,a)=>i.isActive=a===t),e||(this.selectedIndexChange.emit(t),this._tabBodyWrapper.nativeElement.style.minHeight="")})}this._tabs.forEach((e,i)=>{e.position=i-t,this._selectedIndex!=null&&e.position==0&&!e.origin&&(e.origin=t-this._selectedIndex)}),this._selectedIndex!==t&&(this._selectedIndex=t,this._lastFocusedTabIndex=null,this._changeDetectorRef.markForCheck())}ngAfterContentInit(){this._subscribeToAllTabChanges(),this._subscribeToTabLabels(),this._tabsSubscription=this._tabs.changes.subscribe(()=>{let t=this._clampTabIndex(this._indexToSelect);if(t===this._selectedIndex){let e=this._tabs.toArray(),i;for(let a=0;a{e[t].isActive=!0,this.selectedTabChange.emit(this._createChangeEvent(t))})}this._changeDetectorRef.markForCheck()})}ngAfterViewInit(){this._tabBodySubscription=this._tabBodies.changes.subscribe(()=>this._bodyCentered(!0))}_subscribeToAllTabChanges(){this._allTabs.changes.pipe(nt(this._allTabs)).subscribe(t=>{this._tabs.reset(t.filter(e=>e._closestTabGroup===this||!e._closestTabGroup)),this._tabs.notifyOnChanges()})}ngOnDestroy(){this._tabs.destroy(),this._tabsSubscription.unsubscribe(),this._tabLabelSubscription.unsubscribe(),this._tabBodySubscription.unsubscribe()}realignInkBar(){this._tabHeader&&this._tabHeader._alignInkBarToSelectedTab()}updatePagination(){this._tabHeader&&this._tabHeader.updatePagination()}focusTab(t){let e=this._tabHeader;e&&(e.focusIndex=t)}_focusChanged(t){this._lastFocusedTabIndex=t,this.focusChange.emit(this._createChangeEvent(t))}_createChangeEvent(t){let e=new Re;return e.index=t,this._tabs&&this._tabs.length&&(e.tab=this._tabs.toArray()[t]),e}_subscribeToTabLabels(){this._tabLabelSubscription&&this._tabLabelSubscription.unsubscribe(),this._tabLabelSubscription=J(...this._tabs.map(t=>t._stateChanges)).subscribe(()=>this._changeDetectorRef.markForCheck())}_clampTabIndex(t){return Math.min(this._tabs.length-1,Math.max(t||0,0))}_getTabLabelId(t,e){return t.id||`${this._groupId}-label-${e}`}_getTabContentId(t){return`${this._groupId}-content-${t}`}_setTabBodyWrapperHeight(t){if(!this.dynamicHeight||!this._tabBodyWrapperHeight){this._tabBodyWrapperHeight=t;return}let e=this._tabBodyWrapper.nativeElement;e.style.height=this._tabBodyWrapperHeight+"px",this._tabBodyWrapper.nativeElement.offsetHeight&&(e.style.height=t+"px")}_removeTabBodyWrapperHeight(){let t=this._tabBodyWrapper.nativeElement;this._tabBodyWrapperHeight=t.clientHeight,t.style.height="",this._ngZone.run(()=>this.animationDone.emit())}_handleClick(t,e,i){e.focusIndex=i,t.disabled||(this.selectedIndex=i)}_getTabIndex(t){let e=this._lastFocusedTabIndex??this.selectedIndex;return t===e?0:-1}_tabFocusChanged(t,e){t&&t!=="mouse"&&t!=="touch"&&(this._tabHeader.focusIndex=e)}_bodyCentered(t){t&&this._tabBodies?.forEach((e,i)=>e._setActiveClass(i===this._selectedIndex))}_animationsDisabled(){return this._diAnimationsDisabled||this.animationDuration==="0"||this.animationDuration==="0ms"}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-tab-group"]],contentQueries:function(e,i,a){if(e&1&&et(a,Ae,5),e&2){let o;m(o=h())&&(i._allTabs=o)}},viewQuery:function(e,i){if(e&1&&V(ca,5)(da,5)(Oe,5),e&2){let a;m(a=h())&&(i._tabBodyWrapper=a.first),m(a=h())&&(i._tabHeader=a.first),m(a=h())&&(i._tabBodies=a)}},hostAttrs:[1,"mat-mdc-tab-group"],hostVars:11,hostBindings:function(e,i){e&2&&(T("mat-align-tabs",i.alignTabs),bt("mat-"+(i.color||"primary")),Ce("--mat-tab-animation-duration",i.animationDuration),_("mat-mdc-tab-group-dynamic-height",i.dynamicHeight)("mat-mdc-tab-group-inverted-header",i.headerPosition==="below")("mat-mdc-tab-group-stretch-tabs",i.stretchTabs))},inputs:{color:"color",fitInkBarToContent:[2,"fitInkBarToContent","fitInkBarToContent",S],stretchTabs:[2,"mat-stretch-tabs","stretchTabs",S],alignTabs:[0,"mat-align-tabs","alignTabs"],dynamicHeight:[2,"dynamicHeight","dynamicHeight",S],selectedIndex:[2,"selectedIndex","selectedIndex",gt],headerPosition:"headerPosition",animationDuration:"animationDuration",contentTabIndex:[2,"contentTabIndex","contentTabIndex",gt],disablePagination:[2,"disablePagination","disablePagination",S],disableRipple:[2,"disableRipple","disableRipple",S],preserveContent:[2,"preserveContent","preserveContent",S],backgroundColor:"backgroundColor",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"]},outputs:{selectedIndexChange:"selectedIndexChange",focusChange:"focusChange",animationDone:"animationDone",selectedTabChange:"selectedTabChange"},exportAs:["matTabGroup"],features:[$([{provide:sn,useExisting:n}])],ngContentSelectors:Bt,decls:9,vars:8,consts:[["tabHeader",""],["tabBodyWrapper",""],["tabNode",""],[3,"indexFocused","selectFocusedIndex","selectedIndex","disableRipple","disablePagination","aria-label","aria-labelledby"],["role","tab","matTabLabelWrapper","","cdkMonitorElementFocus","",1,"mdc-tab","mat-mdc-tab","mat-focus-indicator",3,"id","mdc-tab--active","class","disabled","fitInkBarToContent"],[1,"mat-mdc-tab-body-wrapper"],["role","tabpanel",3,"id","class","content","position","animationDuration","preserveContent"],["role","tab","matTabLabelWrapper","","cdkMonitorElementFocus","",1,"mdc-tab","mat-mdc-tab","mat-focus-indicator",3,"click","cdkFocusChange","id","disabled","fitInkBarToContent"],[1,"mdc-tab__ripple"],["mat-ripple","",1,"mat-mdc-tab-ripple",3,"matRippleTrigger","matRippleDisabled"],[1,"mdc-tab__content"],[1,"mdc-tab__text-label"],[3,"cdkPortalOutlet"],["role","tabpanel",3,"_onCentered","_onCentering","_beforeCentering","id","content","position","animationDuration","preserveContent"]],template:function(e,i){e&1&&(B(),l(0,"mat-tab-header",3,0),u("indexFocused",function(o){return i._focusChanged(o)})("selectFocusedIndex",function(o){return i.selectedIndex=o}),Et(2,fa,8,17,"div",4,xe),d(),g(4,ua,1,0),l(5,"div",5,1),Et(7,_a,1,10,"mat-tab-body",6,xe),d()),e&2&&(D("selectedIndex",i.selectedIndex||0)("disableRipple",i.disableRipple)("disablePagination",i.disablePagination),ri("aria-label",i.ariaLabel)("aria-labelledby",i.ariaLabelledby),c(2),Ft(i._tabs),c(2),v(i._isServer?4:-1),c(),_("_mat-animation-noopable",i._animationsDisabled()),c(2),Ft(i._tabs))},dependencies:[Ta,cn,Bi,vt,Lt,Oe],styles:[`.mdc-tab { - min-width: 90px; - padding: 0 24px; - display: flex; - flex: 1 0 auto; - justify-content: center; - box-sizing: border-box; - border: none; - outline: none; - text-align: center; - white-space: nowrap; - cursor: pointer; - z-index: 1; - touch-action: manipulation; -} - -.mdc-tab__content { - display: flex; - align-items: center; - justify-content: center; - height: inherit; - pointer-events: none; -} - -.mdc-tab__text-label { - transition: 150ms color linear; - display: inline-block; - line-height: 1; - z-index: 2; -} - -.mdc-tab--active .mdc-tab__text-label { - transition-delay: 100ms; -} - -._mat-animation-noopable .mdc-tab__text-label { - transition: none; -} - -.mdc-tab-indicator { - display: flex; - position: absolute; - top: 0; - left: 0; - justify-content: center; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 1; -} - -.mdc-tab-indicator__content { - transition: var(--mat-tab-animation-duration, 250ms) transform cubic-bezier(0.4, 0, 0.2, 1); - transform-origin: left; - opacity: 0; -} - -.mdc-tab-indicator__content--underline { - align-self: flex-end; - box-sizing: border-box; - width: 100%; - border-top-style: solid; -} - -.mdc-tab-indicator--active .mdc-tab-indicator__content { - opacity: 1; -} - -._mat-animation-noopable .mdc-tab-indicator__content, .mdc-tab-indicator--no-transition .mdc-tab-indicator__content { - transition: none; -} - -.mat-mdc-tab-ripple.mat-mdc-tab-ripple { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - pointer-events: none; -} - -.mat-mdc-tab { - -webkit-tap-highlight-color: transparent; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-decoration: none; - background: none; - height: var(--mat-tab-container-height, 48px); - font-family: var(--mat-tab-label-text-font, var(--mat-sys-title-small-font)); - font-size: var(--mat-tab-label-text-size, var(--mat-sys-title-small-size)); - letter-spacing: var(--mat-tab-label-text-tracking, var(--mat-sys-title-small-tracking)); - line-height: var(--mat-tab-label-text-line-height, var(--mat-sys-title-small-line-height)); - font-weight: var(--mat-tab-label-text-weight, var(--mat-sys-title-small-weight)); -} -.mat-mdc-tab.mdc-tab { - flex-grow: 0; -} -.mat-mdc-tab .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-active-indicator-color, var(--mat-sys-primary)); - border-top-width: var(--mat-tab-active-indicator-height, 2px); - border-radius: var(--mat-tab-active-indicator-shape, 0); -} -.mat-mdc-tab:hover .mdc-tab__text-label { - color: var(--mat-tab-inactive-hover-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab:focus .mdc-tab__text-label { - color: var(--mat-tab-inactive-focus-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab.mdc-tab--active .mdc-tab__text-label { - color: var(--mat-tab-active-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab.mdc-tab--active .mdc-tab__ripple::before, -.mat-mdc-tab.mdc-tab--active .mat-ripple-element { - background-color: var(--mat-tab-active-ripple-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab.mdc-tab--active:hover .mdc-tab__text-label { - color: var(--mat-tab-active-hover-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab.mdc-tab--active:hover .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-active-hover-indicator-color, var(--mat-sys-primary)); -} -.mat-mdc-tab.mdc-tab--active:focus .mdc-tab__text-label { - color: var(--mat-tab-active-focus-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab.mdc-tab--active:focus .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-active-focus-indicator-color, var(--mat-sys-primary)); -} -.mat-mdc-tab.mat-mdc-tab-disabled { - opacity: 0.4; - pointer-events: none; -} -.mat-mdc-tab.mat-mdc-tab-disabled .mdc-tab__content { - pointer-events: none; -} -.mat-mdc-tab.mat-mdc-tab-disabled .mdc-tab__ripple::before, -.mat-mdc-tab.mat-mdc-tab-disabled .mat-ripple-element { - background-color: var(--mat-tab-disabled-ripple-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-tab .mdc-tab__ripple::before { - content: ""; - display: block; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - opacity: 0; - pointer-events: none; - background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab .mdc-tab__text-label { - color: var(--mat-tab-inactive-label-text-color, var(--mat-sys-on-surface)); - display: inline-flex; - align-items: center; -} -.mat-mdc-tab .mdc-tab__content { - position: relative; - pointer-events: auto; -} -.mat-mdc-tab:hover .mdc-tab__ripple::before { - opacity: 0.04; -} -.mat-mdc-tab.cdk-program-focused .mdc-tab__ripple::before, .mat-mdc-tab.cdk-keyboard-focused .mdc-tab__ripple::before { - opacity: 0.12; -} -.mat-mdc-tab .mat-ripple-element { - opacity: 0.12; - background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-group.mat-mdc-tab-group-stretch-tabs > .mat-mdc-tab-header .mat-mdc-tab { - flex-grow: 1; -} - -.mat-mdc-tab-group { - display: flex; - flex-direction: column; - max-width: 100%; -} -.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination { - background-color: var(--mat-tab-background-color); -} -.mat-mdc-tab-group.mat-tabs-with-background.mat-primary > .mat-mdc-tab-header .mat-mdc-tab .mdc-tab__text-label { - color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-group.mat-tabs-with-background.mat-primary > .mat-mdc-tab-header .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-group.mat-tabs-with-background:not(.mat-primary) > .mat-mdc-tab-header .mat-mdc-tab:not(.mdc-tab--active) .mdc-tab__text-label { - color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-group.mat-tabs-with-background:not(.mat-primary) > .mat-mdc-tab-header .mat-mdc-tab:not(.mdc-tab--active) .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mat-mdc-tab-header-pagination-chevron, -.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mat-focus-indicator::before, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron, -.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-focus-indicator::before { - border-color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mat-ripple-element, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mdc-tab__ripple::before, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-ripple-element, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mdc-tab__ripple::before { - background-color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header .mat-mdc-tab-header-pagination-chevron, .mat-mdc-tab-group.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron { - color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-group.mat-mdc-tab-group-inverted-header { - flex-direction: column-reverse; -} -.mat-mdc-tab-group.mat-mdc-tab-group-inverted-header .mdc-tab-indicator__content--underline { - align-self: flex-start; -} - -.mat-mdc-tab-body-wrapper { - position: relative; - overflow: hidden; - display: flex; - transition: height 500ms cubic-bezier(0.35, 0, 0.25, 1); -} -.mat-mdc-tab-body-wrapper._mat-animation-noopable { - transition: none !important; - animation: none !important; -} -`],encapsulation:2})}return n})(),Re=class{index;tab},Ma=(()=>{class n extends dn{_focusedItem=Z(null);get fitInkBarToContent(){return this._fitInkBarToContent.value}set fitInkBarToContent(t){this._fitInkBarToContent.next(t),this._changeDetectorRef.markForCheck()}_fitInkBarToContent=new St(!1);stretchTabs=!0;get animationDuration(){return this._animationDuration}set animationDuration(t){let e=t+"";this._animationDuration=/^\d+$/.test(e)?t+"ms":e}_animationDuration;_items;get backgroundColor(){return this._backgroundColor}set backgroundColor(t){let e=this._elementRef.nativeElement.classList;e.remove("mat-tabs-with-background",`mat-background-${this.backgroundColor}`),t&&e.add("mat-tabs-with-background",`mat-background-${t}`),this._backgroundColor=t}_backgroundColor;get disableRipple(){return this._disableRipple()}set disableRipple(t){this._disableRipple.set(t)}_disableRipple=Z(!1);color="primary";tabPanel;_tabListContainer;_tabList;_tabListInner;_nextPaginator;_previousPaginator;_inkBar;constructor(){let t=r(mn,{optional:!0});super(),this.disablePagination=t&&t.disablePagination!=null?t.disablePagination:!1,this.fitInkBarToContent=t&&t.fitInkBarToContent!=null?t.fitInkBarToContent:!1,this.stretchTabs=t&&t.stretchTabs!=null?t.stretchTabs:!0}_itemSelected(){}ngAfterContentInit(){this._inkBar=new pe(this._items),this._items.changes.pipe(nt(null),I(this._destroyed)).subscribe(()=>this.updateActiveLink()),super.ngAfterContentInit(),this._keyManager.change.pipe(nt(null),I(this._destroyed)).subscribe(()=>this._focusedItem.set(this._keyManager?.activeItem||null))}ngAfterViewInit(){this.tabPanel,super.ngAfterViewInit()}updateActiveLink(){if(!this._items)return;let t=this._items.toArray();for(let e=0;e .mat-mdc-tab-link-container .mat-mdc-tab-links { - justify-content: center; -} -[mat-align-tabs=end] > .mat-mdc-tab-link-container .mat-mdc-tab-links { - justify-content: flex-end; -} -.cdk-drop-list .mat-mdc-tab-links, .mat-mdc-tab-links.cdk-drop-list { - min-height: var(--mat-tab-container-height, 48px); -} - -.mat-mdc-tab-link-container { - display: flex; - flex-grow: 1; - overflow: hidden; - z-index: 1; - border-bottom-style: solid; - border-bottom-width: var(--mat-tab-divider-height, 1px); - border-bottom-color: var(--mat-tab-divider-color, var(--mat-sys-surface-variant)); -} - -.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination { - background-color: var(--mat-tab-background-color); -} -.mat-mdc-tab-nav-bar.mat-tabs-with-background.mat-primary > .mat-mdc-tab-link-container .mat-mdc-tab-link .mdc-tab__text-label { - color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-nav-bar.mat-tabs-with-background.mat-primary > .mat-mdc-tab-link-container .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-nav-bar.mat-tabs-with-background:not(.mat-primary) > .mat-mdc-tab-link-container .mat-mdc-tab-link:not(.mdc-tab--active) .mdc-tab__text-label { - color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-nav-bar.mat-tabs-with-background:not(.mat-primary) > .mat-mdc-tab-link-container .mat-mdc-tab-link:not(.mdc-tab--active) .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mat-mdc-tab-header-pagination-chevron, -.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mat-focus-indicator::before, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron, -.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-focus-indicator::before { - border-color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mat-ripple-element, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mdc-tab__ripple::before, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-ripple-element, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mdc-tab__ripple::before { - background-color: var(--mat-tab-foreground-color); -} -.mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-link-container .mat-mdc-tab-header-pagination-chevron, .mat-mdc-tab-nav-bar.mat-tabs-with-background > .mat-mdc-tab-header-pagination .mat-mdc-tab-header-pagination-chevron { - color: var(--mat-tab-foreground-color); -} -`],encapsulation:2})}return n})(),Sa=(()=>{class n extends ln{_tabNavBar=r(Ma);elementRef=r(O);_focusMonitor=r(le);_destroyed=new k;_isActive=!1;_tabIndex=At(()=>this._tabNavBar._focusedItem()===this?this.tabIndex:-1);get active(){return this._isActive}set active(t){t!==this._isActive&&(this._isActive=t,this._tabNavBar.updateActiveLink())}disabled=!1;get disableRipple(){return this._disableRipple()}set disableRipple(t){this._disableRipple.set(t)}_disableRipple=Z(!1);tabIndex=0;rippleConfig;get rippleDisabled(){return this.disabled||this.disableRipple||this._tabNavBar.disableRipple||!!this.rippleConfig.disabled}id=r(U).getId("mat-tab-link-");constructor(){super(),r(wt).load(Ct);let t=r(Yt,{optional:!0}),e=r(new ie("tabindex"),{optional:!0});this.rippleConfig=t||{},this.tabIndex=e==null?0:parseInt(e)||0,Q()&&(this.rippleConfig.animation={enterDuration:0,exitDuration:0}),this._tabNavBar._fitInkBarToContent.pipe(I(this._destroyed)).subscribe(i=>{this.fitInkBarToContent=i})}focus(){this.elementRef.nativeElement.focus()}ngAfterViewInit(){this._focusMonitor.monitor(this.elementRef)}ngOnDestroy(){this._destroyed.next(),this._destroyed.complete(),super.ngOnDestroy(),this._focusMonitor.stopMonitoring(this.elementRef)}_handleFocus(){this._tabNavBar.focusIndex=this._tabNavBar._items.toArray().indexOf(this)}_handleKeydown(t){(t.keyCode===32||t.keyCode===13)&&(this.disabled?t.preventDefault():this._tabNavBar.tabPanel&&(t.keyCode===32&&t.preventDefault(),this.elementRef.nativeElement.click()))}_getAriaControls(){return this._tabNavBar.tabPanel?this._tabNavBar.tabPanel?.id:this.elementRef.nativeElement.getAttribute("aria-controls")}_getAriaSelected(){return this._tabNavBar.tabPanel?this.active?"true":"false":this.elementRef.nativeElement.getAttribute("aria-selected")}_getAriaCurrent(){return this.active&&!this._tabNavBar.tabPanel?"page":null}_getRole(){return this._tabNavBar.tabPanel?"tab":this.elementRef.nativeElement.getAttribute("role")}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["","mat-tab-link",""],["","matTabLink",""]],hostAttrs:[1,"mdc-tab","mat-mdc-tab-link","mat-focus-indicator"],hostVars:11,hostBindings:function(e,i){e&1&&u("focus",function(){return i._handleFocus()})("keydown",function(o){return i._handleKeydown(o)}),e&2&&(T("aria-controls",i._getAriaControls())("aria-current",i._getAriaCurrent())("aria-disabled",i.disabled)("aria-selected",i._getAriaSelected())("id",i.id)("tabIndex",i._tabIndex())("role",i._getRole()),_("mat-mdc-tab-disabled",i.disabled)("mdc-tab--active",i.active))},inputs:{active:[2,"active","active",S],disabled:[2,"disabled","disabled",S],disableRipple:[2,"disableRipple","disableRipple",S],tabIndex:[2,"tabIndex","tabIndex",t=>t==null?0:gt(t)],id:"id"},exportAs:["matTabLink"],features:[q],attrs:ga,ngContentSelectors:Bt,decls:5,vars:2,consts:[[1,"mdc-tab__ripple"],["mat-ripple","",1,"mat-mdc-tab-ripple",3,"matRippleTrigger","matRippleDisabled"],[1,"mdc-tab__content"],[1,"mdc-tab__text-label"]],template:function(e,i){e&1&&(B(),M(0,"span",0)(1,"div",1),l(2,"span",2)(3,"span",3),y(4),d()()),e&2&&(c(),D("matRippleTrigger",i.elementRef.nativeElement)("matRippleDisabled",i.rippleDisabled))},dependencies:[vt],styles:[`.mat-mdc-tab-link { - -webkit-tap-highlight-color: transparent; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-decoration: none; - background: none; - height: var(--mat-tab-container-height, 48px); - font-family: var(--mat-tab-label-text-font, var(--mat-sys-title-small-font)); - font-size: var(--mat-tab-label-text-size, var(--mat-sys-title-small-size)); - letter-spacing: var(--mat-tab-label-text-tracking, var(--mat-sys-title-small-tracking)); - line-height: var(--mat-tab-label-text-line-height, var(--mat-sys-title-small-line-height)); - font-weight: var(--mat-tab-label-text-weight, var(--mat-sys-title-small-weight)); -} -.mat-mdc-tab-link.mdc-tab { - flex-grow: 0; -} -.mat-mdc-tab-link .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-active-indicator-color, var(--mat-sys-primary)); - border-top-width: var(--mat-tab-active-indicator-height, 2px); - border-radius: var(--mat-tab-active-indicator-shape, 0); -} -.mat-mdc-tab-link:hover .mdc-tab__text-label { - color: var(--mat-tab-inactive-hover-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-link:focus .mdc-tab__text-label { - color: var(--mat-tab-inactive-focus-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-link.mdc-tab--active .mdc-tab__text-label { - color: var(--mat-tab-active-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-link.mdc-tab--active .mdc-tab__ripple::before, -.mat-mdc-tab-link.mdc-tab--active .mat-ripple-element { - background-color: var(--mat-tab-active-ripple-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-link.mdc-tab--active:hover .mdc-tab__text-label { - color: var(--mat-tab-active-hover-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-link.mdc-tab--active:hover .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-active-hover-indicator-color, var(--mat-sys-primary)); -} -.mat-mdc-tab-link.mdc-tab--active:focus .mdc-tab__text-label { - color: var(--mat-tab-active-focus-label-text-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-link.mdc-tab--active:focus .mdc-tab-indicator__content--underline { - border-color: var(--mat-tab-active-focus-indicator-color, var(--mat-sys-primary)); -} -.mat-mdc-tab-link.mat-mdc-tab-disabled { - opacity: 0.4; - pointer-events: none; -} -.mat-mdc-tab-link.mat-mdc-tab-disabled .mdc-tab__content { - pointer-events: none; -} -.mat-mdc-tab-link.mat-mdc-tab-disabled .mdc-tab__ripple::before, -.mat-mdc-tab-link.mat-mdc-tab-disabled .mat-ripple-element { - background-color: var(--mat-tab-disabled-ripple-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-tab-link .mdc-tab__ripple::before { - content: ""; - display: block; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - opacity: 0; - pointer-events: none; - background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-link .mdc-tab__text-label { - color: var(--mat-tab-inactive-label-text-color, var(--mat-sys-on-surface)); - display: inline-flex; - align-items: center; -} -.mat-mdc-tab-link .mdc-tab__content { - position: relative; - pointer-events: auto; -} -.mat-mdc-tab-link:hover .mdc-tab__ripple::before { - opacity: 0.04; -} -.mat-mdc-tab-link.cdk-program-focused .mdc-tab__ripple::before, .mat-mdc-tab-link.cdk-keyboard-focused .mdc-tab__ripple::before { - opacity: 0.12; -} -.mat-mdc-tab-link .mat-ripple-element { - opacity: 0.12; - background-color: var(--mat-tab-inactive-ripple-color, var(--mat-sys-on-surface)); -} -.mat-mdc-tab-header.mat-mdc-tab-nav-bar-stretch-tabs .mat-mdc-tab-link { - flex-grow: 1; -} -.mat-mdc-tab-link::before { - margin: 5px; -} - -@media (max-width: 599px) { - .mat-mdc-tab-link { - min-width: 72px; - } -} -`],encapsulation:2,changeDetection:0})}return n})(),ss=(()=>{class n{id=r(U).getId("mat-tab-nav-panel-");_activeTabId;static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-tab-nav-panel"]],hostAttrs:["role","tabpanel",1,"mat-mdc-tab-nav-panel"],hostVars:2,hostBindings:function(e,i){e&2&&T("aria-labelledby",i._activeTabId)("id",i.id)},inputs:{id:"id"},exportAs:["matTabNavPanel"],ngContentSelectors:Bt,decls:1,vars:0,template:function(e,i){e&1&&(B(),y(0))},encapsulation:2,changeDetection:0})}return n})(),pn=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({imports:[j]})}return n})();var Da=["notch"],Ea=["matFormFieldNotchedOutline",""],Fa=["*"],fn=["iconPrefixContainer"],un=["textPrefixContainer"],_n=["iconSuffixContainer"],bn=["textSuffixContainer"],Oa=["textField"],Ra=["*",[["mat-label"]],[["","matPrefix",""],["","matIconPrefix",""]],[["","matTextPrefix",""]],[["","matTextSuffix",""]],[["","matSuffix",""],["","matIconSuffix",""]],[["mat-error"],["","matError",""]],[["mat-hint",3,"align","end"]],[["mat-hint","align","end"]]],Aa=["*","mat-label","[matPrefix], [matIconPrefix]","[matTextPrefix]","[matTextSuffix]","[matSuffix], [matIconSuffix]","mat-error, [matError]","mat-hint:not([align='end'])","mat-hint[align='end']"];function La(n,s){n&1&&M(0,"span",21)}function Pa(n,s){if(n&1&&(l(0,"label",20),y(1,1),g(2,La,1,0,"span",21),d()),n&2){let t=p(2);D("floating",t._shouldLabelFloat())("monitorResize",t._hasOutline())("id",t._labelId),T("for",t._control.disableAutomaticLabeling?null:t._control.id),c(2),v(!t.hideRequiredMarker&&t._control.required?2:-1)}}function Ba(n,s){if(n&1&&g(0,Pa,3,5,"label",20),n&2){let t=p();v(t._hasFloatingLabel()?0:-1)}}function za(n,s){n&1&&M(0,"div",7)}function Na(n,s){}function ja(n,s){if(n&1&&tt(0,Na,0,0,"ng-template",13),n&2){p(2);let t=ht(1);D("ngTemplateOutlet",t)}}function Va(n,s){if(n&1&&(l(0,"div",9),g(1,ja,1,1,null,13),d()),n&2){let t=p();D("matFormFieldNotchedOutlineOpen",t._shouldLabelFloat()),c(),v(t._forceDisplayInfixLabel()?-1:1)}}function Ha(n,s){n&1&&(l(0,"div",10,2),y(2,2),d())}function $a(n,s){n&1&&(l(0,"div",11,3),y(2,3),d())}function Wa(n,s){}function Qa(n,s){if(n&1&&tt(0,Wa,0,0,"ng-template",13),n&2){p();let t=ht(1);D("ngTemplateOutlet",t)}}function Ga(n,s){n&1&&(l(0,"div",14,4),y(2,4),d())}function qa(n,s){n&1&&(l(0,"div",15,5),y(2,5),d())}function Ua(n,s){n&1&&M(0,"div",16)}function Ya(n,s){n&1&&(l(0,"div",18),y(1,6),d())}function Ka(n,s){if(n&1&&(l(0,"mat-hint",22),E(1),d()),n&2){let t=p(2);D("id",t._hintLabelId),c(),pt(t.hintLabel)}}function Za(n,s){if(n&1&&(l(0,"div",19),g(1,Ka,2,2,"mat-hint",22),y(2,7),M(3,"div",23),y(4,8),d()),n&2){let t=p();c(),v(t.hintLabel?1:-1)}}var Le=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["mat-label"]]})}return n})(),Cn=new w("MatError"),Xa=(()=>{class n{id=r(U).getId("mat-mdc-error-");constructor(){}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["mat-error"],["","matError",""]],hostAttrs:[1,"mat-mdc-form-field-error","mat-mdc-form-field-bottom-align"],hostVars:1,hostBindings:function(e,i){e&2&&Rt("id",i.id)},inputs:{id:"id"},features:[$([{provide:Cn,useExisting:n}])]})}return n})(),Pe=(()=>{class n{align="start";id=r(U).getId("mat-mdc-hint-");static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["mat-hint"]],hostAttrs:[1,"mat-mdc-form-field-hint","mat-mdc-form-field-bottom-align"],hostVars:4,hostBindings:function(e,i){e&2&&(Rt("id",i.id),T("align",null),_("mat-mdc-form-field-hint-end",i.align==="end"))},inputs:{align:"align",id:"id"}})}return n})(),Tn=new w("MatPrefix"),Ja=(()=>{class n{set _isTextSelector(t){this._isText=!0}_isText=!1;static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matPrefix",""],["","matIconPrefix",""],["","matTextPrefix",""]],inputs:{_isTextSelector:[0,"matTextPrefix","_isTextSelector"]},features:[$([{provide:Tn,useExisting:n}])]})}return n})(),Mn=new w("MatSuffix"),to=(()=>{class n{set _isTextSelector(t){this._isText=!0}_isText=!1;static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matSuffix",""],["","matIconSuffix",""],["","matTextSuffix",""]],inputs:{_isTextSelector:[0,"matTextSuffix","_isTextSelector"]},features:[$([{provide:Mn,useExisting:n}])]})}return n})(),Sn=new w("FloatingLabelParent"),gn=(()=>{class n{_elementRef=r(O);get floating(){return this._floating}set floating(t){this._floating=t,this.monitorResize&&this._handleResize()}_floating=!1;get monitorResize(){return this._monitorResize}set monitorResize(t){this._monitorResize=t,this._monitorResize?this._subscribeToResize():this._resizeSubscription.unsubscribe()}_monitorResize=!1;_resizeObserver=r(he);_ngZone=r(H);_parent=r(Sn);_resizeSubscription=new it;constructor(){}ngOnDestroy(){this._resizeSubscription.unsubscribe()}getWidth(){return eo(this._elementRef.nativeElement)}get element(){return this._elementRef.nativeElement}_handleResize(){setTimeout(()=>this._parent._handleLabelResized())}_subscribeToResize(){this._resizeSubscription.unsubscribe(),this._ngZone.runOutsideAngular(()=>{this._resizeSubscription=this._resizeObserver.observe(this._elementRef.nativeElement,{box:"border-box"}).subscribe(()=>this._handleResize())})}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["label","matFormFieldFloatingLabel",""]],hostAttrs:[1,"mdc-floating-label","mat-mdc-floating-label"],hostVars:2,hostBindings:function(e,i){e&2&&_("mdc-floating-label--float-above",i.floating)},inputs:{floating:"floating",monitorResize:"monitorResize"}})}return n})();function eo(n){let s=n;if(s.offsetParent!==null)return s.scrollWidth;let t=s.cloneNode(!0);t.style.setProperty("position","absolute"),t.style.setProperty("transform","translate(-9999px, -9999px)"),document.documentElement.appendChild(t);let e=t.scrollWidth;return t.remove(),e}var vn="mdc-line-ripple--active",fe="mdc-line-ripple--deactivating",yn=(()=>{class n{_elementRef=r(O);_cleanupTransitionEnd;constructor(){let t=r(H),e=r(st);t.runOutsideAngular(()=>{this._cleanupTransitionEnd=e.listen(this._elementRef.nativeElement,"transitionend",this._handleTransitionEnd)})}activate(){let t=this._elementRef.nativeElement.classList;t.remove(fe),t.add(vn)}deactivate(){this._elementRef.nativeElement.classList.add(fe)}_handleTransitionEnd=t=>{let e=this._elementRef.nativeElement.classList,i=e.contains(fe);t.propertyName==="opacity"&&i&&e.remove(vn,fe)};ngOnDestroy(){this._cleanupTransitionEnd()}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["div","matFormFieldLineRipple",""]],hostAttrs:[1,"mdc-line-ripple"]})}return n})(),xn=(()=>{class n{_elementRef=r(O);_ngZone=r(H);open=!1;_notch;ngAfterViewInit(){let t=this._elementRef.nativeElement,e=t.querySelector(".mdc-floating-label");e?(t.classList.add("mdc-notched-outline--upgraded"),typeof requestAnimationFrame=="function"&&(e.style.transitionDuration="0s",this._ngZone.runOutsideAngular(()=>{requestAnimationFrame(()=>e.style.transitionDuration="")}))):t.classList.add("mdc-notched-outline--no-label")}_setNotchWidth(t){let e=this._notch.nativeElement;!this.open||!t?e.style.width="":e.style.width=`calc(${t}px * var(--mat-mdc-form-field-floating-label-scale, 0.75) + 9px)`}_setMaxWidth(t){this._notch.nativeElement.style.setProperty("--mat-form-field-notch-max-width",`calc(100% - ${t}px)`)}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["div","matFormFieldNotchedOutline",""]],viewQuery:function(e,i){if(e&1&&V(Da,5),e&2){let a;m(a=h())&&(i._notch=a.first)}},hostAttrs:[1,"mdc-notched-outline"],hostVars:2,hostBindings:function(e,i){e&2&&_("mdc-notched-outline--notched",i.open)},inputs:{open:[0,"matFormFieldNotchedOutlineOpen","open"]},attrs:Ea,ngContentSelectors:Fa,decls:5,vars:0,consts:[["notch",""],[1,"mat-mdc-notch-piece","mdc-notched-outline__leading"],[1,"mat-mdc-notch-piece","mdc-notched-outline__notch"],[1,"mat-mdc-notch-piece","mdc-notched-outline__trailing"]],template:function(e,i){e&1&&(B(),ke(0,"div",1),kt(1,"div",2,0),y(3),Ot(),ke(4,"div",3))},encapsulation:2,changeDetection:0})}return n})(),Be=(()=>{class n{value=null;stateChanges;id;placeholder;ngControl=null;focused=!1;empty=!1;shouldLabelFloat=!1;required=!1;disabled=!1;errorState=!1;controlType;autofilled;userAriaDescribedBy;disableAutomaticLabeling;describedByIds;static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n})}return n})();var ze=new w("MatFormField"),io=new w("MAT_FORM_FIELD_DEFAULT_OPTIONS"),kn="fill",no="auto",wn="fixed",ao="translateY(-50%)",In=(()=>{class n{_elementRef=r(O);_changeDetectorRef=r(W);_platform=r(ot);_idGenerator=r(U);_ngZone=r(H);_defaults=r(io,{optional:!0});_currentDirection;_textField;_iconPrefixContainer;_textPrefixContainer;_iconSuffixContainer;_textSuffixContainer;_floatingLabel;_notchedOutline;_lineRipple;_iconPrefixContainerSignal=Qt("iconPrefixContainer");_textPrefixContainerSignal=Qt("textPrefixContainer");_iconSuffixContainerSignal=Qt("iconSuffixContainer");_textSuffixContainerSignal=Qt("textSuffixContainer");_prefixSuffixContainers=At(()=>[this._iconPrefixContainerSignal(),this._textPrefixContainerSignal(),this._iconSuffixContainerSignal(),this._textSuffixContainerSignal()].map(t=>t?.nativeElement).filter(t=>t!==void 0));_formFieldControl;_prefixChildren;_suffixChildren;_errorChildren;_hintChildren;_labelChild=bi(Le);get hideRequiredMarker(){return this._hideRequiredMarker}set hideRequiredMarker(t){this._hideRequiredMarker=K(t)}_hideRequiredMarker=!1;color="primary";get floatLabel(){return this._floatLabel||this._defaults?.floatLabel||no}set floatLabel(t){t!==this._floatLabel&&(this._floatLabel=t,this._changeDetectorRef.markForCheck())}_floatLabel;get appearance(){return this._appearanceSignal()}set appearance(t){let e=t||this._defaults?.appearance||kn;this._appearanceSignal.set(e)}_appearanceSignal=Z(kn);get subscriptSizing(){return this._subscriptSizing||this._defaults?.subscriptSizing||wn}set subscriptSizing(t){this._subscriptSizing=t||this._defaults?.subscriptSizing||wn}_subscriptSizing=null;get hintLabel(){return this._hintLabel}set hintLabel(t){this._hintLabel=t,this._processHints()}_hintLabel="";_hasIconPrefix=!1;_hasTextPrefix=!1;_hasIconSuffix=!1;_hasTextSuffix=!1;_labelId=this._idGenerator.getId("mat-mdc-form-field-label-");_hintLabelId=this._idGenerator.getId("mat-mdc-hint-");_describedByIds;get _control(){return this._explicitFormFieldControl||this._formFieldControl}set _control(t){this._explicitFormFieldControl=t}_destroyed=new k;_isFocused=null;_explicitFormFieldControl;_previousControl=null;_previousControlValidatorFn=null;_stateChanges;_valueChanges;_describedByChanges;_outlineLabelOffsetResizeObserver=null;_animationsDisabled=Q();constructor(){let t=this._defaults,e=r(ut);t&&(t.appearance&&(this.appearance=t.appearance),this._hideRequiredMarker=!!t?.hideRequiredMarker,t.color&&(this.color=t.color)),ii(()=>this._currentDirection=e.valueSignal()),this._syncOutlineLabelOffset()}ngAfterViewInit(){this._updateFocusState(),this._animationsDisabled||this._ngZone.runOutsideAngular(()=>{setTimeout(()=>{this._elementRef.nativeElement.classList.add("mat-form-field-animations-enabled")},300)}),this._changeDetectorRef.detectChanges()}ngAfterContentInit(){this._assertFormFieldControl(),this._initializeSubscript(),this._initializePrefixAndSuffix()}ngAfterContentChecked(){this._assertFormFieldControl(),this._control!==this._previousControl&&(this._initializeControl(this._previousControl),this._control.ngControl&&this._control.ngControl.control&&(this._previousControlValidatorFn=this._control.ngControl.control.validator),this._previousControl=this._control),this._control.ngControl&&this._control.ngControl.control&&this._control.ngControl.control.validator!==this._previousControlValidatorFn&&this._changeDetectorRef.markForCheck()}ngOnDestroy(){this._outlineLabelOffsetResizeObserver?.disconnect(),this._stateChanges?.unsubscribe(),this._valueChanges?.unsubscribe(),this._describedByChanges?.unsubscribe(),this._destroyed.next(),this._destroyed.complete()}getLabelId=At(()=>this._hasFloatingLabel()?this._labelId:null);getConnectedOverlayOrigin(){return this._textField||this._elementRef}_animateAndLockLabel(){this._hasFloatingLabel()&&(this.floatLabel="always")}_initializeControl(t){let e=this._control,i="mat-mdc-form-field-type-";t&&this._elementRef.nativeElement.classList.remove(i+t.controlType),e.controlType&&this._elementRef.nativeElement.classList.add(i+e.controlType),this._stateChanges?.unsubscribe(),this._stateChanges=e.stateChanges.subscribe(()=>{this._updateFocusState(),this._changeDetectorRef.markForCheck()}),this._describedByChanges?.unsubscribe(),this._describedByChanges=e.stateChanges.pipe(nt([void 0,void 0]),Ht(()=>[e.errorState,e.userAriaDescribedBy]),Je(),dt(([[a,o],[f,b]])=>a!==f||o!==b)).subscribe(()=>this._syncDescribedByIds()),this._valueChanges?.unsubscribe(),e.ngControl&&e.ngControl.valueChanges&&(this._valueChanges=e.ngControl.valueChanges.pipe(I(this._destroyed)).subscribe(()=>this._changeDetectorRef.markForCheck()))}_checkPrefixAndSuffixTypes(){this._hasIconPrefix=!!this._prefixChildren.find(t=>!t._isText),this._hasTextPrefix=!!this._prefixChildren.find(t=>t._isText),this._hasIconSuffix=!!this._suffixChildren.find(t=>!t._isText),this._hasTextSuffix=!!this._suffixChildren.find(t=>t._isText)}_initializePrefixAndSuffix(){this._checkPrefixAndSuffixTypes(),J(this._prefixChildren.changes,this._suffixChildren.changes).subscribe(()=>{this._checkPrefixAndSuffixTypes(),this._changeDetectorRef.markForCheck()})}_initializeSubscript(){this._hintChildren.changes.subscribe(()=>{this._processHints(),this._changeDetectorRef.markForCheck()}),this._errorChildren.changes.subscribe(()=>{this._syncDescribedByIds(),this._changeDetectorRef.markForCheck()}),this._validateHints(),this._syncDescribedByIds()}_assertFormFieldControl(){this._control}_updateFocusState(){let t=this._control.focused;t&&!this._isFocused?(this._isFocused=!0,this._lineRipple?.activate()):!t&&(this._isFocused||this._isFocused===null)&&(this._isFocused=!1,this._lineRipple?.deactivate()),this._elementRef.nativeElement.classList.toggle("mat-focused",t),this._textField?.nativeElement.classList.toggle("mdc-text-field--focused",t)}_syncOutlineLabelOffset(){gi({earlyRead:()=>{if(this._appearanceSignal()!=="outline")return this._outlineLabelOffsetResizeObserver?.disconnect(),null;if(globalThis.ResizeObserver){this._outlineLabelOffsetResizeObserver||=new globalThis.ResizeObserver(()=>{this._writeOutlinedLabelStyles(this._getOutlinedLabelOffset())});for(let t of this._prefixSuffixContainers())this._outlineLabelOffsetResizeObserver.observe(t,{box:"border-box"})}return this._getOutlinedLabelOffset()},write:t=>this._writeOutlinedLabelStyles(t())})}_shouldAlwaysFloat(){return this.floatLabel==="always"}_hasOutline(){return this.appearance==="outline"}_forceDisplayInfixLabel(){return!this._platform.isBrowser&&this._prefixChildren.length&&!this._shouldLabelFloat()}_hasFloatingLabel=At(()=>!!this._labelChild());_shouldLabelFloat(){return this._hasFloatingLabel()?this._control.shouldLabelFloat||this._shouldAlwaysFloat():!1}_shouldForward(t){let e=this._control?this._control.ngControl:null;return e&&e[t]}_getSubscriptMessageType(){return this._errorChildren&&this._errorChildren.length>0&&this._control.errorState?"error":"hint"}_handleLabelResized(){this._refreshOutlineNotchWidth()}_refreshOutlineNotchWidth(){!this._hasOutline()||!this._floatingLabel||!this._shouldLabelFloat()?this._notchedOutline?._setNotchWidth(0):this._notchedOutline?._setNotchWidth(this._floatingLabel.getWidth())}_processHints(){this._validateHints(),this._syncDescribedByIds()}_validateHints(){this._hintChildren}_syncDescribedByIds(){if(this._control){let t=[];if(this._control.userAriaDescribedBy&&typeof this._control.userAriaDescribedBy=="string"&&t.push(...this._control.userAriaDescribedBy.split(" ")),this._getSubscriptMessageType()==="hint"){let a=this._hintChildren?this._hintChildren.find(f=>f.align==="start"):null,o=this._hintChildren?this._hintChildren.find(f=>f.align==="end"):null;a?t.push(a.id):this._hintLabel&&t.push(this._hintLabelId),o&&t.push(o.id)}else this._errorChildren&&t.push(...this._errorChildren.map(a=>a.id));let e=this._control.describedByIds,i;if(e){let a=this._describedByIds||t;i=t.concat(e.filter(o=>o&&!a.includes(o)))}else i=t;this._control.setDescribedByIds(i),this._describedByIds=t}}_getOutlinedLabelOffset(){if(!this._hasOutline()||!this._floatingLabel)return null;if(!this._iconPrefixContainer&&!this._textPrefixContainer)return["",null];if(!this._isAttachedToDom())return null;let t=this._iconPrefixContainer?.nativeElement,e=this._textPrefixContainer?.nativeElement,i=this._iconSuffixContainer?.nativeElement,a=this._textSuffixContainer?.nativeElement,o=t?.getBoundingClientRect().width??0,f=e?.getBoundingClientRect().width??0,b=i?.getBoundingClientRect().width??0,xt=a?.getBoundingClientRect().width??0,Mt=this._currentDirection==="rtl"?"-1":"1",ia=`${o+f}px`,na=`calc(${Mt} * (${ia} + var(--mat-mdc-form-field-label-offset-x, 0px)))`,aa=`var(--mat-mdc-form-field-label-transform, ${ao} translateX(${na}))`,oa=o+f+b+xt;return[aa,oa]}_writeOutlinedLabelStyles(t){if(t!==null){let[e,i]=t;this._floatingLabel&&(this._floatingLabel.element.style.transform=e),i!==null&&this._notchedOutline?._setMaxWidth(i)}}_isAttachedToDom(){let t=this._elementRef.nativeElement;if(t.getRootNode){let e=t.getRootNode();return e&&e!==t}return document.documentElement.contains(t)}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-form-field"]],contentQueries:function(e,i,a){if(e&1&&(li(a,i._labelChild,Le,5),et(a,Be,5)(a,Tn,5)(a,Mn,5)(a,Cn,5)(a,Pe,5)),e&2){we();let o;m(o=h())&&(i._formFieldControl=o.first),m(o=h())&&(i._prefixChildren=o),m(o=h())&&(i._suffixChildren=o),m(o=h())&&(i._errorChildren=o),m(o=h())&&(i._hintChildren=o)}},viewQuery:function(e,i){if(e&1&&(ci(i._iconPrefixContainerSignal,fn,5)(i._textPrefixContainerSignal,un,5)(i._iconSuffixContainerSignal,_n,5)(i._textSuffixContainerSignal,bn,5),V(Oa,5)(fn,5)(un,5)(_n,5)(bn,5)(gn,5)(xn,5)(yn,5)),e&2){we(4);let a;m(a=h())&&(i._textField=a.first),m(a=h())&&(i._iconPrefixContainer=a.first),m(a=h())&&(i._textPrefixContainer=a.first),m(a=h())&&(i._iconSuffixContainer=a.first),m(a=h())&&(i._textSuffixContainer=a.first),m(a=h())&&(i._floatingLabel=a.first),m(a=h())&&(i._notchedOutline=a.first),m(a=h())&&(i._lineRipple=a.first)}},hostAttrs:[1,"mat-mdc-form-field"],hostVars:38,hostBindings:function(e,i){e&2&&_("mat-mdc-form-field-label-always-float",i._shouldAlwaysFloat())("mat-mdc-form-field-has-icon-prefix",i._hasIconPrefix)("mat-mdc-form-field-has-icon-suffix",i._hasIconSuffix)("mat-form-field-invalid",i._control.errorState)("mat-form-field-disabled",i._control.disabled)("mat-form-field-autofilled",i._control.autofilled)("mat-form-field-appearance-fill",i.appearance=="fill")("mat-form-field-appearance-outline",i.appearance=="outline")("mat-form-field-hide-placeholder",i._hasFloatingLabel()&&!i._shouldLabelFloat())("mat-primary",i.color!=="accent"&&i.color!=="warn")("mat-accent",i.color==="accent")("mat-warn",i.color==="warn")("ng-untouched",i._shouldForward("untouched"))("ng-touched",i._shouldForward("touched"))("ng-pristine",i._shouldForward("pristine"))("ng-dirty",i._shouldForward("dirty"))("ng-valid",i._shouldForward("valid"))("ng-invalid",i._shouldForward("invalid"))("ng-pending",i._shouldForward("pending"))},inputs:{hideRequiredMarker:"hideRequiredMarker",color:"color",floatLabel:"floatLabel",appearance:"appearance",subscriptSizing:"subscriptSizing",hintLabel:"hintLabel"},exportAs:["matFormField"],features:[$([{provide:ze,useExisting:n},{provide:Sn,useExisting:n}])],ngContentSelectors:Aa,decls:18,vars:21,consts:[["labelTemplate",""],["textField",""],["iconPrefixContainer",""],["textPrefixContainer",""],["textSuffixContainer",""],["iconSuffixContainer",""],[1,"mat-mdc-text-field-wrapper","mdc-text-field",3,"click"],[1,"mat-mdc-form-field-focus-overlay"],[1,"mat-mdc-form-field-flex"],["matFormFieldNotchedOutline","",3,"matFormFieldNotchedOutlineOpen"],[1,"mat-mdc-form-field-icon-prefix"],[1,"mat-mdc-form-field-text-prefix"],[1,"mat-mdc-form-field-infix"],[3,"ngTemplateOutlet"],[1,"mat-mdc-form-field-text-suffix"],[1,"mat-mdc-form-field-icon-suffix"],["matFormFieldLineRipple",""],["aria-atomic","true","aria-live","polite",1,"mat-mdc-form-field-subscript-wrapper","mat-mdc-form-field-bottom-align"],[1,"mat-mdc-form-field-error-wrapper"],[1,"mat-mdc-form-field-hint-wrapper"],["matFormFieldFloatingLabel","",3,"floating","monitorResize","id"],["aria-hidden","true",1,"mat-mdc-form-field-required-marker","mdc-floating-label--required"],[3,"id"],[1,"mat-mdc-form-field-hint-spacer"]],template:function(e,i){if(e&1&&(B(Ra),tt(0,Ba,1,1,"ng-template",null,0,Te),l(2,"div",6,1),u("click",function(o){return i._control.onContainerClick(o)}),g(4,za,1,0,"div",7),l(5,"div",8),g(6,Va,2,2,"div",9),g(7,Ha,3,0,"div",10),g(8,$a,3,0,"div",11),l(9,"div",12),g(10,Qa,1,1,null,13),y(11),d(),g(12,Ga,3,0,"div",14),g(13,qa,3,0,"div",15),d(),g(14,Ua,1,0,"div",16),d(),l(15,"div",17),g(16,Ya,2,0,"div",18)(17,Za,5,1,"div",19),d()),e&2){let a;c(2),_("mdc-text-field--filled",!i._hasOutline())("mdc-text-field--outlined",i._hasOutline())("mdc-text-field--no-label",!i._hasFloatingLabel())("mdc-text-field--disabled",i._control.disabled)("mdc-text-field--invalid",i._control.errorState),c(2),v(!i._hasOutline()&&!i._control.disabled?4:-1),c(2),v(i._hasOutline()?6:-1),c(),v(i._hasIconPrefix?7:-1),c(),v(i._hasTextPrefix?8:-1),c(2),v(!i._hasOutline()||i._forceDisplayInfixLabel()?10:-1),c(2),v(i._hasTextSuffix?12:-1),c(),v(i._hasIconSuffix?13:-1),c(),v(i._hasOutline()?-1:14),c(),_("mat-mdc-form-field-subscript-dynamic-size",i.subscriptSizing==="dynamic");let o=i._getSubscriptMessageType();c(),v((a=o)==="error"?16:a==="hint"?17:-1)}},dependencies:[gn,xn,xi,yn,Pe],styles:[`.mdc-text-field { - display: inline-flex; - align-items: baseline; - padding: 0 16px; - position: relative; - box-sizing: border-box; - overflow: hidden; - will-change: opacity, transform, color; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} - -.mdc-text-field__input { - width: 100%; - min-width: 0; - border: none; - border-radius: 0; - background: none; - padding: 0; - -moz-appearance: none; - -webkit-appearance: none; - height: 28px; -} -.mdc-text-field__input::-webkit-calendar-picker-indicator, .mdc-text-field__input::-webkit-search-cancel-button { - display: none; -} -.mdc-text-field__input::-ms-clear { - display: none; -} -.mdc-text-field__input:focus { - outline: none; -} -.mdc-text-field__input:invalid { - box-shadow: none; -} -.mdc-text-field__input::placeholder { - opacity: 0; -} -.mdc-text-field__input::-moz-placeholder { - opacity: 0; -} -.mdc-text-field__input::-webkit-input-placeholder { - opacity: 0; -} -.mdc-text-field__input:-ms-input-placeholder { - opacity: 0; -} -.mdc-text-field--no-label .mdc-text-field__input::placeholder, .mdc-text-field--focused .mdc-text-field__input::placeholder { - opacity: 1; -} -.mdc-text-field--no-label .mdc-text-field__input::-moz-placeholder, .mdc-text-field--focused .mdc-text-field__input::-moz-placeholder { - opacity: 1; -} -.mdc-text-field--no-label .mdc-text-field__input::-webkit-input-placeholder, .mdc-text-field--focused .mdc-text-field__input::-webkit-input-placeholder { - opacity: 1; -} -.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder, .mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder { - opacity: 1; -} -.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive::placeholder { - opacity: 0; -} -.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive::-moz-placeholder { - opacity: 0; -} -.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive::-webkit-input-placeholder { - opacity: 0; -} -.mdc-text-field--disabled:not(.mdc-text-field--no-label) .mdc-text-field__input.mat-mdc-input-disabled-interactive:-ms-input-placeholder { - opacity: 0; -} -.mdc-text-field--outlined .mdc-text-field__input, .mdc-text-field--filled.mdc-text-field--no-label .mdc-text-field__input { - height: 100%; -} -.mdc-text-field--outlined .mdc-text-field__input { - display: flex; - border: none !important; - background-color: transparent; -} -.mdc-text-field--disabled .mdc-text-field__input { - pointer-events: auto; -} -.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input { - color: var(--mat-form-field-filled-input-text-color, var(--mat-sys-on-surface)); - caret-color: var(--mat-form-field-filled-caret-color, var(--mat-sys-primary)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder { - color: var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input::-moz-placeholder { - color: var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input::-webkit-input-placeholder { - color: var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder { - color: var(--mat-form-field-filled-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input { - color: var(--mat-form-field-outlined-input-text-color, var(--mat-sys-on-surface)); - caret-color: var(--mat-form-field-outlined-caret-color, var(--mat-sys-primary)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input::placeholder { - color: var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input::-moz-placeholder { - color: var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input::-webkit-input-placeholder { - color: var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-text-field__input:-ms-input-placeholder { - color: var(--mat-form-field-outlined-input-text-placeholder-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--filled.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__input { - caret-color: var(--mat-form-field-filled-error-caret-color, var(--mat-sys-error)); -} -.mdc-text-field--outlined.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-text-field__input { - caret-color: var(--mat-form-field-outlined-error-caret-color, var(--mat-sys-error)); -} -.mdc-text-field--filled.mdc-text-field--disabled .mdc-text-field__input { - color: var(--mat-form-field-filled-disabled-input-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mdc-text-field--outlined.mdc-text-field--disabled .mdc-text-field__input { - color: var(--mat-form-field-outlined-disabled-input-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -@media (forced-colors: active) { - .mdc-text-field--disabled .mdc-text-field__input { - background-color: Window; - } -} - -.mdc-text-field--filled { - height: 56px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - border-top-left-radius: var(--mat-form-field-filled-container-shape, var(--mat-sys-corner-extra-small)); - border-top-right-radius: var(--mat-form-field-filled-container-shape, var(--mat-sys-corner-extra-small)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled) { - background-color: var(--mat-form-field-filled-container-color, var(--mat-sys-surface-variant)); -} -.mdc-text-field--filled.mdc-text-field--disabled { - background-color: var(--mat-form-field-filled-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 4%, transparent)); -} - -.mdc-text-field--outlined { - height: 56px; - overflow: visible; - padding-right: max(16px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))); - padding-left: max(16px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)) + 4px); -} -[dir=rtl] .mdc-text-field--outlined { - padding-right: max(16px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)) + 4px); - padding-left: max(16px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))); -} - -.mdc-floating-label { - position: absolute; - left: 0; - transform-origin: left top; - line-height: 1.15rem; - text-align: left; - text-overflow: ellipsis; - white-space: nowrap; - cursor: text; - overflow: hidden; - will-change: transform; -} -[dir=rtl] .mdc-floating-label { - right: 0; - left: auto; - transform-origin: right top; - text-align: right; -} -.mdc-text-field .mdc-floating-label { - top: 50%; - transform: translateY(-50%); - pointer-events: none; -} -.mdc-notched-outline .mdc-floating-label { - display: inline-block; - position: relative; - max-width: 100%; -} -.mdc-text-field--outlined .mdc-floating-label { - left: 4px; - right: auto; -} -[dir=rtl] .mdc-text-field--outlined .mdc-floating-label { - left: auto; - right: 4px; -} -.mdc-text-field--filled .mdc-floating-label { - left: 16px; - right: auto; -} -[dir=rtl] .mdc-text-field--filled .mdc-floating-label { - left: auto; - right: 16px; -} -.mdc-text-field--disabled .mdc-floating-label { - cursor: default; -} -@media (forced-colors: active) { - .mdc-text-field--disabled .mdc-floating-label { - z-index: 1; - } -} -.mdc-text-field--filled.mdc-text-field--no-label .mdc-floating-label { - display: none; -} -.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-floating-label { - color: var(--mat-form-field-filled-label-text-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-floating-label { - color: var(--mat-form-field-filled-focus-label-text-color, var(--mat-sys-primary)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-floating-label { - color: var(--mat-form-field-filled-hover-label-text-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--filled.mdc-text-field--disabled .mdc-floating-label { - color: var(--mat-form-field-filled-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid .mdc-floating-label { - color: var(--mat-form-field-filled-error-label-text-color, var(--mat-sys-error)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid.mdc-text-field--focused .mdc-floating-label { - color: var(--mat-form-field-filled-error-focus-label-text-color, var(--mat-sys-error)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-floating-label { - color: var(--mat-form-field-filled-error-hover-label-text-color, var(--mat-sys-on-error-container)); -} -.mdc-text-field--filled .mdc-floating-label { - font-family: var(--mat-form-field-filled-label-text-font, var(--mat-sys-body-large-font)); - font-size: var(--mat-form-field-filled-label-text-size, var(--mat-sys-body-large-size)); - font-weight: var(--mat-form-field-filled-label-text-weight, var(--mat-sys-body-large-weight)); - letter-spacing: var(--mat-form-field-filled-label-text-tracking, var(--mat-sys-body-large-tracking)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-floating-label { - color: var(--mat-form-field-outlined-label-text-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-floating-label { - color: var(--mat-form-field-outlined-focus-label-text-color, var(--mat-sys-primary)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-floating-label { - color: var(--mat-form-field-outlined-hover-label-text-color, var(--mat-sys-on-surface)); -} -.mdc-text-field--outlined.mdc-text-field--disabled .mdc-floating-label { - color: var(--mat-form-field-outlined-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid .mdc-floating-label { - color: var(--mat-form-field-outlined-error-label-text-color, var(--mat-sys-error)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid.mdc-text-field--focused .mdc-floating-label { - color: var(--mat-form-field-outlined-error-focus-label-text-color, var(--mat-sys-error)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--disabled):hover .mdc-floating-label { - color: var(--mat-form-field-outlined-error-hover-label-text-color, var(--mat-sys-on-error-container)); -} -.mdc-text-field--outlined .mdc-floating-label { - font-family: var(--mat-form-field-outlined-label-text-font, var(--mat-sys-body-large-font)); - font-size: var(--mat-form-field-outlined-label-text-size, var(--mat-sys-body-large-size)); - font-weight: var(--mat-form-field-outlined-label-text-weight, var(--mat-sys-body-large-weight)); - letter-spacing: var(--mat-form-field-outlined-label-text-tracking, var(--mat-sys-body-large-tracking)); -} - -.mdc-floating-label--float-above { - cursor: auto; - transform: translateY(-106%) scale(0.75); -} -.mdc-text-field--filled .mdc-floating-label--float-above { - transform: translateY(-106%) scale(0.75); -} -.mdc-text-field--outlined .mdc-floating-label--float-above { - transform: translateY(-37.25px) scale(1); - font-size: 0.75rem; -} -.mdc-notched-outline .mdc-floating-label--float-above { - text-overflow: clip; -} -.mdc-notched-outline--upgraded .mdc-floating-label--float-above { - max-width: 133.3333333333%; -} -.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above, .mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above { - transform: translateY(-34.75px) scale(0.75); -} -.mdc-text-field--outlined.mdc-notched-outline--upgraded .mdc-floating-label--float-above, .mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above { - font-size: 1rem; -} - -.mdc-floating-label--required:not(.mdc-floating-label--hide-required-marker)::after { - margin-left: 1px; - margin-right: 0; - content: "*"; -} -[dir=rtl] .mdc-floating-label--required:not(.mdc-floating-label--hide-required-marker)::after { - margin-left: 0; - margin-right: 1px; -} - -.mdc-notched-outline { - display: flex; - position: absolute; - top: 0; - right: 0; - left: 0; - box-sizing: border-box; - width: 100%; - max-width: 100%; - height: 100%; - text-align: left; - pointer-events: none; -} -[dir=rtl] .mdc-notched-outline { - text-align: right; -} -.mdc-text-field--outlined .mdc-notched-outline { - z-index: 1; -} - -.mat-mdc-notch-piece { - box-sizing: border-box; - height: 100%; - pointer-events: none; - border: none; - border-top: 1px solid; - border-bottom: 1px solid; -} -.mdc-text-field--focused .mat-mdc-notch-piece { - border-width: 2px; -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled) .mat-mdc-notch-piece { - border-color: var(--mat-form-field-outlined-outline-color, var(--mat-sys-outline)); - border-width: var(--mat-form-field-outlined-outline-width, 1px); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mat-mdc-notch-piece { - border-color: var(--mat-form-field-outlined-hover-outline-color, var(--mat-sys-on-surface)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mat-mdc-notch-piece { - border-color: var(--mat-form-field-outlined-focus-outline-color, var(--mat-sys-primary)); -} -.mdc-text-field--outlined.mdc-text-field--disabled .mat-mdc-notch-piece { - border-color: var(--mat-form-field-outlined-disabled-outline-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid .mat-mdc-notch-piece { - border-color: var(--mat-form-field-outlined-error-outline-color, var(--mat-sys-error)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--focused):hover .mdc-notched-outline .mat-mdc-notch-piece { - border-color: var(--mat-form-field-outlined-error-hover-outline-color, var(--mat-sys-on-error-container)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--invalid.mdc-text-field--focused .mat-mdc-notch-piece { - border-color: var(--mat-form-field-outlined-error-focus-outline-color, var(--mat-sys-error)); -} -.mdc-text-field--outlined:not(.mdc-text-field--disabled).mdc-text-field--focused .mdc-notched-outline .mat-mdc-notch-piece { - border-width: var(--mat-form-field-outlined-focus-outline-width, 2px); -} - -.mdc-notched-outline__leading { - border-left: 1px solid; - border-right: none; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-top-left-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); - border-bottom-left-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); -} -.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading { - width: max(12px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))); -} -[dir=rtl] .mdc-notched-outline__leading { - border-left: none; - border-right: 1px solid; - border-bottom-left-radius: 0; - border-top-left-radius: 0; - border-top-right-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); - border-bottom-right-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); -} - -.mdc-notched-outline__trailing { - flex-grow: 1; - border-left: none; - border-right: 1px solid; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-top-right-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); - border-bottom-right-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); -} -[dir=rtl] .mdc-notched-outline__trailing { - border-left: 1px solid; - border-right: none; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-top-left-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); - border-bottom-left-radius: var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small)); -} - -.mdc-notched-outline__notch { - flex: 0 0 auto; - width: auto; -} -.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__notch { - max-width: min(var(--mat-form-field-notch-max-width, 100%), calc(100% - max(12px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))) * 2)); -} -.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch { - max-width: min(100%, calc(100% - max(12px, var(--mat-form-field-outlined-container-shape, var(--mat-sys-corner-extra-small))) * 2)); -} -.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch { - padding-top: 1px; -} -.mdc-text-field--focused.mdc-text-field--outlined .mdc-notched-outline--notched .mdc-notched-outline__notch { - padding-top: 2px; -} -.mdc-notched-outline--notched .mdc-notched-outline__notch { - padding-left: 0; - padding-right: 8px; - border-top: none; -} -[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch { - padding-left: 8px; - padding-right: 0; -} -.mdc-notched-outline--no-label .mdc-notched-outline__notch { - display: none; -} - -.mdc-line-ripple::before, .mdc-line-ripple::after { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - border-bottom-style: solid; - content: ""; -} -.mdc-line-ripple::before { - z-index: 1; - border-bottom-width: var(--mat-form-field-filled-active-indicator-height, 1px); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::before { - border-bottom-color: var(--mat-form-field-filled-active-indicator-color, var(--mat-sys-on-surface-variant)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled):not(.mdc-text-field--focused):hover .mdc-line-ripple::before { - border-bottom-color: var(--mat-form-field-filled-hover-active-indicator-color, var(--mat-sys-on-surface)); -} -.mdc-text-field--filled.mdc-text-field--disabled .mdc-line-ripple::before { - border-bottom-color: var(--mat-form-field-filled-disabled-active-indicator-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid .mdc-line-ripple::before { - border-bottom-color: var(--mat-form-field-filled-error-active-indicator-color, var(--mat-sys-error)); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled).mdc-text-field--invalid:not(.mdc-text-field--focused):hover .mdc-line-ripple::before { - border-bottom-color: var(--mat-form-field-filled-error-hover-active-indicator-color, var(--mat-sys-on-error-container)); -} -.mdc-line-ripple::after { - transform: scaleX(0); - opacity: 0; - z-index: 2; -} -.mdc-text-field--filled .mdc-line-ripple::after { - border-bottom-width: var(--mat-form-field-filled-focus-active-indicator-height, 2px); -} -.mdc-text-field--filled:not(.mdc-text-field--disabled) .mdc-line-ripple::after { - border-bottom-color: var(--mat-form-field-filled-focus-active-indicator-color, var(--mat-sys-primary)); -} -.mdc-text-field--filled.mdc-text-field--invalid:not(.mdc-text-field--disabled) .mdc-line-ripple::after { - border-bottom-color: var(--mat-form-field-filled-error-focus-active-indicator-color, var(--mat-sys-error)); -} - -.mdc-line-ripple--active::after { - transform: scaleX(1); - opacity: 1; -} - -.mdc-line-ripple--deactivating::after { - opacity: 0; -} - -.mdc-text-field--disabled { - pointer-events: none; -} - -.mat-mdc-form-field-textarea-control { - vertical-align: middle; - resize: vertical; - box-sizing: border-box; - height: auto; - margin: 0; - padding: 0; - border: none; - overflow: auto; -} - -.mat-mdc-form-field-input-control.mat-mdc-form-field-input-control { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - font: inherit; - letter-spacing: inherit; - text-decoration: inherit; - text-transform: inherit; - border: none; -} - -.mat-mdc-form-field .mat-mdc-floating-label.mdc-floating-label { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - line-height: normal; - pointer-events: all; - will-change: auto; -} - -.mat-mdc-form-field:not(.mat-form-field-disabled) .mat-mdc-floating-label.mdc-floating-label { - cursor: inherit; -} - -.mdc-text-field--no-label:not(.mdc-text-field--textarea) .mat-mdc-form-field-input-control.mdc-text-field__input, -.mat-mdc-text-field-wrapper .mat-mdc-form-field-input-control { - height: auto; -} - -.mat-mdc-text-field-wrapper .mat-mdc-form-field-input-control.mdc-text-field__input[type=color] { - height: 23px; -} - -.mat-mdc-text-field-wrapper { - height: auto; - flex: auto; - will-change: auto; -} - -.mat-mdc-form-field-has-icon-prefix .mat-mdc-text-field-wrapper { - padding-left: 0; - --mat-mdc-form-field-label-offset-x: -16px; -} - -.mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { - padding-right: 0; -} - -[dir=rtl] .mat-mdc-text-field-wrapper { - padding-left: 16px; - padding-right: 16px; -} -[dir=rtl] .mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { - padding-left: 0; -} -[dir=rtl] .mat-mdc-form-field-has-icon-prefix .mat-mdc-text-field-wrapper { - padding-right: 0; -} - -.mat-form-field-disabled .mdc-text-field__input::placeholder { - color: var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-form-field-disabled .mdc-text-field__input::-moz-placeholder { - color: var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-form-field-disabled .mdc-text-field__input::-webkit-input-placeholder { - color: var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-form-field-disabled .mdc-text-field__input:-ms-input-placeholder { - color: var(--mat-form-field-disabled-input-text-placeholder-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} - -.mat-mdc-form-field-label-always-float .mdc-text-field__input::placeholder { - transition-delay: 40ms; - transition-duration: 110ms; - opacity: 1; -} - -.mat-mdc-text-field-wrapper .mat-mdc-form-field-infix .mat-mdc-floating-label { - left: auto; - right: auto; -} - -.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-text-field__input { - display: inline-block; -} - -.mat-mdc-form-field .mat-mdc-text-field-wrapper.mdc-text-field .mdc-notched-outline__notch { - padding-top: 0; -} - -.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field .mdc-notched-outline__notch { - border-left: 1px solid transparent; -} - -[dir=rtl] .mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field.mat-mdc-form-field .mdc-notched-outline__notch { - border-left: none; - border-right: 1px solid transparent; -} - -.mat-mdc-form-field-infix { - min-height: var(--mat-form-field-container-height, 56px); - padding-top: var(--mat-form-field-filled-with-label-container-padding-top, 24px); - padding-bottom: var(--mat-form-field-filled-with-label-container-padding-bottom, 8px); -} -.mdc-text-field--outlined .mat-mdc-form-field-infix, .mdc-text-field--no-label .mat-mdc-form-field-infix { - padding-top: var(--mat-form-field-container-vertical-padding, 16px); - padding-bottom: var(--mat-form-field-container-vertical-padding, 16px); -} - -.mat-mdc-text-field-wrapper .mat-mdc-form-field-flex .mat-mdc-floating-label { - top: calc(var(--mat-form-field-container-height, 56px) / 2); -} - -.mdc-text-field--filled .mat-mdc-floating-label { - display: var(--mat-form-field-filled-label-display, block); -} - -.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above { - --mat-mdc-form-field-label-transform: translateY(calc(calc(6.75px + var(--mat-form-field-container-height, 56px) / 2) * -1)) - scale(var(--mat-mdc-form-field-floating-label-scale, 0.75)); - transform: var(--mat-mdc-form-field-label-transform); -} - -@keyframes _mat-form-field-subscript-animation { - from { - opacity: 0; - transform: translateY(-5px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -.mat-mdc-form-field-subscript-wrapper { - box-sizing: border-box; - width: 100%; - position: relative; -} - -.mat-mdc-form-field-hint-wrapper, -.mat-mdc-form-field-error-wrapper { - position: absolute; - top: 0; - left: 0; - right: 0; - padding: 0 16px; - opacity: 1; - transform: translateY(0); - animation: _mat-form-field-subscript-animation 0ms cubic-bezier(0.55, 0, 0.55, 0.2); -} - -.mat-mdc-form-field-subscript-dynamic-size .mat-mdc-form-field-hint-wrapper, -.mat-mdc-form-field-subscript-dynamic-size .mat-mdc-form-field-error-wrapper { - position: static; -} - -.mat-mdc-form-field-bottom-align::before { - content: ""; - display: inline-block; - height: 16px; -} - -.mat-mdc-form-field-bottom-align.mat-mdc-form-field-subscript-dynamic-size::before { - content: unset; -} - -.mat-mdc-form-field-hint-end { - order: 1; -} - -.mat-mdc-form-field-hint-wrapper { - display: flex; -} - -.mat-mdc-form-field-hint-spacer { - flex: 1 0 1em; -} - -.mat-mdc-form-field-error { - display: block; - color: var(--mat-form-field-error-text-color, var(--mat-sys-error)); -} - -.mat-mdc-form-field-subscript-wrapper, -.mat-mdc-form-field-bottom-align::before { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - font-family: var(--mat-form-field-subscript-text-font, var(--mat-sys-body-small-font)); - line-height: var(--mat-form-field-subscript-text-line-height, var(--mat-sys-body-small-line-height)); - font-size: var(--mat-form-field-subscript-text-size, var(--mat-sys-body-small-size)); - letter-spacing: var(--mat-form-field-subscript-text-tracking, var(--mat-sys-body-small-tracking)); - font-weight: var(--mat-form-field-subscript-text-weight, var(--mat-sys-body-small-weight)); -} - -.mat-mdc-form-field-focus-overlay { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - opacity: 0; - pointer-events: none; - background-color: var(--mat-form-field-state-layer-color, var(--mat-sys-on-surface)); -} -.mat-mdc-text-field-wrapper:hover .mat-mdc-form-field-focus-overlay { - opacity: var(--mat-form-field-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); -} -.mat-mdc-form-field.mat-focused .mat-mdc-form-field-focus-overlay { - opacity: var(--mat-form-field-focus-state-layer-opacity, 0); -} - -select.mat-mdc-form-field-input-control { - -moz-appearance: none; - -webkit-appearance: none; - background-color: transparent; - display: inline-flex; - box-sizing: border-box; -} -select.mat-mdc-form-field-input-control:not(:disabled) { - cursor: pointer; -} -select.mat-mdc-form-field-input-control:not(.mat-mdc-native-select-inline) option { - color: var(--mat-form-field-select-option-text-color, var(--mat-sys-neutral10)); -} -select.mat-mdc-form-field-input-control:not(.mat-mdc-native-select-inline) option:disabled { - color: var(--mat-form-field-select-disabled-option-text-color, color-mix(in srgb, var(--mat-sys-neutral10) 38%, transparent)); -} - -.mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-infix::after { - content: ""; - width: 0; - height: 0; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 5px solid; - position: absolute; - right: 0; - top: 50%; - margin-top: -2.5px; - pointer-events: none; - color: var(--mat-form-field-enabled-select-arrow-color, var(--mat-sys-on-surface-variant)); -} -[dir=rtl] .mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-infix::after { - right: auto; - left: 0; -} -.mat-mdc-form-field-type-mat-native-select.mat-focused .mat-mdc-form-field-infix::after { - color: var(--mat-form-field-focus-select-arrow-color, var(--mat-sys-primary)); -} -.mat-mdc-form-field-type-mat-native-select.mat-form-field-disabled .mat-mdc-form-field-infix::after { - color: var(--mat-form-field-disabled-select-arrow-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-input-control { - padding-right: 15px; -} -[dir=rtl] .mat-mdc-form-field-type-mat-native-select .mat-mdc-form-field-input-control { - padding-right: 0; - padding-left: 15px; -} - -@media (forced-colors: active) { - .mat-form-field-appearance-fill .mat-mdc-text-field-wrapper { - outline: solid 1px; - } -} -@media (forced-colors: active) { - .mat-form-field-appearance-fill.mat-form-field-disabled .mat-mdc-text-field-wrapper { - outline-color: GrayText; - } -} - -@media (forced-colors: active) { - .mat-form-field-appearance-fill.mat-focused .mat-mdc-text-field-wrapper { - outline: dashed 3px; - } -} - -@media (forced-colors: active) { - .mat-mdc-form-field.mat-focused .mdc-notched-outline { - border: dashed 3px; - } -} - -.mat-mdc-form-field-input-control[type=date], .mat-mdc-form-field-input-control[type=datetime], .mat-mdc-form-field-input-control[type=datetime-local], .mat-mdc-form-field-input-control[type=month], .mat-mdc-form-field-input-control[type=week], .mat-mdc-form-field-input-control[type=time] { - line-height: 1; -} -.mat-mdc-form-field-input-control::-webkit-datetime-edit { - line-height: 1; - padding: 0; - margin-bottom: -2px; -} - -.mat-mdc-form-field { - --mat-mdc-form-field-floating-label-scale: 0.75; - display: inline-flex; - flex-direction: column; - min-width: 0; - text-align: left; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - font-family: var(--mat-form-field-container-text-font, var(--mat-sys-body-large-font)); - line-height: var(--mat-form-field-container-text-line-height, var(--mat-sys-body-large-line-height)); - font-size: var(--mat-form-field-container-text-size, var(--mat-sys-body-large-size)); - letter-spacing: var(--mat-form-field-container-text-tracking, var(--mat-sys-body-large-tracking)); - font-weight: var(--mat-form-field-container-text-weight, var(--mat-sys-body-large-weight)); -} -.mat-mdc-form-field .mdc-text-field--outlined .mdc-floating-label--float-above { - font-size: calc(var(--mat-form-field-outlined-label-text-populated-size) * var(--mat-mdc-form-field-floating-label-scale)); -} -.mat-mdc-form-field .mdc-text-field--outlined .mdc-notched-outline--upgraded .mdc-floating-label--float-above { - font-size: var(--mat-form-field-outlined-label-text-populated-size); -} -[dir=rtl] .mat-mdc-form-field { - text-align: right; -} - -.mat-mdc-form-field-flex { - display: inline-flex; - align-items: baseline; - box-sizing: border-box; - width: 100%; -} - -.mat-mdc-text-field-wrapper { - width: 100%; - z-index: 0; -} - -.mat-mdc-form-field-icon-prefix, -.mat-mdc-form-field-icon-suffix { - align-self: center; - line-height: 0; - pointer-events: auto; - position: relative; - z-index: 1; -} -.mat-mdc-form-field-icon-prefix > .mat-icon, -.mat-mdc-form-field-icon-suffix > .mat-icon { - padding: 0 12px; - box-sizing: content-box; -} - -.mat-mdc-form-field-icon-prefix { - color: var(--mat-form-field-leading-icon-color, var(--mat-sys-on-surface-variant)); -} -.mat-form-field-disabled .mat-mdc-form-field-icon-prefix { - color: var(--mat-form-field-disabled-leading-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} - -.mat-mdc-form-field-icon-suffix { - color: var(--mat-form-field-trailing-icon-color, var(--mat-sys-on-surface-variant)); -} -.mat-form-field-disabled .mat-mdc-form-field-icon-suffix { - color: var(--mat-form-field-disabled-trailing-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-form-field-invalid .mat-mdc-form-field-icon-suffix { - color: var(--mat-form-field-error-trailing-icon-color, var(--mat-sys-error)); -} -.mat-form-field-invalid:not(.mat-focused):not(.mat-form-field-disabled) .mat-mdc-text-field-wrapper:hover .mat-mdc-form-field-icon-suffix { - color: var(--mat-form-field-error-hover-trailing-icon-color, var(--mat-sys-on-error-container)); -} -.mat-form-field-invalid.mat-focused .mat-mdc-text-field-wrapper .mat-mdc-form-field-icon-suffix { - color: var(--mat-form-field-error-focus-trailing-icon-color, var(--mat-sys-error)); -} - -.mat-mdc-form-field-icon-prefix, -[dir=rtl] .mat-mdc-form-field-icon-suffix { - padding: 0 4px 0 0; -} - -.mat-mdc-form-field-icon-suffix, -[dir=rtl] .mat-mdc-form-field-icon-prefix { - padding: 0 0 0 4px; -} - -.mat-mdc-form-field-subscript-wrapper .mat-icon, -.mat-mdc-form-field label .mat-icon { - width: 1em; - height: 1em; - font-size: inherit; -} - -.mat-mdc-form-field-infix { - flex: auto; - min-width: 0; - width: 180px; - position: relative; - box-sizing: border-box; -} -.mat-mdc-form-field-infix:has(textarea[cols]) { - width: auto; -} - -.mat-mdc-form-field .mdc-notched-outline__notch { - margin-left: -1px; - -webkit-clip-path: inset(-9em -999em -9em 1px); - clip-path: inset(-9em -999em -9em 1px); -} -[dir=rtl] .mat-mdc-form-field .mdc-notched-outline__notch { - margin-left: 0; - margin-right: -1px; - -webkit-clip-path: inset(-9em 1px -9em -999em); - clip-path: inset(-9em 1px -9em -999em); -} - -.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-floating-label { - transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1), color 150ms cubic-bezier(0.4, 0, 0.2, 1); -} -.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input { - transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1); -} -.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input::placeholder { - transition: opacity 67ms cubic-bezier(0.4, 0, 0.2, 1); -} -.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input::-moz-placeholder { - transition: opacity 67ms cubic-bezier(0.4, 0, 0.2, 1); -} -.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input::-webkit-input-placeholder { - transition: opacity 67ms cubic-bezier(0.4, 0, 0.2, 1); -} -.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field__input:-ms-input-placeholder { - transition: opacity 67ms cubic-bezier(0.4, 0, 0.2, 1); -} -.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input::placeholder, .mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input::placeholder { - transition-delay: 40ms; - transition-duration: 110ms; -} -.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input::-moz-placeholder, .mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input::-moz-placeholder { - transition-delay: 40ms; - transition-duration: 110ms; -} -.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input::-webkit-input-placeholder, .mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input::-webkit-input-placeholder { - transition-delay: 40ms; - transition-duration: 110ms; -} -.mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--no-label .mdc-text-field__input:-ms-input-placeholder, .mat-mdc-form-field.mat-form-field-animations-enabled.mdc-text-field--focused .mdc-text-field__input:-ms-input-placeholder { - transition-delay: 40ms; - transition-duration: 110ms; -} -.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-text-field--filled:not(.mdc-ripple-upgraded):focus .mdc-text-field__ripple::before { - transition-duration: 75ms; -} -.mat-mdc-form-field.mat-form-field-animations-enabled .mdc-line-ripple::after { - transition: transform 180ms cubic-bezier(0.4, 0, 0.2, 1), opacity 180ms cubic-bezier(0.4, 0, 0.2, 1); -} -.mat-mdc-form-field.mat-form-field-animations-enabled .mat-mdc-form-field-hint-wrapper, -.mat-mdc-form-field.mat-form-field-animations-enabled .mat-mdc-form-field-error-wrapper { - animation-duration: 300ms; -} - -.mdc-notched-outline .mdc-floating-label { - max-width: calc(100% + 1px); -} - -.mdc-notched-outline--upgraded .mdc-floating-label--float-above { - max-width: calc(133.3333333333% + 1px); -} -`],encapsulation:2,changeDetection:0})}return n})();var oo=["*",[["mat-option"],["ng-container"]]],ro=["*","mat-option, ng-container"],so=["text"],lo=[[["mat-icon"]],"*"],co=["mat-icon","*"];function mo(n,s){if(n&1&&M(0,"mat-pseudo-checkbox",1),n&2){let t=p();D("disabled",t.disabled)("state",t.selected?"checked":"unchecked")}}function ho(n,s){if(n&1&&M(0,"mat-pseudo-checkbox",3),n&2){let t=p();D("disabled",t.disabled)}}function po(n,s){if(n&1&&(l(0,"span",4),E(1),d()),n&2){let t=p();c(),ft("(",t.group.label,")")}}var ue=new w("MAT_OPTION_PARENT_COMPONENT"),_e=new w("MatOptgroup"),je=(()=>{class n{label;disabled=!1;_labelId=r(U).getId("mat-optgroup-label-");_inert;constructor(){let t=r(ue,{optional:!0});this._inert=t?.inertGroups??!1}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-optgroup"]],hostAttrs:[1,"mat-mdc-optgroup"],hostVars:3,hostBindings:function(e,i){e&2&&T("role",i._inert?null:"group")("aria-disabled",i._inert?null:i.disabled.toString())("aria-labelledby",i._inert?null:i._labelId)},inputs:{label:"label",disabled:[2,"disabled","disabled",S]},exportAs:["matOptgroup"],features:[$([{provide:_e,useExisting:n}])],ngContentSelectors:ro,decls:5,vars:4,consts:[["role","presentation",1,"mat-mdc-optgroup-label",3,"id"],[1,"mdc-list-item__primary-text"]],template:function(e,i){e&1&&(B(oo),kt(0,"span",0)(1,"span",1),E(2),y(3),Ot()(),y(4,1)),e&2&&(_("mdc-list-item--disabled",i.disabled),Rt("id",i._labelId),c(2),ft("",i.label," "))},styles:[`.mat-mdc-optgroup { - color: var(--mat-optgroup-label-text-color, var(--mat-sys-on-surface-variant)); - font-family: var(--mat-optgroup-label-text-font, var(--mat-sys-title-small-font)); - line-height: var(--mat-optgroup-label-text-line-height, var(--mat-sys-title-small-line-height)); - font-size: var(--mat-optgroup-label-text-size, var(--mat-sys-title-small-size)); - letter-spacing: var(--mat-optgroup-label-text-tracking, var(--mat-sys-title-small-tracking)); - font-weight: var(--mat-optgroup-label-text-weight, var(--mat-sys-title-small-weight)); -} - -.mat-mdc-optgroup-label { - display: flex; - position: relative; - align-items: center; - justify-content: flex-start; - overflow: hidden; - min-height: 48px; - padding: 0 16px; - outline: none; -} -.mat-mdc-optgroup-label.mdc-list-item--disabled { - opacity: 0.38; -} -.mat-mdc-optgroup-label .mdc-list-item__primary-text { - font-size: inherit; - font-weight: inherit; - letter-spacing: inherit; - line-height: inherit; - font-family: inherit; - text-decoration: inherit; - text-transform: inherit; - white-space: normal; - color: inherit; -} -`],encapsulation:2,changeDetection:0})}return n})(),Ne=class{source;isUserInput;constructor(s,t=!1){this.source=s,this.isUserInput=t}},zt=(()=>{class n{_element=r(O);_changeDetectorRef=r(W);_parent=r(ue,{optional:!0});group=r(_e,{optional:!0});_signalDisableRipple=!1;_selected=!1;_active=!1;_mostRecentViewValue="";get multiple(){return this._parent&&this._parent.multiple}get selected(){return this._selected}value;id=r(U).getId("mat-option-");get disabled(){return this.group&&this.group.disabled||this._disabled()}set disabled(t){this._disabled.set(t)}_disabled=Z(!1);get disableRipple(){return this._signalDisableRipple?this._parent.disableRipple():!!this._parent?.disableRipple}get hideSingleSelectionIndicator(){return!!(this._parent&&this._parent.hideSingleSelectionIndicator)}onSelectionChange=new F;_text;_stateChanges=new k;constructor(){let t=r(wt);t.load(Ct),t.load(zi),this._signalDisableRipple=!!this._parent&&oi(this._parent.disableRipple)}get active(){return this._active}get viewValue(){return(this._text?.nativeElement.textContent||"").trim()}select(t=!0){this._selected||(this._selected=!0,this._changeDetectorRef.markForCheck(),t&&this._emitSelectionChangeEvent())}deselect(t=!0){this._selected&&(this._selected=!1,this._changeDetectorRef.markForCheck(),t&&this._emitSelectionChangeEvent())}focus(t,e){let i=this._getHostElement();typeof i.focus=="function"&&i.focus(e)}setActiveStyles(){this._active||(this._active=!0,this._changeDetectorRef.markForCheck())}setInactiveStyles(){this._active&&(this._active=!1,this._changeDetectorRef.markForCheck())}getLabel(){return this.viewValue}_handleKeydown(t){(t.keyCode===13||t.keyCode===32)&&!ct(t)&&(this._selectViaInteraction(),t.preventDefault())}_selectViaInteraction(){this.disabled||(this._selected=this.multiple?!this._selected:!0,this._changeDetectorRef.markForCheck(),this._emitSelectionChangeEvent(!0))}_getTabIndex(){return this.disabled?"-1":"0"}_getHostElement(){return this._element.nativeElement}ngAfterViewChecked(){if(this._selected){let t=this.viewValue;t!==this._mostRecentViewValue&&(this._mostRecentViewValue&&this._stateChanges.next(),this._mostRecentViewValue=t)}}ngOnDestroy(){this._stateChanges.complete()}_emitSelectionChangeEvent(t=!1){this.onSelectionChange.emit(new Ne(this,t))}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-option"]],viewQuery:function(e,i){if(e&1&&V(so,7),e&2){let a;m(a=h())&&(i._text=a.first)}},hostAttrs:["role","option",1,"mat-mdc-option","mdc-list-item"],hostVars:11,hostBindings:function(e,i){e&1&&u("click",function(){return i._selectViaInteraction()})("keydown",function(o){return i._handleKeydown(o)}),e&2&&(Rt("id",i.id),T("aria-selected",i.selected)("aria-disabled",i.disabled.toString()),_("mdc-list-item--selected",i.selected)("mat-mdc-option-multiple",i.multiple)("mat-mdc-option-active",i.active)("mdc-list-item--disabled",i.disabled))},inputs:{value:"value",id:"id",disabled:[2,"disabled","disabled",S]},outputs:{onSelectionChange:"onSelectionChange"},exportAs:["matOption"],ngContentSelectors:co,decls:8,vars:5,consts:[["text",""],["aria-hidden","true",1,"mat-mdc-option-pseudo-checkbox",3,"disabled","state"],[1,"mdc-list-item__primary-text"],["state","checked","aria-hidden","true","appearance","minimal",1,"mat-mdc-option-pseudo-checkbox",3,"disabled"],[1,"cdk-visually-hidden"],["aria-hidden","true","mat-ripple","",1,"mat-mdc-option-ripple","mat-focus-indicator",3,"matRippleTrigger","matRippleDisabled"]],template:function(e,i){e&1&&(B(lo),g(0,mo,1,2,"mat-pseudo-checkbox",1),y(1),l(2,"span",2,0),y(4,1),d(),g(5,ho,1,1,"mat-pseudo-checkbox",3),g(6,po,2,1,"span",4),M(7,"div",5)),e&2&&(v(i.multiple?0:-1),c(5),v(!i.multiple&&i.selected&&!i.hideSingleSelectionIndicator?5:-1),c(),v(i.group&&i.group._inert?6:-1),c(),D("matRippleTrigger",i._getHostElement())("matRippleDisabled",i.disabled||i.disableRipple))},dependencies:[Ki,vt],styles:[`.mat-mdc-option { - -webkit-user-select: none; - user-select: none; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - display: flex; - position: relative; - align-items: center; - justify-content: flex-start; - overflow: hidden; - min-height: 48px; - padding: 0 16px; - cursor: pointer; - -webkit-tap-highlight-color: transparent; - color: var(--mat-option-label-text-color, var(--mat-sys-on-surface)); - font-family: var(--mat-option-label-text-font, var(--mat-sys-label-large-font)); - line-height: var(--mat-option-label-text-line-height, var(--mat-sys-label-large-line-height)); - font-size: var(--mat-option-label-text-size, var(--mat-sys-body-large-size)); - letter-spacing: var(--mat-option-label-text-tracking, var(--mat-sys-label-large-tracking)); - font-weight: var(--mat-option-label-text-weight, var(--mat-sys-body-large-weight)); -} -.mat-mdc-option:hover:not(.mdc-list-item--disabled) { - background-color: var(--mat-option-hover-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent)); -} -.mat-mdc-option:focus.mdc-list-item, .mat-mdc-option.mat-mdc-option-active.mdc-list-item { - background-color: var(--mat-option-focus-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent)); - outline: 0; -} -.mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-active, .mat-mdc-option-multiple, :focus, :hover) { - background-color: var(--mat-option-selected-state-layer-color, var(--mat-sys-secondary-container)); -} -.mat-mdc-option.mdc-list-item--selected:not(.mdc-list-item--disabled):not(.mat-mdc-option-active, .mat-mdc-option-multiple, :focus, :hover) .mdc-list-item__primary-text { - color: var(--mat-option-selected-state-label-text-color, var(--mat-sys-on-secondary-container)); -} -.mat-mdc-option .mat-pseudo-checkbox { - --mat-pseudo-checkbox-minimal-selected-checkmark-color: var(--mat-option-selected-state-label-text-color, var(--mat-sys-on-secondary-container)); -} -.mat-mdc-option.mdc-list-item { - align-items: center; - background: transparent; -} -.mat-mdc-option.mdc-list-item--disabled { - cursor: default; - pointer-events: none; -} -.mat-mdc-option.mdc-list-item--disabled .mat-mdc-option-pseudo-checkbox, .mat-mdc-option.mdc-list-item--disabled .mdc-list-item__primary-text, .mat-mdc-option.mdc-list-item--disabled > mat-icon { - opacity: 0.38; -} -.mat-mdc-optgroup .mat-mdc-option:not(.mat-mdc-option-multiple) { - padding-left: 32px; -} -[dir=rtl] .mat-mdc-optgroup .mat-mdc-option:not(.mat-mdc-option-multiple) { - padding-left: 16px; - padding-right: 32px; -} -.mat-mdc-option .mat-icon, -.mat-mdc-option .mat-pseudo-checkbox-full { - margin-right: 16px; - flex-shrink: 0; -} -[dir=rtl] .mat-mdc-option .mat-icon, -[dir=rtl] .mat-mdc-option .mat-pseudo-checkbox-full { - margin-right: 0; - margin-left: 16px; -} -.mat-mdc-option .mat-pseudo-checkbox-minimal { - margin-left: 16px; - flex-shrink: 0; -} -[dir=rtl] .mat-mdc-option .mat-pseudo-checkbox-minimal { - margin-right: 16px; - margin-left: 0; -} -.mat-mdc-option .mat-mdc-option-ripple { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - pointer-events: none; -} -.mat-mdc-option .mdc-list-item__primary-text { - white-space: normal; - font-size: inherit; - font-weight: inherit; - letter-spacing: inherit; - line-height: inherit; - font-family: inherit; - text-decoration: inherit; - text-transform: inherit; - margin-right: auto; -} -[dir=rtl] .mat-mdc-option .mdc-list-item__primary-text { - margin-right: 0; - margin-left: auto; -} -@media (forced-colors: active) { - .mat-mdc-option.mdc-list-item--selected:not(:has(.mat-mdc-option-pseudo-checkbox))::after { - content: ""; - position: absolute; - top: 50%; - right: 16px; - transform: translateY(-50%); - width: 10px; - height: 0; - border-bottom: solid 10px; - border-radius: 10px; - } - [dir=rtl] .mat-mdc-option.mdc-list-item--selected:not(:has(.mat-mdc-option-pseudo-checkbox))::after { - right: auto; - left: 16px; - } -} - -.mat-mdc-option-multiple { - --mat-list-list-item-selected-container-color: var(--mat-list-list-item-container-color, transparent); -} - -.mat-mdc-option-active .mat-focus-indicator::before { - content: ""; -} -`],encapsulation:2,changeDetection:0})}return n})();function Dn(n,s,t){if(t.length){let e=s.toArray(),i=t.toArray(),a=0;for(let o=0;ot+e?Math.max(0,n-e+s):t}var Fn=(()=>{class n{isErrorState(t,e){return!!(t&&t.invalid&&(t.touched||e&&e.submitted))}static \u0275fac=function(e){return new(e||n)};static \u0275prov=P({token:n,factory:n.\u0275fac,providedIn:"root"})}return n})();var be=class{_defaultMatcher;ngControl;_parentFormGroup;_parentForm;_stateChanges;errorState=!1;matcher;constructor(s,t,e,i,a){this._defaultMatcher=s,this.ngControl=t,this._parentFormGroup=e,this._parentForm=i,this._stateChanges=a}updateErrorState(){let s=this.errorState,t=this._parentFormGroup||this._parentForm,e=this.matcher||this._defaultMatcher,i=this.ngControl?this.ngControl.control:null,a=e?.isErrorState(i,t)??!1;a!==s&&(this.errorState=a,this._stateChanges.next())}};var On=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({imports:[ce,In,j]})}return n})();var ge=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({imports:[j]})}return n})();var Nt=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({imports:[Kt,ge,zt,j]})}return n})();var bo=["trigger"],go=["panel"],vo=[[["mat-select-trigger"]],"*"],yo=["mat-select-trigger","*"];function xo(n,s){if(n&1&&(l(0,"span",4),E(1),d()),n&2){let t=p();c(),pt(t.placeholder)}}function ko(n,s){n&1&&y(0)}function wo(n,s){if(n&1&&(l(0,"span",11),E(1),d()),n&2){let t=p(2);c(),pt(t.triggerValue)}}function Co(n,s){if(n&1&&(l(0,"span",5),g(1,ko,1,0)(2,wo,2,1,"span",11),d()),n&2){let t=p();c(),v(t.customTrigger?1:2)}}function To(n,s){if(n&1){let t=lt();l(0,"div",12,1),u("keydown",function(i){R(t);let a=p();return A(a._handleKeydown(i))}),y(2,1),d()}if(n&2){let t=p();bt(t.panelClass),_("mat-select-panel-animations-enabled",!t._animationsDisabled)("mat-primary",(t._parentFormField==null?null:t._parentFormField.color)==="primary")("mat-accent",(t._parentFormField==null?null:t._parentFormField.color)==="accent")("mat-warn",(t._parentFormField==null?null:t._parentFormField.color)==="warn")("mat-undefined",!(t._parentFormField!=null&&t._parentFormField.color)),T("id",t.id+"-panel")("aria-multiselectable",t.multiple)("aria-label",t.ariaLabel||null)("aria-labelledby",t._getPanelAriaLabelledby())}}var Mo=new w("mat-select-scroll-strategy",{providedIn:"root",factory:()=>{let n=r(G);return()=>re(n)}}),So=new w("MAT_SELECT_CONFIG"),Ln=new w("MatSelectTrigger"),Ve=class{source;value;constructor(s,t){this.source=s,this.value=t}},Pn=(()=>{class n{_viewportRuler=r(ne);_changeDetectorRef=r(W);_elementRef=r(O);_dir=r(ut,{optional:!0});_idGenerator=r(U);_renderer=r(st);_parentFormField=r(ze,{optional:!0});ngControl=r(Ai,{self:!0,optional:!0});_liveAnnouncer=r(de);_defaultOptions=r(So,{optional:!0});_animationsDisabled=Q();_popoverLocation;_initialized=new k;_cleanupDetach;options;optionGroups;customTrigger;_positions=[{originX:"start",originY:"bottom",overlayX:"start",overlayY:"top"},{originX:"end",originY:"bottom",overlayX:"end",overlayY:"top"},{originX:"start",originY:"top",overlayX:"start",overlayY:"bottom",panelClass:"mat-mdc-select-panel-above"},{originX:"end",originY:"top",overlayX:"end",overlayY:"bottom",panelClass:"mat-mdc-select-panel-above"}];_scrollOptionIntoView(t){let e=this.options.toArray()[t];if(e){let i=this.panel.nativeElement,a=Dn(t,this.options,this.optionGroups),o=e._getHostElement();t===0&&a===1?i.scrollTop=0:i.scrollTop=En(o.offsetTop,o.offsetHeight,i.scrollTop,i.offsetHeight)}}_positioningSettled(){this._scrollOptionIntoView(this._keyManager.activeItemIndex||0)}_getChangeEvent(t){return new Ve(this,t)}_scrollStrategyFactory=r(Mo);_panelOpen=!1;_compareWith=(t,e)=>t===e;_uid=this._idGenerator.getId("mat-select-");_triggerAriaLabelledBy=null;_previousControl;_destroy=new k;_errorStateTracker;stateChanges=new k;disableAutomaticLabeling=!0;userAriaDescribedBy;_selectionModel;_keyManager;_preferredOverlayOrigin;_overlayWidth;_onChange=()=>{};_onTouched=()=>{};_valueId=this._idGenerator.getId("mat-select-value-");_scrollStrategy;_overlayPanelClass=this._defaultOptions?.overlayPanelClass||"";get focused(){return this._focused||this._panelOpen}_focused=!1;controlType="mat-select";trigger;panel;_overlayDir;panelClass;disabled=!1;get disableRipple(){return this._disableRipple()}set disableRipple(t){this._disableRipple.set(t)}_disableRipple=Z(!1);tabIndex=0;get hideSingleSelectionIndicator(){return this._hideSingleSelectionIndicator}set hideSingleSelectionIndicator(t){this._hideSingleSelectionIndicator=t,this._syncParentProperties()}_hideSingleSelectionIndicator=this._defaultOptions?.hideSingleSelectionIndicator??!1;get placeholder(){return this._placeholder}set placeholder(t){this._placeholder=t,this.stateChanges.next()}_placeholder;get required(){return this._required??this.ngControl?.control?.hasValidator(Ri.required)??!1}set required(t){this._required=t,this.stateChanges.next()}_required;get multiple(){return this._multiple}set multiple(t){this._selectionModel,this._multiple=t}_multiple=!1;disableOptionCentering=this._defaultOptions?.disableOptionCentering??!1;get compareWith(){return this._compareWith}set compareWith(t){this._compareWith=t,this._selectionModel&&this._initializeSelection()}get value(){return this._value}set value(t){this._assignValue(t)&&this._onChange(t)}_value;ariaLabel="";ariaLabelledby;get errorStateMatcher(){return this._errorStateTracker.matcher}set errorStateMatcher(t){this._errorStateTracker.matcher=t}typeaheadDebounceInterval;sortComparator;get id(){return this._id}set id(t){this._id=t||this._uid,this.stateChanges.next()}_id;get errorState(){return this._errorStateTracker.errorState}set errorState(t){this._errorStateTracker.errorState=t}panelWidth=this._defaultOptions&&typeof this._defaultOptions.panelWidth<"u"?this._defaultOptions.panelWidth:"auto";canSelectNullableOptions=this._defaultOptions?.canSelectNullableOptions??!1;optionSelectionChanges=Ye(()=>{let t=this.options;return t?t.changes.pipe(nt(t),$t(()=>J(...t.map(e=>e.onSelectionChange)))):this._initialized.pipe($t(()=>this.optionSelectionChanges))});openedChange=new F;_openedStream=this.openedChange.pipe(dt(t=>t),Ht(()=>{}));_closedStream=this.openedChange.pipe(dt(t=>!t),Ht(()=>{}));selectionChange=new F;valueChange=new F;constructor(){let t=r(Fn),e=r(Li,{optional:!0}),i=r(Pi,{optional:!0}),a=r(new ie("tabindex"),{optional:!0}),o=r(Oi,{optional:!0});this.ngControl&&(this.ngControl.valueAccessor=this),this._defaultOptions?.typeaheadDebounceInterval!=null&&(this.typeaheadDebounceInterval=this._defaultOptions.typeaheadDebounceInterval),this._errorStateTracker=new be(t,this.ngControl,i,e,this.stateChanges),this._scrollStrategy=this._scrollStrategyFactory(),this.tabIndex=a==null?0:parseInt(a)||0,this._popoverLocation=o?.usePopover===!1?null:"inline",this.id=this.id}ngOnInit(){this._selectionModel=new Yi(this.multiple),this.stateChanges.next(),this._viewportRuler.change().pipe(I(this._destroy)).subscribe(()=>{this.panelOpen&&(this._overlayWidth=this._getOverlayWidth(this._preferredOverlayOrigin),this._changeDetectorRef.detectChanges())})}ngAfterContentInit(){this._initialized.next(),this._initialized.complete(),this._initKeyManager(),this._selectionModel.changed.pipe(I(this._destroy)).subscribe(t=>{t.added.forEach(e=>e.select()),t.removed.forEach(e=>e.deselect())}),this.options.changes.pipe(nt(null),I(this._destroy)).subscribe(()=>{this._resetOptions(),this._initializeSelection()})}ngDoCheck(){let t=this._getTriggerAriaLabelledby(),e=this.ngControl;if(t!==this._triggerAriaLabelledBy){let i=this._elementRef.nativeElement;this._triggerAriaLabelledBy=t,t?i.setAttribute("aria-labelledby",t):i.removeAttribute("aria-labelledby")}e&&(this._previousControl!==e.control&&(this._previousControl!==void 0&&e.disabled!==null&&e.disabled!==this.disabled&&(this.disabled=e.disabled),this._previousControl=e.control),this.updateErrorState())}ngOnChanges(t){(t.disabled||t.userAriaDescribedBy)&&this.stateChanges.next(),t.typeaheadDebounceInterval&&this._keyManager&&this._keyManager.withTypeAhead(this.typeaheadDebounceInterval),t.panelClass&&this.panelClass instanceof Set&&(this.panelClass=Array.from(this.panelClass))}ngOnDestroy(){this._cleanupDetach?.(),this._keyManager?.destroy(),this._destroy.next(),this._destroy.complete(),this.stateChanges.complete(),this._clearFromModal()}toggle(){this.panelOpen?this.close():this.open()}open(){this._canOpen()&&(this._parentFormField&&(this._preferredOverlayOrigin=this._parentFormField.getConnectedOverlayOrigin()),this._cleanupDetach?.(),this._overlayWidth=this._getOverlayWidth(this._preferredOverlayOrigin),this._applyModalPanelOwnership(),this._panelOpen=!0,this._overlayDir.positionChange.pipe(Xe(1)).subscribe(()=>{this._changeDetectorRef.detectChanges(),this._positioningSettled()}),this._overlayDir.attachOverlay(),this._keyManager.withHorizontalOrientation(null),this._highlightCorrectOption(),this._changeDetectorRef.markForCheck(),this.stateChanges.next(),Promise.resolve().then(()=>this.openedChange.emit(!0)))}_trackedModal=null;_applyModalPanelOwnership(){let t=this._elementRef.nativeElement.closest('body > .cdk-overlay-container [aria-modal="true"]');if(!t)return;let e=`${this.id}-panel`;this._trackedModal&&Ie(this._trackedModal,"aria-owns",e),Wi(t,"aria-owns",e),this._trackedModal=t}_clearFromModal(){if(!this._trackedModal)return;let t=`${this.id}-panel`;Ie(this._trackedModal,"aria-owns",t),this._trackedModal=null}close(){this._panelOpen&&(this._panelOpen=!1,this._exitAndDetach(),this._keyManager.withHorizontalOrientation(this._isRtl()?"rtl":"ltr"),this._changeDetectorRef.markForCheck(),this._onTouched(),this.stateChanges.next(),Promise.resolve().then(()=>this.openedChange.emit(!1)))}_exitAndDetach(){if(this._animationsDisabled||!this.panel){this._detachOverlay();return}this._cleanupDetach?.(),this._cleanupDetach=()=>{e(),clearTimeout(i),this._cleanupDetach=void 0};let t=this.panel.nativeElement,e=this._renderer.listen(t,"animationend",a=>{a.animationName==="_mat-select-exit"&&(this._cleanupDetach?.(),this._detachOverlay())}),i=setTimeout(()=>{this._cleanupDetach?.(),this._detachOverlay()},200);t.classList.add("mat-select-panel-exit")}_detachOverlay(){this._overlayDir.detachOverlay(),this._changeDetectorRef.markForCheck()}writeValue(t){this._assignValue(t)}registerOnChange(t){this._onChange=t}registerOnTouched(t){this._onTouched=t}setDisabledState(t){this.disabled=t,this._changeDetectorRef.markForCheck(),this.stateChanges.next()}get panelOpen(){return this._panelOpen}get selected(){return this.multiple?this._selectionModel?.selected||[]:this._selectionModel?.selected[0]}get triggerValue(){if(this.empty)return"";if(this._multiple){let t=this._selectionModel.selected.map(e=>e.viewValue);return this._isRtl()&&t.reverse(),t.join(", ")}return this._selectionModel.selected[0].viewValue}updateErrorState(){this._errorStateTracker.updateErrorState()}_isRtl(){return this._dir?this._dir.value==="rtl":!1}_handleKeydown(t){this.disabled||(this.panelOpen?this._handleOpenKeydown(t):this._handleClosedKeydown(t))}_handleClosedKeydown(t){let e=t.keyCode,i=e===40||e===38||e===37||e===39,a=e===13||e===32,o=this._keyManager;if(!o.isTyping()&&a&&!ct(t)||(this.multiple||t.altKey)&&i)t.preventDefault(),this.open();else if(!this.multiple){let f=this.selected;o.onKeydown(t);let b=this.selected;b&&f!==b&&this._liveAnnouncer.announce(b.viewValue,1e4)}}_handleOpenKeydown(t){let e=this._keyManager,i=t.keyCode,a=i===40||i===38,o=e.isTyping();if(a&&t.altKey)t.preventDefault(),this.close();else if(!o&&(i===13||i===32)&&e.activeItem&&!ct(t))t.preventDefault(),e.activeItem._selectViaInteraction();else if(!o&&this._multiple&&i===65&&t.ctrlKey){t.preventDefault();let f=this.options.some(b=>!b.disabled&&!b.selected);this.options.forEach(b=>{b.disabled||(f?b.select():b.deselect())})}else{let f=e.activeItemIndex;e.onKeydown(t),this._multiple&&a&&t.shiftKey&&e.activeItem&&e.activeItemIndex!==f&&e.activeItem._selectViaInteraction()}}_handleOverlayKeydown(t){t.keyCode===27&&!ct(t)&&(t.preventDefault(),this.close())}_onFocus(){this.disabled||(this._focused=!0,this.stateChanges.next())}_onBlur(){this._focused=!1,this._keyManager?.cancelTypeahead(),!this.disabled&&!this.panelOpen&&(this._onTouched(),this._changeDetectorRef.markForCheck(),this.stateChanges.next())}get empty(){return!this._selectionModel||this._selectionModel.isEmpty()}_initializeSelection(){Promise.resolve().then(()=>{this.ngControl&&(this._value=this.ngControl.value),this._setSelectionByValue(this._value),this.stateChanges.next()})}_setSelectionByValue(t){if(this.options.forEach(e=>e.setInactiveStyles()),this._selectionModel.clear(),this.multiple&&t)Array.isArray(t),t.forEach(e=>this._selectOptionByValue(e)),this._sortValues();else{let e=this._selectOptionByValue(t);e?this._keyManager.updateActiveItem(e):this.panelOpen||this._keyManager.updateActiveItem(-1)}this._changeDetectorRef.markForCheck()}_selectOptionByValue(t){let e=this.options.find(i=>{if(this._selectionModel.isSelected(i))return!1;try{return(i.value!=null||this.canSelectNullableOptions)&&this._compareWith(i.value,t)}catch{return!1}});return e&&this._selectionModel.select(e),e}_assignValue(t){return t!==this._value||this._multiple&&Array.isArray(t)?(this.options&&this._setSelectionByValue(t),this._value=t,!0):!1}_skipPredicate=t=>this.panelOpen?!1:t.disabled;_getOverlayWidth(t){return this.panelWidth==="auto"?(t instanceof Me?t.elementRef:t||this._elementRef).nativeElement.getBoundingClientRect().width:this.panelWidth===null?"":this.panelWidth}_syncParentProperties(){if(this.options)for(let t of this.options)t._changeDetectorRef.markForCheck()}_initKeyManager(){this._keyManager=new Hi(this.options).withTypeAhead(this.typeaheadDebounceInterval).withVerticalOrientation().withHorizontalOrientation(this._isRtl()?"rtl":"ltr").withHomeAndEnd().withPageUpDown().withAllowedModifierKeys(["shiftKey"]).skipPredicate(this._skipPredicate),this._keyManager.tabOut.subscribe(()=>{this.panelOpen&&(!this.multiple&&this._keyManager.activeItem&&this._keyManager.activeItem._selectViaInteraction(),this.focus(),this.close())}),this._keyManager.change.subscribe(()=>{this._panelOpen&&this.panel?this._scrollOptionIntoView(this._keyManager.activeItemIndex||0):!this._panelOpen&&!this.multiple&&this._keyManager.activeItem&&this._keyManager.activeItem._selectViaInteraction()})}_resetOptions(){let t=J(this.options.changes,this._destroy);this.optionSelectionChanges.pipe(I(t)).subscribe(e=>{this._onSelect(e.source,e.isUserInput),e.isUserInput&&!this.multiple&&this._panelOpen&&(this.close(),this.focus())}),J(...this.options.map(e=>e._stateChanges)).pipe(I(t)).subscribe(()=>{this._changeDetectorRef.detectChanges(),this.stateChanges.next()})}_onSelect(t,e){let i=this._selectionModel.isSelected(t);!this.canSelectNullableOptions&&t.value==null&&!this._multiple?(t.deselect(),this._selectionModel.clear(),this.value!=null&&this._propagateChanges(t.value)):(i!==t.selected&&(t.selected?this._selectionModel.select(t):this._selectionModel.deselect(t)),e&&this._keyManager.setActiveItem(t),this.multiple&&(this._sortValues(),e&&this.focus())),i!==this._selectionModel.isSelected(t)&&this._propagateChanges(),this.stateChanges.next()}_sortValues(){if(this.multiple){let t=this.options.toArray();this._selectionModel.sort((e,i)=>this.sortComparator?this.sortComparator(e,i,t):t.indexOf(e)-t.indexOf(i)),this.stateChanges.next()}}_propagateChanges(t){let e;this.multiple?e=this.selected.map(i=>i.value):e=this.selected?this.selected.value:t,this._value=e,this.valueChange.emit(e),this._onChange(e),this.selectionChange.emit(this._getChangeEvent(e)),this._changeDetectorRef.markForCheck()}_highlightCorrectOption(){if(this._keyManager)if(this.empty){let t=-1;for(let e=0;e0&&!!this._overlayDir}focus(t){this._elementRef.nativeElement.focus(t)}_getPanelAriaLabelledby(){if(this.ariaLabel)return null;let t=this._parentFormField?.getLabelId()||null,e=t?t+" ":"";return this.ariaLabelledby?e+this.ariaLabelledby:t}_getAriaActiveDescendant(){return this.panelOpen&&this._keyManager&&this._keyManager.activeItem?this._keyManager.activeItem.id:null}_getTriggerAriaLabelledby(){if(this.ariaLabel)return null;let t=this._parentFormField?.getLabelId()||"";return this.ariaLabelledby&&(t+=" "+this.ariaLabelledby),t||(t=this._valueId),t}get describedByIds(){return this._elementRef.nativeElement.getAttribute("aria-describedby")?.split(" ")||[]}setDescribedByIds(t){let e=this._elementRef.nativeElement;t.length?e.setAttribute("aria-describedby",t.join(" ")):e.removeAttribute("aria-describedby")}onContainerClick(t){let e=wi(t);e&&(e.tagName==="MAT-OPTION"||e.classList.contains("cdk-overlay-backdrop")||e.closest(".mat-mdc-select-panel"))||(this.focus(),this.open())}get shouldLabelFloat(){return this.panelOpen||!this.empty||this.focused&&!!this.placeholder}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-select"]],contentQueries:function(e,i,a){if(e&1&&et(a,Ln,5)(a,zt,5)(a,_e,5),e&2){let o;m(o=h())&&(i.customTrigger=o.first),m(o=h())&&(i.options=o),m(o=h())&&(i.optionGroups=o)}},viewQuery:function(e,i){if(e&1&&V(bo,5)(go,5)(Se,5),e&2){let a;m(a=h())&&(i.trigger=a.first),m(a=h())&&(i.panel=a.first),m(a=h())&&(i._overlayDir=a.first)}},hostAttrs:["role","combobox","aria-haspopup","listbox",1,"mat-mdc-select"],hostVars:21,hostBindings:function(e,i){e&1&&u("keydown",function(o){return i._handleKeydown(o)})("focus",function(){return i._onFocus()})("blur",function(){return i._onBlur()}),e&2&&(T("id",i.id)("tabindex",i.disabled?-1:i.tabIndex)("aria-controls",i.panelOpen?i.id+"-panel":null)("aria-expanded",i.panelOpen)("aria-label",i.ariaLabel||null)("aria-required",i.required.toString())("aria-disabled",i.disabled.toString())("aria-invalid",i.errorState)("aria-activedescendant",i._getAriaActiveDescendant()),_("mat-mdc-select-disabled",i.disabled)("mat-mdc-select-invalid",i.errorState)("mat-mdc-select-required",i.required)("mat-mdc-select-empty",i.empty)("mat-mdc-select-multiple",i.multiple)("mat-select-open",i.panelOpen))},inputs:{userAriaDescribedBy:[0,"aria-describedby","userAriaDescribedBy"],panelClass:"panelClass",disabled:[2,"disabled","disabled",S],disableRipple:[2,"disableRipple","disableRipple",S],tabIndex:[2,"tabIndex","tabIndex",t=>t==null?0:gt(t)],hideSingleSelectionIndicator:[2,"hideSingleSelectionIndicator","hideSingleSelectionIndicator",S],placeholder:"placeholder",required:[2,"required","required",S],multiple:[2,"multiple","multiple",S],disableOptionCentering:[2,"disableOptionCentering","disableOptionCentering",S],compareWith:"compareWith",value:"value",ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],errorStateMatcher:"errorStateMatcher",typeaheadDebounceInterval:[2,"typeaheadDebounceInterval","typeaheadDebounceInterval",gt],sortComparator:"sortComparator",id:"id",panelWidth:"panelWidth",canSelectNullableOptions:[2,"canSelectNullableOptions","canSelectNullableOptions",S]},outputs:{openedChange:"openedChange",_openedStream:"opened",_closedStream:"closed",selectionChange:"selectionChange",valueChange:"valueChange"},exportAs:["matSelect"],features:[$([{provide:Be,useExisting:n},{provide:ue,useExisting:n}]),Wt],ngContentSelectors:yo,decls:11,vars:10,consts:[["fallbackOverlayOrigin","cdkOverlayOrigin","trigger",""],["panel",""],["cdk-overlay-origin","",1,"mat-mdc-select-trigger",3,"click"],[1,"mat-mdc-select-value"],[1,"mat-mdc-select-placeholder","mat-mdc-select-min-line"],[1,"mat-mdc-select-value-text"],[1,"mat-mdc-select-arrow-wrapper"],[1,"mat-mdc-select-arrow"],["viewBox","0 0 24 24","width","24px","height","24px","focusable","false","aria-hidden","true"],["d","M7 10l5 5 5-5z"],["cdk-connected-overlay","","cdkConnectedOverlayHasBackdrop","","cdkConnectedOverlayBackdropClass","cdk-overlay-transparent-backdrop",3,"detach","backdropClick","overlayKeydown","cdkConnectedOverlayDisableClose","cdkConnectedOverlayPanelClass","cdkConnectedOverlayScrollStrategy","cdkConnectedOverlayOrigin","cdkConnectedOverlayPositions","cdkConnectedOverlayWidth","cdkConnectedOverlayFlexibleDimensions","cdkConnectedOverlayUsePopover"],[1,"mat-mdc-select-min-line"],["role","listbox","tabindex","-1",1,"mat-mdc-select-panel","mdc-menu-surface","mdc-menu-surface--open",3,"keydown"]],template:function(e,i){if(e&1&&(B(vo),l(0,"div",2,0),u("click",function(){return i.open()}),l(3,"div",3),g(4,xo,2,1,"span",4)(5,Co,3,1,"span",5),d(),l(6,"div",6)(7,"div",7),mt(),l(8,"svg",8),M(9,"path",9),d()()()(),tt(10,To,3,16,"ng-template",10),u("detach",function(){return i.close()})("backdropClick",function(){return i.close()})("overlayKeydown",function(o){return i._handleOverlayKeydown(o)})),e&2){let a=ht(1);c(3),T("id",i._valueId),c(),v(i.empty?4:5),c(6),D("cdkConnectedOverlayDisableClose",!0)("cdkConnectedOverlayPanelClass",i._overlayPanelClass)("cdkConnectedOverlayScrollStrategy",i._scrollStrategy)("cdkConnectedOverlayOrigin",i._preferredOverlayOrigin||a)("cdkConnectedOverlayPositions",i._positions)("cdkConnectedOverlayWidth",i._overlayWidth)("cdkConnectedOverlayFlexibleDimensions",!0)("cdkConnectedOverlayUsePopover",i._popoverLocation)}},dependencies:[Me,Se],styles:[`@keyframes _mat-select-enter { - from { - opacity: 0; - transform: scaleY(0.8); - } - to { - opacity: 1; - transform: none; - } -} -@keyframes _mat-select-exit { - from { - opacity: 1; - } - to { - opacity: 0; - } -} -.mat-mdc-select { - display: inline-block; - width: 100%; - outline: none; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - color: var(--mat-select-enabled-trigger-text-color, var(--mat-sys-on-surface)); - font-family: var(--mat-select-trigger-text-font, var(--mat-sys-body-large-font)); - line-height: var(--mat-select-trigger-text-line-height, var(--mat-sys-body-large-line-height)); - font-size: var(--mat-select-trigger-text-size, var(--mat-sys-body-large-size)); - font-weight: var(--mat-select-trigger-text-weight, var(--mat-sys-body-large-weight)); - letter-spacing: var(--mat-select-trigger-text-tracking, var(--mat-sys-body-large-tracking)); -} - -div.mat-mdc-select-panel { - box-shadow: var(--mat-select-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12)); -} - -.mat-mdc-select-disabled { - color: var(--mat-select-disabled-trigger-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-mdc-select-disabled .mat-mdc-select-placeholder { - color: var(--mat-select-disabled-trigger-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} - -.mat-mdc-select-trigger { - display: inline-flex; - align-items: center; - cursor: pointer; - position: relative; - box-sizing: border-box; - width: 100%; -} -.mat-mdc-select-disabled .mat-mdc-select-trigger { - -webkit-user-select: none; - user-select: none; - cursor: default; -} - -.mat-mdc-select-value { - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.mat-mdc-select-value-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.mat-mdc-select-arrow-wrapper { - height: 24px; - flex-shrink: 0; - display: inline-flex; - align-items: center; -} -.mat-form-field-appearance-fill .mdc-text-field--no-label .mat-mdc-select-arrow-wrapper { - transform: none; -} - -.mat-mdc-form-field .mat-mdc-select.mat-mdc-select-invalid .mat-mdc-select-arrow, -.mat-form-field-invalid:not(.mat-form-field-disabled) .mat-mdc-form-field-infix::after { - color: var(--mat-select-invalid-arrow-color, var(--mat-sys-error)); -} - -.mat-mdc-select-arrow { - width: 10px; - height: 5px; - position: relative; - color: var(--mat-select-enabled-arrow-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-form-field.mat-focused .mat-mdc-select-arrow { - color: var(--mat-select-focused-arrow-color, var(--mat-sys-primary)); -} -.mat-mdc-form-field .mat-mdc-select.mat-mdc-select-disabled .mat-mdc-select-arrow { - color: var(--mat-select-disabled-arrow-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-select-open .mat-mdc-select-arrow { - transform: rotate(180deg); -} -.mat-form-field-animations-enabled .mat-mdc-select-arrow { - transition: transform 80ms linear; -} -.mat-mdc-select-arrow svg { - fill: currentColor; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} -@media (forced-colors: active) { - .mat-mdc-select-arrow svg { - fill: CanvasText; - } - .mat-mdc-select-disabled .mat-mdc-select-arrow svg { - fill: GrayText; - } -} - -div.mat-mdc-select-panel { - width: 100%; - max-height: 275px; - outline: 0; - overflow: auto; - padding: 8px 0; - box-sizing: border-box; - transform-origin: top center; - border-radius: 0 0 4px 4px; - position: relative; - background-color: var(--mat-select-panel-background-color, var(--mat-sys-surface-container)); -} -.mat-mdc-select-panel-above div.mat-mdc-select-panel { - border-radius: 4px 4px 0 0; - transform-origin: bottom center; -} -@media (forced-colors: active) { - div.mat-mdc-select-panel { - outline: solid 1px; - } -} - -.mat-select-panel-animations-enabled { - animation: _mat-select-enter 120ms cubic-bezier(0, 0, 0.2, 1); -} -.mat-select-panel-animations-enabled.mat-select-panel-exit { - animation: _mat-select-exit 100ms linear; -} - -.mat-mdc-select-placeholder { - transition: color 400ms 133.3333333333ms cubic-bezier(0.25, 0.8, 0.25, 1); - color: var(--mat-select-placeholder-text-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-form-field:not(.mat-form-field-animations-enabled) .mat-mdc-select-placeholder, ._mat-animation-noopable .mat-mdc-select-placeholder { - transition: none; -} -.mat-form-field-hide-placeholder .mat-mdc-select-placeholder { - color: transparent; - -webkit-text-fill-color: transparent; - transition: none; - display: block; -} - -.mat-mdc-form-field-type-mat-select:not(.mat-form-field-disabled) .mat-mdc-text-field-wrapper { - cursor: pointer; -} -.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-fill .mat-mdc-floating-label { - max-width: calc(100% - 18px); -} -.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-fill .mdc-floating-label--float-above { - max-width: calc(100% / 0.75 - 24px); -} -.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-outline .mdc-notched-outline__notch { - max-width: calc(100% - 60px); -} -.mat-mdc-form-field-type-mat-select.mat-form-field-appearance-outline .mdc-text-field--label-floating .mdc-notched-outline__notch { - max-width: calc(100% - 24px); -} - -.mat-mdc-select-min-line:empty::before { - content: " "; - white-space: pre; - width: 1px; - display: inline-block; - visibility: hidden; -} - -.mat-form-field-appearance-fill .mat-mdc-select-arrow-wrapper { - transform: var(--mat-select-arrow-transform, translateY(-8px)); -} -`],encapsulation:2,changeDetection:0})}return n})(),El=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["mat-select-trigger"]],features:[$([{provide:Ln,useExisting:n}])]})}return n})(),Bn=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({imports:[Pt,Nt,j,ae,On,Nt]})}return n})();var Rl=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({imports:[j]})}return n})();var Do=["mat-internal-form-field",""],Eo=["*"],Ll=(()=>{class n{labelPosition="after";static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["div","mat-internal-form-field",""]],hostAttrs:[1,"mdc-form-field","mat-internal-form-field"],hostVars:2,hostBindings:function(e,i){e&2&&_("mdc-form-field--align-end",i.labelPosition==="before")},inputs:{labelPosition:"labelPosition"},attrs:Do,ngContentSelectors:Eo,decls:1,vars:0,template:function(e,i){e&1&&(B(),y(0))},styles:[`.mat-internal-form-field { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - display: inline-flex; - align-items: center; - vertical-align: middle; -} -.mat-internal-form-field > label { - margin-left: 0; - margin-right: auto; - padding-left: 4px; - padding-right: 0; - order: 0; -} -[dir=rtl] .mat-internal-form-field > label { - margin-left: auto; - margin-right: 0; - padding-left: 0; - padding-right: 4px; -} - -.mdc-form-field--align-end > label { - margin-left: auto; - margin-right: 0; - padding-left: 0; - padding-right: 4px; - order: -1; -} -[dir=rtl] .mdc-form-field--align-end .mdc-form-field--align-end label { - margin-left: 0; - margin-right: auto; - padding-left: 4px; - padding-right: 0; -} -`],encapsulation:2,changeDetection:0})}return n})();var $e=(()=>{class n{get vertical(){return this._vertical}set vertical(t){this._vertical=K(t)}_vertical=!1;get inset(){return this._inset}set inset(t){this._inset=K(t)}_inset=!1;static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-divider"]],hostAttrs:["role","separator",1,"mat-divider"],hostVars:7,hostBindings:function(e,i){e&2&&(T("aria-orientation",i.vertical?"vertical":"horizontal"),_("mat-divider-vertical",i.vertical)("mat-divider-horizontal",!i.vertical)("mat-divider-inset",i.inset))},inputs:{vertical:"vertical",inset:"inset"},decls:0,vars:0,template:function(e,i){},styles:[`.mat-divider { - display: block; - margin: 0; - border-top-style: solid; - border-top-color: var(--mat-divider-color, var(--mat-sys-outline-variant)); - border-top-width: var(--mat-divider-width, 1px); -} -.mat-divider.mat-divider-vertical { - border-top: 0; - border-right-style: solid; - border-right-color: var(--mat-divider-color, var(--mat-sys-outline-variant)); - border-right-width: var(--mat-divider-width, 1px); -} -.mat-divider.mat-divider-inset { - margin-left: 80px; -} -[dir=rtl] .mat-divider.mat-divider-inset { - margin-left: auto; - margin-right: 80px; -} -`],encapsulation:2,changeDetection:0})}return n})(),ve=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({imports:[j]})}return n})();var Fo=["tooltip"],Oo=20;var Ro=new w("mat-tooltip-scroll-strategy",{providedIn:"root",factory:()=>{let n=r(G);return()=>re(n,{scrollThrottle:Oo})}}),Ao=new w("mat-tooltip-default-options",{providedIn:"root",factory:()=>({showDelay:0,hideDelay:0,touchendHideDelay:1500})});var zn="tooltip-panel",Lo={passive:!0},Po=8,Bo=8,zo=24,No=200,We=(()=>{class n{_elementRef=r(O);_ngZone=r(H);_platform=r(ot);_ariaDescriber=r(Qi);_focusMonitor=r(le);_dir=r(ut);_injector=r(G);_viewContainerRef=r(ee);_mediaMatcher=r(Ni);_document=r(It);_renderer=r(st);_animationsDisabled=Q();_defaultOptions=r(Ao,{optional:!0});_overlayRef=null;_tooltipInstance=null;_overlayPanelClass;_portal;_position="below";_positionAtOrigin=!1;_disabled=!1;_tooltipClass;_viewInitialized=!1;_pointerExitEventsInitialized=!1;_tooltipComponent=Nn;_viewportMargin=8;_currentPosition;_cssClassPrefix="mat-mdc";_ariaDescriptionPending=!1;_dirSubscribed=!1;get position(){return this._position}set position(t){t!==this._position&&(this._position=t,this._overlayRef&&(this._updatePosition(this._overlayRef),this._tooltipInstance?.show(0),this._overlayRef.updatePosition()))}get positionAtOrigin(){return this._positionAtOrigin}set positionAtOrigin(t){this._positionAtOrigin=K(t),this._detach(),this._overlayRef=null}get disabled(){return this._disabled}set disabled(t){let e=K(t);this._disabled!==e&&(this._disabled=e,e?this.hide(0):this._setupPointerEnterEventsIfNeeded(),this._syncAriaDescription(this.message))}get showDelay(){return this._showDelay}set showDelay(t){this._showDelay=Gt(t)}_showDelay;get hideDelay(){return this._hideDelay}set hideDelay(t){this._hideDelay=Gt(t),this._tooltipInstance&&(this._tooltipInstance._mouseLeaveHideDelay=this._hideDelay)}_hideDelay;touchGestures="auto";get message(){return this._message}set message(t){let e=this._message;this._message=t!=null?String(t).trim():"",!this._message&&this._isTooltipVisible()?this.hide(0):(this._setupPointerEnterEventsIfNeeded(),this._updateTooltipMessage()),this._syncAriaDescription(e)}_message="";get tooltipClass(){return this._tooltipClass}set tooltipClass(t){this._tooltipClass=t,this._tooltipInstance&&this._setTooltipClass(this._tooltipClass)}_eventCleanups=[];_touchstartTimeout=null;_destroyed=new k;_isDestroyed=!1;constructor(){let t=this._defaultOptions;t&&(this._showDelay=t.showDelay,this._hideDelay=t.hideDelay,t.position&&(this.position=t.position),t.positionAtOrigin&&(this.positionAtOrigin=t.positionAtOrigin),t.touchGestures&&(this.touchGestures=t.touchGestures),t.tooltipClass&&(this.tooltipClass=t.tooltipClass)),this._viewportMargin=Po}ngAfterViewInit(){this._viewInitialized=!0,this._setupPointerEnterEventsIfNeeded(),this._focusMonitor.monitor(this._elementRef).pipe(I(this._destroyed)).subscribe(t=>{t?t==="keyboard"&&this._ngZone.run(()=>this.show()):this._ngZone.run(()=>this.hide(0))})}ngOnDestroy(){let t=this._elementRef.nativeElement;this._touchstartTimeout&&clearTimeout(this._touchstartTimeout),this._overlayRef&&(this._overlayRef.dispose(),this._tooltipInstance=null),this._eventCleanups.forEach(e=>e()),this._eventCleanups.length=0,this._destroyed.next(),this._destroyed.complete(),this._isDestroyed=!0,this._ariaDescriber.removeDescription(t,this.message,"tooltip"),this._focusMonitor.stopMonitoring(t)}show(t=this.showDelay,e){if(this.disabled||!this.message||this._isTooltipVisible()){this._tooltipInstance?._cancelPendingAnimations();return}let i=this._createOverlay(e);this._detach(),this._portal=this._portal||new qt(this._tooltipComponent,this._viewContainerRef);let a=this._tooltipInstance=i.attach(this._portal).instance;a._triggerElement=this._elementRef.nativeElement,a._mouseLeaveHideDelay=this._hideDelay,a.afterHidden().pipe(I(this._destroyed)).subscribe(()=>this._detach()),this._setTooltipClass(this._tooltipClass),this._updateTooltipMessage(),a.show(t)}hide(t=this.hideDelay){let e=this._tooltipInstance;e&&(e.isVisible()?e.hide(t):(e._cancelPendingAnimations(),this._detach()))}toggle(t){this._isTooltipVisible()?this.hide():this.show(void 0,t)}_isTooltipVisible(){return!!this._tooltipInstance&&this._tooltipInstance.isVisible()}_createOverlay(t){if(this._overlayRef){let o=this._overlayRef.getConfig().positionStrategy;if((!this.positionAtOrigin||!t)&&o._origin instanceof O)return this._overlayRef;this._detach()}let e=this._injector.get(Ci).getAncestorScrollContainers(this._elementRef),i=`${this._cssClassPrefix}-${zn}`,a=Ei(this._injector,this.positionAtOrigin?t||this._elementRef:this._elementRef).withTransformOriginOn(`.${this._cssClassPrefix}-tooltip`).withFlexibleDimensions(!1).withViewportMargin(this._viewportMargin).withScrollableContainers(e).withPopoverLocation("global");return a.positionChanges.pipe(I(this._destroyed)).subscribe(o=>{this._updateCurrentPositionClass(o.connectionPair),this._tooltipInstance&&o.scrollableViewProperties.isOverlayClipped&&this._tooltipInstance.isVisible()&&this._ngZone.run(()=>this.hide(0))}),this._overlayRef=se(this._injector,{direction:this._dir,positionStrategy:a,panelClass:this._overlayPanelClass?[...this._overlayPanelClass,i]:i,scrollStrategy:this._injector.get(Ro)(),disableAnimations:this._animationsDisabled,eventPredicate:this._overlayEventPredicate}),this._updatePosition(this._overlayRef),this._overlayRef.detachments().pipe(I(this._destroyed)).subscribe(()=>this._detach()),this._overlayRef.outsidePointerEvents().pipe(I(this._destroyed)).subscribe(()=>this._tooltipInstance?._handleBodyInteraction()),this._overlayRef.keydownEvents().pipe(I(this._destroyed)).subscribe(o=>{o.preventDefault(),o.stopPropagation(),this._ngZone.run(()=>this.hide(0))}),this._defaultOptions?.disableTooltipInteractivity&&this._overlayRef.addPanelClass(`${this._cssClassPrefix}-tooltip-panel-non-interactive`),this._dirSubscribed||(this._dirSubscribed=!0,this._dir.change.pipe(I(this._destroyed)).subscribe(()=>{this._overlayRef&&this._updatePosition(this._overlayRef)})),this._overlayRef}_detach(){this._overlayRef&&this._overlayRef.hasAttached()&&this._overlayRef.detach(),this._tooltipInstance=null}_updatePosition(t){let e=t.getConfig().positionStrategy,i=this._getOrigin(),a=this._getOverlayPosition();e.withPositions([this._addOffset(X(X({},i.main),a.main)),this._addOffset(X(X({},i.fallback),a.fallback))])}_addOffset(t){let e=Bo,i=!this._dir||this._dir.value=="ltr";return t.originY==="top"?t.offsetY=-e:t.originY==="bottom"?t.offsetY=e:t.originX==="start"?t.offsetX=i?-e:e:t.originX==="end"&&(t.offsetX=i?e:-e),t}_getOrigin(){let t=!this._dir||this._dir.value=="ltr",e=this.position,i;e=="above"||e=="below"?i={originX:"center",originY:e=="above"?"top":"bottom"}:e=="before"||e=="left"&&t||e=="right"&&!t?i={originX:"start",originY:"center"}:(e=="after"||e=="right"&&t||e=="left"&&!t)&&(i={originX:"end",originY:"center"});let{x:a,y:o}=this._invertPosition(i.originX,i.originY);return{main:i,fallback:{originX:a,originY:o}}}_getOverlayPosition(){let t=!this._dir||this._dir.value=="ltr",e=this.position,i;e=="above"?i={overlayX:"center",overlayY:"bottom"}:e=="below"?i={overlayX:"center",overlayY:"top"}:e=="before"||e=="left"&&t||e=="right"&&!t?i={overlayX:"end",overlayY:"center"}:(e=="after"||e=="right"&&t||e=="left"&&!t)&&(i={overlayX:"start",overlayY:"center"});let{x:a,y:o}=this._invertPosition(i.overlayX,i.overlayY);return{main:i,fallback:{overlayX:a,overlayY:o}}}_updateTooltipMessage(){this._tooltipInstance&&(this._tooltipInstance.message=this.message,this._tooltipInstance._markForCheck(),rt(()=>{this._tooltipInstance&&this._overlayRef.updatePosition()},{injector:this._injector}))}_setTooltipClass(t){this._tooltipInstance&&(this._tooltipInstance.tooltipClass=t instanceof Set?Array.from(t):t,this._tooltipInstance._markForCheck())}_invertPosition(t,e){return this.position==="above"||this.position==="below"?e==="top"?e="bottom":e==="bottom"&&(e="top"):t==="end"?t="start":t==="start"&&(t="end"),{x:t,y:e}}_updateCurrentPositionClass(t){let{overlayY:e,originX:i,originY:a}=t,o;if(e==="center"?this._dir&&this._dir.value==="rtl"?o=i==="end"?"left":"right":o=i==="start"?"left":"right":o=e==="bottom"&&a==="top"?"above":"below",o!==this._currentPosition){let f=this._overlayRef;if(f){let b=`${this._cssClassPrefix}-${zn}-`;f.removePanelClass(b+this._currentPosition),f.addPanelClass(b+o)}this._currentPosition=o}}_setupPointerEnterEventsIfNeeded(){this._disabled||!this.message||!this._viewInitialized||this._eventCleanups.length||(this._isTouchPlatform()?this.touchGestures!=="off"&&(this._disableNativeGesturesIfNecessary(),this._addListener("touchstart",t=>{let e=t.targetTouches?.[0],i=e?{x:e.clientX,y:e.clientY}:void 0;this._setupPointerExitEventsIfNeeded(),this._touchstartTimeout&&clearTimeout(this._touchstartTimeout);let a=500;this._touchstartTimeout=setTimeout(()=>{this._touchstartTimeout=null,this.show(void 0,i)},this._defaultOptions?.touchLongPressShowDelay??a)})):this._addListener("mouseenter",t=>{this._setupPointerExitEventsIfNeeded();let e;t.x!==void 0&&t.y!==void 0&&(e=t),this.show(void 0,e)}))}_setupPointerExitEventsIfNeeded(){if(!this._pointerExitEventsInitialized){if(this._pointerExitEventsInitialized=!0,!this._isTouchPlatform())this._addListener("mouseleave",t=>{let e=t.relatedTarget;(!e||!this._overlayRef?.overlayElement.contains(e))&&this.hide()}),this._addListener("wheel",t=>{if(this._isTooltipVisible()){let e=this._document.elementFromPoint(t.clientX,t.clientY),i=this._elementRef.nativeElement;e!==i&&!i.contains(e)&&this.hide()}});else if(this.touchGestures!=="off"){this._disableNativeGesturesIfNecessary();let t=()=>{this._touchstartTimeout&&clearTimeout(this._touchstartTimeout),this.hide(this._defaultOptions?.touchendHideDelay)};this._addListener("touchend",t),this._addListener("touchcancel",t)}}}_addListener(t,e){this._eventCleanups.push(this._renderer.listen(this._elementRef.nativeElement,t,e,Lo))}_isTouchPlatform(){let t=this._defaultOptions?.detectHoverCapability;return typeof t=="function"?!t():this._platform.IOS||this._platform.ANDROID?!0:this._platform.isBrowser?!!t&&this._mediaMatcher.matchMedia("(any-hover: none)").matches:!1}_disableNativeGesturesIfNecessary(){let t=this.touchGestures;if(t!=="off"){let e=this._elementRef.nativeElement,i=e.style;(t==="on"||e.nodeName!=="INPUT"&&e.nodeName!=="TEXTAREA")&&(i.userSelect=i.msUserSelect=i.webkitUserSelect=i.MozUserSelect="none"),(t==="on"||!e.draggable)&&(i.webkitUserDrag="none"),i.touchAction="none",i.webkitTapHighlightColor="transparent"}}_syncAriaDescription(t){this._ariaDescriptionPending||(this._ariaDescriptionPending=!0,this._ariaDescriber.removeDescription(this._elementRef.nativeElement,t,"tooltip"),this._isDestroyed||rt({write:()=>{this._ariaDescriptionPending=!1,this.message&&!this.disabled&&this._ariaDescriber.describe(this._elementRef.nativeElement,this.message,"tooltip")}},{injector:this._injector}))}_overlayEventPredicate=t=>t.type==="keydown"?this._isTooltipVisible()&&t.keyCode===27&&!ct(t):!0;static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matTooltip",""]],hostAttrs:[1,"mat-mdc-tooltip-trigger"],hostVars:2,hostBindings:function(e,i){e&2&&_("mat-mdc-tooltip-disabled",i.disabled)},inputs:{position:[0,"matTooltipPosition","position"],positionAtOrigin:[0,"matTooltipPositionAtOrigin","positionAtOrigin"],disabled:[0,"matTooltipDisabled","disabled"],showDelay:[0,"matTooltipShowDelay","showDelay"],hideDelay:[0,"matTooltipHideDelay","hideDelay"],touchGestures:[0,"matTooltipTouchGestures","touchGestures"],message:[0,"matTooltip","message"],tooltipClass:[0,"matTooltipClass","tooltipClass"]},exportAs:["matTooltip"]})}return n})(),Nn=(()=>{class n{_changeDetectorRef=r(W);_elementRef=r(O);_isMultiline=!1;message;tooltipClass;_showTimeoutId;_hideTimeoutId;_triggerElement;_mouseLeaveHideDelay;_animationsDisabled=Q();_tooltip;_closeOnInteraction=!1;_isVisible=!1;_onHide=new k;_showAnimation="mat-mdc-tooltip-show";_hideAnimation="mat-mdc-tooltip-hide";constructor(){}show(t){this._hideTimeoutId!=null&&clearTimeout(this._hideTimeoutId),this._showTimeoutId=setTimeout(()=>{this._toggleVisibility(!0),this._showTimeoutId=void 0},t)}hide(t){this._showTimeoutId!=null&&clearTimeout(this._showTimeoutId),this._hideTimeoutId=setTimeout(()=>{this._toggleVisibility(!1),this._hideTimeoutId=void 0},t)}afterHidden(){return this._onHide}isVisible(){return this._isVisible}ngOnDestroy(){this._cancelPendingAnimations(),this._onHide.complete(),this._triggerElement=null}_handleBodyInteraction(){this._closeOnInteraction&&this.hide(0)}_markForCheck(){this._changeDetectorRef.markForCheck()}_handleMouseLeave({relatedTarget:t}){(!t||!this._triggerElement.contains(t))&&(this.isVisible()?this.hide(this._mouseLeaveHideDelay):this._finalizeAnimation(!1))}_onShow(){this._isMultiline=this._isTooltipMultiline(),this._markForCheck()}_isTooltipMultiline(){let t=this._elementRef.nativeElement.getBoundingClientRect();return t.height>zo&&t.width>=No}_handleAnimationEnd({animationName:t}){(t===this._showAnimation||t===this._hideAnimation)&&this._finalizeAnimation(t===this._showAnimation)}_cancelPendingAnimations(){this._showTimeoutId!=null&&clearTimeout(this._showTimeoutId),this._hideTimeoutId!=null&&clearTimeout(this._hideTimeoutId),this._showTimeoutId=this._hideTimeoutId=void 0}_finalizeAnimation(t){t?this._closeOnInteraction=!0:this.isVisible()||this._onHide.next()}_toggleVisibility(t){let e=this._tooltip.nativeElement,i=this._showAnimation,a=this._hideAnimation;if(e.classList.remove(t?a:i),e.classList.add(t?i:a),this._isVisible!==t&&(this._isVisible=t,this._changeDetectorRef.markForCheck()),t&&!this._animationsDisabled&&typeof getComputedStyle=="function"){let o=getComputedStyle(e);(o.getPropertyValue("animation-duration")==="0s"||o.getPropertyValue("animation-name")==="none")&&(this._animationsDisabled=!0)}t&&this._onShow(),this._animationsDisabled&&(e.classList.add("_mat-animation-noopable"),this._finalizeAnimation(t))}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-tooltip-component"]],viewQuery:function(e,i){if(e&1&&V(Fo,7),e&2){let a;m(a=h())&&(i._tooltip=a.first)}},hostAttrs:["aria-hidden","true"],hostBindings:function(e,i){e&1&&u("mouseleave",function(o){return i._handleMouseLeave(o)})},decls:4,vars:5,consts:[["tooltip",""],[1,"mdc-tooltip","mat-mdc-tooltip",3,"animationend"],[1,"mat-mdc-tooltip-surface","mdc-tooltip__surface"]],template:function(e,i){e&1&&(kt(0,"div",1,0),si("animationend",function(o){return i._handleAnimationEnd(o)}),kt(2,"div",2),E(3),Ot()()),e&2&&(bt(i.tooltipClass),_("mdc-tooltip--multiline",i._isMultiline),c(3),pt(i.message))},styles:[`.mat-mdc-tooltip { - position: relative; - transform: scale(0); - display: inline-flex; -} -.mat-mdc-tooltip::before { - content: ""; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: -1; - position: absolute; -} -.mat-mdc-tooltip-panel-below .mat-mdc-tooltip::before { - top: -8px; -} -.mat-mdc-tooltip-panel-above .mat-mdc-tooltip::before { - bottom: -8px; -} -.mat-mdc-tooltip-panel-right .mat-mdc-tooltip::before { - left: -8px; -} -.mat-mdc-tooltip-panel-left .mat-mdc-tooltip::before { - right: -8px; -} -.mat-mdc-tooltip._mat-animation-noopable { - animation: none; - transform: scale(1); -} - -.mat-mdc-tooltip-surface { - word-break: normal; - overflow-wrap: anywhere; - padding: 4px 8px; - min-width: 40px; - max-width: 200px; - min-height: 24px; - max-height: 40vh; - box-sizing: border-box; - overflow: hidden; - text-align: center; - will-change: transform, opacity; - background-color: var(--mat-tooltip-container-color, var(--mat-sys-inverse-surface)); - color: var(--mat-tooltip-supporting-text-color, var(--mat-sys-inverse-on-surface)); - border-radius: var(--mat-tooltip-container-shape, var(--mat-sys-corner-extra-small)); - font-family: var(--mat-tooltip-supporting-text-font, var(--mat-sys-body-small-font)); - font-size: var(--mat-tooltip-supporting-text-size, var(--mat-sys-body-small-size)); - font-weight: var(--mat-tooltip-supporting-text-weight, var(--mat-sys-body-small-weight)); - line-height: var(--mat-tooltip-supporting-text-line-height, var(--mat-sys-body-small-line-height)); - letter-spacing: var(--mat-tooltip-supporting-text-tracking, var(--mat-sys-body-small-tracking)); -} -.mat-mdc-tooltip-surface::before { - position: absolute; - box-sizing: border-box; - width: 100%; - height: 100%; - top: 0; - left: 0; - border: 1px solid transparent; - border-radius: inherit; - content: ""; - pointer-events: none; -} -.mdc-tooltip--multiline .mat-mdc-tooltip-surface { - text-align: left; -} -[dir=rtl] .mdc-tooltip--multiline .mat-mdc-tooltip-surface { - text-align: right; -} - -.mat-mdc-tooltip-panel { - line-height: normal; -} -.mat-mdc-tooltip-panel.mat-mdc-tooltip-panel-non-interactive { - pointer-events: none; -} - -@keyframes mat-mdc-tooltip-show { - 0% { - opacity: 0; - transform: scale(0.8); - } - 100% { - opacity: 1; - transform: scale(1); - } -} -@keyframes mat-mdc-tooltip-hide { - 0% { - opacity: 1; - transform: scale(1); - } - 100% { - opacity: 0; - transform: scale(0.8); - } -} -.mat-mdc-tooltip-show { - animation: mat-mdc-tooltip-show 150ms cubic-bezier(0, 0, 0.2, 1) forwards; -} - -.mat-mdc-tooltip-hide { - animation: mat-mdc-tooltip-hide 75ms cubic-bezier(0.4, 0, 1, 1) forwards; -} -`],encapsulation:2,changeDetection:0})}return n})();var jn=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({imports:[Vi,Pt,j,ae]})}return n})();var jt=class{data=[];keyIndex=new Map;dataChange=new St([]);itemUpdated=new k;getItems(){return this.data}add(s){if(this.findIndex(s)>=0){this.update(s);return}this.data=[...this.data,s],this.keyIndex.set(this.getItemKey(s),this.data.length-1),this.dataChange.next(this.data)}set(s){let t=[];for(let e of s||[]){let i=this.getItemKey(e),a=this.keyIndex.get(i);t.push(a!==void 0?Object.assign(this.data[a],e):e)}this.data=t,this.reindex(),this.dataChange.next(this.data)}get(s){let t=this.keyIndex.get(s);if(t!==void 0)return this.data[t]}update(s){let t=this.findIndex(s);if(t>=0){let e=Object.assign(this.data[t],s);this.data=[...this.data],this.data[t]=e,this.dataChange.next(this.data),this.itemUpdated.next(e)}}remove(s){let t=this.findIndex(s);t>=0&&(this.data=this.data.filter((e,i)=>i!==t),this.reindex(),this.dataChange.next(this.data))}applyBatch(s,t){if(s.length>0){let e=this.data.length;this.data=[...this.data,...s];for(let i=0;i0){let e=new Set(t.map(i=>this.getItemKey(i)));this.data=this.data.filter(i=>!e.has(this.getItemKey(i))),this.reindex()}else s.length===0&&(this.data=[...this.data]);this.dataChange.next(this.data)}get changes(){return this.dataChange}get itemChanged(){return this.itemUpdated}clear(){this.data=[],this.keyIndex.clear(),this.dataChange.next(this.data)}reindex(){this.keyIndex.clear();for(let s=0;s{class n extends jt{getItemKey(t){return t.link_id}static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275prov=P({token:n,factory:n.\u0275fac})}return n})();var Hn=(()=>{class n extends jt{getItemKey(t){return t.node_id}static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275prov=P({token:n,factory:n.\u0275fac})}return n})();var $n=(()=>{class n{httpController;constructor(t){this.httpController=t}getComputes(t){return this.httpController.get(t,"/computes")}getCompute(t,e){return this.httpController.get(t,`/computes/${e}`)}createCompute(t,e){return this.httpController.post(t,"/computes",e)}updateCompute(t,e,i){return this.httpController.put(t,`/computes/${e}`,i)}deleteCompute(t,e){return this.httpController.delete(t,`/computes/${e}`)}connectCompute(t,e){return this.httpController.post(t,`/computes/${e}/connect`,null)}getStatistics(t){return this.httpController.get(t,"/statistics")}static \u0275fac=function(e){return new(e||n)(at(me))};static \u0275prov=P({token:n,factory:n.\u0275fac})}return n})();var Wn=(()=>{class n{settings={crash_reports:!0,console_command:void 0,anonymous_statistics:!0};reportsSettings="crash_reports";consoleSettings="console_command";statisticsSettings="statistics_command";constructor(){this.getItem(this.reportsSettings)&&(this.settings.crash_reports=this.getItem(this.reportsSettings)==="true"),this.getItem(this.consoleSettings)&&(this.settings.console_command=this.getItem(this.consoleSettings)),this.getItem(this.statisticsSettings)&&(this.settings.anonymous_statistics=this.getItem(this.statisticsSettings)==="true")}setReportsSettings(t){this.settings.crash_reports=t,this.removeItem(this.reportsSettings),t?this.setItem(this.reportsSettings,"true"):this.setItem(this.reportsSettings,"false")}setStatisticsSettings(t){this.settings.anonymous_statistics=t,this.removeItem(this.statisticsSettings),t?this.setItem(this.statisticsSettings,"true"):this.setItem(this.statisticsSettings,"false")}getReportsSettings(){return this.getItem(this.reportsSettings)==="true"}getStatisticsSettings(){return this.getItem(this.statisticsSettings)==="true"}setConsoleSettings(t){this.settings.console_command=t,this.removeItem(this.consoleSettings),this.setItem(this.consoleSettings,t)}getConsoleSettings(){return this.getItem(this.consoleSettings)}removeItem(t){localStorage.removeItem(t)}setItem(t,e){localStorage.setItem(t,e)}getItem(t){return localStorage.getItem(t)}getAll(){return this.settings}setAll(t){this.settings=t,this.setConsoleSettings(t.console_command),this.setReportsSettings(t.crash_reports),this.setStatisticsSettings(t.anonymous_statistics)}static \u0275fac=function(e){return new(e||n)};static \u0275prov=P({token:n,factory:n.\u0275fac,providedIn:"root"})}return n})();var Qn=(()=>{class n{controllerId;projectId;controllerIdProjectList;setcontrollerId(t){this.controllerId=t}setProjectId(t){this.projectId=t}setcontrollerIdProjectList(t){this.controllerIdProjectList=t}getcontrollerId(){return this.controllerId}getProjectId(){return this.projectId}getcontrollerIdProjectList(){return this.controllerIdProjectList}removeData(){this.controllerId="",this.projectId=""}static \u0275fac=function(e){return new(e||n)};static \u0275prov=P({token:n,factory:n.\u0275fac})}return n})();var Gn=(()=>{class n{httpController;settingsService;recentlyOpenedProjectService;compression_methods=[{id:1,value:"none",name:"None"},{id:2,value:"zip",name:"Zip compression (deflate)"},{id:3,value:"bzip2",name:"Bzip2 compression"},{id:4,value:"lzma",name:"Lzma compression"},{id:5,value:"zstd",name:"Zstandard compression"}];compression_level_default_value=[{id:1,name:"none",value:"",selectionValues:[]},{id:2,name:"zip",value:6,selectionValues:[0,1,2,3,4,5,6,7,8,9]},{id:3,name:"bzip2",value:9,selectionValues:[1,2,3,4,5,6,7,8,9]},{id:4,name:"lzma",value:" ",selectionValues:[]},{id:5,name:"zstd",value:3,selectionValues:[1,2,3,4,5,6,7,8,9.1,11,12,13,14,15,16,17,18,19,20,21,22]}];projectListSubject=new k;projectLockIconSubject=new k;constructor(t,e,i){this.httpController=t,this.settingsService=e,this.recentlyOpenedProjectService=i}projectListUpdated(){this.projectListSubject.next(!0)}getReadmeFile(t,e){return this.httpController.getText(t,`/projects/${e}/files/README.txt`)}postReadmeFile(t,e,i){return this.httpController.post(t,`/projects/${e}/files/README.txt`,i)}get(t,e){return this.httpController.get(t,`/projects/${e}`)}open(t,e){return this.httpController.post(t,`/projects/${e}/open`,{})}close(t,e){return this.recentlyOpenedProjectService.removeData(),this.httpController.post(t,`/projects/${e}/close`,{})}list(t){return this.httpController.get(t,"/projects")}nodes(t,e){return this.httpController.get(t,`/projects/${e}/nodes`)}links(t,e){return this.httpController.get(t,`/projects/${e}/links`)}drawings(t,e){return this.httpController.get(t,`/projects/${e}/drawings`)}add(t,e,i){return this.httpController.post(t,"/projects",{name:e,project_id:i})}update(t,e){return this.httpController.put(t,`/projects/${e.project_id}`,{auto_close:e.auto_close,auto_open:e.auto_open,auto_start:e.auto_start,drawing_grid_size:e.drawing_grid_size,grid_size:e.grid_size,name:e.name,scene_width:e.scene_width,scene_height:e.scene_height,snap_to_grid:e.snap_to_grid,show_grid:e.show_grid,show_interface_labels:e.show_interface_labels,show_layers:e.show_layers,variables:e.variables,zoom:e.zoom})}delete(t,e){return this.httpController.delete(t,`/projects/${e}`)}getUploadPath(t,e,i){return`${t.protocol}//${t.host}:${t.port}/${_t.current_version}/projects/${e}/import?name=${i}`}getExportPath(t,e){return`${t.protocol}//${t.host}:${t.port}/${_t.current_version}/projects/${e.project_id}/export`}export(t,e){return this.httpController.get(t,`/projects/${e}/export`)}getStatistics(t,e){return this.httpController.get(t,`/projects/${e}/stats`)}duplicate(t,e,i){return this.httpController.post(t,`/projects/${e}/duplicate`,{name:i})}isReadOnly(t){return t.readonly?t.readonly:!1}getCompression(){return this.compression_methods}getCompressionLevel(){return this.compression_level_default_value}getexportPortableProjectPath(t,e,i={}){return i.compression_level!=null&&i.compression_level!=""?`${t.protocol}//${t.host}:${t.port}/${_t.current_version}/projects/${e}/export?include_snapshots=${i.include_snapshots}&include_images=${i.include_base_image}&reset_mac_addresses=${i.reset_mac_address}&compression=${i.compression}&compression_level=${i.compression_level}&token=${t.authToken}`:`${t.protocol}//${t.host}:${t.port}/${_t.current_version}/projects/${e}/export?include_snapshots=${i.include_snapshots}&include_images=${i.include_base_image}&reset_mac_addresses=${i.reset_mac_address}&compression=${i.compression}&token=${t.authToken}`}getProjectStatus(t,e){return this.get(t,`${e}/locked`)}projectUpdateLockIcon(){this.projectLockIconSubject.next(!0)}static \u0275fac=function(e){return new(e||n)(at(me),at(Wn),at(Qn))};static \u0275prov=P({token:n,factory:n.\u0275fac})}return n})();var Wo=new w("DEFAULT_THEME_TOKEN",{providedIn:"root",factory:()=>"indigo-pink"}),qn=(()=>{class n{document;_darkMode$=new St(!1);darkMode$=this._darkMode$.asObservable();themeChanged=new F;mapThemeChanged=new F;currentTheme="indigo-pink";currentMapTheme="auto";savedTheme="indigo-pink";savedMapTheme="auto";availableThemes=[{key:"deeppurple-amber",label:"Deep Purple & Amber",type:"light",primaryColor:"#6750A4"},{key:"indigo-pink",label:"Indigo & Pink",type:"light",primaryColor:"#3F51B5"},{key:"magenta-violet",label:"Magenta & Violet",type:"light",primaryColor:"#D81B60"},{key:"rose-red",label:"Rose & Red",type:"light",primaryColor:"#E91E63"},{key:"pink-bluegrey",label:"Pink & Bluegrey",type:"dark",primaryColor:"#E91E63"},{key:"purple-green",label:"Purple & Green",type:"dark",primaryColor:"#7E57C2"},{key:"azure-blue",label:"Azure & Blue",type:"dark",primaryColor:"#0078D4"},{key:"cyan-orange",label:"Cyan & Orange",type:"dark",primaryColor:"#00B7C3"}];availableMapBackgrounds=[{key:"auto",label:"Follow global theme",background:"",textColor:"",type:"light"},{key:"light-1",label:"Cyan Sky",background:"radial-gradient(ellipse at 20% 20%, #B2EBF2 0%, #E0F7FA 70%)",textColor:"#006064",type:"light"},{key:"light-2",label:"Blue Sky",background:"radial-gradient(ellipse at 20% 20%, #BBDEFB 0%, #E3F2FD 70%)",textColor:"#1565C0",type:"light"},{key:"light-3",label:"Cloud Gray",background:"radial-gradient(ellipse at 20% 20%, #F5F5F5 0%, #FAFAFA 70%)",textColor:"#424242",type:"light"},{key:"light-4",label:"Lavender",background:"radial-gradient(ellipse at 20% 20%, #E1BEE7 0%, #F3E5F5 70%)",textColor:"#4A148C",type:"light"},{key:"dark-1",label:"Deep Cyan",background:"linear-gradient(135deg, #006064 0%, #00838F 50%, #006064 100%)",textColor:"#FFFFFF",type:"dark"},{key:"dark-2",label:"Deep Blue",background:"linear-gradient(135deg, #1565C0 0%, #1976D2 50%, #1565C0 100%)",textColor:"#FFFFFF",type:"dark"},{key:"dark-3",label:"Charcoal",background:"linear-gradient(135deg, #424242 0%, #616161 50%, #424242 100%)",textColor:"#FFFFFF",type:"dark"},{key:"dark-4",label:"Deep Purple",background:"linear-gradient(135deg, #4A148C 0%, #6A1B9A 50%, #4A148C 100%)",textColor:"#FFFFFF",type:"dark"}];constructor(t,e){this.document=t;let i=localStorage.getItem("theme");this.currentTheme=i||e,this.savedTheme=this.currentTheme;let a=localStorage.getItem("mapTheme");this.currentMapTheme=a||"auto",this.savedMapTheme=this.currentMapTheme,this.applyTheme(this.currentTheme)}getCurrentTheme(){return this.currentTheme}getThemeType(){return this.isDarkTheme(this.currentTheme)?"dark":"light"}getActualMapTheme(){if(this.savedMapTheme==="auto")return this.getThemeType();let t=this.availableMapBackgrounds.find(e=>e.key===this.savedMapTheme);return t?t.type:"light"}getActualTheme(){return this.getThemeType()}isDarkTheme(t){return t==="pink-bluegrey"||t==="purple-green"||t==="azure-blue"||t==="cyan-orange"}setTheme(t){this.currentTheme!==t&&(this.currentTheme=t,this.savedTheme=t,this.applyTheme(t),this.saveThemePreference(t),this.themeChanged.emit(t),this.currentMapTheme==="auto"&&this.mapThemeChanged.emit(t),this._darkMode$.next(this.isDarkTheme(t)))}toggleTheme(){let t=this.getThemeType(),e;t==="dark"?e=this.availableThemes.find(i=>i.type==="light")?.key||"deeppurple-amber":e=this.availableThemes.find(i=>i.type==="dark")?.key||"pink-bluegrey",this.setTheme(e)}setDarkMode(t){let e=t?"pink-bluegrey":"indigo-pink";this.setTheme(e)}setMapTheme(t){this.currentMapTheme=t,this.savedMapTheme=t,localStorage.setItem("mapTheme",t),this.mapThemeChanged.emit(this.getActualMapTheme())}restoreTheme(){let t=localStorage.getItem("theme");t&&this.availableThemes.some(e=>e.key===t)&&this.setTheme(t)}applyTheme(t){let e=this.document.documentElement;e.classList.remove("theme-deeppurple-amber","theme-indigo-pink","theme-magenta-violet","theme-rose-red","theme-pink-bluegrey","theme-purple-green","theme-azure-blue","theme-cyan-orange"),e.classList.add(`theme-${t}`)}saveThemePreference(t){localStorage.setItem("theme",t)}isDarkMode(){return this.isDarkTheme(this.currentTheme)}isLightMode(){return!this.isDarkTheme(this.currentTheme)}getCanvasLabelColor(){return this.getActualMapTheme()==="dark"?"#FFFFFF":"#000000"}getCanvasLinkColor(){return this.getActualMapTheme()==="dark"?"#FFFFFF":"#000000"}static \u0275fac=function(e){return new(e||n)(at(It),at(Wo))};static \u0275prov=P({token:n,factory:n.\u0275fac,providedIn:"root"})}return n})();var Un=(()=>{class n{ws;currentController;computeNotificationEmitter=new F;computeCache=new Map;projectNotificationEmitter=new F;computeCacheUpdated=new F;notificationsPath(t){let e="ws";return t.protocol==="https:"&&(e="wss"),`${e}://${t.host}:${t.port}/${_t.current_version}/notifications/ws?token=${t.authToken}`}projectNotificationsPath(t,e){let i="ws";return t.protocol==="https:"&&(i="wss"),`${i}://${t.host}:${t.port}/${_t.current_version}/projects/${e}/notifications/ws?token=${t.authToken}`}markerNotificationsPath(t,e){let i="ws";return t.protocol==="https:"&&(i="wss"),`${i}://${t.host}:${t.port}/${_t.current_version}/projects/${e}/notifications/markers/ws?token=${t.authToken}`}connectToComputeNotifications(t){this.ws&&this.currentController===t||(this.disconnect(),this.currentController=t,this.ws=new WebSocket(this.notificationsPath(t)),this.ws.onmessage=e=>{let i=JSON.parse(e.data);this.handleMessage(i)},this.ws.onerror=()=>{console.error("Compute notifications WebSocket error")},this.ws.onclose=()=>{this.ws=null})}disconnect(){this.ws&&(this.ws.close(),this.ws=null,this.currentController=null,this.computeCache.clear())}getCachedComputes(){return Array.from(this.computeCache.values())}hasCachedData(){return this.computeCache.size>0}setInitialComputes(t){this.computeCache.clear(),t.forEach(e=>{this.computeCache.set(e.compute_id,e)}),this.computeCacheUpdated.emit(this.getCachedComputes())}handleMessage(t){switch(t.action){case"compute.created":this.computeCache.set(t.event.compute_id,t.event),this.computeNotificationEmitter.emit(t),this.computeCacheUpdated.emit(this.getCachedComputes());break;case"compute.updated":this.computeCache.set(t.event.compute_id,t.event),this.computeNotificationEmitter.emit(t),this.computeCacheUpdated.emit(this.getCachedComputes());break;case"compute.deleted":this.computeCache.delete(t.event.compute_id),this.computeNotificationEmitter.emit(t),this.computeCacheUpdated.emit(this.getCachedComputes());break;case"project.created":case"project.opened":case"project.closed":case"project.updated":case"project.deleted":this.projectNotificationEmitter.emit(t);break}}static \u0275fac=function(e){return new(e||n)};static \u0275prov=P({token:n,factory:n.\u0275fac})}return n})();function Qo(n,s){if(n&1){let t=lt();l(0,"div",1)(1,"button",2),u("click",function(){R(t);let i=p();return A(i.action())}),E(2),d()()}if(n&2){let t=p();c(2),ft(" ",t.data.action," ")}}var Go=["label"];function qo(n,s){}var Uo=Math.pow(2,31)-1,Zt=class{_overlayRef;instance;containerInstance;_afterDismissed=new k;_afterOpened=new k;_onAction=new k;_durationTimeoutId;_dismissedByAction=!1;constructor(s,t){this._overlayRef=t,this.containerInstance=s,s._onExit.subscribe(()=>this._finishDismiss())}dismiss(){this._afterDismissed.closed||this.containerInstance.exit(),clearTimeout(this._durationTimeoutId)}dismissWithAction(){this._onAction.closed||(this._dismissedByAction=!0,this._onAction.next(),this._onAction.complete(),this.dismiss()),clearTimeout(this._durationTimeoutId)}closeWithAction(){this.dismissWithAction()}_dismissAfter(s){this._durationTimeoutId=setTimeout(()=>this.dismiss(),Math.min(s,Uo))}_open(){this._afterOpened.closed||(this._afterOpened.next(),this._afterOpened.complete())}_finishDismiss(){this._overlayRef.dispose(),this._onAction.closed||this._onAction.complete(),this._afterDismissed.next({dismissedByAction:this._dismissedByAction}),this._afterDismissed.complete(),this._dismissedByAction=!1}afterDismissed(){return this._afterDismissed}afterOpened(){return this.containerInstance._onEnter}onAction(){return this._onAction}},Yn=new w("MatSnackBarData"),Vt=class{politeness="polite";announcementMessage="";viewContainerRef;duration=0;panelClass;direction;data=null;horizontalPosition="center";verticalPosition="bottom"},Yo=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matSnackBarLabel",""]],hostAttrs:[1,"mat-mdc-snack-bar-label","mdc-snackbar__label"]})}return n})(),Ko=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matSnackBarActions",""]],hostAttrs:[1,"mat-mdc-snack-bar-actions","mdc-snackbar__actions"]})}return n})(),Zo=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matSnackBarAction",""]],hostAttrs:[1,"mat-mdc-snack-bar-action","mdc-snackbar__action"]})}return n})(),Kn=(()=>{class n{snackBarRef=r(Zt);data=r(Yn);constructor(){}action(){this.snackBarRef.dismissWithAction()}get hasAction(){return!!this.data.action}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["simple-snack-bar"]],hostAttrs:[1,"mat-mdc-simple-snack-bar"],exportAs:["matSnackBar"],decls:3,vars:2,consts:[["matSnackBarLabel",""],["matSnackBarActions",""],["matButton","","matSnackBarAction","",3,"click"]],template:function(e,i){e&1&&(l(0,"div",0),E(1),d(),g(2,Qo,3,1,"div",1)),e&2&&(c(),ft(" ",i.data.message,` -`),c(),v(i.hasAction?2:-1))},dependencies:[qi,Yo,Ko,Zo],styles:[`.mat-mdc-simple-snack-bar { - display: flex; -} -.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-label { - max-height: 50vh; - overflow: auto; -} -`],encapsulation:2,changeDetection:0})}return n})(),Qe="_mat-snack-bar-enter",Ge="_mat-snack-bar-exit",Xo=(()=>{class n extends Mi{_ngZone=r(H);_elementRef=r(O);_changeDetectorRef=r(W);_platform=r(ot);_animationsDisabled=Q();snackBarConfig=r(Vt);_document=r(It);_trackedModals=new Set;_enterFallback;_exitFallback;_injector=r(G);_announceDelay=150;_announceTimeoutId;_destroyed=!1;_portalOutlet;_onAnnounce=new k;_onExit=new k;_onEnter=new k;_animationState="void";_live;_label;_role;_liveElementId=r(U).getId("mat-snack-bar-container-live-");constructor(){super();let t=this.snackBarConfig;t.politeness==="assertive"&&!t.announcementMessage?this._live="assertive":t.politeness==="off"?this._live="off":this._live="polite",this._platform.FIREFOX&&(this._live==="polite"&&(this._role="status"),this._live==="assertive"&&(this._role="alert"))}attachComponentPortal(t){this._assertNotAttached();let e=this._portalOutlet.attachComponentPortal(t);return this._afterPortalAttached(),e}attachTemplatePortal(t){this._assertNotAttached();let e=this._portalOutlet.attachTemplatePortal(t);return this._afterPortalAttached(),e}attachDomPortal=t=>{this._assertNotAttached();let e=this._portalOutlet.attachDomPortal(t);return this._afterPortalAttached(),e};onAnimationEnd(t){t===Ge?this._completeExit():t===Qe&&(clearTimeout(this._enterFallback),this._ngZone.run(()=>{this._onEnter.next(),this._onEnter.complete()}))}enter(){this._destroyed||(this._animationState="visible",this._changeDetectorRef.markForCheck(),this._changeDetectorRef.detectChanges(),this._screenReaderAnnounce(),this._animationsDisabled?rt(()=>{this._ngZone.run(()=>queueMicrotask(()=>this.onAnimationEnd(Qe)))},{injector:this._injector}):(clearTimeout(this._enterFallback),this._enterFallback=setTimeout(()=>{this._elementRef.nativeElement.classList.add("mat-snack-bar-fallback-visible"),this.onAnimationEnd(Qe)},200)))}exit(){return this._destroyed?te(void 0):(this._ngZone.run(()=>{this._animationState="hidden",this._changeDetectorRef.markForCheck(),this._elementRef.nativeElement.setAttribute("mat-exit",""),clearTimeout(this._announceTimeoutId),this._animationsDisabled?rt(()=>{this._ngZone.run(()=>queueMicrotask(()=>this.onAnimationEnd(Ge)))},{injector:this._injector}):(clearTimeout(this._exitFallback),this._exitFallback=setTimeout(()=>this.onAnimationEnd(Ge),200))}),this._onExit)}ngOnDestroy(){this._destroyed=!0,this._clearFromModals(),this._completeExit()}_completeExit(){clearTimeout(this._exitFallback),queueMicrotask(()=>{this._onExit.next(),this._onExit.complete()})}_afterPortalAttached(){let t=this._elementRef.nativeElement,e=this.snackBarConfig.panelClass;e&&(Array.isArray(e)?e.forEach(o=>t.classList.add(o)):t.classList.add(e)),this._exposeToModals();let i=this._label.nativeElement,a="mdc-snackbar__label";i.classList.toggle(a,!i.querySelector(`.${a}`))}_exposeToModals(){let t=this._liveElementId,e=this._document.querySelectorAll('body > .cdk-overlay-container [aria-modal="true"]');for(let i=0;i{let e=t.getAttribute("aria-owns");if(e){let i=e.replace(this._liveElementId,"").trim();i.length>0?t.setAttribute("aria-owns",i):t.removeAttribute("aria-owns")}}),this._trackedModals.clear()}_assertNotAttached(){this._portalOutlet.hasAttached()}_screenReaderAnnounce(){this._announceTimeoutId||this._ngZone.runOutsideAngular(()=>{this._announceTimeoutId=setTimeout(()=>{if(this._destroyed)return;let t=this._elementRef.nativeElement,e=t.querySelector("[aria-hidden]"),i=t.querySelector("[aria-live]");if(e&&i){let a=null;this._platform.isBrowser&&document.activeElement instanceof HTMLElement&&e.contains(document.activeElement)&&(a=document.activeElement),e.removeAttribute("aria-hidden"),i.appendChild(e),a?.focus(),this._onAnnounce.next(),this._onAnnounce.complete()}},this._announceDelay)})}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["mat-snack-bar-container"]],viewQuery:function(e,i){if(e&1&&V(Lt,7)(Go,7),e&2){let a;m(a=h())&&(i._portalOutlet=a.first),m(a=h())&&(i._label=a.first)}},hostAttrs:[1,"mdc-snackbar","mat-mdc-snack-bar-container"],hostVars:6,hostBindings:function(e,i){e&1&&u("animationend",function(o){return i.onAnimationEnd(o.animationName)})("animationcancel",function(o){return i.onAnimationEnd(o.animationName)}),e&2&&_("mat-snack-bar-container-enter",i._animationState==="visible")("mat-snack-bar-container-exit",i._animationState==="hidden")("mat-snack-bar-container-animations-enabled",!i._animationsDisabled)},features:[q],decls:6,vars:3,consts:[["label",""],[1,"mdc-snackbar__surface","mat-mdc-snackbar-surface"],[1,"mat-mdc-snack-bar-label"],["aria-hidden","true"],["cdkPortalOutlet",""]],template:function(e,i){e&1&&(l(0,"div",1)(1,"div",2,0)(3,"div",3),tt(4,qo,0,0,"ng-template",4),d(),M(5,"div"),d()()),e&2&&(c(5),T("aria-live",i._live)("role",i._role)("id",i._liveElementId))},dependencies:[Lt],styles:[`@keyframes _mat-snack-bar-enter { - from { - transform: scale(0.8); - opacity: 0; - } - to { - transform: scale(1); - opacity: 1; - } -} -@keyframes _mat-snack-bar-exit { - from { - opacity: 1; - } - to { - opacity: 0; - } -} -.mat-mdc-snack-bar-container { - display: flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - margin: 8px; -} -.mat-mdc-snack-bar-handset .mat-mdc-snack-bar-container { - width: 100vw; -} - -.mat-snack-bar-container-animations-enabled { - opacity: 0; -} -.mat-snack-bar-container-animations-enabled.mat-snack-bar-fallback-visible { - opacity: 1; -} -.mat-snack-bar-container-animations-enabled.mat-snack-bar-container-enter { - animation: _mat-snack-bar-enter 150ms cubic-bezier(0, 0, 0.2, 1) forwards; -} -.mat-snack-bar-container-animations-enabled.mat-snack-bar-container-exit { - animation: _mat-snack-bar-exit 75ms cubic-bezier(0.4, 0, 1, 1) forwards; -} - -.mat-mdc-snackbar-surface { - box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); - display: flex; - align-items: center; - justify-content: flex-start; - box-sizing: border-box; - padding-left: 0; - padding-right: 8px; -} -[dir=rtl] .mat-mdc-snackbar-surface { - padding-right: 0; - padding-left: 8px; -} -.mat-mdc-snack-bar-container .mat-mdc-snackbar-surface { - min-width: 344px; - max-width: 672px; -} -.mat-mdc-snack-bar-handset .mat-mdc-snackbar-surface { - width: 100%; - min-width: 0; -} -@media (forced-colors: active) { - .mat-mdc-snackbar-surface { - outline: solid 1px; - } -} -.mat-mdc-snack-bar-container .mat-mdc-snackbar-surface { - color: var(--mat-snack-bar-supporting-text-color, var(--mat-sys-inverse-on-surface)); - border-radius: var(--mat-snack-bar-container-shape, var(--mat-sys-corner-extra-small)); - background-color: var(--mat-snack-bar-container-color, var(--mat-sys-inverse-surface)); -} - -.mdc-snackbar__label { - width: 100%; - flex-grow: 1; - box-sizing: border-box; - margin: 0; - padding: 14px 8px 14px 16px; -} -[dir=rtl] .mdc-snackbar__label { - padding-left: 8px; - padding-right: 16px; -} -.mat-mdc-snack-bar-container .mdc-snackbar__label { - font-family: var(--mat-snack-bar-supporting-text-font, var(--mat-sys-body-medium-font)); - font-size: var(--mat-snack-bar-supporting-text-size, var(--mat-sys-body-medium-size)); - font-weight: var(--mat-snack-bar-supporting-text-weight, var(--mat-sys-body-medium-weight)); - line-height: var(--mat-snack-bar-supporting-text-line-height, var(--mat-sys-body-medium-line-height)); -} - -.mat-mdc-snack-bar-actions { - display: flex; - flex-shrink: 0; - align-items: center; - box-sizing: border-box; -} - -.mat-mdc-snack-bar-handset, -.mat-mdc-snack-bar-container, -.mat-mdc-snack-bar-label { - flex: 1 1 auto; -} - -.mat-mdc-snack-bar-container .mat-mdc-button.mat-mdc-snack-bar-action:not(:disabled).mat-unthemed { - color: var(--mat-snack-bar-button-color, var(--mat-sys-inverse-primary)); -} -.mat-mdc-snack-bar-container .mat-mdc-button.mat-mdc-snack-bar-action:not(:disabled) { - --mat-button-text-state-layer-color: currentColor; - --mat-button-text-ripple-color: currentColor; -} -.mat-mdc-snack-bar-container .mat-mdc-button.mat-mdc-snack-bar-action:not(:disabled) .mat-ripple-element { - opacity: 0.1; -} -`],encapsulation:2})}return n})(),Jo=new w("mat-snack-bar-default-options",{providedIn:"root",factory:()=>new Vt}),qe=(()=>{class n{_live=r(de);_injector=r(G);_breakpointObserver=r(ji);_parentSnackBar=r(n,{optional:!0,skipSelf:!0});_defaultConfig=r(Jo);_animationsDisabled=Q();_snackBarRefAtThisLevel=null;simpleSnackBarComponent=Kn;snackBarContainerComponent=Xo;handsetCssClass="mat-mdc-snack-bar-handset";get _openedSnackBarRef(){let t=this._parentSnackBar;return t?t._openedSnackBarRef:this._snackBarRefAtThisLevel}set _openedSnackBarRef(t){this._parentSnackBar?this._parentSnackBar._openedSnackBarRef=t:this._snackBarRefAtThisLevel=t}constructor(){}openFromComponent(t,e){return this._attach(t,e)}openFromTemplate(t,e){return this._attach(t,e)}open(t,e="",i){let a=X(X({},this._defaultConfig),i);return a.data={message:t,action:e},a.announcementMessage===t&&(a.announcementMessage=void 0),this.openFromComponent(this.simpleSnackBarComponent,a)}dismiss(){this._openedSnackBarRef&&this._openedSnackBarRef.dismiss()}ngOnDestroy(){this._snackBarRefAtThisLevel&&this._snackBarRefAtThisLevel.dismiss()}_attachSnackBarContainer(t,e){let i=e&&e.viewContainerRef&&e.viewContainerRef.injector,a=G.create({parent:i||this._injector,providers:[{provide:Vt,useValue:e}]}),o=new qt(this.snackBarContainerComponent,e.viewContainerRef,a),f=t.attach(o);return f.instance.snackBarConfig=e,f.instance}_attach(t,e){let i=X(X(X({},new Vt),this._defaultConfig),e),a=this._createOverlay(i),o=this._attachSnackBarContainer(a,i),f=new Zt(o,a);if(t instanceof Dt){let b=new oe(t,null,{$implicit:i.data,snackBarRef:f});f.instance=o.attachTemplatePortal(b)}else{let b=this._createInjector(i,f),xt=new qt(t,void 0,b),Mt=o.attachComponentPortal(xt);f.instance=Mt.instance}return this._breakpointObserver.observe(Gi.HandsetPortrait).pipe(I(a.detachments())).subscribe(b=>{a.overlayElement.classList.toggle(this.handsetCssClass,b.matches)}),i.announcementMessage&&o._onAnnounce.subscribe(()=>{this._live.announce(i.announcementMessage,i.politeness)}),this._animateSnackBar(f,i),this._openedSnackBarRef=f,this._openedSnackBarRef}_animateSnackBar(t,e){t.afterDismissed().subscribe(()=>{this._openedSnackBarRef==t&&(this._openedSnackBarRef=null),e.announcementMessage&&this._live.clear()}),e.duration&&e.duration>0&&t.afterOpened().subscribe(()=>t._dismissAfter(e.duration)),this._openedSnackBarRef?(this._openedSnackBarRef.afterDismissed().subscribe(()=>{t.containerInstance.enter()}),this._openedSnackBarRef.dismiss()):t.containerInstance.enter()}_createOverlay(t){let e=new Di;e.direction=t.direction;let i=Fi(this._injector),a=t.direction==="rtl",o=t.horizontalPosition==="left"||t.horizontalPosition==="start"&&!a||t.horizontalPosition==="end"&&a,f=!o&&t.horizontalPosition!=="center";return o?i.left("0"):f?i.right("0"):i.centerHorizontally(),t.verticalPosition==="top"?i.top("0"):i.bottom("0"),e.positionStrategy=i,e.disableAnimations=this._animationsDisabled,se(this._injector,e)}_createInjector(t,e){let i=t&&t.viewContainerRef&&t.viewContainerRef.injector;return G.create({parent:i||this._injector,providers:[{provide:Zt,useValue:e},{provide:Yn,useValue:t.data}]})}static \u0275fac=function(e){return new(e||n)};static \u0275prov=P({token:n,factory:n.\u0275fac,providedIn:"root"})}return n})();var ad=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({providers:[qe],imports:[Pt,Ii,Ui,Kn,j]})}return n})();var Zn=(()=>{class n{snackbar;snackBarConfigForSuccess={duration:4e3,panelClass:["snackabar-success"],MatSnackBarHorizontalPosition:"center",MatSnackBarVerticalPosition:"bottom"};snackBarConfigForWarning={duration:4e3,panelClass:["snackabar-warning"],MatSnackBarHorizontalPosition:"center",MatSnackBarVerticalPosition:"bottom"};snackBarConfigForError={duration:1e4,panelClass:["snackabar-error"],MatSnackBarHorizontalPosition:"center",MatSnackBarVerticalPosition:"bottom"};constructor(t){this.snackbar=t}error(t){console.error(t),this.snackbar.open(t,"Close",this.snackBarConfigForError)}warning(t){this.snackbar.open(t,"Close",this.snackBarConfigForWarning)}success(t){this.snackbar.open(t,"Close",this.snackBarConfigForSuccess)}static \u0275fac=function(e){return new(e||n)(at(qe))};static \u0275prov=P({token:n,factory:n.\u0275fac})}return n})();var Xn=["*"],Jn=`.mdc-list { - margin: 0; - padding: 8px 0; - list-style-type: none; -} -.mdc-list:focus { - outline: none; -} - -.mdc-list-item { - display: flex; - position: relative; - justify-content: flex-start; - overflow: hidden; - padding: 0; - align-items: stretch; - cursor: pointer; - padding-left: 16px; - padding-right: 16px; - background-color: var(--mat-list-list-item-container-color, transparent); - border-radius: var(--mat-list-list-item-container-shape, var(--mat-sys-corner-none)); -} -.mdc-list-item.mdc-list-item--selected { - background-color: var(--mat-list-list-item-selected-container-color); -} -.mdc-list-item:focus { - outline: 0; -} -.mdc-list-item.mdc-list-item--disabled { - cursor: auto; -} -.mdc-list-item.mdc-list-item--with-one-line { - height: var(--mat-list-list-item-one-line-container-height, 48px); -} -.mdc-list-item.mdc-list-item--with-one-line .mdc-list-item__start { - align-self: center; - margin-top: 0; -} -.mdc-list-item.mdc-list-item--with-one-line .mdc-list-item__end { - align-self: center; - margin-top: 0; -} -.mdc-list-item.mdc-list-item--with-two-lines { - height: var(--mat-list-list-item-two-line-container-height, 64px); -} -.mdc-list-item.mdc-list-item--with-two-lines .mdc-list-item__start { - align-self: flex-start; - margin-top: 16px; -} -.mdc-list-item.mdc-list-item--with-two-lines .mdc-list-item__end { - align-self: center; - margin-top: 0; -} -.mdc-list-item.mdc-list-item--with-three-lines { - height: var(--mat-list-list-item-three-line-container-height, 88px); -} -.mdc-list-item.mdc-list-item--with-three-lines .mdc-list-item__start { - align-self: flex-start; - margin-top: 16px; -} -.mdc-list-item.mdc-list-item--with-three-lines .mdc-list-item__end { - align-self: flex-start; - margin-top: 16px; -} -.mdc-list-item.mdc-list-item--selected::before, .mdc-list-item.mdc-list-item--selected:focus::before, .mdc-list-item:not(.mdc-list-item--selected):focus::before { - position: absolute; - box-sizing: border-box; - width: 100%; - height: 100%; - top: 0; - left: 0; - content: ""; - pointer-events: none; -} - -a.mdc-list-item { - color: inherit; - text-decoration: none; -} - -.mdc-list-item__start { - fill: currentColor; - flex-shrink: 0; - pointer-events: none; -} -.mdc-list-item--with-leading-icon .mdc-list-item__start { - color: var(--mat-list-list-item-leading-icon-color, var(--mat-sys-on-surface-variant)); - width: var(--mat-list-list-item-leading-icon-size, 24px); - height: var(--mat-list-list-item-leading-icon-size, 24px); - margin-left: 16px; - margin-right: 32px; -} -[dir=rtl] .mdc-list-item--with-leading-icon .mdc-list-item__start { - margin-left: 32px; - margin-right: 16px; -} -.mdc-list-item--with-leading-icon:hover .mdc-list-item__start { - color: var(--mat-list-list-item-hover-leading-icon-color); -} -.mdc-list-item--with-leading-avatar .mdc-list-item__start { - width: var(--mat-list-list-item-leading-avatar-size, 40px); - height: var(--mat-list-list-item-leading-avatar-size, 40px); - margin-left: 16px; - margin-right: 16px; - border-radius: 50%; -} -.mdc-list-item--with-leading-avatar .mdc-list-item__start, [dir=rtl] .mdc-list-item--with-leading-avatar .mdc-list-item__start { - margin-left: 16px; - margin-right: 16px; - border-radius: 50%; -} - -.mdc-list-item__end { - flex-shrink: 0; - pointer-events: none; -} -.mdc-list-item--with-trailing-meta .mdc-list-item__end { - font-family: var(--mat-list-list-item-trailing-supporting-text-font, var(--mat-sys-label-small-font)); - line-height: var(--mat-list-list-item-trailing-supporting-text-line-height, var(--mat-sys-label-small-line-height)); - font-size: var(--mat-list-list-item-trailing-supporting-text-size, var(--mat-sys-label-small-size)); - font-weight: var(--mat-list-list-item-trailing-supporting-text-weight, var(--mat-sys-label-small-weight)); - letter-spacing: var(--mat-list-list-item-trailing-supporting-text-tracking, var(--mat-sys-label-small-tracking)); -} -.mdc-list-item--with-trailing-icon .mdc-list-item__end { - color: var(--mat-list-list-item-trailing-icon-color, var(--mat-sys-on-surface-variant)); - width: var(--mat-list-list-item-trailing-icon-size, 24px); - height: var(--mat-list-list-item-trailing-icon-size, 24px); -} -.mdc-list-item--with-trailing-icon:hover .mdc-list-item__end { - color: var(--mat-list-list-item-hover-trailing-icon-color); -} -.mdc-list-item.mdc-list-item--with-trailing-meta .mdc-list-item__end { - color: var(--mat-list-list-item-trailing-supporting-text-color, var(--mat-sys-on-surface-variant)); -} -.mdc-list-item--selected.mdc-list-item--with-trailing-icon .mdc-list-item__end { - color: var(--mat-list-list-item-selected-trailing-icon-color, var(--mat-sys-primary)); -} - -.mdc-list-item__content { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - align-self: center; - flex: 1; - pointer-events: none; -} -.mdc-list-item--with-two-lines .mdc-list-item__content, .mdc-list-item--with-three-lines .mdc-list-item__content { - align-self: stretch; -} - -.mdc-list-item__primary-text { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - color: var(--mat-list-list-item-label-text-color, var(--mat-sys-on-surface)); - font-family: var(--mat-list-list-item-label-text-font, var(--mat-sys-body-large-font)); - line-height: var(--mat-list-list-item-label-text-line-height, var(--mat-sys-body-large-line-height)); - font-size: var(--mat-list-list-item-label-text-size, var(--mat-sys-body-large-size)); - font-weight: var(--mat-list-list-item-label-text-weight, var(--mat-sys-body-large-weight)); - letter-spacing: var(--mat-list-list-item-label-text-tracking, var(--mat-sys-body-large-tracking)); -} -.mdc-list-item:hover .mdc-list-item__primary-text { - color: var(--mat-list-list-item-hover-label-text-color, var(--mat-sys-on-surface)); -} -.mdc-list-item:focus .mdc-list-item__primary-text { - color: var(--mat-list-list-item-focus-label-text-color, var(--mat-sys-on-surface)); -} -.mdc-list-item--with-two-lines .mdc-list-item__primary-text, .mdc-list-item--with-three-lines .mdc-list-item__primary-text { - display: block; - margin-top: 0; - line-height: normal; - margin-bottom: -20px; -} -.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before, .mdc-list-item--with-three-lines .mdc-list-item__primary-text::before { - display: inline-block; - width: 0; - height: 28px; - content: ""; - vertical-align: 0; -} -.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after, .mdc-list-item--with-three-lines .mdc-list-item__primary-text::after { - display: inline-block; - width: 0; - height: 20px; - content: ""; - vertical-align: -20px; -} - -.mdc-list-item__secondary-text { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - display: block; - margin-top: 0; - color: var(--mat-list-list-item-supporting-text-color, var(--mat-sys-on-surface-variant)); - font-family: var(--mat-list-list-item-supporting-text-font, var(--mat-sys-body-medium-font)); - line-height: var(--mat-list-list-item-supporting-text-line-height, var(--mat-sys-body-medium-line-height)); - font-size: var(--mat-list-list-item-supporting-text-size, var(--mat-sys-body-medium-size)); - font-weight: var(--mat-list-list-item-supporting-text-weight, var(--mat-sys-body-medium-weight)); - letter-spacing: var(--mat-list-list-item-supporting-text-tracking, var(--mat-sys-body-medium-tracking)); -} -.mdc-list-item__secondary-text::before { - display: inline-block; - width: 0; - height: 20px; - content: ""; - vertical-align: 0; -} -.mdc-list-item--with-three-lines .mdc-list-item__secondary-text { - white-space: normal; - line-height: 20px; -} -.mdc-list-item--with-overline .mdc-list-item__secondary-text { - white-space: nowrap; - line-height: auto; -} - -.mdc-list-item--with-leading-radio.mdc-list-item, -.mdc-list-item--with-leading-checkbox.mdc-list-item, -.mdc-list-item--with-leading-icon.mdc-list-item, -.mdc-list-item--with-leading-avatar.mdc-list-item { - padding-left: 0; - padding-right: 16px; -} -[dir=rtl] .mdc-list-item--with-leading-radio.mdc-list-item, -[dir=rtl] .mdc-list-item--with-leading-checkbox.mdc-list-item, -[dir=rtl] .mdc-list-item--with-leading-icon.mdc-list-item, -[dir=rtl] .mdc-list-item--with-leading-avatar.mdc-list-item { - padding-left: 16px; - padding-right: 0; -} -.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines .mdc-list-item__primary-text, -.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines .mdc-list-item__primary-text, -.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines .mdc-list-item__primary-text, -.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines .mdc-list-item__primary-text { - display: block; - margin-top: 0; - line-height: normal; - margin-bottom: -20px; -} -.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before, -.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before, -.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before, -.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before { - display: inline-block; - width: 0; - height: 32px; - content: ""; - vertical-align: 0; -} -.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after, -.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after, -.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after, -.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after { - display: inline-block; - width: 0; - height: 20px; - content: ""; - vertical-align: -20px; -} -.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end, -.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end, -.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end, -.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end { - display: block; - margin-top: 0; - line-height: normal; -} -.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before, -.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before, -.mdc-list-item--with-leading-icon.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before, -.mdc-list-item--with-leading-avatar.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before { - display: inline-block; - width: 0; - height: 32px; - content: ""; - vertical-align: 0; -} - -.mdc-list-item--with-trailing-icon.mdc-list-item, [dir=rtl] .mdc-list-item--with-trailing-icon.mdc-list-item { - padding-left: 0; - padding-right: 0; -} -.mdc-list-item--with-trailing-icon .mdc-list-item__end { - margin-left: 16px; - margin-right: 16px; -} - -.mdc-list-item--with-trailing-meta.mdc-list-item { - padding-left: 16px; - padding-right: 0; -} -[dir=rtl] .mdc-list-item--with-trailing-meta.mdc-list-item { - padding-left: 0; - padding-right: 16px; -} -.mdc-list-item--with-trailing-meta .mdc-list-item__end { - -webkit-user-select: none; - user-select: none; - margin-left: 28px; - margin-right: 16px; -} -[dir=rtl] .mdc-list-item--with-trailing-meta .mdc-list-item__end { - margin-left: 16px; - margin-right: 28px; -} -.mdc-list-item--with-trailing-meta.mdc-list-item--with-three-lines .mdc-list-item__end, .mdc-list-item--with-trailing-meta.mdc-list-item--with-two-lines .mdc-list-item__end { - display: block; - line-height: normal; - align-self: flex-start; - margin-top: 0; -} -.mdc-list-item--with-trailing-meta.mdc-list-item--with-three-lines .mdc-list-item__end::before, .mdc-list-item--with-trailing-meta.mdc-list-item--with-two-lines .mdc-list-item__end::before { - display: inline-block; - width: 0; - height: 28px; - content: ""; - vertical-align: 0; -} - -.mdc-list-item--with-leading-radio .mdc-list-item__start, -.mdc-list-item--with-leading-checkbox .mdc-list-item__start { - margin-left: 8px; - margin-right: 24px; -} -[dir=rtl] .mdc-list-item--with-leading-radio .mdc-list-item__start, -[dir=rtl] .mdc-list-item--with-leading-checkbox .mdc-list-item__start { - margin-left: 24px; - margin-right: 8px; -} -.mdc-list-item--with-leading-radio.mdc-list-item--with-two-lines .mdc-list-item__start, -.mdc-list-item--with-leading-checkbox.mdc-list-item--with-two-lines .mdc-list-item__start { - align-self: flex-start; - margin-top: 8px; -} - -.mdc-list-item--with-trailing-radio.mdc-list-item, -.mdc-list-item--with-trailing-checkbox.mdc-list-item { - padding-left: 16px; - padding-right: 0; -} -[dir=rtl] .mdc-list-item--with-trailing-radio.mdc-list-item, -[dir=rtl] .mdc-list-item--with-trailing-checkbox.mdc-list-item { - padding-left: 0; - padding-right: 16px; -} -.mdc-list-item--with-trailing-radio.mdc-list-item--with-leading-icon, .mdc-list-item--with-trailing-radio.mdc-list-item--with-leading-avatar, -.mdc-list-item--with-trailing-checkbox.mdc-list-item--with-leading-icon, -.mdc-list-item--with-trailing-checkbox.mdc-list-item--with-leading-avatar { - padding-left: 0; -} -[dir=rtl] .mdc-list-item--with-trailing-radio.mdc-list-item--with-leading-icon, [dir=rtl] .mdc-list-item--with-trailing-radio.mdc-list-item--with-leading-avatar, -[dir=rtl] .mdc-list-item--with-trailing-checkbox.mdc-list-item--with-leading-icon, -[dir=rtl] .mdc-list-item--with-trailing-checkbox.mdc-list-item--with-leading-avatar { - padding-right: 0; -} -.mdc-list-item--with-trailing-radio .mdc-list-item__end, -.mdc-list-item--with-trailing-checkbox .mdc-list-item__end { - margin-left: 24px; - margin-right: 8px; -} -[dir=rtl] .mdc-list-item--with-trailing-radio .mdc-list-item__end, -[dir=rtl] .mdc-list-item--with-trailing-checkbox .mdc-list-item__end { - margin-left: 8px; - margin-right: 24px; -} -.mdc-list-item--with-trailing-radio.mdc-list-item--with-three-lines .mdc-list-item__end, -.mdc-list-item--with-trailing-checkbox.mdc-list-item--with-three-lines .mdc-list-item__end { - align-self: flex-start; - margin-top: 8px; -} - -.mdc-list-group__subheader { - margin: 0.75rem 16px; -} - -.mdc-list-item--disabled .mdc-list-item__start, -.mdc-list-item--disabled .mdc-list-item__content, -.mdc-list-item--disabled .mdc-list-item__end { - opacity: 1; -} -.mdc-list-item--disabled .mdc-list-item__primary-text, -.mdc-list-item--disabled .mdc-list-item__secondary-text { - opacity: var(--mat-list-list-item-disabled-label-text-opacity, 0.3); -} -.mdc-list-item--disabled.mdc-list-item--with-leading-icon .mdc-list-item__start { - color: var(--mat-list-list-item-disabled-leading-icon-color, var(--mat-sys-on-surface)); - opacity: var(--mat-list-list-item-disabled-leading-icon-opacity, 0.38); -} -.mdc-list-item--disabled.mdc-list-item--with-trailing-icon .mdc-list-item__end { - color: var(--mat-list-list-item-disabled-trailing-icon-color, var(--mat-sys-on-surface)); - opacity: var(--mat-list-list-item-disabled-trailing-icon-opacity, 0.38); -} - -.mat-mdc-list-item.mat-mdc-list-item-both-leading-and-trailing, [dir=rtl] .mat-mdc-list-item.mat-mdc-list-item-both-leading-and-trailing { - padding-left: 0; - padding-right: 0; -} - -.mdc-list-item.mdc-list-item--disabled .mdc-list-item__primary-text { - color: var(--mat-list-list-item-disabled-label-text-color, var(--mat-sys-on-surface)); -} - -.mdc-list-item:hover::before { - background-color: var(--mat-list-list-item-hover-state-layer-color, var(--mat-sys-on-surface)); - opacity: var(--mat-list-list-item-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); -} - -.mdc-list-item.mdc-list-item--disabled::before { - background-color: var(--mat-list-list-item-disabled-state-layer-color, var(--mat-sys-on-surface)); - opacity: var(--mat-list-list-item-disabled-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); -} - -.mdc-list-item:focus::before { - background-color: var(--mat-list-list-item-focus-state-layer-color, var(--mat-sys-on-surface)); - opacity: var(--mat-list-list-item-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); -} - -.mdc-list-item--disabled .mdc-radio, -.mdc-list-item--disabled .mdc-checkbox { - opacity: var(--mat-list-list-item-disabled-label-text-opacity, 0.3); -} - -.mdc-list-item--with-leading-avatar .mat-mdc-list-item-avatar { - border-radius: var(--mat-list-list-item-leading-avatar-shape, var(--mat-sys-corner-full)); - background-color: var(--mat-list-list-item-leading-avatar-color, var(--mat-sys-primary-container)); -} - -.mat-mdc-list-item-icon { - font-size: var(--mat-list-list-item-leading-icon-size, 24px); -} - -@media (forced-colors: active) { - a.mdc-list-item--activated::after { - content: ""; - position: absolute; - top: 50%; - right: 16px; - transform: translateY(-50%); - width: 10px; - height: 0; - border-bottom: solid 10px; - border-radius: 10px; - } - a.mdc-list-item--activated [dir=rtl]::after { - right: auto; - left: 16px; - } -} - -.mat-mdc-list-base { - display: block; -} -.mat-mdc-list-base .mdc-list-item__start, -.mat-mdc-list-base .mdc-list-item__end, -.mat-mdc-list-base .mdc-list-item__content { - pointer-events: auto; -} - -.mat-mdc-list-item, -.mat-mdc-list-option { - width: 100%; - box-sizing: border-box; - -webkit-tap-highlight-color: transparent; -} -.mat-mdc-list-item:not(.mat-mdc-list-item-interactive), -.mat-mdc-list-option:not(.mat-mdc-list-item-interactive) { - cursor: default; -} -.mat-mdc-list-item .mat-divider-inset, -.mat-mdc-list-option .mat-divider-inset { - position: absolute; - left: 0; - right: 0; - bottom: 0; -} -.mat-mdc-list-item .mat-mdc-list-item-avatar ~ .mat-divider-inset, -.mat-mdc-list-option .mat-mdc-list-item-avatar ~ .mat-divider-inset { - margin-left: 72px; -} -[dir=rtl] .mat-mdc-list-item .mat-mdc-list-item-avatar ~ .mat-divider-inset, -[dir=rtl] .mat-mdc-list-option .mat-mdc-list-item-avatar ~ .mat-divider-inset { - margin-right: 72px; -} - -.mat-mdc-list-item-interactive::before { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - content: ""; - opacity: 0; - pointer-events: none; - border-radius: inherit; -} - -.mat-mdc-list-item > .mat-focus-indicator { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - pointer-events: none; -} -.mat-mdc-list-item:focus-visible > .mat-focus-indicator::before { - content: ""; -} - -.mat-mdc-list-item.mdc-list-item--with-three-lines .mat-mdc-list-item-line.mdc-list-item__secondary-text { - white-space: nowrap; - line-height: normal; -} -.mat-mdc-list-item.mdc-list-item--with-three-lines .mat-mdc-list-item-unscoped-content.mdc-list-item__secondary-text { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; -} - -mat-action-list button { - background: none; - color: inherit; - border: none; - font: inherit; - outline: inherit; - -webkit-tap-highlight-color: transparent; - text-align: start; -} -mat-action-list button::-moz-focus-inner { - border: 0; -} - -.mdc-list-item--with-leading-icon .mdc-list-item__start { - margin-inline-start: var(--mat-list-list-item-leading-icon-start-space, 16px); - margin-inline-end: var(--mat-list-list-item-leading-icon-end-space, 16px); -} - -.mat-mdc-nav-list .mat-mdc-list-item { - border-radius: var(--mat-list-active-indicator-shape, var(--mat-sys-corner-full)); - --mat-focus-indicator-border-radius: var(--mat-list-active-indicator-shape, var(--mat-sys-corner-full)); -} -.mat-mdc-nav-list .mat-mdc-list-item.mdc-list-item--activated { - background-color: var(--mat-list-active-indicator-color, var(--mat-sys-secondary-container)); -} -`,er=["unscopedContent"],ir=["text"],nr=[[["","matListItemAvatar",""],["","matListItemIcon",""]],[["","matListItemTitle",""]],[["","matListItemLine",""]],"*",[["","matListItemMeta",""]],[["mat-divider"]]],ar=["[matListItemAvatar],[matListItemIcon]","[matListItemTitle]","[matListItemLine]","*","[matListItemMeta]","mat-divider"];var or=new w("ListOption"),rr=(()=>{class n{_elementRef=r(O);constructor(){}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matListItemTitle",""]],hostAttrs:[1,"mat-mdc-list-item-title","mdc-list-item__primary-text"]})}return n})(),sr=(()=>{class n{_elementRef=r(O);constructor(){}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matListItemLine",""]],hostAttrs:[1,"mat-mdc-list-item-line","mdc-list-item__secondary-text"]})}return n})(),lr=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,selectors:[["","matListItemMeta",""]],hostAttrs:[1,"mat-mdc-list-item-meta","mdc-list-item__end"]})}return n})(),ta=(()=>{class n{_listOption=r(or,{optional:!0});constructor(){}_isAlignedAtStart(){return!this._listOption||this._listOption?._getTogglePosition()==="after"}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,hostVars:4,hostBindings:function(e,i){e&2&&_("mdc-list-item__start",i._isAlignedAtStart())("mdc-list-item__end",!i._isAlignedAtStart())}})}return n})(),cr=(()=>{class n extends ta{static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275dir=x({type:n,selectors:[["","matListItemAvatar",""]],hostAttrs:[1,"mat-mdc-list-item-avatar"],features:[q]})}return n})(),dr=(()=>{class n extends ta{static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275dir=x({type:n,selectors:[["","matListItemIcon",""]],hostAttrs:[1,"mat-mdc-list-item-icon"],features:[q]})}return n})(),mr=new w("MAT_LIST_CONFIG"),Xt=(()=>{class n{_isNonInteractive=!0;get disableRipple(){return this._disableRipple}set disableRipple(t){this._disableRipple=K(t)}_disableRipple=!1;get disabled(){return this._disabled()}set disabled(t){this._disabled.set(K(t))}_disabled=Z(!1);_defaultOptions=r(mr,{optional:!0});static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,hostVars:1,hostBindings:function(e,i){e&2&&T("aria-disabled",i.disabled)},inputs:{disableRipple:"disableRipple",disabled:"disabled"}})}return n})(),hr=(()=>{class n{_elementRef=r(O);_ngZone=r(H);_listBase=r(Xt,{optional:!0});_platform=r(ot);_hostElement;_isButtonElement;_noopAnimations=Q();_avatars;_icons;set lines(t){this._explicitLines=Gt(t,null),this._updateItemLines(!1)}_explicitLines=null;get disableRipple(){return this.disabled||this._disableRipple||this._noopAnimations||!!this._listBase?.disableRipple}set disableRipple(t){this._disableRipple=K(t)}_disableRipple=!1;get disabled(){return this._disabled()||!!this._listBase?.disabled}set disabled(t){this._disabled.set(K(t))}_disabled=Z(!1);_subscriptions=new it;_rippleRenderer=null;_hasUnscopedTextContent=!1;rippleConfig;get rippleDisabled(){return this.disableRipple||!!this.rippleConfig.disabled}constructor(){r(wt).load(Ct);let t=r(Yt,{optional:!0});this.rippleConfig=t||{},this._hostElement=this._elementRef.nativeElement,this._isButtonElement=this._hostElement.nodeName.toLowerCase()==="button",this._listBase&&!this._listBase._isNonInteractive&&this._initInteractiveListItem(),this._isButtonElement&&!this._hostElement.hasAttribute("type")&&this._hostElement.setAttribute("type","button")}ngAfterViewInit(){this._monitorProjectedLinesAndTitle(),this._updateItemLines(!0)}ngOnDestroy(){this._subscriptions.unsubscribe(),this._rippleRenderer!==null&&this._rippleRenderer._removeTriggerEvents()}_hasIconOrAvatar(){return!!(this._avatars.length||this._icons.length)}_initInteractiveListItem(){this._hostElement.classList.add("mat-mdc-list-item-interactive"),this._rippleRenderer=new De(this,this._ngZone,this._hostElement,this._platform,r(G)),this._rippleRenderer.setupTriggerEvents(this._hostElement)}_monitorProjectedLinesAndTitle(){this._ngZone.runOutsideAngular(()=>{this._subscriptions.add(J(this._lines.changes,this._titles.changes).subscribe(()=>this._updateItemLines(!1)))})}_updateItemLines(t){if(!this._lines||!this._titles||!this._unscopedContent)return;t&&this._checkDomForUnscopedTextContent();let e=this._explicitLines??this._inferLinesFromContent(),i=this._unscopedContent.nativeElement;if(this._hostElement.classList.toggle("mat-mdc-list-item-single-line",e<=1),this._hostElement.classList.toggle("mdc-list-item--with-one-line",e<=1),this._hostElement.classList.toggle("mdc-list-item--with-two-lines",e===2),this._hostElement.classList.toggle("mdc-list-item--with-three-lines",e===3),this._hasUnscopedTextContent){let a=this._titles.length===0&&e===1;i.classList.toggle("mdc-list-item__primary-text",a),i.classList.toggle("mdc-list-item__secondary-text",!a)}else i.classList.remove("mdc-list-item__primary-text"),i.classList.remove("mdc-list-item__secondary-text")}_inferLinesFromContent(){let t=this._titles.length+this._lines.length;return this._hasUnscopedTextContent&&(t+=1),t}_checkDomForUnscopedTextContent(){this._hasUnscopedTextContent=Array.from(this._unscopedContent.nativeElement.childNodes).filter(t=>t.nodeType!==t.COMMENT_NODE).some(t=>!!(t.textContent&&t.textContent.trim()))}static \u0275fac=function(e){return new(e||n)};static \u0275dir=x({type:n,contentQueries:function(e,i,a){if(e&1&&et(a,cr,4)(a,dr,4),e&2){let o;m(o=h())&&(i._avatars=o),m(o=h())&&(i._icons=o)}},hostVars:4,hostBindings:function(e,i){e&2&&(T("aria-disabled",i.disabled)("disabled",i._isButtonElement&&i.disabled||null),_("mdc-list-item--disabled",i.disabled))},inputs:{lines:"lines",disableRipple:"disableRipple",disabled:"disabled"}})}return n})();var Od=(()=>{class n extends Xt{static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275cmp=C({type:n,selectors:[["mat-list"]],hostAttrs:[1,"mat-mdc-list","mat-mdc-list-base","mdc-list"],exportAs:["matList"],features:[$([{provide:Xt,useExisting:n}]),q],ngContentSelectors:Xn,decls:1,vars:0,template:function(e,i){e&1&&(B(),y(0))},styles:[Jn],encapsulation:2,changeDetection:0})}return n})(),Rd=(()=>{class n extends hr{_lines;_titles;_meta;_unscopedContent;_itemText;get activated(){return this._activated}set activated(t){this._activated=K(t)}_activated=!1;_getAriaCurrent(){return this._hostElement.nodeName==="A"&&this._activated?"page":null}_hasBothLeadingAndTrailing(){return this._meta.length!==0&&(this._avatars.length!==0||this._icons.length!==0)}static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275cmp=C({type:n,selectors:[["mat-list-item"],["a","mat-list-item",""],["button","mat-list-item",""]],contentQueries:function(e,i,a){if(e&1&&et(a,sr,5)(a,rr,5)(a,lr,5),e&2){let o;m(o=h())&&(i._lines=o),m(o=h())&&(i._titles=o),m(o=h())&&(i._meta=o)}},viewQuery:function(e,i){if(e&1&&V(er,5)(ir,5),e&2){let a;m(a=h())&&(i._unscopedContent=a.first),m(a=h())&&(i._itemText=a.first)}},hostAttrs:[1,"mat-mdc-list-item","mdc-list-item"],hostVars:13,hostBindings:function(e,i){e&2&&(T("aria-current",i._getAriaCurrent()),_("mdc-list-item--activated",i.activated)("mdc-list-item--with-leading-avatar",i._avatars.length!==0)("mdc-list-item--with-leading-icon",i._icons.length!==0)("mdc-list-item--with-trailing-meta",i._meta.length!==0)("mat-mdc-list-item-both-leading-and-trailing",i._hasBothLeadingAndTrailing())("_mat-animation-noopable",i._noopAnimations))},inputs:{activated:"activated"},exportAs:["matListItem"],features:[q],ngContentSelectors:ar,decls:10,vars:0,consts:[["unscopedContent",""],[1,"mdc-list-item__content"],[1,"mat-mdc-list-item-unscoped-content",3,"cdkObserveContent"],[1,"mat-focus-indicator"]],template:function(e,i){e&1&&(B(nr),y(0),l(1,"span",1),y(2,1),y(3,2),l(4,"span",2,0),u("cdkObserveContent",function(){return i._updateItemLines(!0)}),y(6,3),d()(),y(7,4),y(8,5),M(9,"div",3))},dependencies:[Ut],encapsulation:2,changeDetection:0})}return n})();var Ad=(()=>{class n extends Xt{_isNonInteractive=!1;static \u0275fac=(()=>{let t;return function(i){return(t||(t=Y(n)))(i||n)}})();static \u0275cmp=C({type:n,selectors:[["mat-nav-list"]],hostAttrs:["role","navigation",1,"mat-mdc-nav-list","mat-mdc-list-base","mdc-list"],exportAs:["matNavList"],features:[$([{provide:Xt,useExisting:n}]),q],ngContentSelectors:Xn,decls:1,vars:0,template:function(e,i){e&1&&(B(),y(0))},styles:[Jn],encapsulation:2,changeDetection:0})}return n})();var Ld=(()=>{class n{static \u0275fac=function(e){return new(e||n)};static \u0275mod=N({type:n});static \u0275inj=z({imports:[ce,Kt,ge,j,ve]})}return n})();var fr=n=>({lightTheme:n}),ur=()=>({right:!0,left:!0,bottom:!0,top:!0}),ea=(n,s)=>s.name;function _r(n,s){if(n&1){let t=lt();l(0,"div",1),u("mousemove",function(i){R(t);let a=p();return A(a.dragWidget(i))},ye)("mouseup",function(){R(t);let i=p();return A(i.toggleDragging(!1))},ye),d()}}function br(n,s){n&1&&(mt(),l(0,"svg",25),M(1,"rect",26),d())}function gr(n,s){n&1&&(mt(),l(0,"svg",25),M(1,"rect",27),d())}function vr(n,s){n&1&&(mt(),l(0,"svg",25),M(1,"rect",28),d())}function yr(n,s){if(n&1&&(l(0,"div"),E(1),d()),n&2){let t=p().$implicit;c(),mi("",t.console_type,"://",t.console_host,":",t.console)}}function xr(n,s){n&1&&(l(0,"div"),E(1,"none"),d())}function kr(n,s){if(n&1&&(l(0,"div",21)(1,"div"),g(2,br,2,0,":svg:svg",25),g(3,gr,2,0,":svg:svg",25),g(4,vr,2,0,":svg:svg",25),E(5),d(),g(6,yr,2,3,"div"),g(7,xr,2,0,"div"),d()),n&2){let t=s.$implicit;c(2),v(t.status==="started"?2:-1),c(),v(t.status==="suspended"?3:-1),c(),v(t.status==="stopped"?4:-1),c(),ft(" ",t.name," "),c(),v(t.console!==null&&t.console!==void 0&&t.console_type!=="none"?6:-1),c(),v(t.console===null||t.console===void 0||t.console_type==="none"?7:-1)}}function wr(n,s){n&1&&(mt(),l(0,"svg",25),M(1,"rect",26),d())}function Cr(n,s){n&1&&(mt(),l(0,"svg",25),M(1,"rect",28),d())}function Tr(n,s){if(n&1&&(l(0,"div",24)(1,"div"),g(2,wr,2,0,":svg:svg",25),g(3,Cr,2,0,":svg:svg",25),E(4),d(),l(5,"div"),E(6),d()()),n&2){let t=s.$implicit,e=p(2);D("matTooltip",e.getComputeTooltip(t)),c(2),v(t.connected?2:-1),c(),v(t.connected?-1:3),c(),ft(" ",e.truncateComputeName(t.name)," "),c(2),di(" ",t.host,":",t.port," ")}}function Mr(n,s){if(n&1){let t=lt();l(0,"div",2),u("mousedown",function(){R(t);let i=p();return A(i.toggleDragging(!0))})("resizeStart",function(){R(t);let i=p();return A(i.toggleDragging(!1))})("resizeEnd",function(i){R(t);let a=p();return A(a.onResizeEnd(i))}),l(1,"div",3)(2,"mat-tab-group")(3,"mat-tab",4),u("click",function(){R(t);let i=p();return A(i.toggleTopologyVisibility(!0))}),l(4,"div",5)(5,"div",6)(6,"mat-select",7)(7,"mat-optgroup",8)(8,"mat-option",9),u("onSelectionChange",function(){R(t);let i=p();return A(i.applyStatusFilter("started"))}),E(9,"started"),d(),l(10,"mat-option",10),u("onSelectionChange",function(){R(t);let i=p();return A(i.applyStatusFilter("suspended"))}),E(11,"suspended"),d(),l(12,"mat-option",11),u("onSelectionChange",function(){R(t);let i=p();return A(i.applyStatusFilter("stopped"))}),E(13,"stopped"),d()(),l(14,"mat-optgroup",12)(15,"mat-option",13),u("onSelectionChange",function(){R(t);let i=p();return A(i.applyCaptureFilter("capture"))}),E(16,"active capture(s)"),d(),l(17,"mat-option",14),u("onSelectionChange",function(){R(t);let i=p();return A(i.applyCaptureFilter("packet"))}),E(18,"active packet captures"),d()()()(),l(19,"div",15)(20,"mat-select",16),u("selectionChange",function(){R(t);let i=p();return A(i.setSortingOrder())}),fi("valueChange",function(i){R(t);let a=p();return pi(a.sortingOrder,i)||(a.sortingOrder=i),A(i)}),l(21,"mat-option",17),E(22,"sort by name ascending"),d(),l(23,"mat-option",18),E(24,"sort by name descending"),d()()(),M(25,"mat-divider",19),l(26,"div",20),Et(27,kr,8,6,"div",21,ea),d()()(),l(29,"mat-tab",22),u("click",function(){R(t);let i=p();return A(i.toggleTopologyVisibility(!1))}),l(30,"div",5)(31,"div",23),Et(32,Tr,7,6,"div",24,ea),d()()()()()()}if(n&2){let t=p();D("ngStyle",t.style)("ngClass",_i(7,fr,t.isLightThemeEnabled))("validateResize",t.validate)("resizeEdges",ui(9,ur))("enableGhostResize",!0),c(20),hi("value",t.sortingOrder),c(6),D("ngStyle",t.styleInside),c(),Ft(t.filteredNodes),c(5),Ft(t.computes)}}var im=(()=>{class n{nodesDataSource=r(Hn);projectService=r(Gn);computeService=r($n);linksDataSource=r(Vn);themeService=r(qn);notificationService=r(Un);toasterService=r(Zn);cd=r(W);controller;project;computesInitialized=!1;closeTopologySummary=new F;style={};styleInside={height:"280px"};subscriptions=[];projectsStatistics;nodes=[];filteredNodes=[];sortingOrder="asc";startedStatusFilterEnabled=!1;suspendedStatusFilterEnabled=!1;stoppedStatusFilterEnabled=!1;captureFilterEnabled=!1;packetFilterEnabled=!1;computes=[];isTopologyVisible=!0;isDraggingEnabled=!1;isLightThemeEnabled=!1;constructor(){}ngOnInit(){this.themeService.getActualTheme()==="light"?this.isLightThemeEnabled=!0:this.isLightThemeEnabled=!1,this.subscriptions.push(this.nodesDataSource.changes.subscribe(t=>{let e=[...t];this.nodes=e,e.forEach(i=>{(i.console_host==="0.0.0.0"||i.console_host==="0:0:0:0:0:0:0:0"||i.console_host==="::")&&(i.console_host=this.controller?.host)}),this.filteredNodes=this.sortingOrder==="asc"?e.sort(this.compareAsc):e.sort(this.compareDesc),this.cd.markForCheck()})),setTimeout(()=>{this.initializeComputesAndNotifications()},0),this.revertPosition()}initializeComputesAndNotifications(){if(!(!this.controller||!this.project||this.computesInitialized)){if(this.computesInitialized=!0,this.projectService.getStatistics(this.controller,this.project.project_id).subscribe({next:t=>{this.projectsStatistics=t,this.cd.markForCheck()},error:t=>{let e=t.error?.message||t.message||"Failed to load project statistics";this.toasterService.error(e),this.cd.markForCheck()}}),this.notificationService.hasCachedData()){let t=this.notificationService.getCachedComputes();this.computes=t,this.cd.markForCheck()}else this.computeService.getComputes(this.controller).subscribe({next:t=>{this.notificationService.setInitialComputes(t),this.computes=t,this.cd.markForCheck()},error:t=>{let e=t.error?.message||t.message||"Failed to load computes";this.toasterService.error(e),this.cd.markForCheck()}});this.subscriptions.push(this.notificationService.computeNotificationEmitter.subscribe(t=>{this.handleComputeNotification(t)})),this.subscriptions.push(this.notificationService.computeCacheUpdated.subscribe(t=>{this.computes=t,this.cd.markForCheck()}))}}revertPosition(){let t=localStorage.getItem("leftPosition"),e=localStorage.getItem("rightPosition"),i=localStorage.getItem("topPosition"),a=localStorage.getItem("widthOfWidget"),o=localStorage.getItem("heightOfWidget");i?this.style={position:"fixed",left:`${+t}px`,right:`${+e}px`,top:`${+i}px`,width:`${+a}px`,height:`${+o}px`}:this.style={top:"60px",right:"0px",width:"320px",height:"400px"}}toggleDragging(t){this.isDraggingEnabled=t}dragWidget(t){let e=Number(t.movementX),i=Number(t.movementY),a=Number(this.style.width.split("px")[0]),o=Number(this.style.height.split("px")[0]),f=Number(this.style.top.split("px")[0])+i;if(this.style.left){let b=Number(this.style.left.split("px")[0])+e;this.style={position:"fixed",left:`${b}px`,top:`${f}px`,width:`${a}px`,height:`${o}px`},localStorage.setItem("leftPosition",b.toString()),localStorage.setItem("topPosition",f.toString()),localStorage.setItem("widthOfWidget",a.toString()),localStorage.setItem("heightOfWidget",o.toString())}else{let b=Number(this.style.right.split("px")[0])-e;this.style={position:"fixed",right:`${b}px`,top:`${f}px`,width:`${a}px`,height:`${o}px`},localStorage.setItem("rightPosition",b.toString()),localStorage.setItem("topPosition",f.toString()),localStorage.setItem("widthOfWidget",a.toString()),localStorage.setItem("heightOfWidget",o.toString())}}validate(t){return!(t.rectangle.width&&t.rectangle.height&&(t.rectangle.width<290||t.rectangle.height<260))}onResizeEnd(t){this.style={position:"fixed",left:`${t.rectangle.left}px`,top:`${t.rectangle.top}px`,width:`${t.rectangle.width}px`,height:`${t.rectangle.height}px`},this.styleInside={height:`${t.rectangle.height-120}px`}}toggleTopologyVisibility(t){this.isTopologyVisible=t,this.revertPosition()}compareAsc(t,e){return t.namei.compute_id===t.event.compute_id)!==-1?this.computes=this.computes.map(i=>i.compute_id===t.event.compute_id?t.event:i):this.computes=[...this.computes,t.event];break;case"compute.deleted":this.computes=this.computes.filter(i=>i.compute_id!==t.event.compute_id);break}this.cd.markForCheck()}ngOnDestroy(){this.subscriptions.forEach(t=>t.unsubscribe())}setSortingOrder(){this.sortingOrder==="asc"?this.filteredNodes=this.filteredNodes.sort(this.compareAsc):this.filteredNodes=this.filteredNodes.sort(this.compareDesc)}applyStatusFilter(t){t==="started"?this.startedStatusFilterEnabled=!this.startedStatusFilterEnabled:t==="stopped"?this.stoppedStatusFilterEnabled=!this.stoppedStatusFilterEnabled:t==="suspended"&&(this.suspendedStatusFilterEnabled=!this.suspendedStatusFilterEnabled),this.applyFilters()}applyCaptureFilter(t){t==="capture"?this.captureFilterEnabled=!this.captureFilterEnabled:t==="packet"&&(this.packetFilterEnabled=!this.packetFilterEnabled),this.applyFilters()}applyFilters(){let t=[];this.startedStatusFilterEnabled&&(t=t.concat(this.nodes.filter(e=>e.status==="started"))),this.stoppedStatusFilterEnabled&&(t=t.concat(this.nodes.filter(e=>e.status==="stopped"))),this.suspendedStatusFilterEnabled&&(t=t.concat(this.nodes.filter(e=>e.status==="suspended"))),!this.startedStatusFilterEnabled&&!this.stoppedStatusFilterEnabled&&!this.suspendedStatusFilterEnabled&&(t=t.concat(this.nodes)),this.captureFilterEnabled&&(t=this.checkCapturing(t)),this.packetFilterEnabled&&(t=this.checkPacketFilters(t)),this.sortingOrder==="asc"?this.filteredNodes=t.sort(this.compareAsc):this.filteredNodes=t.sort(this.compareDesc)}checkCapturing(t){let e=this.linksDataSource.getItems(),i=[];e.forEach(o=>{o.capturing&&o.nodes.forEach(f=>{i.push(f.node_id)})});let a=[];return t.forEach(o=>{i.includes(o.node_id)&&a.push(o)}),a}checkPacketFilters(t){let e=this.linksDataSource.getItems(),i=[];e.forEach(o=>{(o.filters.bpf||o.filters.corrupt||o.filters.corrupt||o.filters.packet_loss||o.filters.frequency_drop)&&o.nodes.forEach(f=>{i.push(f.node_id)})});let a=[];return t.forEach(o=>{i.includes(o.node_id)&&a.push(o)}),a}close(){this.closeTopologySummary.emit(!1)}truncateComputeName(t){if(!t)return"";let e=15;return t.length<=e?t:t.substring(0,e)+"..."}getComputeTooltip(t){return t?[`Name: ${t.name||"N/A"}`,`Host: ${t.host}:${t.port}`,`Connected: ${t.connected?"Yes":"No"}`,t.cpu_usage_percent!=null?`CPU: ${t.cpu_usage_percent.toFixed(1)}%`:null,t.memory_usage_percent!=null?`Memory: ${t.memory_usage_percent.toFixed(1)}%`:null].filter(Boolean).join(` -`):""}static \u0275fac=function(e){return new(e||n)};static \u0275cmp=C({type:n,selectors:[["app-topology-summary"]],inputs:{controller:"controller",project:"project"},outputs:{closeTopologySummary:"closeTopologySummary"},decls:2,vars:2,consts:[["mwlResizable","",1,"summaryWrapper",3,"ngStyle","ngClass","validateResize","resizeEdges","enableGhostResize"],[3,"mousemove","mouseup"],["mwlResizable","",1,"summaryWrapper",3,"mousedown","resizeStart","resizeEnd","ngStyle","ngClass","validateResize","resizeEdges","enableGhostResize"],[1,"summaryHeader"],["label","Map topology",3,"click"],[1,"tabContent"],[1,"summaryFilters"],["placeholder","Filter nodes","multiple",""],["label","Status filter"],["value","started",3,"onSelectionChange"],["value","suspended",3,"onSelectionChange"],["value","stopped",3,"onSelectionChange"],["label","Capture filter"],["value","capture",3,"onSelectionChange"],["value","packet",3,"onSelectionChange"],[1,"summarySorting"],["placeholder","Sorting",3,"selectionChange","valueChange","value"],["value","asc"],["value","desc"],[1,"divider"],[1,"summaryContent",3,"ngStyle"],[1,"nodeRow"],["label","Computes",3,"click"],[1,"summaryContentComputes"],["matTooltipPosition","above",1,"nodeRow",3,"matTooltip"],["width","10","height","10"],["x","0","y","0","width","10","height","10","fill","green",1,"status_started"],["x","0","y","0","width","10","height","10","fill","yellow",1,"status_suspended"],["x","0","y","0","width","10","height","10","fill","red",1,"status_stopped"]],template:function(e,i){e&1&&(g(0,_r,1,0,"div"),g(1,Mr,34,10,"div",0)),e&2&&(v(i.isDraggingEnabled?0:-1),c(),v(i.projectsStatistics?1:-1))},dependencies:[ki,vi,yi,pn,Ae,hn,Bn,Pn,zt,je,Nt,ve,$e,jn,We],styles:["@media screen and (max-width:600px){.summaryWrapper[_ngcontent-%COMP%]{visibility:hidden}}mat-tab-group[_ngcontent-%COMP%]{width:100%}.summaryWrapper[_ngcontent-%COMP%]{box-shadow:0 4px 16px color-mix(in srgb,var(--mat-sys-shadow) 25%,transparent);position:fixed;top:60px;right:0;height:400px;width:320px;background:var(--mat-sys-surface);color:var(--mat-sys-on-surface);overflow:hidden;font-size:12px;margin:16px;border-radius:4px}.summaryHeaderMenu[_ngcontent-%COMP%]{height:24px}.summaryHeader[_ngcontent-%COMP%]{width:100%;display:flex}.summaryFilters[_ngcontent-%COMP%], .summarySorting[_ngcontent-%COMP%]{height:25px;margin-left:8px;margin-right:8px}.tabContent[_ngcontent-%COMP%]{padding:10px}.summaryContent[_ngcontent-%COMP%]{overflow:auto;scrollbar-color:darkgrey var(--mat-sys-surface);scrollbar-width:thin}.summaryContentComputes[_ngcontent-%COMP%]{max-height:350px;overflow:auto;scrollbar-color:darkgrey var(--mat-sys-surface);scrollbar-width:thin}.titleButton[_ngcontent-%COMP%]{margin-left:8px;margin-top:4px;outline:none;border-radius:0}.marked[_ngcontent-%COMP%]{color:var(--mat-sys-primary);border-bottom:2px solid var(--mat-sys-primary)}.divider[_ngcontent-%COMP%]{margin-top:8px;margin-bottom:8px;width:100%;height:2px}.nodeRow[_ngcontent-%COMP%]{width:100%;display:flex;justify-content:space-between;padding-right:8px}.nodeRow[_ngcontent-%COMP%] > div[_ngcontent-%COMP%]{display:flex;align-items:center;gap:6px}.nodeRow[_ngcontent-%COMP%] svg[_ngcontent-%COMP%]{flex-shrink:0;vertical-align:middle}.radio-group-wrapper[_ngcontent-%COMP%]{margin-top:8px}.radio-group[_ngcontent-%COMP%]{display:flex;justify-content:space-between}.closeButton[_ngcontent-%COMP%]{cursor:pointer;font-size:18px;margin-top:8px;margin-right:8px}.filterBox[_ngcontent-%COMP%]{display:flex;justify-content:space-between}.notvisible[_ngcontent-%COMP%]{display:none}"],changeDetection:0})}return n})();export{We as a,jn as b,Le as c,Xa as d,Pe as e,Ja as f,to as g,Be as h,ze as i,In as j,On as k,Fn as l,be as m,ue as n,_e as o,je as p,Ne as q,zt as r,Dn as s,En as t,Nt as u,Pn as v,El as w,Bn as x,$n as y,Un as z,Zt as A,Yn as B,qe as C,ad as D,Zn as E,Rl as F,Ll as G,$e as H,ve as I,Od as J,Rd as K,Ad as L,Ld as M,qn as N,ya as O,ka as P,Ae as Q,hn as R,Ma as S,Sa as T,ss as U,pn as V,Wn as W,Qn as X,Gn as Y,jt as Z,Vn as _,Hn as $,im as aa}; diff --git a/gns3server/static/web-ui/chunk-G7NALUVX.js b/gns3server/static/web-ui/chunk-G7NALUVX.js new file mode 100644 index 000000000..6c1e8735c --- /dev/null +++ b/gns3server/static/web-ui/chunk-G7NALUVX.js @@ -0,0 +1,1028 @@ +var $S=Object.create;var $a=Object.defineProperty,zS=Object.defineProperties,GS=Object.getOwnPropertyDescriptor,WS=Object.getOwnPropertyDescriptors,qS=Object.getOwnPropertyNames,Ha=Object.getOwnPropertySymbols,YS=Object.getPrototypeOf,Ud=Object.prototype.hasOwnProperty,Rv=Object.prototype.propertyIsEnumerable;var Av=(t,n,e)=>n in t?$a(t,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[n]=e,g=(t,n)=>{for(var e in n||={})Ud.call(n,e)&&Av(t,e,n[e]);if(Ha)for(var e of Ha(n))Rv.call(n,e)&&Av(t,e,n[e]);return t},F=(t,n)=>zS(t,WS(n));var ZS=(t,n)=>{var e={};for(var r in t)Ud.call(t,r)&&n.indexOf(r)<0&&(e[r]=t[r]);if(t!=null&&Ha)for(var r of Ha(t))n.indexOf(r)<0&&Rv.call(t,r)&&(e[r]=t[r]);return e};var o1=(t,n)=>()=>{try{return n||t((n={exports:{}}).exports,n),n.exports}catch(e){throw n=0,e}},i1=(t,n)=>{for(var e in n)$a(t,e,{get:n[e],enumerable:!0})},KS=(t,n,e,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of qS(n))!Ud.call(t,o)&&o!==e&&$a(t,o,{get:()=>n[o],enumerable:!(r=GS(n,o))||r.enumerable});return t};var s1=(t,n,e)=>(e=t!=null?$S(YS(t)):{},KS(n||!t||!t.__esModule?$a(e,"default",{value:t,enumerable:!0}):e,t));function R(t){return typeof t=="function"}function tr(t){let e=t(r=>{Error.call(r),r.stack=new Error().stack});return e.prototype=Object.create(Error.prototype),e.prototype.constructor=e,e}var za=tr(t=>function(e){t(this),this.message=e?`${e.length} errors occurred during unsubscription: +${e.map((r,o)=>`${o+1}) ${r.toString()}`).join(` + `)}`:"",this.name="UnsubscriptionError",this.errors=e});function Rr(t,n){if(t){let e=t.indexOf(n);0<=e&&t.splice(e,1)}}var G=class t{constructor(n){this.initialTeardown=n,this.closed=!1,this._parentage=null,this._finalizers=null}unsubscribe(){let n;if(!this.closed){this.closed=!0;let{_parentage:e}=this;if(e)if(this._parentage=null,Array.isArray(e))for(let i of e)i.remove(this);else e.remove(this);let{initialTeardown:r}=this;if(R(r))try{r()}catch(i){n=i instanceof za?i.errors:[i]}let{_finalizers:o}=this;if(o){this._finalizers=null;for(let i of o)try{Nv(i)}catch(s){n=n??[],s instanceof za?n=[...n,...s.errors]:n.push(s)}}if(n)throw new za(n)}}add(n){var e;if(n&&n!==this)if(this.closed)Nv(n);else{if(n instanceof t){if(n.closed||n._hasParent(this))return;n._addParent(this)}(this._finalizers=(e=this._finalizers)!==null&&e!==void 0?e:[]).push(n)}}_hasParent(n){let{_parentage:e}=this;return e===n||Array.isArray(e)&&e.includes(n)}_addParent(n){let{_parentage:e}=this;this._parentage=Array.isArray(e)?(e.push(n),e):e?[e,n]:n}_removeParent(n){let{_parentage:e}=this;e===n?this._parentage=null:Array.isArray(e)&&Rr(e,n)}remove(n){let{_finalizers:e}=this;e&&Rr(e,n),n instanceof t&&n._removeParent(this)}};G.EMPTY=(()=>{let t=new G;return t.closed=!0,t})();var Hd=G.EMPTY;function Ga(t){return t instanceof G||t&&"closed"in t&&R(t.remove)&&R(t.add)&&R(t.unsubscribe)}function Nv(t){R(t)?t():t.unsubscribe()}var Pt={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Co={setTimeout(t,n,...e){let{delegate:r}=Co;return r?.setTimeout?r.setTimeout(t,n,...e):setTimeout(t,n,...e)},clearTimeout(t){let{delegate:n}=Co;return(n?.clearTimeout||clearTimeout)(t)},delegate:void 0};function Wa(t){Co.setTimeout(()=>{let{onUnhandledError:n}=Pt;if(n)n(t);else throw t})}function Nr(){}var Ov=$d("C",void 0,void 0);function kv(t){return $d("E",void 0,t)}function Fv(t){return $d("N",t,void 0)}function $d(t,n,e){return{kind:t,value:n,error:e}}var Or=null;function Io(t){if(Pt.useDeprecatedSynchronousErrorHandling){let n=!Or;if(n&&(Or={errorThrown:!1,error:null}),t(),n){let{errorThrown:e,error:r}=Or;if(Or=null,e)throw r}}else t()}function Pv(t){Pt.useDeprecatedSynchronousErrorHandling&&Or&&(Or.errorThrown=!0,Or.error=t)}var kr=class extends G{constructor(n){super(),this.isStopped=!1,n?(this.destination=n,Ga(n)&&n.add(this)):this.destination=JS}static create(n,e,r){return new Lt(n,e,r)}next(n){this.isStopped?Gd(Fv(n),this):this._next(n)}error(n){this.isStopped?Gd(kv(n),this):(this.isStopped=!0,this._error(n))}complete(){this.isStopped?Gd(Ov,this):(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe(),this.destination=null)}_next(n){this.destination.next(n)}_error(n){try{this.destination.error(n)}finally{this.unsubscribe()}}_complete(){try{this.destination.complete()}finally{this.unsubscribe()}}},QS=Function.prototype.bind;function zd(t,n){return QS.call(t,n)}var Wd=class{constructor(n){this.partialObserver=n}next(n){let{partialObserver:e}=this;if(e.next)try{e.next(n)}catch(r){qa(r)}}error(n){let{partialObserver:e}=this;if(e.error)try{e.error(n)}catch(r){qa(r)}else qa(n)}complete(){let{partialObserver:n}=this;if(n.complete)try{n.complete()}catch(e){qa(e)}}},Lt=class extends kr{constructor(n,e,r){super();let o;if(R(n)||!n)o={next:n??void 0,error:e??void 0,complete:r??void 0};else{let i;this&&Pt.useDeprecatedNextContext?(i=Object.create(n),i.unsubscribe=()=>this.unsubscribe(),o={next:n.next&&zd(n.next,i),error:n.error&&zd(n.error,i),complete:n.complete&&zd(n.complete,i)}):o=n}this.destination=new Wd(o)}};function qa(t){Pt.useDeprecatedSynchronousErrorHandling?Pv(t):Wa(t)}function XS(t){throw t}function Gd(t,n){let{onStoppedNotification:e}=Pt;e&&Co.setTimeout(()=>e(t,n))}var JS={closed:!0,next:Nr,error:XS,complete:Nr};var So=typeof Symbol=="function"&&Symbol.observable||"@@observable";function dt(t){return t}function qd(...t){return Yd(t)}function Yd(t){return t.length===0?dt:t.length===1?t[0]:function(e){return t.reduce((r,o)=>o(r),e)}}var P=(()=>{class t{constructor(e){e&&(this._subscribe=e)}lift(e){let r=new t;return r.source=this,r.operator=e,r}subscribe(e,r,o){let i=tM(e)?e:new Lt(e,r,o);return Io(()=>{let{operator:s,source:a}=this;i.add(s?s.call(i,a):a?this._subscribe(i):this._trySubscribe(i))}),i}_trySubscribe(e){try{return this._subscribe(e)}catch(r){e.error(r)}}forEach(e,r){return r=Lv(r),new r((o,i)=>{let s=new Lt({next:a=>{try{e(a)}catch(c){i(c),s.unsubscribe()}},error:i,complete:o});this.subscribe(s)})}_subscribe(e){var r;return(r=this.source)===null||r===void 0?void 0:r.subscribe(e)}[So](){return this}pipe(...e){return Yd(e)(this)}toPromise(e){return e=Lv(e),new e((r,o)=>{let i;this.subscribe(s=>i=s,s=>o(s),()=>r(i))})}}return t.create=n=>new t(n),t})();function Lv(t){var n;return(n=t??Pt.Promise)!==null&&n!==void 0?n:Promise}function eM(t){return t&&R(t.next)&&R(t.error)&&R(t.complete)}function tM(t){return t&&t instanceof kr||eM(t)&&Ga(t)}var jv=tr(t=>function(){t(this),this.name="ObjectUnsubscribedError",this.message="object unsubscribed"});var x=(()=>{class t extends P{constructor(){super(),this.closed=!1,this.currentObservers=null,this.observers=[],this.isStopped=!1,this.hasError=!1,this.thrownError=null}lift(e){let r=new Ya(this,this);return r.operator=e,r}_throwIfClosed(){if(this.closed)throw new jv}next(e){Io(()=>{if(this._throwIfClosed(),!this.isStopped){this.currentObservers||(this.currentObservers=Array.from(this.observers));for(let r of this.currentObservers)r.next(e)}})}error(e){Io(()=>{if(this._throwIfClosed(),!this.isStopped){this.hasError=this.isStopped=!0,this.thrownError=e;let{observers:r}=this;for(;r.length;)r.shift().error(e)}})}complete(){Io(()=>{if(this._throwIfClosed(),!this.isStopped){this.isStopped=!0;let{observers:e}=this;for(;e.length;)e.shift().complete()}})}unsubscribe(){this.isStopped=this.closed=!0,this.observers=this.currentObservers=null}get observed(){var e;return((e=this.observers)===null||e===void 0?void 0:e.length)>0}_trySubscribe(e){return this._throwIfClosed(),super._trySubscribe(e)}_subscribe(e){return this._throwIfClosed(),this._checkFinalizedStatuses(e),this._innerSubscribe(e)}_innerSubscribe(e){let{hasError:r,isStopped:o,observers:i}=this;return r||o?Hd:(this.currentObservers=null,i.push(e),new G(()=>{this.currentObservers=null,Rr(i,e)}))}_checkFinalizedStatuses(e){let{hasError:r,thrownError:o,isStopped:i}=this;r?e.error(o):i&&e.complete()}asObservable(){let e=new P;return e.source=this,e}}return t.create=(n,e)=>new Ya(n,e),t})(),Ya=class extends x{constructor(n,e){super(),this.destination=n,this.source=e}next(n){var e,r;(r=(e=this.destination)===null||e===void 0?void 0:e.next)===null||r===void 0||r.call(e,n)}error(n){var e,r;(r=(e=this.destination)===null||e===void 0?void 0:e.error)===null||r===void 0||r.call(e,n)}complete(){var n,e;(e=(n=this.destination)===null||n===void 0?void 0:n.complete)===null||e===void 0||e.call(n)}_subscribe(n){var e,r;return(r=(e=this.source)===null||e===void 0?void 0:e.subscribe(n))!==null&&r!==void 0?r:Hd}};function Zd(t){return R(t?.lift)}function k(t){return n=>{if(Zd(n))return n.lift(function(e){try{return t(e,this)}catch(r){this.error(r)}});throw new TypeError("Unable to lift unknown Observable type")}}function N(t,n,e,r,o){return new Kd(t,n,e,r,o)}var Kd=class extends kr{constructor(n,e,r,o,i,s){super(n),this.onFinalize=i,this.shouldUnsubscribe=s,this._next=e?function(a){try{e(a)}catch(c){n.error(c)}}:super._next,this._error=o?function(a){try{o(a)}catch(c){n.error(c)}finally{this.unsubscribe()}}:super._error,this._complete=r?function(){try{r()}catch(a){n.error(a)}finally{this.unsubscribe()}}:super._complete}unsubscribe(){var n;if(!this.shouldUnsubscribe||this.shouldUnsubscribe()){let{closed:e}=this;super.unsubscribe(),!e&&((n=this.onFinalize)===null||n===void 0||n.call(this))}}};function Bv(t,n,e,r){function o(i){return i instanceof e?i:new e(function(s){s(i)})}return new(e||(e=Promise))(function(i,s){function a(u){try{l(r.next(u))}catch(d){s(d)}}function c(u){try{l(r.throw(u))}catch(d){s(d)}}function l(u){u.done?i(u.value):o(u.value).then(a,c)}l((r=r.apply(t,n||[])).next())})}function Vv(t){var n=typeof Symbol=="function"&&Symbol.iterator,e=n&&t[n],r=0;if(e)return e.call(t);if(t&&typeof t.length=="number")return{next:function(){return t&&r>=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}};throw new TypeError(n?"Object is not iterable.":"Symbol.iterator is not defined.")}function Fr(t){return this instanceof Fr?(this.v=t,this):new Fr(t)}function Uv(t,n,e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var r=e.apply(t,n||[]),o,i=[];return o=Object.create((typeof AsyncIterator=="function"?AsyncIterator:Object).prototype),a("next"),a("throw"),a("return",s),o[Symbol.asyncIterator]=function(){return this},o;function s(p){return function(m){return Promise.resolve(m).then(p,d)}}function a(p,m){r[p]&&(o[p]=function(_){return new Promise(function(D,C){i.push([p,_,D,C])>1||c(p,_)})},m&&(o[p]=m(o[p])))}function c(p,m){try{l(r[p](m))}catch(_){h(i[0][3],_)}}function l(p){p.value instanceof Fr?Promise.resolve(p.value.v).then(u,d):h(i[0][2],p)}function u(p){c("next",p)}function d(p){c("throw",p)}function h(p,m){p(m),i.shift(),i.length&&c(i[0][0],i[0][1])}}function Hv(t){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var n=t[Symbol.asyncIterator],e;return n?n.call(t):(t=typeof Vv=="function"?Vv(t):t[Symbol.iterator](),e={},r("next"),r("throw"),r("return"),e[Symbol.asyncIterator]=function(){return this},e);function r(i){e[i]=t[i]&&function(s){return new Promise(function(a,c){s=t[i](s),o(a,c,s.done,s.value)})}}function o(i,s,a,c){Promise.resolve(c).then(function(l){i({value:l,done:a})},s)}}var Mo=t=>t&&typeof t.length=="number"&&typeof t!="function";function Za(t){return R(t?.then)}function Ka(t){return R(t[So])}function Qa(t){return Symbol.asyncIterator&&R(t?.[Symbol.asyncIterator])}function Xa(t){return new TypeError(`You provided ${t!==null&&typeof t=="object"?"an invalid object":`'${t}'`} where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`)}function nM(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Ja=nM();function ec(t){return R(t?.[Ja])}function tc(t){return Uv(this,arguments,function*(){let e=t.getReader();try{for(;;){let{value:r,done:o}=yield Fr(e.read());if(o)return yield Fr(void 0);yield yield Fr(r)}}finally{e.releaseLock()}})}function nc(t){return R(t?.getReader)}function X(t){if(t instanceof P)return t;if(t!=null){if(Ka(t))return rM(t);if(Mo(t))return oM(t);if(Za(t))return iM(t);if(Qa(t))return $v(t);if(ec(t))return sM(t);if(nc(t))return aM(t)}throw Xa(t)}function rM(t){return new P(n=>{let e=t[So]();if(R(e.subscribe))return e.subscribe(n);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function oM(t){return new P(n=>{for(let e=0;e{t.then(e=>{n.closed||(n.next(e),n.complete())},e=>n.error(e)).then(null,Wa)})}function sM(t){return new P(n=>{for(let e of t)if(n.next(e),n.closed)return;n.complete()})}function $v(t){return new P(n=>{cM(t,n).catch(e=>n.error(e))})}function aM(t){return $v(tc(t))}function cM(t,n){var e,r,o,i;return Bv(this,void 0,void 0,function*(){try{for(e=Hv(t);r=yield e.next(),!r.done;){let s=r.value;if(n.next(s),n.closed)return}}catch(s){o={error:s}}finally{try{r&&!r.done&&(i=e.return)&&(yield i.call(e))}finally{if(o)throw o.error}}n.complete()})}function jt(t){return k((n,e)=>{X(t).subscribe(N(e,()=>e.complete(),Nr)),!e.closed&&n.subscribe(e)})}function zv(){return k((t,n)=>{let e=null;t._refCount++;let r=N(n,void 0,void 0,void 0,()=>{if(!t||t._refCount<=0||0<--t._refCount){e=null;return}let o=t._connection,i=e;e=null,o&&(!i||o===i)&&o.unsubscribe(),n.unsubscribe()});t.subscribe(r),r.closed||(e=t.connect())})}var Ui=class extends P{constructor(n,e){super(),this.source=n,this.subjectFactory=e,this._subject=null,this._refCount=0,this._connection=null,Zd(n)&&(this.lift=n.lift)}_subscribe(n){return this.getSubject().subscribe(n)}getSubject(){let n=this._subject;return(!n||n.isStopped)&&(this._subject=this.subjectFactory()),this._subject}_teardown(){this._refCount=0;let{_connection:n}=this;this._subject=this._connection=null,n?.unsubscribe()}connect(){let n=this._connection;if(!n){n=this._connection=new G;let e=this.getSubject();n.add(this.source.subscribe(N(e,void 0,()=>{this._teardown(),e.complete()},r=>{this._teardown(),e.error(r)},()=>this._teardown()))),n.closed&&(this._connection=null,n=G.EMPTY)}return n}refCount(){return zv()(this)}};var To={schedule(t){let n=requestAnimationFrame,e=cancelAnimationFrame,{delegate:r}=To;r&&(n=r.requestAnimationFrame,e=r.cancelAnimationFrame);let o=n(i=>{e=void 0,t(i)});return new G(()=>e?.(o))},requestAnimationFrame(...t){let{delegate:n}=To;return(n?.requestAnimationFrame||requestAnimationFrame)(...t)},cancelAnimationFrame(...t){let{delegate:n}=To;return(n?.cancelAnimationFrame||cancelAnimationFrame)(...t)},delegate:void 0};var Ae=class extends x{constructor(n){super(),this._value=n}get value(){return this.getValue()}_subscribe(n){let e=super._subscribe(n);return!e.closed&&n.next(this._value),e}getValue(){let{hasError:n,thrownError:e,_value:r}=this;if(n)throw e;return this._throwIfClosed(),r}next(n){super.next(this._value=n)}};var Hi={now(){return(Hi.delegate||Date).now()},delegate:void 0};var $i=class extends x{constructor(n=1/0,e=1/0,r=Hi){super(),this._bufferSize=n,this._windowTime=e,this._timestampProvider=r,this._buffer=[],this._infiniteTimeWindow=!0,this._infiniteTimeWindow=e===1/0,this._bufferSize=Math.max(1,n),this._windowTime=Math.max(1,e)}next(n){let{isStopped:e,_buffer:r,_infiniteTimeWindow:o,_timestampProvider:i,_windowTime:s}=this;e||(r.push(n),!o&&r.push(i.now()+s)),this._trimBuffer(),super.next(n)}_subscribe(n){this._throwIfClosed(),this._trimBuffer();let e=this._innerSubscribe(n),{_infiniteTimeWindow:r,_buffer:o}=this,i=o.slice();for(let s=0;sGv(n)&&t()),n},clearImmediate(t){Gv(t)}};var{setImmediate:uM,clearImmediate:dM}=Wv,Gi={setImmediate(...t){let{delegate:n}=Gi;return(n?.setImmediate||uM)(...t)},clearImmediate(t){let{delegate:n}=Gi;return(n?.clearImmediate||dM)(t)},delegate:void 0};var oc=class extends nr{constructor(n,e){super(n,e),this.scheduler=n,this.work=e}requestAsyncId(n,e,r=0){return r!==null&&r>0?super.requestAsyncId(n,e,r):(n.actions.push(this),n._scheduled||(n._scheduled=Gi.setImmediate(n.flush.bind(n,void 0))))}recycleAsyncId(n,e,r=0){var o;if(r!=null?r>0:this.delay>0)return super.recycleAsyncId(n,e,r);let{actions:i}=n;e!=null&&((o=i[i.length-1])===null||o===void 0?void 0:o.id)!==e&&(Gi.clearImmediate(e),n._scheduled===e&&(n._scheduled=void 0))}};var xo=class t{constructor(n,e=t.now){this.schedulerActionCtor=n,this.now=e}schedule(n,e=0,r){return new this.schedulerActionCtor(this,n).schedule(r,e)}};xo.now=Hi.now;var rr=class extends xo{constructor(n,e=xo.now){super(n,e),this.actions=[],this._active=!1}flush(n){let{actions:e}=this;if(this._active){e.push(n);return}let r;this._active=!0;do if(r=n.execute(n.state,n.delay))break;while(n=e.shift());if(this._active=!1,r){for(;n=e.shift();)n.unsubscribe();throw r}}};var ic=class extends rr{flush(n){this._active=!0;let e=this._scheduled;this._scheduled=void 0;let{actions:r}=this,o;n=n||r.shift();do if(o=n.execute(n.state,n.delay))break;while((n=r[0])&&n.id===e&&r.shift());if(this._active=!1,o){for(;(n=r[0])&&n.id===e&&r.shift();)n.unsubscribe();throw o}}};var qv=new ic(oc);var Ct=new rr(nr),Jd=Ct;var sc=class extends nr{constructor(n,e){super(n,e),this.scheduler=n,this.work=e}requestAsyncId(n,e,r=0){return r!==null&&r>0?super.requestAsyncId(n,e,r):(n.actions.push(this),n._scheduled||(n._scheduled=To.requestAnimationFrame(()=>n.flush(void 0))))}recycleAsyncId(n,e,r=0){var o;if(r!=null?r>0:this.delay>0)return super.recycleAsyncId(n,e,r);let{actions:i}=n;e!=null&&e===n._scheduled&&((o=i[i.length-1])===null||o===void 0?void 0:o.id)!==e&&(To.cancelAnimationFrame(e),n._scheduled=void 0)}};var ac=class extends rr{flush(n){this._active=!0;let e;n?e=n.id:(e=this._scheduled,this._scheduled=void 0);let{actions:r}=this,o;n=n||r.shift();do if(o=n.execute(n.state,n.delay))break;while((n=r[0])&&n.id===e&&r.shift());if(this._active=!1,o){for(;(n=r[0])&&n.id===e&&r.shift();)n.unsubscribe();throw o}}};var Yv=new ac(sc);var Re=new P(t=>t.complete());function cc(t){return t&&R(t.schedule)}function ef(t){return t[t.length-1]}function lc(t){return R(ef(t))?t.pop():void 0}function an(t){return cc(ef(t))?t.pop():void 0}function Zv(t,n){return typeof ef(t)=="number"?t.pop():n}function We(t,n,e,r=0,o=!1){let i=n.schedule(function(){e(),o?t.add(this.schedule(null,r)):this.unsubscribe()},r);if(t.add(i),!o)return i}function uc(t,n=0){return k((e,r)=>{e.subscribe(N(r,o=>We(r,t,()=>r.next(o),n),()=>We(r,t,()=>r.complete(),n),o=>We(r,t,()=>r.error(o),n)))})}function Wi(t,n=0){return k((e,r)=>{r.add(t.schedule(()=>e.subscribe(r),n))})}function Kv(t,n){return X(t).pipe(Wi(n),uc(n))}function Qv(t,n){return X(t).pipe(Wi(n),uc(n))}function Xv(t,n){return new P(e=>{let r=0;return n.schedule(function(){r===t.length?e.complete():(e.next(t[r++]),e.closed||this.schedule())})})}function Jv(t,n){return new P(e=>{let r;return We(e,n,()=>{r=t[Ja](),We(e,n,()=>{let o,i;try{({value:o,done:i}=r.next())}catch(s){e.error(s);return}i?e.complete():e.next(o)},0,!0)}),()=>R(r?.return)&&r.return()})}function dc(t,n){if(!t)throw new Error("Iterable cannot be null");return new P(e=>{We(e,n,()=>{let r=t[Symbol.asyncIterator]();We(e,n,()=>{r.next().then(o=>{o.done?e.complete():e.next(o.value)})},0,!0)})})}function ey(t,n){return dc(tc(t),n)}function ty(t,n){if(t!=null){if(Ka(t))return Kv(t,n);if(Mo(t))return Xv(t,n);if(Za(t))return Qv(t,n);if(Qa(t))return dc(t,n);if(ec(t))return Jv(t,n);if(nc(t))return ey(t,n)}throw Xa(t)}function ae(t,n){return n?ty(t,n):X(t)}function S(...t){let n=an(t);return ae(t,n)}function xn(t,n){let e=R(t)?t:()=>t,r=o=>o.error(e());return new P(n?o=>n.schedule(r,0,o):r)}function Vt(t){return!!t&&(t instanceof P||R(t.lift)&&R(t.subscribe))}var An=tr(t=>function(){t(this),this.name="EmptyError",this.message="no elements in sequence"});function fM(t,n){let e=typeof n=="object";return new Promise((r,o)=>{let i=new Lt({next:s=>{r(s),i.unsubscribe()},error:o,complete:()=>{e?r(n.defaultValue):o(new An)}});t.subscribe(i)})}function fc(t){return t instanceof Date&&!isNaN(t)}var hM=tr(t=>function(e=null){t(this),this.message="Timeout has occurred",this.name="TimeoutError",this.info=e});function pM(t,n){let{first:e,each:r,with:o=mM,scheduler:i=n??Ct,meta:s=null}=fc(t)?{first:t}:typeof t=="number"?{each:t}:t;if(e==null&&r==null)throw new TypeError("No timeout provided.");return k((a,c)=>{let l,u,d=null,h=0,p=m=>{u=We(c,i,()=>{try{l.unsubscribe(),X(o({meta:s,lastValue:d,seen:h})).subscribe(c)}catch(_){c.error(_)}},m)};l=a.subscribe(N(c,m=>{u?.unsubscribe(),h++,c.next(d=m),r>0&&p(r)},void 0,void 0,()=>{u?.closed||u?.unsubscribe(),d=null})),!h&&p(e!=null?typeof e=="number"?e:+e-i.now():r)})}function mM(t){throw new hM(t)}function V(t,n){return k((e,r)=>{let o=0;e.subscribe(N(r,i=>{r.next(t.call(n,i,o++))}))})}var{isArray:gM}=Array;function vM(t,n){return gM(n)?t(...n):t(n)}function Ao(t){return V(n=>vM(t,n))}var{isArray:yM}=Array,{getPrototypeOf:bM,prototype:_M,keys:DM}=Object;function hc(t){if(t.length===1){let n=t[0];if(yM(n))return{args:n,keys:null};if(EM(n)){let e=DM(n);return{args:e.map(r=>n[r]),keys:e}}}return{args:t,keys:null}}function EM(t){return t&&typeof t=="object"&&bM(t)===_M}function pc(t,n){return t.reduce((e,r,o)=>(e[r]=n[o],e),{})}function Ro(...t){let n=an(t),e=lc(t),{args:r,keys:o}=hc(t);if(r.length===0)return ae([],n);let i=new P(wM(r,n,o?s=>pc(o,s):dt));return e?i.pipe(Ao(e)):i}function wM(t,n,e=dt){return r=>{ny(n,()=>{let{length:o}=t,i=new Array(o),s=o,a=o;for(let c=0;c{let l=ae(t[c],n),u=!1;l.subscribe(N(r,d=>{i[c]=d,u||(u=!0,a--),a||r.next(e(i.slice()))},()=>{--s||r.complete()}))},r)},r)}}function ny(t,n,e){t?We(e,t,n):n()}function ry(t,n,e,r,o,i,s,a){let c=[],l=0,u=0,d=!1,h=()=>{d&&!c.length&&!l&&n.complete()},p=_=>l{i&&n.next(_),l++;let D=!1;X(e(_,u++)).subscribe(N(n,C=>{o?.(C),i?p(C):n.next(C)},()=>{D=!0},void 0,()=>{if(D)try{for(l--;c.length&&lm(C)):m(C)}h()}catch(C){n.error(C)}}))};return t.subscribe(N(n,p,()=>{d=!0,h()})),()=>{a?.()}}function Ee(t,n,e=1/0){return R(n)?Ee((r,o)=>V((i,s)=>n(r,i,o,s))(X(t(r,o))),e):(typeof n=="number"&&(e=n),k((r,o)=>ry(r,o,t,e)))}function cn(t=1/0){return Ee(dt,t)}function oy(){return cn(1)}function ln(...t){return oy()(ae(t,an(t)))}function qi(t){return new P(n=>{X(t()).subscribe(n)})}function Yi(...t){let n=lc(t),{args:e,keys:r}=hc(t),o=new P(i=>{let{length:s}=e;if(!s){i.complete();return}let a=new Array(s),c=s,l=s;for(let u=0;u{d||(d=!0,l--),a[u]=h},()=>c--,void 0,()=>{(!c||!d)&&(l||i.next(r?pc(r,a):a),i.complete())}))}});return n?o.pipe(Ao(n)):o}var CM=["addListener","removeListener"],IM=["addEventListener","removeEventListener"],SM=["on","off"];function tf(t,n,e,r){if(R(e)&&(r=e,e=void 0),r)return tf(t,n,e).pipe(Ao(r));let[o,i]=xM(t)?IM.map(s=>a=>t[s](n,a,e)):MM(t)?CM.map(iy(t,n)):TM(t)?SM.map(iy(t,n)):[];if(!o&&Mo(t))return Ee(s=>tf(s,n,e))(X(t));if(!o)throw new TypeError("Invalid event target");return new P(s=>{let a=(...c)=>s.next(1i(a)})}function iy(t,n){return e=>r=>t[e](n,r)}function MM(t){return R(t.addListener)&&R(t.removeListener)}function TM(t){return R(t.on)&&R(t.off)}function xM(t){return R(t.addEventListener)&&R(t.removeEventListener)}function Pr(t=0,n,e=Jd){let r=-1;return n!=null&&(cc(n)?e=n:r=n),new P(o=>{let i=fc(t)?+t-e.now():t;i<0&&(i=0);let s=0;return e.schedule(function(){o.closed||(o.next(s++),0<=r?this.schedule(void 0,r):o.complete())},i)})}function AM(t=0,n=Ct){return t<0&&(t=0),Pr(t,t,n)}function RM(...t){let n=an(t),e=Zv(t,1/0),r=t;return r.length?r.length===1?X(r[0]):cn(e)(ae(r,n)):Re}function he(t,n){return k((e,r)=>{let o=0;e.subscribe(N(r,i=>t.call(n,i,o++)&&r.next(i)))})}function sy(t){return k((n,e)=>{let r=!1,o=null,i=null,s=!1,a=()=>{if(i?.unsubscribe(),i=null,r){r=!1;let l=o;o=null,e.next(l)}s&&e.complete()},c=()=>{i=null,s&&e.complete()};n.subscribe(N(e,l=>{r=!0,o=l,i||X(t(l)).subscribe(i=N(e,a,c))},()=>{s=!0,(!r||!i||i.closed)&&e.complete()}))})}function mc(t,n=Ct){return sy(()=>Pr(t,n))}function It(t){return k((n,e)=>{let r=null,o=!1,i;r=n.subscribe(N(e,void 0,void 0,s=>{i=X(t(s,It(t)(n))),r?(r.unsubscribe(),r=null,i.subscribe(e)):o=!0})),o&&(r.unsubscribe(),r=null,i.subscribe(e))})}function ay(t,n,e,r,o){return(i,s)=>{let a=e,c=n,l=0;i.subscribe(N(s,u=>{let d=l++;c=a?t(c,u,d):(a=!0,u),r&&s.next(c)},o&&(()=>{a&&s.next(c),s.complete()})))}}function NM(t,n){return k(ay(t,n,arguments.length>=2,!1,!0))}function or(t,n){return R(n)?Ee(t,n,1):Ee(t,1)}function Lr(t,n=Ct){return k((e,r)=>{let o=null,i=null,s=null,a=()=>{if(o){o.unsubscribe(),o=null;let l=i;i=null,r.next(l)}};function c(){let l=s+t,u=n.now();if(u{i=l,s=n.now(),o||(o=n.schedule(c,t),r.add(o))},()=>{a(),r.complete()},void 0,()=>{i=o=null}))})}function cy(t){return k((n,e)=>{let r=!1;n.subscribe(N(e,o=>{r=!0,e.next(o)},()=>{r||e.next(t),e.complete()}))})}function Ne(t){return t<=0?()=>Re:k((n,e)=>{let r=0;n.subscribe(N(e,o=>{++r<=t&&(e.next(o),t<=r&&e.complete())}))})}function ly(){return k((t,n)=>{t.subscribe(N(n,Nr))})}function nf(t){return V(()=>t)}function rf(t,n){return n?e=>ln(n.pipe(Ne(1),ly()),e.pipe(rf(t))):Ee((e,r)=>X(t(e,r)).pipe(Ne(1),nf(e)))}function OM(t,n=Ct){let e=Pr(t,n);return rf(()=>e)}function gc(t,n=dt){return t=t??kM,k((e,r)=>{let o,i=!0;e.subscribe(N(r,s=>{let a=n(s);(i||!t(o,a))&&(i=!1,o=a,r.next(s))}))})}function kM(t,n){return t===n}function uy(t=FM){return k((n,e)=>{let r=!1;n.subscribe(N(e,o=>{r=!0,e.next(o)},()=>r?e.complete():e.error(t())))})}function FM(){return new An}function Rn(t){return k((n,e)=>{try{n.subscribe(e)}finally{e.add(t)}})}function Nn(t,n){let e=arguments.length>=2;return r=>r.pipe(t?he((o,i)=>t(o,i,r)):dt,Ne(1),e?cy(n):uy(()=>new An))}function vc(t){return t<=0?()=>Re:k((n,e)=>{let r=[];n.subscribe(N(e,o=>{r.push(o),t{for(let o of r)e.next(o);e.complete()},void 0,()=>{r=null}))})}function dy(){return k((t,n)=>{let e,r=!1;t.subscribe(N(n,o=>{let i=e;e=o,r&&n.next([i,o]),r=!0}))})}function Zi(t={}){let{connector:n=()=>new x,resetOnError:e=!0,resetOnComplete:r=!0,resetOnRefCountZero:o=!0}=t;return i=>{let s,a,c,l=0,u=!1,d=!1,h=()=>{a?.unsubscribe(),a=void 0},p=()=>{h(),s=c=void 0,u=d=!1},m=()=>{let _=s;p(),_?.unsubscribe()};return k((_,D)=>{l++,!d&&!u&&h();let C=c=c??n();D.add(()=>{l--,l===0&&!d&&!u&&(a=of(m,o))}),C.subscribe(D),!s&&l>0&&(s=new Lt({next:z=>C.next(z),error:z=>{d=!0,h(),a=of(p,e,z),C.error(z)},complete:()=>{u=!0,h(),a=of(p,r),C.complete()}}),X(_).subscribe(s))})(i)}}function of(t,n,...e){if(n===!0){t();return}if(n===!1)return;let r=new Lt({next:()=>{r.unsubscribe(),t()}});return X(n(...e)).subscribe(r)}function fy(t,n,e){let r,o=!1;return t&&typeof t=="object"?{bufferSize:r=1/0,windowTime:n=1/0,refCount:o=!1,scheduler:e}=t:r=t??1/0,Zi({connector:()=>new $i(r,n,e),resetOnError:!0,resetOnComplete:!1,resetOnRefCountZero:o})}function Ki(t){return he((n,e)=>t<=e)}function No(...t){let n=an(t);return k((e,r)=>{(n?ln(t,e,n):ln(t,e)).subscribe(r)})}function Ve(t,n){return k((e,r)=>{let o=null,i=0,s=!1,a=()=>s&&!o&&r.complete();e.subscribe(N(r,c=>{o?.unsubscribe();let l=0,u=i++;X(t(c,u)).subscribe(o=N(r,d=>r.next(n?n(c,d,u,l++):d),()=>{o=null,a()}))},()=>{s=!0,a()}))})}function sf(t,n=!1){return k((e,r)=>{let o=0;e.subscribe(N(r,i=>{let s=t(i,o++);(s||n)&&r.next(i),!s&&r.complete()}))})}function Be(t,n,e){let r=R(t)||n||e?{next:t,error:n,complete:e}:t;return r?k((o,i)=>{var s;(s=r.subscribe)===null||s===void 0||s.call(r);let a=!0;o.subscribe(N(i,c=>{var l;(l=r.next)===null||l===void 0||l.call(r,c),i.next(c)},()=>{var c;a=!1,(c=r.complete)===null||c===void 0||c.call(r),i.complete()},c=>{var l;a=!1,(l=r.error)===null||l===void 0||l.call(r,c),i.error(c)},()=>{var c,l;a&&((c=r.unsubscribe)===null||c===void 0||c.call(r)),(l=r.finalize)===null||l===void 0||l.call(r)}))}):dt}var qe=null,yc=!1,af=1,PM=null,me=Symbol("SIGNAL");function M(t){let n=qe;return qe=t,n}function bc(){return qe}var ir={version:0,lastCleanEpoch:0,dirty:!1,producers:void 0,producersTail:void 0,consumers:void 0,consumersTail:void 0,recomputing:!1,consumerAllowSignalWrites:!1,consumerIsAlwaysLive:!1,kind:"unknown",producerMustRecompute:()=>!1,producerRecomputeValue:()=>{},consumerMarkedDirty:()=>{},consumerOnSignalRead:()=>{}};function On(t){if(yc)throw new Error("");if(qe===null)return;qe.consumerOnSignalRead(t);let n=qe.producersTail;if(n!==void 0&&n.producer===t)return;let e,r=qe.recomputing;if(r&&(e=n!==void 0?n.nextProducer:qe.producers,e!==void 0&&e.producer===t)){qe.producersTail=e,e.lastReadVersion=t.version;return}let o=t.consumersTail;if(o!==void 0&&o.consumer===qe&&(!r||jM(o,qe)))return;let i=Fo(qe),s={producer:t,consumer:qe,nextProducer:e,prevConsumer:o,lastReadVersion:t.version,nextConsumer:void 0};qe.producersTail=s,n!==void 0?n.nextProducer=s:qe.producers=s,i&&gy(t,s)}function hy(){af++}function Br(t){if(!(Fo(t)&&!t.dirty)&&!(!t.dirty&&t.lastCleanEpoch===af)){if(!t.producerMustRecompute(t)&&!ko(t)){Oo(t);return}t.producerRecomputeValue(t),Oo(t)}}function cf(t){if(t.consumers===void 0)return;let n=yc;yc=!0;try{for(let e=t.consumers;e!==void 0;e=e.nextConsumer){let r=e.consumer;r.dirty||LM(r)}}finally{yc=n}}function lf(){return qe?.consumerAllowSignalWrites!==!1}function LM(t){t.dirty=!0,cf(t),t.consumerMarkedDirty?.(t)}function Oo(t){t.dirty=!1,t.lastCleanEpoch=af}function kn(t){return t&&py(t),M(t)}function py(t){t.producersTail=void 0,t.recomputing=!0}function sr(t,n){M(n),t&&my(t)}function my(t){t.recomputing=!1;let n=t.producersTail,e=n!==void 0?n.nextProducer:t.producers;if(e!==void 0){if(Fo(t))do e=uf(e);while(e!==void 0);n!==void 0?n.nextProducer=void 0:t.producers=void 0}}function ko(t){for(let n=t.producers;n!==void 0;n=n.nextProducer){let e=n.producer,r=n.lastReadVersion;if(r!==e.version||(Br(e),r!==e.version))return!0}return!1}function ar(t){if(Fo(t)){let n=t.producers;for(;n!==void 0;)n=uf(n)}t.producers=void 0,t.producersTail=void 0,t.consumers=void 0,t.consumersTail=void 0}function gy(t,n){let e=t.consumersTail,r=Fo(t);if(e!==void 0?(n.nextConsumer=e.nextConsumer,e.nextConsumer=n):(n.nextConsumer=void 0,t.consumers=n),n.prevConsumer=e,t.consumersTail=n,!r)for(let o=t.producers;o!==void 0;o=o.nextProducer)gy(o.producer,o)}function uf(t){let n=t.producer,e=t.nextProducer,r=t.nextConsumer,o=t.prevConsumer;if(t.nextConsumer=void 0,t.prevConsumer=void 0,r!==void 0?r.prevConsumer=o:n.consumersTail=o,o!==void 0)o.nextConsumer=r;else if(n.consumers=r,!Fo(n)){let i=n.producers;for(;i!==void 0;)i=uf(i)}return e}function Fo(t){return t.consumerIsAlwaysLive||t.consumers!==void 0}function Qi(t){PM?.(t)}function jM(t,n){let e=n.producersTail;if(e!==void 0){let r=n.producers;do{if(r===t)return!0;if(r===e)break;r=r.nextProducer}while(r!==void 0)}return!1}function Xi(t,n){return Object.is(t,n)}function Ji(t,n){let e=Object.create(VM);e.computation=t,n!==void 0&&(e.equal=n);let r=()=>{if(Br(e),On(e),e.value===un)throw e.error;return e.value};return r[me]=e,Qi(e),r}var jr=Symbol("UNSET"),Vr=Symbol("COMPUTING"),un=Symbol("ERRORED"),VM=F(g({},ir),{value:jr,dirty:!0,error:null,equal:Xi,kind:"computed",producerMustRecompute(t){return t.value===jr||t.value===Vr},producerRecomputeValue(t){if(t.value===Vr)throw new Error("");let n=t.value;t.value=Vr;let e=kn(t),r,o=!1;try{r=t.computation(),M(null),o=n!==jr&&n!==un&&r!==un&&t.equal(n,r)}catch(i){r=un,t.error=i}finally{sr(t,e)}if(o){t.value=n;return}t.value=r,t.version++}});function BM(){throw new Error}var vy=BM;function yy(t){vy(t)}function df(t){vy=t}var UM=null;function ff(t,n){let e=Object.create(es);e.value=t,n!==void 0&&(e.equal=n);let r=()=>by(e);return r[me]=e,Qi(e),[r,s=>cr(e,s),s=>_c(e,s)]}function by(t){return On(t),t.value}function cr(t,n){lf()||yy(t),t.equal(t.value,n)||(t.value=n,HM(t))}function _c(t,n){lf()||yy(t),cr(t,n(t.value))}var es=F(g({},ir),{equal:Xi,value:void 0,kind:"signal"});function HM(t){t.version++,hy(),cf(t),UM?.(t)}var hf=F(g({},ir),{consumerIsAlwaysLive:!0,consumerAllowSignalWrites:!0,dirty:!0,kind:"effect"});function pf(t){if(t.dirty=!1,t.version>0&&!ko(t))return;t.version++;let n=kn(t);try{t.cleanup(),t.fn()}finally{sr(t,n)}}var mf;function Dc(){return mf}function dn(t){let n=mf;return mf=t,n}var _y=Symbol("NotFound");function Po(t){return t===_y||t?.name==="\u0275NotFound"}function gf(t,n,e){let r=Object.create($M);r.source=t,r.computation=n,e!=null&&(r.equal=e);let i=()=>{if(Br(r),On(r),r.value===un)throw r.error;return r.value};return i[me]=r,Qi(r),i}function Dy(t,n){Br(t),cr(t,n),Oo(t)}function Ey(t,n){if(Br(t),t.value===un)throw t.error;_c(t,n),Oo(t)}var $M=F(g({},ir),{value:jr,dirty:!0,error:null,equal:Xi,kind:"linkedSignal",producerMustRecompute(t){return t.value===jr||t.value===Vr},producerRecomputeValue(t){if(t.value===Vr)throw new Error("");let n=t.value;t.value=Vr;let e=kn(t),r,o=!1;try{let i=t.source(),s=n!==jr&&n!==un,a=s?{source:t.sourceValue,value:n}:void 0;r=t.computation(i,a),t.sourceValue=i,M(null),o=s&&r!==un&&t.equal(n,r)}catch(i){r=un,t.error=i}finally{sr(t,e)}if(o){t.value=n;return}t.value=r,t.version++}});function wy(t){let n=M(null);try{return t()}finally{M(n)}}var xc="https://angular.dev/best-practices/security#preventing-cross-site-scripting-xss",b=class extends Error{code;constructor(n,e){super(St(n,e)),this.code=n}};function zM(t){return`NG0${Math.abs(t)}`}function St(t,n){return`${zM(t)}${n?": "+n:""}`}var we=globalThis;function ne(t){for(let n in t)if(t[n]===ne)return n;throw Error("")}function Ty(t,n){for(let e in n)n.hasOwnProperty(e)&&!t.hasOwnProperty(e)&&(t[e]=n[e])}function cs(t){if(typeof t=="string")return t;if(Array.isArray(t))return`[${t.map(cs).join(", ")}]`;if(t==null)return""+t;let n=t.overriddenName||t.name;if(n)return`${n}`;let e=t.toString();if(e==null)return""+e;let r=e.indexOf(` +`);return r>=0?e.slice(0,r):e}function Ac(t,n){return t?n?`${t} ${n}`:t:n||""}var GM=ne({__forward_ref__:ne});function Ie(t){return t.__forward_ref__=Ie,t}function Oe(t){return xf(t)?t():t}function xf(t){return typeof t=="function"&&t.hasOwnProperty(GM)&&t.__forward_ref__===Ie}function v(t){return{token:t.token,providedIn:t.providedIn||null,factory:t.factory,value:void 0}}function q(t){return{providers:t.providers||[],imports:t.imports||[]}}function ls(t){return WM(t,Rc)}function Af(t){return ls(t)!==null}function WM(t,n){return t.hasOwnProperty(n)&&t[n]||null}function qM(t){let n=t?.[Rc]??null;return n||null}function yf(t){return t&&t.hasOwnProperty(Cc)?t[Cc]:null}var Rc=ne({\u0275prov:ne}),Cc=ne({\u0275inj:ne}),y=class{_desc;ngMetadataName="InjectionToken";\u0275prov;constructor(n,e){this._desc=n,this.\u0275prov=void 0,typeof e=="number"?this.__NG_ELEMENT_ID__=e:e!==void 0&&(this.\u0275prov=v({token:this,providedIn:e.providedIn||"root",factory:e.factory}))}get multi(){return this}toString(){return`InjectionToken ${this._desc}`}};function Rf(t){return t&&!!t.\u0275providers}var us=ne({\u0275cmp:ne}),ds=ne({\u0275dir:ne}),Nf=ne({\u0275pipe:ne}),Of=ne({\u0275mod:ne}),ns=ne({\u0275fac:ne}),Gr=ne({__NG_ELEMENT_ID__:ne}),Cy=ne({__NG_ENV_ID__:ne});function kf(t){return Oc(t,"@NgModule"),t[Of]||null}function hn(t){return Oc(t,"@Component"),t[us]||null}function Nc(t){return Oc(t,"@Directive"),t[ds]||null}function xy(t){return Oc(t,"@Pipe"),t[Nf]||null}function Oc(t,n){if(t==null)throw new b(-919,!1)}function Se(t){return typeof t=="string"?t:t==null?"":String(t)}var Ay=ne({ngErrorCode:ne}),YM=ne({ngErrorMessage:ne}),ZM=ne({ngTokenPath:ne});function Ff(t,n){return Ry("",-200,n)}function kc(t,n){throw new b(-201,!1)}function Ry(t,n,e){let r=new b(n,t);return r[Ay]=n,r[YM]=t,e&&(r[ZM]=e),r}function KM(t){return t[Ay]}var bf;function Ny(){return bf}function tt(t){let n=bf;return bf=t,n}function Pf(t,n,e){let r=ls(t);if(r&&r.providedIn=="root")return r.value===void 0?r.value=r.factory():r.value;if(e&8)return null;if(n!==void 0)return n;kc(t,"")}var QM={},Ur=QM,XM="__NG_DI_FLAG__",_f=class{injector;constructor(n){this.injector=n}retrieve(n,e){let r=Hr(e)||0;try{return this.injector.get(n,r&8?null:Ur,r)}catch(o){if(Po(o))return o;throw o}}};function JM(t,n=0){let e=Dc();if(e===void 0)throw new b(-203,!1);if(e===null)return Pf(t,void 0,n);{let r=eT(n),o=e.retrieve(t,r);if(Po(o)){if(r.optional)return null;throw o}return o}}function w(t,n=0){return(Ny()||JM)(Oe(t),n)}function f(t,n){return w(t,Hr(n))}function Hr(t){return typeof t>"u"||typeof t=="number"?t:0|(t.optional&&8)|(t.host&&1)|(t.self&&2)|(t.skipSelf&&4)}function eT(t){return{optional:!!(t&8),host:!!(t&1),self:!!(t&2),skipSelf:!!(t&4)}}function Df(t){let n=[];for(let e=0;eArray.isArray(e)?Fc(e,n):n(e))}function Lf(t,n,e){n>=t.length?t.push(e):t.splice(n,0,e)}function fs(t,n){return n>=t.length-1?t.pop():t.splice(n,1)[0]}function Fy(t,n){let e=[];for(let r=0;rn;){let i=o-2;t[o]=t[i],o--}t[n]=e,t[n+1]=r}}function Pc(t,n,e){let r=jo(t,n);return r>=0?t[r|1]=e:(r=~r,Py(t,r,n,e)),r}function Lc(t,n){let e=jo(t,n);if(e>=0)return t[e|1]}function jo(t,n){return nT(t,n,1)}function nT(t,n,e){let r=0,o=t.length>>e;for(;o!==r;){let i=r+(o-r>>1),s=t[i<n?o=i:r=i+1}return~(o<{e.push(s)};return Fc(n,s=>{let a=s;Ic(a,i,[],r)&&(o||=[],o.push(a))}),o!==void 0&&jy(o,i),e}function jy(t,n){for(let e=0;e{n(i,r)})}}function Ic(t,n,e,r){if(t=Oe(t),!t)return!1;let o=null,i=yf(t),s=!i&&hn(t);if(!i&&!s){let c=t.ngModule;if(i=yf(c),i)o=c;else return!1}else{if(s&&!s.standalone)return!1;o=t}let a=r.has(o);if(s){if(a)return!1;if(r.add(o),s.dependencies){let c=typeof s.dependencies=="function"?s.dependencies():s.dependencies;for(let l of c)Ic(l,n,e,r)}}else if(i){if(i.imports!=null&&!a){r.add(o);let l;Fc(i.imports,u=>{Ic(u,n,e,r)&&(l||=[],l.push(u))}),l!==void 0&&jy(l,n)}if(!a){let l=ur(o)||(()=>new o);n({provide:o,useFactory:l,deps:Ye},o),n({provide:Vf,useValue:o,multi:!0},o),n({provide:Wr,useValue:()=>w(o),multi:!0},o)}let c=i.providers;if(c!=null&&!a){let l=t;Uf(c,u=>{n(u,l)})}}else return!1;return o!==t&&t.providers!==void 0}function Uf(t,n){for(let e of t)Rf(e)&&(e=e.\u0275providers),Array.isArray(e)?Uf(e,n):n(e)}var rT=ne({provide:String,useValue:ne});function Vy(t){return t!==null&&typeof t=="object"&&rT in t}function oT(t){return!!(t&&t.useExisting)}function iT(t){return!!(t&&t.useFactory)}function $r(t){return typeof t=="function"}function By(t){return!!t.useClass}var hs=new y(""),Ec={},Iy={},vf;function Vo(){return vf===void 0&&(vf=new rs),vf}var re=class{},zr=class extends re{parent;source;scopes;records=new Map;_ngOnDestroyHooks=new Set;_onDestroyHooks=[];get destroyed(){return this._destroyed}_destroyed=!1;injectorDefTypes;constructor(n,e,r,o){super(),this.parent=e,this.source=r,this.scopes=o,wf(n,s=>this.processProvider(s)),this.records.set(jf,Lo(void 0,this)),o.has("environment")&&this.records.set(re,Lo(void 0,this));let i=this.records.get(hs);i!=null&&typeof i.value=="string"&&this.scopes.add(i.value),this.injectorDefTypes=new Set(this.get(Vf,Ye,{self:!0}))}retrieve(n,e){let r=Hr(e)||0;try{return this.get(n,Ur,r)}catch(o){if(Po(o))return o;throw o}}destroy(){ts(this),this._destroyed=!0;let n=M(null);try{for(let r of this._ngOnDestroyHooks)r.ngOnDestroy();let e=this._onDestroyHooks;this._onDestroyHooks=[];for(let r of e)r()}finally{this.records.clear(),this._ngOnDestroyHooks.clear(),this.injectorDefTypes.clear(),M(n)}}onDestroy(n){return ts(this),this._onDestroyHooks.push(n),()=>this.removeOnDestroy(n)}runInContext(n){ts(this);let e=dn(this),r=tt(void 0),o;try{return n()}finally{dn(e),tt(r)}}get(n,e=Ur,r){if(ts(this),n.hasOwnProperty(Cy))return n[Cy](this);let o=Hr(r),i,s=dn(this),a=tt(void 0);try{if(!(o&4)){let l=this.records.get(n);if(l===void 0){let u=uT(n)&&ls(n);u&&this.injectableDefInScope(u)?l=Lo(Ef(n),Ec):l=null,this.records.set(n,l)}if(l!=null)return this.hydrate(n,l,o)}let c=o&2?Vo():this.parent;return e=o&8&&e===Ur?null:e,c.get(n,e)}catch(c){let l=KM(c);throw l===-200||l===-201?new b(l,null):c}finally{tt(a),dn(s)}}resolveInjectorInitializers(){let n=M(null),e=dn(this),r=tt(void 0),o;try{let i=this.get(Wr,Ye,{self:!0});for(let s of i)s()}finally{dn(e),tt(r),M(n)}}toString(){return"R3Injector[...]"}processProvider(n){n=Oe(n);let e=$r(n)?n:Oe(n&&n.provide),r=aT(n);if(!$r(n)&&n.multi===!0){let o=this.records.get(e);o||(o=Lo(void 0,Ec,!0),o.factory=()=>Df(o.multi),this.records.set(e,o)),e=n,o.multi.push(n)}this.records.set(e,r)}hydrate(n,e,r){let o=M(null);try{if(e.value===Iy)throw Ff("");return e.value===Ec&&(e.value=Iy,e.value=e.factory(void 0,r)),typeof e.value=="object"&&e.value&&lT(e.value)&&this._ngOnDestroyHooks.add(e.value),e.value}finally{M(o)}}injectableDefInScope(n){if(!n.providedIn)return!1;let e=Oe(n.providedIn);return typeof e=="string"?e==="any"||this.scopes.has(e):this.injectorDefTypes.has(e)}removeOnDestroy(n){let e=this._onDestroyHooks.indexOf(n);e!==-1&&this._onDestroyHooks.splice(e,1)}};function Ef(t){let n=ls(t),e=n!==null?n.factory:ur(t);if(e!==null)return e;if(t instanceof y)throw new b(-204,!1);if(t instanceof Function)return sT(t);throw new b(-204,!1)}function sT(t){if(t.length>0)throw new b(-204,!1);let e=qM(t);return e!==null?()=>e.factory(t):()=>new t}function aT(t){if(Vy(t))return Lo(void 0,t.useValue);{let n=Hf(t);return Lo(n,Ec)}}function Hf(t,n,e){let r;if($r(t)){let o=Oe(t);return ur(o)||Ef(o)}else if(Vy(t))r=()=>Oe(t.useValue);else if(iT(t))r=()=>t.useFactory(...Df(t.deps||[]));else if(oT(t))r=(o,i)=>w(Oe(t.useExisting),i!==void 0&&i&8?8:void 0);else{let o=Oe(t&&(t.useClass||t.provide));if(cT(t))r=()=>new o(...Df(t.deps));else return ur(o)||Ef(o)}return r}function ts(t){if(t.destroyed)throw new b(-205,!1)}function Lo(t,n,e=!1){return{factory:t,value:n,multi:e?[]:void 0}}function cT(t){return!!t.deps}function lT(t){return t!==null&&typeof t=="object"&&typeof t.ngOnDestroy=="function"}function uT(t){return typeof t=="function"||typeof t=="object"&&t.ngMetadataName==="InjectionToken"}function wf(t,n){for(let e of t)Array.isArray(e)?wf(e,n):e&&Rf(e)?wf(e.\u0275providers,n):n(e)}function Fe(t,n){let e;t instanceof zr?(ts(t),e=t):e=new _f(t);let r,o=dn(e),i=tt(void 0);try{return n()}finally{dn(o),tt(i)}}function $f(){return Ny()!==void 0||Dc()!=null}var Ut=0,A=1,L=2,ke=3,Mt=4,nt=5,qr=6,Bo=7,ge=8,Pn=9,Ht=10,se=11,Uo=12,zf=13,Yr=14,Ke=15,pr=16,Zr=17,pn=18,Ln=19,Gf=20,Fn=21,jc=22,dr=23,ft=24,Kr=25,mr=26,de=27,Uy=1,Wf=6,gr=7,ps=8,Qr=9,ve=10;function jn(t){return Array.isArray(t)&&typeof t[Uy]=="object"}function $t(t){return Array.isArray(t)&&t[Uy]===!0}function qf(t){return(t.flags&4)!==0}function mn(t){return t.componentOffset>-1}function Ho(t){return(t.flags&1)===1}function zt(t){return!!t.template}function $o(t){return(t[L]&512)!==0}function Xr(t){return(t[L]&256)===256}var J=(function(t){return t[t.NONE=0]="NONE",t[t.HTML=1]="HTML",t[t.STYLE=2]="STYLE",t[t.SCRIPT=3]="SCRIPT",t[t.URL=4]="URL",t[t.RESOURCE_URL=5]="RESOURCE_URL",t[t.ATTRIBUTE_NO_BINDING=6]="ATTRIBUTE_NO_BINDING",t})(J||{}),wc,os="svg",Yf="math";function Hy(){return wc||(wc={},lr(J.HTML,void 0,[["iframe",["srcdoc"]],["*",["innerHTML","outerHTML"]]]),lr(J.STYLE,void 0,[["*",["style"]]]),lr(J.URL,void 0,[["*",["formAction"]],["area",["href"]],["a",["href","xlink:href"]],["form",["action"]],["img",["src"]],["video",["src"]]]),lr(J.URL,Yf,[["*",["href","xlink:href"]],["annotation",["href","xlink:href"]],["annotation-xml",["href","xlink:href"]],["maction",["href","xlink:href"]],["malignmark",["href","xlink:href"]],["math",["href","xlink:href"]],["mroot",["href","xlink:href"]],["msqrt",["href","xlink:href"]],["merror",["href","xlink:href"]],["mfrac",["href","xlink:href"]],["mglyph",["href","xlink:href"]],["msub",["href","xlink:href"]],["msup",["href","xlink:href"]],["msubsup",["href","xlink:href"]],["mmultiscripts",["href","xlink:href"]],["mprescripts",["href","xlink:href"]],["mi",["href","xlink:href"]],["mn",["href","xlink:href"]],["mo",["href","xlink:href"]],["mpadded",["href","xlink:href"]],["mphantom",["href","xlink:href"]],["mrow",["href","xlink:href"]],["ms",["href","xlink:href"]],["mspace",["href","xlink:href"]],["mstyle",["href","xlink:href"]],["mtable",["href","xlink:href"]],["mtd",["href","xlink:href"]],["mtr",["href","xlink:href"]],["mtext",["href","xlink:href"]],["mover",["href","xlink:href"]],["munder",["href","xlink:href"]],["munderover",["href","xlink:href"]],["semantics",["href","xlink:href"]],["none",["href","xlink:href"]]]),lr(J.RESOURCE_URL,void 0,[["base",["href"]],["embed",["src"]],["frame",["src"]],["iframe",["src"]],["link",["href"]],["object",["codebase","data"]]]),lr(J.URL,os,[["a",["href","xlink:href"]]]),lr(J.ATTRIBUTE_NO_BINDING,os,[["animate",["attributeName","values","to","from"]],["set",["to","attributeName"]],["animateMotion",["attributeName"]],["animateTransform",["attributeName"]]]),lr(J.ATTRIBUTE_NO_BINDING,void 0,[["unknown",["attributeName","values","to","from","sandbox","allow","allowFullscreen","referrerPolicy","csp","fetchPriority"]],["iframe",["sandbox","allow","allowFullscreen","referrerPolicy","csp","fetchPriority"]]])),wc}function lr(t,n,e){for(let[r,o]of e){let i=n&&r!=="unknown"?`:${n}:${r}`:r;i=i.toLowerCase();for(let s of o)wc[`${i}|${s.toLowerCase()}`]=t}}function $y(t,n,e){let r=Hy(),o=t.toLowerCase(),i=n.toLowerCase(),s=e&&o!=="*"&&o!=="unknown"?r[`:${e}:${o}|${i}`]:void 0,a=e?r[`:${e}:*|${i}`]:void 0;return s??a??r[`${o}|${i}`]??r[`*|${i}`]??J.NONE}function Tt(t){for(;Array.isArray(t);)t=t[Ut];return t}function Zf(t,n){return Tt(n[t])}function ht(t,n){return Tt(n[t.index])}function Vc(t,n){return t.data[n]}function ms(t,n){return t[n]}function Kf(t,n,e,r){e>=t.data.length&&(t.data[e]=null,t.blueprint[e]=null),n[e]=r}function xt(t,n){let e=n[t];return jn(e)?e:e[Ut]}function zy(t){return(t[L]&4)===4}function Bc(t){return(t[L]&128)===128}function Gy(t){return $t(t[ke])}function pt(t,n){return n==null?null:t[n]}function Qf(t){t[Zr]=0}function Xf(t){t[L]&1024||(t[L]|=1024,Bc(t)&&Jr(t))}function Wy(t,n){for(;t>0;)n=n[Yr],t--;return n}function gs(t){return!!(t[L]&9216||t[ft]?.dirty)}function Uc(t){t[Ht].changeDetectionScheduler?.notify(8),t[L]&64&&(t[L]|=1024),gs(t)&&Jr(t)}function Jr(t){t[Ht].changeDetectionScheduler?.notify(0);let n=fr(t);for(;n!==null&&!(n[L]&8192||(n[L]|=8192,!Bc(n)));)n=fr(n)}function Jf(t,n){if(Xr(t))throw new b(911,!1);t[Fn]===null&&(t[Fn]=[]),t[Fn].push(n)}function qy(t,n){if(t[Fn]===null)return;let e=t[Fn].indexOf(n);e!==-1&&t[Fn].splice(e,1)}function fr(t){let n=t[ke];return $t(n)?n[ke]:n}function eh(t){return t[Bo]??=[]}function th(t){return t.cleanup??=[]}function Yy(t,n,e,r){let o=eh(n);o.push(e),t.firstCreatePass&&th(t).push(r,o.length-1)}var U={lFrame:ab(null),bindingsEnabled:!0,skipHydrationRootTNode:null};var Cf=!1;function Zy(){return U.lFrame.elementDepthCount}function Ky(){U.lFrame.elementDepthCount++}function nh(){U.lFrame.elementDepthCount--}function Hc(){return U.bindingsEnabled}function rh(){return U.skipHydrationRootTNode!==null}function oh(t){return U.skipHydrationRootTNode===t}function ih(){U.skipHydrationRootTNode=null}function I(){return U.lFrame.lView}function le(){return U.lFrame.tView}function Qy(t){return U.lFrame.contextLView=t,t[ge]}function Xy(t){return U.lFrame.contextLView=null,t}function Me(){let t=sh();for(;t!==null&&t.type===64;)t=t.parent;return t}function sh(){return U.lFrame.currentTNode}function Jy(){let t=U.lFrame,n=t.currentTNode;return t.isParent?n:n.parent}function zo(t,n){let e=U.lFrame;e.currentTNode=t,e.isParent=n}function ah(){return U.lFrame.isParent}function ch(){U.lFrame.isParent=!1}function eb(){return U.lFrame.contextLView}function lh(){return Cf}function is(t){let n=Cf;return Cf=t,n}function gn(){let t=U.lFrame,n=t.bindingRootIndex;return n===-1&&(n=t.bindingRootIndex=t.tView.bindingStartIndex),n}function vs(){return U.lFrame.bindingIndex}function tb(t){return U.lFrame.bindingIndex=t}function Vn(){return U.lFrame.bindingIndex++}function eo(t){let n=U.lFrame,e=n.bindingIndex;return n.bindingIndex=n.bindingIndex+t,e}function nb(){return U.lFrame.inI18n}function rb(t,n){let e=U.lFrame;e.bindingIndex=e.bindingRootIndex=t,$c(n)}function ob(){return U.lFrame.currentDirectiveIndex}function $c(t){U.lFrame.currentDirectiveIndex=t}function ib(t){let n=U.lFrame.currentDirectiveIndex;return n===-1?null:t[n]}function zc(){return U.lFrame.currentQueryIndex}function ys(t){U.lFrame.currentQueryIndex=t}function dT(t){let n=t[A];return n.type===2?n.declTNode:n.type===1?t[nt]:null}function uh(t,n,e){if(e&4){let o=n,i=t;for(;o=o.parent,o===null&&!(e&1);)if(o=dT(i),o===null||(i=i[Yr],o.type&10))break;if(o===null)return!1;n=o,t=i}let r=U.lFrame=sb();return r.currentTNode=n,r.lView=t,!0}function Gc(t){let n=sb(),e=t[A];U.lFrame=n,n.currentTNode=e.firstChild,n.lView=t,n.tView=e,n.contextLView=t,n.bindingIndex=e.bindingStartIndex,n.inI18n=!1}function sb(){let t=U.lFrame,n=t===null?null:t.child;return n===null?ab(t):n}function ab(t){let n={currentTNode:null,isParent:!0,lView:null,tView:null,selectedIndex:-1,contextLView:null,elementDepthCount:0,currentNamespace:null,currentDirectiveIndex:-1,bindingRootIndex:-1,bindingIndex:-1,currentQueryIndex:0,parent:t,child:null,inI18n:!1};return t!==null&&(t.child=n),n}function cb(){let t=U.lFrame;return U.lFrame=t.parent,t.currentTNode=null,t.lView=null,t}var dh=cb;function Wc(){let t=cb();t.isParent=!0,t.tView=null,t.selectedIndex=-1,t.contextLView=null,t.elementDepthCount=0,t.currentDirectiveIndex=-1,t.currentNamespace=null,t.bindingRootIndex=-1,t.bindingIndex=-1,t.currentQueryIndex=0}function lb(t){return(U.lFrame.contextLView=Wy(t,U.lFrame.contextLView))[ge]}function st(){return U.lFrame.selectedIndex}function vr(t){U.lFrame.selectedIndex=t}function to(){let t=U.lFrame;return Vc(t.tView,t.selectedIndex)}function ub(){U.lFrame.currentNamespace=os}function db(){fT()}function fT(){U.lFrame.currentNamespace=null}function fh(){return U.lFrame.currentNamespace}var fb=!0;function qc(){return fb}function bs(t){fb=t}function If(t,n=null,e=null,r){let o=hh(t,n,e,r);return o.resolveInjectorInitializers(),o}function hh(t,n=null,e=null,r,o=new Set){let i=[e||Ye,Ly(t)],s;return new zr(i,n||Vo(),s||null,o)}var W=class t{static THROW_IF_NOT_FOUND=Ur;static NULL=new rs;static create(n,e){if(Array.isArray(n))return If({name:""},e,n,"");{let r=n.name??"";return If({name:r},n.parent,n.providers,r)}}static \u0275prov=v({token:t,providedIn:"any",factory:()=>w(jf)});static __NG_ELEMENT_ID__=-1},O=new y(""),Ue=(()=>{class t{static __NG_ELEMENT_ID__=hT;static __NG_ENV_ID__=e=>e}return t})(),Sc=class extends Ue{_lView;constructor(n){super(),this._lView=n}get destroyed(){return Xr(this._lView)}onDestroy(n){let e=this._lView;return Jf(e,n),()=>qy(e,n)}};function hT(){return new Sc(I())}var hb=!1,pb=new y(""),Bn=(()=>{class t{taskId=0;pendingTasks=new Set;destroyed=!1;pendingTask=new Ae(!1);debugTaskTracker=f(pb,{optional:!0});get hasPendingTasks(){return this.destroyed?!1:this.pendingTask.value}get hasPendingTasksObservable(){return this.destroyed?new P(e=>{e.next(!1),e.complete()}):this.pendingTask}add(){!this.hasPendingTasks&&!this.destroyed&&this.pendingTask.next(!0);let e=this.taskId++;return this.pendingTasks.add(e),this.debugTaskTracker?.add(e),e}has(e){return this.pendingTasks.has(e)}remove(e){this.pendingTasks.delete(e),this.debugTaskTracker?.remove(e),this.pendingTasks.size===0&&this.hasPendingTasks&&this.pendingTask.next(!1)}ngOnDestroy(){this.pendingTasks.clear(),this.hasPendingTasks&&this.pendingTask.next(!1),this.destroyed=!0,this.pendingTask.unsubscribe()}static \u0275prov=v({token:t,providedIn:"root",factory:()=>new t})}return t})(),Sf=class extends x{__isAsync;destroyRef=void 0;pendingTasks=void 0;constructor(n=!1){super(),this.__isAsync=n,$f()&&(this.destroyRef=f(Ue,{optional:!0})??void 0,this.pendingTasks=f(Bn,{optional:!0})??void 0)}emit(n){let e=M(null);try{super.next(n)}finally{M(e)}}subscribe(n,e,r){let o=n,i=e||(()=>null),s=r;if(n&&typeof n=="object"){let c=n;o=c.next?.bind(c),i=c.error?.bind(c),s=c.complete?.bind(c)}this.__isAsync&&(i=this.wrapInTimeout(i),o&&(o=this.wrapInTimeout(o)),s&&(s=this.wrapInTimeout(s)));let a=super.subscribe({next:o,error:i,complete:s});return n instanceof G&&n.add(a),a}wrapInTimeout(n){return e=>{let r=this.pendingTasks?.add();setTimeout(()=>{try{n(e)}finally{r!==void 0&&this.pendingTasks?.remove(r)}})}}},H=Sf;function Mc(...t){}function ph(t){let n,e;function r(){t=Mc;try{e!==void 0&&typeof cancelAnimationFrame=="function"&&cancelAnimationFrame(e),n!==void 0&&clearTimeout(n)}catch{}}return n=setTimeout(()=>{t(),r()}),typeof requestAnimationFrame=="function"&&(e=requestAnimationFrame(()=>{t(),r()})),()=>r()}function mb(t){return queueMicrotask(()=>t()),()=>{t=Mc}}var mh="isAngularZone",ss=mh+"_ID",pT=0,j=class t{hasPendingMacrotasks=!1;hasPendingMicrotasks=!1;isStable=!0;onUnstable=new H(!1);onMicrotaskEmpty=new H(!1);onStable=new H(!1);onError=new H(!1);constructor(n){let{enableLongStackTrace:e=!1,shouldCoalesceEventChangeDetection:r=!1,shouldCoalesceRunChangeDetection:o=!1,scheduleInRootZone:i=hb}=n;if(typeof Zone>"u")throw new b(908,!1);Zone.assertZonePatched();let s=this;s._nesting=0,s._outer=s._inner=Zone.current,Zone.TaskTrackingZoneSpec&&(s._inner=s._inner.fork(new Zone.TaskTrackingZoneSpec)),e&&Zone.longStackTraceZoneSpec&&(s._inner=s._inner.fork(Zone.longStackTraceZoneSpec)),s.shouldCoalesceEventChangeDetection=!o&&r,s.shouldCoalesceRunChangeDetection=o,s.callbackScheduled=!1,s.scheduleInRootZone=i,vT(s)}static isInAngularZone(){return typeof Zone<"u"&&Zone.current.get(mh)===!0}static assertInAngularZone(){if(!t.isInAngularZone())throw new b(909,!1)}static assertNotInAngularZone(){if(t.isInAngularZone())throw new b(909,!1)}run(n,e,r){return this._inner.run(n,e,r)}runTask(n,e,r,o){let i=this._inner,s=i.scheduleEventTask("NgZoneEvent: "+o,n,mT,Mc,Mc);try{return i.runTask(s,e,r)}finally{i.cancelTask(s)}}runGuarded(n,e,r){return this._inner.runGuarded(n,e,r)}runOutsideAngular(n){return this._outer.run(n)}},mT={};function gh(t){if(t._nesting==0&&!t.hasPendingMicrotasks&&!t.isStable)try{t._nesting++,t.onMicrotaskEmpty.emit(null)}finally{if(t._nesting--,!t.hasPendingMicrotasks)try{t.runOutsideAngular(()=>t.onStable.emit(null))}finally{t.isStable=!0}}}function gT(t){if(t.isCheckStableRunning||t.callbackScheduled)return;t.callbackScheduled=!0;function n(){ph(()=>{t.callbackScheduled=!1,Mf(t),t.isCheckStableRunning=!0,gh(t),t.isCheckStableRunning=!1})}t.scheduleInRootZone?Zone.root.run(()=>{n()}):t._outer.run(()=>{n()}),Mf(t)}function vT(t){let n=()=>{gT(t)},e=pT++;t._inner=t._inner.fork({name:"angular",properties:{[mh]:!0,[ss]:e,[ss+e]:!0},onInvokeTask:(r,o,i,s,a,c)=>{if(yT(c))return r.invokeTask(i,s,a,c);try{return Sy(t),r.invokeTask(i,s,a,c)}finally{(t.shouldCoalesceEventChangeDetection&&s.type==="eventTask"||t.shouldCoalesceRunChangeDetection)&&n(),My(t)}},onInvoke:(r,o,i,s,a,c,l)=>{try{return Sy(t),r.invoke(i,s,a,c,l)}finally{t.shouldCoalesceRunChangeDetection&&!t.callbackScheduled&&!bT(c)&&n(),My(t)}},onHasTask:(r,o,i,s)=>{r.hasTask(i,s),o===i&&(s.change=="microTask"?(t._hasPendingMicrotasks=s.microTask,Mf(t),gh(t)):s.change=="macroTask"&&(t.hasPendingMacrotasks=s.macroTask))},onHandleError:(r,o,i,s)=>(r.handleError(i,s),t.runOutsideAngular(()=>t.onError.emit(s)),!1)})}function Mf(t){t._hasPendingMicrotasks||(t.shouldCoalesceEventChangeDetection||t.shouldCoalesceRunChangeDetection)&&t.callbackScheduled===!0?t.hasPendingMicrotasks=!0:t.hasPendingMicrotasks=!1}function Sy(t){t._nesting++,t.isStable&&(t.isStable=!1,t.onUnstable.emit(null))}function My(t){t._nesting--,gh(t)}var as=class{hasPendingMicrotasks=!1;hasPendingMacrotasks=!1;isStable=!0;onUnstable=new H;onMicrotaskEmpty=new H;onStable=new H;onError=new H;run(n,e,r){return n.apply(e,r)}runGuarded(n,e,r){return n.apply(e,r)}runOutsideAngular(n){return n()}runTask(n,e,r,o){return n.apply(e,r)}};function yT(t){return gb(t,"__ignore_ng_zone__")}function bT(t){return gb(t,"__scheduler_tick__")}function gb(t,n){return!Array.isArray(t)||t.length!==1?!1:t[0]?.data?.[n]===!0}var Ze=class{_console=console;handleError(n){this._console.error("ERROR",n)}},mt=new y("",{factory:()=>{let t=f(j),n=f(re),e;return r=>{t.runOutsideAngular(()=>{n.destroyed&&!e?setTimeout(()=>{throw r}):(e??=n.get(Ze),e.handleError(r))})}}}),vb={provide:Wr,useValue:()=>{let t=f(Ze,{optional:!0})},multi:!0};function K(t,n){let[e,r,o]=ff(t,n?.equal),i=e,s=i[me];return i.set=r,i.update=o,i.asReadonly=_s.bind(i),i}function _s(){let t=this[me];if(t.readonlyFn===void 0){let n=()=>this();n[me]=t,t.readonlyFn=n}return t.readonlyFn}var Go=(()=>{class t{view;node;constructor(e,r){this.view=e,this.node=r}static __NG_ELEMENT_ID__=_T}return t})();function _T(){return new Go(I(),Me())}var fn=class{},Ds=new y("",{factory:()=>!0});var vh=new y(""),Wo=(()=>{class t{internalPendingTasks=f(Bn);scheduler=f(fn);errorHandler=f(mt);add(){let e=this.internalPendingTasks.add();return()=>{this.internalPendingTasks.has(e)&&(this.scheduler.notify(11),this.internalPendingTasks.remove(e))}}run(e){let r=this.add();e().catch(this.errorHandler).finally(r)}static \u0275prov=v({token:t,providedIn:"root",factory:()=>new t})}return t})(),Yc=(()=>{class t{static \u0275prov=v({token:t,providedIn:"root",factory:()=>new Tf})}return t})(),Tf=class{dirtyEffectCount=0;queues=new Map;add(n){this.enqueue(n),this.schedule(n)}schedule(n){n.dirty&&this.dirtyEffectCount++}remove(n){let e=n.zone,r=this.queues.get(e);r.has(n)&&(r.delete(n),n.dirty&&this.dirtyEffectCount--)}enqueue(n){let e=n.zone;this.queues.has(e)||this.queues.set(e,new Set);let r=this.queues.get(e);r.has(n)||r.add(n)}flush(){for(;this.dirtyEffectCount>0;){let n=!1;for(let[e,r]of this.queues)e===null?n||=this.flushQueue(r):n||=e.run(()=>this.flushQueue(r));n||(this.dirtyEffectCount=0)}}flushQueue(n){let e=!1;for(let r of n)r.dirty&&(this.dirtyEffectCount--,e=!0,r.run());return e}},Tc=class{[me];constructor(n){this[me]=n}destroy(){this[me].destroy()}};function Es(t,n){let e=n?.injector??f(W),r=n?.manualCleanup!==!0?e.get(Ue):null,o,i=e.get(Go,null,{optional:!0}),s=e.get(fn);return i!==null?(o=wT(i.view,s,t),r instanceof Sc&&r._lView===i.view&&(r=null)):o=CT(t,e.get(Yc),s),o.injector=e,r!==null&&(o.onDestroyFns=[r.onDestroy(()=>o.destroy())]),new Tc(o)}var yb=F(g({},hf),{cleanupFns:void 0,zone:null,onDestroyFns:null,run(){let t=is(!1);try{pf(this)}finally{is(t)}},cleanup(){if(!this.cleanupFns?.length)return;let t=M(null);try{for(;this.cleanupFns.length;)this.cleanupFns.pop()()}finally{this.cleanupFns=[],M(t)}}}),DT=F(g({},yb),{consumerMarkedDirty(){this.scheduler.schedule(this),this.notifier.notify(12)},destroy(){if(ar(this),this.onDestroyFns!==null)for(let t of this.onDestroyFns)t();this.cleanup(),this.scheduler.remove(this)}}),ET=F(g({},yb),{consumerMarkedDirty(){this.view[L]|=8192,Jr(this.view),this.notifier.notify(13)},destroy(){if(ar(this),this.onDestroyFns!==null)for(let t of this.onDestroyFns)t();this.cleanup(),this.view[dr]?.delete(this)}});function wT(t,n,e){let r=Object.create(ET);return r.view=t,r.zone=typeof Zone<"u"?Zone.current:null,r.notifier=n,r.fn=bb(r,e),t[dr]??=new Set,t[dr].add(r),r.consumerMarkedDirty(r),r}function CT(t,n,e){let r=Object.create(DT);return r.fn=bb(r,t),r.scheduler=n,r.notifier=e,r.zone=typeof Zone<"u"?Zone.current:null,r.scheduler.add(r),r.notifier.notify(12),r}function bb(t,n){return()=>{n(e=>(t.cleanupFns??=[]).push(e))}}function Os(t){return{toString:t}.toString()}function RT(t){return typeof t=="function"}function r_(t,n,e,r){n!==null?n.applyValueToInputSignal(n,r):t[e]=r}var ol=class{previousValue;currentValue;firstChange;constructor(n,e,r){this.previousValue=n,this.currentValue=e,this.firstChange=r}isFirstChange(){return this.firstChange}},$e=(()=>{let t=()=>o_;return t.ngInherit=!0,t})();function o_(t){return t.type.prototype.ngOnChanges&&(t.setInput=OT),NT}function NT(){let t=s_(this),n=t?.current;if(n){let e=t.previous;if(e===Bt)t.previous=n;else for(let r in n)e[r]=n[r];t.current=null,this.ngOnChanges(n)}}function OT(t,n,e,r,o){let i=this.declaredInputs[r],s=s_(t)||kT(t,{previous:Bt,current:null}),a=s.current||(s.current={}),c=s.previous,l=c[i];a[i]=new ol(l&&l.currentValue,e,c===Bt),r_(t,n,o,e)}var i_="__ngSimpleChanges__";function s_(t){return t[i_]||null}function kT(t,n){return t[i_]=n}var _b=[];var oe=function(t,n=null,e){for(let r=0;r<_b.length;r++){let o=_b[r];o(t,n,e)}},ee=(function(t){return t[t.TemplateCreateStart=0]="TemplateCreateStart",t[t.TemplateCreateEnd=1]="TemplateCreateEnd",t[t.TemplateUpdateStart=2]="TemplateUpdateStart",t[t.TemplateUpdateEnd=3]="TemplateUpdateEnd",t[t.LifecycleHookStart=4]="LifecycleHookStart",t[t.LifecycleHookEnd=5]="LifecycleHookEnd",t[t.OutputStart=6]="OutputStart",t[t.OutputEnd=7]="OutputEnd",t[t.BootstrapApplicationStart=8]="BootstrapApplicationStart",t[t.BootstrapApplicationEnd=9]="BootstrapApplicationEnd",t[t.BootstrapComponentStart=10]="BootstrapComponentStart",t[t.BootstrapComponentEnd=11]="BootstrapComponentEnd",t[t.ChangeDetectionStart=12]="ChangeDetectionStart",t[t.ChangeDetectionEnd=13]="ChangeDetectionEnd",t[t.ChangeDetectionSyncStart=14]="ChangeDetectionSyncStart",t[t.ChangeDetectionSyncEnd=15]="ChangeDetectionSyncEnd",t[t.AfterRenderHooksStart=16]="AfterRenderHooksStart",t[t.AfterRenderHooksEnd=17]="AfterRenderHooksEnd",t[t.ComponentStart=18]="ComponentStart",t[t.ComponentEnd=19]="ComponentEnd",t[t.DeferBlockStateStart=20]="DeferBlockStateStart",t[t.DeferBlockStateEnd=21]="DeferBlockStateEnd",t[t.DynamicComponentStart=22]="DynamicComponentStart",t[t.DynamicComponentEnd=23]="DynamicComponentEnd",t[t.HostBindingsUpdateStart=24]="HostBindingsUpdateStart",t[t.HostBindingsUpdateEnd=25]="HostBindingsUpdateEnd",t})(ee||{});function FT(t,n,e){let{ngOnChanges:r,ngOnInit:o,ngDoCheck:i}=n.type.prototype;if(r){let s=o_(n);(e.preOrderHooks??=[]).push(t,s),(e.preOrderCheckHooks??=[]).push(t,s)}o&&(e.preOrderHooks??=[]).push(0-t,o),i&&((e.preOrderHooks??=[]).push(t,i),(e.preOrderCheckHooks??=[]).push(t,i))}function a_(t,n){for(let e=n.directiveStart,r=n.directiveEnd;e=r)break}else n[c]<0&&(t[Zr]+=65536),(a>14>16&&(t[L]&3)===n&&(t[L]+=16384,Db(a,i)):Db(a,i)}var Yo=-1,ro=class{factory;name;injectImpl;resolving=!1;canSeeViewProviders;multi;componentProviders;index;providerFactory;constructor(n,e,r,o){this.factory=n,this.name=o,this.canSeeViewProviders=e,this.injectImpl=r}};function LT(t){return(t.flags&8)!==0}function jT(t){return(t.flags&16)!==0}function VT(t,n,e){let r=0;for(;rn){s=i-1;break}}}for(;i>16}function sl(t,n){let e=UT(t),r=n;for(;e>0;)r=r[Yr],e--;return r}var xh=!0;function al(t){let n=xh;return xh=t,n}var HT=256,d_=HT-1,f_=5,$T=0,vn={};function zT(t,n,e){let r;typeof e=="string"?r=e.charCodeAt(0)||0:e.hasOwnProperty(Gr)&&(r=e[Gr]),r==null&&(r=e[Gr]=$T++);let o=r&d_,i=1<>f_)]|=i}function cl(t,n){let e=h_(t,n);if(e!==-1)return e;let r=n[A];r.firstCreatePass&&(t.injectorIndex=n.length,bh(r.data,t),bh(n,null),bh(r.blueprint,null));let o=hp(t,n),i=t.injectorIndex;if(u_(o)){let s=il(o),a=sl(o,n),c=a[A].data;for(let l=0;l<8;l++)n[i+l]=a[s+l]|c[s+l]}return n[i+8]=o,i}function bh(t,n){t.push(0,0,0,0,0,0,0,0,n)}function h_(t,n){return t.injectorIndex===-1||t.parent&&t.parent.injectorIndex===t.injectorIndex||n[t.injectorIndex+8]===null?-1:t.injectorIndex}function hp(t,n){if(t.parent&&t.parent.injectorIndex!==-1)return t.parent.injectorIndex;let e=0,r=null,o=n;for(;o!==null;){if(r=y_(o),r===null)return Yo;if(e++,o=o[Yr],r.injectorIndex!==-1)return r.injectorIndex|e<<16}return Yo}function Ah(t,n,e){zT(t,n,e)}function GT(t,n){if(n==="class")return t.classes;if(n==="style")return t.styles;let e=t.attrs;if(e){let r=e.length,o=0;for(;o>20,d=r?a:a+u,h=o?a+u:l;for(let p=d;p=c&&m.type===e)return p}if(o){let p=s[c];if(p&&zt(p)&&p.type===e)return c}return null}function Ms(t,n,e,r,o){let i=t[e],s=n.data;if(i instanceof ro){let a=i;if(a.resolving)throw Ff("");let c=al(a.canSeeViewProviders);a.resolving=!0;let l=s[e].type||s[e],u,d=a.injectImpl?tt(a.injectImpl):null,h=uh(t,r,0);try{i=t[e]=a.factory(void 0,o,s,t,r),n.firstCreatePass&&e>=r.directiveStart&&FT(e,s[e],n)}finally{d!==null&&tt(d),al(c),a.resolving=!1,dh()}}return i}function qT(t){if(typeof t=="string")return t.charCodeAt(0)||0;let n=t.hasOwnProperty(Gr)?t[Gr]:void 0;return typeof n=="number"?n>=0?n&d_:YT:n}function wb(t,n,e){let r=1<>f_)]&r)}function Cb(t,n){return!(t&2)&&!(t&1&&n)}var no=class{_tNode;_lView;constructor(n,e){this._tNode=n,this._lView=e}get(n,e,r){return g_(this._tNode,this._lView,n,Hr(r),e)}};function YT(){return new no(Me(),I())}function Pe(t){return Os(()=>{let n=t.prototype.constructor,e=n[ns]||Rh(n),r=Object.prototype,o=Object.getPrototypeOf(t.prototype).constructor;for(;o&&o!==r;){let i=o[ns]||Rh(o);if(i&&i!==e)return i;o=Object.getPrototypeOf(o)}return i=>new i})}function Rh(t){return xf(t)?()=>{let n=Rh(Oe(t));return n&&n()}:ur(t)}function ZT(t,n,e,r,o){let i=t,s=n;for(;i!==null&&s!==null&&s[L]&2048&&!$o(s);){let a=v_(i,s,e,r|2,vn);if(a!==vn)return a;let c=i.parent;if(!c){let l=s[Gf];if(l){let u=l.get(e,vn,r&-5);if(u!==vn)return u}c=y_(s),s=s[Yr]}i=c}return o}function y_(t){let n=t[A],e=n.type;return e===2?n.declTNode:e===1?t[nt]:null}function ks(t){return GT(Me(),t)}function KT(){return ti(Me(),I())}function ti(t,n){return new $(ht(t,n))}var $=(()=>{class t{nativeElement;constructor(e){this.nativeElement=e}static __NG_ELEMENT_ID__=KT}return t})();function b_(t){return t instanceof $?t.nativeElement:t}function QT(){return this._results[Symbol.iterator]()}var Un=class{_emitDistinctChangesOnly;dirty=!0;_onDirty=void 0;_results=[];_changesDetected=!1;_changes=void 0;length=0;first=void 0;last=void 0;get changes(){return this._changes??=new x}constructor(n=!1){this._emitDistinctChangesOnly=n}get(n){return this._results[n]}map(n){return this._results.map(n)}filter(n){return this._results.filter(n)}find(n){return this._results.find(n)}reduce(n,e){return this._results.reduce(n,e)}forEach(n){this._results.forEach(n)}some(n){return this._results.some(n)}toArray(){return this._results.slice()}toString(){return this._results.toString()}reset(n,e){this.dirty=!1;let r=ky(n);(this._changesDetected=!Oy(this._results,r,e))&&(this._results=r,this.length=r.length,this.last=r[this.length-1],this.first=r[0])}notifyOnChanges(){this._changes!==void 0&&(this._changesDetected||!this._emitDistinctChangesOnly)&&this._changes.next(this)}onDirty(n){this._onDirty=n}setDirty(){this.dirty=!0,this._onDirty?.()}destroy(){this._changes!==void 0&&(this._changes.complete(),this._changes.unsubscribe())}[Symbol.iterator]=QT};function __(t){return(t.flags&128)===128}var pp=(function(t){return t[t.OnPush=0]="OnPush",t[t.Eager=1]="Eager",t[t.Default=1]="Default",t})(pp||{}),D_=new Map,XT=0;function JT(){return XT++}function e0(t){D_.set(t[Ln],t)}function Nh(t){D_.delete(t[Ln])}var Ib="__ngContext__";function Ko(t,n){jn(n)?(t[Ib]=n[Ln],e0(n)):t[Ib]=n}function E_(t){return C_(t[Uo])}function w_(t){return C_(t[Mt])}function C_(t){for(;t!==null&&!$t(t);)t=t[Mt];return t}var Oh;function mp(t){Oh=t}function I_(){if(Oh!==void 0)return Oh;if(typeof document<"u")return document;throw new b(210,!1)}var _r=new y("",{factory:()=>t0}),t0="ng";var El=new y(""),so=new y("",{providedIn:"platform",factory:()=>"unknown"}),Fs=new y(""),ao=new y("",{factory:()=>f(O).body?.querySelector("[ngCspNonce]")?.getAttribute("ngCspNonce")||null});var S_="r";var M_="di";var gp=new y(""),T_=!1,x_=new y("",{factory:()=>T_});var wl=new y("");var Sb=new WeakMap;function n0(t,n){if(t==null||typeof t!="object")return;let e=Sb.get(t);e||(e=new WeakSet,Sb.set(t,e)),e.add(n)}var r0=(t,n,e,r)=>{};function o0(t,n,e,r){r0(t,n,e,r)}function Cl(t){return(t.flags&32)===32}var i0=()=>null;function A_(t,n,e=!1){return i0(t,n,e)}function R_(t,n){let e=t.contentQueries;if(e!==null){let r=M(null);try{for(let o=0;ot,createScript:t=>t,createScriptURL:t=>t})}catch{}return Zc}function Il(t){return l0()?.createHTML(t)||t}var Kc;function N_(){if(Kc===void 0&&(Kc=null,we.trustedTypes))try{Kc=we.trustedTypes.createPolicy("angular#unsafe-bypass",{createHTML:t=>t,createScript:t=>t,createScriptURL:t=>t})}catch{}return Kc}function Mb(t){return N_()?.createHTML(t)||t}function Tb(t){return N_()?.createScriptURL(t)||t}var Hn=class{changingThisBreaksApplicationSecurity;constructor(n){this.changingThisBreaksApplicationSecurity=n}toString(){return`SafeValue must use [property]=binding: ${this.changingThisBreaksApplicationSecurity} (see ${xc})`}},Fh=class extends Hn{getTypeName(){return"HTML"}},Ph=class extends Hn{getTypeName(){return"Style"}},Lh=class extends Hn{getTypeName(){return"Script"}},jh=class extends Hn{getTypeName(){return"URL"}},Vh=class extends Hn{getTypeName(){return"ResourceURL"}};function vt(t){return t instanceof Hn?t.changingThisBreaksApplicationSecurity:t}function bn(t,n){let e=O_(t);if(e!=null&&e!==n){if(e==="ResourceURL"&&n==="URL")return!0;throw new Error(`Required a safe ${n}, got a ${e} (see ${xc})`)}return e===n}function O_(t){return t instanceof Hn&&t.getTypeName()||null}function yp(t){return new Fh(t)}function bp(t){return new Ph(t)}function _p(t){return new Lh(t)}function Dp(t){return new jh(t)}function Ep(t){return new Vh(t)}function u0(t){let n=new Uh(t);return d0()?new Bh(n):n}var Bh=class{inertDocumentHelper;constructor(n){this.inertDocumentHelper=n}getInertBodyElement(n){n=""+n;try{let e=new window.DOMParser().parseFromString(Il(n),"text/html").body;return e===null?this.inertDocumentHelper.getInertBodyElement(n):(e.firstChild?.remove(),e)}catch{return null}}},Uh=class{defaultDoc;inertDocument;constructor(n){this.defaultDoc=n,this.inertDocument=this.defaultDoc.implementation.createHTMLDocument("sanitization-inert")}getInertBodyElement(n){let e=this.inertDocument.createElement("template");return e.innerHTML=Il(n),e}};function d0(){try{return!!new window.DOMParser().parseFromString(Il(""),"text/html")}catch{return!1}}var f0=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:\/?#]*(?:[\/?#]|$))/i;function Ps(t){return t=String(t),t.match(f0)?t:"unsafe:"+t}function $n(t){let n={};for(let e of t.split(","))n[e]=!0;return n}function Ls(...t){let n={};for(let e of t)for(let r in e)e.hasOwnProperty(r)&&(n[r]=!0);return n}var k_=$n("area,br,col,hr,img,wbr"),F_=$n("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),P_=$n("rp,rt"),h0=Ls(P_,F_),p0=Ls(F_,$n("address,article,aside,blockquote,caption,center,del,details,dialog,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,main,map,menu,nav,ol,pre,section,summary,table,ul")),m0=Ls(P_,$n("a,abbr,acronym,audio,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,picture,q,ruby,rp,rt,s,samp,small,source,span,strike,strong,sub,sup,time,track,tt,u,var,video")),xb=Ls(k_,p0,m0,h0),L_=$n("background,cite,href,itemtype,longdesc,poster,src,xlink:href"),g0=$n("abbr,accesskey,align,alt,autoplay,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,controls,coords,datetime,default,dir,download,face,headers,height,hidden,hreflang,hspace,ismap,itemscope,itemprop,kind,label,lang,language,loop,media,muted,nohref,nowrap,open,preload,rel,rev,role,rows,rowspan,rules,scope,scrolling,shape,size,sizes,span,srclang,srcset,start,summary,tabindex,target,title,translate,type,usemap,valign,value,vspace,width"),v0=$n("aria-activedescendant,aria-atomic,aria-autocomplete,aria-busy,aria-checked,aria-colcount,aria-colindex,aria-colspan,aria-controls,aria-current,aria-describedby,aria-details,aria-disabled,aria-dropeffect,aria-errormessage,aria-expanded,aria-flowto,aria-grabbed,aria-haspopup,aria-hidden,aria-invalid,aria-keyshortcuts,aria-label,aria-labelledby,aria-level,aria-live,aria-modal,aria-multiline,aria-multiselectable,aria-orientation,aria-owns,aria-placeholder,aria-posinset,aria-pressed,aria-readonly,aria-relevant,aria-required,aria-roledescription,aria-rowcount,aria-rowindex,aria-rowspan,aria-selected,aria-setsize,aria-sort,aria-valuemax,aria-valuemin,aria-valuenow,aria-valuetext"),y0=Ls(L_,g0,v0),b0=$n("script,style,template"),Hh=class{sanitizedSomething=!1;buf=[];sanitizeChildren(n){let e=n.firstChild,r=!0,o=[];for(;e;){if(e.nodeType===Node.ELEMENT_NODE?r=this.startElement(e):e.nodeType===Node.TEXT_NODE?this.chars(e.nodeValue):this.sanitizedSomething=!0,r&&e.firstChild){o.push(e),e=E0(e);continue}for(;e;){e.nodeType===Node.ELEMENT_NODE&&this.endElement(e);let i=D0(e);if(i){e=i;break}e=o.pop()}}return this.buf.join("")}startElement(n){let e=Ab(n).toLowerCase();if(!xb.hasOwnProperty(e))return this.sanitizedSomething=!0,!b0.hasOwnProperty(e);this.buf.push("<"),this.buf.push(e);let r=n.attributes;for(let o=0;o"),!0}endElement(n){let e=Ab(n).toLowerCase();xb.hasOwnProperty(e)&&!k_.hasOwnProperty(e)&&(this.buf.push(""))}chars(n){this.buf.push(Rb(n))}};function _0(t,n){return(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_CONTAINED_BY)!==Node.DOCUMENT_POSITION_CONTAINED_BY}function D0(t){let n=t.nextSibling;if(n&&t!==n.previousSibling)throw j_(n);return n}function E0(t){let n=t.firstChild;if(n&&_0(t,n))throw j_(n);return n}function Ab(t){let n=t.nodeName;return typeof n=="string"?n:"FORM"}function j_(t){return new Error(`Failed to sanitize html because the element is clobbered: ${t.outerHTML}`)}var w0=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,C0=/([^\#-~ |!])/g;function Rb(t){return t.replace(/&/g,"&").replace(w0,function(n){let e=n.charCodeAt(0),r=n.charCodeAt(1);return"&#"+((e-55296)*1024+(r-56320)+65536)+";"}).replace(C0,function(n){return"&#"+n.charCodeAt(0)+";"}).replace(//g,">")}var Qc;function Sl(t,n){let e=null;try{Qc=Qc||u0(t);let r=n?String(n):"";e=Qc.getInertBodyElement(r);let o=5,i=r;do{if(o===0)throw new Error("Failed to sanitize html because the input is unstable");o--,r=i,i=e.innerHTML,e=Qc.getInertBodyElement(r)}while(r!==i);let a=new Hh().sanitizeChildren(Nb(e)||e);return Il(a)}finally{if(e){let r=Nb(e)||e;for(;r.firstChild;)r.firstChild.remove()}}}function Nb(t){return"content"in t&&I0(t)?t.content:null}function I0(t){return t.nodeType===Node.ELEMENT_NODE&&t.nodeName==="TEMPLATE"}var S0=/^>|^->||--!>|)/g,T0="\u200B$1\u200B";function x0(t){return t.replace(S0,n=>n.replace(M0,T0))}function A0(t,n){return t.createText(n)}function R0(t,n,e){t.setValue(n,e)}function N0(t,n){return t.createComment(x0(n))}function V_(t,n,e){return t.createElement(n,e)}function ll(t,n,e,r,o){t.insertBefore(n,e,r,o)}function B_(t,n,e){t.appendChild(n,e)}function Ob(t,n,e,r,o){r!==null?ll(t,n,e,r,o):B_(t,n,e)}function U_(t,n,e,r){t.removeChild(null,n,e,r)}function O0(t,n,e){t.setAttribute(n,"style",e)}function k0(t,n,e){e===""?t.removeAttribute(n,"class"):t.setAttribute(n,"class",e)}function H_(t,n,e){let{mergedAttrs:r,classes:o,styles:i}=e;r!==null&&VT(t,n,r),o!==null&&k0(t,n,o),i!==null&&O0(t,n,i)}function F0(t,n=!0){if(t[0]!=":")return[null,t];let e=t.indexOf(":",1);if(e===-1){if(n)throw new Error(`Unsupported format "${t}" expecting ":namespace:name"`);return[null,t]}return[t.slice(1,e),t.slice(e+1)]}function P0(t,n,e){if(n!==void 0&&e!==void 0&&G_(n,e)!==J.HTML)return t;let r=Cp();return r?Mb(r.sanitize(J.HTML,t)||""):bn(t,"HTML")?Mb(vt(t)):Sl(I_(),Se(t))}function $_(t){let n=Cp();return n?n.sanitize(J.URL,t)||"":bn(t,"URL")?vt(t):Ps(Se(t))}function z_(t){let n=Cp();if(n)return Tb(n.sanitize(J.RESOURCE_URL,t)||"");if(bn(t,"ResourceURL"))return Tb(vt(t));throw new b(904,!1)}function L0(t,n){switch(G_(t,n)){case J.RESOURCE_URL:return z_;case J.URL:return $_;default:return null}}function wp(t,n,e){return L0(n,e)?.(t)??t}function Cp(){let t=I();return t&&t[Ht].sanitizer}function G_(t,n){let[e,r]=j0(t);return $y(r,n,e)}function j0(t){t=t.toLowerCase();let n=F0(t,!1);if(n[0])return n;let r=st()===-1?null:to(),o=r?.namespace;if(t==="#host"&&r?.type===2){let i=ht(r,I());if(i.tagName&&(t=i.tagName.toLowerCase()),o==null){let s=i.namespaceURI;o=s&&c0[s]}}return[o,t]}function V0(t){return t.ownerDocument.defaultView}function B0(t){return t.ownerDocument}function W_(t){return t instanceof Function?t():t}function U0(t,n,e){let r=t.length;for(;;){let o=t.indexOf(n,e);if(o===-1)return o;if(o===0||t.charCodeAt(o-1)<=32){let i=n.length;if(o+i===r||t.charCodeAt(o+i)<=32)return o}e=o+1}}var q_="ng-template";function H0(t,n,e,r){let o=0;if(r){for(;o-1){let i;for(;++oi?d="":d=o[u+1].toLowerCase(),r&2&&l!==d){if(Gt(r))return!1;s=!0}}}}return Gt(r)||s}function Gt(t){return(t&1)===0}function G0(t,n,e,r){if(n===null)return-1;let o=0;if(r||!e){let i=!1;for(;o-1)for(e++;e0?'="'+a+'"':"")+"]"}else r&8?o+="."+s:r&4&&(o+=" "+s);else o!==""&&!Gt(s)&&(n+=kb(i,o),o=""),r=s,i=i||!Gt(r);e++}return o!==""&&(n+=kb(i,o)),n}function Q0(t){return t.map(K0).join(",")}function X0(t){let n=[],e=[],r=1,o=2;for(;r=0;i--){let s=e[i],a=s.parentNode;s===n?(e.splice(i,1),Cs.add(s),s.dispatchEvent(new CustomEvent("animationend",{detail:{cancel:!0}}))):(o&&s===o||a&&r&&a!==r)&&(e.splice(i,1),s.dispatchEvent(new CustomEvent("animationend",{detail:{cancel:!0}})),s.parentNode?.removeChild(s))}}function ox(t,n){let e=zh.get(t);e?e.includes(n)||e.push(n):zh.set(t,[n])}var oo=new Set,Tl=(function(t){return t[t.CHANGE_DETECTION=0]="CHANGE_DETECTION",t[t.AFTER_NEXT_RENDER=1]="AFTER_NEXT_RENDER",t})(Tl||{}),Zt=new y(""),Fb=new Set;function Kt(t){Fb.has(t)||(Fb.add(t),performance?.mark?.("mark_feature_usage",{detail:{feature:t}}))}var xl=(()=>{class t{impl=null;execute(){this.impl?.execute()}static \u0275prov=v({token:t,providedIn:"root",factory:()=>new t})}return t})(),Ap=[0,1,2,3],Rp=(()=>{class t{ngZone=f(j);scheduler=f(fn);errorHandler=f(Ze,{optional:!0});sequences=new Set;deferredRegistrations=new Set;executing=!1;constructor(){f(Zt,{optional:!0})}execute(){let e=this.sequences.size>0;e&&oe(ee.AfterRenderHooksStart),this.executing=!0;for(let r of Ap)for(let o of this.sequences)if(!(o.erroredOrDestroyed||!o.hooks[r]))try{o.pipelinedValue=this.ngZone.runOutsideAngular(()=>this.maybeTrace(()=>{let i=o.hooks[r];return i(o.pipelinedValue)},o.snapshot))}catch(i){o.erroredOrDestroyed=!0,this.errorHandler?.handleError(i)}this.executing=!1;for(let r of this.sequences)r.afterRun(),r.once&&(this.sequences.delete(r),r.destroy());for(let r of this.deferredRegistrations)this.sequences.add(r);this.deferredRegistrations.size>0&&this.scheduler.notify(7),this.deferredRegistrations.clear(),e&&oe(ee.AfterRenderHooksEnd)}register(e){let{view:r}=e;r!==void 0?((r[Kr]??=[]).push(e),Jr(r),r[L]|=8192):this.executing?this.deferredRegistrations.add(e):this.addSequence(e)}addSequence(e){this.sequences.add(e),this.scheduler.notify(7)}unregister(e){this.executing&&this.sequences.has(e)?(e.erroredOrDestroyed=!0,e.pipelinedValue=void 0,e.once=!0):(this.sequences.delete(e),this.deferredRegistrations.delete(e))}maybeTrace(e,r){return r?r.run(Tl.AFTER_NEXT_RENDER,e):e()}static \u0275prov=v({token:t,providedIn:"root",factory:()=>new t})}return t})(),Ts=class{impl;hooks;view;once;snapshot;erroredOrDestroyed=!1;pipelinedValue=void 0;unregisterOnDestroy;constructor(n,e,r,o,i,s=null){this.impl=n,this.hooks=e,this.view=r,this.once=o,this.snapshot=s,this.unregisterOnDestroy=i?.onDestroy(()=>this.destroy())}afterRun(){this.erroredOrDestroyed=!1,this.pipelinedValue=void 0,this.snapshot?.dispose(),this.snapshot=null}destroy(){this.impl.unregister(this),this.unregisterOnDestroy?.();let n=this.view?.[Kr];n&&(this.view[Kr]=n.filter(e=>e!==this))}};function At(t,n){let e=n?.injector??f(W);return Kt("NgAfterNextRender"),sx(t,e,n,!0)}function ix(t){return t instanceof Function?[void 0,void 0,t,void 0]:[t.earlyRead,t.write,t.mixedReadWrite,t.read]}function sx(t,n,e,r){let o=n.get(xl);o.impl??=n.get(Rp);let i=n.get(Zt,null,{optional:!0}),s=e?.manualCleanup!==!0?n.get(Ue):null,a=n.get(Go,null,{optional:!0}),c=new Ts(o.impl,ix(t),a?.view,r,s,i?.snapshot(null));return o.impl.register(c),c}var J_=new y("",{factory:()=>({queue:new Set,isScheduled:!1,scheduler:null,injector:f(re)})});function eD(t,n,e){let r=t.get(J_);if(Array.isArray(n))for(let o of n)r.queue.add(o),e?.detachedLeaveAnimationFns?.push(o);else r.queue.add(n),e?.detachedLeaveAnimationFns?.push(n);r.scheduler&&r.scheduler(t)}function ax(t,n){let e=t.get(J_);if(n.detachedLeaveAnimationFns){for(let r of n.detachedLeaveAnimationFns)e.queue.delete(r);n.detachedLeaveAnimationFns=void 0}}function cx(t,n){for(let[e,r]of n)eD(t,r.animateFns)}function Pb(t,n,e,r){let o=t?.[mr]?.enter;n!==null&&o&&o.has(e.index)&&cx(r,o)}function qo(t,n,e,r,o,i,s,a){if(o!=null){let c,l=!1;$t(o)?c=o:jn(o)&&(l=!0,o=o[Ut]);let u=Tt(o);t===0&&r!==null?(Pb(a,r,i,e),s==null?B_(n,r,u):ll(n,r,u,s||null,!0)):t===1&&r!==null?(Pb(a,r,i,e),ll(n,r,u,s||null,!0),rx(i,u)):t===2?(a?.[mr]?.leave?.has(i.index)&&ox(i,u),Cs.delete(u),Lb(a,i,e,d=>{if(Cs.has(u)){Cs.delete(u);return}U_(n,u,l,d)})):t===3&&(Cs.delete(u),Lb(a,i,e,()=>{n.destroyNode(u)})),c!=null&&bx(n,t,e,c,i,r,s)}}function lx(t,n){tD(t,n),n[Ut]=null,n[nt]=null}function ux(t,n,e,r,o,i){r[Ut]=o,r[nt]=n,Rl(t,r,e,1,o,i)}function tD(t,n){n[Ht].changeDetectionScheduler?.notify(9),Rl(t,n,n[se],2,null,null)}function dx(t){let n=t[Uo];if(!n)return _h(t[A],t);for(;n;){let e=null;if(jn(n))e=n[Uo];else{let r=n[ve];r&&(e=r)}if(!e){for(;n&&!n[Mt]&&n!==t;)jn(n)&&_h(n[A],n),n=n[ke];n===null&&(n=t),jn(n)&&_h(n[A],n),e=n&&n[Mt]}n=e}}function Np(t,n){let e=t[Qr],r=e.indexOf(n);e.splice(r,1)}function Al(t,n){if(Xr(n))return;let e=n[se];e.destroyNode&&Rl(t,n,e,3,null,null),dx(n)}function _h(t,n){if(Xr(n))return;let e=M(null);try{n[L]&=-129,n[L]|=256,n[ft]&&ar(n[ft]),px(t,n),hx(t,n),n[A].type===1&&n[se].destroy();let r=n[pr];if(r!==null&&$t(n[ke])){r!==n[ke]&&Np(r,n);let o=n[pn];o!==null&&o.detachView(t)}Nh(n)}finally{M(e)}}function Lb(t,n,e,r){let o=t?.[mr];if(o==null||o.leave==null||!o.leave.has(n.index))return r(!1);t&&oo.add(t[Ln]),eD(e,()=>{if(o.leave&&o.leave.has(n.index)){let s=o.leave.get(n.index),a=[];if(s){for(let c=0;c{t[mr].running=void 0,oo.delete(t[Ln]),n(!0)});return}n(!1)}function hx(t,n){let e=t.cleanup,r=n[Bo];if(e!==null)for(let s=0;s=0?r[a]():r[-a].unsubscribe(),s+=2}else{let a=r[e[s+1]];e[s].call(a)}r!==null&&(n[Bo]=null);let o=n[Fn];if(o!==null){n[Fn]=null;for(let s=0;sde&&X_(t,n,de,!1);let a=s?ee.TemplateUpdateStart:ee.TemplateCreateStart;oe(a,o,e),e(r,o)}finally{vr(i);let a=s?ee.TemplateUpdateEnd:ee.TemplateCreateEnd;oe(a,o,e)}}function Nl(t,n,e){Ix(t,n,e),(e.flags&64)===64&&Sx(t,n,e)}function js(t,n,e=ht){let r=n.localNames;if(r!==null){let o=n.index+1;for(let i=0;inull;function Cx(t){return t==="class"?"className":t==="for"?"htmlFor":t==="formaction"?"formAction":t==="innerHtml"?"innerHTML":t==="readonly"?"readOnly":t==="tabindex"?"tabIndex":t}function aD(t,n,e,r,o,i){let s=n[A];if(Ol(t,s,n,e,r)){mn(t)&&lD(n,t.index);return}t.type&3&&(e=Cx(e)),cD(t,n,e,r,o,i)}function cD(t,n,e,r,o,i){if(t.type&3){let s=ht(t,n);r=i!=null?i(r,t.value||"",e):r,o.setProperty(s,e,r)}else t.type&12}function lD(t,n){let e=xt(n,t);e[L]&16||(e[L]|=64)}function Ix(t,n,e){let r=e.directiveStart,o=e.directiveEnd;mn(e)&&tx(n,e,t.data[r+e.componentOffset]),t.firstCreatePass||cl(e,n);let i=e.initialInputs;for(let s=r;s{Jr(t.lView)},consumerOnSignalRead(){this.lView[ft]=this}});function jx(t){let n=t[ft]??Object.create(Vx);return n.lView=t,n}var Vx=F(g({},ir),{consumerIsAlwaysLive:!0,kind:"template",consumerMarkedDirty:t=>{let n=fr(t.lView);for(;n&&!pD(n[A]);)n=fr(n);n&&Xf(n)},consumerOnSignalRead(){this.lView[ft]=this}});function pD(t){return t.type!==2}function mD(t){if(t[dr]===null)return;let n=!0;for(;n;){let e=!1;for(let r of t[dr])r.dirty&&(e=!0,r.zone===null||Zone.current===r.zone?r.run():r.zone.run(()=>r.run()));n=e&&!!(t[L]&8192)}}var Bx=100;function gD(t,n=0){let r=t[Ht].rendererFactory,o=!1;o||r.begin?.();try{Ux(t,n)}finally{o||r.end?.()}}function Ux(t,n){let e=lh();try{is(!0),Wh(t,n);let r=0;for(;gs(t);){if(r===Bx)throw new b(103,!1);r++,Wh(t,1)}}finally{is(e)}}function Hx(t,n,e,r){if(Xr(n))return;let o=n[L],i=!1,s=!1;Gc(n);let a=!0,c=null,l=null;i||(pD(t)?(l=kx(n),c=kn(l)):bc()===null?(a=!1,l=jx(n),c=kn(l)):n[ft]&&(ar(n[ft]),n[ft]=null));try{Qf(n),tb(t.bindingStartIndex),e!==null&&sD(t,n,e,2,r);let u=(o&3)===3;if(!i)if(u){let p=t.preOrderCheckHooks;p!==null&&Jc(n,p,null)}else{let p=t.preOrderHooks;p!==null&&el(n,p,0,null),yh(n,0)}if(s||$x(n),mD(n),vD(n,0),t.contentQueries!==null&&R_(t,n),!i)if(u){let p=t.contentCheckHooks;p!==null&&Jc(n,p)}else{let p=t.contentHooks;p!==null&&el(n,p,1),yh(n,1)}Gx(t,n);let d=t.components;d!==null&&bD(n,d,0);let h=t.viewQuery;if(h!==null&&kh(2,h,r),!i)if(u){let p=t.viewCheckHooks;p!==null&&Jc(n,p)}else{let p=t.viewHooks;p!==null&&el(n,p,2),yh(n,2)}if(t.firstUpdatePass===!0&&(t.firstUpdatePass=!1),n[jc]){for(let p of n[jc])p();n[jc]=null}i||(fD(n),n[L]&=-73)}catch(u){throw i||Jr(n),u}finally{l!==null&&(sr(l,c),a&&Px(l)),Wc()}}function vD(t,n){for(let e=E_(t);e!==null;e=w_(e))for(let r=ve;r0&&(t[e-1][Mt]=r[Mt]);let i=fs(t,ve+n);lx(r[A],r);let s=i[pn];s!==null&&s.detachView(i[A]),r[ke]=null,r[Mt]=null,r[L]&=-129}return r}function Wx(t,n,e,r){let o=ve+r,i=e.length;r>0&&(e[o-1][Mt]=n),r-1&&(As(n,r),fs(e,r))}this._attachedToViewContainer=!1}Al(this._lView[A],this._lView)}onDestroy(n){Jf(this._lView,n)}markForCheck(){Vp(this._cdRefInjectingView||this._lView,4)}detach(){this._lView[L]&=-129}reattach(){Uc(this._lView),this._lView[L]|=128}detectChanges(){this._lView[L]|=1024,gD(this._lView)}checkNoChanges(){}attachToViewContainerRef(){if(this._appRef)throw new b(902,!1);this._attachedToViewContainer=!0}detachFromAppRef(){this._appRef=null;let n=$o(this._lView),e=this._lView[pr];e!==null&&!n&&Np(e,this._lView),tD(this._lView[A],this._lView)}attachToAppRef(n){if(this._attachedToViewContainer)throw new b(902,!1);this._appRef=n;let e=$o(this._lView),r=this._lView[pr];r!==null&&!e&&wD(r,this._lView),Uc(this._lView)}};var gt=(()=>{class t{_declarationLView;_declarationTContainer;elementRef;static __NG_ELEMENT_ID__=qx;constructor(e,r,o){this._declarationLView=e,this._declarationTContainer=r,this.elementRef=o}get ssrId(){return this._declarationTContainer.tView?.ssrId||null}createEmbeddedView(e,r){return this.createEmbeddedViewImpl(e,r)}createEmbeddedViewImpl(e,r,o){let i=Vs(this._declarationLView,this._declarationTContainer,e,{embeddedViewInjector:r,dehydratedView:o});return new yr(i)}}return t})();function qx(){return kl(Me(),I())}function kl(t,n){return t.type&4?new gt(n,t,ti(t,n)):null}function ni(t,n,e,r,o){let i=t.data[n];if(i===null)i=Yx(t,n,e,r,o),nb()&&(i.flags|=32);else if(i.type&64){i.type=e,i.value=r,i.attrs=o;let s=Jy();i.injectorIndex=s===null?-1:s.injectorIndex}return zo(i,!0),i}function Yx(t,n,e,r,o){let i=sh(),s=ah(),a=s?i:i&&i.parent,c=t.data[n]=Kx(t,a,e,n,r,o);return Zx(t,c,i,s),c}function Zx(t,n,e,r){t.firstChild===null&&(t.firstChild=n),e!==null&&(r?e.child==null&&n.parent!==null&&(e.child=n):e.next===null&&(e.next=n,n.prev=e))}function Kx(t,n,e,r,o,i){let s=n?n.injectorIndex:-1,a=0;return rh()&&(a|=128),{type:e,index:r,insertBeforeIndex:null,injectorIndex:s,directiveStart:-1,directiveEnd:-1,directiveStylingLast:-1,componentOffset:-1,controlDirectiveIndex:-1,customControlIndex:-1,propertyBindings:null,flags:a,providerIndexes:0,value:o,namespace:fh(),attrs:i,mergedAttrs:null,localNames:null,initialInputs:null,inputs:null,hostDirectiveInputs:null,outputs:null,hostDirectiveOutputs:null,directiveToIndex:null,tView:null,next:null,prev:null,projectionNext:null,child:null,parent:n,projection:null,styles:null,stylesWithoutHost:null,residualStyles:void 0,classes:null,classesWithoutHost:null,residualClasses:void 0,classBindings:0,styleBindings:0}}function Qx(t){let n=t[Wf]??[],r=t[ke][se],o=[];for(let i of n)i.data[M_]!==void 0?o.push(i):Xx(i,r);t[Wf]=o}function Xx(t,n){let e=0,r=t.firstChild;if(r){let o=t.data[S_];for(;enull,eA=()=>null;function ul(t,n){return Jx(t,n)}function CD(t,n,e){return eA(t,n,e)}var ID=class{},Fl=class{},qh=class{resolveComponentFactory(n){throw new b(917,!1)}},Us=class{static NULL=new qh},He=class{},Le=(()=>{class t{destroyNode=null;static __NG_ELEMENT_ID__=()=>tA()}return t})();function tA(){let t=I(),n=Me(),e=xt(n.index,t);return(jn(e)?e:t)[se]}var SD=(()=>{class t{static \u0275prov=v({token:t,providedIn:"root",factory:()=>null})}return t})();var nl={},Yh=class{injector;parentInjector;constructor(n,e){this.injector=n,this.parentInjector=e}get(n,e,r){let o=this.injector.get(n,nl,r);return o!==nl||e===nl?o:this.parentInjector.get(n,e,r)}};function dl(t,n,e){let r=e?t.styles:null,o=e?t.classes:null,i=0;if(n!==null)for(let s=0;s0&&(e.directiveToIndex=new Map);for(let h=0;h0;){let e=t[--n];if(typeof e=="number"&&e<0)return e}return 0}function lA(t,n,e){if(e){if(n.exportAs)for(let r=0;rr(Tt(_[t.index])):t.index;OD(m,n,e,i,a,p,!1)}}return l}function fA(t){return t.startsWith("animation")||t.startsWith("transition")}function hA(t,n,e,r){let o=t.cleanup;if(o!=null)for(let i=0;ic?a[c]:null}typeof s=="string"&&(i+=2)}return null}function OD(t,n,e,r,o,i,s){let a=n.firstCreatePass?th(n):null,c=eh(e),l=c.length;c.push(o,i),a&&a.push(r,t,l,(l+1)*(s?-1:1))}function $b(t,n,e,r,o,i){let s=n[e],a=n[A],l=a.data[e].outputs[r],d=s[l].subscribe(i);OD(t.index,a,n,o,i,d,!0)}var Zh=Symbol("BINDING");function kD(t){return t.debugInfo?.className||t.type.name||null}var fl=class extends Us{ngModule;constructor(n){super(),this.ngModule=n}resolveComponentFactory(n){let e=hn(n);return new br(e,this.ngModule)}};function pA(t){return Object.keys(t).map(n=>{let[e,r,o]=t[n],i={propName:e,templateName:n,isSignal:(r&Ml.SignalBased)!==0};return o&&(i.transform=o),i})}function mA(t){return Object.keys(t).map(n=>({propName:t[n],templateName:n}))}function gA(t,n,e){let r=n instanceof re?n:n?.injector;return r&&t.getStandaloneInjector!==null&&(r=t.getStandaloneInjector(r)||r),r?new Yh(e,r):e}function vA(t){let n=t.get(He,null);if(n===null)throw new b(407,!1);let e=t.get(SD,null),r=t.get(fn,null),o=t.get(Zt,null,{optional:!0});return{rendererFactory:n,sanitizer:e,changeDetectionScheduler:r,ngReflect:!1,tracingService:o}}function yA(t,n){let e=FD(t);return V_(n,e,e==="svg"?os:e==="math"?Yf:null)}function bA(t){if(t?.toLowerCase()==="script")throw new b(905,!1)}function FD(t){return(t.selectors[0][0]||"div").toLowerCase()}var br=class extends Fl{componentDef;ngModule;selector;componentType;ngContentSelectors;isBoundToModule;cachedInputs=null;cachedOutputs=null;get inputs(){return this.cachedInputs??=pA(this.componentDef.inputs),this.cachedInputs}get outputs(){return this.cachedOutputs??=mA(this.componentDef.outputs),this.cachedOutputs}constructor(n,e){super(),this.componentDef=n,this.ngModule=e,this.componentType=n.type,this.selector=Q0(n.selectors),this.ngContentSelectors=n.ngContentSelectors??[],this.isBoundToModule=!!e}create(n,e,r,o,i,s){oe(ee.DynamicComponentStart);let a=M(null);try{let c=this.componentDef,l=gA(c,o||this.ngModule,n),u=vA(l),d=u.tracingService;return d&&d.componentCreate?d.componentCreate(kD(c),()=>this.createComponentRef(u,l,e,r,i,s)):this.createComponentRef(u,l,e,r,i,s)}finally{M(a)}}createComponentRef(n,e,r,o,i,s){let a=this.componentDef,c=_A(o,a,s,i),l=n.rendererFactory.createRenderer(null,a),u=o?Dx(l,o,a.encapsulation,e):yA(a,l);bA(u?.tagName);let d=s?.some(zb)||i?.some(m=>typeof m!="function"&&m.bindings.some(zb)),h=Mp(null,c,null,512|Z_(a),null,null,n,l,e,null,A_(u,e,!0));h[de]=u,Gc(h);let p=null;try{let m=Up(de,h,2,"#host",()=>c.directiveRegistry,!0,0);H_(l,u,m),Ko(u,h),Nl(c,h,m),vp(c,m,h),Hp(c,m),r!==void 0&&EA(m,this.ngContentSelectors,r),p=xt(m.index,h),h[ge]=p[ge],jp(c,h,null)}catch(m){throw p!==null&&Nh(p),Nh(h),m}finally{oe(ee.DynamicComponentEnd),Wc()}return new hl(this.componentType,h,!!d)}};function _A(t,n,e,r){let o=t?["ng-version","21.2.21"]:X0(n.selectors[0]),i=null,s=null,a=0;if(e)for(let u of e)a+=u[Zh].requiredVars,u.create&&(u.targetIdx=0,(i??=[]).push(u)),u.update&&(u.targetIdx=0,(s??=[]).push(u));if(r)for(let u=0;u{if(e&1&&t)for(let r of t)r.create();if(e&2&&n)for(let r of n)r.update()}}function zb(t){let n=t[Zh].kind;return n==="input"||n==="twoWay"}var hl=class extends ID{_rootLView;_hasInputBindings;instance;hostView;changeDetectorRef;componentType;location;previousInputValues=null;_tNode;constructor(n,e,r){super(),this._rootLView=e,this._hasInputBindings=r,this._tNode=Vc(e[A],de),this.location=ti(this._tNode,e),this.instance=xt(this._tNode.index,e)[ge],this.hostView=this.changeDetectorRef=new yr(e,void 0),this.componentType=n}setInput(n,e){this._hasInputBindings;let r=this._tNode;if(this.previousInputValues??=new Map,this.previousInputValues.has(n)&&Object.is(this.previousInputValues.get(n),e))return;let o=this._rootLView,i=Ol(r,o[A],o,n,e);this.previousInputValues.set(n,e);let s=xt(r.index,o);Vp(s,1)}get injector(){return new no(this._tNode,this._rootLView)}destroy(){this.hostView.destroy()}onDestroy(n){this.hostView.onDestroy(n)}};function EA(t,n,e){let r=t.projection=[];for(let o=0;o{class t{static __NG_ELEMENT_ID__=wA}return t})();function wA(){let t=Me();return PD(t,I())}var Kh=class t extends Qe{_lContainer;_hostTNode;_hostLView;constructor(n,e,r){super(),this._lContainer=n,this._hostTNode=e,this._hostLView=r}get element(){return ti(this._hostTNode,this._hostLView)}get injector(){return new no(this._hostTNode,this._hostLView)}get parentInjector(){let n=hp(this._hostTNode,this._hostLView);if(u_(n)){let e=sl(n,this._hostLView),r=il(n),o=e[A].data[r+8];return new no(o,e)}else return new no(null,this._hostLView)}clear(){for(;this.length>0;)this.remove(this.length-1)}get(n){let e=Gb(this._lContainer);return e!==null&&e[n]||null}get length(){return this._lContainer.length-ve}createEmbeddedView(n,e,r){let o,i;typeof r=="number"?o=r:r!=null&&(o=r.index,i=r.injector);let s=ul(this._lContainer,n.ssrId),a=n.createEmbeddedViewImpl(e||{},i,s);return this.insertImpl(a,o,Qo(this._hostTNode,s)),a}createComponent(n,e,r,o,i,s,a){let c=n&&!RT(n),l;if(c)l=e;else{let D=e||{};l=D.index,r=D.injector,o=D.projectableNodes,i=D.environmentInjector||D.ngModuleRef,s=D.directives,a=D.bindings}let u=c?n:new br(hn(n)),d=r||this.parentInjector;if(!i&&u.ngModule==null){let C=(c?d:this.parentInjector).get(re,null);C&&(i=C)}let h=hn(u.componentType??{}),p=ul(this._lContainer,h?.id??null),m=p?.firstChild??null,_=u.create(d,o,m,i,s,a);return this.insertImpl(_.hostView,l,Qo(this._hostTNode,p)),_}insert(n,e){return this.insertImpl(n,e,!0)}insertImpl(n,e,r){let o=n._lView;if(Gy(o)){let a=this.indexOf(n);if(a!==-1)this.detach(a);else{let c=o[ke],l=new t(c,c[nt],c[ke]);l.detach(l.indexOf(n))}}let i=this._adjustIndex(e),s=this._lContainer;return Bs(s,o,i,r),n.attachToViewContainerRef(),Lf(Dh(s),i,n),n}move(n,e){return this.insert(n,e)}indexOf(n){let e=Gb(this._lContainer);return e!==null?e.indexOf(n):-1}remove(n){let e=this._adjustIndex(n,-1),r=As(this._lContainer,e);r&&(fs(Dh(this._lContainer),e),Al(r[A],r))}detach(n){let e=this._adjustIndex(n,-1),r=As(this._lContainer,e);return r&&fs(Dh(this._lContainer),e)!=null?new yr(r):null}_adjustIndex(n,e=0){return n??this.length+e}};function Gb(t){return t[ps]}function Dh(t){return t[ps]||(t[ps]=[])}function PD(t,n){let e,r=n[t.index];return $t(r)?e=r:(e=_D(r,n,null,t),n[t.index]=e,Tp(n,e)),IA(e,n,t,r),new Kh(e,t,n)}function CA(t,n){let e=t[se],r=e.createComment(""),o=ht(n,t),i=e.parentNode(o);return ll(e,i,r,e.nextSibling(o),!1),r}var IA=TA,SA=()=>!1;function MA(t,n,e){return SA(t,n,e)}function TA(t,n,e,r){if(t[gr])return;let o;e.type&8?o=Tt(r):o=CA(n,e),t[gr]=o}var Qh=class t{queryList;matches=null;constructor(n){this.queryList=n}clone(){return new t(this.queryList)}setDirty(){this.queryList.setDirty()}},Xh=class t{queries;constructor(n=[]){this.queries=n}createEmbeddedView(n){let e=n.queries;if(e!==null){let r=n.contentQueries!==null?n.contentQueries[0]:e.length,o=[];for(let i=0;i0)r.push(s[a/2]);else{let l=i[a+1],u=n[-c];for(let d=ve;dn.trim())}function UD(t,n,e){t.queries===null&&(t.queries=new Jh),t.queries.track(new ep(n,e))}function kA(t,n){let e=t.contentQueries||(t.contentQueries=[]),r=e.length?e[e.length-1]:-1;n!==r&&e.push(t.queries.length-1,n)}function Wp(t,n){return t.queries.getByIndex(n)}function HD(t,n){let e=t[A],r=Wp(e,n);return r.crossesNgTemplate?tp(e,t,n,[]):LD(e,t,r,n)}function qp(t,n,e){let r,o=Ji(()=>{r._dirtyCounter();let i=FA(r,t);if(n&&i===void 0)throw new b(-951,!1);return i});return r=o[me],r._dirtyCounter=K(0),r._flatValue=void 0,o}function Yp(t){return qp(!0,!1,t)}function Zp(t){return qp(!0,!0,t)}function $D(t){return qp(!1,!1,t)}function zD(t,n){let e=t[me];e._lView=I(),e._queryIndex=n,e._queryList=Gp(e._lView,n),e._queryList.onDirty(()=>e._dirtyCounter.update(r=>r+1))}function FA(t,n){let e=t._lView,r=t._queryIndex;if(e===void 0||r===void 0||e[L]&4)return n?void 0:Ye;let o=Gp(e,r),i=HD(e,r);return o.reset(i,b_),n?o.first:o._changesDetected||t._flatValue===void 0?t._flatValue=o.toArray():t._flatValue}var yn=class{},Ll=class{};var ml=class extends yn{ngModuleType;_parent;_bootstrapComponents=[];_r3Injector;instance;destroyCbs=[];componentFactoryResolver=new fl(this);constructor(n,e,r,o=!0){super(),this.ngModuleType=n,this._parent=e;let i=kf(n);this._bootstrapComponents=W_(i.bootstrap),this._r3Injector=hh(n,e,[{provide:yn,useValue:this},{provide:Us,useValue:this.componentFactoryResolver},...r],cs(n),new Set(["environment"])),o&&this.resolveInjectorInitializers()}resolveInjectorInitializers(){this._r3Injector.resolveInjectorInitializers(),this.instance=this._r3Injector.get(this.ngModuleType)}get injector(){return this._r3Injector}destroy(){let n=this._r3Injector;!n.destroyed&&n.destroy(),this.destroyCbs.forEach(e=>e()),this.destroyCbs=null}onDestroy(n){this.destroyCbs.push(n)}},gl=class extends Ll{moduleType;constructor(n){super(),this.moduleType=n}create(n){return new ml(this.moduleType,n,[])}};var Rs=class extends yn{injector;componentFactoryResolver=new fl(this);instance=null;constructor(n){super();let e=new zr([...n.providers,{provide:yn,useValue:this},{provide:Us,useValue:this.componentFactoryResolver}],n.parent||Vo(),n.debugName,new Set(["environment"]));this.injector=e,n.runEnvironmentInitializers&&e.resolveInjectorInitializers()}destroy(){this.injector.destroy()}onDestroy(n){this.injector.onDestroy(n)}};function oi(t,n,e=null){return new Rs({providers:t,parent:n,debugName:e,runEnvironmentInitializers:!0}).injector}var PA=(()=>{class t{_injector;cachedInjectors=new Map;constructor(e){this._injector=e}getOrCreateStandaloneInjector(e){if(!e.standalone)return null;if(!this.cachedInjectors.has(e)){let r=Bf(!1,e.type),o=r.length>0?oi([r],this._injector,""):null;this.cachedInjectors.set(e,o)}return this.cachedInjectors.get(e)}ngOnDestroy(){try{for(let e of this.cachedInjectors.values())e!==null&&e.destroy()}finally{this.cachedInjectors.clear()}}static \u0275prov=v({token:t,providedIn:"environment",factory:()=>new t(w(re))})}return t})();function Te(t){return Os(()=>{let n=GD(t),e=F(g({},n),{decls:t.decls,vars:t.vars,template:t.template,consts:t.consts||null,ngContentSelectors:t.ngContentSelectors,onPush:t.changeDetection===pp.OnPush,directiveDefs:null,pipeDefs:null,dependencies:n.standalone&&t.dependencies||null,getStandaloneInjector:n.standalone?o=>o.get(PA).getOrCreateStandaloneInjector(e):null,getExternalStyles:null,signals:t.signals??!1,data:t.data||{},encapsulation:t.encapsulation||qt.Emulated,styles:t.styles||Ye,_:null,schemas:t.schemas||null,tView:null,id:""});n.standalone&&Kt("NgStandalone"),WD(e);let r=t.dependencies;return e.directiveDefs=Wb(r,LA),e.pipeDefs=Wb(r,xy),e.id=BA(e),e})}function LA(t){return hn(t)||Nc(t)}function Q(t){return Os(()=>({type:t.type,bootstrap:t.bootstrap||Ye,declarations:t.declarations||Ye,imports:t.imports||Ye,exports:t.exports||Ye,transitiveCompileScopes:null,schemas:t.schemas||null,id:t.id||null}))}function jA(t,n){if(t==null)return Bt;let e={};for(let r in t)if(t.hasOwnProperty(r)){let o=t[r],i,s,a,c;Array.isArray(o)?(a=o[0],i=o[1],s=o[2]??i,c=o[3]||null):(i=o,s=o,a=Ml.None,c=null),e[i]=[r,a,c],n[i]=s}return e}function VA(t){if(t==null)return Bt;let n={};for(let e in t)t.hasOwnProperty(e)&&(n[t[e]]=e);return n}function T(t){return Os(()=>{let n=GD(t);return WD(n),n})}function ii(t){return{type:t.type,name:t.name,factory:null,pure:t.pure!==!1,standalone:t.standalone??!0,onDestroy:t.type.prototype.ngOnDestroy||null}}function GD(t){let n={};return{type:t.type,providersResolver:null,viewProvidersResolver:null,factory:null,hostBindings:t.hostBindings||null,hostVars:t.hostVars||0,hostAttrs:t.hostAttrs||null,contentQueries:t.contentQueries||null,declaredInputs:n,inputConfig:t.inputs||Bt,exportAs:t.exportAs||null,standalone:t.standalone??!0,signals:t.signals===!0,selectors:t.selectors||Ye,viewQuery:t.viewQuery||null,features:t.features||null,setInput:null,resolveHostDirectives:null,hostDirectives:null,controlDef:null,inputs:jA(t.inputs,n),outputs:VA(t.outputs),debugInfo:null}}function WD(t){t.features?.forEach(n=>n(t))}function Wb(t,n){return t?()=>{let e=typeof t=="function"?t():t,r=[];for(let o of e){let i=n(o);i!==null&&r.push(i)}return r}:null}function BA(t){let n=0,e=typeof t.consts=="function"?"":t.consts,r=[t.selectors,t.ngContentSelectors,t.hostVars,t.hostAttrs,e,t.vars,t.decls,t.encapsulation,t.standalone,t.signals,t.exportAs,JSON.stringify(t.inputs),JSON.stringify(t.outputs),Object.getOwnPropertyNames(t.type.prototype),!!t.contentQueries,!!t.viewQuery];for(let i of r.join("|"))n=Math.imul(31,n)+i.charCodeAt(0)<<0;return n+=2147483648,"c"+n}function UA(t){let n=e=>{let r=Array.isArray(t);e.hostDirectives===null?(e.resolveHostDirectives=HA,e.hostDirectives=r?t.map(np):[t]):r?e.hostDirectives.unshift(...t.map(np)):e.hostDirectives.unshift(t)};return n.ngInherit=!0,n}function HA(t){let n=[],e=!1,r=null,o=null;for(let i=0;i=0;r--){let o=t[r];o.hostVars=n+=o.hostVars,o.hostAttrs=Zo(o.hostAttrs,e=Zo(e,o.hostAttrs))}}function Eh(t){return t===Bt?{}:t===Ye?[]:t}function qA(t,n){let e=t.viewQuery;e?t.viewQuery=(r,o)=>{n(r,o),e(r,o)}:t.viewQuery=n}function YA(t,n){let e=t.contentQueries;e?t.contentQueries=(r,o,i)=>{n(r,o,i),e(r,o,i)}:t.contentQueries=n}function ZA(t,n){let e=t.hostBindings;e?t.hostBindings=(r,o)=>{n(r,o),e(r,o)}:t.hostBindings=n}function YD(t,n,e,r,o,i,s,a){if(e.firstCreatePass){t.mergedAttrs=Zo(t.mergedAttrs,t.attrs);let u=t.tView=Sp(2,t,o,i,s,e.directiveRegistry,e.pipeRegistry,null,e.schemas,e.consts,null);e.queries!==null&&(e.queries.template(e,t),u.queries=e.queries.embeddedTView(t))}a&&(t.flags|=a),zo(t,!1);let c=QA(e,n,t,r);qc()&&Op(e,n,c,t),Ko(c,n);let l=_D(c,n,c,t);n[r+de]=l,Tp(n,l),MA(l,t,n)}function KA(t,n,e,r,o,i,s,a,c,l,u){let d=e+de,h;return n.firstCreatePass?(h=ni(n,d,4,s||null,a||null),Hc()&&MD(n,t,h,pt(n.consts,l),Fp),a_(n,h)):h=n.data[d],YD(h,t,n,e,r,o,i,c),Ho(h)&&Nl(n,t,h),l!=null&&js(t,h,u),h}function Jo(t,n,e,r,o,i,s,a,c,l,u){let d=e+de,h;if(n.firstCreatePass){if(h=ni(n,d,4,s||null,a||null),l!=null){let p=pt(n.consts,l);h.localNames=[];for(let m=0;m{class t{log(e){console.log(e)}warn(e){console.warn(e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"platform"})}return t})();function si(t){return typeof t=="function"&&t[me]!==void 0}function Kp(t){return si(t)&&typeof t.set=="function"}var Vl=new y(""),Bl=new y(""),Hs=(()=>{class t{_ngZone;registry;_isZoneStable=!0;_callbacks=[];_taskTrackingZone=null;_destroyRef;constructor(e,r,o){this._ngZone=e,this.registry=r,$f()&&(this._destroyRef=f(Ue,{optional:!0})??void 0),Qp||(XD(o),o.addToWindow(r)),this._watchAngularEvents(),e.run(()=>{this._taskTrackingZone=typeof Zone>"u"?null:Zone.current.get("TaskTrackingZone")})}_watchAngularEvents(){let e=this._ngZone.onUnstable.subscribe({next:()=>{this._isZoneStable=!1}}),r=this._ngZone.runOutsideAngular(()=>this._ngZone.onStable.subscribe({next:()=>{j.assertNotInAngularZone(),queueMicrotask(()=>{this._isZoneStable=!0,this._runCallbacksIfReady()})}}));this._destroyRef?.onDestroy(()=>{e.unsubscribe(),r.unsubscribe()})}isStable(){return this._isZoneStable&&!this._ngZone.hasPendingMacrotasks}_runCallbacksIfReady(){if(this.isStable())queueMicrotask(()=>{for(;this._callbacks.length!==0;){let e=this._callbacks.pop();clearTimeout(e.timeoutId),e.doneCb()}});else{let e=this.getPendingTasks();this._callbacks=this._callbacks.filter(r=>r.updateCb&&r.updateCb(e)?(clearTimeout(r.timeoutId),!1):!0)}}getPendingTasks(){return this._taskTrackingZone?this._taskTrackingZone.macroTasks.map(e=>({source:e.source,creationLocation:e.creationLocation,data:e.data})):[]}addCallback(e,r,o){let i=-1;r&&r>0&&(i=setTimeout(()=>{this._callbacks=this._callbacks.filter(s=>s.timeoutId!==i),e()},r)),this._callbacks.push({doneCb:e,timeoutId:i,updateCb:o})}whenStable(e,r,o){if(o&&!this._taskTrackingZone)throw new Error('Task tracking zone is required when passing an update callback to whenStable(). Is "zone.js/plugins/task-tracking" loaded?');this.addCallback(e,r,o),this._runCallbacksIfReady()}registerApplication(e){this.registry.registerApplication(e,this)}unregisterApplication(e){this.registry.unregisterApplication(e)}findProviders(e,r,o){return[]}static \u0275fac=function(r){return new(r||t)(w(j),w(QD),w(Bl))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),QD=(()=>{class t{_applications=new Map;registerApplication(e,r){this._applications.set(e,r)}unregisterApplication(e){this._applications.delete(e)}unregisterAllApplications(){this._applications.clear()}getTestability(e){return this._applications.get(e)||null}getAllTestabilities(){return Array.from(this._applications.values())}getAllRootElements(){return Array.from(this._applications.keys())}findTestabilityInTree(e,r=!0){return Qp?.findTestabilityInTree(this,e,r)??null}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"platform"})}return t})();function XD(t){Qp=t}var Qp;function zn(t){return!!t&&typeof t.then=="function"}function Ul(t){return!!t&&typeof t.subscribe=="function"}var Xp=new y("");function Hl(t){return hr([{provide:Xp,multi:!0,useValue:t}])}var Jp=(()=>{class t{resolve;reject;initialized=!1;done=!1;donePromise=new Promise((e,r)=>{this.resolve=e,this.reject=r});appInits=f(Xp,{optional:!0})??[];injector=f(W);constructor(){}runInitializers(){if(this.initialized)return;let e=[];for(let o of this.appInits){let i=Fe(this.injector,o);if(zn(i))e.push(i);else if(Ul(i)){let s=new Promise((a,c)=>{i.subscribe({complete:a,error:c})});e.push(s)}}let r=()=>{this.done=!0,this.resolve()};Promise.all(e).then(()=>{r()}).catch(o=>{this.reject(o)}),e.length===0&&r(),this.initialized=!0}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),$l=new y("");function JD(){df(()=>{let t="";throw new b(600,t)})}function eE(t){return t.isBoundToModule}var JA=10;var Ge=(()=>{class t{_runningTick=!1;_destroyed=!1;_destroyListeners=[];_views=[];internalErrorHandler=f(mt);afterRenderManager=f(xl);zonelessEnabled=f(Ds);rootEffectScheduler=f(Yc);dirtyFlags=0;tracingSnapshot=null;allTestViews=new Set;autoDetectTestViews=new Set;includeAllTestViews=!1;afterTick=new x;get allViews(){return[...(this.includeAllTestViews?this.allTestViews:this.autoDetectTestViews).keys(),...this._views]}get destroyed(){return this._destroyed}componentTypes=[];components=[];internalPendingTask=f(Bn);get isStable(){return this.internalPendingTask.hasPendingTasksObservable.pipe(V(e=>!e))}constructor(){f(Zt,{optional:!0})}whenStable(){let e;return new Promise(r=>{e=this.isStable.subscribe({next:o=>{o&&r()}})}).finally(()=>{e.unsubscribe()})}_injector=f(re);_rendererFactory=null;get injector(){return this._injector}bootstrap(e,r){return this.bootstrapImpl(e,r)}bootstrapImpl(e,r,o=W.NULL){return this._injector.get(j).run(()=>{oe(ee.BootstrapComponentStart);let s=e instanceof Fl;if(!this._injector.get(Jp).done){let m="";throw new b(405,m)}let c;s?c=e:c=this._injector.get(Us).resolveComponentFactory(e),this.componentTypes.push(c.componentType);let l=eE(c)?void 0:this._injector.get(yn),u=r||c.selector,d=c.create(o,[],u,l),h=d.location.nativeElement,p=d.injector.get(Vl,null);return p?.registerApplication(h),d.onDestroy(()=>{this.detachView(d.hostView),Ss(this.components,d),p?.unregisterApplication(h)}),this._loadComponent(d),oe(ee.BootstrapComponentEnd,d),d})}tick(){this.zonelessEnabled||(this.dirtyFlags|=1),this._tick()}_tick(){oe(ee.ChangeDetectionStart),this.tracingSnapshot!==null?this.tracingSnapshot.run(Tl.CHANGE_DETECTION,this.tickImpl):this.tickImpl()}tickImpl=()=>{if(this._runningTick)throw oe(ee.ChangeDetectionEnd),new b(101,!1);let e=M(null);try{this._runningTick=!0,this.synchronize()}finally{this._runningTick=!1,this.tracingSnapshot?.dispose(),this.tracingSnapshot=null,M(e),this.afterTick.next(),oe(ee.ChangeDetectionEnd)}};synchronize(){this._rendererFactory===null&&!this._injector.destroyed&&(this._rendererFactory=this._injector.get(He,null,{optional:!0}));let e=0;for(;this.dirtyFlags!==0&&e++gs(e))){this.dirtyFlags|=2;return}else this.dirtyFlags&=-8}attachView(e){let r=e;this._views.push(r),r.attachToAppRef(this)}detachView(e){let r=e;Ss(this._views,r),r.detachFromAppRef()}_loadComponent(e){this.attachView(e.hostView);try{this.tick()}catch(o){this.internalErrorHandler(o)}this.components.push(e),this._injector.get($l,[]).forEach(o=>o(e))}ngOnDestroy(){if(!this._destroyed)try{this._destroyListeners.forEach(e=>e()),this._views.slice().forEach(e=>e.destroy())}finally{this._destroyed=!0,this._views=[],this._destroyListeners=[]}}onDestroy(e){return this._destroyListeners.push(e),()=>Ss(this._destroyListeners,e)}destroy(){if(this._destroyed)throw new b(406,!1);let e=this._injector;e.destroy&&!e.destroyed&&e.destroy()}get viewCount(){return this._views.length}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function Ss(t,n){let e=t.indexOf(n);e>-1&&t.splice(e,1)}function tE(t,n){let e=I(),r=Vn();if(at(e,r,n)){let o=le(),i=to();if(Ol(i,o,e,t,n))mn(i)&&lD(e,i.index);else{let a=ht(i,e);uD(e[se],a,null,i.value,t,n,null)}}return tE}function yt(t,n,e,r){let o=I(),i=Vn();if(at(o,i,n)){let s=le(),a=to();Tx(a,o,t,n,e,r)}return yt}function eR(){return I()[Ke][ge]}var rp=class{destroy(n){}updateValue(n,e){}swap(n,e){let r=Math.min(n,e),o=Math.max(n,e),i=this.detach(o);if(o-r>1){let s=this.detach(r);this.attach(r,i),this.attach(o,s)}else this.attach(r,i)}move(n,e){this.attach(e,this.detach(n))}};function wh(t,n,e,r,o){return t===e&&Object.is(n,r)?1:Object.is(o(t,n),o(e,r))?-1:0}function tR(t,n,e,r){let o,i,s=0,a=t.length-1,c=void 0;if(Array.isArray(n)){M(r);let l=n.length-1;for(M(null);s<=a&&s<=l;){let u=t.at(s),d=n[s],h=wh(s,u,s,d,e);if(h!==0){h<0&&t.updateValue(s,d),s++;continue}let p=t.at(a),m=n[l],_=wh(a,p,l,m,e);if(_!==0){_<0&&t.updateValue(a,m),a--,l--;continue}let D=e(s,u),C=e(a,p),z=e(s,d);if(Object.is(z,C)){let De=e(l,m);Object.is(De,D)?(t.swap(s,a),t.updateValue(a,m),l--,a--):t.move(a,s),t.updateValue(s,d),s++;continue}if(o??=new vl,i??=Kb(t,s,a,e),op(t,o,s,z))t.updateValue(s,d),s++,a++;else if(i.has(z))o.set(D,t.detach(s)),a--;else{let De=t.create(s,n[s]);t.attach(s,De),s++,a++}}for(;s<=l;)Zb(t,o,e,s,n[s]),s++}else if(n!=null){M(r);let l=n[Symbol.iterator]();M(null);let u=l.next();for(;!u.done&&s<=a;){let d=t.at(s),h=u.value,p=wh(s,d,s,h,e);if(p!==0)p<0&&t.updateValue(s,h),s++,u=l.next();else{o??=new vl,i??=Kb(t,s,a,e);let m=e(s,h);if(op(t,o,s,m))t.updateValue(s,h),s++,a++,u=l.next();else if(!i.has(m))t.attach(s,t.create(s,h)),s++,a++,u=l.next();else{let _=e(s,d);o.set(_,t.detach(s)),a--}}}for(;!u.done;)Zb(t,o,e,t.length,u.value),u=l.next()}for(;s<=a;)t.destroy(t.detach(a--));o?.forEach(l=>{t.destroy(l)})}function op(t,n,e,r){return n!==void 0&&n.has(r)?(t.attach(e,n.get(r)),n.delete(r),!0):!1}function Zb(t,n,e,r,o){if(op(t,n,r,e(r,o)))t.updateValue(r,o);else{let i=t.create(r,o);t.attach(r,i)}}function Kb(t,n,e,r){let o=new Set;for(let i=n;i<=e;i++)o.add(r(i,t.at(i)));return o}var vl=class{kvMap=new Map;_vMap=void 0;has(n){return this.kvMap.has(n)}delete(n){if(!this.has(n))return!1;let e=this.kvMap.get(n);return this._vMap!==void 0&&this._vMap.has(e)?(this.kvMap.set(n,this._vMap.get(e)),this._vMap.delete(e)):this.kvMap.delete(n),!0}get(n){return this.kvMap.get(n)}set(n,e){if(this.kvMap.has(n)){let r=this.kvMap.get(n);this._vMap===void 0&&(this._vMap=new Map);let o=this._vMap;for(;o.has(r);)r=o.get(r);o.set(r,e)}else this.kvMap.set(n,e)}forEach(n){for(let[e,r]of this.kvMap)if(n(r,e),this._vMap!==void 0){let o=this._vMap;for(;o.has(r);)r=o.get(r),n(r,e)}}};function nR(t,n,e,r,o,i,s,a){Kt("NgControlFlow");let c=I(),l=le(),u=pt(l.consts,i);return Jo(c,l,t,n,e,r,o,u,256,s,a),em}function em(t,n,e,r,o,i,s,a){Kt("NgControlFlow");let c=I(),l=le(),u=pt(l.consts,i);return Jo(c,l,t,n,e,r,o,u,512,s,a),em}function rR(t,n){Kt("NgControlFlow");let e=I(),r=Vn(),o=e[r]!==ye?e[r]:-1,i=o!==-1?yl(e,de+o):void 0,s=0;if(at(e,r,t)){let a=M(null);try{if(i!==void 0&&ED(i,s),t!==-1){let c=de+t,l=yl(e,c),u=cp(e[A],c),d=CD(l,u,e),h=Vs(e,u,n,{dehydratedView:d});Bs(l,h,s,Qo(u,d))}}finally{M(a)}}else if(i!==void 0){let a=DD(i,s);a!==void 0&&(a[ge]=n)}}var ip=class{lContainer;$implicit;$index;constructor(n,e,r){this.lContainer=n,this.$implicit=e,this.$index=r}get $count(){return this.lContainer.length-ve}};function oR(t){return t}function iR(t,n){return n}var sp=class{hasEmptyBlock;trackByFn;liveCollection;constructor(n,e,r){this.hasEmptyBlock=n,this.trackByFn=e,this.liveCollection=r}};function sR(t,n,e,r,o,i,s,a,c,l,u,d,h){Kt("NgControlFlow");let p=I(),m=le(),_=c!==void 0,D=I(),C=a?s.bind(D[Ke][ge]):s,z=new sp(_,C);D[de+t]=z,Jo(p,m,t+1,n,e,r,o,pt(m.consts,i),256),_&&Jo(p,m,t+2,c,l,u,d,pt(m.consts,h),512)}var ap=class extends rp{lContainer;hostLView;templateTNode;operationsCounter=void 0;needsIndexUpdate=!1;constructor(n,e,r){super(),this.lContainer=n,this.hostLView=e,this.templateTNode=r}get length(){return this.lContainer.length-ve}at(n){return this.getLView(n)[ge].$implicit}attach(n,e){let r=e[qr];this.needsIndexUpdate||=n!==this.length,Bs(this.lContainer,e,n,Qo(this.templateTNode,r)),cR(this.lContainer,n)}detach(n){return this.needsIndexUpdate||=n!==this.length-1,lR(this.lContainer,n),uR(this.lContainer,n)}create(n,e){let r=ul(this.lContainer,this.templateTNode.tView.ssrId);return Vs(this.hostLView,this.templateTNode,new ip(this.lContainer,e,n),{dehydratedView:r})}destroy(n){Al(n[A],n)}updateValue(n,e){this.getLView(n)[ge].$implicit=e}reset(){this.needsIndexUpdate=!1}updateIndexes(){if(this.needsIndexUpdate)for(let n=0;n0){let i=r[Pn];ax(i,o),oo.delete(r[Ln]),o.detachedLeaveAnimationFns=void 0}}function lR(t,n){if(t.length<=ve)return;let e=ve+n,r=t[e],o=r?r[mr]:void 0;o&&o.leave&&o.leave.size>0&&(o.detachedLeaveAnimationFns=[])}function uR(t,n){return As(t,n)}function dR(t,n){return DD(t,n)}function cp(t,n){return Vc(t,n)}function nE(t,n,e){let r=I(),o=Vn();if(at(r,o,n)){let i=le(),s=to();aD(s,r,t,n,r[se],e)}return nE}function lp(t,n,e,r,o){Ol(n,t,e,o?"class":"style",r)}function bl(t,n,e,r){let o=I(),i=o[A],s=t+de,a=i.firstCreatePass?Up(s,o,2,n,Fp,Hc(),e,r):i.data[s];if(mn(a)){let c=o[Ht].tracingService;if(c&&c.componentCreate){let l=i.data[a.directiveStart+a.componentOffset];return c.componentCreate(kD(l),()=>(Qb(t,n,o,a,r),bl))}}return Qb(t,n,o,a,r),bl}function Qb(t,n,e,r,o){if(Pp(r,e,t,n,rE),Ho(r)){let i=e[A];Nl(i,e,r),vp(i,r,e)}o!=null&&js(e,r)}function tm(){let t=le(),n=Me(),e=Lp(n);return t.firstCreatePass&&Hp(t,e),oh(e)&&ih(),nh(),e.classesWithoutHost!=null&<(e)&&lp(t,e,I(),e.classesWithoutHost,!0),e.stylesWithoutHost!=null&&jT(e)&&lp(t,e,I(),e.stylesWithoutHost,!1),tm}function zl(t,n,e,r){return bl(t,n,e,r),tm(),zl}function ai(t,n,e,r){let o=I(),i=o[A],s=t+de,a=i.firstCreatePass?dA(s,i,2,n,e,r):i.data[s];return Pp(a,o,t,n,rE),r!=null&&js(o,a),ai}function ci(){let t=Me(),n=Lp(t);return oh(n)&&ih(),nh(),ci}function Gn(t,n,e,r){return ai(t,n,e,r),ci(),Gn}var rE=(t,n,e,r,o)=>(bs(!0),V_(n[se],r,fh()));function nm(t,n,e){let r=I(),o=r[A],i=t+de,s=o.firstCreatePass?Up(i,r,8,"ng-container",Fp,Hc(),n,e):o.data[i];if(Pp(s,r,t,"ng-container",fR),Ho(s)){let a=r[A];Nl(a,r,s),vp(a,s,r)}return e!=null&&js(r,s),nm}function rm(){let t=le(),n=Me(),e=Lp(n);return t.firstCreatePass&&Hp(t,e),rm}function oE(t,n,e){return nm(t,n,e),rm(),oE}var fR=(t,n,e,r,o)=>(bs(!0),N0(n[se],""));function hR(){return I()}function iE(t,n,e){let r=I(),o=Vn();if(at(r,o,n)){let i=le(),s=to();cD(s,r,t,n,r[se],e)}return iE}var ws=void 0;function pR(t){let n=Math.floor(Math.abs(t)),e=t.toString().replace(/^[^.]*\.?/,"").length;return n===1&&e===0?1:5}var mR=["en",[["a","p"],["AM","PM"]],[["AM","PM"]],[["S","M","T","W","T","F","S"],["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],["Su","Mo","Tu","We","Th","Fr","Sa"]],ws,[["J","F","M","A","M","J","J","A","S","O","N","D"],["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],["January","February","March","April","May","June","July","August","September","October","November","December"]],ws,[["B","A"],["BC","AD"],["Before Christ","Anno Domini"]],0,[6,0],["M/d/yy","MMM d, y","MMMM d, y","EEEE, MMMM d, y"],["h:mm\u202Fa","h:mm:ss\u202Fa","h:mm:ss\u202Fa z","h:mm:ss\u202Fa zzzz"],["{1}, {0}",ws,ws,ws],[".",",",";","%","+","-","E","\xD7","\u2030","\u221E","NaN",":"],["#,##0.###","#,##0%","\xA4#,##0.00","#E0"],"USD","$","US Dollar",{},"ltr",pR],Ch=Object.create(null);function ct(t){let n=gR(t),e=Xb(n);if(e)return e;let r=n.split("-")[0];if(e=Xb(r),e)return e;if(r==="en")return mR;throw new b(701,!1)}function Xb(t){if(!(t in Ch)){let n=we.ng&&we.ng.common&&we.ng.common.locales&&we.ng.common.locales[t];return n!==void 0&&(Ch[t]=n),n}return Ch[t]}var be=(function(t){return t[t.LocaleId=0]="LocaleId",t[t.DayPeriodsFormat=1]="DayPeriodsFormat",t[t.DayPeriodsStandalone=2]="DayPeriodsStandalone",t[t.DaysFormat=3]="DaysFormat",t[t.DaysStandalone=4]="DaysStandalone",t[t.MonthsFormat=5]="MonthsFormat",t[t.MonthsStandalone=6]="MonthsStandalone",t[t.Eras=7]="Eras",t[t.FirstDayOfWeek=8]="FirstDayOfWeek",t[t.WeekendRange=9]="WeekendRange",t[t.DateFormat=10]="DateFormat",t[t.TimeFormat=11]="TimeFormat",t[t.DateTimeFormat=12]="DateTimeFormat",t[t.NumberSymbols=13]="NumberSymbols",t[t.NumberFormats=14]="NumberFormats",t[t.CurrencyCode=15]="CurrencyCode",t[t.CurrencySymbol=16]="CurrencySymbol",t[t.CurrencyName=17]="CurrencyName",t[t.Currencies=18]="Currencies",t[t.Directionality=19]="Directionality",t[t.PluralCase=20]="PluralCase",t[t.ExtraData=21]="ExtraData",t})(be||{});function gR(t){return t.toLowerCase().replace(/_/g,"-")}var $s="en-US";var vR=$s;function sE(t){typeof t=="string"&&(vR=t.toLowerCase().replace(/_/g,"-"))}function Qt(t,n,e){let r=I(),o=le(),i=Me();return cE(o,r,r[se],i,t,n,e),Qt}function aE(t,n,e){let r=I(),o=le(),i=Me();return(i.type&3||e)&&ND(i,o,r,e,r[se],t,n,rl(i,r,n)),aE}function cE(t,n,e,r,o,i,s){let a=!0,c=null;if((r.type&3||s)&&(c??=rl(r,n,i),ND(r,t,n,s,e,o,i,c)&&(a=!1)),a){let l=r.outputs?.[o],u=r.hostDirectiveOutputs?.[o];if(u&&u.length)for(let d=0;d>17&32767}function wR(t){return(t&2)==2}function CR(t,n){return t&131071|n<<17}function up(t){return t|2}function ei(t){return(t&131068)>>2}function Ih(t,n){return t&-131069|n<<2}function IR(t){return(t&1)===1}function dp(t){return t|1}function SR(t,n,e,r,o,i){let s=i?n.classBindings:n.styleBindings,a=io(s),c=ei(s);t[r]=e;let l=!1,u;if(Array.isArray(e)){let d=e;u=d[1],(u===null||jo(d,u)>0)&&(l=!0)}else u=e;if(o)if(c!==0){let h=io(t[a+1]);t[r+1]=Xc(h,a),h!==0&&(t[h+1]=Ih(t[h+1],r)),t[a+1]=CR(t[a+1],r)}else t[r+1]=Xc(a,0),a!==0&&(t[a+1]=Ih(t[a+1],r)),a=r;else t[r+1]=Xc(c,0),a===0?a=r:t[c+1]=Ih(t[c+1],r),c=r;l&&(t[r+1]=up(t[r+1])),Jb(t,u,r,!0),Jb(t,u,r,!1),MR(n,u,t,r,i),s=Xc(a,c),i?n.classBindings=s:n.styleBindings=s}function MR(t,n,e,r,o){let i=o?t.residualClasses:t.residualStyles;i!=null&&typeof n=="string"&&jo(i,n)>=0&&(e[r+1]=dp(e[r+1]))}function Jb(t,n,e,r){let o=t[e+1],i=n===null,s=r?io(o):ei(o),a=!1;for(;s!==0&&(a===!1||i);){let c=t[s],l=t[s+1];TR(c,n)&&(a=!0,t[s+1]=r?dp(l):up(l)),s=r?io(l):ei(l)}a&&(t[e+1]=r?up(o):dp(o))}function TR(t,n){return t===null||n==null||(Array.isArray(t)?t[1]:t)===n?!0:Array.isArray(t)&&typeof n=="string"?jo(t,n)>=0:!1}var Wt={textEnd:0,key:0,keyEnd:0,value:0,valueEnd:0};function xR(t){return t.substring(Wt.key,Wt.keyEnd)}function AR(t){return RR(t),dE(t,fE(t,0,Wt.textEnd))}function dE(t,n){let e=Wt.textEnd;return e===n?-1:(n=Wt.keyEnd=NR(t,Wt.key=n,e),fE(t,n,e))}function RR(t){Wt.key=0,Wt.keyEnd=0,Wt.value=0,Wt.valueEnd=0,Wt.textEnd=t.length}function fE(t,n,e){for(;n32;)n++;return n}function im(t,n,e){return hE(t,n,e,!1),im}function Xe(t,n){return hE(t,n,null,!0),Xe}function zs(t){kR(BR,OR,t,!0)}function OR(t,n){for(let e=AR(n);e>=0;e=dE(n,e))Pc(t,xR(n),!0)}function hE(t,n,e,r){let o=I(),i=le(),s=eo(2);if(i.firstUpdatePass&&mE(i,t,s,r),n!==ye&&at(o,s,n)){let a=i.data[st()];gE(i,a,o,o[se],t,o[s+1]=HR(n,e),r,s)}}function kR(t,n,e,r){let o=le(),i=eo(2);o.firstUpdatePass&&mE(o,null,i,r);let s=I();if(e!==ye&&at(s,i,e)){let a=o.data[st()];if(vE(a,r)&&!pE(o,i)){let c=r?a.classesWithoutHost:a.stylesWithoutHost;c!==null&&(e=Ac(c,e||"")),lp(o,a,s,e,r)}else UR(o,a,s,s[se],s[i+1],s[i+1]=VR(t,n,e),r,i)}}function pE(t,n){return n>=t.expandoStartIndex}function mE(t,n,e,r){let o=t.data;if(o[e+1]===null){let i=o[st()],s=pE(t,e);vE(i,r)&&n===null&&!s&&(n=!1),n=FR(o,i,n,r),SR(o,i,n,e,s,r)}}function FR(t,n,e,r){let o=ib(t),i=r?n.residualClasses:n.residualStyles;if(o===null)(r?n.classBindings:n.styleBindings)===0&&(e=Sh(null,t,n,e,r),e=Ns(e,n.attrs,r),i=null);else{let s=n.directiveStylingLast;if(s===-1||t[s]!==o)if(e=Sh(o,t,n,e,r),i===null){let c=PR(t,n,r);c!==void 0&&Array.isArray(c)&&(c=Sh(null,t,n,c[1],r),c=Ns(c,n.attrs,r),LR(t,n,r,c))}else i=jR(t,n,r)}return i!==void 0&&(r?n.residualClasses=i:n.residualStyles=i),e}function PR(t,n,e){let r=e?n.classBindings:n.styleBindings;if(ei(r)!==0)return t[io(r)]}function LR(t,n,e,r){let o=e?n.classBindings:n.styleBindings;t[io(o)]=r}function jR(t,n,e){let r,o=n.directiveEnd;for(let i=1+n.directiveStylingLast;i0;){let c=t[o],l=Array.isArray(c),u=l?c[1]:c,d=u===null,h=e[o+1];h===ye&&(h=d?Ye:void 0);let p=d?Lc(h,r):u===r?h:void 0;if(l&&!_l(p)&&(p=Lc(c,r)),_l(p)&&(a=p,s))return a;let m=t[o+1];o=s?io(m):ei(m)}if(n!==null){let c=i?n.residualClasses:n.residualStyles;c!=null&&(a=Lc(c,r))}return a}function _l(t){return t!==void 0}function HR(t,n){return t==null||t===""||(typeof n=="string"?t=t+n:typeof t=="object"&&(t=cs(vt(t)))),t}function vE(t,n){return(t.flags&(n?8:16))!==0}function $R(t,n=""){let e=I(),r=le(),o=t+de,i=r.firstCreatePass?ni(r,o,1,n,null):r.data[o],s=zR(r,e,i,n);e[o]=s,qc()&&Op(r,e,s,i),zo(i,!1)}var zR=(t,n,e,r)=>(bs(!0),A0(n[se],r));function yE(t,n,e,r=""){return at(t,Vn(),e)?n+Se(e)+r:ye}function GR(t,n,e,r,o,i=""){let s=vs(),a=Xo(t,s,e,o);return eo(2),a?n+Se(e)+r+Se(o)+i:ye}function WR(t,n,e,r,o,i,s,a=""){let c=vs(),l=RD(t,c,e,o,s);return eo(3),l?n+Se(e)+r+Se(o)+i+Se(s)+a:ye}function qR(t,n,e,r,o,i,s,a,c,l=""){let u=vs(),d=zp(t,u,e,o,s,c);return eo(4),d?n+Se(e)+r+Se(o)+i+Se(s)+a+Se(c)+l:ye}function YR(t,n,e,r,o,i,s,a,c,l,u,d,h,p=""){let m=vs(),_=zp(t,m,e,o,s,c);return _=Xo(t,m+4,u,h)||_,eo(6),_?n+Se(e)+r+Se(o)+i+Se(s)+a+Se(c)+l+Se(u)+d+Se(h)+p:ye}function bE(t){return sm("",t),bE}function sm(t,n,e){let r=I(),o=yE(r,t,n,e);return o!==ye&&Gs(r,st(),o),sm}function _E(t,n,e,r,o){let i=I(),s=GR(i,t,n,e,r,o);return s!==ye&&Gs(i,st(),s),_E}function DE(t,n,e,r,o,i,s){let a=I(),c=WR(a,t,n,e,r,o,i,s);return c!==ye&&Gs(a,st(),c),DE}function EE(t,n,e,r,o,i,s,a,c){let l=I(),u=qR(l,t,n,e,r,o,i,s,a,c);return u!==ye&&Gs(l,st(),u),EE}function wE(t,n,e,r,o,i,s,a,c,l,u,d,h){let p=I(),m=YR(p,t,n,e,r,o,i,s,a,c,l,u,d,h);return m!==ye&&Gs(p,st(),m),wE}function Gs(t,n,e){let r=Zf(n,t);R0(t[se],r,e)}function CE(t,n,e){Kp(n)&&(n=n());let r=I(),o=Vn();if(at(r,o,n)){let i=le(),s=to();aD(s,r,t,n,r[se],e)}return CE}function ZR(t,n){let e=Kp(t);return e&&t.set(n),e}function IE(t,n){let e=I(),r=le(),o=Me();return cE(r,e,e[se],o,t,n),IE}function KR(t,n,e=""){return yE(I(),t,n,e)}function QR(t,n,e){let r=gn()+t,o=I();return o[r]===ye?ri(o,r,n(e,o)):AD(o,r)}function t_(t,n,e){let r=le();r.firstCreatePass&&SE(n,r.data,r.blueprint,zt(t),e)}function SE(t,n,e,r,o){if(t=Oe(t),Array.isArray(t))for(let i=0;i>20;if($r(t)||!t.multi){let p=new ro(l,o,E,null),m=Th(c,n,o?u:u+h,d);m===-1?(Ah(cl(a,s),i,c),Mh(i,t,n.length),n.push(c),a.directiveStart++,a.directiveEnd++,o&&(a.providerIndexes+=1048576),e.push(p),s.push(p)):(e[m]=p,s[m]=p)}else{let p=Th(c,n,u+h,d),m=Th(c,n,u,u+h),_=p>=0&&e[p],D=m>=0&&e[m];if(o&&!D||!o&&!_){Ah(cl(a,s),i,c);let C=eN(o?JR:XR,e.length,o,r,l,t);!o&&D&&(e[m].providerFactory=C),Mh(i,t,n.length,0),n.push(c),a.directiveStart++,a.directiveEnd++,o&&(a.providerIndexes+=1048576),e.push(C),s.push(C)}else{let C=ME(e[o?m:p],l,!o&&r);Mh(i,t,p>-1?p:m,C)}!o&&r&&D&&e[m].componentProviders++}}}function Mh(t,n,e,r){let o=$r(n),i=By(n);if(o||i){let c=(i?Oe(n.useClass):n).prototype.ngOnDestroy;if(c){let l=t.destroyHooks||(t.destroyHooks=[]);if(!o&&n.multi){let u=l.indexOf(e);u===-1?l.push(e,[r,c]):l[u+1].push(r,c)}else l.push(e,c)}}}function ME(t,n,e){return e&&t.componentProviders++,t.multi.push(n)-1}function Th(t,n,e,r){for(let o=e;o{e.providersResolver=(r,o)=>t_(r,o?o(t):t,!1),n&&(e.viewProvidersResolver=(r,o)=>t_(r,o?o(n):n,!0))}}function tN(t,n){let e=gn()+t,r=I();return r[e]===ye?ri(r,e,n()):AD(r,e)}function nN(t,n,e){return TE(I(),gn(),t,n,e)}function rN(t,n,e,r){return xE(I(),gn(),t,n,e,r)}function oN(t,n,e,r,o){return AE(I(),gn(),t,n,e,r,o)}function iN(t,n,e,r,o,i,s){return sN(I(),gn(),t,n,e,r,o,i)}function Yl(t,n){let e=t[n];return e===ye?void 0:e}function TE(t,n,e,r,o,i){let s=n+e;return at(t,s,o)?ri(t,s+1,i?r.call(i,o):r(o)):Yl(t,s+1)}function xE(t,n,e,r,o,i,s){let a=n+e;return Xo(t,a,o,i)?ri(t,a+2,s?r.call(s,o,i):r(o,i)):Yl(t,a+2)}function AE(t,n,e,r,o,i,s,a){let c=n+e;return RD(t,c,o,i,s)?ri(t,c+3,a?r.call(a,o,i,s):r(o,i,s)):Yl(t,c+3)}function sN(t,n,e,r,o,i,s,a,c){let l=n+e;return zp(t,l,o,i,s,a)?ri(t,l+4,c?r.call(c,o,i,s,a):r(o,i,s,a)):Yl(t,l+4)}function aN(t,n){let e=le(),r,o=t+de;e.firstCreatePass?(r=cN(n,e.pipeRegistry),e.data[o]=r,r.onDestroy&&(e.destroyHooks??=[]).push(o,r.onDestroy)):r=e.data[o];let i=r.factory||(r.factory=ur(r.type,!0)),s,a=tt(E);try{let c=al(!1),l=i();return al(c),Kf(e,I(),o,l),l}finally{tt(a)}}function cN(t,n){if(n)for(let e=n.length-1;e>=0;e--){let r=n[e];if(t===r.name)return r}}function lN(t,n,e){let r=t+de,o=I(),i=ms(o,r);return am(o,r)?TE(o,gn(),n,i.transform,e,i):i.transform(e)}function uN(t,n,e,r){let o=t+de,i=I(),s=ms(i,o);return am(i,o)?xE(i,gn(),n,s.transform,e,r,s):s.transform(e,r)}function dN(t,n,e,r,o){let i=t+de,s=I(),a=ms(s,i);return am(s,i)?AE(s,gn(),n,a.transform,e,r,o,a):a.transform(e,r,o)}function am(t,n){return t[A].data[n].pure}function fN(t,n){return kl(t,n)}var Dl=class{ngModuleFactory;componentFactories;constructor(n,e){this.ngModuleFactory=n,this.componentFactories=e}},cm=(()=>{class t{compileModuleSync(e){return new gl(e)}compileModuleAsync(e){return Promise.resolve(this.compileModuleSync(e))}compileModuleAndAllComponentsSync(e){let r=this.compileModuleSync(e),o=kf(e),i=W_(o.declarations).reduce((s,a)=>{let c=hn(a);return c&&s.push(new br(c)),s},[]);return new Dl(r,i)}compileModuleAndAllComponentsAsync(e){return Promise.resolve(this.compileModuleAndAllComponentsSync(e))}clearCache(){}clearCacheFor(e){}getModuleId(e){}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var RE=(()=>{class t{applicationErrorHandler=f(mt);appRef=f(Ge);taskService=f(Bn);ngZone=f(j);zonelessEnabled=f(Ds);tracing=f(Zt,{optional:!0});zoneIsDefined=typeof Zone<"u"&&!!Zone.root.run;schedulerTickApplyArgs=[{data:{__scheduler_tick__:!0}}];subscriptions=new G;angularZoneId=this.zoneIsDefined?this.ngZone._inner?.get(ss):null;scheduleInRootZone=!this.zonelessEnabled&&this.zoneIsDefined&&(f(vh,{optional:!0})??!1);cancelScheduledCallback=null;useMicrotaskScheduler=!1;runningTick=!1;pendingRenderTaskId=null;constructor(){this.subscriptions.add(this.appRef.afterTick.subscribe(()=>{let e=this.taskService.add();if(!this.runningTick&&(this.cleanup(),!this.zonelessEnabled||this.appRef.includeAllTestViews)){this.taskService.remove(e);return}this.switchToMicrotaskScheduler(),this.taskService.remove(e)})),this.subscriptions.add(this.ngZone.onUnstable.subscribe(()=>{this.runningTick||this.cleanup()}))}switchToMicrotaskScheduler(){this.ngZone.runOutsideAngular(()=>{let e=this.taskService.add();this.useMicrotaskScheduler=!0,queueMicrotask(()=>{this.useMicrotaskScheduler=!1,this.taskService.remove(e)})})}notify(e){if(!this.zonelessEnabled&&e===5)return;switch(e){case 0:{this.appRef.dirtyFlags|=2;break}case 3:case 2:case 4:case 5:case 1:{this.appRef.dirtyFlags|=4;break}case 6:{this.appRef.dirtyFlags|=2;break}case 12:{this.appRef.dirtyFlags|=16;break}case 13:{this.appRef.dirtyFlags|=2;break}case 11:break;default:this.appRef.dirtyFlags|=8}if(this.appRef.tracingSnapshot=this.tracing?.snapshot(this.appRef.tracingSnapshot)??null,!this.shouldScheduleTick())return;let r=this.useMicrotaskScheduler?mb:ph;this.pendingRenderTaskId=this.taskService.add(),this.scheduleInRootZone?this.cancelScheduledCallback=Zone.root.run(()=>r(()=>this.tick())):this.cancelScheduledCallback=this.ngZone.runOutsideAngular(()=>r(()=>this.tick()))}shouldScheduleTick(){return!(this.appRef.destroyed||this.pendingRenderTaskId!==null||this.runningTick||this.appRef._runningTick||!this.zonelessEnabled&&this.zoneIsDefined&&Zone.current.get(ss+this.angularZoneId))}tick(){if(this.runningTick||this.appRef.destroyed)return;if(this.appRef.dirtyFlags===0){this.cleanup();return}!this.zonelessEnabled&&this.appRef.dirtyFlags&7&&(this.appRef.dirtyFlags|=1);let e=this.taskService.add();try{this.ngZone.run(()=>{this.runningTick=!0,this.appRef._tick()},void 0,this.schedulerTickApplyArgs)}catch(r){this.applicationErrorHandler(r)}finally{this.taskService.remove(e),this.cleanup()}}ngOnDestroy(){this.subscriptions.unsubscribe(),this.cleanup()}cleanup(){if(this.runningTick=!1,this.cancelScheduledCallback?.(),this.cancelScheduledCallback=null,this.pendingRenderTaskId!==null){let e=this.pendingRenderTaskId;this.pendingRenderTaskId=null,this.taskService.remove(e)}}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function hN(){return Kt("NgZoneless"),hr([...lm(),[]])}function lm(){return[{provide:fn,useExisting:RE},{provide:j,useClass:as},{provide:Ds,useValue:!0}]}function pN(){return typeof $localize<"u"&&$localize.locale||$s}var li=new y("",{factory:()=>f(li,{optional:!0,skipSelf:!0})||pN()});var Ws=class{destroyed=!1;listeners=null;errorHandler=f(Ze,{optional:!0});destroyRef=f(Ue);constructor(){this.destroyRef.onDestroy(()=>{this.destroyed=!0,this.listeners=null})}subscribe(n){if(this.destroyed)throw new b(953,!1);return(this.listeners??=[]).push(n),{unsubscribe:()=>{let e=this.listeners?.indexOf(n);e!==void 0&&e!==-1&&this.listeners?.splice(e,1)}}}emit(n){if(this.destroyed){console.warn(St(953,!1));return}if(this.listeners===null)return;let e=M(null);try{for(let r of this.listeners)try{r(n)}catch(o){this.errorHandler?.handleError(o)}}finally{M(e)}}};function Y(t){return wy(t)}function Xt(t,n){return Ji(t,n?.equal)}var mN=t=>t;function um(t,n){if(typeof t=="function"){let e=gf(t,mN,n?.equal);return NE(e,n?.debugName)}else{let e=gf(t.source,t.computation,t.equal);return NE(e,t.debugName)}}function NE(t,n){let e=t[me],r=t;return r.set=o=>Dy(e,o),r.update=o=>Ey(e,o),r.asReadonly=_s.bind(t),r}var Ql=Symbol("InputSignalNode#UNSET"),HE=F(g({},es),{transformFn:void 0,applyValueToInputSignal(t,n){cr(t,n)}});function $E(t,n){let e=Object.create(HE);e.value=t,e.transformFn=n?.transform;function r(){if(On(e),e.value===Ql){let o=null;throw new b(-950,o)}return e.value}return r[me]=e,r}var ui=class{attributeName;constructor(n){this.attributeName=n}__NG_ELEMENT_ID__=()=>ks(this.attributeName);toString(){return`HostAttributeToken ${this.attributeName}`}},M5=(()=>{let t=new y("");return t.__NG_ELEMENT_ID__=n=>{let e=Me();if(e===null)throw new b(-204,!1);if(e.type&2)return e.value;if(n&8)return null;throw new b(-204,!1)},t})();function T5(t){return new Ws}function OE(t,n){return $E(t,n)}function SN(t){return $E(Ql,t)}var zE=(OE.required=SN,OE);function kE(t,n){return Yp(n)}function MN(t,n){return Zp(n)}var x5=(kE.required=MN,kE);function A5(t,n){return $D(n)}function FE(t,n){return Yp(n)}function TN(t,n){return Zp(n)}var R5=(FE.required=TN,FE);function GE(t,n){let e=Object.create(HE),r=new Ws;e.value=t;function o(){return On(e),PE(e.value),e.value}return o[me]=e,o.asReadonly=_s.bind(o),o.set=i=>{e.equal(e.value,i)||(cr(e,i),r.emit(i))},o.update=i=>{PE(e.value),o.set(i(e.value))},o.subscribe=r.subscribe.bind(r),o.destroyRef=r.destroyRef,o}function PE(t){if(t===Ql)throw new b(952,!1)}function LE(t,n){return GE(t,n)}function xN(t){return GE(Ql,t)}var N5=(LE.required=xN,LE);var fm=new y(""),AN=new y("");function qs(t){return!t.moduleRef}function RN(t){let n=qs(t)?t.r3Injector:t.moduleRef.injector,e=n.get(j);return e.run(()=>{qs(t)?t.r3Injector.resolveInjectorInitializers():t.moduleRef.resolveInjectorInitializers();let r=n.get(mt),o;if(e.runOutsideAngular(()=>{o=e.onError.subscribe({next:r})}),qs(t)){let i=()=>n.destroy(),s=t.platformInjector.get(fm);s.add(i),n.onDestroy(()=>{o.unsubscribe(),s.delete(i)})}else{let i=()=>t.moduleRef.destroy(),s=t.platformInjector.get(fm);s.add(i),t.moduleRef.onDestroy(()=>{Ss(t.allPlatformModules,t.moduleRef),o.unsubscribe(),s.delete(i)})}return ON(r,e,()=>{let i=n.get(Bn),s=i.add(),a=n.get(Jp);return a.runInitializers(),a.donePromise.then(()=>{let c=n.get(li,$s);if(sE(c||$s),!n.get(AN,!0))return qs(t)?n.get(Ge):(t.allPlatformModules.push(t.moduleRef),t.moduleRef);if(qs(t)){let u=n.get(Ge);return t.rootComponent!==void 0&&u.bootstrap(t.rootComponent),u}else return NN?.(t.moduleRef,t.allPlatformModules),t.moduleRef}).finally(()=>{i.remove(s)})})})}var NN;function ON(t,n,e){try{let r=e();return zn(r)?r.catch(o=>{throw n.runOutsideAngular(()=>t(o)),o}):r}catch(r){throw n.runOutsideAngular(()=>t(r)),r}}var Zl=null;function kN(t=[],n){return W.create({name:n,providers:[{provide:hs,useValue:"platform"},{provide:fm,useValue:new Set([()=>Zl=null])},...t]})}function FN(t=[]){if(Zl)return Zl;let n=kN(t);return Zl=n,JD(),PN(n),n}function PN(t){let n=t.get(El,null);Fe(t,()=>{n?.forEach(e=>e())})}var LN=1e4;var O5=LN-1e3;var Jt=(()=>{class t{static __NG_ELEMENT_ID__=jN}return t})();function jN(t){return VN(Me(),I(),(t&16)===16)}function VN(t,n,e){if(mn(t)&&!e){let r=xt(t.index,n);return new yr(r,r)}else if(t.type&175){let r=n[Ke];return new yr(r,n)}return null}var hm=class{supports(n){return $p(n)}create(n){return new pm(n)}},BN=(t,n)=>n,pm=class{length=0;collection;_linkedRecords=null;_unlinkedRecords=null;_previousItHead=null;_itHead=null;_itTail=null;_additionsHead=null;_additionsTail=null;_movesHead=null;_movesTail=null;_removalsHead=null;_removalsTail=null;_identityChangesHead=null;_identityChangesTail=null;_trackByFn;constructor(n){this._trackByFn=n||BN}forEachItem(n){let e;for(e=this._itHead;e!==null;e=e._next)n(e)}forEachOperation(n){let e=this._itHead,r=this._removalsHead,o=0,i=null;for(;e||r;){let s=!r||e&&e.currentIndex{s=this._trackByFn(o,a),e===null||!Object.is(e.trackById,s)?(e=this._mismatch(e,a,s,o),r=!0):(r&&(e=this._verifyReinsertion(e,a,s,o)),Object.is(e.item,a)||this._addIdentityChange(e,a)),e=e._next,o++}),this.length=o;return this._truncate(e),this.collection=n,this.isDirty}get isDirty(){return this._additionsHead!==null||this._movesHead!==null||this._removalsHead!==null||this._identityChangesHead!==null}_reset(){if(this.isDirty){let n;for(n=this._previousItHead=this._itHead;n!==null;n=n._next)n._nextPrevious=n._next;for(n=this._additionsHead;n!==null;n=n._nextAdded)n.previousIndex=n.currentIndex;for(this._additionsHead=this._additionsTail=null,n=this._movesHead;n!==null;n=n._nextMoved)n.previousIndex=n.currentIndex;this._movesHead=this._movesTail=null,this._removalsHead=this._removalsTail=null,this._identityChangesHead=this._identityChangesTail=null}}_mismatch(n,e,r,o){let i;return n===null?i=this._itTail:(i=n._prev,this._remove(n)),n=this._unlinkedRecords===null?null:this._unlinkedRecords.get(r,null),n!==null?(Object.is(n.item,e)||this._addIdentityChange(n,e),this._reinsertAfter(n,i,o)):(n=this._linkedRecords===null?null:this._linkedRecords.get(r,o),n!==null?(Object.is(n.item,e)||this._addIdentityChange(n,e),this._moveAfter(n,i,o)):n=this._addAfter(new mm(e,r),i,o)),n}_verifyReinsertion(n,e,r,o){let i=this._unlinkedRecords===null?null:this._unlinkedRecords.get(r,null);return i!==null?n=this._reinsertAfter(i,n._prev,o):n.currentIndex!=o&&(n.currentIndex=o,this._addToMoves(n,o)),n}_truncate(n){for(;n!==null;){let e=n._next;this._addToRemovals(this._unlink(n)),n=e}this._unlinkedRecords!==null&&this._unlinkedRecords.clear(),this._additionsTail!==null&&(this._additionsTail._nextAdded=null),this._movesTail!==null&&(this._movesTail._nextMoved=null),this._itTail!==null&&(this._itTail._next=null),this._removalsTail!==null&&(this._removalsTail._nextRemoved=null),this._identityChangesTail!==null&&(this._identityChangesTail._nextIdentityChange=null)}_reinsertAfter(n,e,r){this._unlinkedRecords!==null&&this._unlinkedRecords.remove(n);let o=n._prevRemoved,i=n._nextRemoved;return o===null?this._removalsHead=i:o._nextRemoved=i,i===null?this._removalsTail=o:i._prevRemoved=o,this._insertAfter(n,e,r),this._addToMoves(n,r),n}_moveAfter(n,e,r){return this._unlink(n),this._insertAfter(n,e,r),this._addToMoves(n,r),n}_addAfter(n,e,r){return this._insertAfter(n,e,r),this._additionsTail===null?this._additionsTail=this._additionsHead=n:this._additionsTail=this._additionsTail._nextAdded=n,n}_insertAfter(n,e,r){let o=e===null?this._itHead:e._next;return n._next=o,n._prev=e,o===null?this._itTail=n:o._prev=n,e===null?this._itHead=n:e._next=n,this._linkedRecords===null&&(this._linkedRecords=new Kl),this._linkedRecords.put(n),n.currentIndex=r,n}_remove(n){return this._addToRemovals(this._unlink(n))}_unlink(n){this._linkedRecords!==null&&this._linkedRecords.remove(n);let e=n._prev,r=n._next;return e===null?this._itHead=r:e._next=r,r===null?this._itTail=e:r._prev=e,n}_addToMoves(n,e){return n.previousIndex===e||(this._movesTail===null?this._movesTail=this._movesHead=n:this._movesTail=this._movesTail._nextMoved=n),n}_addToRemovals(n){return this._unlinkedRecords===null&&(this._unlinkedRecords=new Kl),this._unlinkedRecords.put(n),n.currentIndex=null,n._nextRemoved=null,this._removalsTail===null?(this._removalsTail=this._removalsHead=n,n._prevRemoved=null):(n._prevRemoved=this._removalsTail,this._removalsTail=this._removalsTail._nextRemoved=n),n}_addIdentityChange(n,e){return n.item=e,this._identityChangesTail===null?this._identityChangesTail=this._identityChangesHead=n:this._identityChangesTail=this._identityChangesTail._nextIdentityChange=n,n}},mm=class{item;trackById;currentIndex=null;previousIndex=null;_nextPrevious=null;_prev=null;_next=null;_prevDup=null;_nextDup=null;_prevRemoved=null;_nextRemoved=null;_nextAdded=null;_nextMoved=null;_nextIdentityChange=null;constructor(n,e){this.item=n,this.trackById=e}},gm=class{_head=null;_tail=null;add(n){this._head===null?(this._head=this._tail=n,n._nextDup=null,n._prevDup=null):(this._tail._nextDup=n,n._prevDup=this._tail,n._nextDup=null,this._tail=n)}get(n,e){let r;for(r=this._head;r!==null;r=r._nextDup)if((e===null||e<=r.currentIndex)&&Object.is(r.trackById,n))return r;return null}remove(n){let e=n._prevDup,r=n._nextDup;return e===null?this._head=r:e._nextDup=r,r===null?this._tail=e:r._prevDup=e,this._head===null}},Kl=class{map=new Map;put(n){let e=n.trackById,r=this.map.get(e);r||(r=new gm,this.map.set(e,r)),r.add(n)}get(n,e){let r=n,o=this.map.get(r);return o?o.get(n,e):null}remove(n){let e=n.trackById;return this.map.get(e).remove(n)&&this.map.delete(e),n}get isEmpty(){return this.map.size===0}clear(){this.map.clear()}};function jE(t,n,e){let r=t.previousIndex;if(r===null)return r;let o=0;return e&&r{if(e&&e.key===o)this._maybeAddToChanges(e,r),this._appendAfter=e,e=e._next;else{let i=this._getOrCreateRecordForKey(o,r);e=this._insertBeforeOrAppend(e,i)}}),e){e._prev&&(e._prev._next=null),this._removalsHead=e;for(let r=e;r!==null;r=r._nextRemoved)r===this._mapHead&&(this._mapHead=null),this._records.delete(r.key),r._nextRemoved=r._next,r.previousValue=r.currentValue,r.currentValue=null,r._prev=null,r._next=null}return this._changesTail&&(this._changesTail._nextChanged=null),this._additionsTail&&(this._additionsTail._nextAdded=null),this.isDirty}_insertBeforeOrAppend(n,e){if(n){let r=n._prev;return e._next=n,e._prev=r,n._prev=e,r&&(r._next=e),n===this._mapHead&&(this._mapHead=e),this._appendAfter=n,n}return this._appendAfter?(this._appendAfter._next=e,e._prev=this._appendAfter):this._mapHead=e,this._appendAfter=e,null}_getOrCreateRecordForKey(n,e){if(this._records.has(n)){let o=this._records.get(n);this._maybeAddToChanges(o,e);let i=o._prev,s=o._next;return i&&(i._next=s),s&&(s._prev=i),o._next=null,o._prev=null,o}let r=new bm(n);return this._records.set(n,r),r.currentValue=e,this._addToAdditions(r),r}_reset(){if(this.isDirty){let n;for(this._previousMapHead=this._mapHead,n=this._previousMapHead;n!==null;n=n._next)n._nextPrevious=n._next;for(n=this._changesHead;n!==null;n=n._nextChanged)n.previousValue=n.currentValue;for(n=this._additionsHead;n!=null;n=n._nextAdded)n.previousValue=n.currentValue;this._changesHead=this._changesTail=null,this._additionsHead=this._additionsTail=null,this._removalsHead=null}}_maybeAddToChanges(n,e){Object.is(e,n.currentValue)||(n.previousValue=n.currentValue,n.currentValue=e,this._addToChanges(n))}_addToAdditions(n){this._additionsHead===null?this._additionsHead=this._additionsTail=n:(this._additionsTail._nextAdded=n,this._additionsTail=n)}_addToChanges(n){this._changesHead===null?this._changesHead=this._changesTail=n:(this._changesTail._nextChanged=n,this._changesTail=n)}_forEach(n,e){n instanceof Map?n.forEach(e):Object.keys(n).forEach(r=>e(n[r],r))}},bm=class{key;previousValue=null;currentValue=null;_nextPrevious=null;_next=null;_prev=null;_nextAdded=null;_nextRemoved=null;_nextChanged=null;constructor(n){this.key=n}};function VE(){return new Xl([new hm])}var Xl=(()=>{class t{factories;static \u0275prov=v({token:t,providedIn:"root",factory:VE});constructor(e){this.factories=e}static create(e,r){if(r!=null){let o=r.factories.slice();e=e.concat(o)}return new t(e)}static extend(e){return{provide:t,useFactory:()=>{let r=f(t,{optional:!0,skipSelf:!0});return t.create(e,r||VE())}}}find(e){let r=this.factories.find(o=>o.supports(e));if(r!=null)return r;throw new b(901,!1)}}return t})();function BE(){return new Em([new vm])}var Em=(()=>{class t{static \u0275prov=v({token:t,providedIn:"root",factory:BE});factories;constructor(e){this.factories=e}static create(e,r){if(r){let o=r.factories.slice();e=e.concat(o)}return new t(e)}static extend(e){return{provide:t,useFactory:()=>{let r=f(t,{optional:!0,skipSelf:!0});return t.create(e,r||BE())}}}find(e){let r=this.factories.find(o=>o.supports(e));if(r)return r;throw new b(901,!1)}}return t})();var WE=(()=>{class t{constructor(e){}static \u0275fac=function(r){return new(r||t)(w(Ge))};static \u0275mod=Q({type:t});static \u0275inj=q({})}return t})();function qE(t){let{rootComponent:n,appProviders:e,platformProviders:r,platformRef:o}=t;oe(ee.BootstrapApplicationStart);try{let i=o?.injector??FN(r),s=[lm(),vb,...e||[]],a=new Rs({providers:s,parent:i,debugName:"",runEnvironmentInitializers:!1});return RN({r3Injector:a.injector,platformInjector:i,rootComponent:n})}catch(i){return Promise.reject(i)}finally{oe(ee.BootstrapApplicationEnd)}}function ue(t){return typeof t=="boolean"?t:t!=null&&t!=="false"}function wm(t,n=NaN){return!isNaN(parseFloat(t))&&!isNaN(Number(t))?Number(t):n}var dm=Symbol("NOT_SET"),YE=new Set,UN=F(g({},es),{kind:"afterRenderEffectPhase",consumerIsAlwaysLive:!0,consumerAllowSignalWrites:!0,value:dm,cleanup:null,consumerMarkedDirty(){if(this.sequence.impl.executing){if(this.sequence.lastPhase===null||this.sequence.lastPhase(On(l),l.value),l.signal[me]=l,l.registerCleanupFn=u=>(l.cleanup??=new Set).add(u),this.nodes[a]=l,this.hooks[a]=u=>l.phaseFn(u)}}afterRun(){super.afterRun(),this.lastPhase=null}destroy(){if(this.onDestroyFns!==null)for(let n of this.onDestroyFns)n();super.destroy();for(let n of this.nodes)if(n)try{for(let e of n.cleanup??YE)e()}finally{ar(n)}}};function k5(t,n){let e=n?.injector??f(W),r=e.get(fn),o=e.get(xl),i=e.get(Zt,null,{optional:!0});o.impl??=e.get(Rp);let s=t;typeof s=="function"&&(s={mixedReadWrite:t});let a=e.get(Go,null,{optional:!0}),c=new _m(o.impl,[s.earlyRead,s.write,s.mixedReadWrite,s.read],a?.view,r,e,i?.snapshot(null));return o.impl.register(c),c}function Jl(t,n){let e=hn(t),r=n.elementInjector||Vo();return new br(e).create(r,n.projectableNodes,n.hostElement,n.environmentInjector,n.directives,n.bindings)}function ZE(t){let n=hn(t);if(!n)return null;let e=new br(n);return{get selector(){return e.selector},get type(){return e.componentType},get inputs(){return e.inputs},get outputs(){return e.outputs},get ngContentSelectors(){return e.ngContentSelectors},get isStandalone(){return n.standalone},get isSignal(){return n.signals}}}var KE=null;function bt(){return KE}function Cm(t){KE??=t}var Ys=class{},Wn=(()=>{class t{historyGo(e){throw new Error("")}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(QE),providedIn:"platform"})}return t})(),Im=new y(""),QE=(()=>{class t extends Wn{_location;_history;_doc=f(O);constructor(){super(),this._location=window.location,this._history=window.history}getBaseHrefFromDOM(){return bt().getBaseHref(this._doc)}onPopState(e){let r=bt().getGlobalEventTarget(this._doc,"window");return r.addEventListener("popstate",e,!1),()=>r.removeEventListener("popstate",e)}onHashChange(e){let r=bt().getGlobalEventTarget(this._doc,"window");return r.addEventListener("hashchange",e,!1),()=>r.removeEventListener("hashchange",e)}get href(){return this._location.href}get protocol(){return this._location.protocol}get hostname(){return this._location.hostname}get port(){return this._location.port}get pathname(){return this._location.pathname}get search(){return this._location.search}get hash(){return this._location.hash}set pathname(e){this._location.pathname=e}pushState(e,r,o){this._history.pushState(e,r,o)}replaceState(e,r,o){this._history.replaceState(e,r,o)}forward(){this._history.forward()}back(){this._history.back()}historyGo(e=0){this._history.go(e)}getState(){return this._history.state}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>new t,providedIn:"platform"})}return t})();function eu(t,n){return t?n?t.endsWith("/")?n.startsWith("/")?t+n.slice(1):t+n:n.startsWith("/")?t+n:`${t}/${n}`:t:n}function XE(t){let n=t.search(/#|\?|$/);return t[n-1]==="/"?t.slice(0,n-1)+t.slice(n):t}function en(t){return t&&t[0]!=="?"?`?${t}`:t}var tn=(()=>{class t{historyGo(e){throw new Error("")}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(nu),providedIn:"root"})}return t})(),tu=new y(""),nu=(()=>{class t extends tn{_platformLocation;_baseHref;_removeListenerFns=[];constructor(e,r){super(),this._platformLocation=e,this._baseHref=r??this._platformLocation.getBaseHrefFromDOM()??f(O).location?.origin??""}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(e){this._removeListenerFns.push(this._platformLocation.onPopState(e),this._platformLocation.onHashChange(e))}getBaseHref(){return this._baseHref}prepareExternalUrl(e){return eu(this._baseHref,e)}path(e=!1){let r=this._platformLocation.pathname+en(this._platformLocation.search),o=this._platformLocation.hash;return o&&e?`${r}${o}`:r}pushState(e,r,o,i){let s=this.prepareExternalUrl(o+en(i));this._platformLocation.pushState(e,r,s)}replaceState(e,r,o,i){let s=this.prepareExternalUrl(o+en(i));this._platformLocation.replaceState(e,r,s)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(e=0){this._platformLocation.historyGo?.(e)}static \u0275fac=function(r){return new(r||t)(w(Wn),w(tu,8))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var Dn=(()=>{class t{_subject=new x;_basePath;_locationStrategy;_urlChangeListeners=[];_urlChangeSubscription=null;constructor(e){this._locationStrategy=e;let r=this._locationStrategy.getBaseHref();this._basePath=zN(XE(JE(r))),this._locationStrategy.onPopState(o=>{this._subject.next({url:this.path(!0),pop:!0,state:o.state,type:o.type})})}ngOnDestroy(){this._urlChangeSubscription?.unsubscribe(),this._urlChangeListeners=[]}path(e=!1){return this.normalize(this._locationStrategy.path(e))}getState(){return this._locationStrategy.getState()}isCurrentPathEqualTo(e,r=""){return this.path()==this.normalize(e+en(r))}normalize(e){return t.stripTrailingSlash($N(this._basePath,JE(e)))}prepareExternalUrl(e){return e&&e[0]!=="/"&&(e="/"+e),this._locationStrategy.prepareExternalUrl(e)}go(e,r="",o=null){this._locationStrategy.pushState(o,"",e,r),this._notifyUrlChangeListeners(this.prepareExternalUrl(e+en(r)),o)}replaceState(e,r="",o=null){this._locationStrategy.replaceState(o,"",e,r),this._notifyUrlChangeListeners(this.prepareExternalUrl(e+en(r)),o)}forward(){this._locationStrategy.forward()}back(){this._locationStrategy.back()}historyGo(e=0){this._locationStrategy.historyGo?.(e)}onUrlChange(e){return this._urlChangeListeners.push(e),this._urlChangeSubscription??=this.subscribe(r=>{this._notifyUrlChangeListeners(r.url,r.state)}),()=>{let r=this._urlChangeListeners.indexOf(e);this._urlChangeListeners.splice(r,1),this._urlChangeListeners.length===0&&(this._urlChangeSubscription?.unsubscribe(),this._urlChangeSubscription=null)}}_notifyUrlChangeListeners(e="",r){this._urlChangeListeners.forEach(o=>o(e,r))}subscribe(e,r,o){return this._subject.subscribe({next:e,error:r??void 0,complete:o??void 0})}static normalizeQueryParams=en;static joinWithSlash=eu;static stripTrailingSlash=XE;static \u0275fac=function(r){return new(r||t)(w(tn))};static \u0275prov=v({token:t,factory:()=>HN(),providedIn:"root"})}return t})();function HN(){return new Dn(w(tn))}function $N(t,n){if(!t||!n.startsWith(t))return n;let e=n.substring(t.length);return e===""||["/",";","?","#"].includes(e[0])?e:n}function JE(t){return t.replace(/\/index\.html$/,"")}function zN(t){if(new RegExp("^(https?:)?//").test(t)){let[,e]=t.split(/\/\/[^\/]+/);return e}return t}var km=(()=>{class t extends tn{_platformLocation;_baseHref="";_removeListenerFns=[];constructor(e,r){super(),this._platformLocation=e,r!=null&&(this._baseHref=r)}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(e){this._removeListenerFns.push(this._platformLocation.onPopState(e),this._platformLocation.onHashChange(e))}getBaseHref(){return this._baseHref}path(e=!1){let r=this._platformLocation.hash??"#";return r.length>0?r.substring(1):r}prepareExternalUrl(e){let r=eu(this._baseHref,e);return r.length>0?"#"+r:r}pushState(e,r,o,i){let s=this.prepareExternalUrl(o+en(i))||this._platformLocation.pathname;this._platformLocation.pushState(e,r,s)}replaceState(e,r,o,i){let s=this.prepareExternalUrl(o+en(i))||this._platformLocation.pathname;this._platformLocation.replaceState(e,r,s)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(e=0){this._platformLocation.historyGo?.(e)}static \u0275fac=function(r){return new(r||t)(w(Wn),w(tu,8))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})();var Fm=(function(t){return t[t.Decimal=0]="Decimal",t[t.Percent=1]="Percent",t[t.Currency=2]="Currency",t[t.Scientific=3]="Scientific",t})(Fm||{});var Je=(function(t){return t[t.Format=0]="Format",t[t.Standalone=1]="Standalone",t})(Je||{}),ce=(function(t){return t[t.Narrow=0]="Narrow",t[t.Abbreviated=1]="Abbreviated",t[t.Wide=2]="Wide",t[t.Short=3]="Short",t})(ce||{}),lt=(function(t){return t[t.Short=0]="Short",t[t.Medium=1]="Medium",t[t.Long=2]="Long",t[t.Full=3]="Full",t})(lt||{}),ut={Decimal:0,Group:1,List:2,PercentSign:3,PlusSign:4,MinusSign:5,Exponential:6,SuperscriptingExponent:7,PerMille:8,Infinity:9,NaN:10,TimeSeparator:11,CurrencyDecimal:12,CurrencyGroup:13};function iw(t){return ct(t)[be.LocaleId]}function sw(t,n,e){let r=ct(t),o=[r[be.DayPeriodsFormat],r[be.DayPeriodsStandalone]],i=Rt(o,n);return Rt(i,e)}function aw(t,n,e){let r=ct(t),o=[r[be.DaysFormat],r[be.DaysStandalone]],i=Rt(o,n);return Rt(i,e)}function cw(t,n,e){let r=ct(t),o=[r[be.MonthsFormat],r[be.MonthsStandalone]],i=Rt(o,n);return Rt(i,e)}function lw(t,n){let r=ct(t)[be.Eras];return Rt(r,n)}function Zs(t,n){let e=ct(t);return Rt(e[be.DateFormat],n)}function Ks(t,n){let e=ct(t);return Rt(e[be.TimeFormat],n)}function Qs(t,n){let r=ct(t)[be.DateTimeFormat];return Rt(r,n)}function En(t,n){let e=ct(t),r=e[be.NumberSymbols][n];if(typeof r>"u"){if(n===ut.CurrencyDecimal)return e[be.NumberSymbols][ut.Decimal];if(n===ut.CurrencyGroup)return e[be.NumberSymbols][ut.Group]}return r}function uw(t,n){return ct(t)[be.NumberFormats][n]}function dw(t){if(!t[be.ExtraData])throw new b(2303,!1)}function fw(t){let n=ct(t);return dw(n),(n[be.ExtraData][2]||[]).map(r=>typeof r=="string"?Sm(r):[Sm(r[0]),Sm(r[1])])}function hw(t,n,e){let r=ct(t);dw(r);let o=[r[be.ExtraData][0],r[be.ExtraData][1]],i=Rt(o,n)||[];return Rt(i,e)||[]}function Rt(t,n){for(let e=n;e>-1;e--)if(typeof t[e]<"u")return t[e];throw new b(2304,!1)}function Sm(t){let[n,e]=t.split(":");return{hours:+n,minutes:+e}}var GN=/^(\d{4,})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/,ru={},WN=/((?:[^BEGHLMOSWYZabcdhmswyz']+)|(?:'(?:[^']|'')*')|(?:G{1,5}|y{1,4}|Y{1,4}|M{1,5}|L{1,5}|w{1,2}|W{1}|d{1,2}|E{1,6}|c{1,6}|a{1,5}|b{1,5}|B{1,5}|h{1,2}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|z{1,4}|Z{1,5}|O{1,4}))([\s\S]*)/,qN=256;function pw(t,n,e,r){let o=rO(t);YN(n),n=qn(e,n)||n;let s=[],a;for(;n;)if(a=WN.exec(n),a){s=s.concat(a.slice(1));let u=s.pop();if(!u)break;n=u}else{s.push(n);break}let c=o.getTimezoneOffset();r&&(c=gw(r,c),o=nO(o,r));let l="";return s.forEach(u=>{let d=eO(u);l+=d?d(o,e,c):u==="''"?"'":u.replace(/(^'|'$)/g,"").replace(/''/g,"'")}),l}function YN(t){if(t.length>qN)throw new b(2300,!1)}function cu(t,n,e){let r=new Date(0);return r.setFullYear(t,n,e),r.setHours(0,0,0),r}function qn(t,n){let e=iw(t);if(ru[e]??={},ru[e][n])return ru[e][n];let r="";switch(n){case"shortDate":r=Zs(t,lt.Short);break;case"mediumDate":r=Zs(t,lt.Medium);break;case"longDate":r=Zs(t,lt.Long);break;case"fullDate":r=Zs(t,lt.Full);break;case"shortTime":r=Ks(t,lt.Short);break;case"mediumTime":r=Ks(t,lt.Medium);break;case"longTime":r=Ks(t,lt.Long);break;case"fullTime":r=Ks(t,lt.Full);break;case"short":let o=qn(t,"shortTime"),i=qn(t,"shortDate");r=ou(Qs(t,lt.Short),[o,i]);break;case"medium":let s=qn(t,"mediumTime"),a=qn(t,"mediumDate");r=ou(Qs(t,lt.Medium),[s,a]);break;case"long":let c=qn(t,"longTime"),l=qn(t,"longDate");r=ou(Qs(t,lt.Long),[c,l]);break;case"full":let u=qn(t,"fullTime"),d=qn(t,"fullDate");r=ou(Qs(t,lt.Full),[u,d]);break}return r&&(ru[e][n]=r),r}function ou(t,n){return n&&(t=t.replace(/\{([^}]+)}/g,function(e,r){return n!=null&&r in n?n[r]:e})),t}function nn(t,n,e="-",r,o){let i="";(t<0||o&&t<=0)&&(o?t=-t+1:(t=-t,i=e));let s=String(t);for(;s.length0||a>-e)&&(a+=e),t===3)a===0&&e===-12&&(a=12);else if(t===6)return ZN(a,n);let c=En(s,ut.MinusSign);return nn(a,n,c,r,o)}}function KN(t,n){switch(t){case 0:return n.getFullYear();case 1:return n.getMonth();case 2:return n.getDate();case 3:return n.getHours();case 4:return n.getMinutes();case 5:return n.getSeconds();case 6:return n.getMilliseconds();case 7:return n.getDay();default:throw new b(2301,!1)}}function fe(t,n,e=Je.Format,r=!1){return function(o,i){return QN(o,i,t,n,e,r)}}function QN(t,n,e,r,o,i){switch(e){case 2:return cw(n,o,r)[t.getMonth()];case 1:return aw(n,o,r)[t.getDay()];case 0:let s=t.getHours(),a=t.getMinutes();if(i){let l=fw(n),u=hw(n,o,r),d=l.findIndex(h=>{if(Array.isArray(h)){let[p,m]=h,_=s>=p.hours&&a>=p.minutes,D=s0?Math.floor(o/60):Math.ceil(o/60);switch(t){case 0:return(o>=0?"+":"")+nn(s,2,i)+nn(Math.abs(o%60),2,i);case 1:return"GMT"+(o>=0?"+":"")+nn(s,1,i);case 2:return"GMT"+(o>=0?"+":"")+nn(s,2,i)+":"+nn(Math.abs(o%60),2,i);case 3:return r===0?"Z":(o>=0?"+":"")+nn(s,2,i)+":"+nn(Math.abs(o%60),2,i);default:throw new b(2310,!1)}}}var XN=0,au=4;function JN(t){let n=cu(t,XN,1).getDay();return cu(t,0,1+(n<=au?au:au+7)-n)}function mw(t){let n=t.getDay(),e=n===0?-3:au-n;return cu(t.getFullYear(),t.getMonth(),t.getDate()+e)}function Mm(t,n=!1){return function(e,r){let o;if(n){let i=new Date(e.getFullYear(),e.getMonth(),1).getDay()-1,s=e.getDate();o=1+Math.floor((s+i)/7)}else{let i=mw(e),s=JN(i.getFullYear()),a=i.getTime()-s.getTime();o=1+Math.round(a/6048e5)}return nn(o,t,En(r,ut.MinusSign))}}function su(t,n=!1){return function(e,r){let i=mw(e).getFullYear();return nn(i,t,En(r,ut.MinusSign),n)}}var Tm={};function eO(t){if(Tm[t])return Tm[t];let n;switch(t){case"G":case"GG":case"GGG":n=fe(3,ce.Abbreviated);break;case"GGGG":n=fe(3,ce.Wide);break;case"GGGGG":n=fe(3,ce.Narrow);break;case"y":n=xe(0,1,0,!1,!0);break;case"yy":n=xe(0,2,0,!0,!0);break;case"yyy":n=xe(0,3,0,!1,!0);break;case"yyyy":n=xe(0,4,0,!1,!0);break;case"Y":n=su(1);break;case"YY":n=su(2,!0);break;case"YYY":n=su(3);break;case"YYYY":n=su(4);break;case"M":case"L":n=xe(1,1,1);break;case"MM":case"LL":n=xe(1,2,1);break;case"MMM":n=fe(2,ce.Abbreviated);break;case"MMMM":n=fe(2,ce.Wide);break;case"MMMMM":n=fe(2,ce.Narrow);break;case"LLL":n=fe(2,ce.Abbreviated,Je.Standalone);break;case"LLLL":n=fe(2,ce.Wide,Je.Standalone);break;case"LLLLL":n=fe(2,ce.Narrow,Je.Standalone);break;case"w":n=Mm(1);break;case"ww":n=Mm(2);break;case"W":n=Mm(1,!0);break;case"d":n=xe(2,1);break;case"dd":n=xe(2,2);break;case"c":case"cc":n=xe(7,1);break;case"ccc":n=fe(1,ce.Abbreviated,Je.Standalone);break;case"cccc":n=fe(1,ce.Wide,Je.Standalone);break;case"ccccc":n=fe(1,ce.Narrow,Je.Standalone);break;case"cccccc":n=fe(1,ce.Short,Je.Standalone);break;case"E":case"EE":case"EEE":n=fe(1,ce.Abbreviated);break;case"EEEE":n=fe(1,ce.Wide);break;case"EEEEE":n=fe(1,ce.Narrow);break;case"EEEEEE":n=fe(1,ce.Short);break;case"a":case"aa":case"aaa":n=fe(0,ce.Abbreviated);break;case"aaaa":n=fe(0,ce.Wide);break;case"aaaaa":n=fe(0,ce.Narrow);break;case"b":case"bb":case"bbb":n=fe(0,ce.Abbreviated,Je.Standalone,!0);break;case"bbbb":n=fe(0,ce.Wide,Je.Standalone,!0);break;case"bbbbb":n=fe(0,ce.Narrow,Je.Standalone,!0);break;case"B":case"BB":case"BBB":n=fe(0,ce.Abbreviated,Je.Format,!0);break;case"BBBB":n=fe(0,ce.Wide,Je.Format,!0);break;case"BBBBB":n=fe(0,ce.Narrow,Je.Format,!0);break;case"h":n=xe(3,1,-12);break;case"hh":n=xe(3,2,-12);break;case"H":n=xe(3,1);break;case"HH":n=xe(3,2);break;case"m":n=xe(4,1);break;case"mm":n=xe(4,2);break;case"s":n=xe(5,1);break;case"ss":n=xe(5,2);break;case"S":n=xe(6,1);break;case"SS":n=xe(6,2);break;case"SSS":n=xe(6,3);break;case"Z":case"ZZ":case"ZZZ":n=iu(0);break;case"ZZZZZ":n=iu(3);break;case"O":case"OO":case"OOO":case"z":case"zz":case"zzz":n=iu(1);break;case"OOOO":case"ZZZZ":case"zzzz":n=iu(2);break;default:return null}return Tm[t]=n,n}function gw(t,n){t=t.replace(/:/g,"");let e=Date.parse("Jan 01, 1970 00:00:00 "+t)/6e4;return isNaN(e)?n:e}function tO(t,n){return t=new Date(t.getTime()),t.setMinutes(t.getMinutes()+n),t}function nO(t,n,e){let o=t.getTimezoneOffset(),i=gw(n,o);return tO(t,-1*(i-o))}function rO(t){if(ew(t))return t;if(typeof t=="number"&&!isNaN(t))return new Date(t);if(typeof t=="string"){if(t=t.trim(),/^(\d{4}(-\d{1,2}(-\d{1,2})?)?)$/.test(t)){let[o,i=1,s=1]=t.split("-").map(a=>+a);return cu(o,i-1,s)}let e=parseFloat(t);if(!isNaN(t-e))return new Date(e);let r;if(r=t.match(GN))return oO(r)}let n=new Date(t);if(!ew(n))throw new b(2311,!1);return n}function oO(t){let n=new Date(0),e=0,r=0,o=t[8]?n.setUTCFullYear:n.setFullYear,i=t[8]?n.setUTCHours:n.setHours;t[9]&&(e=Number(t[9]+t[10]),r=Number(t[9]+t[11])),o.call(n,Number(t[1]),Number(t[2])-1,Number(t[3]));let s=Number(t[4]||0)-e,a=Number(t[5]||0)-r,c=Number(t[6]||0),l=Math.floor(parseFloat("0."+(t[7]||0))*1e3);return i.call(n,s,a,c,l),n}function ew(t){return t instanceof Date&&!isNaN(t.valueOf())}var iO=/^(\d+)?\.((\d+)(-(\d+))?)?$/,tw=22,lu=".",Xs="0",sO=";",aO=",",xm="#";function cO(t,n,e,r,o,i,s=!1){let a="",c=!1;if(!isFinite(t))a=En(e,ut.Infinity);else{let l=dO(t);s&&(l=uO(l));let u=n.minInt,d=n.minFrac,h=n.maxFrac;if(i){let z=i.match(iO);if(z===null)throw new b(2306,!1);let De=z[1],pe=z[3],er=z[5];De!=null&&(u=Am(De)),pe!=null&&(d=Am(pe)),er!=null?h=Am(er):pe!=null&&d>h&&(h=d);let Tn=100;if(u>Tn||d>Tn||h>Tn)throw new b(2306,!1)}fO(l,d,h);let p=l.digits,m=l.integerLen,_=l.exponent,D=[];for(c=p.every(z=>!z);m0?D=p.splice(m,p.length):(D=p,p=[0]);let C=[];for(p.length>=n.lgSize&&C.unshift(p.splice(-n.lgSize,p.length).join(""));p.length>n.gSize;)C.unshift(p.splice(-n.gSize,p.length).join(""));p.length&&C.unshift(p.join("")),a=C.join(En(e,r)),D.length&&(a+=En(e,o)+D.join("")),_&&(a+=En(e,ut.Exponential)+"+"+_)}return t<0&&!c?a=n.negPre+a+n.negSuf:a=n.posPre+a+n.posSuf,a}function vw(t,n,e){let r=uw(n,Fm.Decimal),o=lO(r,En(n,ut.MinusSign));return cO(t,o,n,ut.Group,ut.Decimal,e)}function lO(t,n="-"){let e={minInt:1,minFrac:0,maxFrac:0,posPre:"",posSuf:"",negPre:"",negSuf:"",gSize:0,lgSize:0},r=t.split(sO),o=r[0],i=r[1],s=o.indexOf(lu)!==-1?o.split(lu):[o.substring(0,o.lastIndexOf(Xs)+1),o.substring(o.lastIndexOf(Xs)+1)],a=s[0],c=s[1]||"";e.posPre=a.substring(0,a.indexOf(xm));for(let u=0;u-1&&(n=n.replace(lu,"")),(i=n.search(/e/i))>0?(o<0&&(o=i),o+=+n.slice(i+1),n=n.substring(0,i)):o<0&&(o=n.length),i=0;n.charAt(i)===Xs;i++);if(i===(a=n.length))r=[0],o=1;else{for(a--;n.charAt(a)===Xs;)a--;for(o-=i,r=[],s=0;i<=a;i++,s++)r[s]=Number(n.charAt(i))}return o>tw&&(r=r.splice(0,tw-1),e=o-1,o=1),{digits:r,exponent:e,integerLen:o}}function fO(t,n,e){if(n>e)throw new b(2307,!1);let r=t.digits,o=r.length-t.integerLen,i=Math.min(Math.max(n,o),e),s=i+t.integerLen,a=r[s];if(s>0){r.splice(Math.max(t.integerLen,s));for(let d=s;d=5)if(s-1<0){for(let d=0;d>s;d--)r.unshift(0),t.integerLen++;r.unshift(1),t.integerLen++}else r[s-1]++;for(;o=l?m.pop():c=!1),h>=10?1:0},0);u&&(r.unshift(u),t.integerLen++)}function Am(t){let n=parseInt(t);if(isNaN(n))throw new b(2305,!1);return n}var Rm=/\s+/,nw=[],hO=(()=>{class t{_ngEl;_renderer;initialClasses=nw;rawClass;stateMap=new Map;constructor(e,r){this._ngEl=e,this._renderer=r}set klass(e){this.initialClasses=e!=null?e.trim().split(Rm):nw}set ngClass(e){this.rawClass=typeof e=="string"?e.trim().split(Rm):e}ngDoCheck(){for(let r of this.initialClasses)this._updateState(r,!0);let e=this.rawClass;if(Array.isArray(e)||e instanceof Set)for(let r of e)this._updateState(r,!0);else if(e!=null)for(let r of Object.keys(e))this._updateState(r,!!e[r]);this._applyStateDiff()}_updateState(e,r){let o=this.stateMap.get(e);o!==void 0?(o.enabled!==r&&(o.changed=!0,o.enabled=r),o.touched=!0):this.stateMap.set(e,{enabled:r,changed:!0,touched:!0})}_applyStateDiff(){for(let e of this.stateMap){let r=e[0],o=e[1];o.changed?(this._toggleClass(r,o.enabled),o.changed=!1):o.touched||(o.enabled&&this._toggleClass(r,!1),this.stateMap.delete(r)),o.touched=!1}}_toggleClass(e,r){e=e.trim(),e.length>0&&e.split(Rm).forEach(o=>{r?this._renderer.addClass(this._ngEl.nativeElement,o):this._renderer.removeClass(this._ngEl.nativeElement,o)})}static \u0275fac=function(r){return new(r||t)(E($),E(Le))};static \u0275dir=T({type:t,selectors:[["","ngClass",""]],inputs:{klass:[0,"class","klass"],ngClass:"ngClass"}})}return t})();var uu=class{$implicit;ngForOf;index;count;constructor(n,e,r,o){this.$implicit=n,this.ngForOf=e,this.index=r,this.count=o}get first(){return this.index===0}get last(){return this.index===this.count-1}get even(){return this.index%2===0}get odd(){return!this.even}},yw=(()=>{class t{_viewContainer;_template;_differs;set ngForOf(e){this._ngForOf=e,this._ngForOfDirty=!0}set ngForTrackBy(e){this._trackByFn=e}get ngForTrackBy(){return this._trackByFn}_ngForOf=null;_ngForOfDirty=!0;_differ=null;_trackByFn;constructor(e,r,o){this._viewContainer=e,this._template=r,this._differs=o}set ngForTemplate(e){e&&(this._template=e)}ngDoCheck(){if(this._ngForOfDirty){this._ngForOfDirty=!1;let e=this._ngForOf;!this._differ&&e&&(this._differ=this._differs.find(e).create(this.ngForTrackBy))}if(this._differ){let e=this._differ.diff(this._ngForOf);e&&this._applyChanges(e)}}_applyChanges(e){let r=this._viewContainer;e.forEachOperation((o,i,s)=>{if(o.previousIndex==null)r.createEmbeddedView(this._template,new uu(o.item,this._ngForOf,-1,-1),s===null?void 0:s);else if(s==null)r.remove(i===null?void 0:i);else if(i!==null){let a=r.get(i);r.move(a,s),rw(a,o)}});for(let o=0,i=r.length;o{let i=r.get(o.currentIndex);rw(i,o)})}static ngTemplateContextGuard(e,r){return!0}static \u0275fac=function(r){return new(r||t)(E(Qe),E(gt),E(Xl))};static \u0275dir=T({type:t,selectors:[["","ngFor","","ngForOf",""]],inputs:{ngForOf:"ngForOf",ngForTrackBy:"ngForTrackBy",ngForTemplate:"ngForTemplate"}})}return t})();function rw(t,n){t.context.$implicit=n.item}var pO=(()=>{class t{_viewContainer;_context=new du;_thenTemplateRef=null;_elseTemplateRef=null;_thenViewRef=null;_elseViewRef=null;constructor(e,r){this._viewContainer=e,this._thenTemplateRef=r}set ngIf(e){this._context.$implicit=this._context.ngIf=e,this._updateView()}set ngIfThen(e){ow(e,!1),this._thenTemplateRef=e,this._thenViewRef=null,this._updateView()}set ngIfElse(e){ow(e,!1),this._elseTemplateRef=e,this._elseViewRef=null,this._updateView()}_updateView(){this._context.$implicit?this._thenViewRef||(this._viewContainer.clear(),this._elseViewRef=null,this._thenTemplateRef&&(this._thenViewRef=this._viewContainer.createEmbeddedView(this._thenTemplateRef,this._context))):this._elseViewRef||(this._viewContainer.clear(),this._thenViewRef=null,this._elseTemplateRef&&(this._elseViewRef=this._viewContainer.createEmbeddedView(this._elseTemplateRef,this._context)))}static ngIfUseIfTypeGuard;static ngTemplateGuard_ngIf;static ngTemplateContextGuard(e,r){return!0}static \u0275fac=function(r){return new(r||t)(E(Qe),E(gt))};static \u0275dir=T({type:t,selectors:[["","ngIf",""]],inputs:{ngIf:"ngIf",ngIfThen:"ngIfThen",ngIfElse:"ngIfElse"}})}return t})(),du=class{$implicit=null;ngIf=null};function ow(t,n){if(t&&!t.createEmbeddedView)throw new b(2020,!1)}var mO=(()=>{class t{_ngEl;_differs;_renderer;_ngStyle=null;_differ=null;constructor(e,r,o){this._ngEl=e,this._differs=r,this._renderer=o}set ngStyle(e){this._ngStyle=e,!this._differ&&e&&(this._differ=this._differs.find(e).create())}ngDoCheck(){if(this._differ){let e=this._differ.diff(this._ngStyle);e&&this._applyChanges(e)}}_setStyle(e,r){let[o,i]=e.split("."),s=o.indexOf("-")===-1?void 0:Yt.DashCase;r!=null?this._renderer.setStyle(this._ngEl.nativeElement,o,i?`${r}${i}`:r,s):this._renderer.removeStyle(this._ngEl.nativeElement,o,s)}_applyChanges(e){e.forEachRemovedItem(r=>this._setStyle(r.key,null)),e.forEachAddedItem(r=>this._setStyle(r.key,r.currentValue)),e.forEachChangedItem(r=>this._setStyle(r.key,r.currentValue))}static \u0275fac=function(r){return new(r||t)(E($),E(Em),E(Le))};static \u0275dir=T({type:t,selectors:[["","ngStyle",""]],inputs:{ngStyle:"ngStyle"}})}return t})(),gO=(()=>{class t{_viewContainerRef;_viewRef=null;ngTemplateOutletContext=null;ngTemplateOutlet=null;ngTemplateOutletInjector=null;injector=f(W);constructor(e){this._viewContainerRef=e}ngOnChanges(e){if(this._shouldRecreateView(e)){let r=this._viewContainerRef;if(this._viewRef&&r.remove(r.indexOf(this._viewRef)),!this.ngTemplateOutlet){this._viewRef=null;return}let o=this._createContextForwardProxy();this._viewRef=r.createEmbeddedView(this.ngTemplateOutlet,o,{injector:this._getInjector()})}}_getInjector(){return this.ngTemplateOutletInjector==="outlet"?this.injector:this.ngTemplateOutletInjector??void 0}_shouldRecreateView(e){return!!e.ngTemplateOutlet||!!e.ngTemplateOutletInjector}_createContextForwardProxy(){return new Proxy({},{set:(e,r,o)=>this.ngTemplateOutletContext?Reflect.set(this.ngTemplateOutletContext,r,o):!1,get:(e,r,o)=>{if(this.ngTemplateOutletContext)return Reflect.get(this.ngTemplateOutletContext,r,o)}})}static \u0275fac=function(r){return new(r||t)(E(Qe))};static \u0275dir=T({type:t,selectors:[["","ngTemplateOutlet",""]],inputs:{ngTemplateOutletContext:"ngTemplateOutletContext",ngTemplateOutlet:"ngTemplateOutlet",ngTemplateOutletInjector:"ngTemplateOutletInjector"},features:[$e]})}return t})();function fu(t,n){return new b(2100,!1)}var Nm=class{createSubscription(n,e,r){return Y(()=>n.subscribe({next:e,error:r}))}dispose(n){Y(()=>n.unsubscribe())}},Om=class{createSubscription(n,e,r){return n.then(o=>e?.(o),o=>r?.(o)),{unsubscribe:()=>{e=null,r=null}}}dispose(n){n.unsubscribe()}},vO=new Om,yO=new Nm,bO=(()=>{class t{_ref;_latestValue=null;markForCheckOnValueUpdate=!0;_subscription=null;_obj=null;_strategy=null;applicationErrorHandler=f(mt);constructor(e){this._ref=e}ngOnDestroy(){this._subscription&&this._dispose(),this._ref=null}transform(e){if(!this._obj){if(e)try{this.markForCheckOnValueUpdate=!1,this._subscribe(e)}finally{this.markForCheckOnValueUpdate=!0}return this._latestValue}return e!==this._obj?(this._dispose(),this.transform(e)):this._latestValue}_subscribe(e){this._obj=e,this._strategy=this._selectStrategy(e),this._subscription=this._strategy.createSubscription(e,r=>this._updateLatestValue(e,r),r=>this.applicationErrorHandler(r))}_selectStrategy(e){if(zn(e))return vO;if(Ul(e))return yO;throw fu(t,e)}_dispose(){this._strategy.dispose(this._subscription),this._latestValue=null,this._subscription=null,this._obj=null}_updateLatestValue(e,r){e===this._obj&&(this._latestValue=r,this.markForCheckOnValueUpdate&&this._ref?.markForCheck())}static \u0275fac=function(r){return new(r||t)(E(Jt,16))};static \u0275pipe=ii({name:"async",type:t,pure:!1})}return t})();var _O=/(?:[0-9A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])\S*/g,DO=(()=>{class t{transform(e){return e==null?null:(EO(t,e),e.replace(_O,r=>r[0].toUpperCase()+r.slice(1).toLowerCase()))}static \u0275fac=function(r){return new(r||t)};static \u0275pipe=ii({name:"titlecase",type:t,pure:!0})}return t})();function EO(t,n){if(typeof n!="string")throw fu(t,n)}var wO="mediumDate",bw=new y(""),_w=new y(""),CO=(()=>{class t{locale;defaultTimezone;defaultOptions;constructor(e,r,o){this.locale=e,this.defaultTimezone=r,this.defaultOptions=o}transform(e,r,o,i){if(e==null||e===""||e!==e)return null;try{let s=r??this.defaultOptions?.dateFormat??wO,a=o??this.defaultOptions?.timezone??this.defaultTimezone??void 0;return pw(e,s,i||this.locale,a)}catch(s){throw fu(t,s.message)}}static \u0275fac=function(r){return new(r||t)(E(li,16),E(bw,24),E(_w,24))};static \u0275pipe=ii({name:"date",type:t,pure:!0})}return t})();var IO=(()=>{class t{_locale;constructor(e){this._locale=e}transform(e,r,o){if(!SO(e))return null;o||=this._locale;try{let i=MO(e);return vw(i,o,r)}catch(i){throw fu(t,i.message)}}static \u0275fac=function(r){return new(r||t)(E(li,16))};static \u0275pipe=ii({name:"number",type:t,pure:!0})}return t})();function SO(t){return!(t==null||t===""||t!==t)}function MO(t){if(typeof t=="string"&&!isNaN(Number(t)-parseFloat(t)))return Number(t);if(typeof t!="number")throw new b(2309,!1);return t}var Pm=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({})}return t})();function Js(t,n){n=encodeURIComponent(n);for(let e of t.split(";")){let r=e.indexOf("="),[o,i]=r==-1?[e,""]:[e.slice(0,r),e.slice(r+1)];if(o.trim()===n)return decodeURIComponent(i)}return null}var co=class{};var jm="browser";function Dw(t){return t===jm}var Vm=(()=>{class t{static \u0275prov=v({token:t,providedIn:"root",factory:()=>new Lm(f(O),window)})}return t})(),Lm=class{document;window;offset=()=>[0,0];constructor(n,e){this.document=n,this.window=e}setOffset(n){Array.isArray(n)?this.offset=()=>n:this.offset=n}getScrollPosition(){return[this.window.scrollX,this.window.scrollY]}scrollToPosition(n,e){this.window.scrollTo(F(g({},e),{left:n[0],top:n[1]}))}scrollToAnchor(n,e){let r=TO(this.document,n);r&&(this.scrollToElement(r,e),r.focus({preventScroll:!0}))}setHistoryScrollRestoration(n){try{this.window.history.scrollRestoration=n}catch{console.warn(St(2400,!1))}}scrollToElement(n,e){let r=n.getBoundingClientRect(),o=r.left+this.window.pageXOffset,i=r.top+this.window.pageYOffset,s=this.offset();this.window.scrollTo(F(g({},e),{left:o-s[0],top:i-s[1]}))}};function TO(t,n){let e=t.getElementById(n)||t.getElementsByName(n)[0];if(e)return e;if(typeof t.createTreeWalker=="function"&&t.body&&typeof t.body.attachShadow=="function"){let r=t.createTreeWalker(t.body,NodeFilter.SHOW_ELEMENT),o=r.currentNode;for(;o;){let i=o.shadowRoot;if(i){let s=i.getElementById(n)||i.querySelector(`[name="${n}"]`);if(s)return s}o=r.nextNode()}}return null}var ea=class{_doc;constructor(n){this._doc=n}manager},hu=(()=>{class t extends ea{constructor(e){super(e)}supports(e){return!0}addEventListener(e,r,o,i){return e.addEventListener(r,o,i),()=>this.removeEventListener(e,r,o,i)}removeEventListener(e,r,o,i){return e.removeEventListener(r,o,i)}static \u0275fac=function(r){return new(r||t)(w(O))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),gu=new y(""),$m=(()=>{class t{_zone;_plugins;_eventNameToPlugin=new Map;constructor(e,r){this._zone=r,e.forEach(s=>{s.manager=this});let o=e.filter(s=>!(s instanceof hu));this._plugins=o.slice().reverse();let i=e.find(s=>s instanceof hu);i&&this._plugins.push(i)}addEventListener(e,r,o,i){return this._findPluginFor(r).addEventListener(e,r,o,i)}getZone(){return this._zone}_findPluginFor(e){let r=this._eventNameToPlugin.get(e);if(r)return r;if(r=this._plugins.find(i=>i.supports(e)),!r)throw new b(5101,!1);return this._eventNameToPlugin.set(e,r),r}static \u0275fac=function(r){return new(r||t)(w(gu),w(j))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),Bm="ng-app-id";function Ew(t){for(let n of t)n.remove()}function ww(t,n){let e=n.createElement("style");return e.textContent=t,e}function AO(t,n,e,r){let o=t.head?.querySelectorAll(`style[${Bm}="${n}"],link[${Bm}="${n}"]`);if(o)for(let i of o)i.removeAttribute(Bm),i instanceof HTMLLinkElement?r.set(i.href.slice(i.href.lastIndexOf("/")+1),{usage:0,elements:[i]}):i.textContent&&e.set(i.textContent,{usage:0,elements:[i]})}function Hm(t,n){let e=n.createElement("link");return e.setAttribute("rel","stylesheet"),e.setAttribute("href",t),e}var zm=(()=>{class t{doc;appId;nonce;inline=new Map;external=new Map;hosts=new Set;constructor(e,r,o,i={}){this.doc=e,this.appId=r,this.nonce=o,AO(e,r,this.inline,this.external),this.hosts.add(e.head)}addStyles(e,r){for(let o of e)this.addUsage(o,this.inline,ww);r?.forEach(o=>this.addUsage(o,this.external,Hm))}removeStyles(e,r){for(let o of e)this.removeUsage(o,this.inline);r?.forEach(o=>this.removeUsage(o,this.external))}addUsage(e,r,o){let i=r.get(e);i?i.usage++:r.set(e,{usage:1,elements:[...this.hosts].map(s=>this.addElement(s,o(e,this.doc)))})}removeUsage(e,r){let o=r.get(e);o&&(o.usage--,o.usage<=0&&(Ew(o.elements),r.delete(e)))}ngOnDestroy(){for(let[,{elements:e}]of[...this.inline,...this.external])Ew(e);this.hosts.clear()}addHost(e){this.hosts.add(e);for(let[r,{elements:o}]of this.inline)o.push(this.addElement(e,ww(r,this.doc)));for(let[r,{elements:o}]of this.external)o.push(this.addElement(e,Hm(r,this.doc)))}removeHost(e){this.hosts.delete(e)}addElement(e,r){return this.nonce&&r.setAttribute("nonce",this.nonce),e.appendChild(r)}static \u0275fac=function(r){return new(r||t)(w(O),w(_r),w(ao,8),w(so))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),Um={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/",math:"http://www.w3.org/1998/Math/MathML"},Gm=/%COMP%/g;var Iw="%COMP%",RO=`_nghost-${Iw}`,NO=`_ngcontent-${Iw}`,OO=!0,kO=new y("",{factory:()=>OO});function FO(t){return NO.replace(Gm,t)}function PO(t){return RO.replace(Gm,t)}function Sw(t,n){return n.map(e=>e.replace(Gm,t))}var Wm=(()=>{class t{eventManager;sharedStylesHost;appId;removeStylesOnCompDestroy;doc;ngZone;nonce;tracingService;rendererByCompId=new Map;defaultRenderer;constructor(e,r,o,i,s,a,c=null,l=null){this.eventManager=e,this.sharedStylesHost=r,this.appId=o,this.removeStylesOnCompDestroy=i,this.doc=s,this.ngZone=a,this.nonce=c,this.tracingService=l,this.defaultRenderer=new ta(e,s,a,this.tracingService)}createRenderer(e,r){if(!e||!r)return this.defaultRenderer;let o=this.getOrCreateRenderer(e,r);return o instanceof mu?o.applyToHost(e):o instanceof na&&o.applyStyles(),o}getOrCreateRenderer(e,r){let o=this.rendererByCompId,i=o.get(r.id);if(!i){let s=this.doc,a=this.ngZone,c=this.eventManager,l=this.sharedStylesHost,u=this.removeStylesOnCompDestroy,d=this.tracingService;switch(r.encapsulation){case qt.Emulated:i=new mu(c,l,r,this.appId,u,s,a,d);break;case qt.ShadowDom:return new pu(c,e,r,s,a,this.nonce,d,l);case qt.ExperimentalIsolatedShadowDom:return new pu(c,e,r,s,a,this.nonce,d);default:i=new na(c,l,r,u,s,a,d);break}o.set(r.id,i)}return i}ngOnDestroy(){this.rendererByCompId.clear()}componentReplaced(e){this.rendererByCompId.delete(e)}static \u0275fac=function(r){return new(r||t)(w($m),w(zm),w(_r),w(kO),w(O),w(j),w(ao),w(Zt,8))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),ta=class{eventManager;doc;ngZone;tracingService;data=Object.create(null);throwOnSyntheticProps=!0;constructor(n,e,r,o){this.eventManager=n,this.doc=e,this.ngZone=r,this.tracingService=o}destroy(){}destroyNode=null;createElement(n,e){return e?this.doc.createElementNS(Um[e]||e,n):this.doc.createElement(n)}createComment(n){return this.doc.createComment(n)}createText(n){return this.doc.createTextNode(n)}appendChild(n,e){(Cw(n)?n.content:n).appendChild(e)}insertBefore(n,e,r){n&&(Cw(n)?n.content:n).insertBefore(e,r)}removeChild(n,e){e.remove()}selectRootElement(n,e){let r=typeof n=="string"?this.doc.querySelector(n):n;if(!r)throw new b(-5104,!1);return e||(r.textContent=""),r}parentNode(n){return n.parentNode}nextSibling(n){return n.nextSibling}setAttribute(n,e,r,o){if(o){e=o+":"+e;let i=Um[o];i?n.setAttributeNS(i,e,r):n.setAttribute(e,r)}else n.setAttribute(e,r)}removeAttribute(n,e,r){if(r){let o=Um[r];o?n.removeAttributeNS(o,e):n.removeAttribute(`${r}:${e}`)}else n.removeAttribute(e)}addClass(n,e){n.classList.add(e)}removeClass(n,e){n.classList.remove(e)}setStyle(n,e,r,o){o&(Yt.DashCase|Yt.Important)?n.style.setProperty(e,r,o&Yt.Important?"important":""):n.style[e]=r}removeStyle(n,e,r){r&Yt.DashCase?n.style.removeProperty(e):n.style[e]=""}setProperty(n,e,r){n!=null&&(n[e]=r)}setValue(n,e){n.nodeValue=e}listen(n,e,r,o){if(typeof n=="string"&&(n=bt().getGlobalEventTarget(this.doc,n),!n))throw new b(5102,!1);let i=this.decoratePreventDefault(r);return this.tracingService?.wrapEventListener&&(i=this.tracingService.wrapEventListener(n,e,i)),this.eventManager.addEventListener(n,e,i,o)}decoratePreventDefault(n){return e=>{if(e==="__ngUnwrap__")return n;n(e)===!1&&e.preventDefault()}}};function Cw(t){return t.tagName==="TEMPLATE"&&t.content!==void 0}var pu=class extends ta{hostEl;sharedStylesHost;shadowRoot;constructor(n,e,r,o,i,s,a,c){super(n,o,i,a),this.hostEl=e,this.sharedStylesHost=c,this.shadowRoot=e.attachShadow({mode:"open"}),this.sharedStylesHost&&this.sharedStylesHost.addHost(this.shadowRoot);let l=r.styles;l=Sw(r.id,l);for(let d of l){let h=document.createElement("style");s&&h.setAttribute("nonce",s),h.textContent=d,this.shadowRoot.appendChild(h)}let u=r.getExternalStyles?.();if(u)for(let d of u){let h=Hm(d,o);s&&h.setAttribute("nonce",s),this.shadowRoot.appendChild(h)}}nodeOrShadowRoot(n){return n===this.hostEl?this.shadowRoot:n}appendChild(n,e){return super.appendChild(this.nodeOrShadowRoot(n),e)}insertBefore(n,e,r){return super.insertBefore(this.nodeOrShadowRoot(n),e,r)}removeChild(n,e){return super.removeChild(null,e)}parentNode(n){return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(n)))}destroy(){this.sharedStylesHost&&this.sharedStylesHost.removeHost(this.shadowRoot)}},na=class extends ta{sharedStylesHost;removeStylesOnCompDestroy;styles;styleUrls;constructor(n,e,r,o,i,s,a,c){super(n,i,s,a),this.sharedStylesHost=e,this.removeStylesOnCompDestroy=o;let l=r.styles;this.styles=c?Sw(c,l):l,this.styleUrls=r.getExternalStyles?.(c)}applyStyles(){this.sharedStylesHost.addStyles(this.styles,this.styleUrls)}destroy(){this.removeStylesOnCompDestroy&&oo.size===0&&this.sharedStylesHost.removeStyles(this.styles,this.styleUrls)}},mu=class extends na{contentAttr;hostAttr;constructor(n,e,r,o,i,s,a,c){let l=o+"-"+r.id;super(n,e,r,i,s,a,c,l),this.contentAttr=FO(l),this.hostAttr=PO(l)}applyToHost(n){this.applyStyles(),this.setAttribute(n,this.hostAttr,"")}createElement(n,e){let r=super.createElement(n,e);return super.setAttribute(r,this.contentAttr,""),r}};var vu=class t extends Ys{supportsDOMEvents=!0;static makeCurrent(){Cm(new t)}onAndCancel(n,e,r,o){return n.addEventListener(e,r,o),()=>{n.removeEventListener(e,r,o)}}dispatchEvent(n,e){n.dispatchEvent(e)}remove(n){n.remove()}createElement(n,e){return e=e||this.getDefaultDocument(),e.createElement(n)}createHtmlDocument(){return document.implementation.createHTMLDocument("fakeTitle")}getDefaultDocument(){return document}isElementNode(n){return n.nodeType===Node.ELEMENT_NODE}isShadowRoot(n){return n instanceof DocumentFragment}getGlobalEventTarget(n,e){return e==="window"?window:e==="document"?n:e==="body"?n.body:null}getBaseHref(n){let e=LO();return e==null?null:jO(e)}resetBaseElement(){ra=null}getUserAgent(){return window.navigator.userAgent}getCookie(n){return Js(document.cookie,n)}},ra=null;function LO(){return ra=ra||document.head.querySelector("base"),ra?ra.getAttribute("href"):null}function jO(t){return new URL(t,document.baseURI).pathname}var yu=class{addToWindow(n){we.getAngularTestability=(r,o=!0)=>{let i=n.findTestabilityInTree(r,o);if(i==null)throw new b(5103,!1);return i},we.getAllAngularTestabilities=()=>n.getAllTestabilities(),we.getAllAngularRootElements=()=>n.getAllRootElements();let e=r=>{let o=we.getAllAngularTestabilities(),i=o.length,s=function(){i--,i==0&&r()};o.forEach(a=>{a.whenStable(s)})};we.frameworkStabilizers||(we.frameworkStabilizers=[]),we.frameworkStabilizers.push(e)}findTestabilityInTree(n,e,r){if(e==null)return null;let o=n.getTestability(e);return o??(r?bt().isShadowRoot(e)?this.findTestabilityInTree(n,e.host,!0):this.findTestabilityInTree(n,e.parentElement,!0):null)}},VO=(()=>{class t{build(){return new XMLHttpRequest}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),Mw=["alt","control","meta","shift"],BO={"\b":"Backspace"," ":"Tab","\x7F":"Delete","\x1B":"Escape",Del:"Delete",Esc:"Escape",Left:"ArrowLeft",Right:"ArrowRight",Up:"ArrowUp",Down:"ArrowDown",Menu:"ContextMenu",Scroll:"ScrollLock",Win:"OS"},UO={alt:t=>t.altKey,control:t=>t.ctrlKey,meta:t=>t.metaKey,shift:t=>t.shiftKey},Tw=(()=>{class t extends ea{constructor(e){super(e)}supports(e){return t.parseEventName(e)!=null}addEventListener(e,r,o,i){let s=t.parseEventName(r),a=t.eventCallback(s.fullKey,o,this.manager.getZone());return this.manager.getZone().runOutsideAngular(()=>bt().onAndCancel(e,s.domEventName,a,i))}static parseEventName(e){let r=e.toLowerCase().split("."),o=r.shift();if(r.length===0||!(o==="keydown"||o==="keyup"))return null;let i=t._normalizeKey(r.pop()),s="",a=r.indexOf("code");if(a>-1&&(r.splice(a,1),s="code."),Mw.forEach(l=>{let u=r.indexOf(l);u>-1&&(r.splice(u,1),s+=l+".")}),s+=i,r.length!=0||i.length===0)return null;let c={};return c.domEventName=o,c.fullKey=s,c}static matchEventFullKeyCode(e,r){let o=BO[e.key]||e.key,i="";return r.indexOf("code.")>-1&&(o=e.code,i="code."),o==null||!o?!1:(o=o.toLowerCase(),o===" "?o="space":o==="."&&(o="dot"),Mw.forEach(s=>{if(s!==o){let a=UO[s];a(e)&&(i+=s+".")}}),i+=o,i===r)}static eventCallback(e,r,o){return i=>{t.matchEventFullKeyCode(i,e)&&o.runGuarded(()=>r(i))}}static _normalizeKey(e){return e==="esc"?"escape":e}static \u0275fac=function(r){return new(r||t)(w(O))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})();async function HO(t,n,e){let r=g({rootComponent:t},$O(n,e));return qE(r)}function $O(t,n){return{platformRef:n?.platformRef,appProviders:[...xw,...t?.providers??[]],platformProviders:qO}}function zO(){vu.makeCurrent()}function GO(){return new Ze}function WO(){return mp(document),document}var qO=[{provide:so,useValue:jm},{provide:El,useValue:zO,multi:!0},{provide:O,useFactory:WO}];var YO=[{provide:Bl,useClass:yu},{provide:Vl,useClass:Hs},{provide:Hs,useClass:Hs}],xw=[{provide:hs,useValue:"root"},{provide:Ze,useFactory:GO},{provide:gu,useClass:hu,multi:!0},{provide:gu,useClass:Tw,multi:!0},Wm,zm,$m,{provide:He,useExisting:Wm},{provide:co,useClass:VO},[]],ZO=(()=>{class t{constructor(){}static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({providers:[...xw,...YO],imports:[Pm,WE]})}return t})();var wn=class t{headers;normalizedNames=new Map;lazyInit;lazyUpdate=null;constructor(n){n?typeof n=="string"?this.lazyInit=()=>{this.headers=new Map,n.split(` +`).forEach(e=>{let r=e.indexOf(":");if(r>0){let o=e.slice(0,r),i=e.slice(r+1).trim();this.addHeaderEntry(o,i)}})}:typeof Headers<"u"&&n instanceof Headers?(this.headers=new Map,n.forEach((e,r)=>{this.addHeaderEntry(r,e)})):this.lazyInit=()=>{this.headers=new Map,Object.entries(n).forEach(([e,r])=>{this.setHeaderEntries(e,r)})}:this.headers=new Map}has(n){return this.init(),this.headers.has(n.toLowerCase())}get(n){this.init();let e=this.headers.get(n.toLowerCase());return e&&e.length>0?e[0]:null}keys(){return this.init(),Array.from(this.normalizedNames.values())}getAll(n){return this.init(),this.headers.get(n.toLowerCase())||null}append(n,e){return this.clone({name:n,value:e,op:"a"})}set(n,e){return this.clone({name:n,value:e,op:"s"})}delete(n,e){return this.clone({name:n,value:e,op:"d"})}maybeSetNormalizedName(n,e){this.normalizedNames.has(e)||this.normalizedNames.set(e,n)}init(){this.lazyInit&&(this.lazyInit instanceof t?this.copyFrom(this.lazyInit):this.lazyInit(),this.lazyInit=null,this.lazyUpdate&&(this.lazyUpdate.forEach(n=>this.applyUpdate(n)),this.lazyUpdate=null))}copyFrom(n){n.init();for(let[e,r]of n.headers.entries())this.headers.set(e,r),this.normalizedNames.set(e,n.normalizedNames.get(e))}clone(n){let e=new t;return e.lazyInit=this.lazyInit&&this.lazyInit instanceof t?this.lazyInit:this,e.lazyUpdate=(this.lazyUpdate||[]).concat([n]),e}applyUpdate(n){let e=n.name.toLowerCase();switch(n.op){case"a":case"s":let r=n.value;if(typeof r=="string"&&(r=[r]),r.length===0)return;this.maybeSetNormalizedName(n.name,e);let o=n.op==="a"?(this.headers.get(e)||[]).slice():[];o.push(...r),this.headers.set(e,o);break;case"d":let i=n.value;if(i===void 0)this.headers.delete(e),this.normalizedNames.delete(e);else{let s=Array.isArray(i)?i:[i],a=this.headers.get(e);if(!a)return;a=a.filter(c=>s.indexOf(c)===-1),a.length===0?(this.headers.delete(e),this.normalizedNames.delete(e)):this.headers.set(e,a)}break}}addHeaderEntry(n,e){let r=n.toLowerCase();this.maybeSetNormalizedName(n,r),this.headers.has(r)?this.headers.get(r).push(e):this.headers.set(r,[e])}setHeaderEntries(n,e){let r=(Array.isArray(e)?e:[e]).map(i=>i.toString()),o=n.toLowerCase();this.headers.set(o,r),this.maybeSetNormalizedName(n,o)}forEach(n){this.init(),Array.from(this.normalizedNames.keys()).forEach(e=>n(this.normalizedNames.get(e),this.headers.get(e)))}};var _u=class{map=new Map;set(n,e){return this.map.set(n,e),this}get(n){return this.map.has(n)||this.map.set(n,n.defaultValue()),this.map.get(n)}delete(n){return this.map.delete(n),this}has(n){return this.map.has(n)}keys(){return this.map.keys()}},Du=class{encodeKey(n){return Aw(n)}encodeValue(n){return Aw(n)}decodeKey(n){return decodeURIComponent(n)}decodeValue(n){return decodeURIComponent(n)}};function KO(t,n){let e=new Map;return t.length>0&&t.replace(/^\?/,"").split("&").forEach(o=>{let i=o.indexOf("="),[s,a]=i==-1?[n.decodeKey(o),""]:[n.decodeKey(o.slice(0,i)),n.decodeValue(o.slice(i+1))],c=e.get(s)||[];c.push(a),e.set(s,c)}),e}var QO=/%(\d[a-f0-9])/gi,XO={40:"@","3A":":",24:"$","2C":",","3B":";","3D":"=","3F":"?","2F":"/"};function Aw(t){return encodeURIComponent(t).replace(QO,(n,e)=>XO[e]??n)}function bu(t){return`${t}`}var Yn=class t{map;encoder;updates=null;cloneFrom=null;constructor(n={}){if(this.encoder=n.encoder||new Du,n.fromString){if(n.fromObject)throw new b(2805,!1);this.map=KO(n.fromString,this.encoder)}else n.fromObject?(this.map=new Map,Object.keys(n.fromObject).forEach(e=>{let r=n.fromObject[e],o=Array.isArray(r)?r.map(bu):[bu(r)];this.map.set(e,o)})):this.map=null}has(n){return this.init(),this.map.has(n)}get(n){this.init();let e=this.map.get(n);return e?e[0]:null}getAll(n){return this.init(),this.map.get(n)||null}keys(){return this.init(),Array.from(this.map.keys())}append(n,e){return this.clone({param:n,value:e,op:"a"})}appendAll(n){let e=[];return Object.keys(n).forEach(r=>{let o=n[r];Array.isArray(o)?o.forEach(i=>{e.push({param:r,value:i,op:"a"})}):e.push({param:r,value:o,op:"a"})}),this.clone(e)}set(n,e){return this.clone({param:n,value:e,op:"s"})}delete(n,e){return this.clone({param:n,value:e,op:"d"})}toString(){return this.init(),this.keys().map(n=>{let e=this.encoder.encodeKey(n);return this.map.get(n).map(r=>e+"="+this.encoder.encodeValue(r)).join("&")}).filter(n=>n!=="").join("&")}clone(n){let e=new t({encoder:this.encoder});return e.cloneFrom=this.cloneFrom||this,e.updates=(this.updates||[]).concat(n),e}init(){if(this.map===null&&(this.map=new Map),this.cloneFrom!==null){this.cloneFrom.init();for(let[n,e]of this.cloneFrom.map.entries())this.map.set(n,e);this.updates.forEach(n=>{switch(n.op){case"a":case"s":let e=n.op==="a"?(this.map.get(n.param)||[]).slice():[];e.push(bu(n.value)),this.map.set(n.param,e);break;case"d":if(n.value!==void 0){let r=(this.map.get(n.param)||[]).slice(),o=r.indexOf(bu(n.value));o!==-1&&r.splice(o,1),r.length>0?this.map.set(n.param,r):this.map.delete(n.param)}else{this.map.delete(n.param);break}}}),this.cloneFrom=this.updates=null}}};function JO(t){switch(t){case"DELETE":case"GET":case"HEAD":case"OPTIONS":case"JSONP":return!1;default:return!0}}function Rw(t){return typeof ArrayBuffer<"u"&&t instanceof ArrayBuffer}function Nw(t){return typeof Blob<"u"&&t instanceof Blob}function Ow(t){return typeof FormData<"u"&&t instanceof FormData}function ek(t){return typeof URLSearchParams<"u"&&t instanceof URLSearchParams}var kw="Content-Type",Fw="Accept",Lw="text/plain",jw="application/json",tk=`${jw}, ${Lw}, */*`,di=class t{url;body=null;headers;context;reportProgress=!1;withCredentials=!1;credentials;keepalive=!1;cache;priority;mode;redirect;referrer;integrity;referrerPolicy;responseType="json";method;params;urlWithParams;transferCache;timeout;constructor(n,e,r,o){this.url=e,this.method=n.toUpperCase();let i;if(JO(this.method)||o?(this.body=r!==void 0?r:null,i=o):i=r,i){if(this.reportProgress=!!i.reportProgress,this.withCredentials=!!i.withCredentials,this.keepalive=!!i.keepalive,i.responseType&&(this.responseType=i.responseType),i.headers&&(this.headers=i.headers),i.context&&(this.context=i.context),i.params&&(this.params=i.params),i.priority&&(this.priority=i.priority),i.cache&&(this.cache=i.cache),i.credentials&&(this.credentials=i.credentials),typeof i.timeout=="number"){if(i.timeout<1||!Number.isInteger(i.timeout))throw new b(2822,"");this.timeout=i.timeout}i.mode&&(this.mode=i.mode),i.redirect&&(this.redirect=i.redirect),i.integrity&&(this.integrity=i.integrity),i.referrer!==void 0&&(this.referrer=i.referrer),i.referrerPolicy&&(this.referrerPolicy=i.referrerPolicy),this.transferCache=i.transferCache}if(this.headers??=new wn,this.context??=new _u,!this.params)this.params=new Yn,this.urlWithParams=e;else{let s=this.params.toString();if(s.length===0)this.urlWithParams=e;else{let a=e.indexOf("?"),c=a===-1?"?":aTn.set(Ar,n.setHeaders[Ar]),De)),n.setParams&&(pe=Object.keys(n.setParams).reduce((Tn,Ar)=>Tn.set(Ar,n.setParams[Ar]),pe)),new t(e,r,D,{params:pe,headers:De,context:er,reportProgress:z,responseType:o,withCredentials:C,transferCache:m,keepalive:i,cache:a,priority:s,timeout:_,mode:c,redirect:l,credentials:u,referrer:d,integrity:h,referrerPolicy:p})}},lo=(function(t){return t[t.Sent=0]="Sent",t[t.UploadProgress=1]="UploadProgress",t[t.ResponseHeader=2]="ResponseHeader",t[t.DownloadProgress=3]="DownloadProgress",t[t.Response=4]="Response",t[t.User=5]="User",t})(lo||{}),hi=class{headers;status;statusText;url;ok;type;redirected;responseType;constructor(n,e=200,r="OK"){this.headers=n.headers||new wn,this.status=n.status!==void 0?n.status:e,this.statusText=n.statusText||r,this.url=n.url||null,this.redirected=n.redirected,this.responseType=n.responseType,this.ok=this.status>=200&&this.status<300}},Eu=class t extends hi{constructor(n={}){super(n)}type=lo.ResponseHeader;clone(n={}){return new t({headers:n.headers||this.headers,status:n.status!==void 0?n.status:this.status,statusText:n.statusText||this.statusText,url:n.url||this.url||void 0})}},oa=class t extends hi{body;constructor(n={}){super(n),this.body=n.body!==void 0?n.body:null}type=lo.Response;clone(n={}){return new t({body:n.body!==void 0?n.body:this.body,headers:n.headers||this.headers,status:n.status!==void 0?n.status:this.status,statusText:n.statusText||this.statusText,url:n.url||this.url||void 0,redirected:n.redirected??this.redirected,responseType:n.responseType??this.responseType})}},fi=class extends hi{name="HttpErrorResponse";message;error;ok=!1;constructor(n){super(n,0,"Unknown Error"),this.status>=200&&this.status<300?this.message=`Http failure during parsing for ${n.url||"(unknown url)"}`:this.message=`Http failure response for ${n.url||"(unknown url)"}: ${n.status} ${n.statusText}`,this.error=n.error||null}},nk=200,rk=204;var ok=new y("");var ik=/^\)\]\}',?\n/;var Ym=(()=>{class t{xhrFactory;tracingService=f(Zt,{optional:!0});constructor(e){this.xhrFactory=e}maybePropagateTrace(e){return this.tracingService?.propagate?this.tracingService.propagate(e):e}handle(e){if(e.method==="JSONP")throw new b(-2800,!1);let r=this.xhrFactory;return S(null).pipe(Ve(()=>new P(i=>{let s=r.build();if(s.open(e.method,e.urlWithParams),e.withCredentials&&(s.withCredentials=!0),e.headers.forEach((D,C)=>s.setRequestHeader(D,C.join(","))),e.headers.has(Fw)||s.setRequestHeader(Fw,tk),!e.headers.has(kw)){let D=e.detectContentTypeHeader();D!==null&&s.setRequestHeader(kw,D)}if(e.timeout&&(s.timeout=e.timeout),e.responseType){let D=e.responseType.toLowerCase();s.responseType=D!=="json"?D:"text"}let a=e.serializeBody(),c=null,l=()=>{if(c!==null)return c;let D=s.statusText||"OK",C=new wn(s.getAllResponseHeaders()),z=s.responseURL||e.url;return c=new Eu({headers:C,status:s.status,statusText:D,url:z}),c},u=this.maybePropagateTrace(()=>{let{headers:D,status:C,statusText:z,url:De}=l(),pe=null;C!==rk&&(pe=typeof s.response>"u"?s.responseText:s.response),C===0&&(C=pe?nk:0);let er=C>=200&&C<300;if(e.responseType==="json"&&typeof pe=="string"){let Tn=pe;pe=pe.replace(ik,"");try{pe=pe!==""?JSON.parse(pe):null}catch(Ar){pe=Tn,er&&(er=!1,pe={error:Ar,text:pe})}}er?(i.next(new oa({body:pe,headers:D,status:C,statusText:z,url:De||void 0})),i.complete()):i.error(new fi({error:pe,headers:D,status:C,statusText:z,url:De||void 0}))}),d=this.maybePropagateTrace(D=>{let{url:C}=l(),z=new fi({error:D,status:s.status||0,statusText:s.statusText||"Unknown Error",url:C||void 0});i.error(z)}),h=d;e.timeout&&(h=this.maybePropagateTrace(D=>{let{url:C}=l(),z=new fi({error:new DOMException("Request timed out","TimeoutError"),status:s.status||0,statusText:s.statusText||"Request timeout",url:C||void 0});i.error(z)}));let p=!1,m=this.maybePropagateTrace(D=>{p||(i.next(l()),p=!0);let C={type:lo.DownloadProgress,loaded:D.loaded};D.lengthComputable&&(C.total=D.total),e.responseType==="text"&&s.responseText&&(C.partialText=s.responseText),i.next(C)}),_=this.maybePropagateTrace(D=>{let C={type:lo.UploadProgress,loaded:D.loaded};D.lengthComputable&&(C.total=D.total),i.next(C)});return s.addEventListener("load",u),s.addEventListener("error",d),s.addEventListener("timeout",h),s.addEventListener("abort",d),e.reportProgress&&(s.addEventListener("progress",m),a!==null&&s.upload&&s.upload.addEventListener("progress",_)),s.send(a),i.next({type:lo.Sent}),()=>{s.removeEventListener("error",d),s.removeEventListener("abort",d),s.removeEventListener("load",u),s.removeEventListener("timeout",h),e.reportProgress&&(s.removeEventListener("progress",m),a!==null&&s.upload&&s.upload.removeEventListener("progress",_)),s.readyState!==s.DONE&&s.abort()}})))}static \u0275fac=function(r){return new(r||t)(w(co))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),sk=new y("",{factory:()=>!0}),ak="XSRF-TOKEN",ck=new y("",{factory:()=>ak}),lk="X-XSRF-TOKEN",uk=new y("",{factory:()=>lk}),dk=(()=>{class t{cookieName=f(ck);doc=f(O);lastCookieString="";lastToken=null;parseCount=0;getToken(){let e=this.doc.cookie||"";return e!==this.lastCookieString&&(this.parseCount++,this.lastToken=Js(e,this.cookieName),this.lastCookieString=e),this.lastToken}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Vw=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:function(r){let o=null;return r?o=new(r||t):o=w(dk),o},providedIn:"root"})}return t})();function Bw(t,n){if(!f(sk)||t.method==="GET"||t.method==="HEAD")return n(t);try{let o=f(Wn).href,{origin:i}=new URL(o),{origin:s}=new URL(t.url,i);if(i!==s)return n(t)}catch{return n(t)}let e=f(Vw).getToken(),r=f(uk);return e!=null&&!t.headers.has(r)&&(t=t.clone({headers:t.headers.set(r,e)})),n(t)}function Uw(t,n){return n(t)}function fk(t,n){return(e,r)=>n.intercept(e,{handle:o=>t(o,r)})}function hk(t,n,e){return(r,o)=>Fe(e,()=>n(r,i=>t(i,o)))}var Hw=new y(""),Zm=new y("",{factory:()=>[Bw]}),$w=new y(""),Km=new y("",{factory:()=>!0});function pk(){let t=null;return(n,e)=>{t===null&&(t=(f(Hw,{optional:!0})??[]).reduceRight(fk,Uw));let r=f(Wo);if(f(Km)){let i=r.add();return t(n,e).pipe(Rn(i))}else return t(n,e)}}var Qm=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:function(r){let o=null;return r?o=new(r||t):o=w(Ym),o},providedIn:"root"})}return t})();var wu=(()=>{class t{backend;injector;chain=null;pendingTasks=f(Wo);contributeToStability=f(Km);constructor(e,r){this.backend=e,this.injector=r}handle(e){if(this.chain===null){let r=this.injector.get(Cu,null,{skipSelf:!0}),o=r!==null&&this.backend===r,i=this.injector.get($w,[],o?{self:!0}:void 0),s=Array.from(new Set([...this.injector.get(Zm),...i]));this.chain=s.reduceRight((a,c)=>hk(a,c,this.injector),Uw)}if(this.contributeToStability){let r=this.pendingTasks.add();return this.chain(e,o=>this.backend.handle(o)).pipe(Rn(r))}else return this.chain(e,r=>this.backend.handle(r))}static \u0275fac=function(r){return new(r||t)(w(Qm),w(re))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Cu=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:function(r){let o=null;return r?o=new(r||t):o=w(wu),o},providedIn:"root"})}return t})();function qm(t,n){return{body:n,headers:t.headers,context:t.context,observe:t.observe,params:t.params,reportProgress:t.reportProgress,responseType:t.responseType,withCredentials:t.withCredentials,credentials:t.credentials,transferCache:t.transferCache,timeout:t.timeout,keepalive:t.keepalive,priority:t.priority,cache:t.cache,mode:t.mode,redirect:t.redirect,integrity:t.integrity,referrer:t.referrer,referrerPolicy:t.referrerPolicy}}var pi=(()=>{class t{handler;constructor(e){this.handler=e}request(e,r,o={}){let i;if(e instanceof di)i=e;else{let c;o.headers instanceof wn?c=o.headers:c=new wn(o.headers);let l;o.params&&(o.params instanceof Yn?l=o.params:l=new Yn({fromObject:o.params})),i=new di(e,r,o.body!==void 0?o.body:null,{headers:c,context:o.context,params:l,reportProgress:o.reportProgress,responseType:o.responseType||"json",withCredentials:o.withCredentials,transferCache:o.transferCache,keepalive:o.keepalive,priority:o.priority,cache:o.cache,mode:o.mode,redirect:o.redirect,credentials:o.credentials,referrer:o.referrer,referrerPolicy:o.referrerPolicy,integrity:o.integrity,timeout:o.timeout})}let s=S(i).pipe(or(c=>this.handler.handle(c)));if(e instanceof di||o.observe==="events")return s;let a=s.pipe(he(c=>c instanceof oa));switch(o.observe||"body"){case"body":switch(i.responseType){case"arraybuffer":return a.pipe(V(c=>{if(c.body!==null&&!(c.body instanceof ArrayBuffer))throw new b(2806,!1);return c.body}));case"blob":return a.pipe(V(c=>{if(c.body!==null&&!(c.body instanceof Blob))throw new b(2807,!1);return c.body}));case"text":return a.pipe(V(c=>{if(c.body!==null&&typeof c.body!="string")throw new b(2808,!1);return c.body}));default:return a.pipe(V(c=>c.body))}case"response":return a;default:throw new b(2809,!1)}}delete(e,r={}){return this.request("DELETE",e,r)}get(e,r={}){return this.request("GET",e,r)}head(e,r={}){return this.request("HEAD",e,r)}jsonp(e,r){return this.request("JSONP",e,{params:new Yn().append(r,"JSONP_CALLBACK"),observe:"body",responseType:"json"})}options(e,r={}){return this.request("OPTIONS",e,r)}patch(e,r,o={}){return this.request("PATCH",e,qm(o,r))}post(e,r,o={}){return this.request("POST",e,qm(o,r))}put(e,r,o={}){return this.request("PUT",e,qm(o,r))}static \u0275fac=function(r){return new(r||t)(w(Cu))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var Xm=(function(t){return t[t.Interceptors=0]="Interceptors",t[t.LegacyInterceptors=1]="LegacyInterceptors",t[t.CustomXsrfConfiguration=2]="CustomXsrfConfiguration",t[t.NoXsrfProtection=3]="NoXsrfProtection",t[t.JsonpSupport=4]="JsonpSupport",t[t.RequestsMadeViaParent=5]="RequestsMadeViaParent",t[t.Fetch=6]="Fetch",t})(Xm||{});function mk(t,n){return{\u0275kind:t,\u0275providers:n}}function zw(...t){let n=[pi,wu,{provide:Cu,useExisting:wu},{provide:Qm,useFactory:()=>f(ok,{optional:!0})??f(Ym)},{provide:Zm,useValue:Bw,multi:!0}];for(let e of t)n.push(...e.\u0275providers);return hr(n)}var Pw=new y("");function Gw(){return mk(Xm.LegacyInterceptors,[{provide:Pw,useFactory:pk},{provide:Zm,useExisting:Pw,multi:!0}])}var gk=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({providers:[zw(Gw())]})}return t})();var qw=(()=>{class t{_doc;constructor(e){this._doc=e}getTitle(){return this._doc.title}setTitle(e){this._doc.title=e||""}static \u0275fac=function(r){return new(r||t)(w(O))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function vk(t,n){if(typeof COMPILED>"u"||!COMPILED){let e=we.ng=we.ng||{};e[t]=n}}var Jm=class{msPerTick;numTicks;constructor(n,e){this.msPerTick=n,this.numTicks=e}},eg=class{appRef;constructor(n){this.appRef=n.injector.get(Ge)}timeChangeDetection(n){let e=n&&n.record,r="Change Detection";e&&"profile"in console&&typeof console.profile=="function"&&console.profile(r);let o=performance.now(),i=0;for(;i<5||performance.now()-o<500;)this.appRef.tick(),i++;let s=performance.now();e&&"profileEnd"in console&&typeof console.profileEnd=="function"&&console.profileEnd(r);let a=(s-o)/i;return console.log(`ran ${i} change detection cycles`),console.log(`${a.toFixed(2)} ms per check`),new Jm(a,i)}},yk="profiler";function cY(t){return vk(yk,new eg(t)),t}var ia=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:function(r){let o=null;return r?o=new(r||t):o=w(bk),o},providedIn:"root"})}return t})(),bk=(()=>{class t extends ia{_doc;constructor(e){super(),this._doc=e}sanitize(e,r){if(r==null)return null;switch(e){case J.NONE:return r;case J.HTML:return bn(r,"HTML")?vt(r):Sl(this._doc,String(r)).toString();case J.STYLE:return bn(r,"Style")?vt(r):r;case J.SCRIPT:if(bn(r,"Script"))return vt(r);throw new b(5200,!1);case J.URL:return bn(r,"URL")?vt(r):Ps(String(r));case J.RESOURCE_URL:if(bn(r,"ResourceURL"))return vt(r);throw new b(5201,!1);default:throw new b(5202,!1)}}bypassSecurityTrustHtml(e){return yp(e)}bypassSecurityTrustStyle(e){return bp(e)}bypassSecurityTrustScript(e){return _p(e)}bypassSecurityTrustUrl(e){return Dp(e)}bypassSecurityTrustResourceUrl(e){return Ep(e)}static \u0275fac=function(r){return new(r||t)(w(O))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var oC=(()=>{class t{_renderer;_elementRef;onChange=e=>{};onTouched=()=>{};constructor(e,r){this._renderer=e,this._elementRef=r}setProperty(e,r){this._renderer.setProperty(this._elementRef.nativeElement,e,r)}registerOnTouched(e){this.onTouched=e}registerOnChange(e){this.onChange=e}setDisabledState(e){this.setProperty("disabled",e)}static \u0275fac=function(r){return new(r||t)(E(Le),E($))};static \u0275dir=T({type:t})}return t})(),Fu=(()=>{class t extends oC{static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275dir=T({type:t,features:[ie]})}return t})(),fo=new y("");var _k={provide:fo,useExisting:Ie(()=>iC),multi:!0};function Dk(){let t=bt()?bt().getUserAgent():"";return/android (\d+)/.test(t.toLowerCase())}var Ek=new y(""),iC=(()=>{class t extends oC{_compositionMode;_composing=!1;constructor(e,r,o){super(e,r),this._compositionMode=o,this._compositionMode==null&&(this._compositionMode=!Dk())}writeValue(e){let r=e??"";this.setProperty("value",r)}_handleInput(e){(!this._compositionMode||this._compositionMode&&!this._composing)&&this.onChange(e)}_compositionStart(){this._composing=!0}_compositionEnd(e){this._composing=!1,this._compositionMode&&this.onChange(e)}static \u0275fac=function(r){return new(r||t)(E(Le),E($),E(Ek,8))};static \u0275dir=T({type:t,selectors:[["input","formControlName","",3,"type","checkbox"],["textarea","formControlName",""],["input","formControl","",3,"type","checkbox"],["textarea","formControl",""],["input","ngModel","",3,"type","checkbox"],["textarea","ngModel",""],["","ngDefaultControl",""]],hostBindings:function(r,o){r&1&&Qt("input",function(s){return o._handleInput(s.target.value)})("blur",function(){return o.onTouched()})("compositionstart",function(){return o._compositionStart()})("compositionend",function(s){return o._compositionEnd(s.target.value)})},standalone:!1,features:[ze([_k]),ie]})}return t})();function og(t){return t==null||ig(t)===0}function ig(t){return t==null?null:Array.isArray(t)||typeof t=="string"?t.length:t instanceof Set?t.size:null}var Cn=new y(""),ho=new y(""),wk=/^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,Zw=class{static min(n){return sC(n)}static max(n){return aC(n)}static required(n){return cC(n)}static requiredTrue(n){return Ck(n)}static email(n){return Ik(n)}static minLength(n){return Sk(n)}static maxLength(n){return Mk(n)}static pattern(n){return Tk(n)}static nullValidator(n){return Su()}static compose(n){return pC(n)}static composeAsync(n){return mC(n)}};function sC(t){return n=>{if(n.value==null||t==null)return null;let e=parseFloat(n.value);return!isNaN(e)&&e{if(n.value==null||t==null)return null;let e=parseFloat(n.value);return!isNaN(e)&&e>t?{max:{max:t,actual:n.value}}:null}}function cC(t){return og(t.value)?{required:!0}:null}function Ck(t){return t.value===!0?null:{required:!0}}function Ik(t){return og(t.value)||wk.test(t.value)?null:{email:!0}}function Sk(t){return n=>{let e=n.value?.length??ig(n.value);return e===null||e===0?null:e{let e=n.value?.length??ig(n.value);return e!==null&&e>t?{maxlength:{requiredLength:t,actualLength:e}}:null}}function Tk(t){if(!t)return Su;let n,e;return typeof t=="string"?(e="",t.charAt(0)!=="^"&&(e+="^"),e+=t,t.charAt(t.length-1)!=="$"&&(e+="$"),n=new RegExp(e)):(e=t.toString(),n=t),r=>{if(og(r.value))return null;let o=r.value;return n.test(o)?null:{pattern:{requiredPattern:e,actualValue:o}}}}function Su(t){return null}function lC(t){return t!=null}function uC(t){return zn(t)?ae(t):t}function dC(t){let n={};return t.forEach(e=>{n=e!=null?g(g({},n),e):n}),Object.keys(n).length===0?null:n}function fC(t,n){return n.map(e=>e(t))}function xk(t){return!t.validate}function hC(t){return t.map(n=>xk(n)?n:e=>n.validate(e))}function pC(t){if(!t)return null;let n=t.filter(lC);return n.length==0?null:function(e){return dC(fC(e,n))}}function sg(t){return t!=null?pC(hC(t)):null}function mC(t){if(!t)return null;let n=t.filter(lC);return n.length==0?null:function(e){let r=fC(e,n).map(uC);return Yi(r).pipe(V(dC))}}function ag(t){return t!=null?mC(hC(t)):null}function Kw(t,n){return t===null?[n]:Array.isArray(t)?[...t,n]:[t,n]}function gC(t){return t._rawValidators}function vC(t){return t._rawAsyncValidators}function tg(t){return t?Array.isArray(t)?t:[t]:[]}function Mu(t,n){return Array.isArray(t)?t.includes(n):t===n}function Qw(t,n){let e=tg(n);return tg(t).forEach(o=>{Mu(e,o)||e.push(o)}),e}function Xw(t,n){return tg(n).filter(e=>!Mu(t,e))}var Tu=class{get value(){return this.control?this.control.value:null}get valid(){return this.control?this.control.valid:null}get invalid(){return this.control?this.control.invalid:null}get pending(){return this.control?this.control.pending:null}get disabled(){return this.control?this.control.disabled:null}get enabled(){return this.control?this.control.enabled:null}get errors(){return this.control?this.control.errors:null}get pristine(){return this.control?this.control.pristine:null}get dirty(){return this.control?this.control.dirty:null}get touched(){return this.control?this.control.touched:null}get status(){return this.control?this.control.status:null}get untouched(){return this.control?this.control.untouched:null}get statusChanges(){return this.control?this.control.statusChanges:null}get valueChanges(){return this.control?this.control.valueChanges:null}get path(){return null}_composedValidatorFn;_composedAsyncValidatorFn;_rawValidators=[];_rawAsyncValidators=[];_setValidators(n){this._rawValidators=n||[],this._composedValidatorFn=sg(this._rawValidators)}_setAsyncValidators(n){this._rawAsyncValidators=n||[],this._composedAsyncValidatorFn=ag(this._rawAsyncValidators)}get validator(){return this._composedValidatorFn||null}get asyncValidator(){return this._composedAsyncValidatorFn||null}_onDestroyCallbacks=[];_registerOnDestroy(n){this._onDestroyCallbacks.push(n)}_invokeOnDestroyCallbacks(){this._onDestroyCallbacks.forEach(n=>n()),this._onDestroyCallbacks=[]}reset(n=void 0){this.control?.reset(n)}hasError(n,e){return this.control?this.control.hasError(n,e):!1}getError(n,e){return this.control?this.control.getError(n,e):null}},rt=class extends Tu{name;get formDirective(){return null}get path(){return null}},Zn=class extends Tu{_parent=null;name=null;valueAccessor=null},xu=class{_cd;constructor(n){this._cd=n}get isTouched(){return this._cd?.control?._touched?.(),!!this._cd?.control?.touched}get isUntouched(){return!!this._cd?.control?.untouched}get isPristine(){return this._cd?.control?._pristine?.(),!!this._cd?.control?.pristine}get isDirty(){return!!this._cd?.control?.dirty}get isValid(){return this._cd?.control?._status?.(),!!this._cd?.control?.valid}get isInvalid(){return!!this._cd?.control?.invalid}get isPending(){return!!this._cd?.control?.pending}get isSubmitted(){return this._cd?._submitted?.(),!!this._cd?.submitted}};var IY=(()=>{class t extends xu{constructor(e){super(e)}static \u0275fac=function(r){return new(r||t)(E(Zn,2))};static \u0275dir=T({type:t,selectors:[["","formControlName",""],["","ngModel",""],["","formControl",""]],hostVars:14,hostBindings:function(r,o){r&2&&Xe("ng-untouched",o.isUntouched)("ng-touched",o.isTouched)("ng-pristine",o.isPristine)("ng-dirty",o.isDirty)("ng-valid",o.isValid)("ng-invalid",o.isInvalid)("ng-pending",o.isPending)},standalone:!1,features:[ie]})}return t})(),SY=(()=>{class t extends xu{constructor(e){super(e)}static \u0275fac=function(r){return new(r||t)(E(rt,10))};static \u0275dir=T({type:t,selectors:[["","formGroupName",""],["","formArrayName",""],["","ngModelGroup",""],["","formGroup",""],["","formArray",""],["form",3,"ngNoForm",""],["","ngForm",""]],hostVars:16,hostBindings:function(r,o){r&2&&Xe("ng-untouched",o.isUntouched)("ng-touched",o.isTouched)("ng-pristine",o.isPristine)("ng-dirty",o.isDirty)("ng-valid",o.isValid)("ng-invalid",o.isInvalid)("ng-pending",o.isPending)("ng-submitted",o.isSubmitted)},standalone:!1,features:[ie]})}return t})();var sa="VALID",Iu="INVALID",mi="PENDING",aa="DISABLED",Er=class{},Au=class extends Er{value;source;constructor(n,e){super(),this.value=n,this.source=e}},la=class extends Er{pristine;source;constructor(n,e){super(),this.pristine=n,this.source=e}},ua=class extends Er{touched;source;constructor(n,e){super(),this.touched=n,this.source=e}},gi=class extends Er{status;source;constructor(n,e){super(),this.status=n,this.source=e}},Ru=class extends Er{source;constructor(n){super(),this.source=n}},da=class extends Er{source;constructor(n){super(),this.source=n}};function cg(t){return(Pu(t)?t.validators:t)||null}function Ak(t){return Array.isArray(t)?sg(t):t||null}function lg(t,n){return(Pu(n)?n.asyncValidators:t)||null}function Rk(t){return Array.isArray(t)?ag(t):t||null}function Pu(t){return t!=null&&!Array.isArray(t)&&typeof t=="object"}function yC(t,n,e){let r=t.controls;if(!(n?Object.keys(r):r).length)throw new b(1e3,"");if(!r[e])throw new b(1001,"")}function bC(t,n,e){t._forEachChild((r,o)=>{if(e[o]===void 0)throw new b(-1002,"")})}var yi=class{_pendingDirty=!1;_hasOwnPendingAsyncValidator=null;_pendingTouched=!1;_onCollectionChange=()=>{};_updateOn;_parent=null;_asyncValidationSubscription;_composedValidatorFn;_composedAsyncValidatorFn;_rawValidators;_rawAsyncValidators;value;constructor(n,e){this._assignValidators(n),this._assignAsyncValidators(e)}get validator(){return this._composedValidatorFn}set validator(n){this._rawValidators=this._composedValidatorFn=n}get asyncValidator(){return this._composedAsyncValidatorFn}set asyncValidator(n){this._rawAsyncValidators=this._composedAsyncValidatorFn=n}get parent(){return this._parent}get status(){return Y(this.statusReactive)}set status(n){Y(()=>this.statusReactive.set(n))}_status=Xt(()=>this.statusReactive());statusReactive=K(void 0);get valid(){return this.status===sa}get invalid(){return this.status===Iu}get pending(){return this.status===mi}get disabled(){return this.status===aa}get enabled(){return this.status!==aa}errors;get pristine(){return Y(this.pristineReactive)}set pristine(n){Y(()=>this.pristineReactive.set(n))}_pristine=Xt(()=>this.pristineReactive());pristineReactive=K(!0);get dirty(){return!this.pristine}get touched(){return Y(this.touchedReactive)}set touched(n){Y(()=>this.touchedReactive.set(n))}_touched=Xt(()=>this.touchedReactive());touchedReactive=K(!1);get untouched(){return!this.touched}_events=new x;events=this._events.asObservable();valueChanges;statusChanges;get updateOn(){return this._updateOn?this._updateOn:this.parent?this.parent.updateOn:"change"}setValidators(n){this._assignValidators(n)}setAsyncValidators(n){this._assignAsyncValidators(n)}addValidators(n){this.setValidators(Qw(n,this._rawValidators))}addAsyncValidators(n){this.setAsyncValidators(Qw(n,this._rawAsyncValidators))}removeValidators(n){this.setValidators(Xw(n,this._rawValidators))}removeAsyncValidators(n){this.setAsyncValidators(Xw(n,this._rawAsyncValidators))}hasValidator(n){return Mu(this._rawValidators,n)}hasAsyncValidator(n){return Mu(this._rawAsyncValidators,n)}clearValidators(){this.validator=null}clearAsyncValidators(){this.asyncValidator=null}markAsTouched(n={}){let e=this.touched===!1;this.touched=!0;let r=n.sourceControl??this;n.onlySelf||this._parent?.markAsTouched(F(g({},n),{sourceControl:r})),e&&n.emitEvent!==!1&&this._events.next(new ua(!0,r))}markAllAsDirty(n={}){this.markAsDirty({onlySelf:!0,emitEvent:n.emitEvent,sourceControl:this}),this._forEachChild(e=>e.markAllAsDirty(n))}markAllAsTouched(n={}){this.markAsTouched({onlySelf:!0,emitEvent:n.emitEvent,sourceControl:this}),this._forEachChild(e=>e.markAllAsTouched(n))}markAsUntouched(n={}){let e=this.touched===!0;this.touched=!1,this._pendingTouched=!1;let r=n.sourceControl??this;this._forEachChild(o=>{o.markAsUntouched({onlySelf:!0,emitEvent:n.emitEvent,sourceControl:r})}),n.onlySelf||this._parent?._updateTouched(n,r),e&&n.emitEvent!==!1&&this._events.next(new ua(!1,r))}markAsDirty(n={}){let e=this.pristine===!0;this.pristine=!1;let r=n.sourceControl??this;n.onlySelf||this._parent?.markAsDirty(F(g({},n),{sourceControl:r})),e&&n.emitEvent!==!1&&this._events.next(new la(!1,r))}markAsPristine(n={}){let e=this.pristine===!1;this.pristine=!0,this._pendingDirty=!1;let r=n.sourceControl??this;this._forEachChild(o=>{o.markAsPristine({onlySelf:!0,emitEvent:n.emitEvent})}),n.onlySelf||this._parent?._updatePristine(n,r),e&&n.emitEvent!==!1&&this._events.next(new la(!0,r))}markAsPending(n={}){this.status=mi;let e=n.sourceControl??this;n.emitEvent!==!1&&(this._events.next(new gi(this.status,e)),this.statusChanges.emit(this.status)),n.onlySelf||this._parent?.markAsPending(F(g({},n),{sourceControl:e}))}disable(n={}){let e=this._parentMarkedDirty(n.onlySelf);this.status=aa,this.errors=null,this._forEachChild(o=>{o.disable(F(g({},n),{onlySelf:!0}))}),this._updateValue();let r=n.sourceControl??this;n.emitEvent!==!1&&(this._events.next(new Au(this.value,r)),this._events.next(new gi(this.status,r)),this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._updateAncestors(F(g({},n),{skipPristineCheck:e}),this),this._onDisabledChange.forEach(o=>o(!0))}enable(n={}){let e=this._parentMarkedDirty(n.onlySelf);this.status=sa,this._forEachChild(r=>{r.enable(F(g({},n),{onlySelf:!0}))}),this.updateValueAndValidity({onlySelf:!0,emitEvent:n.emitEvent}),this._updateAncestors(F(g({},n),{skipPristineCheck:e}),this),this._onDisabledChange.forEach(r=>r(!1))}_updateAncestors(n,e){n.onlySelf||(this._parent?.updateValueAndValidity(n),n.skipPristineCheck||this._parent?._updatePristine({},e),this._parent?._updateTouched({},e))}setParent(n){this._parent=n}getRawValue(){return this.value}updateValueAndValidity(n={}){if(this._setInitialStatus(),this._updateValue(),this.enabled){let r=this._cancelExistingSubscription();this.errors=this._runValidator(),this.status=this._calculateStatus(),(this.status===sa||this.status===mi)&&this._runAsyncValidator(r,n.emitEvent)}let e=n.sourceControl??this;n.emitEvent!==!1&&(this._events.next(new Au(this.value,e)),this._events.next(new gi(this.status,e)),this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),n.onlySelf||this._parent?.updateValueAndValidity(F(g({},n),{sourceControl:e}))}_updateTreeValidity(n={emitEvent:!0}){this._forEachChild(e=>e._updateTreeValidity(n)),this.updateValueAndValidity({onlySelf:!0,emitEvent:n.emitEvent})}_setInitialStatus(){this.status=this._allControlsDisabled()?aa:sa}_runValidator(){return this.validator?this.validator(this):null}_runAsyncValidator(n,e){if(this.asyncValidator){this.status=mi,this._hasOwnPendingAsyncValidator={emitEvent:e!==!1,shouldHaveEmitted:n!==!1};let r=uC(this.asyncValidator(this));this._asyncValidationSubscription=r.subscribe(o=>{this._hasOwnPendingAsyncValidator=null,this.setErrors(o,{emitEvent:e,shouldHaveEmitted:n})})}}_cancelExistingSubscription(){if(this._asyncValidationSubscription){this._asyncValidationSubscription.unsubscribe();let n=(this._hasOwnPendingAsyncValidator?.emitEvent||this._hasOwnPendingAsyncValidator?.shouldHaveEmitted)??!1;return this._hasOwnPendingAsyncValidator=null,n}return!1}setErrors(n,e={}){this.errors=n,this._updateControlsErrors(e.emitEvent!==!1,this,e.shouldHaveEmitted)}get(n){let e=n;return e==null||(Array.isArray(e)||(e=e.split(".")),e.length===0)?null:e.reduce((r,o)=>r&&r._find(o),this)}getError(n,e){let r=e?this.get(e):this;return r?.errors?r.errors[n]:null}hasError(n,e){return!!this.getError(n,e)}get root(){let n=this;for(;n._parent;)n=n._parent;return n}_updateControlsErrors(n,e,r){this.status=this._calculateStatus(),n&&this.statusChanges.emit(this.status),(n||r)&&this._events.next(new gi(this.status,e)),this._parent&&this._parent._updateControlsErrors(n,e,r)}_initObservables(){this.valueChanges=new H,this.statusChanges=new H}_calculateStatus(){return this._allControlsDisabled()?aa:this.errors?Iu:this._hasOwnPendingAsyncValidator||this._anyControlsHaveStatus(mi)?mi:this._anyControlsHaveStatus(Iu)?Iu:sa}_anyControlsHaveStatus(n){return this._anyControls(e=>e.status===n)}_anyControlsDirty(){return this._anyControls(n=>n.dirty)}_anyControlsTouched(){return this._anyControls(n=>n.touched)}_updatePristine(n,e){let r=!this._anyControlsDirty(),o=this.pristine!==r;this.pristine=r,n.onlySelf||this._parent?._updatePristine(n,e),o&&this._events.next(new la(this.pristine,e))}_updateTouched(n={},e){this.touched=this._anyControlsTouched(),this._events.next(new ua(this.touched,e)),n.onlySelf||this._parent?._updateTouched(n,e)}_onDisabledChange=[];_registerOnCollectionChange(n){this._onCollectionChange=n}_setUpdateStrategy(n){Pu(n)&&n.updateOn!=null&&(this._updateOn=n.updateOn)}_parentMarkedDirty(n){return!n&&!!this._parent?.dirty&&!this._parent._anyControlsDirty()}_find(n){return null}_assignValidators(n){this._rawValidators=Array.isArray(n)?n.slice():n,this._composedValidatorFn=Ak(this._rawValidators)}_assignAsyncValidators(n){this._rawAsyncValidators=Array.isArray(n)?n.slice():n,this._composedAsyncValidatorFn=Rk(this._rawAsyncValidators)}},uo=class extends yi{constructor(n,e,r){super(cg(e),lg(r,e)),this.controls=n,this._initObservables(),this._setUpdateStrategy(e),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator})}controls;registerControl(n,e){return this.controls[n]?this.controls[n]:(this.controls[n]=e,e.setParent(this),e._registerOnCollectionChange(this._onCollectionChange),e)}addControl(n,e,r={}){this.registerControl(n,e),this.updateValueAndValidity({emitEvent:r.emitEvent}),this._onCollectionChange()}removeControl(n,e={}){this.controls[n]&&this.controls[n]._registerOnCollectionChange(()=>{}),delete this.controls[n],this.updateValueAndValidity({emitEvent:e.emitEvent}),this._onCollectionChange()}setControl(n,e,r={}){this.controls[n]&&this.controls[n]._registerOnCollectionChange(()=>{}),delete this.controls[n],e&&this.registerControl(n,e),this.updateValueAndValidity({emitEvent:r.emitEvent}),this._onCollectionChange()}contains(n){return this.controls.hasOwnProperty(n)&&this.controls[n].enabled}setValue(n,e={}){bC(this,!0,n),Object.keys(n).forEach(r=>{yC(this,!0,r),this.controls[r].setValue(n[r],{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e)}patchValue(n,e={}){n!=null&&(Object.keys(n).forEach(r=>{let o=this.controls[r];o&&o.patchValue(n[r],{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e))}reset(n={},e={}){this._forEachChild((r,o)=>{r.reset(n?n[o]:null,F(g({},e),{onlySelf:!0}))}),this._updatePristine(e,this),this._updateTouched(e,this),this.updateValueAndValidity(e),e?.emitEvent!==!1&&this._events.next(new da(this))}getRawValue(){return this._reduceChildren({},(n,e,r)=>(n[r]=e.getRawValue(),n))}_syncPendingControls(){let n=this._reduceChildren(!1,(e,r)=>r._syncPendingControls()?!0:e);return n&&this.updateValueAndValidity({onlySelf:!0}),n}_forEachChild(n){Object.keys(this.controls).forEach(e=>{let r=this.controls[e];r&&n(r,e)})}_setUpControls(){this._forEachChild(n=>{n.setParent(this),n._registerOnCollectionChange(this._onCollectionChange)})}_updateValue(){this.value=this._reduceValue()}_anyControls(n){for(let[e,r]of Object.entries(this.controls))if(this.contains(e)&&n(r))return!0;return!1}_reduceValue(){let n={};return this._reduceChildren(n,(e,r,o)=>((r.enabled||this.disabled)&&(e[o]=r.value),e))}_reduceChildren(n,e){let r=n;return this._forEachChild((o,i)=>{r=e(r,o,i)}),r}_allControlsDisabled(){for(let n of Object.keys(this.controls))if(this.controls[n].enabled)return!1;return Object.keys(this.controls).length>0||this.disabled}_find(n){return this.controls.hasOwnProperty(n)?this.controls[n]:null}};var MY=uo;var ng=class extends uo{};var bi=new y("",{factory:()=>Lu}),Lu="always";function ju(t,n){return[...n.path,t]}function fa(t,n,e=Lu){ug(t,n),n.valueAccessor.writeValue(t.value),(t.disabled||e==="always")&&n.valueAccessor.setDisabledState?.(t.disabled),Ok(t,n),Fk(t,n),kk(t,n),Nk(t,n)}function Nu(t,n,e=!0){let r=()=>{};n?.valueAccessor?.registerOnChange(r),n?.valueAccessor?.registerOnTouched(r),ku(t,n),t&&(n._invokeOnDestroyCallbacks(),t._registerOnCollectionChange(()=>{}))}function Ou(t,n){t.forEach(e=>{e.registerOnValidatorChange&&e.registerOnValidatorChange(n)})}function Nk(t,n){if(n.valueAccessor.setDisabledState){let e=r=>{n.valueAccessor.setDisabledState(r)};t.registerOnDisabledChange(e),n._registerOnDestroy(()=>{t._unregisterOnDisabledChange(e)})}}function ug(t,n){let e=gC(t);n.validator!==null?t.setValidators(Kw(e,n.validator)):typeof e=="function"&&t.setValidators([e]);let r=vC(t);n.asyncValidator!==null?t.setAsyncValidators(Kw(r,n.asyncValidator)):typeof r=="function"&&t.setAsyncValidators([r]);let o=()=>t.updateValueAndValidity();Ou(n._rawValidators,o),Ou(n._rawAsyncValidators,o)}function ku(t,n){let e=!1;if(t!==null){if(n.validator!==null){let o=gC(t);if(Array.isArray(o)&&o.length>0){let i=o.filter(s=>s!==n.validator);i.length!==o.length&&(e=!0,t.setValidators(i))}}if(n.asyncValidator!==null){let o=vC(t);if(Array.isArray(o)&&o.length>0){let i=o.filter(s=>s!==n.asyncValidator);i.length!==o.length&&(e=!0,t.setAsyncValidators(i))}}}let r=()=>{};return Ou(n._rawValidators,r),Ou(n._rawAsyncValidators,r),e}function Ok(t,n){n.valueAccessor.registerOnChange(e=>{t._pendingValue=e,t._pendingChange=!0,t._pendingDirty=!0,t.updateOn==="change"&&_C(t,n)})}function kk(t,n){n.valueAccessor.registerOnTouched(()=>{t._pendingTouched=!0,t.updateOn==="blur"&&t._pendingChange&&_C(t,n),t.updateOn!=="submit"&&t.markAsTouched()})}function _C(t,n){t._pendingDirty&&t.markAsDirty(),t.setValue(t._pendingValue,{emitModelToViewChange:!1}),n.viewToModelUpdate(t._pendingValue),t._pendingChange=!1}function Fk(t,n){let e=(r,o)=>{n.valueAccessor.writeValue(r),o&&n.viewToModelUpdate(r)};t.registerOnChange(e),n._registerOnDestroy(()=>{t._unregisterOnChange(e)})}function DC(t,n){t==null,ug(t,n)}function Pk(t,n){return ku(t,n)}function dg(t,n){if(!t.hasOwnProperty("model"))return!1;let e=t.model;return e.isFirstChange()?!0:!Object.is(n,e.currentValue)}function Lk(t){return Object.getPrototypeOf(t.constructor)===Fu}function EC(t,n){t._syncPendingControls(),n.forEach(e=>{let r=e.control;r.updateOn==="submit"&&r._pendingChange&&(e.viewToModelUpdate(r._pendingValue),r._pendingChange=!1)})}function fg(t,n){if(!n)return null;Array.isArray(n);let e,r,o;return n.forEach(i=>{i.constructor===iC?e=i:Lk(i)?r=i:o=i}),o||r||e||null}function jk(t,n){let e=t.indexOf(n);e>-1&&t.splice(e,1)}var Vk={provide:rt,useExisting:Ie(()=>Bk)},ca=Promise.resolve(),Bk=(()=>{class t extends rt{callSetDisabledState;get submitted(){return Y(this.submittedReactive)}_submitted=Xt(()=>this.submittedReactive());submittedReactive=K(!1);_directives=new Set;form;ngSubmit=new H;options;constructor(e,r,o){super(),this.callSetDisabledState=o,this.form=new uo({},sg(e),ag(r))}ngAfterViewInit(){this._setUpdateStrategy()}get formDirective(){return this}get control(){return this.form}get path(){return[]}get controls(){return this.form.controls}addControl(e){ca.then(()=>{let r=this._findContainer(e.path);e.control=r.registerControl(e.name,e.control),fa(e.control,e,this.callSetDisabledState),e.control.updateValueAndValidity({emitEvent:!1}),this._directives.add(e)})}getControl(e){return this.form.get(e.path)}removeControl(e){ca.then(()=>{this._findContainer(e.path)?.removeControl(e.name),this._directives.delete(e)})}addFormGroup(e){ca.then(()=>{let r=this._findContainer(e.path),o=new uo({});DC(o,e),r.registerControl(e.name,o),o.updateValueAndValidity({emitEvent:!1})})}removeFormGroup(e){ca.then(()=>{this._findContainer(e.path)?.removeControl?.(e.name)})}getFormGroup(e){return this.form.get(e.path)}updateModel(e,r){ca.then(()=>{this.form.get(e.path).setValue(r)})}setValue(e){this.control.setValue(e)}onSubmit(e){return this.submittedReactive.set(!0),EC(this.form,this._directives),this.ngSubmit.emit(e),this.form._events.next(new Ru(this.control)),e?.target?.method==="dialog"}onReset(){this.resetForm()}resetForm(e=void 0){this.form.reset(e),this.submittedReactive.set(!1)}_setUpdateStrategy(){this.options&&this.options.updateOn!=null&&(this.form._updateOn=this.options.updateOn)}_findContainer(e){return e.pop(),e.length?this.form.get(e):this.form}static \u0275fac=function(r){return new(r||t)(E(Cn,10),E(ho,10),E(bi,8))};static \u0275dir=T({type:t,selectors:[["form",3,"ngNoForm","",3,"formGroup","",3,"formArray",""],["ng-form"],["","ngForm",""]],hostBindings:function(r,o){r&1&&Qt("submit",function(s){return o.onSubmit(s)})("reset",function(){return o.onReset()})},inputs:{options:[0,"ngFormOptions","options"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],standalone:!1,features:[ze([Vk]),ie]})}return t})();function Jw(t,n){let e=t.indexOf(n);e>-1&&t.splice(e,1)}function eC(t){return typeof t=="object"&&t!==null&&Object.keys(t).length===2&&"value"in t&&"disabled"in t}var vi=class extends yi{defaultValue=null;_onChange=[];_pendingValue;_pendingChange=!1;constructor(n=null,e,r){super(cg(e),lg(r,e)),this._applyFormState(n),this._setUpdateStrategy(e),this._initObservables(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator}),Pu(e)&&(e.nonNullable||e.initialValueIsDefault)&&(eC(n)?this.defaultValue=n.value:this.defaultValue=n)}setValue(n,e={}){this.value=this._pendingValue=n,this._onChange.length&&e.emitModelToViewChange!==!1&&this._onChange.forEach(r=>r(this.value,e.emitViewToModelChange!==!1)),this.updateValueAndValidity(e)}patchValue(n,e={}){this.setValue(n,e)}reset(n=this.defaultValue,e={}){this._applyFormState(n),this.markAsPristine(e),this.markAsUntouched(e),this.setValue(this.value,e),e.overwriteDefaultValue&&(this.defaultValue=this.value),this._pendingChange=!1,e?.emitEvent!==!1&&this._events.next(new da(this))}_updateValue(){}_anyControls(n){return!1}_allControlsDisabled(){return this.disabled}registerOnChange(n){this._onChange.push(n)}_unregisterOnChange(n){Jw(this._onChange,n)}registerOnDisabledChange(n){this._onDisabledChange.push(n)}_unregisterOnDisabledChange(n){Jw(this._onDisabledChange,n)}_forEachChild(n){}_syncPendingControls(){return this.updateOn==="submit"&&(this._pendingDirty&&this.markAsDirty(),this._pendingTouched&&this.markAsTouched(),this._pendingChange)?(this.setValue(this._pendingValue,{onlySelf:!0,emitModelToViewChange:!1}),!0):!1}_applyFormState(n){eC(n)?(this.value=this._pendingValue=n.value,n.disabled?this.disable({onlySelf:!0,emitEvent:!1}):this.enable({onlySelf:!0,emitEvent:!1})):this.value=this._pendingValue=n}},xY=vi,Uk=t=>t instanceof vi,Hk=(()=>{class t extends rt{_parent;ngOnInit(){this._checkParentType(),this.formDirective.addFormGroup(this)}ngOnDestroy(){this.formDirective?.removeFormGroup(this)}get control(){return this.formDirective.getFormGroup(this)}get path(){return ju(this.name==null?this.name:this.name.toString(),this._parent)}get formDirective(){return this._parent?this._parent.formDirective:null}_checkParentType(){}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275dir=T({type:t,standalone:!1,features:[ie]})}return t})();var $k={provide:Zn,useExisting:Ie(()=>zk)},tC=Promise.resolve(),zk=(()=>{class t extends Zn{_changeDetectorRef;callSetDisabledState;control=new vi;static ngAcceptInputType_isDisabled;_registered=!1;viewModel;name="";isDisabled;model;options;update=new H;constructor(e,r,o,i,s,a){super(),this._changeDetectorRef=s,this.callSetDisabledState=a,this._parent=e,this._setValidators(r),this._setAsyncValidators(o),this.valueAccessor=fg(this,i)}ngOnChanges(e){if(this._checkForErrors(),!this._registered||"name"in e){if(this._registered&&(this._checkName(),this.formDirective)){let r=e.name.previousValue;this.formDirective.removeControl({name:r,path:this._getPath(r)})}this._setUpControl()}"isDisabled"in e&&this._updateDisabled(e),dg(e,this.viewModel)&&(this._updateValue(this.model),this.viewModel=this.model)}ngOnDestroy(){this.formDirective?.removeControl(this)}get path(){return this._getPath(this.name)}get formDirective(){return this._parent?this._parent.formDirective:null}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}_setUpControl(){this._setUpdateStrategy(),this._isStandalone()?this._setUpStandalone():this.formDirective.addControl(this),this._registered=!0}_setUpdateStrategy(){this.options&&this.options.updateOn!=null&&(this.control._updateOn=this.options.updateOn)}_isStandalone(){return!this._parent||!!(this.options&&this.options.standalone)}_setUpStandalone(){fa(this.control,this,this.callSetDisabledState),this.control.updateValueAndValidity({emitEvent:!1})}_checkForErrors(){this._checkName()}_checkName(){this.options&&this.options.name&&(this.name=this.options.name),!this._isStandalone()&&this.name}_updateValue(e){tC.then(()=>{this.control.setValue(e,{emitViewToModelChange:!1}),this._changeDetectorRef?.markForCheck()})}_updateDisabled(e){let r=e.isDisabled.currentValue,o=r!==0&&ue(r);tC.then(()=>{o&&!this.control.disabled?this.control.disable():!o&&this.control.disabled&&this.control.enable(),this._changeDetectorRef?.markForCheck()})}_getPath(e){return this._parent?ju(e,this._parent):[e]}static \u0275fac=function(r){return new(r||t)(E(rt,9),E(Cn,10),E(ho,10),E(fo,10),E(Jt,8),E(bi,8))};static \u0275dir=T({type:t,selectors:[["","ngModel","",3,"formControlName","",3,"formControl",""]],inputs:{name:"name",isDisabled:[0,"disabled","isDisabled"],model:[0,"ngModel","model"],options:[0,"ngModelOptions","options"]},outputs:{update:"ngModelChange"},exportAs:["ngModel"],standalone:!1,features:[ze([$k]),ie,$e]})}return t})();var AY=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["form",3,"ngNoForm","",3,"ngNativeValidate",""]],hostAttrs:["novalidate",""],standalone:!1})}return t})(),Gk={provide:fo,useExisting:Ie(()=>Wk),multi:!0},Wk=(()=>{class t extends Fu{writeValue(e){let r=e??"";this.setProperty("value",r)}registerOnChange(e){this.onChange=r=>{e(r==""?null:parseFloat(r))}}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275dir=T({type:t,selectors:[["input","type","number","formControlName",""],["input","type","number","formControl",""],["input","type","number","ngModel",""]],hostBindings:function(r,o){r&1&&Qt("input",function(s){return o.onChange(s.target.value)})("blur",function(){return o.onTouched()})},standalone:!1,features:[ze([Gk]),ie]})}return t})();var rg=class extends yi{constructor(n,e,r){super(cg(e),lg(r,e)),this.controls=n,this._initObservables(),this._setUpdateStrategy(e),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator})}controls;at(n){return this.controls[this._adjustIndex(n)]}push(n,e={}){Array.isArray(n)?n.forEach(r=>{this.controls.push(r),this._registerControl(r)}):(this.controls.push(n),this._registerControl(n)),this.updateValueAndValidity({emitEvent:e.emitEvent}),this._onCollectionChange()}insert(n,e,r={}){this.controls.splice(n,0,e),this._registerControl(e),this.updateValueAndValidity({emitEvent:r.emitEvent})}removeAt(n,e={}){let r=this._adjustIndex(n);r<0&&(r=0),this.controls[r]&&this.controls[r]._registerOnCollectionChange(()=>{}),this.controls.splice(r,1),this.updateValueAndValidity({emitEvent:e.emitEvent})}setControl(n,e,r={}){let o=this._adjustIndex(n);o<0&&(o=0),this.controls[o]&&this.controls[o]._registerOnCollectionChange(()=>{}),this.controls.splice(o,1),e&&(this.controls.splice(o,0,e),this._registerControl(e)),this.updateValueAndValidity({emitEvent:r.emitEvent}),this._onCollectionChange()}get length(){return this.controls.length}setValue(n,e={}){bC(this,!1,n),n.forEach((r,o)=>{yC(this,!1,o),this.at(o).setValue(r,{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e)}patchValue(n,e={}){n!=null&&(n.forEach((r,o)=>{this.at(o)&&this.at(o).patchValue(r,{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e))}reset(n=[],e={}){this._forEachChild((r,o)=>{r.reset(n[o],F(g({},e),{onlySelf:!0}))}),this._updatePristine(e,this),this._updateTouched(e,this),this.updateValueAndValidity(e),e?.emitEvent!==!1&&this._events.next(new da(this))}getRawValue(){return this.controls.map(n=>n.getRawValue())}clear(n={}){this.controls.length<1||(this._forEachChild(e=>e._registerOnCollectionChange(()=>{})),this.controls.splice(0),this.updateValueAndValidity({emitEvent:n.emitEvent}))}_adjustIndex(n){return n<0?n+this.length:n}_syncPendingControls(){let n=this.controls.reduce((e,r)=>r._syncPendingControls()?!0:e,!1);return n&&this.updateValueAndValidity({onlySelf:!0}),n}_forEachChild(n){this.controls.forEach((e,r)=>{n(e,r)})}_updateValue(){this.value=this.controls.filter(n=>n.enabled||this.disabled).map(n=>n.value)}_anyControls(n){return this.controls.some(e=>e.enabled&&n(e))}_setUpControls(){this._forEachChild(n=>this._registerControl(n))}_allControlsDisabled(){for(let n of this.controls)if(n.enabled)return!1;return this.controls.length>0||this.disabled}_registerControl(n){n.setParent(this),n._registerOnCollectionChange(this._onCollectionChange)}_find(n){return this.at(n)??null}};var wC=(()=>{class t extends rt{callSetDisabledState;get submitted(){return Y(this._submittedReactive)}set submitted(e){this._submittedReactive.set(e)}_submitted=Xt(()=>this._submittedReactive());_submittedReactive=K(!1);_oldForm;_onCollectionChange=()=>this._updateDomValue();directives=[];constructor(e,r,o){super(),this.callSetDisabledState=o,this._setValidators(e),this._setAsyncValidators(r)}ngOnChanges(e){this.onChanges(e)}ngOnDestroy(){this.onDestroy()}onChanges(e){this._checkFormPresent(),e.hasOwnProperty("form")&&(this._updateValidators(),this._updateDomValue(),this._updateRegistrations(),this._oldForm=this.form)}onDestroy(){this.form&&(ku(this.form,this),this.form._onCollectionChange===this._onCollectionChange&&this.form._registerOnCollectionChange(()=>{}))}get formDirective(){return this}get path(){return[]}addControl(e){let r=this.form.get(e.path);return fa(r,e,this.callSetDisabledState),r.updateValueAndValidity({emitEvent:!1}),this.directives.push(e),r}getControl(e){return this.form.get(e.path)}removeControl(e){Nu(e.control||null,e,!1),jk(this.directives,e)}addFormGroup(e){this._setUpFormContainer(e)}removeFormGroup(e){this._cleanUpFormContainer(e)}getFormGroup(e){return this.form.get(e.path)}getFormArray(e){return this.form.get(e.path)}addFormArray(e){this._setUpFormContainer(e)}removeFormArray(e){this._cleanUpFormContainer(e)}updateModel(e,r){this.form.get(e.path).setValue(r)}onReset(){this.resetForm()}resetForm(e=void 0,r={}){this.form.reset(e,r),this._submittedReactive.set(!1)}onSubmit(e){return this.submitted=!0,EC(this.form,this.directives),this.ngSubmit.emit(e),this.form._events.next(new Ru(this.control)),e?.target?.method==="dialog"}_updateDomValue(){this.directives.forEach(e=>{let r=e.control,o=this.form.get(e.path);r!==o&&(Nu(r||null,e),Uk(o)&&(fa(o,e,this.callSetDisabledState),e.control=o))}),this.form._updateTreeValidity({emitEvent:!1})}_setUpFormContainer(e){let r=this.form.get(e.path);DC(r,e),r.updateValueAndValidity({emitEvent:!1})}_cleanUpFormContainer(e){let r=this.form?.get(e.path);r&&Pk(r,e)&&r.updateValueAndValidity({emitEvent:!1})}_updateRegistrations(){this.form._registerOnCollectionChange(this._onCollectionChange),this._oldForm?._registerOnCollectionChange(()=>{})}_updateValidators(){ug(this.form,this),this._oldForm&&ku(this._oldForm,this)}_checkFormPresent(){this.form}static \u0275fac=function(r){return new(r||t)(E(Cn,10),E(ho,10),E(bi,8))};static \u0275dir=T({type:t,features:[ie,$e]})}return t})();var hg=new y(""),qk={provide:Zn,useExisting:Ie(()=>Yk)},Yk=(()=>{class t extends Zn{_ngModelWarningConfig;callSetDisabledState;viewModel;form;set isDisabled(e){}model;update=new H;static _ngModelWarningSentOnce=!1;_ngModelWarningSent=!1;constructor(e,r,o,i,s){super(),this._ngModelWarningConfig=i,this.callSetDisabledState=s,this._setValidators(e),this._setAsyncValidators(r),this.valueAccessor=fg(this,o)}ngOnChanges(e){if(this._isControlChanged(e)){let r=e.form.previousValue;r&&Nu(r,this,!1),fa(this.form,this,this.callSetDisabledState),this.form.updateValueAndValidity({emitEvent:!1})}dg(e,this.viewModel)&&(this.form.setValue(this.model),this.viewModel=this.model)}ngOnDestroy(){this.form&&Nu(this.form,this,!1)}get path(){return[]}get control(){return this.form}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}_isControlChanged(e){return e.hasOwnProperty("form")}static \u0275fac=function(r){return new(r||t)(E(Cn,10),E(ho,10),E(fo,10),E(hg,8),E(bi,8))};static \u0275dir=T({type:t,selectors:[["","formControl",""]],inputs:{form:[0,"formControl","form"],isDisabled:[0,"disabled","isDisabled"],model:[0,"ngModel","model"]},outputs:{update:"ngModelChange"},exportAs:["ngForm"],standalone:!1,features:[ze([qk]),ie,$e]})}return t})(),Zk={provide:rt,useExisting:Ie(()=>CC)},CC=(()=>{class t extends Hk{name=null;constructor(e,r,o){super(),this._parent=e,this._setValidators(r),this._setAsyncValidators(o)}_checkParentType(){SC(this._parent)}static \u0275fac=function(r){return new(r||t)(E(rt,13),E(Cn,10),E(ho,10))};static \u0275dir=T({type:t,selectors:[["","formGroupName",""]],inputs:{name:[0,"formGroupName","name"]},standalone:!1,features:[ze([Zk]),ie]})}return t})(),Kk={provide:rt,useExisting:Ie(()=>IC)},IC=(()=>{class t extends rt{_parent;name=null;constructor(e,r,o){super(),this._parent=e,this._setValidators(r),this._setAsyncValidators(o)}ngOnInit(){SC(this._parent),this.formDirective.addFormArray(this)}ngOnDestroy(){this.formDirective?.removeFormArray(this)}get control(){return this.formDirective.getFormArray(this)}get formDirective(){return this._parent?this._parent.formDirective:null}get path(){return ju(this.name==null?this.name:this.name.toString(),this._parent)}static \u0275fac=function(r){return new(r||t)(E(rt,13),E(Cn,10),E(ho,10))};static \u0275dir=T({type:t,selectors:[["","formArrayName",""]],inputs:{name:[0,"formArrayName","name"]},standalone:!1,features:[ze([Kk]),ie]})}return t})();function SC(t){return!(t instanceof CC)&&!(t instanceof wC)&&!(t instanceof IC)}var Qk={provide:Zn,useExisting:Ie(()=>Xk)},Xk=(()=>{class t extends Zn{_ngModelWarningConfig;_added=!1;viewModel;control;name=null;set isDisabled(e){}model;update=new H;static _ngModelWarningSentOnce=!1;_ngModelWarningSent=!1;constructor(e,r,o,i,s){super(),this._ngModelWarningConfig=s,this._parent=e,this._setValidators(r),this._setAsyncValidators(o),this.valueAccessor=fg(this,i)}ngOnChanges(e){this._added||this._setUpControl(),dg(e,this.viewModel)&&(this.viewModel=this.model,this.formDirective.updateModel(this,this.model))}ngOnDestroy(){this.formDirective?.removeControl(this)}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}get path(){return ju(this.name==null?this.name:this.name.toString(),this._parent)}get formDirective(){return this._parent?this._parent.formDirective:null}_setUpControl(){this.control=this.formDirective.addControl(this),this._added=!0}static \u0275fac=function(r){return new(r||t)(E(rt,13),E(Cn,10),E(ho,10),E(fo,10),E(hg,8))};static \u0275dir=T({type:t,selectors:[["","formControlName",""]],inputs:{name:[0,"formControlName","name"],isDisabled:[0,"disabled","isDisabled"],model:[0,"ngModel","model"]},outputs:{update:"ngModelChange"},standalone:!1,features:[ze([Qk]),ie,$e]})}return t})();var Jk={provide:rt,useExisting:Ie(()=>eF)},eF=(()=>{class t extends wC{form=null;ngSubmit=new H;get control(){return this.form}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275dir=T({type:t,selectors:[["","formGroup",""]],hostBindings:function(r,o){r&1&&Qt("submit",function(s){return o.onSubmit(s)})("reset",function(){return o.onReset()})},inputs:{form:[0,"formGroup","form"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],standalone:!1,features:[ze([Jk]),ie]})}return t})(),tF={provide:fo,useExisting:Ie(()=>TC),multi:!0};function MC(t,n){return t==null?`${n}`:(n&&typeof n=="object"&&(n="Object"),`${t}: ${n}`.slice(0,50))}function nF(t){return t.split(":")[0]}var TC=(()=>{class t extends Fu{value;_optionMap=new Map;_idCounter=0;set compareWith(e){this._compareWith=e}_compareWith=Object.is;appRefInjector=f(Ge).injector;destroyRef=f(Ue);cdr=f(Jt);_queuedWrite=!1;_writeValueAfterRender(){this._queuedWrite||this.appRefInjector.destroyed||(this._queuedWrite=!0,At({write:()=>{this.destroyRef.destroyed||(this._queuedWrite=!1,this.writeValue(this.value))}},{injector:this.appRefInjector}))}writeValue(e){this.cdr.markForCheck(),this.value=e;let r=this._getOptionId(e),o=MC(r,e);this.setProperty("value",o)}registerOnChange(e){this.onChange=r=>{this.value=this._getOptionValue(r),e(this.value)}}_registerOption(){return(this._idCounter++).toString()}_getOptionId(e){for(let r of this._optionMap.keys())if(this._compareWith(this._optionMap.get(r),e))return r;return null}_getOptionValue(e){let r=nF(e);return this._optionMap.has(r)?this._optionMap.get(r):e}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275dir=T({type:t,selectors:[["select","formControlName","",3,"multiple",""],["select","formControl","",3,"multiple",""],["select","ngModel","",3,"multiple",""]],hostBindings:function(r,o){r&1&&Qt("change",function(s){return o.onChange(s.target.value)})("blur",function(){return o.onTouched()})},inputs:{compareWith:"compareWith"},standalone:!1,features:[ze([tF]),ie]})}return t})(),RY=(()=>{class t{_element;_renderer;_select;id;constructor(e,r,o){this._element=e,this._renderer=r,this._select=o,this._select&&(this.id=this._select._registerOption())}set ngValue(e){this._select!=null&&(this._select._optionMap.set(this.id,e),this._setElementValue(MC(this.id,e)),this._select._writeValueAfterRender())}set value(e){this._setElementValue(e),this._select?._writeValueAfterRender()}_setElementValue(e){this._renderer.setProperty(this._element.nativeElement,"value",e)}ngOnDestroy(){this._select?._optionMap.delete(this.id),this._select?._writeValueAfterRender()}static \u0275fac=function(r){return new(r||t)(E($),E(Le),E(TC,9))};static \u0275dir=T({type:t,selectors:[["option"]],inputs:{ngValue:"ngValue",value:"value"},standalone:!1})}return t})(),rF={provide:fo,useExisting:Ie(()=>xC),multi:!0};function nC(t,n){return t==null?`${n}`:(typeof n=="string"&&(n=`'${n}'`),n&&typeof n=="object"&&(n="Object"),`${t}: ${n}`.slice(0,50))}function oF(t){return t.split(":")[0]}var xC=(()=>{class t extends Fu{value;_optionMap=new Map;_idCounter=0;set compareWith(e){this._compareWith=e}_compareWith=Object.is;writeValue(e){this.value=e;let r;if(Array.isArray(e)){let o=e.map(i=>this._getOptionId(i));r=(i,s)=>{i._setSelected(o.indexOf(s)>-1)}}else r=o=>{o._setSelected(!1)};this._optionMap.forEach(r)}registerOnChange(e){this.onChange=r=>{let o=[],i=r.selectedOptions;if(i!==void 0){let s=i;for(let a=0;a{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275dir=T({type:t,selectors:[["select","multiple","","formControlName",""],["select","multiple","","formControl",""],["select","multiple","","ngModel",""]],hostBindings:function(r,o){r&1&&Qt("change",function(s){return o.onChange(s.target)})("blur",function(){return o.onTouched()})},inputs:{compareWith:"compareWith"},standalone:!1,features:[ze([rF]),ie]})}return t})(),NY=(()=>{class t{_element;_renderer;_select;id;_value;constructor(e,r,o){this._element=e,this._renderer=r,this._select=o,this._select&&(this.id=this._select._registerOption(this))}set ngValue(e){this._select!=null&&(this._value=e,this._setElementValue(nC(this.id,e)),this._select.writeValue(this._select.value))}set value(e){this._select?(this._value=e,this._setElementValue(nC(this.id,e)),this._select.writeValue(this._select.value)):this._setElementValue(e)}_setElementValue(e){this._renderer.setProperty(this._element.nativeElement,"value",e)}_setSelected(e){this._renderer.setProperty(this._element.nativeElement,"selected",e)}ngOnDestroy(){this._select&&(this._select._optionMap.delete(this.id),this._select.writeValue(this._select.value))}static \u0275fac=function(r){return new(r||t)(E($),E(Le),E(xC,9))};static \u0275dir=T({type:t,selectors:[["option"]],inputs:{ngValue:"ngValue",value:"value"},standalone:!1})}return t})();function AC(t){return typeof t=="number"?t:parseFloat(t)}var pg=(()=>{class t{_validator=Su;_onChange;_enabled;ngOnChanges(e){if(this.inputName in e){let r=this.normalizeInput(e[this.inputName].currentValue);this._enabled=this.enabled(r),this._validator=this._enabled?this.createValidator(r):Su,this._onChange?.()}}validate(e){return this._validator(e)}registerOnValidatorChange(e){this._onChange=e}enabled(e){return e!=null}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,features:[$e]})}return t})(),iF={provide:Cn,useExisting:Ie(()=>sF),multi:!0},sF=(()=>{class t extends pg{max;inputName="max";normalizeInput=e=>AC(e);createValidator=e=>aC(e);static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275dir=T({type:t,selectors:[["input","type","number","max","","formControlName",""],["input","type","number","max","","formControl",""],["input","type","number","max","","ngModel",""]],hostVars:1,hostBindings:function(r,o){r&2&&yt("max",o._enabled?o.max:null)},inputs:{max:"max"},standalone:!1,features:[ze([iF]),ie]})}return t})(),aF={provide:Cn,useExisting:Ie(()=>cF),multi:!0},cF=(()=>{class t extends pg{min;inputName="min";normalizeInput=e=>AC(e);createValidator=e=>sC(e);static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275dir=T({type:t,selectors:[["input","type","number","min","","formControlName",""],["input","type","number","min","","formControl",""],["input","type","number","min","","ngModel",""]],hostVars:1,hostBindings:function(r,o){r&2&&yt("min",o._enabled?o.min:null)},inputs:{min:"min"},standalone:!1,features:[ze([aF]),ie]})}return t})(),lF={provide:Cn,useExisting:Ie(()=>uF),multi:!0};var uF=(()=>{class t extends pg{required;inputName="required";normalizeInput=ue;createValidator=e=>cC;enabled(e){return e}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275dir=T({type:t,selectors:[["","required","","formControlName","",3,"type","checkbox"],["","required","","formControl","",3,"type","checkbox"],["","required","","ngModel","",3,"type","checkbox"]],hostVars:1,hostBindings:function(r,o){r&2&&yt("required",o._enabled?"":null)},inputs:{required:"required"},standalone:!1,features:[ze([lF]),ie]})}return t})();var RC=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({})}return t})();function rC(t){return!!t&&(t.asyncValidators!==void 0||t.validators!==void 0||t.updateOn!==void 0)}var dF=(()=>{class t{useNonNullable=!1;get nonNullable(){let e=new t;return e.useNonNullable=!0,e}group(e,r=null){let o=this._reduceControls(e),i={};return rC(r)?i=r:r!==null&&(i.validators=r.validator,i.asyncValidators=r.asyncValidator),new uo(o,i)}record(e,r=null){let o=this._reduceControls(e);return new ng(o,r)}control(e,r,o){let i={};return this.useNonNullable?(rC(r)?i=r:(i.validators=r,i.asyncValidators=o),new vi(e,F(g({},i),{nonNullable:!0}))):new vi(e,r,o)}array(e,r,o){let i=e.map(s=>this._createControl(s));return new rg(i,r,o)}_reduceControls(e){let r={};return Object.keys(e).forEach(o=>{r[o]=this._createControl(e[o])}),r}_createControl(e){if(e instanceof vi)return e;if(e instanceof yi)return e;if(Array.isArray(e)){let r=e[0],o=e.length>1?e[1]:null,i=e.length>2?e[2]:null;return this.control(r,o,i)}else return this.control(e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var OY=(()=>{class t extends dF{group(e,r=null){return super.group(e,r)}control(e,r,o){return super.control(e,r,o)}array(e,r,o){return super.array(e,r,o)}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),kY=(()=>{class t{static withConfig(e){return{ngModule:t,providers:[{provide:bi,useValue:e.callSetDisabledState??Lu}]}}static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({imports:[RC]})}return t})(),FY=(()=>{class t{static withConfig(e){return{ngModule:t,providers:[{provide:hg,useValue:e.warnOnNgModelWithFormControl??"always"},{provide:bi,useValue:e.callSetDisabledState??Lu}]}}static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({imports:[RC]})}return t})();var Vu=new WeakMap,Nt=(()=>{class t{_appRef;_injector=f(W);_environmentInjector=f(re);load(e){let r=this._appRef=this._appRef||this._injector.get(Ge),o=Vu.get(r);o||(o={loaders:new Set,refs:[]},Vu.set(r,o),r.onDestroy(()=>{Vu.get(r)?.refs.forEach(i=>i.destroy()),Vu.delete(r)})),o.loaders.has(e)||(o.loaders.add(e),o.refs.push(Jl(e,{environmentInjector:this._environmentInjector})))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var Uu=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=Te({type:t,selectors:[["ng-component"]],exportAs:["cdkVisuallyHidden"],decls:0,vars:0,template:function(r,o){},styles:[`.cdk-visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; + outline: 0; + -webkit-appearance: none; + -moz-appearance: none; + left: 0; +} +[dir=rtl] .cdk-visually-hidden { + left: auto; + right: 0; +} +`],encapsulation:2,changeDetection:0})}return t})(),Bu;function fF(){if(Bu===void 0&&(Bu=null,typeof window<"u")){let t=window;t.trustedTypes!==void 0&&(Bu=t.trustedTypes.createPolicy("angular#components",{createHTML:n=>n}))}return Bu}function po(t){return fF()?.createHTML(t)||t}function NC(t,n,e){let r=e.sanitize(J.HTML,n);t.innerHTML=po(r||"")}function OC(t){return Error(`Unable to find icon with the name "${t}"`)}function hF(){return Error("Could not find HttpClient for use with Angular Material icons. Please add provideHttpClient() to your providers.")}function kC(t){return Error(`The URL provided to MatIconRegistry was not trusted as a resource URL via Angular's DomSanitizer. Attempted URL was "${t}".`)}function FC(t){return Error(`The literal provided to MatIconRegistry was not trusted as safe HTML by Angular's DomSanitizer. Attempted literal was "${t}".`)}var Kn=class{url;svgText;options;svgElement=null;constructor(n,e,r){this.url=n,this.svgText=e,this.options=r}},LC=(()=>{class t{_httpClient;_sanitizer;_errorHandler;_document;_svgIconConfigs=new Map;_iconSetConfigs=new Map;_cachedIconsByUrl=new Map;_inProgressUrlFetches=new Map;_fontCssClassesByAlias=new Map;_resolvers=[];_defaultFontSetClass=["material-icons","mat-ligature-font"];constructor(e,r,o,i){this._httpClient=e,this._sanitizer=r,this._errorHandler=i,this._document=o}addSvgIcon(e,r,o){return this.addSvgIconInNamespace("",e,r,o)}addSvgIconLiteral(e,r,o){return this.addSvgIconLiteralInNamespace("",e,r,o)}addSvgIconInNamespace(e,r,o,i){return this._addSvgIconConfig(e,r,new Kn(o,null,i))}addSvgIconResolver(e){return this._resolvers.push(e),this}addSvgIconLiteralInNamespace(e,r,o,i){let s=this._sanitizer.sanitize(J.HTML,o);if(!s)throw FC(o);let a=po(s);return this._addSvgIconConfig(e,r,new Kn("",a,i))}addSvgIconSet(e,r){return this.addSvgIconSetInNamespace("",e,r)}addSvgIconSetLiteral(e,r){return this.addSvgIconSetLiteralInNamespace("",e,r)}addSvgIconSetInNamespace(e,r,o){return this._addSvgIconSetConfig(e,new Kn(r,null,o))}addSvgIconSetLiteralInNamespace(e,r,o){let i=this._sanitizer.sanitize(J.HTML,r);if(!i)throw FC(r);let s=po(i);return this._addSvgIconSetConfig(e,new Kn("",s,o))}registerFontClassAlias(e,r=e){return this._fontCssClassesByAlias.set(e,r),this}classNameForFontAlias(e){return this._fontCssClassesByAlias.get(e)||e}setDefaultFontSetClass(...e){return this._defaultFontSetClass=e,this}getDefaultFontSetClass(){return this._defaultFontSetClass}getSvgIconFromUrl(e){let r=this._sanitizer.sanitize(J.RESOURCE_URL,e);if(!r)throw kC(e);let o=this._cachedIconsByUrl.get(r);return o?S(Hu(o)):this._loadSvgIconFromConfig(new Kn(e,null)).pipe(Be(i=>this._cachedIconsByUrl.set(r,i)),V(i=>Hu(i)))}getNamedSvgIcon(e,r=""){let o=PC(r,e),i=this._svgIconConfigs.get(o);if(i)return this._getSvgFromConfig(i);if(i=this._getIconConfigFromResolvers(r,e),i)return this._svgIconConfigs.set(o,i),this._getSvgFromConfig(i);let s=this._iconSetConfigs.get(r);return s?this._getSvgFromIconSetConfigs(e,s):xn(OC(o))}ngOnDestroy(){this._resolvers=[],this._svgIconConfigs.clear(),this._iconSetConfigs.clear(),this._cachedIconsByUrl.clear()}_getSvgFromConfig(e){return e.svgText?S(Hu(this._svgElementFromConfig(e))):this._loadSvgIconFromConfig(e).pipe(V(r=>Hu(r)))}_getSvgFromIconSetConfigs(e,r){let o=this._extractIconWithNameFromAnySet(e,r);if(o)return S(o);let i=r.filter(s=>!s.svgText).map(s=>this._loadSvgIconSetFromConfig(s).pipe(It(a=>{let l=`Loading icon set URL: ${this._sanitizer.sanitize(J.RESOURCE_URL,s.url)} failed: ${a.message}`;return this._errorHandler.handleError(new Error(l)),S(null)})));return Yi(i).pipe(V(()=>{let s=this._extractIconWithNameFromAnySet(e,r);if(!s)throw OC(e);return s}))}_extractIconWithNameFromAnySet(e,r){for(let o=r.length-1;o>=0;o--){let i=r[o];if(i.svgText&&i.svgText.toString().indexOf(e)>-1){let s=this._svgElementFromConfig(i),a=this._extractSvgIconFromSet(s,e,i.options);if(a)return a}}return null}_loadSvgIconFromConfig(e){return this._fetchIcon(e).pipe(Be(r=>e.svgText=r),V(()=>this._svgElementFromConfig(e)))}_loadSvgIconSetFromConfig(e){return e.svgText?S(null):this._fetchIcon(e).pipe(Be(r=>e.svgText=r))}_extractSvgIconFromSet(e,r,o){let i=e.querySelector(`[id="${r}"]`);if(!i)return null;let s=i.cloneNode(!0);if(s.removeAttribute("id"),s.nodeName.toLowerCase()==="svg")return this._setSvgAttributes(s,o);if(s.nodeName.toLowerCase()==="symbol")return this._setSvgAttributes(this._toSvgElement(s),o);let a=this._svgElementFromString(po(""));return a.appendChild(s),this._setSvgAttributes(a,o)}_svgElementFromString(e){let r=this._document.createElement("DIV");r.innerHTML=e;let o=r.querySelector("svg");if(!o)throw Error(" tag not found");return o}_toSvgElement(e){let r=this._svgElementFromString(po("")),o=e.attributes;for(let i=0;ipo(l)),Rn(()=>this._inProgressUrlFetches.delete(s)),Zi());return this._inProgressUrlFetches.set(s,c),c}_addSvgIconConfig(e,r,o){return this._svgIconConfigs.set(PC(e,r),o),this}_addSvgIconSetConfig(e,r){let o=this._iconSetConfigs.get(e);return o?o.push(r):this._iconSetConfigs.set(e,[r]),this}_svgElementFromConfig(e){if(!e.svgElement){let r=this._svgElementFromString(e.svgText);this._setSvgAttributes(r,e.options),e.svgElement=r}return e.svgElement}_getIconConfigFromResolvers(e,r){for(let o=0;of(O)}),gF=/^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_](Adlm|Arab|Hebr|Nkoo|Rohg|Thaa))(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)/i;function jC(t){let n=t?.toLowerCase()||"";return n==="auto"&&typeof navigator<"u"&&navigator?.language?gF.test(navigator.language)?"rtl":"ltr":n==="rtl"?"rtl":"ltr"}var _i=(()=>{class t{get value(){return this.valueSignal()}valueSignal=K("ltr");change=new H;constructor(){let e=f(mF,{optional:!0});if(e){let r=e.body?e.body.dir:null,o=e.documentElement?e.documentElement.dir:null;this.valueSignal.set(jC(r||o||"ltr"))}}ngOnDestroy(){this.change.complete()}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var rn=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({})}return t})();var vF=["*"],yF=new y("MAT_ICON_DEFAULT_OPTIONS"),bF=new y("mat-icon-location",{providedIn:"root",factory:()=>{let t=f(O),n=t?t.location:null;return{getPathname:()=>n?n.pathname+n.search:""}}}),VC=["clip-path","color-profile","src","cursor","fill","filter","marker","marker-start","marker-mid","marker-end","mask","stroke"],_F=VC.map(t=>`[${t}]`).join(", "),DF=/^url\(['"]?#(.*?)['"]?\)$/,gZ=(()=>{class t{_elementRef=f($);_iconRegistry=f(LC);_location=f(bF);_errorHandler=f(Ze);_defaultColor;get color(){return this._color||this._defaultColor}set color(e){this._color=e}_color;inline=!1;get svgIcon(){return this._svgIcon}set svgIcon(e){e!==this._svgIcon&&(e?this._updateSvgIcon(e):this._svgIcon&&this._clearSvgElement(),this._svgIcon=e)}_svgIcon;get fontSet(){return this._fontSet}set fontSet(e){let r=this._cleanupFontValue(e);r!==this._fontSet&&(this._fontSet=r,this._updateFontIconClasses())}_fontSet;get fontIcon(){return this._fontIcon}set fontIcon(e){let r=this._cleanupFontValue(e);r!==this._fontIcon&&(this._fontIcon=r,this._updateFontIconClasses())}_fontIcon;_previousFontSetClass=[];_previousFontIconClass;_svgName=null;_svgNamespace=null;_previousPath;_elementsWithExternalReferences;_currentIconFetch=G.EMPTY;constructor(){let e=f(new ui("aria-hidden"),{optional:!0}),r=f(yF,{optional:!0});r&&(r.color&&(this.color=this._defaultColor=r.color),r.fontSet&&(this.fontSet=r.fontSet)),e||this._elementRef.nativeElement.setAttribute("aria-hidden","true")}_splitIconName(e){if(!e)return["",""];let r=e.split(":");switch(r.length){case 1:return["",r[0]];case 2:return r;default:throw Error(`Invalid icon name: "${e}"`)}}ngOnInit(){this._updateFontIconClasses()}ngAfterViewChecked(){let e=this._elementsWithExternalReferences;if(e&&e.size){let r=this._location.getPathname();r!==this._previousPath&&(this._previousPath=r,this._prependPathToReferences(r))}}ngOnDestroy(){this._currentIconFetch.unsubscribe(),this._elementsWithExternalReferences&&this._elementsWithExternalReferences.clear()}_usingFontIcon(){return!this.svgIcon}_setSvgElement(e){this._clearSvgElement();let r=this._location.getPathname();this._previousPath=r,this._cacheChildrenWithExternalReferences(e),this._prependPathToReferences(r),this._elementRef.nativeElement.appendChild(e)}_clearSvgElement(){let e=this._elementRef.nativeElement,r=e.childNodes.length;for(this._elementsWithExternalReferences&&this._elementsWithExternalReferences.clear();r--;){let o=e.childNodes[r];(o.nodeType!==1||o.nodeName.toLowerCase()==="svg")&&o.remove()}}_updateFontIconClasses(){if(!this._usingFontIcon())return;let e=this._elementRef.nativeElement,r=(this.fontSet?this._iconRegistry.classNameForFontAlias(this.fontSet).split(/ +/):this._iconRegistry.getDefaultFontSetClass()).filter(o=>o.length>0);this._previousFontSetClass.forEach(o=>e.classList.remove(o)),r.forEach(o=>e.classList.add(o)),this._previousFontSetClass=r,this.fontIcon!==this._previousFontIconClass&&!r.includes("mat-ligature-font")&&(this._previousFontIconClass&&e.classList.remove(this._previousFontIconClass),this.fontIcon&&e.classList.add(this.fontIcon),this._previousFontIconClass=this.fontIcon)}_cleanupFontValue(e){return typeof e=="string"?e.trim().split(" ")[0]:e}_prependPathToReferences(e){let r=this._elementsWithExternalReferences;r&&r.forEach((o,i)=>{o.forEach(s=>{i.setAttribute(s.name,`url('${e}#${s.value}')`)})})}_cacheChildrenWithExternalReferences(e){let r=e.querySelectorAll(_F),o=this._elementsWithExternalReferences=this._elementsWithExternalReferences||new Map;for(let i=0;i{let a=r[i],c=a.getAttribute(s),l=c?c.match(DF):null;if(l){let u=o.get(a);u||(u=[],o.set(a,u)),u.push({name:s,value:l[1]})}})}_updateSvgIcon(e){if(this._svgNamespace=null,this._svgName=null,this._currentIconFetch.unsubscribe(),e){let[r,o]=this._splitIconName(e);r&&(this._svgNamespace=r),o&&(this._svgName=o),this._currentIconFetch=this._iconRegistry.getNamedSvgIcon(o,r).pipe(Ne(1)).subscribe(i=>this._setSvgElement(i),i=>{let s=`Error retrieving icon ${r}:${o}! ${i.message}`;this._errorHandler.handleError(new Error(s))})}}static \u0275fac=function(r){return new(r||t)};static \u0275cmp=Te({type:t,selectors:[["mat-icon"]],hostAttrs:["role","img",1,"mat-icon","notranslate"],hostVars:10,hostBindings:function(r,o){r&2&&(yt("data-mat-icon-type",o._usingFontIcon()?"font":"svg")("data-mat-icon-name",o._svgName||o.fontIcon)("data-mat-icon-namespace",o._svgNamespace||o.fontSet)("fontIcon",o._usingFontIcon()?o.fontIcon:null),zs(o.color?"mat-"+o.color:""),Xe("mat-icon-inline",o.inline)("mat-icon-no-color",o.color!=="primary"&&o.color!=="accent"&&o.color!=="warn"))},inputs:{color:"color",inline:[2,"inline","inline",ue],svgIcon:"svgIcon",fontSet:"fontSet",fontIcon:"fontIcon"},exportAs:["matIcon"],ngContentSelectors:vF,decls:1,vars:0,template:function(r,o){r&1&&(Dr(),_n(0))},styles:[`mat-icon, mat-icon.mat-primary, mat-icon.mat-accent, mat-icon.mat-warn { + color: var(--mat-icon-color, inherit); +} + +.mat-icon { + -webkit-user-select: none; + user-select: none; + background-repeat: no-repeat; + display: inline-block; + fill: currentColor; + height: 24px; + width: 24px; + overflow: hidden; +} +.mat-icon.mat-icon-inline { + font-size: inherit; + height: inherit; + line-height: inherit; + width: inherit; +} +.mat-icon.mat-ligature-font[fontIcon]::before { + content: attr(fontIcon); +} + +[dir=rtl] .mat-icon-rtl-mirror { + transform: scale(-1, 1); +} + +.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-prefix .mat-icon, +.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-suffix .mat-icon { + display: block; +} +.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-prefix .mat-icon-button .mat-icon, +.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-suffix .mat-icon-button .mat-icon { + margin: auto; +} +`],encapsulation:2,changeDetection:0})}return t})(),vZ=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({imports:[rn]})}return t})();function ha(t){return t.buttons===0||t.detail===0}function pa(t){let n=t.touches&&t.touches[0]||t.changedTouches&&t.changedTouches[0];return!!n&&n.identifier===-1&&(n.radiusX==null||n.radiusX===1)&&(n.radiusY==null||n.radiusY===1)}var mg;function BC(){if(mg==null){let t=typeof document<"u"?document.head:null;mg=!!(t&&(t.createShadowRoot||t.attachShadow))}return mg}function gg(t){if(BC()){let n=t.getRootNode?t.getRootNode():null;if(typeof ShadowRoot<"u"&&ShadowRoot&&n instanceof ShadowRoot)return n}return null}function EF(){let t=typeof document<"u"&&document?document.activeElement:null;for(;t&&t.shadowRoot;){let n=t.shadowRoot.activeElement;if(n===t)break;t=n}return t}function ot(t){return t.composedPath?t.composedPath()[0]:t.target}var vg;try{vg=typeof Intl<"u"&&Intl.v8BreakIterator}catch{vg=!1}var _e=(()=>{class t{_platformId=f(so);isBrowser=this._platformId?Dw(this._platformId):typeof document=="object"&&!!document;EDGE=this.isBrowser&&/(edge)/i.test(navigator.userAgent);TRIDENT=this.isBrowser&&/(msie|trident)/i.test(navigator.userAgent);BLINK=this.isBrowser&&!!(window.chrome||vg)&&typeof CSS<"u"&&!this.EDGE&&!this.TRIDENT;WEBKIT=this.isBrowser&&/AppleWebKit/i.test(navigator.userAgent)&&!this.BLINK&&!this.EDGE&&!this.TRIDENT;IOS=this.isBrowser&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!("MSStream"in window);FIREFOX=this.isBrowser&&/(firefox|minefield)/i.test(navigator.userAgent);ANDROID=this.isBrowser&&/android/i.test(navigator.userAgent)&&!this.TRIDENT;SAFARI=this.isBrowser&&/safari/i.test(navigator.userAgent)&&this.WEBKIT;constructor(){}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var ma;function UC(){if(ma==null&&typeof window<"u")try{window.addEventListener("test",null,Object.defineProperty({},"passive",{get:()=>ma=!0}))}finally{ma=ma||!1}return ma}function Di(t){return UC()?t:!!t.capture}function $u(t,n=0){return HC(t)?Number(t):arguments.length===2?n:0}function HC(t){return!isNaN(parseFloat(t))&&!isNaN(Number(t))}function Ot(t){return t instanceof $?t.nativeElement:t}var $C=new y("cdk-input-modality-detector-options"),zC={ignoreKeys:[18,17,224,91,16]},GC=650,yg={passive:!0,capture:!0},WC=(()=>{class t{_platform=f(_e);_listenerCleanups;modalityDetected;modalityChanged;get mostRecentModality(){return this._modality.value}_mostRecentTarget=null;_modality=new Ae(null);_options;_lastTouchMs=0;_onKeydown=e=>{this._options?.ignoreKeys?.some(r=>r===e.keyCode)||(this._modality.next("keyboard"),this._mostRecentTarget=ot(e))};_onMousedown=e=>{Date.now()-this._lastTouchMs{if(pa(e)){this._modality.next("keyboard");return}this._lastTouchMs=Date.now(),this._modality.next("touch"),this._mostRecentTarget=ot(e)};constructor(){let e=f(j),r=f(O),o=f($C,{optional:!0});if(this._options=g(g({},zC),o),this.modalityDetected=this._modality.pipe(Ki(1)),this.modalityChanged=this.modalityDetected.pipe(gc()),this._platform.isBrowser){let i=f(He).createRenderer(null,null);this._listenerCleanups=e.runOutsideAngular(()=>[i.listen(r,"keydown",this._onKeydown,yg),i.listen(r,"mousedown",this._onMousedown,yg),i.listen(r,"touchstart",this._onTouchstart,yg)])}}ngOnDestroy(){this._modality.complete(),this._listenerCleanups?.forEach(e=>e())}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),ga=(function(t){return t[t.IMMEDIATE=0]="IMMEDIATE",t[t.EVENTUAL=1]="EVENTUAL",t})(ga||{}),qC=new y("cdk-focus-monitor-default-options"),zu=Di({passive:!0,capture:!0}),Gu=(()=>{class t{_ngZone=f(j);_platform=f(_e);_inputModalityDetector=f(WC);_origin=null;_lastFocusOrigin=null;_windowFocused=!1;_windowFocusTimeoutId;_originTimeoutId;_originFromTouchInteraction=!1;_elementInfo=new Map;_monitoredElementCount=0;_rootNodeFocusListenerCount=new Map;_detectionMode;_windowFocusListener=()=>{this._windowFocused=!0,this._windowFocusTimeoutId=setTimeout(()=>this._windowFocused=!1)};_document=f(O);_stopInputModalityDetector=new x;constructor(){let e=f(qC,{optional:!0});this._detectionMode=e?.detectionMode||ga.IMMEDIATE}_rootNodeFocusAndBlurListener=e=>{let r=ot(e);for(let o=r;o;o=o.parentElement)e.type==="focus"?this._onFocus(e,o):this._onBlur(e,o)};monitor(e,r=!1){let o=Ot(e);if(!this._platform.isBrowser||o.nodeType!==1)return S();let i=gg(o)||this._document,s=this._elementInfo.get(o);if(s)return r&&(s.checkChildren=!0),s.subject;let a={checkChildren:r,subject:new x,rootNode:i};return this._elementInfo.set(o,a),this._registerGlobalListeners(a),a.subject}stopMonitoring(e){let r=Ot(e),o=this._elementInfo.get(r);o&&(o.subject.complete(),this._setClasses(r),this._elementInfo.delete(r),this._removeGlobalListeners(o))}focusVia(e,r,o){let i=Ot(e),s=this._document.activeElement;i===s?this._getClosestElementsInfo(i).forEach(([a,c])=>this._originChanged(a,r,c)):(this._setOrigin(r),typeof i.focus=="function"&&i.focus(o))}ngOnDestroy(){this._elementInfo.forEach((e,r)=>this.stopMonitoring(r))}_getWindow(){return this._document.defaultView||window}_getFocusOrigin(e){return this._origin?this._originFromTouchInteraction?this._shouldBeAttributedToTouch(e)?"touch":"program":this._origin:this._windowFocused&&this._lastFocusOrigin?this._lastFocusOrigin:e&&this._isLastInteractionFromInputLabel(e)?"mouse":"program"}_shouldBeAttributedToTouch(e){return this._detectionMode===ga.EVENTUAL||!!e?.contains(this._inputModalityDetector._mostRecentTarget)}_setClasses(e,r){e.classList.toggle("cdk-focused",!!r),e.classList.toggle("cdk-touch-focused",r==="touch"),e.classList.toggle("cdk-keyboard-focused",r==="keyboard"),e.classList.toggle("cdk-mouse-focused",r==="mouse"),e.classList.toggle("cdk-program-focused",r==="program")}_setOrigin(e,r=!1){this._ngZone.runOutsideAngular(()=>{if(this._origin=e,this._originFromTouchInteraction=e==="touch"&&r,this._detectionMode===ga.IMMEDIATE){clearTimeout(this._originTimeoutId);let o=this._originFromTouchInteraction?GC:1;this._originTimeoutId=setTimeout(()=>this._origin=null,o)}})}_onFocus(e,r){let o=this._elementInfo.get(r),i=ot(e);!o||!o.checkChildren&&r!==i||this._originChanged(r,this._getFocusOrigin(i),o)}_onBlur(e,r){let o=this._elementInfo.get(r);!o||o.checkChildren&&e.relatedTarget instanceof Node&&r.contains(e.relatedTarget)||(this._setClasses(r),this._emitOrigin(o,null))}_emitOrigin(e,r){e.subject.observers.length&&this._ngZone.run(()=>e.subject.next(r))}_registerGlobalListeners(e){if(!this._platform.isBrowser)return;let r=e.rootNode,o=this._rootNodeFocusListenerCount.get(r)||0;o||this._ngZone.runOutsideAngular(()=>{r.addEventListener("focus",this._rootNodeFocusAndBlurListener,zu),r.addEventListener("blur",this._rootNodeFocusAndBlurListener,zu)}),this._rootNodeFocusListenerCount.set(r,o+1),++this._monitoredElementCount===1&&(this._ngZone.runOutsideAngular(()=>{this._getWindow().addEventListener("focus",this._windowFocusListener)}),this._inputModalityDetector.modalityDetected.pipe(jt(this._stopInputModalityDetector)).subscribe(i=>{this._setOrigin(i,!0)}))}_removeGlobalListeners(e){let r=e.rootNode;if(this._rootNodeFocusListenerCount.has(r)){let o=this._rootNodeFocusListenerCount.get(r);o>1?this._rootNodeFocusListenerCount.set(r,o-1):(r.removeEventListener("focus",this._rootNodeFocusAndBlurListener,zu),r.removeEventListener("blur",this._rootNodeFocusAndBlurListener,zu),this._rootNodeFocusListenerCount.delete(r))}--this._monitoredElementCount||(this._getWindow().removeEventListener("focus",this._windowFocusListener),this._stopInputModalityDetector.next(),clearTimeout(this._windowFocusTimeoutId),clearTimeout(this._originTimeoutId))}_originChanged(e,r,o){this._setClasses(e,r),this._emitOrigin(o,r),this._lastFocusOrigin=r}_getClosestElementsInfo(e){let r=[];return this._elementInfo.forEach((o,i)=>{(i===e||o.checkChildren&&i.contains(e))&&r.push([i,o])}),r}_isLastInteractionFromInputLabel(e){let{_mostRecentTarget:r,mostRecentModality:o}=this._inputModalityDetector;if(o!=="mouse"||!r||r===e||e.nodeName!=="INPUT"&&e.nodeName!=="TEXTAREA"||e.disabled)return!1;let i=e.labels;if(i){for(let s=0;s{class t{_elementRef=f($);_focusMonitor=f(Gu);_monitorSubscription;_focusOrigin=null;cdkFocusChange=new H;constructor(){}get focusOrigin(){return this._focusOrigin}ngAfterViewInit(){let e=this._elementRef.nativeElement;this._monitorSubscription=this._focusMonitor.monitor(e,e.nodeType===1&&e.hasAttribute("cdkMonitorSubtreeFocus")).subscribe(r=>{this._focusOrigin=r,this.cdkFocusChange.emit(r)})}ngOnDestroy(){this._focusMonitor.stopMonitoring(this._elementRef),this._monitorSubscription?.unsubscribe()}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["","cdkMonitorElementFocus",""],["","cdkMonitorSubtreeFocus",""]],outputs:{cdkFocusChange:"cdkFocusChange"},exportAs:["cdkMonitorFocus"]})}return t})();function mo(t){return Array.isArray(t)?t:[t]}var YC=new Set,go,Wu=(()=>{class t{_platform=f(_e);_nonce=f(ao,{optional:!0});_matchMedia;constructor(){this._matchMedia=this._platform.isBrowser&&window.matchMedia?window.matchMedia.bind(window):IF}matchMedia(e){return(this._platform.WEBKIT||this._platform.BLINK)&&CF(e,this._nonce),this._matchMedia(e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function CF(t,n){if(!YC.has(t))try{go||(go=document.createElement("style"),n&&go.setAttribute("nonce",n),go.setAttribute("type","text/css"),document.head.appendChild(go)),go.sheet&&(go.sheet.insertRule(`@media ${t} {body{ }}`,0),YC.add(t))}catch(e){console.error(e)}}function IF(t){return{matches:t==="all"||t==="",media:t,addListener:()=>{},removeListener:()=>{}}}var bg=(()=>{class t{_mediaMatcher=f(Wu);_zone=f(j);_queries=new Map;_destroySubject=new x;constructor(){}ngOnDestroy(){this._destroySubject.next(),this._destroySubject.complete()}isMatched(e){return ZC(mo(e)).some(o=>this._registerQuery(o).mql.matches)}observe(e){let o=ZC(mo(e)).map(s=>this._registerQuery(s).observable),i=Ro(o);return i=ln(i.pipe(Ne(1)),i.pipe(Ki(1),Lr(0))),i.pipe(V(s=>{let a={matches:!1,breakpoints:{}};return s.forEach(({matches:c,query:l})=>{a.matches=a.matches||c,a.breakpoints[l]=c}),a}))}_registerQuery(e){if(this._queries.has(e))return this._queries.get(e);let r=this._mediaMatcher.matchMedia(e),i={observable:new P(s=>{let a=c=>this._zone.run(()=>s.next(c));return r.addListener(a),()=>{r.removeListener(a)}}).pipe(No(r),V(({matches:s})=>({query:e,matches:s})),jt(this._destroySubject)),mql:r};return this._queries.set(e,i),i}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function ZC(t){return t.map(n=>n.split(",")).reduce((n,e)=>n.concat(e)).map(n=>n.trim())}function SF(t){if(t.type==="characterData"&&t.target instanceof Comment)return!0;if(t.type==="childList"){for(let n=0;n{class t{create(e){return typeof MutationObserver>"u"?null:new MutationObserver(e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),QC=(()=>{class t{_mutationObserverFactory=f(KC);_observedElements=new Map;_ngZone=f(j);constructor(){}ngOnDestroy(){this._observedElements.forEach((e,r)=>this._cleanupObserver(r))}observe(e){let r=Ot(e);return new P(o=>{let s=this._observeElement(r).pipe(V(a=>a.filter(c=>!SF(c))),he(a=>!!a.length)).subscribe(a=>{this._ngZone.run(()=>{o.next(a)})});return()=>{s.unsubscribe(),this._unobserveElement(r)}})}_observeElement(e){return this._ngZone.runOutsideAngular(()=>{if(this._observedElements.has(e))this._observedElements.get(e).count++;else{let r=new x,o=this._mutationObserverFactory.create(i=>r.next(i));o&&o.observe(e,{characterData:!0,childList:!0,subtree:!0}),this._observedElements.set(e,{observer:o,stream:r,count:1})}return this._observedElements.get(e).stream})}_unobserveElement(e){this._observedElements.has(e)&&(this._observedElements.get(e).count--,this._observedElements.get(e).count||this._cleanupObserver(e))}_cleanupObserver(e){if(this._observedElements.has(e)){let{observer:r,stream:o}=this._observedElements.get(e);r&&r.disconnect(),o.complete(),this._observedElements.delete(e)}}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),l7=(()=>{class t{_contentObserver=f(QC);_elementRef=f($);event=new H;get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._disabled?this._unsubscribe():this._subscribe()}_disabled=!1;get debounce(){return this._debounce}set debounce(e){this._debounce=$u(e),this._subscribe()}_debounce;_currentSubscription=null;constructor(){}ngAfterContentInit(){!this._currentSubscription&&!this.disabled&&this._subscribe()}ngOnDestroy(){this._unsubscribe()}_subscribe(){this._unsubscribe();let e=this._contentObserver.observe(this._elementRef);this._currentSubscription=(this.debounce?e.pipe(Lr(this.debounce)):e).subscribe(this.event)}_unsubscribe(){this._currentSubscription?.unsubscribe()}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["","cdkObserveContent",""]],inputs:{disabled:[2,"cdkObserveContentDisabled","disabled",ue],debounce:"debounce"},outputs:{event:"cdkObserveContent"},exportAs:["cdkObserveContent"]})}return t})(),XC=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({providers:[KC]})}return t})();var nI=(()=>{class t{_platform=f(_e);constructor(){}isDisabled(e){return e.hasAttribute("disabled")}isVisible(e){return TF(e)&&getComputedStyle(e).visibility==="visible"}isTabbable(e){if(!this._platform.isBrowser)return!1;let r=MF(PF(e));if(r&&(JC(r)===-1||!this.isVisible(r)))return!1;let o=e.nodeName.toLowerCase(),i=JC(e);return e.hasAttribute("contenteditable")?i!==-1:o==="iframe"||o==="object"||this._platform.WEBKIT&&this._platform.IOS&&!kF(e)?!1:o==="audio"?e.hasAttribute("controls")?i!==-1:!1:o==="video"?i===-1?!1:i!==null?!0:this._platform.FIREFOX||e.hasAttribute("controls"):e.tabIndex>=0}isFocusable(e,r){return FF(e)&&!this.isDisabled(e)&&(r?.ignoreVisibility||this.isVisible(e))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function MF(t){try{return t.frameElement}catch{return null}}function TF(t){return!!(t.offsetWidth||t.offsetHeight||typeof t.getClientRects=="function"&&t.getClientRects().length)}function xF(t){let n=t.nodeName.toLowerCase();return n==="input"||n==="select"||n==="button"||n==="textarea"}function AF(t){return NF(t)&&t.type=="hidden"}function RF(t){return OF(t)&&t.hasAttribute("href")}function NF(t){return t.nodeName.toLowerCase()=="input"}function OF(t){return t.nodeName.toLowerCase()=="a"}function rI(t){if(!t.hasAttribute("tabindex")||t.tabIndex===void 0)return!1;let n=t.getAttribute("tabindex");return!!(n&&!isNaN(parseInt(n,10)))}function JC(t){if(!rI(t))return null;let n=parseInt(t.getAttribute("tabindex")||"",10);return isNaN(n)?-1:n}function kF(t){let n=t.nodeName.toLowerCase(),e=n==="input"&&t.type;return e==="text"||e==="password"||n==="select"||n==="textarea"}function FF(t){return AF(t)?!1:xF(t)||RF(t)||t.hasAttribute("contenteditable")||rI(t)}function PF(t){return t.ownerDocument&&t.ownerDocument.defaultView||window}var qu=class{_element;_checker;_ngZone;_document;_injector;_startAnchor=null;_endAnchor=null;_hasAttached=!1;startAnchorListener=()=>this.focusLastTabbableElement();endAnchorListener=()=>this.focusFirstTabbableElement();get enabled(){return this._enabled}set enabled(n){this._enabled=n,this._startAnchor&&this._endAnchor&&(this._toggleAnchorTabIndex(n,this._startAnchor),this._toggleAnchorTabIndex(n,this._endAnchor))}_enabled=!0;constructor(n,e,r,o,i=!1,s){this._element=n,this._checker=e,this._ngZone=r,this._document=o,this._injector=s,i||this.attachAnchors()}destroy(){let n=this._startAnchor,e=this._endAnchor;n&&(n.removeEventListener("focus",this.startAnchorListener),n.remove()),e&&(e.removeEventListener("focus",this.endAnchorListener),e.remove()),this._startAnchor=this._endAnchor=null,this._hasAttached=!1}attachAnchors(){return this._hasAttached?!0:(this._ngZone.runOutsideAngular(()=>{this._startAnchor||(this._startAnchor=this._createAnchor(),this._startAnchor.addEventListener("focus",this.startAnchorListener)),this._endAnchor||(this._endAnchor=this._createAnchor(),this._endAnchor.addEventListener("focus",this.endAnchorListener))}),this._element.parentNode&&(this._element.parentNode.insertBefore(this._startAnchor,this._element),this._element.parentNode.insertBefore(this._endAnchor,this._element.nextSibling),this._hasAttached=!0),this._hasAttached)}focusInitialElementWhenReady(n){return new Promise(e=>{this._executeOnStable(()=>e(this.focusInitialElement(n)))})}focusFirstTabbableElementWhenReady(n){return new Promise(e=>{this._executeOnStable(()=>e(this.focusFirstTabbableElement(n)))})}focusLastTabbableElementWhenReady(n){return new Promise(e=>{this._executeOnStable(()=>e(this.focusLastTabbableElement(n)))})}_getRegionBoundary(n){let e=this._element.querySelectorAll(`[cdk-focus-region-${n}], [cdkFocusRegion${n}], [cdk-focus-${n}]`);return n=="start"?e.length?e[0]:this._getFirstTabbableElement(this._element):e.length?e[e.length-1]:this._getLastTabbableElement(this._element)}focusInitialElement(n){let e=this._element.querySelector("[cdk-focus-initial], [cdkFocusInitial]");if(e){if(!this._checker.isFocusable(e)){let r=this._getFirstTabbableElement(e);return r?.focus(n),!!r}return e.focus(n),!0}return this.focusFirstTabbableElement(n)}focusFirstTabbableElement(n){let e=this._getRegionBoundary("start");return e&&e.focus(n),!!e}focusLastTabbableElement(n){let e=this._getRegionBoundary("end");return e&&e.focus(n),!!e}hasAttached(){return this._hasAttached}_getFirstTabbableElement(n){if(this._checker.isFocusable(n)&&this._checker.isTabbable(n))return n;let e=n.children;for(let r=0;r=0;r--){let o=e[r].nodeType===this._document.ELEMENT_NODE?this._getLastTabbableElement(e[r]):null;if(o)return o}return null}_createAnchor(){let n=this._document.createElement("div");return this._toggleAnchorTabIndex(this._enabled,n),n.classList.add("cdk-visually-hidden"),n.classList.add("cdk-focus-trap-anchor"),n.setAttribute("aria-hidden","true"),n}_toggleAnchorTabIndex(n,e){n?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")}toggleAnchors(n){this._startAnchor&&this._endAnchor&&(this._toggleAnchorTabIndex(n,this._startAnchor),this._toggleAnchorTabIndex(n,this._endAnchor))}_executeOnStable(n){this._injector?At(n,{injector:this._injector}):setTimeout(n)}},LF=(()=>{class t{_checker=f(nI);_ngZone=f(j);_document=f(O);_injector=f(W);constructor(){f(Nt).load(Uu)}create(e,r=!1){return new qu(e,this._checker,this._ngZone,this._document,r,this._injector)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var oI=new y("liveAnnouncerElement",{providedIn:"root",factory:()=>null}),iI=new y("LIVE_ANNOUNCER_DEFAULT_OPTIONS"),jF=0,VF=(()=>{class t{_ngZone=f(j);_defaultOptions=f(iI,{optional:!0});_liveElement;_document=f(O);_sanitizer=f(ia);_previousTimeout;_currentPromise;_currentResolve;constructor(){let e=f(oI,{optional:!0});this._liveElement=e||this._createLiveElement()}announce(e,...r){let o=this._defaultOptions,i,s;return r.length===1&&typeof r[0]=="number"?s=r[0]:[i,s]=r,this.clear(),clearTimeout(this._previousTimeout),i||(i=o&&o.politeness?o.politeness:"polite"),s==null&&o&&(s=o.duration),this._liveElement.setAttribute("aria-live",i),this._liveElement.id&&this._exposeAnnouncerToModals(this._liveElement.id),this._ngZone.runOutsideAngular(()=>(this._currentPromise||(this._currentPromise=new Promise(a=>this._currentResolve=a)),clearTimeout(this._previousTimeout),this._previousTimeout=setTimeout(()=>{!e||typeof e=="string"?this._liveElement.textContent=e:NC(this._liveElement,e,this._sanitizer),typeof s=="number"&&(this._previousTimeout=setTimeout(()=>this.clear(),s)),this._currentResolve?.(),this._currentPromise=this._currentResolve=void 0},100),this._currentPromise))}clear(){this._liveElement&&(this._liveElement.textContent="")}ngOnDestroy(){clearTimeout(this._previousTimeout),this._liveElement?.remove(),this._liveElement=null,this._currentResolve?.(),this._currentPromise=this._currentResolve=void 0}_createLiveElement(){let e="cdk-live-announcer-element",r=this._document.getElementsByClassName(e),o=this._document.createElement("div");for(let i=0;i .cdk-overlay-container [aria-modal="true"]');for(let o=0;o{class t{_platform=f(_e);_hasCheckedHighContrastMode=!1;_document=f(O);_breakpointSubscription;constructor(){this._breakpointSubscription=f(bg).observe("(forced-colors: active)").subscribe(()=>{this._hasCheckedHighContrastMode&&(this._hasCheckedHighContrastMode=!1,this._applyBodyHighContrastModeCssClasses())})}getHighContrastMode(){if(!this._platform.isBrowser)return wr.NONE;let e=this._document.createElement("div");e.style.backgroundColor="rgb(1,2,3)",e.style.position="absolute",this._document.body.appendChild(e);let r=this._document.defaultView||window,o=r&&r.getComputedStyle?r.getComputedStyle(e):null,i=(o&&o.backgroundColor||"").replace(/ /g,"");switch(e.remove(),i){case"rgb(0,0,0)":case"rgb(45,50,54)":case"rgb(32,32,32)":return wr.WHITE_ON_BLACK;case"rgb(255,255,255)":case"rgb(255,250,239)":return wr.BLACK_ON_WHITE}return wr.NONE}ngOnDestroy(){this._breakpointSubscription.unsubscribe()}_applyBodyHighContrastModeCssClasses(){if(!this._hasCheckedHighContrastMode&&this._platform.isBrowser&&this._document.body){let e=this._document.body.classList;e.remove(_g,eI,tI),this._hasCheckedHighContrastMode=!0;let r=this.getHighContrastMode();r===wr.BLACK_ON_WHITE?e.add(_g,eI):r===wr.WHITE_ON_BLACK&&e.add(_g,tI)}}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),BF=(()=>{class t{constructor(){f(sI)._applyBodyHighContrastModeCssClasses()}static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({imports:[XC]})}return t})();var UF=200,Ei=class{_letterKeyStream=new x;_items=[];_selectedItemIndex=-1;_pressedLetters=[];_skipPredicateFn;_selectedItem=new x;selectedItem=this._selectedItem;constructor(n,e){let r=typeof e?.debounceInterval=="number"?e.debounceInterval:UF;e?.skipPredicate&&(this._skipPredicateFn=e.skipPredicate),this.setItems(n),this._setupKeyHandler(r)}destroy(){this._pressedLetters=[],this._letterKeyStream.complete(),this._selectedItem.complete()}setCurrentSelectedItemIndex(n){this._selectedItemIndex=n}setItems(n){this._items=n}handleKey(n){let e=n.keyCode;n.key&&n.key.length===1?this._letterKeyStream.next(n.key.toLocaleUpperCase()):(e>=65&&e<=90||e>=48&&e<=57)&&this._letterKeyStream.next(String.fromCharCode(e))}isTyping(){return this._pressedLetters.length>0}reset(){this._pressedLetters=[]}_setupKeyHandler(n){this._letterKeyStream.pipe(Be(e=>this._pressedLetters.push(e)),Lr(n),he(()=>this._pressedLetters.length>0),V(()=>this._pressedLetters.join("").toLocaleUpperCase())).subscribe(e=>{for(let r=1;rt[e]):t.altKey||t.shiftKey||t.ctrlKey||t.metaKey}var wi=class{_items;_activeItemIndex=K(-1);_activeItem=K(null);_wrap=!1;_typeaheadSubscription=G.EMPTY;_itemChangesSubscription;_vertical=!0;_horizontal=null;_allowedModifierKeys=[];_homeAndEnd=!1;_pageUpAndDown={enabled:!1,delta:10};_effectRef;_typeahead;_skipPredicateFn=n=>n.disabled;constructor(n,e){this._items=n,n instanceof Un?this._itemChangesSubscription=n.changes.subscribe(r=>this._itemsChanged(r.toArray())):si(n)&&(this._effectRef=Es(()=>this._itemsChanged(n()),{injector:e}))}tabOut=new x;change=new x;skipPredicate(n){return this._skipPredicateFn=n,this}withWrap(n=!0){return this._wrap=n,this}withVerticalOrientation(n=!0){return this._vertical=n,this}withHorizontalOrientation(n){return this._horizontal=n,this}withAllowedModifierKeys(n){return this._allowedModifierKeys=n,this}withTypeAhead(n=200){this._typeaheadSubscription.unsubscribe();let e=this._getItemsArray();return this._typeahead=new Ei(e,{debounceInterval:typeof n=="number"?n:void 0,skipPredicate:r=>this._skipPredicateFn(r)}),this._typeaheadSubscription=this._typeahead.selectedItem.subscribe(r=>{this.setActiveItem(r)}),this}cancelTypeahead(){return this._typeahead?.reset(),this}withHomeAndEnd(n=!0){return this._homeAndEnd=n,this}withPageUpDown(n=!0,e=10){return this._pageUpAndDown={enabled:n,delta:e},this}setActiveItem(n){let e=this._activeItem();this.updateActiveItem(n),this._activeItem()!==e&&this.change.next(this._activeItemIndex())}onKeydown(n){let e=n.keyCode,o=["altKey","ctrlKey","metaKey","shiftKey"].every(i=>!n[i]||this._allowedModifierKeys.indexOf(i)>-1);switch(e){case 9:this.tabOut.next();return;case 40:if(this._vertical&&o){this.setNextItemActive();break}else return;case 38:if(this._vertical&&o){this.setPreviousItemActive();break}else return;case 39:if(this._horizontal&&o){this._horizontal==="rtl"?this.setPreviousItemActive():this.setNextItemActive();break}else return;case 37:if(this._horizontal&&o){this._horizontal==="rtl"?this.setNextItemActive():this.setPreviousItemActive();break}else return;case 36:if(this._homeAndEnd&&o){this.setFirstItemActive();break}else return;case 35:if(this._homeAndEnd&&o){this.setLastItemActive();break}else return;case 33:if(this._pageUpAndDown.enabled&&o){let i=this._activeItemIndex()-this._pageUpAndDown.delta;this._setActiveItemByIndex(i>0?i:0,1);break}else return;case 34:if(this._pageUpAndDown.enabled&&o){let i=this._activeItemIndex()+this._pageUpAndDown.delta,s=this._getItemsArray().length;this._setActiveItemByIndex(i-1&&r!==this._activeItemIndex()&&(this._activeItemIndex.set(r),this._typeahead?.setCurrentSelectedItemIndex(r))}}};var Dg=class extends wi{setActiveItem(n){this.activeItem&&this.activeItem.setInactiveStyles(),super.setActiveItem(n),this.activeItem&&this.activeItem.setActiveStyles()}};var Eg=class extends wi{_origin="program";setFocusOrigin(n){return this._origin=n,this}setActiveItem(n){super.setActiveItem(n),this.activeItem&&this.activeItem.focus(this._origin)}};function wg(t){return Vt(t)?t:S(t)}var Cg=class{_activeItemIndex=-1;_activeItem=null;_shouldActivationFollowFocus=!1;_horizontalOrientation="ltr";_skipPredicateFn=n=>!1;_trackByFn=n=>n;_items=[];_typeahead;_typeaheadSubscription=G.EMPTY;_hasInitialFocused=!1;_initializeFocus(){if(this._hasInitialFocused||this._items.length===0)return;let n=0;for(let r=0;rthis._itemsChanged(r.toArray()))):Vt(n)?n.subscribe(r=>this._itemsChanged(r)):(this._items=n,this._initializeFocus()),typeof e.shouldActivationFollowFocus=="boolean"&&(this._shouldActivationFollowFocus=e.shouldActivationFollowFocus),e.horizontalOrientation&&(this._horizontalOrientation=e.horizontalOrientation),e.skipPredicate&&(this._skipPredicateFn=e.skipPredicate),e.trackBy&&(this._trackByFn=e.trackBy),typeof e.typeAheadDebounceInterval<"u"&&this._setTypeAhead(e.typeAheadDebounceInterval)}change=new x;destroy(){this._typeaheadSubscription.unsubscribe(),this._typeahead?.destroy(),this.change.complete()}onKeydown(n){switch(n.key){case"Tab":return;case"ArrowDown":this._focusNextItem();break;case"ArrowUp":this._focusPreviousItem();break;case"ArrowRight":this._horizontalOrientation==="rtl"?this._collapseCurrentItem():this._expandCurrentItem();break;case"ArrowLeft":this._horizontalOrientation==="rtl"?this._expandCurrentItem():this._collapseCurrentItem();break;case"Home":this._focusFirstItem();break;case"End":this._focusLastItem();break;case"Enter":case" ":this._activateCurrentItem();break;default:if(n.key==="*"){this._expandAllItemsAtCurrentItemLevel();break}this._typeahead?.handleKey(n);return}this._typeahead?.reset(),n.preventDefault()}getActiveItemIndex(){return this._activeItemIndex}getActiveItem(){return this._activeItem}_itemsChanged(n){this._hasInitialFocused&&this._activeItem&&!n.includes(this._activeItem)&&(this._activeItem=null,this._hasInitialFocused=!1),this._items=n,this._typeahead?.setItems(this._items),this._updateActiveItemIndex(this._items),this._initializeFocus()}_focusFirstItem(){this.focusItem(this._findNextAvailableItemIndex(-1))}_focusLastItem(){this.focusItem(this._findPreviousAvailableItemIndex(this._items.length))}_focusNextItem(){this.focusItem(this._findNextAvailableItemIndex(this._activeItemIndex))}_focusPreviousItem(){this.focusItem(this._findPreviousAvailableItemIndex(this._activeItemIndex))}focusItem(n,e={}){e.emitChangeEvent??=!0;let r=typeof n=="number"?n:this._items.findIndex(s=>this._trackByFn(s)===this._trackByFn(n));if(r<0||r>=this._items.length)return;let o=this._items[r];if(this._activeItem!==null&&this._trackByFn(o)===this._trackByFn(this._activeItem))return;let i=this._activeItem;this._activeItem=o??null,this._activeItemIndex=r,this._typeahead?.setCurrentSelectedItemIndex(r),this._activeItem?.focus(),i?.unfocus(),e.emitChangeEvent&&this.change.next(this._activeItem),this._shouldActivationFollowFocus&&this._activateCurrentItem()}_updateActiveItemIndex(n){let e=this._activeItem;if(!e)return;let r=n.findIndex(o=>this._trackByFn(o)===this._trackByFn(e));r>-1&&r!==this._activeItemIndex&&(this._activeItemIndex=r,this._typeahead?.setCurrentSelectedItemIndex(r))}_setTypeAhead(n){this._typeahead=new Ei(this._items,{debounceInterval:typeof n=="number"?n:void 0,skipPredicate:e=>this._skipPredicateFn(e)}),this._typeaheadSubscription=this._typeahead.selectedItem.subscribe(e=>{this.focusItem(e)})}_findNextAvailableItemIndex(n){for(let e=n+1;e=0;e--)if(!this._skipPredicateFn(this._items[e]))return e;return n}_collapseCurrentItem(){if(this._activeItem)if(this._isCurrentItemExpanded())this._activeItem.collapse();else{let n=this._activeItem.getParent();if(!n||this._skipPredicateFn(n))return;this.focusItem(n)}}_expandCurrentItem(){this._activeItem&&(this._isCurrentItemExpanded()?wg(this._activeItem.getChildren()).pipe(Ne(1)).subscribe(n=>{let e=n.find(r=>!this._skipPredicateFn(r));e&&this.focusItem(e)}):this._activeItem.expand())}_isCurrentItemExpanded(){return this._activeItem?typeof this._activeItem.isExpanded=="boolean"?this._activeItem.isExpanded:this._activeItem.isExpanded():!1}_isItemDisabled(n){return typeof n.isDisabled=="boolean"?n.isDisabled:n.isDisabled?.()}_expandAllItemsAtCurrentItemLevel(){if(!this._activeItem)return;let n=this._activeItem.getParent(),e;n?e=wg(n.getChildren()):e=S(this._items.filter(r=>r.getParent()===null)),e.pipe(Ne(1)).subscribe(r=>{for(let o of r)o.expand()})}_activateCurrentItem(){this._activeItem?.activate()}},nK=new y("tree-key-manager",{providedIn:"root",factory:()=>(t,n)=>new Cg(t,n)});var Ig={},va=class t{_appId=f(_r);static _infix=`a${Math.floor(Math.random()*1e5).toString()}`;getId(n,e=!1){return this._appId!=="ng"&&(n+=this._appId),Ig.hasOwnProperty(n)||(Ig[n]=0),`${n}${e?t._infix+"-":""}${Ig[n]++}`}static \u0275fac=function(e){return new(e||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})};var cI=" ";function HF(t,n,e){let r=Ku(t,n);e=e.trim(),!r.some(o=>o.trim()===e)&&(r.push(e),t.setAttribute(n,r.join(cI)))}function $F(t,n,e){let r=Ku(t,n);e=e.trim();let o=r.filter(i=>i!==e);o.length?t.setAttribute(n,o.join(cI)):t.removeAttribute(n)}function Ku(t,n){return t.getAttribute(n)?.match(/\S+/g)??[]}var lI="cdk-describedby-message",Zu="cdk-describedby-host",Mg=0,gK=(()=>{class t{_platform=f(_e);_document=f(O);_messageRegistry=new Map;_messagesContainer=null;_id=`${Mg++}`;constructor(){f(Nt).load(Uu),this._id=f(_r)+"-"+Mg++}describe(e,r,o){if(!this._canBeDescribed(e,r))return;let i=Sg(r,o);typeof r!="string"?(aI(r,this._id),this._messageRegistry.set(i,{messageElement:r,referenceCount:0})):this._messageRegistry.has(i)||this._createMessageElement(r,o),this._isElementDescribedByMessage(e,i)||this._addMessageReference(e,i)}removeDescription(e,r,o){if(!r||!this._isElementNode(e))return;let i=Sg(r,o);if(this._isElementDescribedByMessage(e,i)&&this._removeMessageReference(e,i),typeof r=="string"){let s=this._messageRegistry.get(i);s&&s.referenceCount===0&&this._deleteMessageElement(i)}this._messagesContainer?.childNodes.length===0&&(this._messagesContainer.remove(),this._messagesContainer=null)}ngOnDestroy(){let e=this._document.querySelectorAll(`[${Zu}="${this._id}"]`);for(let r=0;ro.indexOf(lI)!=0);e.setAttribute("aria-describedby",r.join(" "))}_addMessageReference(e,r){let o=this._messageRegistry.get(r);HF(e,"aria-describedby",o.messageElement.id),e.setAttribute(Zu,this._id),o.referenceCount++}_removeMessageReference(e,r){let o=this._messageRegistry.get(r);o.referenceCount--,$F(e,"aria-describedby",o.messageElement.id),e.removeAttribute(Zu)}_isElementDescribedByMessage(e,r){let o=Ku(e,"aria-describedby"),i=this._messageRegistry.get(r),s=i&&i.messageElement.id;return!!s&&o.indexOf(s)!=-1}_canBeDescribed(e,r){if(!this._isElementNode(e))return!1;if(r&&typeof r=="object")return!0;let o=r==null?"":`${r}`.trim(),i=e.getAttribute("aria-label");return o?!i||i.trim()!==o:!1}_isElementNode(e){return e.nodeType===this._document.ELEMENT_NODE}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function Sg(t,n){return typeof t=="string"?`${n||""}/${t}`:t}function aI(t,n){t.id||(t.id=`${lI}-${n}-${Mg++}`)}var on=(function(t){return t[t.NORMAL=0]="NORMAL",t[t.NEGATED=1]="NEGATED",t[t.INVERTED=2]="INVERTED",t})(on||{}),Qu,vo;function Xu(){if(vo==null){if(typeof document!="object"||!document||typeof Element!="function"||!Element)return vo=!1,vo;if(document.documentElement?.style&&"scrollBehavior"in document.documentElement.style)vo=!0;else{let t=Element.prototype.scrollTo;t?vo=!/\{\s*\[native code\]\s*\}/.test(t.toString()):vo=!1}}return vo}function Ci(){if(typeof document!="object"||!document)return on.NORMAL;if(Qu==null){let t=document.createElement("div"),n=t.style;t.dir="rtl",n.width="1px",n.overflow="auto",n.visibility="hidden",n.pointerEvents="none",n.position="absolute";let e=document.createElement("div"),r=e.style;r.width="2px",r.height="1px",t.appendChild(e),document.body.appendChild(t),Qu=on.NORMAL,t.scrollLeft===0&&(t.scrollLeft=1,Qu=t.scrollLeft===0?on.NEGATED:on.INVERTED),t.remove()}return Qu}function Tg(){return typeof __karma__<"u"&&!!__karma__||typeof jasmine<"u"&&!!jasmine||typeof jest<"u"&&!!jest||typeof Mocha<"u"&&!!Mocha}var Ii,uI=["color","button","checkbox","date","datetime-local","email","file","hidden","image","month","number","password","radio","range","reset","search","submit","tel","text","time","url","week"];function SK(){if(Ii)return Ii;if(typeof document!="object"||!document)return Ii=new Set(uI),Ii;let t=document.createElement("input");return Ii=new Set(uI.filter(n=>(t.setAttribute("type",n),t.type===n))),Ii}var RK={XSmall:"(max-width: 599.98px)",Small:"(min-width: 600px) and (max-width: 959.98px)",Medium:"(min-width: 960px) and (max-width: 1279.98px)",Large:"(min-width: 1280px) and (max-width: 1919.98px)",XLarge:"(min-width: 1920px)",Handset:"(max-width: 599.98px) and (orientation: portrait), (max-width: 959.98px) and (orientation: landscape)",Tablet:"(min-width: 600px) and (max-width: 839.98px) and (orientation: portrait), (min-width: 960px) and (max-width: 1279.98px) and (orientation: landscape)",Web:"(min-width: 840px) and (orientation: portrait), (min-width: 1280px) and (orientation: landscape)",HandsetPortrait:"(max-width: 599.98px) and (orientation: portrait)",TabletPortrait:"(min-width: 600px) and (max-width: 839.98px) and (orientation: portrait)",WebPortrait:"(min-width: 840px) and (orientation: portrait)",HandsetLandscape:"(max-width: 959.98px) and (orientation: landscape)",TabletLandscape:"(min-width: 960px) and (max-width: 1279.98px) and (orientation: landscape)",WebLandscape:"(min-width: 1280px) and (orientation: landscape)"};var zF=new y("MATERIAL_ANIMATIONS"),dI=null;function GF(){return f(zF,{optional:!0})?.animationsDisabled||f(Fs,{optional:!0})==="NoopAnimations"?"di-disabled":(dI??=f(Wu).matchMedia("(prefers-reduced-motion)").matches,dI?"reduced-motion":"enabled")}function Cr(){return GF()!=="enabled"}function Ce(t){return t==null?"":typeof t=="string"?t:`${t}px`}function jK(t){return t!=null&&`${t}`!="false"}var kt=(function(t){return t[t.FADING_IN=0]="FADING_IN",t[t.VISIBLE=1]="VISIBLE",t[t.FADING_OUT=2]="FADING_OUT",t[t.HIDDEN=3]="HIDDEN",t})(kt||{}),xg=class{_renderer;element;config;_animationForciblyDisabledThroughCss;state=kt.HIDDEN;constructor(n,e,r,o=!1){this._renderer=n,this.element=e,this.config=r,this._animationForciblyDisabledThroughCss=o}fadeOut(){this._renderer.fadeOutRipple(this)}},fI=Di({passive:!0,capture:!0}),Ag=class{_events=new Map;addHandler(n,e,r,o){let i=this._events.get(e);if(i){let s=i.get(r);s?s.add(o):i.set(r,new Set([o]))}else this._events.set(e,new Map([[r,new Set([o])]])),n.runOutsideAngular(()=>{document.addEventListener(e,this._delegateEventHandler,fI)})}removeHandler(n,e,r){let o=this._events.get(n);if(!o)return;let i=o.get(e);i&&(i.delete(r),i.size===0&&o.delete(e),o.size===0&&(this._events.delete(n),document.removeEventListener(n,this._delegateEventHandler,fI)))}_delegateEventHandler=n=>{let e=ot(n);e&&this._events.get(n.type)?.forEach((r,o)=>{(o===e||o.contains(e))&&r.forEach(i=>i.handleEvent(n))})}},ya={enterDuration:225,exitDuration:150},WF=800,hI=Di({passive:!0,capture:!0}),pI=["mousedown","touchstart"],mI=["mouseup","mouseleave","touchend","touchcancel"],qF=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=Te({type:t,selectors:[["ng-component"]],hostAttrs:["mat-ripple-style-loader",""],decls:0,vars:0,template:function(r,o){},styles:[`.mat-ripple { + overflow: hidden; + position: relative; +} +.mat-ripple:not(:empty) { + transform: translateZ(0); +} + +.mat-ripple.mat-ripple-unbounded { + overflow: visible; +} + +.mat-ripple-element { + position: absolute; + border-radius: 50%; + pointer-events: none; + transition: opacity, transform 0ms cubic-bezier(0, 0, 0.2, 1); + transform: scale3d(0, 0, 0); + background-color: var(--mat-ripple-color, color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent)); +} +@media (forced-colors: active) { + .mat-ripple-element { + display: none; + } +} +.cdk-drag-preview .mat-ripple-element, .cdk-drag-placeholder .mat-ripple-element { + display: none; +} +`],encapsulation:2,changeDetection:0})}return t})(),ba=class t{_target;_ngZone;_platform;_containerElement;_triggerElement=null;_isPointerDown=!1;_activeRipples=new Map;_mostRecentTransientRipple=null;_lastTouchStartEvent;_pointerUpEventsRegistered=!1;_containerRect=null;static _eventManager=new Ag;constructor(n,e,r,o,i){this._target=n,this._ngZone=e,this._platform=o,o.isBrowser&&(this._containerElement=Ot(r)),i&&i.get(Nt).load(qF)}fadeInRipple(n,e,r={}){let o=this._containerRect=this._containerRect||this._containerElement.getBoundingClientRect(),i=g(g({},ya),r.animation);r.centered&&(n=o.left+o.width/2,e=o.top+o.height/2);let s=r.radius||YF(n,e,o),a=n-o.left,c=e-o.top,l=i.enterDuration,u=document.createElement("div");u.classList.add("mat-ripple-element"),u.style.left=`${a-s}px`,u.style.top=`${c-s}px`,u.style.height=`${s*2}px`,u.style.width=`${s*2}px`,r.color!=null&&(u.style.backgroundColor=r.color),u.style.transitionDuration=`${l}ms`,this._containerElement.appendChild(u);let d=window.getComputedStyle(u),h=d.transitionProperty,p=d.transitionDuration,m=h==="none"||p==="0s"||p==="0s, 0s"||o.width===0&&o.height===0,_=new xg(this,u,r,m);u.style.transform="scale3d(1, 1, 1)",_.state=kt.FADING_IN,r.persistent||(this._mostRecentTransientRipple=_);let D=null;return!m&&(l||i.exitDuration)&&this._ngZone.runOutsideAngular(()=>{let C=()=>{D&&(D.fallbackTimer=null),clearTimeout(De),this._finishRippleTransition(_)},z=()=>this._destroyRipple(_),De=setTimeout(z,l+100);u.addEventListener("transitionend",C),u.addEventListener("transitioncancel",z),D={onTransitionEnd:C,onTransitionCancel:z,fallbackTimer:De}}),this._activeRipples.set(_,D),(m||!l)&&this._finishRippleTransition(_),_}fadeOutRipple(n){if(n.state===kt.FADING_OUT||n.state===kt.HIDDEN)return;let e=n.element,r=g(g({},ya),n.config.animation);e.style.transitionDuration=`${r.exitDuration}ms`,e.style.opacity="0",n.state=kt.FADING_OUT,(n._animationForciblyDisabledThroughCss||!r.exitDuration)&&this._finishRippleTransition(n)}fadeOutAll(){this._getActiveRipples().forEach(n=>n.fadeOut())}fadeOutAllNonPersistent(){this._getActiveRipples().forEach(n=>{n.config.persistent||n.fadeOut()})}setupTriggerEvents(n){let e=Ot(n);!this._platform.isBrowser||!e||e===this._triggerElement||(this._removeTriggerEvents(),this._triggerElement=e,pI.forEach(r=>{t._eventManager.addHandler(this._ngZone,r,e,this)}))}handleEvent(n){n.type==="mousedown"?this._onMousedown(n):n.type==="touchstart"?this._onTouchStart(n):this._onPointerUp(),this._pointerUpEventsRegistered||(this._ngZone.runOutsideAngular(()=>{mI.forEach(e=>{this._triggerElement.addEventListener(e,this,hI)})}),this._pointerUpEventsRegistered=!0)}_finishRippleTransition(n){n.state===kt.FADING_IN?this._startFadeOutTransition(n):n.state===kt.FADING_OUT&&this._destroyRipple(n)}_startFadeOutTransition(n){let e=n===this._mostRecentTransientRipple,{persistent:r}=n.config;n.state=kt.VISIBLE,!r&&(!e||!this._isPointerDown)&&n.fadeOut()}_destroyRipple(n){let e=this._activeRipples.get(n)??null;this._activeRipples.delete(n),this._activeRipples.size||(this._containerRect=null),n===this._mostRecentTransientRipple&&(this._mostRecentTransientRipple=null),n.state=kt.HIDDEN,e!==null&&(n.element.removeEventListener("transitionend",e.onTransitionEnd),n.element.removeEventListener("transitioncancel",e.onTransitionCancel),e.fallbackTimer!==null&&clearTimeout(e.fallbackTimer)),n.element.remove()}_onMousedown(n){let e=ha(n),r=this._lastTouchStartEvent&&Date.now(){let e=n.state===kt.VISIBLE||n.config.terminateOnPointerUp&&n.state===kt.FADING_IN;!n.config.persistent&&e&&n.fadeOut()}))}_getActiveRipples(){return Array.from(this._activeRipples.keys())}_removeTriggerEvents(){let n=this._triggerElement;n&&(pI.forEach(e=>t._eventManager.removeHandler(e,n,this)),this._pointerUpEventsRegistered&&(mI.forEach(e=>n.removeEventListener(e,this,hI)),this._pointerUpEventsRegistered=!1))}};function YF(t,n,e){let r=Math.max(Math.abs(t-e.left),Math.abs(t-e.right)),o=Math.max(Math.abs(n-e.top),Math.abs(n-e.bottom));return Math.sqrt(r*r+o*o)}var Rg=new y("mat-ripple-global-options"),JK=(()=>{class t{_elementRef=f($);_animationsDisabled=Cr();color;unbounded=!1;centered=!1;radius=0;animation;get disabled(){return this._disabled}set disabled(e){e&&this.fadeOutAllNonPersistent(),this._disabled=e,this._setupTriggerEventsIfEnabled()}_disabled=!1;get trigger(){return this._trigger||this._elementRef.nativeElement}set trigger(e){this._trigger=e,this._setupTriggerEventsIfEnabled()}_trigger;_rippleRenderer;_globalOptions;_isInitialized=!1;constructor(){let e=f(j),r=f(_e),o=f(Rg,{optional:!0}),i=f(W);this._globalOptions=o||{},this._rippleRenderer=new ba(this,e,this._elementRef,r,i)}ngOnInit(){this._isInitialized=!0,this._setupTriggerEventsIfEnabled()}ngOnDestroy(){this._rippleRenderer._removeTriggerEvents()}fadeOutAll(){this._rippleRenderer.fadeOutAll()}fadeOutAllNonPersistent(){this._rippleRenderer.fadeOutAllNonPersistent()}get rippleConfig(){return{centered:this.centered,radius:this.radius,color:this.color,animation:g(g(g({},this._globalOptions.animation),this._animationsDisabled?{enterDuration:0,exitDuration:0}:{}),this.animation),terminateOnPointerUp:this._globalOptions.terminateOnPointerUp}}get rippleDisabled(){return this.disabled||!!this._globalOptions.disabled}_setupTriggerEventsIfEnabled(){!this.disabled&&this._isInitialized&&this._rippleRenderer.setupTriggerEvents(this.trigger)}launch(e,r=0,o){return typeof e=="number"?this._rippleRenderer.fadeInRipple(e,r,g(g({},this.rippleConfig),o)):this._rippleRenderer.fadeInRipple(0,0,g(g({},this.rippleConfig),e))}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["","mat-ripple",""],["","matRipple",""]],hostAttrs:[1,"mat-ripple"],hostVars:2,hostBindings:function(r,o){r&2&&Xe("mat-ripple-unbounded",o.unbounded)},inputs:{color:[0,"matRippleColor","color"],unbounded:[0,"matRippleUnbounded","unbounded"],centered:[0,"matRippleCentered","centered"],radius:[0,"matRippleRadius","radius"],animation:[0,"matRippleAnimation","animation"],disabled:[0,"matRippleDisabled","disabled"],trigger:[0,"matRippleTrigger","trigger"]},exportAs:["matRipple"]})}return t})();var ZF={capture:!0},KF=["focus","mousedown","mouseenter","touchstart"],Ng="mat-ripple-loader-uninitialized",Og="mat-ripple-loader-class-name",gI="mat-ripple-loader-centered",Ju="mat-ripple-loader-disabled",vI=(()=>{class t{_document=f(O);_animationsDisabled=Cr();_globalRippleOptions=f(Rg,{optional:!0});_platform=f(_e);_ngZone=f(j);_injector=f(W);_eventCleanups;_hosts=new Map;constructor(){let e=f(He).createRenderer(null,null);this._eventCleanups=this._ngZone.runOutsideAngular(()=>KF.map(r=>e.listen(this._document,r,this._onInteraction,ZF)))}ngOnDestroy(){let e=this._hosts.keys();for(let r of e)this.destroyRipple(r);this._eventCleanups.forEach(r=>r())}configureRipple(e,r){e.setAttribute(Ng,this._globalRippleOptions?.namespace??""),(r.className||!e.hasAttribute(Og))&&e.setAttribute(Og,r.className||""),r.centered&&e.setAttribute(gI,""),r.disabled&&e.setAttribute(Ju,"")}setDisabled(e,r){let o=this._hosts.get(e);o?(o.target.rippleDisabled=r,!r&&!o.hasSetUpEvents&&(o.hasSetUpEvents=!0,o.renderer.setupTriggerEvents(e))):r?e.setAttribute(Ju,""):e.removeAttribute(Ju)}_onInteraction=e=>{let r=ot(e);if(r instanceof HTMLElement){let o=r.closest(`[${Ng}="${this._globalRippleOptions?.namespace??""}"]`);o&&this._createRipple(o)}};_createRipple(e){if(!this._document||this._hosts.has(e))return;e.querySelector(".mat-ripple")?.remove();let r=this._document.createElement("span");r.classList.add("mat-ripple",e.getAttribute(Og)),e.append(r);let o=this._globalRippleOptions,i=this._animationsDisabled?0:o?.animation?.enterDuration??ya.enterDuration,s=this._animationsDisabled?0:o?.animation?.exitDuration??ya.exitDuration,a={rippleDisabled:this._animationsDisabled||o?.disabled||e.hasAttribute(Ju),rippleConfig:{centered:e.hasAttribute(gI),terminateOnPointerUp:o?.terminateOnPointerUp,animation:{enterDuration:i,exitDuration:s}}},c=new ba(a,this._ngZone,r,this._platform,this._injector),l=!a.rippleDisabled;l&&c.setupTriggerEvents(e),this._hosts.set(e,{target:a,renderer:c,hasSetUpEvents:l}),e.removeAttribute(Ng)}destroyRipple(e){let r=this._hosts.get(e);r&&(r.renderer._removeTriggerEvents(),this._hosts.delete(e))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var yI=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=Te({type:t,selectors:[["structural-styles"]],decls:0,vars:0,template:function(r,o){},styles:[`.mat-focus-indicator { + position: relative; +} +.mat-focus-indicator::before { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + box-sizing: border-box; + pointer-events: none; + display: var(--mat-focus-indicator-display, none); + border-width: var(--mat-focus-indicator-border-width, 3px); + border-style: var(--mat-focus-indicator-border-style, solid); + border-color: var(--mat-focus-indicator-border-color, transparent); + border-radius: var(--mat-focus-indicator-border-radius, 4px); +} +.mat-focus-indicator:focus-visible::before { + content: ""; +} + +@media (forced-colors: active) { + html { + --mat-focus-indicator-display: block; + } +} +`],encapsulation:2,changeDetection:0})}return t})();var QF=["mat-icon-button",""],XF=["*"],JF=new y("MAT_BUTTON_CONFIG");function bI(t){return t==null?void 0:wm(t)}var kg=(()=>{class t{_elementRef=f($);_ngZone=f(j);_animationsDisabled=Cr();_config=f(JF,{optional:!0});_focusMonitor=f(Gu);_cleanupClick;_renderer=f(Le);_rippleLoader=f(vI);_isAnchor;_isFab=!1;color;get disableRipple(){return this._disableRipple}set disableRipple(e){this._disableRipple=e,this._updateRippleDisabled()}_disableRipple=!1;get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._updateRippleDisabled()}_disabled=!1;ariaDisabled;disabledInteractive;tabIndex;set _tabindex(e){this.tabIndex=e}constructor(){f(Nt).load(yI);let e=this._elementRef.nativeElement;this._isAnchor=e.tagName==="A",this.disabledInteractive=this._config?.disabledInteractive??!1,this.color=this._config?.color??null,this._rippleLoader?.configureRipple(e,{className:"mat-mdc-button-ripple"})}ngAfterViewInit(){this._focusMonitor.monitor(this._elementRef,!0),this._isAnchor&&this._setupAsAnchor()}ngOnDestroy(){this._cleanupClick?.(),this._focusMonitor.stopMonitoring(this._elementRef),this._rippleLoader?.destroyRipple(this._elementRef.nativeElement)}focus(e="program",r){e?this._focusMonitor.focusVia(this._elementRef.nativeElement,e,r):this._elementRef.nativeElement.focus(r)}_getAriaDisabled(){return this.ariaDisabled!=null?this.ariaDisabled:this._isAnchor?this.disabled||null:this.disabled&&this.disabledInteractive?!0:null}_getDisabledAttribute(){return this.disabledInteractive||!this.disabled?null:!0}_updateRippleDisabled(){this._rippleLoader?.setDisabled(this._elementRef.nativeElement,this.disableRipple||this.disabled)}_getTabIndex(){return this._isAnchor?this.disabled&&!this.disabledInteractive?-1:this.tabIndex:this.tabIndex}_setupAsAnchor(){this._cleanupClick=this._ngZone.runOutsideAngular(()=>this._renderer.listen(this._elementRef.nativeElement,"click",e=>{this.disabled&&(e.preventDefault(),e.stopImmediatePropagation())}))}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,hostAttrs:[1,"mat-mdc-button-base"],hostVars:13,hostBindings:function(r,o){r&2&&(yt("disabled",o._getDisabledAttribute())("aria-disabled",o._getAriaDisabled())("tabindex",o._getTabIndex()),zs(o.color?"mat-"+o.color:""),Xe("mat-mdc-button-disabled",o.disabled)("mat-mdc-button-disabled-interactive",o.disabledInteractive)("mat-unthemed",!o.color)("_mat-animation-noopable",o._animationsDisabled))},inputs:{color:"color",disableRipple:[2,"disableRipple","disableRipple",ue],disabled:[2,"disabled","disabled",ue],ariaDisabled:[2,"aria-disabled","ariaDisabled",ue],disabledInteractive:[2,"disabledInteractive","disabledInteractive",ue],tabIndex:[2,"tabIndex","tabIndex",bI],_tabindex:[2,"tabindex","_tabindex",bI]}})}return t})(),eP=(()=>{class t extends kg{constructor(){super(),this._rippleLoader.configureRipple(this._elementRef.nativeElement,{centered:!0})}static \u0275fac=function(r){return new(r||t)};static \u0275cmp=Te({type:t,selectors:[["button","mat-icon-button",""],["a","mat-icon-button",""],["button","matIconButton",""],["a","matIconButton",""]],hostAttrs:[1,"mdc-icon-button","mat-mdc-icon-button"],exportAs:["matButton","matAnchor"],features:[ie],attrs:QF,ngContentSelectors:XF,decls:4,vars:0,consts:[[1,"mat-mdc-button-persistent-ripple","mdc-icon-button__ripple"],[1,"mat-focus-indicator"],[1,"mat-mdc-button-touch-target"]],template:function(r,o){r&1&&(Dr(),Gn(0,"span",0),_n(1),Gn(2,"span",1)(3,"span",2))},styles:[`.mat-mdc-icon-button { + -webkit-user-select: none; + user-select: none; + display: inline-block; + position: relative; + box-sizing: border-box; + border: none; + outline: none; + background-color: transparent; + fill: currentColor; + text-decoration: none; + cursor: pointer; + z-index: 0; + overflow: visible; + border-radius: var(--mat-icon-button-container-shape, var(--mat-sys-corner-full, 50%)); + flex-shrink: 0; + text-align: center; + width: var(--mat-icon-button-state-layer-size, 40px); + height: var(--mat-icon-button-state-layer-size, 40px); + padding: calc(calc(var(--mat-icon-button-state-layer-size, 40px) - var(--mat-icon-button-icon-size, 24px)) / 2); + font-size: var(--mat-icon-button-icon-size, 24px); + color: var(--mat-icon-button-icon-color, var(--mat-sys-on-surface-variant)); + -webkit-tap-highlight-color: transparent; +} +.mat-mdc-icon-button .mat-mdc-button-ripple, +.mat-mdc-icon-button .mat-mdc-button-persistent-ripple, +.mat-mdc-icon-button .mat-mdc-button-persistent-ripple::before { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + pointer-events: none; + border-radius: inherit; +} +.mat-mdc-icon-button .mat-mdc-button-ripple { + overflow: hidden; +} +.mat-mdc-icon-button .mat-mdc-button-persistent-ripple::before { + content: ""; + opacity: 0; +} +.mat-mdc-icon-button .mdc-button__label, +.mat-mdc-icon-button .mat-icon { + z-index: 1; + position: relative; +} +.mat-mdc-icon-button .mat-focus-indicator { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + border-radius: inherit; +} +.mat-mdc-icon-button:focus-visible > .mat-focus-indicator::before { + content: ""; + border-radius: inherit; +} +.mat-mdc-icon-button .mat-ripple-element { + background-color: var(--mat-icon-button-ripple-color, color-mix(in srgb, var(--mat-sys-on-surface-variant) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); +} +.mat-mdc-icon-button .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-icon-button-state-layer-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-icon-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-icon-button-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-icon-button:hover > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-icon-button-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); +} +.mat-mdc-icon-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-icon-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-icon-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-icon-button-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); +} +.mat-mdc-icon-button:active > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-icon-button-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); +} +.mat-mdc-icon-button .mat-mdc-button-touch-target { + position: absolute; + top: 50%; + height: var(--mat-icon-button-touch-target-size, 48px); + display: var(--mat-icon-button-touch-target-display, block); + left: 50%; + width: var(--mat-icon-button-touch-target-size, 48px); + transform: translate(-50%, -50%); +} +.mat-mdc-icon-button._mat-animation-noopable { + transition: none !important; + animation: none !important; +} +.mat-mdc-icon-button[disabled], .mat-mdc-icon-button.mat-mdc-button-disabled { + cursor: default; + pointer-events: none; + color: var(--mat-icon-button-disabled-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-mdc-icon-button.mat-mdc-button-disabled-interactive { + pointer-events: auto; +} +.mat-mdc-icon-button img, +.mat-mdc-icon-button svg { + width: var(--mat-icon-button-icon-size, 24px); + height: var(--mat-icon-button-icon-size, 24px); + vertical-align: baseline; +} +.mat-mdc-icon-button .mat-mdc-button-persistent-ripple { + border-radius: var(--mat-icon-button-container-shape, var(--mat-sys-corner-full, 50%)); +} +.mat-mdc-icon-button[hidden] { + display: none; +} +.mat-mdc-icon-button.mat-unthemed:not(.mdc-ripple-upgraded):focus::before, .mat-mdc-icon-button.mat-primary:not(.mdc-ripple-upgraded):focus::before, .mat-mdc-icon-button.mat-accent:not(.mdc-ripple-upgraded):focus::before, .mat-mdc-icon-button.mat-warn:not(.mdc-ripple-upgraded):focus::before { + background: transparent; + opacity: 1; +} +`,`@media (forced-colors: active) { + .mat-mdc-button:not(.mdc-button--outlined), + .mat-mdc-unelevated-button:not(.mdc-button--outlined), + .mat-mdc-raised-button:not(.mdc-button--outlined), + .mat-mdc-outlined-button:not(.mdc-button--outlined), + .mat-mdc-button-base.mat-tonal-button, + .mat-mdc-icon-button.mat-mdc-icon-button, + .mat-mdc-outlined-button .mdc-button__ripple { + outline: solid 1px; + } +} +`],encapsulation:2,changeDetection:0})}return t})();var _I=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({imports:[rn]})}return t})();var tP=["matButton",""],nP=[[["",8,"material-icons",3,"iconPositionEnd",""],["mat-icon",3,"iconPositionEnd",""],["","matButtonIcon","",3,"iconPositionEnd",""]],"*",[["","iconPositionEnd","",8,"material-icons"],["mat-icon","iconPositionEnd",""],["","matButtonIcon","","iconPositionEnd",""]]],rP=[".material-icons:not([iconPositionEnd]), mat-icon:not([iconPositionEnd]), [matButtonIcon]:not([iconPositionEnd])","*",".material-icons[iconPositionEnd], mat-icon[iconPositionEnd], [matButtonIcon][iconPositionEnd]"];var DI=new Map([["text",["mat-mdc-button"]],["filled",["mdc-button--unelevated","mat-mdc-unelevated-button"]],["elevated",["mdc-button--raised","mat-mdc-raised-button"]],["outlined",["mdc-button--outlined","mat-mdc-outlined-button"]],["tonal",["mat-tonal-button"]]]),SQ=(()=>{class t extends kg{get appearance(){return this._appearance}set appearance(e){this.setAppearance(e||this._config?.defaultAppearance||"text")}_appearance=null;constructor(){super();let e=oP(this._elementRef.nativeElement);e&&this.setAppearance(e)}setAppearance(e){if(e===this._appearance)return;let r=this._elementRef.nativeElement.classList,o=this._appearance?DI.get(this._appearance):null,i=DI.get(e);o&&r.remove(...o),r.add(...i),this._appearance=e}static \u0275fac=function(r){return new(r||t)};static \u0275cmp=Te({type:t,selectors:[["button","matButton",""],["a","matButton",""],["button","mat-button",""],["button","mat-raised-button",""],["button","mat-flat-button",""],["button","mat-stroked-button",""],["a","mat-button",""],["a","mat-raised-button",""],["a","mat-flat-button",""],["a","mat-stroked-button",""]],hostAttrs:[1,"mdc-button"],inputs:{appearance:[0,"matButton","appearance"]},exportAs:["matButton","matAnchor"],features:[ie],attrs:tP,ngContentSelectors:rP,decls:7,vars:4,consts:[[1,"mat-mdc-button-persistent-ripple"],[1,"mdc-button__label"],[1,"mat-focus-indicator"],[1,"mat-mdc-button-touch-target"]],template:function(r,o){r&1&&(Dr(nP),Gn(0,"span",0),_n(1),ai(2,"span",1),_n(3,1),ci(),_n(4,2),Gn(5,"span",2)(6,"span",3)),r&2&&Xe("mdc-button__ripple",!o._isFab)("mdc-fab__ripple",o._isFab)},styles:[`.mat-mdc-button-base { + text-decoration: none; +} +.mat-mdc-button-base .mat-icon { + min-height: fit-content; + flex-shrink: 0; +} +@media (hover: none) { + .mat-mdc-button-base:hover > span.mat-mdc-button-persistent-ripple::before { + opacity: 0; + } +} + +.mdc-button { + -webkit-user-select: none; + user-select: none; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + min-width: 64px; + border: none; + outline: none; + line-height: inherit; + -webkit-appearance: none; + overflow: visible; + vertical-align: middle; + background: transparent; + padding: 0 8px; +} +.mdc-button::-moz-focus-inner { + padding: 0; + border: 0; +} +.mdc-button:active { + outline: none; +} +.mdc-button:hover { + cursor: pointer; +} +.mdc-button:disabled { + cursor: default; + pointer-events: none; +} +.mdc-button[hidden] { + display: none; +} +.mdc-button .mdc-button__label { + position: relative; +} + +.mat-mdc-button { + padding: 0 var(--mat-button-text-horizontal-padding, 12px); + height: var(--mat-button-text-container-height, 40px); + font-family: var(--mat-button-text-label-text-font, var(--mat-sys-label-large-font)); + font-size: var(--mat-button-text-label-text-size, var(--mat-sys-label-large-size)); + letter-spacing: var(--mat-button-text-label-text-tracking, var(--mat-sys-label-large-tracking)); + text-transform: var(--mat-button-text-label-text-transform); + font-weight: var(--mat-button-text-label-text-weight, var(--mat-sys-label-large-weight)); +} +.mat-mdc-button, .mat-mdc-button .mdc-button__ripple { + border-radius: var(--mat-button-text-container-shape, var(--mat-sys-corner-full)); +} +.mat-mdc-button:not(:disabled) { + color: var(--mat-button-text-label-text-color, var(--mat-sys-primary)); +} +.mat-mdc-button[disabled], .mat-mdc-button.mat-mdc-button-disabled { + cursor: default; + pointer-events: none; + color: var(--mat-button-text-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-mdc-button.mat-mdc-button-disabled-interactive { + pointer-events: auto; +} +.mat-mdc-button:has(.material-icons, mat-icon, [matButtonIcon]) { + padding: 0 var(--mat-button-text-with-icon-horizontal-padding, 16px); +} +.mat-mdc-button > .mat-icon { + margin-right: var(--mat-button-text-icon-spacing, 8px); + margin-left: var(--mat-button-text-icon-offset, -4px); +} +[dir=rtl] .mat-mdc-button > .mat-icon { + margin-right: var(--mat-button-text-icon-offset, -4px); + margin-left: var(--mat-button-text-icon-spacing, 8px); +} +.mat-mdc-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-text-icon-offset, -4px); + margin-left: var(--mat-button-text-icon-spacing, 8px); +} +[dir=rtl] .mat-mdc-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-text-icon-spacing, 8px); + margin-left: var(--mat-button-text-icon-offset, -4px); +} +.mat-mdc-button .mat-ripple-element { + background-color: var(--mat-button-text-ripple-color, color-mix(in srgb, var(--mat-sys-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); +} +.mat-mdc-button .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-text-state-layer-color, var(--mat-sys-primary)); +} +.mat-mdc-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-text-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-button:hover > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-text-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); +} +.mat-mdc-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-text-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); +} +.mat-mdc-button:active > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-text-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); +} +.mat-mdc-button .mat-mdc-button-touch-target { + position: absolute; + top: 50%; + height: var(--mat-button-text-touch-target-size, 48px); + display: var(--mat-button-text-touch-target-display, block); + left: 0; + right: 0; + transform: translateY(-50%); +} + +.mat-mdc-unelevated-button { + transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + height: var(--mat-button-filled-container-height, 40px); + font-family: var(--mat-button-filled-label-text-font, var(--mat-sys-label-large-font)); + font-size: var(--mat-button-filled-label-text-size, var(--mat-sys-label-large-size)); + letter-spacing: var(--mat-button-filled-label-text-tracking, var(--mat-sys-label-large-tracking)); + text-transform: var(--mat-button-filled-label-text-transform); + font-weight: var(--mat-button-filled-label-text-weight, var(--mat-sys-label-large-weight)); + padding: 0 var(--mat-button-filled-horizontal-padding, 24px); +} +.mat-mdc-unelevated-button > .mat-icon { + margin-right: var(--mat-button-filled-icon-spacing, 8px); + margin-left: var(--mat-button-filled-icon-offset, -8px); +} +[dir=rtl] .mat-mdc-unelevated-button > .mat-icon { + margin-right: var(--mat-button-filled-icon-offset, -8px); + margin-left: var(--mat-button-filled-icon-spacing, 8px); +} +.mat-mdc-unelevated-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-filled-icon-offset, -8px); + margin-left: var(--mat-button-filled-icon-spacing, 8px); +} +[dir=rtl] .mat-mdc-unelevated-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-filled-icon-spacing, 8px); + margin-left: var(--mat-button-filled-icon-offset, -8px); +} +.mat-mdc-unelevated-button .mat-ripple-element { + background-color: var(--mat-button-filled-ripple-color, color-mix(in srgb, var(--mat-sys-on-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); +} +.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-filled-state-layer-color, var(--mat-sys-on-primary)); +} +.mat-mdc-unelevated-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-filled-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-unelevated-button:hover > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-filled-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); +} +.mat-mdc-unelevated-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-unelevated-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-unelevated-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-filled-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); +} +.mat-mdc-unelevated-button:active > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-filled-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); +} +.mat-mdc-unelevated-button .mat-mdc-button-touch-target { + position: absolute; + top: 50%; + height: var(--mat-button-filled-touch-target-size, 48px); + display: var(--mat-button-filled-touch-target-display, block); + left: 0; + right: 0; + transform: translateY(-50%); +} +.mat-mdc-unelevated-button:not(:disabled) { + color: var(--mat-button-filled-label-text-color, var(--mat-sys-on-primary)); + background-color: var(--mat-button-filled-container-color, var(--mat-sys-primary)); +} +.mat-mdc-unelevated-button, .mat-mdc-unelevated-button .mdc-button__ripple { + border-radius: var(--mat-button-filled-container-shape, var(--mat-sys-corner-full)); +} +.mat-mdc-unelevated-button[disabled], .mat-mdc-unelevated-button.mat-mdc-button-disabled { + cursor: default; + pointer-events: none; + color: var(--mat-button-filled-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); + background-color: var(--mat-button-filled-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); +} +.mat-mdc-unelevated-button.mat-mdc-button-disabled-interactive { + pointer-events: auto; +} + +.mat-mdc-raised-button { + transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: var(--mat-button-protected-container-elevation-shadow, var(--mat-sys-level1)); + height: var(--mat-button-protected-container-height, 40px); + font-family: var(--mat-button-protected-label-text-font, var(--mat-sys-label-large-font)); + font-size: var(--mat-button-protected-label-text-size, var(--mat-sys-label-large-size)); + letter-spacing: var(--mat-button-protected-label-text-tracking, var(--mat-sys-label-large-tracking)); + text-transform: var(--mat-button-protected-label-text-transform); + font-weight: var(--mat-button-protected-label-text-weight, var(--mat-sys-label-large-weight)); + padding: 0 var(--mat-button-protected-horizontal-padding, 24px); +} +.mat-mdc-raised-button > .mat-icon { + margin-right: var(--mat-button-protected-icon-spacing, 8px); + margin-left: var(--mat-button-protected-icon-offset, -8px); +} +[dir=rtl] .mat-mdc-raised-button > .mat-icon { + margin-right: var(--mat-button-protected-icon-offset, -8px); + margin-left: var(--mat-button-protected-icon-spacing, 8px); +} +.mat-mdc-raised-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-protected-icon-offset, -8px); + margin-left: var(--mat-button-protected-icon-spacing, 8px); +} +[dir=rtl] .mat-mdc-raised-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-protected-icon-spacing, 8px); + margin-left: var(--mat-button-protected-icon-offset, -8px); +} +.mat-mdc-raised-button .mat-ripple-element { + background-color: var(--mat-button-protected-ripple-color, color-mix(in srgb, var(--mat-sys-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); +} +.mat-mdc-raised-button .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-protected-state-layer-color, var(--mat-sys-primary)); +} +.mat-mdc-raised-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-protected-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-raised-button:hover > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-protected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); +} +.mat-mdc-raised-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-raised-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-raised-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-protected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); +} +.mat-mdc-raised-button:active > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-protected-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); +} +.mat-mdc-raised-button .mat-mdc-button-touch-target { + position: absolute; + top: 50%; + height: var(--mat-button-protected-touch-target-size, 48px); + display: var(--mat-button-protected-touch-target-display, block); + left: 0; + right: 0; + transform: translateY(-50%); +} +.mat-mdc-raised-button:not(:disabled) { + color: var(--mat-button-protected-label-text-color, var(--mat-sys-primary)); + background-color: var(--mat-button-protected-container-color, var(--mat-sys-surface)); +} +.mat-mdc-raised-button, .mat-mdc-raised-button .mdc-button__ripple { + border-radius: var(--mat-button-protected-container-shape, var(--mat-sys-corner-full)); +} +@media (hover: hover) { + .mat-mdc-raised-button:hover { + box-shadow: var(--mat-button-protected-hover-container-elevation-shadow, var(--mat-sys-level2)); + } +} +.mat-mdc-raised-button:focus { + box-shadow: var(--mat-button-protected-focus-container-elevation-shadow, var(--mat-sys-level1)); +} +.mat-mdc-raised-button:active, .mat-mdc-raised-button:focus:active { + box-shadow: var(--mat-button-protected-pressed-container-elevation-shadow, var(--mat-sys-level1)); +} +.mat-mdc-raised-button[disabled], .mat-mdc-raised-button.mat-mdc-button-disabled { + cursor: default; + pointer-events: none; + color: var(--mat-button-protected-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); + background-color: var(--mat-button-protected-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); +} +.mat-mdc-raised-button[disabled].mat-mdc-button-disabled, .mat-mdc-raised-button.mat-mdc-button-disabled.mat-mdc-button-disabled { + box-shadow: var(--mat-button-protected-disabled-container-elevation-shadow, var(--mat-sys-level0)); +} +.mat-mdc-raised-button.mat-mdc-button-disabled-interactive { + pointer-events: auto; +} + +.mat-mdc-outlined-button { + border-style: solid; + transition: border 280ms cubic-bezier(0.4, 0, 0.2, 1); + height: var(--mat-button-outlined-container-height, 40px); + font-family: var(--mat-button-outlined-label-text-font, var(--mat-sys-label-large-font)); + font-size: var(--mat-button-outlined-label-text-size, var(--mat-sys-label-large-size)); + letter-spacing: var(--mat-button-outlined-label-text-tracking, var(--mat-sys-label-large-tracking)); + text-transform: var(--mat-button-outlined-label-text-transform); + font-weight: var(--mat-button-outlined-label-text-weight, var(--mat-sys-label-large-weight)); + border-radius: var(--mat-button-outlined-container-shape, var(--mat-sys-corner-full)); + border-width: var(--mat-button-outlined-outline-width, 1px); + padding: 0 var(--mat-button-outlined-horizontal-padding, 24px); +} +.mat-mdc-outlined-button > .mat-icon { + margin-right: var(--mat-button-outlined-icon-spacing, 8px); + margin-left: var(--mat-button-outlined-icon-offset, -8px); +} +[dir=rtl] .mat-mdc-outlined-button > .mat-icon { + margin-right: var(--mat-button-outlined-icon-offset, -8px); + margin-left: var(--mat-button-outlined-icon-spacing, 8px); +} +.mat-mdc-outlined-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-outlined-icon-offset, -8px); + margin-left: var(--mat-button-outlined-icon-spacing, 8px); +} +[dir=rtl] .mat-mdc-outlined-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-outlined-icon-spacing, 8px); + margin-left: var(--mat-button-outlined-icon-offset, -8px); +} +.mat-mdc-outlined-button .mat-ripple-element { + background-color: var(--mat-button-outlined-ripple-color, color-mix(in srgb, var(--mat-sys-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); +} +.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-outlined-state-layer-color, var(--mat-sys-primary)); +} +.mat-mdc-outlined-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-outlined-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); +} +.mat-mdc-outlined-button:hover > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-outlined-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); +} +.mat-mdc-outlined-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-outlined-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-outlined-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-outlined-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); +} +.mat-mdc-outlined-button:active > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-outlined-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); +} +.mat-mdc-outlined-button .mat-mdc-button-touch-target { + position: absolute; + top: 50%; + height: var(--mat-button-outlined-touch-target-size, 48px); + display: var(--mat-button-outlined-touch-target-display, block); + left: 0; + right: 0; + transform: translateY(-50%); +} +.mat-mdc-outlined-button:not(:disabled) { + color: var(--mat-button-outlined-label-text-color, var(--mat-sys-primary)); + border-color: var(--mat-button-outlined-outline-color, var(--mat-sys-outline)); +} +.mat-mdc-outlined-button[disabled], .mat-mdc-outlined-button.mat-mdc-button-disabled { + cursor: default; + pointer-events: none; + color: var(--mat-button-outlined-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); + border-color: var(--mat-button-outlined-disabled-outline-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); +} +.mat-mdc-outlined-button.mat-mdc-button-disabled-interactive { + pointer-events: auto; +} + +.mat-tonal-button { + transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + height: var(--mat-button-tonal-container-height, 40px); + font-family: var(--mat-button-tonal-label-text-font, var(--mat-sys-label-large-font)); + font-size: var(--mat-button-tonal-label-text-size, var(--mat-sys-label-large-size)); + letter-spacing: var(--mat-button-tonal-label-text-tracking, var(--mat-sys-label-large-tracking)); + text-transform: var(--mat-button-tonal-label-text-transform); + font-weight: var(--mat-button-tonal-label-text-weight, var(--mat-sys-label-large-weight)); + padding: 0 var(--mat-button-tonal-horizontal-padding, 24px); +} +.mat-tonal-button:not(:disabled) { + color: var(--mat-button-tonal-label-text-color, var(--mat-sys-on-secondary-container)); + background-color: var(--mat-button-tonal-container-color, var(--mat-sys-secondary-container)); +} +.mat-tonal-button, .mat-tonal-button .mdc-button__ripple { + border-radius: var(--mat-button-tonal-container-shape, var(--mat-sys-corner-full)); +} +.mat-tonal-button[disabled], .mat-tonal-button.mat-mdc-button-disabled { + cursor: default; + pointer-events: none; + color: var(--mat-button-tonal-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); + background-color: var(--mat-button-tonal-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); +} +.mat-tonal-button.mat-mdc-button-disabled-interactive { + pointer-events: auto; +} +.mat-tonal-button > .mat-icon { + margin-right: var(--mat-button-tonal-icon-spacing, 8px); + margin-left: var(--mat-button-tonal-icon-offset, -8px); +} +[dir=rtl] .mat-tonal-button > .mat-icon { + margin-right: var(--mat-button-tonal-icon-offset, -8px); + margin-left: var(--mat-button-tonal-icon-spacing, 8px); +} +.mat-tonal-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-tonal-icon-offset, -8px); + margin-left: var(--mat-button-tonal-icon-spacing, 8px); +} +[dir=rtl] .mat-tonal-button .mdc-button__label + .mat-icon { + margin-right: var(--mat-button-tonal-icon-spacing, 8px); + margin-left: var(--mat-button-tonal-icon-offset, -8px); +} +.mat-tonal-button .mat-ripple-element { + background-color: var(--mat-button-tonal-ripple-color, color-mix(in srgb, var(--mat-sys-on-secondary-container) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); +} +.mat-tonal-button .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-tonal-state-layer-color, var(--mat-sys-on-secondary-container)); +} +.mat-tonal-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { + background-color: var(--mat-button-tonal-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); +} +.mat-tonal-button:hover > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-tonal-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); +} +.mat-tonal-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-tonal-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-tonal-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-tonal-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); +} +.mat-tonal-button:active > .mat-mdc-button-persistent-ripple::before { + opacity: var(--mat-button-tonal-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); +} +.mat-tonal-button .mat-mdc-button-touch-target { + position: absolute; + top: 50%; + height: var(--mat-button-tonal-touch-target-size, 48px); + display: var(--mat-button-tonal-touch-target-display, block); + left: 0; + right: 0; + transform: translateY(-50%); +} + +.mat-mdc-button, +.mat-mdc-unelevated-button, +.mat-mdc-raised-button, +.mat-mdc-outlined-button, +.mat-tonal-button { + -webkit-tap-highlight-color: transparent; +} +.mat-mdc-button .mat-mdc-button-ripple, +.mat-mdc-button .mat-mdc-button-persistent-ripple, +.mat-mdc-button .mat-mdc-button-persistent-ripple::before, +.mat-mdc-unelevated-button .mat-mdc-button-ripple, +.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple, +.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple::before, +.mat-mdc-raised-button .mat-mdc-button-ripple, +.mat-mdc-raised-button .mat-mdc-button-persistent-ripple, +.mat-mdc-raised-button .mat-mdc-button-persistent-ripple::before, +.mat-mdc-outlined-button .mat-mdc-button-ripple, +.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple, +.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before, +.mat-tonal-button .mat-mdc-button-ripple, +.mat-tonal-button .mat-mdc-button-persistent-ripple, +.mat-tonal-button .mat-mdc-button-persistent-ripple::before { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + pointer-events: none; + border-radius: inherit; +} +.mat-mdc-button .mat-mdc-button-ripple, +.mat-mdc-unelevated-button .mat-mdc-button-ripple, +.mat-mdc-raised-button .mat-mdc-button-ripple, +.mat-mdc-outlined-button .mat-mdc-button-ripple, +.mat-tonal-button .mat-mdc-button-ripple { + overflow: hidden; +} +.mat-mdc-button .mat-mdc-button-persistent-ripple::before, +.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple::before, +.mat-mdc-raised-button .mat-mdc-button-persistent-ripple::before, +.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before, +.mat-tonal-button .mat-mdc-button-persistent-ripple::before { + content: ""; + opacity: 0; +} +.mat-mdc-button .mdc-button__label, +.mat-mdc-button .mat-icon, +.mat-mdc-unelevated-button .mdc-button__label, +.mat-mdc-unelevated-button .mat-icon, +.mat-mdc-raised-button .mdc-button__label, +.mat-mdc-raised-button .mat-icon, +.mat-mdc-outlined-button .mdc-button__label, +.mat-mdc-outlined-button .mat-icon, +.mat-tonal-button .mdc-button__label, +.mat-tonal-button .mat-icon { + z-index: 1; + position: relative; +} +.mat-mdc-button .mat-focus-indicator, +.mat-mdc-unelevated-button .mat-focus-indicator, +.mat-mdc-raised-button .mat-focus-indicator, +.mat-mdc-outlined-button .mat-focus-indicator, +.mat-tonal-button .mat-focus-indicator { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + border-radius: inherit; +} +.mat-mdc-button:focus-visible > .mat-focus-indicator::before, +.mat-mdc-unelevated-button:focus-visible > .mat-focus-indicator::before, +.mat-mdc-raised-button:focus-visible > .mat-focus-indicator::before, +.mat-mdc-outlined-button:focus-visible > .mat-focus-indicator::before, +.mat-tonal-button:focus-visible > .mat-focus-indicator::before { + content: ""; + border-radius: inherit; +} +.mat-mdc-button._mat-animation-noopable, +.mat-mdc-unelevated-button._mat-animation-noopable, +.mat-mdc-raised-button._mat-animation-noopable, +.mat-mdc-outlined-button._mat-animation-noopable, +.mat-tonal-button._mat-animation-noopable { + transition: none !important; + animation: none !important; +} +.mat-mdc-button > .mat-icon, +.mat-mdc-unelevated-button > .mat-icon, +.mat-mdc-raised-button > .mat-icon, +.mat-mdc-outlined-button > .mat-icon, +.mat-tonal-button > .mat-icon { + display: inline-block; + position: relative; + vertical-align: top; + font-size: 1.125rem; + height: 1.125rem; + width: 1.125rem; +} + +.mat-mdc-outlined-button .mat-mdc-button-ripple, +.mat-mdc-outlined-button .mdc-button__ripple { + top: -1px; + left: -1px; + bottom: -1px; + right: -1px; +} + +.mat-mdc-unelevated-button .mat-focus-indicator::before, +.mat-tonal-button .mat-focus-indicator::before, +.mat-mdc-raised-button .mat-focus-indicator::before { + margin: calc(calc(var(--mat-focus-indicator-border-width, 3px) + 2px) * -1); +} + +.mat-mdc-outlined-button .mat-focus-indicator::before { + margin: calc(calc(var(--mat-focus-indicator-border-width, 3px) + 3px) * -1); +} +`,`@media (forced-colors: active) { + .mat-mdc-button:not(.mdc-button--outlined), + .mat-mdc-unelevated-button:not(.mdc-button--outlined), + .mat-mdc-raised-button:not(.mdc-button--outlined), + .mat-mdc-outlined-button:not(.mdc-button--outlined), + .mat-mdc-button-base.mat-tonal-button, + .mat-mdc-icon-button.mat-mdc-icon-button, + .mat-mdc-outlined-button .mdc-button__ripple { + outline: solid 1px; + } +} +`],encapsulation:2,changeDetection:0})}return t})();function oP(t){return t.hasAttribute("mat-raised-button")?"elevated":t.hasAttribute("mat-stroked-button")?"outlined":t.hasAttribute("mat-flat-button")?"filled":t.hasAttribute("mat-button")?"text":null}var MQ=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({imports:[_I,rn]})}return t})();var ed={production:!0,electron:!1,githubio:!1,solarputty_download_url:"",current_version:"v3",compute_id:"local"};var _a=class{};function iP(t){return t&&typeof t.connect=="function"&&!(t instanceof Ui)}var Fg=class extends _a{_data;constructor(n){super(),this._data=n}connect(){return Vt(this._data)?this._data:S(this._data)}disconnect(){}},In=(function(t){return t[t.REPLACED=0]="REPLACED",t[t.INSERTED=1]="INSERTED",t[t.MOVED=2]="MOVED",t[t.REMOVED=3]="REMOVED",t})(In||{}),Pg=class{viewCacheSize=20;_viewCache=[];applyChanges(n,e,r,o,i){n.forEachOperation((s,a,c)=>{let l,u;if(s.previousIndex==null){let d=()=>r(s,a,c);l=this._insertView(d,c,e,o(s)),u=l?In.INSERTED:In.REPLACED}else c==null?(this._detachAndCacheView(a,e),u=In.REMOVED):(l=this._moveView(a,c,e,o(s)),u=In.MOVED);i&&i({context:l?.context,operation:u,record:s})})}detach(){for(let n of this._viewCache)n.destroy();this._viewCache=[]}_insertView(n,e,r,o){let i=this._insertViewFromCache(e,r);if(i){i.context.$implicit=o;return}let s=n();return r.createEmbeddedView(s.templateRef,s.context,s.index)}_detachAndCacheView(n,e){let r=e.detach(n);this._maybeCacheView(r,e)}_moveView(n,e,r,o){let i=r.get(n);return r.move(i,e),i.context.$implicit=o,i}_maybeCacheView(n,e){if(this._viewCache.length{class t{_ngZone=f(j);_platform=f(_e);_renderer=f(He).createRenderer(null,null);_cleanupGlobalListener;constructor(){}_scrolled=new x;_scrolledCount=0;scrollContainers=new Map;register(e){this.scrollContainers.has(e)||this.scrollContainers.set(e,e.elementScrolled().subscribe(()=>this._scrolled.next(e)))}deregister(e){let r=this.scrollContainers.get(e);r&&(r.unsubscribe(),this.scrollContainers.delete(e))}scrolled(e=sP){return this._platform.isBrowser?new P(r=>{this._cleanupGlobalListener||(this._cleanupGlobalListener=this._ngZone.runOutsideAngular(()=>this._renderer.listen("document","scroll",()=>this._scrolled.next())));let o=e>0?this._scrolled.pipe(mc(e)).subscribe(r):this._scrolled.subscribe(r);return this._scrolledCount++,()=>{o.unsubscribe(),this._scrolledCount--,this._scrolledCount||(this._cleanupGlobalListener?.(),this._cleanupGlobalListener=void 0)}}):S()}ngOnDestroy(){this._cleanupGlobalListener?.(),this._cleanupGlobalListener=void 0,this.scrollContainers.forEach((e,r)=>this.deregister(r)),this._scrolled.complete()}ancestorScrolled(e,r){let o=this.getAncestorScrollContainers(e);return this.scrolled(r).pipe(he(i=>!i||o.indexOf(i)>-1))}getAncestorScrollContainers(e){let r=[];return this.scrollContainers.forEach((o,i)=>{this._scrollableContainsElement(i,e)&&r.push(i)}),r}_scrollableContainsElement(e,r){let o=Ot(r),i=e.getElementRef().nativeElement;do if(o==i)return!0;while(o=o.parentElement);return!1}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),aP=(()=>{class t{elementRef=f($);scrollDispatcher=f(Da);ngZone=f(j);dir=f(_i,{optional:!0});_scrollElement=this.elementRef.nativeElement;_destroyed=new x;_renderer=f(Le);_cleanupScroll;_elementScrolled=new x;constructor(){}ngOnInit(){this._cleanupScroll=this.ngZone.runOutsideAngular(()=>this._renderer.listen(this._scrollElement,"scroll",e=>this._elementScrolled.next(e))),this.scrollDispatcher.register(this)}ngOnDestroy(){this._cleanupScroll?.(),this._elementScrolled.complete(),this.scrollDispatcher.deregister(this),this._destroyed.next(),this._destroyed.complete()}elementScrolled(){return this._elementScrolled}getElementRef(){return this.elementRef}scrollTo(e){let r=this.elementRef.nativeElement,o=this.dir&&this.dir.value=="rtl";e.left==null&&(e.left=o?e.end:e.start),e.right==null&&(e.right=o?e.start:e.end),e.bottom!=null&&(e.top=r.scrollHeight-r.clientHeight-e.bottom),o&&Ci()!=on.NORMAL?(e.left!=null&&(e.right=r.scrollWidth-r.clientWidth-e.left),Ci()==on.INVERTED?e.left=e.right:Ci()==on.NEGATED&&(e.left=e.right?-e.right:e.right)):e.right!=null&&(e.left=r.scrollWidth-r.clientWidth-e.right),this._applyScrollToOptions(e)}_applyScrollToOptions(e){let r=this.elementRef.nativeElement;Xu()?r.scrollTo(e):(e.top!=null&&(r.scrollTop=e.top),e.left!=null&&(r.scrollLeft=e.left))}measureScrollOffset(e){let r="left",o="right",i=this.elementRef.nativeElement;if(e=="top")return i.scrollTop;if(e=="bottom")return i.scrollHeight-i.clientHeight-i.scrollTop;let s=this.dir&&this.dir.value=="rtl";return e=="start"?e=s?o:r:e=="end"&&(e=s?r:o),s&&Ci()==on.INVERTED?e==r?i.scrollWidth-i.clientWidth-i.scrollLeft:i.scrollLeft:s&&Ci()==on.NEGATED?e==r?i.scrollLeft+i.scrollWidth-i.clientWidth:-i.scrollLeft:e==r?i.scrollLeft:i.scrollWidth-i.clientWidth-i.scrollLeft}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["","cdk-scrollable",""],["","cdkScrollable",""]]})}return t})(),cP=20,Si=(()=>{class t{_platform=f(_e);_listeners;_viewportSize=null;_change=new x;_document=f(O);constructor(){let e=f(j),r=f(He).createRenderer(null,null);e.runOutsideAngular(()=>{if(this._platform.isBrowser){let o=i=>this._change.next(i);this._listeners=[r.listen("window","resize",o),r.listen("window","orientationchange",o)]}this.change().subscribe(()=>this._viewportSize=null)})}ngOnDestroy(){this._listeners?.forEach(e=>e()),this._change.complete()}getViewportSize(){this._viewportSize||this._updateViewportSize();let e={width:this._viewportSize.width,height:this._viewportSize.height};return this._platform.isBrowser||(this._viewportSize=null),e}getViewportRect(){let e=this.getViewportScrollPosition(),{width:r,height:o}=this.getViewportSize();return{top:e.top,left:e.left,bottom:e.top+o,right:e.left+r,height:o,width:r}}getViewportScrollPosition(){if(!this._platform.isBrowser)return{top:0,left:0};let e=this._document,r=this._getWindow(),o=e.documentElement,i=o.getBoundingClientRect(),s=-i.top||e.body?.scrollTop||r.scrollY||o.scrollTop||0,a=-i.left||e.body?.scrollLeft||r.scrollX||o.scrollLeft||0;return{top:s,left:a}}change(e=cP){return e>0?this._change.pipe(mc(e)):this._change}_getWindow(){return this._document.defaultView||window}_updateViewportSize(){let e=this._getWindow();this._viewportSize=this._platform.isBrowser?{width:e.innerWidth,height:e.innerHeight}:{width:0,height:0}}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var XQ=new y("CDK_VIRTUAL_SCROLL_VIEWPORT");var Lg=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({})}return t})(),jg=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({imports:[rn,Lg,rn,Lg]})}return t})();var Ea=class{_attachedHost=null;attach(n){return this._attachedHost=n,n.attach(this)}detach(){let n=this._attachedHost;n!=null&&(this._attachedHost=null,n.detach())}get isAttached(){return this._attachedHost!=null}setAttachedHost(n){this._attachedHost=n}},Vg=class extends Ea{component;viewContainerRef;injector;projectableNodes;bindings;constructor(n,e,r,o,i){super(),this.component=n,this.viewContainerRef=e,this.injector=r,this.projectableNodes=o,this.bindings=i||null}},Mi=class extends Ea{templateRef;viewContainerRef;context;injector;constructor(n,e,r,o){super(),this.templateRef=n,this.viewContainerRef=e,this.context=r,this.injector=o}get origin(){return this.templateRef.elementRef}attach(n,e=this.context){return this.context=e,super.attach(n)}detach(){return this.context=void 0,super.detach()}},Bg=class extends Ea{element;constructor(n){super(),this.element=n instanceof $?n.nativeElement:n}},td=class{_attachedPortal=null;_disposeFn=null;_isDisposed=!1;hasAttached(){return!!this._attachedPortal}attach(n){if(n instanceof Vg)return this._attachedPortal=n,this.attachComponentPortal(n);if(n instanceof Mi)return this._attachedPortal=n,this.attachTemplatePortal(n);if(this.attachDomPortal&&n instanceof Bg)return this._attachedPortal=n,this.attachDomPortal(n)}attachDomPortal=null;detach(){this._attachedPortal&&(this._attachedPortal.setAttachedHost(null),this._attachedPortal=null),this._invokeDisposeFn()}dispose(){this.hasAttached()&&this.detach(),this._invokeDisposeFn(),this._isDisposed=!0}setDisposeFn(n){this._disposeFn=n}_invokeDisposeFn(){this._disposeFn&&(this._disposeFn(),this._disposeFn=null)}},nd=class extends td{outletElement;_appRef;_defaultInjector;constructor(n,e,r){super(),this.outletElement=n,this._appRef=e,this._defaultInjector=r}attachComponentPortal(n){let e;if(n.viewContainerRef){let r=n.injector||n.viewContainerRef.injector,o=r.get(yn,null,{optional:!0})||void 0;e=n.viewContainerRef.createComponent(n.component,{index:n.viewContainerRef.length,injector:r,ngModuleRef:o,projectableNodes:n.projectableNodes||void 0,bindings:n.bindings||void 0}),this.setDisposeFn(()=>e.destroy())}else{let r=this._appRef,o=n.injector||this._defaultInjector||W.NULL,i=o.get(re,r.injector);e=Jl(n.component,{elementInjector:o,environmentInjector:i,projectableNodes:n.projectableNodes||void 0,bindings:n.bindings||void 0}),r.attachView(e.hostView),this.setDisposeFn(()=>{r.viewCount>0&&r.detachView(e.hostView),e.destroy()})}return this.outletElement.appendChild(this._getComponentRootNode(e)),this._attachedPortal=n,e}attachTemplatePortal(n){let e=n.viewContainerRef,r=e.createEmbeddedView(n.templateRef,n.context,{injector:n.injector});return r.rootNodes.forEach(o=>this.outletElement.appendChild(o)),r.detectChanges(),this.setDisposeFn(()=>{let o=e.indexOf(r);o!==-1&&e.remove(o)}),this._attachedPortal=n,r}attachDomPortal=n=>{let e=n.element;e.parentNode;let r=this.outletElement.ownerDocument.createComment("dom-portal");e.parentNode.insertBefore(r,e),this.outletElement.appendChild(e),this._attachedPortal=n,super.setDisposeFn(()=>{r.parentNode&&r.parentNode.replaceChild(e,r)})};dispose(){super.dispose(),this.outletElement.remove()}_getComponentRootNode(n){return n.hostView.rootNodes[0]}},iX=(()=>{class t extends Mi{constructor(){let e=f(gt),r=f(Qe);super(e,r)}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["","cdkPortal",""]],exportAs:["cdkPortal"],features:[ie]})}return t})(),sX=(()=>{class t extends td{_moduleRef=f(yn,{optional:!0});_document=f(O);_viewContainerRef=f(Qe);_isInitialized=!1;_attachedRef=null;constructor(){super()}get portal(){return this._attachedPortal}set portal(e){this.hasAttached()&&!e&&!this._isInitialized||(this.hasAttached()&&super.detach(),e&&super.attach(e),this._attachedPortal=e||null)}attached=new H;get attachedRef(){return this._attachedRef}ngOnInit(){this._isInitialized=!0}ngOnDestroy(){super.dispose(),this._attachedRef=this._attachedPortal=null}attachComponentPortal(e){e.setAttachedHost(this);let r=e.viewContainerRef!=null?e.viewContainerRef:this._viewContainerRef,o=r.createComponent(e.component,{index:r.length,injector:e.injector||r.injector,projectableNodes:e.projectableNodes||void 0,ngModuleRef:this._moduleRef||void 0,bindings:e.bindings||void 0});return r!==this._viewContainerRef&&this._getRootNode().appendChild(o.hostView.rootNodes[0]),super.setDisposeFn(()=>o.destroy()),this._attachedPortal=e,this._attachedRef=o,this.attached.emit(o),o}attachTemplatePortal(e){e.setAttachedHost(this);let r=this._viewContainerRef.createEmbeddedView(e.templateRef,e.context,{injector:e.injector});return super.setDisposeFn(()=>this._viewContainerRef.clear()),this._attachedPortal=e,this._attachedRef=r,this.attached.emit(r),r}attachDomPortal=e=>{let r=e.element;r.parentNode;let o=this._document.createComment("dom-portal");e.setAttachedHost(this),r.parentNode.insertBefore(o,r),this._getRootNode().appendChild(r),this._attachedPortal=e,super.setDisposeFn(()=>{o.parentNode&&o.parentNode.replaceChild(r,o)})};_getRootNode(){let e=this._viewContainerRef.element.nativeElement;return e.nodeType===e.ELEMENT_NODE?e:e.parentNode}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["","cdkPortalOutlet",""]],inputs:{portal:[0,"cdkPortalOutlet","portal"]},outputs:{attached:"attached"},exportAs:["cdkPortalOutlet"],features:[ie]})}return t})(),EI=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({})}return t})();var wI=Xu();function AI(t){return new rd(t.get(Si),t.get(O))}var rd=class{_viewportRuler;_previousHTMLStyles={top:"",left:""};_previousScrollPosition;_isEnabled=!1;_document;constructor(n,e){this._viewportRuler=n,this._document=e}attach(){}enable(){if(this._canBeEnabled()){let n=this._document.documentElement;this._previousScrollPosition=this._viewportRuler.getViewportScrollPosition(),this._previousHTMLStyles.left=n.style.left||"",this._previousHTMLStyles.top=n.style.top||"",n.style.left=Ce(-this._previousScrollPosition.left),n.style.top=Ce(-this._previousScrollPosition.top),n.classList.add("cdk-global-scrollblock"),this._isEnabled=!0}}disable(){if(this._isEnabled){let n=this._document.documentElement,e=this._document.body,r=n.style,o=e.style,i=r.scrollBehavior||"",s=o.scrollBehavior||"";this._isEnabled=!1,r.left=this._previousHTMLStyles.left,r.top=this._previousHTMLStyles.top,n.classList.remove("cdk-global-scrollblock"),wI&&(r.scrollBehavior=o.scrollBehavior="auto"),window.scroll(this._previousScrollPosition.left,this._previousScrollPosition.top),wI&&(r.scrollBehavior=i,o.scrollBehavior=s)}}_canBeEnabled(){if(this._document.documentElement.classList.contains("cdk-global-scrollblock")||this._isEnabled)return!1;let e=this._document.documentElement,r=this._viewportRuler.getViewportSize();return e.scrollHeight>r.height||e.scrollWidth>r.width}};function RI(t,n){return new od(t.get(Da),t.get(j),t.get(Si),n)}var od=class{_scrollDispatcher;_ngZone;_viewportRuler;_config;_scrollSubscription=null;_overlayRef;_initialScrollPosition;constructor(n,e,r,o){this._scrollDispatcher=n,this._ngZone=e,this._viewportRuler=r,this._config=o}attach(n){this._overlayRef,this._overlayRef=n}enable(){if(this._scrollSubscription)return;let n=this._scrollDispatcher.scrolled(0).pipe(he(e=>!e||!this._overlayRef.overlayElement.contains(e.getElementRef().nativeElement)));this._config&&this._config.threshold&&this._config.threshold>1?(this._initialScrollPosition=this._viewportRuler.getViewportScrollPosition().top,this._scrollSubscription=n.subscribe(()=>{let e=this._viewportRuler.getViewportScrollPosition().top;Math.abs(e-this._initialScrollPosition)>this._config.threshold?this._detach():this._overlayRef.updatePosition()})):this._scrollSubscription=n.subscribe(this._detach)}disable(){this._scrollSubscription&&(this._scrollSubscription.unsubscribe(),this._scrollSubscription=null)}detach(){this.disable(),this._overlayRef=null}_detach=()=>{this.disable(),this._overlayRef.hasAttached()&&this._ngZone.run(()=>this._overlayRef.detach())}};var wa=class{enable(){}disable(){}attach(){}};function Ug(t,n){return n.some(e=>{let r=t.bottome.bottom,i=t.righte.right;return r||o||i||s})}function CI(t,n){return n.some(e=>{let r=t.tope.bottom,i=t.lefte.right;return r||o||i||s})}function zg(t,n){return new id(t.get(Da),t.get(Si),t.get(j),n)}var id=class{_scrollDispatcher;_viewportRuler;_ngZone;_config;_scrollSubscription=null;_overlayRef;constructor(n,e,r,o){this._scrollDispatcher=n,this._viewportRuler=e,this._ngZone=r,this._config=o}attach(n){this._overlayRef,this._overlayRef=n}enable(){if(!this._scrollSubscription){let n=this._config?this._config.scrollThrottle:0;this._scrollSubscription=this._scrollDispatcher.scrolled(n).subscribe(()=>{if(this._overlayRef.updatePosition(),this._config&&this._config.autoClose){let e=this._overlayRef.overlayElement.getBoundingClientRect(),{width:r,height:o}=this._viewportRuler.getViewportSize();Ug(e,[{width:r,height:o,bottom:o,right:r,top:0,left:0}])&&(this.disable(),this._ngZone.run(()=>this._overlayRef.detach()))}})}}disable(){this._scrollSubscription&&(this._scrollSubscription.unsubscribe(),this._scrollSubscription=null)}detach(){this.disable(),this._overlayRef=null}},NI=(()=>{class t{_injector=f(W);constructor(){}noop=()=>new wa;close=e=>RI(this._injector,e);block=()=>AI(this._injector);reposition=e=>zg(this._injector,e);static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Ca=class{positionStrategy;scrollStrategy=new wa;panelClass="";hasBackdrop=!1;backdropClass="cdk-overlay-dark-backdrop";disableAnimations;width;height;minWidth;minHeight;maxWidth;maxHeight;direction;disposeOnNavigation=!1;usePopover;eventPredicate;constructor(n){if(n){let e=Object.keys(n);for(let r of e)n[r]!==void 0&&(this[r]=n[r])}}};var sd=class{connectionPair;scrollableViewProperties;constructor(n,e){this.connectionPair=n,this.scrollableViewProperties=e}};var OI=(()=>{class t{_attachedOverlays=[];_document=f(O);_isAttached=!1;constructor(){}ngOnDestroy(){this.detach()}add(e){this.remove(e),this._attachedOverlays.push(e)}remove(e){let r=this._attachedOverlays.indexOf(e);r>-1&&this._attachedOverlays.splice(r,1),this._attachedOverlays.length===0&&this.detach()}canReceiveEvent(e,r,o){return o.observers.length<1?!1:e.eventPredicate?e.eventPredicate(r):!0}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),kI=(()=>{class t extends OI{_ngZone=f(j);_renderer=f(He).createRenderer(null,null);_cleanupKeydown;add(e){super.add(e),this._isAttached||(this._ngZone.runOutsideAngular(()=>{this._cleanupKeydown=this._renderer.listen("body","keydown",this._keydownListener)}),this._isAttached=!0)}detach(){this._isAttached&&(this._cleanupKeydown?.(),this._isAttached=!1)}_keydownListener=e=>{let r=this._attachedOverlays;for(let o=r.length-1;o>-1;o--){let i=r[o];if(this.canReceiveEvent(i,e,i._keydownEvents)){this._ngZone.run(()=>i._keydownEvents.next(e));break}}};static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),FI=(()=>{class t extends OI{_platform=f(_e);_ngZone=f(j);_renderer=f(He).createRenderer(null,null);_cursorOriginalValue;_cursorStyleIsSet=!1;_pointerDownEventTarget=null;_cleanups;add(e){if(super.add(e),!this._isAttached){let r=this._document.body,o={capture:!0},i=this._renderer;this._cleanups=this._ngZone.runOutsideAngular(()=>[i.listen(r,"pointerdown",this._pointerDownListener,o),i.listen(r,"click",this._clickListener,o),i.listen(r,"auxclick",this._clickListener,o),i.listen(r,"contextmenu",this._clickListener,o)]),this._platform.IOS&&!this._cursorStyleIsSet&&(this._cursorOriginalValue=r.style.cursor,r.style.cursor="pointer",this._cursorStyleIsSet=!0),this._isAttached=!0}}detach(){this._isAttached&&(this._cleanups?.forEach(e=>e()),this._cleanups=void 0,this._platform.IOS&&this._cursorStyleIsSet&&(this._document.body.style.cursor=this._cursorOriginalValue,this._cursorStyleIsSet=!1),this._isAttached=!1)}_pointerDownListener=e=>{this._pointerDownEventTarget=ot(e)};_clickListener=e=>{let r=ot(e),o=e.type==="click"&&this._pointerDownEventTarget?this._pointerDownEventTarget:r;this._pointerDownEventTarget=null;let i=this._attachedOverlays.slice();for(let s=i.length-1;s>-1;s--){let a=i[s],c=a._outsidePointerEvents;if(!(!a.hasAttached()||!this.canReceiveEvent(a,e,c))){if(II(a.overlayElement,r)||II(a.overlayElement,o))break;this._ngZone?this._ngZone.run(()=>c.next(e)):c.next(e)}}};static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function II(t,n){let e=typeof ShadowRoot<"u"&&ShadowRoot,r=n;for(;r;){if(r===t)return!0;r=e&&r instanceof ShadowRoot?r.host:r.parentNode}return!1}var PI=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=Te({type:t,selectors:[["ng-component"]],hostAttrs:["cdk-overlay-style-loader",""],decls:0,vars:0,template:function(r,o){},styles:[`.cdk-overlay-container, .cdk-global-overlay-wrapper { + pointer-events: none; + top: 0; + left: 0; + height: 100%; + width: 100%; +} + +.cdk-overlay-container { + position: fixed; +} +@layer cdk-overlay { + .cdk-overlay-container { + z-index: 1000; + } +} +.cdk-overlay-container:empty { + display: none; +} + +.cdk-global-overlay-wrapper { + display: flex; + position: absolute; +} +@layer cdk-overlay { + .cdk-global-overlay-wrapper { + z-index: 1000; + } +} + +.cdk-overlay-pane { + position: absolute; + pointer-events: auto; + box-sizing: border-box; + display: flex; + max-width: 100%; + max-height: 100%; +} +@layer cdk-overlay { + .cdk-overlay-pane { + z-index: 1000; + } +} + +.cdk-overlay-backdrop { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + pointer-events: auto; + -webkit-tap-highlight-color: transparent; + opacity: 0; + touch-action: manipulation; +} +@layer cdk-overlay { + .cdk-overlay-backdrop { + z-index: 1000; + transition: opacity 400ms cubic-bezier(0.25, 0.8, 0.25, 1); + } +} +@media (prefers-reduced-motion) { + .cdk-overlay-backdrop { + transition-duration: 1ms; + } +} + +.cdk-overlay-backdrop-showing { + opacity: 1; +} +@media (forced-colors: active) { + .cdk-overlay-backdrop-showing { + opacity: 0.6; + } +} + +@layer cdk-overlay { + .cdk-overlay-dark-backdrop { + background: rgba(0, 0, 0, 0.32); + } +} + +.cdk-overlay-transparent-backdrop { + transition: visibility 1ms linear, opacity 1ms linear; + visibility: hidden; + opacity: 1; +} +.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing, .cdk-high-contrast-active .cdk-overlay-transparent-backdrop { + opacity: 0; + visibility: visible; +} + +.cdk-overlay-backdrop-noop-animation { + transition: none; +} + +.cdk-overlay-connected-position-bounding-box { + position: absolute; + display: flex; + flex-direction: column; + min-width: 1px; + min-height: 1px; +} +@layer cdk-overlay { + .cdk-overlay-connected-position-bounding-box { + z-index: 1000; + } +} + +.cdk-global-scrollblock { + position: fixed; + width: 100%; + overflow-y: scroll; +} + +.cdk-overlay-popover { + background: none; + border: none; + padding: 0; + outline: 0; + overflow: visible; + position: fixed; + pointer-events: none; + white-space: normal; + color: inherit; + text-decoration: none; + width: 100%; + height: 100%; + inset: auto; + top: 0; + left: 0; +} +.cdk-overlay-popover::backdrop { + display: none; +} +.cdk-overlay-popover .cdk-overlay-backdrop { + position: fixed; + z-index: auto; +} +`],encapsulation:2,changeDetection:0})}return t})(),Gg=(()=>{class t{_platform=f(_e);_containerElement;_document=f(O);_styleLoader=f(Nt);constructor(){}ngOnDestroy(){this._containerElement?.remove()}getContainerElement(){return this._loadStyles(),this._containerElement||this._createContainer(),this._containerElement}_createContainer(){let e="cdk-overlay-container";if(this._platform.isBrowser||Tg()){let o=this._document.querySelectorAll(`.${e}[platform="server"], .${e}[platform="test"]`);for(let i=0;i{let n=this.element;clearTimeout(this._fallbackTimeout),this._cleanupTransitionEnd?.(),this._cleanupTransitionEnd=this._renderer.listen(n,"transitionend",this.dispose),this._fallbackTimeout=setTimeout(this.dispose,500),n.style.pointerEvents="none",n.classList.remove("cdk-overlay-backdrop-showing")})}dispose=()=>{clearTimeout(this._fallbackTimeout),this._cleanupClick?.(),this._cleanupTransitionEnd?.(),this._cleanupClick=this._cleanupTransitionEnd=this._fallbackTimeout=void 0,this.element.remove()}};function Wg(t){return t&&t.nodeType===1}var ad=class{_portalOutlet;_host;_pane;_config;_ngZone;_keyboardDispatcher;_document;_location;_outsideClickDispatcher;_animationsDisabled;_injector;_renderer;_backdropClick=new x;_attachments=new x;_detachments=new x;_positionStrategy;_scrollStrategy;_locationChanges=G.EMPTY;_backdropRef=null;_detachContentMutationObserver;_detachContentAfterRenderRef;_disposed=!1;_previousHostParent;_keydownEvents=new x;_outsidePointerEvents=new x;_afterNextRenderRef;constructor(n,e,r,o,i,s,a,c,l,u=!1,d,h){this._portalOutlet=n,this._host=e,this._pane=r,this._config=o,this._ngZone=i,this._keyboardDispatcher=s,this._document=a,this._location=c,this._outsideClickDispatcher=l,this._animationsDisabled=u,this._injector=d,this._renderer=h,o.scrollStrategy&&(this._scrollStrategy=o.scrollStrategy,this._scrollStrategy.attach(this)),this._positionStrategy=o.positionStrategy}get overlayElement(){return this._pane}get backdropElement(){return this._backdropRef?.element||null}get hostElement(){return this._host}get eventPredicate(){return this._config?.eventPredicate||null}attach(n){if(this._disposed)return null;this._attachHost();let e=this._portalOutlet.attach(n);return this._positionStrategy?.attach(this),this._updateStackingOrder(),this._updateElementSize(),this._updateElementDirection(),this._scrollStrategy&&this._scrollStrategy.enable(),this._afterNextRenderRef?.destroy(),this._afterNextRenderRef=At(()=>{this.hasAttached()&&this.updatePosition()},{injector:this._injector}),this._togglePointerEvents(!0),this._config.hasBackdrop&&this._attachBackdrop(),this._config.panelClass&&this._toggleClasses(this._pane,this._config.panelClass,!0),this._attachments.next(),this._completeDetachContent(),this._keyboardDispatcher.add(this),this._config.disposeOnNavigation&&(this._locationChanges=this._location.subscribe(()=>this.dispose())),this._outsideClickDispatcher.add(this),typeof e?.onDestroy=="function"&&e.onDestroy(()=>{this.hasAttached()&&this._ngZone.runOutsideAngular(()=>Promise.resolve().then(()=>this.detach()))}),e}detach(){if(!this.hasAttached())return;this.detachBackdrop(),this._togglePointerEvents(!1),this._positionStrategy&&this._positionStrategy.detach&&this._positionStrategy.detach(),this._scrollStrategy&&this._scrollStrategy.disable();let n=this._portalOutlet.detach();return this._detachments.next(),this._completeDetachContent(),this._keyboardDispatcher.remove(this),this._detachContentWhenEmpty(),this._locationChanges.unsubscribe(),this._outsideClickDispatcher.remove(this),n}dispose(){if(this._disposed)return;let n=this.hasAttached();this._positionStrategy&&this._positionStrategy.dispose(),this._disposeScrollStrategy(),this._backdropRef?.dispose(),this._locationChanges.unsubscribe(),this._keyboardDispatcher.remove(this),this._portalOutlet.dispose(),this._attachments.complete(),this._backdropClick.complete(),this._keydownEvents.complete(),this._outsidePointerEvents.complete(),this._outsideClickDispatcher.remove(this),this._host?.remove(),this._afterNextRenderRef?.destroy(),this._previousHostParent=this._pane=this._host=this._backdropRef=null,n&&this._detachments.next(),this._detachments.complete(),this._completeDetachContent(),this._disposed=!0}hasAttached(){return this._portalOutlet.hasAttached()}backdropClick(){return this._backdropClick}attachments(){return this._attachments}detachments(){return this._detachments}keydownEvents(){return this._keydownEvents}outsidePointerEvents(){return this._outsidePointerEvents}getConfig(){return this._config}updatePosition(){this._positionStrategy&&this._positionStrategy.apply()}updatePositionStrategy(n){n!==this._positionStrategy&&(this._positionStrategy&&this._positionStrategy.dispose(),this._positionStrategy=n,this.hasAttached()&&(n.attach(this),this.updatePosition()))}updateSize(n){this._config=g(g({},this._config),n),this._updateElementSize()}setDirection(n){this._config=F(g({},this._config),{direction:n}),this._updateElementDirection()}addPanelClass(n){this._pane&&this._toggleClasses(this._pane,n,!0)}removePanelClass(n){this._pane&&this._toggleClasses(this._pane,n,!1)}getDirection(){let n=this._config.direction;return n?typeof n=="string"?n:n.value:"ltr"}updateScrollStrategy(n){n!==this._scrollStrategy&&(this._disposeScrollStrategy(),this._scrollStrategy=n,this.hasAttached()&&(n.attach(this),n.enable()))}_updateElementDirection(){this._host.setAttribute("dir",this.getDirection())}_updateElementSize(){if(!this._pane)return;let n=this._pane.style;n.width=Ce(this._config.width),n.height=Ce(this._config.height),n.minWidth=Ce(this._config.minWidth),n.minHeight=Ce(this._config.minHeight),n.maxWidth=Ce(this._config.maxWidth),n.maxHeight=Ce(this._config.maxHeight)}_togglePointerEvents(n){this._pane.style.pointerEvents=n?"":"none"}_attachHost(){if(!this._host.parentElement){let n=this._config.usePopover?this._positionStrategy?.getPopoverInsertionPoint?.():null;Wg(n)?n.after(this._host):n?.type==="parent"?n.element.appendChild(this._host):this._previousHostParent?.appendChild(this._host)}if(this._config.usePopover)try{this._host.showPopover()}catch{}}_attachBackdrop(){let n="cdk-overlay-backdrop-showing";this._backdropRef?.dispose(),this._backdropRef=new Hg(this._document,this._renderer,this._ngZone,e=>{this._backdropClick.next(e)}),this._animationsDisabled&&this._backdropRef.element.classList.add("cdk-overlay-backdrop-noop-animation"),this._config.backdropClass&&this._toggleClasses(this._backdropRef.element,this._config.backdropClass,!0),this._config.usePopover?this._host.prepend(this._backdropRef.element):this._host.parentElement.insertBefore(this._backdropRef.element,this._host),!this._animationsDisabled&&typeof requestAnimationFrame<"u"?this._ngZone.runOutsideAngular(()=>{requestAnimationFrame(()=>this._backdropRef?.element.classList.add(n))}):this._backdropRef.element.classList.add(n)}_updateStackingOrder(){!this._config.usePopover&&this._host.nextSibling&&this._host.parentNode.appendChild(this._host)}detachBackdrop(){this._animationsDisabled?(this._backdropRef?.dispose(),this._backdropRef=null):this._backdropRef?.detach()}_toggleClasses(n,e,r){let o=mo(e||[]).filter(i=>!!i);o.length&&(r?n.classList.add(...o):n.classList.remove(...o))}_detachContentWhenEmpty(){let n=!1;try{this._detachContentAfterRenderRef=At(()=>{n=!0,this._detachContent()},{injector:this._injector})}catch(e){if(n)throw e;this._detachContent()}globalThis.MutationObserver&&this._pane&&(this._detachContentMutationObserver||=new globalThis.MutationObserver(()=>{this._detachContent()}),this._detachContentMutationObserver.observe(this._pane,{childList:!0}))}_detachContent(){(!this._pane||!this._host||this._pane.children.length===0)&&(this._pane&&this._config.panelClass&&this._toggleClasses(this._pane,this._config.panelClass,!1),this._host&&this._host.parentElement&&(this._previousHostParent=this._host.parentElement,this._host.remove()),this._completeDetachContent())}_completeDetachContent(){this._detachContentAfterRenderRef?.destroy(),this._detachContentAfterRenderRef=void 0,this._detachContentMutationObserver?.disconnect()}_disposeScrollStrategy(){let n=this._scrollStrategy;n?.disable(),n?.detach?.()}},SI="cdk-overlay-connected-position-bounding-box",lP=/([A-Za-z%]+)$/;function qg(t,n){return new cd(n,t.get(Si),t.get(O),t.get(_e),t.get(Gg))}var cd=class{_viewportRuler;_document;_platform;_overlayContainer;_overlayRef;_isInitialRender=!1;_lastBoundingBoxSize={width:0,height:0};_isPushed=!1;_canPush=!0;_growAfterOpen=!1;_hasFlexibleDimensions=!0;_positionLocked=!1;_originRect;_overlayRect;_viewportRect;_containerRect;_viewportMargin=0;_scrollables=[];_preferredPositions=[];_origin;_pane;_isDisposed=!1;_boundingBox=null;_lastPosition=null;_lastScrollVisibility=null;_positionChanges=new x;_resizeSubscription=G.EMPTY;_offsetX=0;_offsetY=0;_transformOriginSelector;_appliedPanelClasses=[];_previousPushAmount=null;_popoverLocation="global";positionChanges=this._positionChanges;get positions(){return this._preferredPositions}constructor(n,e,r,o,i){this._viewportRuler=e,this._document=r,this._platform=o,this._overlayContainer=i,this.setOrigin(n)}attach(n){this._overlayRef&&this._overlayRef,this._validatePositions(),n.hostElement.classList.add(SI),this._overlayRef=n,this._boundingBox=n.hostElement,this._pane=n.overlayElement,this._isDisposed=!1,this._isInitialRender=!0,this._lastPosition=null,this._resizeSubscription.unsubscribe(),this._resizeSubscription=this._viewportRuler.change().subscribe(()=>{this._isInitialRender=!0,this.apply()})}apply(){if(this._isDisposed||!this._platform.isBrowser)return;if(!this._isInitialRender&&this._positionLocked&&this._lastPosition){this.reapplyLastPosition();return}this._clearPanelClasses(),this._resetOverlayElementStyles(),this._resetBoundingBoxStyles(),this._viewportRect=this._getNarrowedViewportRect(),this._originRect=this._getOriginRect(),this._overlayRect=this._pane.getBoundingClientRect(),this._containerRect=this._getContainerRect();let n=this._originRect,e=this._overlayRect,r=this._viewportRect,o=this._containerRect,i=[],s;for(let a of this._preferredPositions){let c=this._getOriginPoint(n,o,a),l=this._getOverlayPoint(c,e,a),u=this._getOverlayFit(l,e,r,a);if(u.isCompletelyWithinViewport){this._isPushed=!1,this._applyPosition(a,c);return}if(this._canFitWithFlexibleDimensions(u,l,r)){i.push({position:a,origin:c,overlayRect:e,boundingBoxRect:this._calculateBoundingBoxRect(c,a)});continue}(!s||s.overlayFit.visibleAreac&&(c=u,a=l)}this._isPushed=!1,this._applyPosition(a.position,a.origin);return}if(this._canPush){this._isPushed=!0,this._applyPosition(s.position,s.originPoint);return}this._applyPosition(s.position,s.originPoint)}detach(){this._clearPanelClasses(),this._lastPosition=null,this._previousPushAmount=null,this._resizeSubscription.unsubscribe()}dispose(){this._isDisposed||(this._boundingBox&&yo(this._boundingBox.style,{top:"",left:"",right:"",bottom:"",height:"",width:"",alignItems:"",justifyContent:""}),this._pane&&this._resetOverlayElementStyles(),this._overlayRef&&this._overlayRef.hostElement.classList.remove(SI),this.detach(),this._positionChanges.complete(),this._overlayRef=this._boundingBox=null,this._isDisposed=!0)}reapplyLastPosition(){if(this._isDisposed||!this._platform.isBrowser)return;let n=this._lastPosition;n?(this._originRect=this._getOriginRect(),this._overlayRect=this._pane.getBoundingClientRect(),this._viewportRect=this._getNarrowedViewportRect(),this._containerRect=this._getContainerRect(),this._applyPosition(n,this._getOriginPoint(this._originRect,this._containerRect,n))):this.apply()}withScrollableContainers(n){return this._scrollables=n,this}withPositions(n){return this._preferredPositions=n,n.indexOf(this._lastPosition)===-1&&(this._lastPosition=null),this._validatePositions(),this}withViewportMargin(n){return this._viewportMargin=n,this}withFlexibleDimensions(n=!0){return this._hasFlexibleDimensions=n,this}withGrowAfterOpen(n=!0){return this._growAfterOpen=n,this}withPush(n=!0){return this._canPush=n,this}withLockedPosition(n=!0){return this._positionLocked=n,this}setOrigin(n){return this._origin=n,this}withDefaultOffsetX(n){return this._offsetX=n,this}withDefaultOffsetY(n){return this._offsetY=n,this}withTransformOriginOn(n){return this._transformOriginSelector=n,this}withPopoverLocation(n){return this._popoverLocation=n,this}getPopoverInsertionPoint(){return this._popoverLocation==="global"?null:this._popoverLocation!=="inline"?this._popoverLocation:this._origin instanceof $?this._origin.nativeElement:Wg(this._origin)?this._origin:null}_getOriginPoint(n,e,r){let o;if(r.originX=="center")o=n.left+n.width/2;else{let s=this._isRtl()?n.right:n.left,a=this._isRtl()?n.left:n.right;o=r.originX=="start"?s:a}e.left<0&&(o-=e.left);let i;return r.originY=="center"?i=n.top+n.height/2:i=r.originY=="top"?n.top:n.bottom,e.top<0&&(i-=e.top),{x:o,y:i}}_getOverlayPoint(n,e,r){let o;r.overlayX=="center"?o=-e.width/2:r.overlayX==="start"?o=this._isRtl()?-e.width:0:o=this._isRtl()?0:-e.width;let i;return r.overlayY=="center"?i=-e.height/2:i=r.overlayY=="top"?0:-e.height,{x:n.x+o,y:n.y+i}}_getOverlayFit(n,e,r,o){let i=TI(e),{x:s,y:a}=n,c=this._getOffset(o,"x"),l=this._getOffset(o,"y");c&&(s+=c),l&&(a+=l);let u=0-s,d=s+i.width-r.width,h=0-a,p=a+i.height-r.height,m=this._subtractOverflows(i.width,u,d),_=this._subtractOverflows(i.height,h,p),D=m*_;return{visibleArea:D,isCompletelyWithinViewport:i.width*i.height===D,fitsInViewportVertically:_===i.height,fitsInViewportHorizontally:m==i.width}}_canFitWithFlexibleDimensions(n,e,r){if(this._hasFlexibleDimensions){let o=r.bottom-e.y,i=r.right-e.x,s=MI(this._overlayRef.getConfig().minHeight),a=MI(this._overlayRef.getConfig().minWidth),c=n.fitsInViewportVertically||s!=null&&s<=o,l=n.fitsInViewportHorizontally||a!=null&&a<=i;return c&&l}return!1}_pushOverlayOnScreen(n,e,r){if(this._previousPushAmount&&this._positionLocked)return{x:n.x+this._previousPushAmount.x,y:n.y+this._previousPushAmount.y};let o=TI(e),i=this._viewportRect,s=Math.max(n.x+o.width-i.width,0),a=Math.max(n.y+o.height-i.height,0),c=Math.max(i.top-r.top-n.y,0),l=Math.max(i.left-r.left-n.x,0),u=0,d=0;return o.width<=i.width?u=l||-s:u=n.xm&&!this._isInitialRender&&!this._growAfterOpen&&(s=n.y-m/2)}let c=e.overlayX==="start"&&!o||e.overlayX==="end"&&o,l=e.overlayX==="end"&&!o||e.overlayX==="start"&&o,u,d,h;if(l)h=r.width-n.x+this._getViewportMarginStart()+this._getViewportMarginEnd(),u=n.x-this._getViewportMarginStart();else if(c)d=n.x,u=r.right-n.x-this._getViewportMarginEnd();else{let p=Math.min(r.right-n.x+r.left,n.x),m=this._lastBoundingBoxSize.width;u=p*2,d=n.x-p,u>m&&!this._isInitialRender&&!this._growAfterOpen&&(d=n.x-m/2)}return{top:s,left:d,bottom:a,right:h,width:u,height:i}}_setBoundingBoxStyles(n,e){let r=this._calculateBoundingBoxRect(n,e);!this._isInitialRender&&!this._growAfterOpen&&(r.height=Math.min(r.height,this._lastBoundingBoxSize.height),r.width=Math.min(r.width,this._lastBoundingBoxSize.width));let o={};if(this._hasExactPosition())o.top=o.left="0",o.bottom=o.right="auto",o.maxHeight=o.maxWidth="",o.width=o.height="100%";else{let i=this._overlayRef.getConfig().maxHeight,s=this._overlayRef.getConfig().maxWidth;o.width=Ce(r.width),o.height=Ce(r.height),o.top=Ce(r.top)||"auto",o.bottom=Ce(r.bottom)||"auto",o.left=Ce(r.left)||"auto",o.right=Ce(r.right)||"auto",e.overlayX==="center"?o.alignItems="center":o.alignItems=e.overlayX==="end"?"flex-end":"flex-start",e.overlayY==="center"?o.justifyContent="center":o.justifyContent=e.overlayY==="bottom"?"flex-end":"flex-start",i&&(o.maxHeight=Ce(i)),s&&(o.maxWidth=Ce(s))}this._lastBoundingBoxSize=r,yo(this._boundingBox.style,o)}_resetBoundingBoxStyles(){yo(this._boundingBox.style,{top:"0",left:"0",right:"0",bottom:"0",height:"",width:"",alignItems:"",justifyContent:""})}_resetOverlayElementStyles(){yo(this._pane.style,{top:"",left:"",bottom:"",right:"",position:"",transform:""})}_setOverlayElementStyles(n,e){let r={},o=this._hasExactPosition(),i=this._hasFlexibleDimensions,s=this._overlayRef.getConfig();if(o){let u=this._viewportRuler.getViewportScrollPosition();yo(r,this._getExactOverlayY(e,n,u)),yo(r,this._getExactOverlayX(e,n,u))}else r.position="static";let a="",c=this._getOffset(e,"x"),l=this._getOffset(e,"y");c&&(a+=`translateX(${c}px) `),l&&(a+=`translateY(${l}px)`),r.transform=a.trim(),s.maxHeight&&(o?r.maxHeight=Ce(s.maxHeight):i&&(r.maxHeight="")),s.maxWidth&&(o?r.maxWidth=Ce(s.maxWidth):i&&(r.maxWidth="")),yo(this._pane.style,r)}_getExactOverlayY(n,e,r){let o={top:"",bottom:""},i=this._getOverlayPoint(e,this._overlayRect,n);if(this._isPushed&&(i=this._pushOverlayOnScreen(i,this._overlayRect,r)),n.overlayY==="bottom"){let s=this._document.documentElement.clientHeight;o.bottom=`${s-(i.y+this._overlayRect.height)}px`}else o.top=Ce(i.y);return o}_getExactOverlayX(n,e,r){let o={left:"",right:""},i=this._getOverlayPoint(e,this._overlayRect,n);this._isPushed&&(i=this._pushOverlayOnScreen(i,this._overlayRect,r));let s;if(this._isRtl()?s=n.overlayX==="end"?"left":"right":s=n.overlayX==="end"?"right":"left",s==="right"){let a=this._document.documentElement.clientWidth;o.right=`${a-(i.x+this._overlayRect.width)}px`}else o.left=Ce(i.x);return o}_getScrollVisibility(){let n=this._getOriginRect(),e=this._pane.getBoundingClientRect(),r=this._scrollables.map(o=>o.getElementRef().nativeElement.getBoundingClientRect());return{isOriginClipped:CI(n,r),isOriginOutsideView:Ug(n,r),isOverlayClipped:CI(e,r),isOverlayOutsideView:Ug(e,r)}}_subtractOverflows(n,...e){return e.reduce((r,o)=>r-Math.max(o,0),n)}_getNarrowedViewportRect(){let n=this._document.documentElement.clientWidth,e=this._document.documentElement.clientHeight,r=this._viewportRuler.getViewportScrollPosition();return{top:r.top+this._getViewportMarginTop(),left:r.left+this._getViewportMarginStart(),right:r.left+n-this._getViewportMarginEnd(),bottom:r.top+e-this._getViewportMarginBottom(),width:n-this._getViewportMarginStart()-this._getViewportMarginEnd(),height:e-this._getViewportMarginTop()-this._getViewportMarginBottom()}}_isRtl(){return this._overlayRef.getDirection()==="rtl"}_hasExactPosition(){return!this._hasFlexibleDimensions||this._isPushed}_getOffset(n,e){return e==="x"?n.offsetX==null?this._offsetX:n.offsetX:n.offsetY==null?this._offsetY:n.offsetY}_validatePositions(){}_addPanelClasses(n){this._pane&&mo(n).forEach(e=>{e!==""&&this._appliedPanelClasses.indexOf(e)===-1&&(this._appliedPanelClasses.push(e),this._pane.classList.add(e))})}_clearPanelClasses(){this._pane&&(this._appliedPanelClasses.forEach(n=>{this._pane.classList.remove(n)}),this._appliedPanelClasses=[])}_getViewportMarginStart(){return typeof this._viewportMargin=="number"?this._viewportMargin:this._viewportMargin?.start??0}_getViewportMarginEnd(){return typeof this._viewportMargin=="number"?this._viewportMargin:this._viewportMargin?.end??0}_getViewportMarginTop(){return typeof this._viewportMargin=="number"?this._viewportMargin:this._viewportMargin?.top??0}_getViewportMarginBottom(){return typeof this._viewportMargin=="number"?this._viewportMargin:this._viewportMargin?.bottom??0}_getOriginRect(){let n=this._origin;if(n instanceof $)return n.nativeElement.getBoundingClientRect();if(n instanceof Element)return n.getBoundingClientRect();let e=n.width||0,r=n.height||0;return{top:n.y,bottom:n.y+r,left:n.x,right:n.x+e,height:r,width:e}}_getContainerRect(){let n=this._overlayRef.getConfig().usePopover&&this._popoverLocation!=="global",e=this._overlayContainer.getContainerElement();n&&(e.style.display="block");let r=e.getBoundingClientRect();return n&&(e.style.display=""),r}};function yo(t,n){for(let e in n)n.hasOwnProperty(e)&&(t[e]=n[e]);return t}function MI(t){if(typeof t!="number"&&t!=null){let[n,e]=t.split(lP);return!e||e==="px"?parseFloat(n):null}return t||null}function TI(t){return{top:Math.floor(t.top),right:Math.floor(t.right),bottom:Math.floor(t.bottom),left:Math.floor(t.left),width:Math.floor(t.width),height:Math.floor(t.height)}}function uP(t,n){return t===n?!0:t.isOriginClipped===n.isOriginClipped&&t.isOriginOutsideView===n.isOriginOutsideView&&t.isOverlayClipped===n.isOverlayClipped&&t.isOverlayOutsideView===n.isOverlayOutsideView}var xI="cdk-global-overlay-wrapper";function LI(t){return new ld}var ld=class{_overlayRef;_cssPosition="static";_topOffset="";_bottomOffset="";_alignItems="";_xPosition="";_xOffset="";_width="";_height="";_isDisposed=!1;attach(n){let e=n.getConfig();this._overlayRef=n,this._width&&!e.width&&n.updateSize({width:this._width}),this._height&&!e.height&&n.updateSize({height:this._height}),n.hostElement.classList.add(xI),this._isDisposed=!1}top(n=""){return this._bottomOffset="",this._topOffset=n,this._alignItems="flex-start",this}left(n=""){return this._xOffset=n,this._xPosition="left",this}bottom(n=""){return this._topOffset="",this._bottomOffset=n,this._alignItems="flex-end",this}right(n=""){return this._xOffset=n,this._xPosition="right",this}start(n=""){return this._xOffset=n,this._xPosition="start",this}end(n=""){return this._xOffset=n,this._xPosition="end",this}width(n=""){return this._overlayRef?this._overlayRef.updateSize({width:n}):this._width=n,this}height(n=""){return this._overlayRef?this._overlayRef.updateSize({height:n}):this._height=n,this}centerHorizontally(n=""){return this.left(n),this._xPosition="center",this}centerVertically(n=""){return this.top(n),this._alignItems="center",this}apply(){if(!this._overlayRef||!this._overlayRef.hasAttached())return;let n=this._overlayRef.overlayElement.style,e=this._overlayRef.hostElement.style,r=this._overlayRef.getConfig(),{width:o,height:i,maxWidth:s,maxHeight:a}=r,c=(o==="100%"||o==="100vw")&&(!s||s==="100%"||s==="100vw"),l=(i==="100%"||i==="100vh")&&(!a||a==="100%"||a==="100vh"),u=this._xPosition,d=this._xOffset,h=this._overlayRef.getConfig().direction==="rtl",p="",m="",_="";c?_="flex-start":u==="center"?(_="center",h?m=d:p=d):h?u==="left"||u==="end"?(_="flex-end",p=d):(u==="right"||u==="start")&&(_="flex-start",m=d):u==="left"||u==="start"?(_="flex-start",p=d):(u==="right"||u==="end")&&(_="flex-end",m=d),n.position=this._cssPosition,n.marginLeft=c?"0":p,n.marginTop=l?"0":this._topOffset,n.marginBottom=this._bottomOffset,n.marginRight=c?"0":m,e.justifyContent=_,e.alignItems=l?"flex-start":this._alignItems}dispose(){if(this._isDisposed||!this._overlayRef)return;let n=this._overlayRef.overlayElement.style,e=this._overlayRef.hostElement,r=e.style;e.classList.remove(xI),r.justifyContent=r.alignItems=n.marginTop=n.marginBottom=n.marginLeft=n.marginRight=n.position="",this._overlayRef=null,this._isDisposed=!0}},jI=(()=>{class t{_injector=f(W);constructor(){}global(){return LI()}flexibleConnectedTo(e){return qg(this._injector,e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Yg=new y("OVERLAY_DEFAULT_CONFIG");function Zg(t,n){t.get(Nt).load(PI);let e=t.get(Gg),r=t.get(O),o=t.get(va),i=t.get(Ge),s=t.get(_i),a=t.get(Le,null,{optional:!0})||t.get(He).createRenderer(null,null),c=new Ca(n),l=t.get(Yg,null,{optional:!0})?.usePopover??!0;c.direction=c.direction||s.value,"showPopover"in r.body?c.usePopover=n?.usePopover??l:c.usePopover=!1;let u=r.createElement("div"),d=r.createElement("div");u.id=o.getId("cdk-overlay-"),u.classList.add("cdk-overlay-pane"),d.appendChild(u),c.usePopover&&(d.setAttribute("popover","manual"),d.classList.add("cdk-overlay-popover"));let h=c.usePopover?c.positionStrategy?.getPopoverInsertionPoint?.():null;return Wg(h)?h.after(d):h?.type==="parent"?h.element.appendChild(d):e.getContainerElement().appendChild(d),new ad(new nd(u,i,t),d,u,c,t.get(j),t.get(kI),r,t.get(Dn),t.get(FI),n?.disableAnimations??t.get(Fs,null,{optional:!0})==="NoopAnimations",t.get(re),a)}var VI=(()=>{class t{scrollStrategies=f(NI);_positionBuilder=f(jI);_injector=f(W);constructor(){}create(e){return Zg(this._injector,e)}position(){return this._positionBuilder}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),dP=[{originX:"start",originY:"bottom",overlayX:"start",overlayY:"top"},{originX:"start",originY:"top",overlayX:"start",overlayY:"bottom"},{originX:"end",originY:"top",overlayX:"end",overlayY:"bottom"},{originX:"end",originY:"bottom",overlayX:"end",overlayY:"top"}],fP=new y("cdk-connected-overlay-scroll-strategy",{providedIn:"root",factory:()=>{let t=f(W);return()=>zg(t)}}),$g=(()=>{class t{elementRef=f($);constructor(){}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["","cdk-overlay-origin",""],["","overlay-origin",""],["","cdkOverlayOrigin",""]],exportAs:["cdkOverlayOrigin"]})}return t})(),BI=new y("cdk-connected-overlay-default-config"),hP=(()=>{class t{_dir=f(_i,{optional:!0});_injector=f(W);_overlayRef;_templatePortal;_backdropSubscription=G.EMPTY;_attachSubscription=G.EMPTY;_detachSubscription=G.EMPTY;_positionSubscription=G.EMPTY;_offsetX;_offsetY;_position;_scrollStrategyFactory=f(fP);_ngZone=f(j);origin;positions;positionStrategy;get offsetX(){return this._offsetX}set offsetX(e){this._offsetX=e,this._position&&this._updatePositionStrategy(this._position)}get offsetY(){return this._offsetY}set offsetY(e){this._offsetY=e,this._position&&this._updatePositionStrategy(this._position)}width;height;minWidth;minHeight;backdropClass;panelClass;viewportMargin=0;scrollStrategy;open=!1;disableClose=!1;transformOriginSelector;hasBackdrop=!1;lockPosition=!1;flexibleDimensions=!1;growAfterOpen=!1;push=!1;disposeOnNavigation=!1;usePopover;matchWidth=!1;set _config(e){typeof e!="string"&&this._assignConfig(e)}backdropClick=new H;positionChange=new H;attach=new H;detach=new H;overlayKeydown=new H;overlayOutsideClick=new H;constructor(){let e=f(gt),r=f(Qe),o=f(BI,{optional:!0}),i=f(Yg,{optional:!0});this.usePopover=i?.usePopover===!1?null:"global",this._templatePortal=new Mi(e,r),this.scrollStrategy=this._scrollStrategyFactory(),o&&this._assignConfig(o)}get overlayRef(){return this._overlayRef}get dir(){return this._dir?this._dir.value:"ltr"}ngOnDestroy(){this._attachSubscription.unsubscribe(),this._detachSubscription.unsubscribe(),this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this._overlayRef?.dispose()}ngOnChanges(e){this._position&&(this._updatePositionStrategy(this._position),this._overlayRef?.updateSize({width:this._getWidth(),minWidth:this.minWidth,height:this.height,minHeight:this.minHeight}),e.origin&&this.open&&this._position.apply()),e.open&&(this.open?this.attachOverlay():this.detachOverlay())}_createOverlay(){(!this.positions||!this.positions.length)&&(this.positions=dP);let e=this._overlayRef=Zg(this._injector,this._buildConfig());this._attachSubscription=e.attachments().subscribe(()=>this.attach.emit()),this._detachSubscription=e.detachments().subscribe(()=>this.detach.emit()),e.keydownEvents().subscribe(r=>{this.overlayKeydown.next(r),r.keyCode===27&&!this.disableClose&&!Yu(r)&&(r.preventDefault(),this.detachOverlay())}),this._overlayRef.outsidePointerEvents().subscribe(r=>{let o=this._getOriginElement(),i=ot(r);(!o||o!==i&&!o.contains(i))&&this.overlayOutsideClick.next(r)})}_buildConfig(){let e=this._position=this.positionStrategy||this._createPositionStrategy(),r=new Ca({direction:this._dir||"ltr",positionStrategy:e,scrollStrategy:this.scrollStrategy,hasBackdrop:this.hasBackdrop,disposeOnNavigation:this.disposeOnNavigation,usePopover:!!this.usePopover});return(this.height||this.height===0)&&(r.height=this.height),(this.minWidth||this.minWidth===0)&&(r.minWidth=this.minWidth),(this.minHeight||this.minHeight===0)&&(r.minHeight=this.minHeight),this.backdropClass&&(r.backdropClass=this.backdropClass),this.panelClass&&(r.panelClass=this.panelClass),r}_updatePositionStrategy(e){let r=this.positions.map(o=>({originX:o.originX,originY:o.originY,overlayX:o.overlayX,overlayY:o.overlayY,offsetX:o.offsetX||this.offsetX,offsetY:o.offsetY||this.offsetY,panelClass:o.panelClass||void 0}));return e.setOrigin(this._getOrigin()).withPositions(r).withFlexibleDimensions(this.flexibleDimensions).withPush(this.push).withGrowAfterOpen(this.growAfterOpen).withViewportMargin(this.viewportMargin).withLockedPosition(this.lockPosition).withTransformOriginOn(this.transformOriginSelector).withPopoverLocation(this.usePopover===null?"global":this.usePopover)}_createPositionStrategy(){let e=qg(this._injector,this._getOrigin());return this._updatePositionStrategy(e),e}_getOrigin(){return this.origin instanceof $g?this.origin.elementRef:this.origin}_getOriginElement(){return this.origin instanceof $g?this.origin.elementRef.nativeElement:this.origin instanceof $?this.origin.nativeElement:typeof Element<"u"&&this.origin instanceof Element?this.origin:null}_getWidth(){return this.width?this.width:this.matchWidth?this._getOriginElement()?.getBoundingClientRect?.().width:void 0}attachOverlay(){this._overlayRef||this._createOverlay();let e=this._overlayRef;e.getConfig().hasBackdrop=this.hasBackdrop,e.updateSize({width:this._getWidth()}),e.hasAttached()||e.attach(this._templatePortal),this.hasBackdrop?this._backdropSubscription=e.backdropClick().subscribe(r=>this.backdropClick.emit(r)):this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this.positionChange.observers.length>0&&(this._positionSubscription=this._position.positionChanges.pipe(sf(()=>this.positionChange.observers.length>0)).subscribe(r=>{this._ngZone.run(()=>this.positionChange.emit(r)),this.positionChange.observers.length===0&&this._positionSubscription.unsubscribe()})),this.open=!0}detachOverlay(){this._overlayRef?.detach(),this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this.open=!1}_assignConfig(e){this.origin=e.origin??this.origin,this.positions=e.positions??this.positions,this.positionStrategy=e.positionStrategy??this.positionStrategy,this.offsetX=e.offsetX??this.offsetX,this.offsetY=e.offsetY??this.offsetY,this.width=e.width??this.width,this.height=e.height??this.height,this.minWidth=e.minWidth??this.minWidth,this.minHeight=e.minHeight??this.minHeight,this.backdropClass=e.backdropClass??this.backdropClass,this.panelClass=e.panelClass??this.panelClass,this.viewportMargin=e.viewportMargin??this.viewportMargin,this.scrollStrategy=e.scrollStrategy??this.scrollStrategy,this.disableClose=e.disableClose??this.disableClose,this.transformOriginSelector=e.transformOriginSelector??this.transformOriginSelector,this.hasBackdrop=e.hasBackdrop??this.hasBackdrop,this.lockPosition=e.lockPosition??this.lockPosition,this.flexibleDimensions=e.flexibleDimensions??this.flexibleDimensions,this.growAfterOpen=e.growAfterOpen??this.growAfterOpen,this.push=e.push??this.push,this.disposeOnNavigation=e.disposeOnNavigation??this.disposeOnNavigation,this.usePopover=e.usePopover??this.usePopover,this.matchWidth=e.matchWidth??this.matchWidth}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["","cdk-connected-overlay",""],["","connected-overlay",""],["","cdkConnectedOverlay",""]],inputs:{origin:[0,"cdkConnectedOverlayOrigin","origin"],positions:[0,"cdkConnectedOverlayPositions","positions"],positionStrategy:[0,"cdkConnectedOverlayPositionStrategy","positionStrategy"],offsetX:[0,"cdkConnectedOverlayOffsetX","offsetX"],offsetY:[0,"cdkConnectedOverlayOffsetY","offsetY"],width:[0,"cdkConnectedOverlayWidth","width"],height:[0,"cdkConnectedOverlayHeight","height"],minWidth:[0,"cdkConnectedOverlayMinWidth","minWidth"],minHeight:[0,"cdkConnectedOverlayMinHeight","minHeight"],backdropClass:[0,"cdkConnectedOverlayBackdropClass","backdropClass"],panelClass:[0,"cdkConnectedOverlayPanelClass","panelClass"],viewportMargin:[0,"cdkConnectedOverlayViewportMargin","viewportMargin"],scrollStrategy:[0,"cdkConnectedOverlayScrollStrategy","scrollStrategy"],open:[0,"cdkConnectedOverlayOpen","open"],disableClose:[0,"cdkConnectedOverlayDisableClose","disableClose"],transformOriginSelector:[0,"cdkConnectedOverlayTransformOriginOn","transformOriginSelector"],hasBackdrop:[2,"cdkConnectedOverlayHasBackdrop","hasBackdrop",ue],lockPosition:[2,"cdkConnectedOverlayLockPosition","lockPosition",ue],flexibleDimensions:[2,"cdkConnectedOverlayFlexibleDimensions","flexibleDimensions",ue],growAfterOpen:[2,"cdkConnectedOverlayGrowAfterOpen","growAfterOpen",ue],push:[2,"cdkConnectedOverlayPush","push",ue],disposeOnNavigation:[2,"cdkConnectedOverlayDisposeOnNavigation","disposeOnNavigation",ue],usePopover:[0,"cdkConnectedOverlayUsePopover","usePopover"],matchWidth:[2,"cdkConnectedOverlayMatchWidth","matchWidth",ue],_config:[0,"cdkConnectedOverlay","_config"]},outputs:{backdropClick:"backdropClick",positionChange:"positionChange",attach:"attach",detach:"detach",overlayKeydown:"overlayKeydown",overlayOutsideClick:"overlayOutsideClick"},exportAs:["cdkConnectedOverlay"],features:[$e]})}return t})(),pP=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({providers:[VI],imports:[rn,EI,jg,jg]})}return t})();var B="primary",La=Symbol("RouteTitle"),ev=class{params;constructor(n){this.params=n||{}}has(n){return Object.prototype.hasOwnProperty.call(this.params,n)}get(n){if(this.has(n)){let e=this.params[n];return Array.isArray(e)?e[0]:e}return null}getAll(n){if(this.has(n)){let e=this.params[n];return Array.isArray(e)?e:[e]}return[]}get keys(){return Object.keys(this.params)}};function _o(t){return new ev(t)}function Kg(t,n,e){for(let r=0;rt.length||e.pathMatch==="full"&&(n.hasChildren()||r.lengtht.length||e.pathMatch==="full"&&n.hasChildren()&&e.path!=="**")return null;let a={};return!Kg(i,t.slice(0,i.length),a)||!Kg(s,t.slice(t.length-s.length),a)?null:{consumed:t,posParams:a}}function md(t){return new Promise((n,e)=>{t.pipe(Nn()).subscribe({next:r=>n(r),error:r=>e(r)})})}function mP(t,n){if(t.length!==n.length)return!1;for(let e=0;er[i]===o)}else return t===n}function gP(t){return t.length>0?t[t.length-1]:null}function Eo(t){return Vt(t)?t:zn(t)?ae(Promise.resolve(t)):S(t)}function ZI(t){return Vt(t)?md(t):Promise.resolve(t)}var vP={exact:QI,subset:XI},KI={exact:yP,subset:bP,ignored:()=>!0},mv={paths:"exact",fragment:"ignored",matrixParams:"ignored",queryParams:"exact"},xa={paths:"subset",fragment:"ignored",matrixParams:"ignored",queryParams:"subset"};function gv(t,n,e){let r=t instanceof it?t:n.parseUrl(t);return Xt(()=>nv(n.lastSuccessfulNavigation()?.finalUrl??new it,r,g(g({},xa),e)))}function nv(t,n,e){return vP[e.paths](t.root,n.root,e.matrixParams)&&KI[e.queryParams](t.queryParams,n.queryParams)&&!(e.fragment==="exact"&&t.fragment!==n.fragment)}function yP(t,n){return Sn(t,n)}function QI(t,n,e){if(!bo(t.segments,n.segments)||!fd(t.segments,n.segments,e)||t.numberOfChildren!==n.numberOfChildren)return!1;for(let r in n.children)if(!t.children[r]||!QI(t.children[r],n.children[r],e))return!1;return!0}function bP(t,n){return Object.keys(n).length<=Object.keys(t).length&&Object.keys(n).every(e=>YI(t[e],n[e]))}function XI(t,n,e){return JI(t,n,n.segments,e)}function JI(t,n,e,r){if(t.segments.length>e.length){let o=t.segments.slice(0,e.length);return!(!bo(o,e)||n.hasChildren()||!fd(o,e,r))}else if(t.segments.length===e.length){if(!bo(t.segments,e)||!fd(t.segments,e,r))return!1;for(let o in n.children)if(!t.children[o]||!XI(t.children[o],n.children[o],r))return!1;return!0}else{let o=e.slice(0,t.segments.length),i=e.slice(t.segments.length);return!bo(t.segments,o)||!fd(t.segments,o,r)||!t.children[B]?!1:JI(t.children[B],n,i,r)}}function fd(t,n,e){return n.every((r,o)=>KI[e](t[o].parameters,r.parameters))}var it=class{root;queryParams;fragment;_queryParamMap;constructor(n=new te([],{}),e={},r=null){this.root=n,this.queryParams=e,this.fragment=r}get queryParamMap(){return this._queryParamMap??=_o(this.queryParams),this._queryParamMap}toString(){return EP.serialize(this)}},te=class{segments;children;parent=null;constructor(n,e){this.segments=n,this.children=e,Object.values(e).forEach(r=>r.parent=this)}hasChildren(){return this.numberOfChildren>0}get numberOfChildren(){return Object.keys(this.children).length}toString(){return hd(this)}},Ir=class{path;parameters;_parameterMap;constructor(n,e){this.path=n,this.parameters=e}get parameterMap(){return this._parameterMap??=_o(this.parameters),this._parameterMap}toString(){return tS(this)}};function _P(t,n){return bo(t,n)&&t.every((e,r)=>Sn(e.parameters,n[r].parameters))}function bo(t,n){return t.length!==n.length?!1:t.every((e,r)=>e.path===n[r].path)}function DP(t,n){let e=[];return Object.entries(t.children).forEach(([r,o])=>{r===B&&(e=e.concat(n(o,r)))}),Object.entries(t.children).forEach(([r,o])=>{r!==B&&(e=e.concat(n(o,r)))}),e}var Tr=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>new Xn,providedIn:"root"})}return t})(),Xn=class{parse(n){let e=new ov(n);return new it(e.parseRootSegment(),e.parseQueryParams(),e.parseFragment())}serialize(n){let e=`/${Ia(n.root,!0)}`,r=IP(n.queryParams),o=typeof n.fragment=="string"?`#${wP(n.fragment)}`:"";return`${e}${r}${o}`}},EP=new Xn;function hd(t){return t.segments.map(n=>tS(n)).join("/")}function Ia(t,n){if(!t.hasChildren())return hd(t);if(n){let e=t.children[B]?Ia(t.children[B],!1):"",r=[];return Object.entries(t.children).forEach(([o,i])=>{o!==B&&r.push(`${o}:${Ia(i,!1)}`)}),r.length>0?`${e}(${r.join("//")})`:e}else{let e=DP(t,(r,o)=>o===B?[Ia(t.children[B],!1)]:[`${o}:${Ia(r,!1)}`]);return Object.keys(t.children).length===1&&t.children[B]!=null?`${hd(t)}/${e[0]}`:`${hd(t)}/(${e.join("//")})`}}function eS(t){return encodeURIComponent(t).replace(/%40/g,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",")}function ud(t){return eS(t).replace(/%3B/gi,";")}function wP(t){return encodeURI(t)}function rv(t){return eS(t).replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/%26/gi,"&")}function pd(t){return decodeURIComponent(t)}function UI(t){return pd(t.replace(/\+/g,"%20"))}function tS(t){return`${rv(t.path)}${CP(t.parameters)}`}function CP(t){return Object.entries(t).map(([n,e])=>`;${rv(n)}=${rv(e)}`).join("")}function IP(t){let n=Object.entries(t).map(([e,r])=>Array.isArray(r)?r.map(o=>`${ud(e)}=${ud(o)}`).join("&"):`${ud(e)}=${ud(r)}`).filter(e=>e);return n.length?`?${n.join("&")}`:""}var SP=/^[^\/()?;#]+/;function Qg(t){let n=t.match(SP);return n?n[0]:""}var MP=/^[^\/()?;=#]+/;function TP(t){let n=t.match(MP);return n?n[0]:""}var xP=/^[^=?&#]+/;function AP(t){let n=t.match(xP);return n?n[0]:""}var RP=/^[^&#]+/;function NP(t){let n=t.match(RP);return n?n[0]:""}var ov=class{url;remaining;constructor(n){this.url=n,this.remaining=n}parseRootSegment(){for(;this.consumeOptional("/"););return this.remaining===""||this.peekStartsWith("?")||this.peekStartsWith("#")?new te([],{}):new te([],this.parseChildren())}parseQueryParams(){let n={};if(this.consumeOptional("?"))do this.parseQueryParam(n);while(this.consumeOptional("&"));return n}parseFragment(){return this.consumeOptional("#")?decodeURIComponent(this.remaining):null}parseChildren(n=0){if(n>50)throw new b(4010,!1);if(this.remaining==="")return{};this.consumeOptional("/");let e=[];for(this.peekStartsWith("(")||e.push(this.parseSegment());this.peekStartsWith("/")&&!this.peekStartsWith("//")&&!this.peekStartsWith("/(");)this.capture("/"),e.push(this.parseSegment());let r={};this.peekStartsWith("/(")&&(this.capture("/"),r=this.parseParens(!0,n));let o={};return this.peekStartsWith("(")&&(o=this.parseParens(!1,n)),(e.length>0||Object.keys(r).length>0)&&(o[B]=new te(e,r)),o}parseSegment(){let n=Qg(this.remaining);if(n===""&&this.peekStartsWith(";"))throw new b(4009,!1);return this.capture(n),new Ir(pd(n),this.parseMatrixParams())}parseMatrixParams(){let n={};for(;this.consumeOptional(";");)this.parseParam(n);return n}parseParam(n){let e=TP(this.remaining);if(!e)return;this.capture(e);let r="";if(this.consumeOptional("=")){let o=Qg(this.remaining);o&&(r=o,this.capture(r))}n[pd(e)]=pd(r)}parseQueryParam(n){let e=AP(this.remaining);if(!e)return;this.capture(e);let r="";if(this.consumeOptional("=")){let s=NP(this.remaining);s&&(r=s,this.capture(r))}let o=UI(e),i=UI(r);if(n.hasOwnProperty(o)){let s=n[o];Array.isArray(s)||(s=[s],n[o]=s),s.push(i)}else n[o]=i}parseParens(n,e){let r={};for(this.capture("(");!this.consumeOptional(")")&&this.remaining.length>0;){let o=Qg(this.remaining),i=this.remaining[o.length];if(i!=="/"&&i!==")"&&i!==";")throw new b(4010,!1);let s;o.indexOf(":")>-1?(s=o.slice(0,o.indexOf(":")),this.capture(s),this.capture(":")):n&&(s=B);let a=this.parseChildren(e+1);r[s??B]=Object.keys(a).length===1&&a[B]?a[B]:new te([],a),this.consumeOptional("//")}return r}peekStartsWith(n){return this.remaining.startsWith(n)}consumeOptional(n){return this.peekStartsWith(n)?(this.remaining=this.remaining.substring(n.length),!0):!1}capture(n){if(!this.consumeOptional(n))throw new b(4011,!1)}};function nS(t){return t.segments.length>0?new te([],{[B]:t}):t}function rS(t){let n={};for(let[r,o]of Object.entries(t.children)){let i=rS(o);if(r===B&&i.segments.length===0&&i.hasChildren())for(let[s,a]of Object.entries(i.children))n[s]=a;else(i.segments.length>0||i.hasChildren())&&(n[r]=i)}let e=new te(t.segments,n);return OP(e)}function OP(t){if(t.numberOfChildren===1&&t.children[B]){let n=t.children[B];return new te(t.segments.concat(n.segments),n.children)}return t}function Sr(t){return t instanceof it}function oS(t,n,e=null,r=null,o=new Xn){let i=iS(t);return sS(i,n,e,r,o)}function iS(t){let n;function e(i){let s={};for(let c of i.children){let l=e(c);s[c.outlet]=l}let a=new te(i.url,s);return i===t&&(n=a),a}let r=e(t.root),o=nS(r);return n??o}function sS(t,n,e,r,o){let i=t;for(;i.parent;)i=i.parent;if(n.length===0)return Xg(i,i,i,e,r,o);let s=kP(n);if(s.toRoot())return Xg(i,i,new te([],{}),e,r,o);let a=FP(s,i,t),c=a.processChildren?Ma(a.segmentGroup,a.index,s.commands):cS(a.segmentGroup,a.index,s.commands);return Xg(i,a.segmentGroup,c,e,r,o)}function gd(t){return typeof t=="object"&&t!=null&&!t.outlets&&!t.segmentPath}function Aa(t){return typeof t=="object"&&t!=null&&t.outlets}function HI(t,n,e){t||="\u0275";let r=new it;return r.queryParams={[t]:n},e.parse(e.serialize(r)).queryParams[t]}function Xg(t,n,e,r,o,i){let s={};for(let[l,u]of Object.entries(r??{}))s[l]=Array.isArray(u)?u.map(d=>HI(l,d,i)):HI(l,u,i);let a;t===n?a=e:a=aS(t,n,e);let c=nS(rS(a));return new it(c,s,o)}function aS(t,n,e){let r={};return Object.entries(t.children).forEach(([o,i])=>{i===n?r[o]=e:r[o]=aS(i,n,e)}),new te(t.segments,r)}var vd=class{isAbsolute;numberOfDoubleDots;commands;constructor(n,e,r){if(this.isAbsolute=n,this.numberOfDoubleDots=e,this.commands=r,n&&r.length>0&&gd(r[0]))throw new b(4003,!1);let o=r.find(Aa);if(o&&o!==gP(r))throw new b(4004,!1)}toRoot(){return this.isAbsolute&&this.commands.length===1&&this.commands[0]=="/"}};function kP(t){if(typeof t[0]=="string"&&t.length===1&&t[0]==="/")return new vd(!0,0,t);let n=0,e=!1,r=t.reduce((o,i,s)=>{if(typeof i=="object"&&i!=null){if(i.outlets){let a={};return Object.entries(i.outlets).forEach(([c,l])=>{a[c]=typeof l=="string"?l.split("/"):l}),[...o,{outlets:a}]}if(i.segmentPath)return[...o,i.segmentPath]}return typeof i!="string"?[...o,i]:s===0?(i.split("/").forEach((a,c)=>{c==0&&a==="."||(c==0&&a===""?e=!0:a===".."?n++:a!=""&&o.push(a))}),o):[...o,i]},[]);return new vd(e,n,r)}var xi=class{segmentGroup;processChildren;index;constructor(n,e,r){this.segmentGroup=n,this.processChildren=e,this.index=r}};function FP(t,n,e){if(t.isAbsolute)return new xi(n,!0,0);if(!e)return new xi(n,!1,NaN);if(e.parent===null)return new xi(e,!0,0);let r=gd(t.commands[0])?0:1,o=e.segments.length-1+r;return PP(e,o,t.numberOfDoubleDots)}function PP(t,n,e){let r=t,o=n,i=e;for(;i>o;){if(i-=o,r=r.parent,!r)throw new b(4005,!1);o=r.segments.length}return new xi(r,!1,o-i)}function LP(t){return Aa(t[0])?t[0].outlets:{[B]:t}}function cS(t,n,e){if(t??=new te([],{}),t.segments.length===0&&t.hasChildren())return Ma(t,n,e);let r=jP(t,n,e),o=e.slice(r.commandIndex);if(r.match&&r.pathIndexi!==B)&&t.children[B]&&t.numberOfChildren===1&&t.children[B].segments.length===0){let i=Ma(t.children[B],n,e);return new te(t.segments,i.children)}return Object.entries(r).forEach(([i,s])=>{typeof s=="string"&&(s=[s]),s!==null&&(o[i]=cS(t.children[i],n,s))}),Object.entries(t.children).forEach(([i,s])=>{r[i]===void 0&&(o[i]=s)}),new te(t.segments,o)}}function jP(t,n,e){let r=0,o=n,i={match:!1,pathIndex:0,commandIndex:0};for(;o=e.length)return i;let s=t.segments[o],a=e[r];if(Aa(a))break;let c=`${a}`,l=r0&&c===void 0)break;if(c&&l&&typeof l=="object"&&l.outlets===void 0){if(!zI(c,l,s))return i;r+=2}else{if(!zI(c,{},s))return i;r++}o++}return{match:!0,pathIndex:o,commandIndex:r}}function iv(t,n,e){let r=t.segments.slice(0,n),o=0;for(;o{typeof r=="string"&&(r=[r]),r!==null&&(n[e]=iv(new te([],{}),0,r))}),n}function $I(t){let n={};return Object.entries(t).forEach(([e,r])=>n[e]=`${r}`),n}function zI(t,n,e){return t==e.path&&Sn(n,e.parameters)}var Ai="imperative",je=(function(t){return t[t.NavigationStart=0]="NavigationStart",t[t.NavigationEnd=1]="NavigationEnd",t[t.NavigationCancel=2]="NavigationCancel",t[t.NavigationError=3]="NavigationError",t[t.RoutesRecognized=4]="RoutesRecognized",t[t.ResolveStart=5]="ResolveStart",t[t.ResolveEnd=6]="ResolveEnd",t[t.GuardsCheckStart=7]="GuardsCheckStart",t[t.GuardsCheckEnd=8]="GuardsCheckEnd",t[t.RouteConfigLoadStart=9]="RouteConfigLoadStart",t[t.RouteConfigLoadEnd=10]="RouteConfigLoadEnd",t[t.ChildActivationStart=11]="ChildActivationStart",t[t.ChildActivationEnd=12]="ChildActivationEnd",t[t.ActivationStart=13]="ActivationStart",t[t.ActivationEnd=14]="ActivationEnd",t[t.Scroll=15]="Scroll",t[t.NavigationSkipped=16]="NavigationSkipped",t})(je||{}),Dt=class{id;url;constructor(n,e){this.id=n,this.url=e}},Mr=class extends Dt{type=je.NavigationStart;navigationTrigger;restoredState;constructor(n,e,r="imperative",o=null){super(n,e),this.navigationTrigger=r,this.restoredState=o}toString(){return`NavigationStart(id: ${this.id}, url: '${this.url}')`}},Et=class extends Dt{urlAfterRedirects;type=je.NavigationEnd;constructor(n,e,r){super(n,e),this.urlAfterRedirects=r}toString(){return`NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`}},et=(function(t){return t[t.Redirect=0]="Redirect",t[t.SupersededByNewNavigation=1]="SupersededByNewNavigation",t[t.NoDataFromResolver=2]="NoDataFromResolver",t[t.GuardRejected=3]="GuardRejected",t[t.Aborted=4]="Aborted",t})(et||{}),Ni=(function(t){return t[t.IgnoredSameUrlNavigation=0]="IgnoredSameUrlNavigation",t[t.IgnoredByUrlHandlingStrategy=1]="IgnoredByUrlHandlingStrategy",t})(Ni||{}),Ft=class extends Dt{reason;code;type=je.NavigationCancel;constructor(n,e,r,o){super(n,e),this.reason=r,this.code=o}toString(){return`NavigationCancel(id: ${this.id}, url: '${this.url}')`}};function lS(t){return t instanceof Ft&&(t.code===et.Redirect||t.code===et.SupersededByNewNavigation)}var Mn=class extends Dt{reason;code;type=je.NavigationSkipped;constructor(n,e,r,o){super(n,e),this.reason=r,this.code=o}},Do=class extends Dt{error;target;type=je.NavigationError;constructor(n,e,r,o){super(n,e),this.error=r,this.target=o}toString(){return`NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`}},Ra=class extends Dt{urlAfterRedirects;state;type=je.RoutesRecognized;constructor(n,e,r,o){super(n,e),this.urlAfterRedirects=r,this.state=o}toString(){return`RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},yd=class extends Dt{urlAfterRedirects;state;type=je.GuardsCheckStart;constructor(n,e,r,o){super(n,e),this.urlAfterRedirects=r,this.state=o}toString(){return`GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},bd=class extends Dt{urlAfterRedirects;state;shouldActivate;type=je.GuardsCheckEnd;constructor(n,e,r,o,i){super(n,e),this.urlAfterRedirects=r,this.state=o,this.shouldActivate=i}toString(){return`GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`}},_d=class extends Dt{urlAfterRedirects;state;type=je.ResolveStart;constructor(n,e,r,o){super(n,e),this.urlAfterRedirects=r,this.state=o}toString(){return`ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},Dd=class extends Dt{urlAfterRedirects;state;type=je.ResolveEnd;constructor(n,e,r,o){super(n,e),this.urlAfterRedirects=r,this.state=o}toString(){return`ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},Ed=class{route;type=je.RouteConfigLoadStart;constructor(n){this.route=n}toString(){return`RouteConfigLoadStart(path: ${this.route.path})`}},wd=class{route;type=je.RouteConfigLoadEnd;constructor(n){this.route=n}toString(){return`RouteConfigLoadEnd(path: ${this.route.path})`}},Cd=class{snapshot;type=je.ChildActivationStart;constructor(n){this.snapshot=n}toString(){return`ChildActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},Id=class{snapshot;type=je.ChildActivationEnd;constructor(n){this.snapshot=n}toString(){return`ChildActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},Sd=class{snapshot;type=je.ActivationStart;constructor(n){this.snapshot=n}toString(){return`ActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},Md=class{snapshot;type=je.ActivationEnd;constructor(n){this.snapshot=n}toString(){return`ActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},Oi=class{routerEvent;position;anchor;scrollBehavior;type=je.Scroll;constructor(n,e,r,o){this.routerEvent=n,this.position=e,this.anchor=r,this.scrollBehavior=o}toString(){let n=this.position?`${this.position[0]}, ${this.position[1]}`:null;return`Scroll(anchor: '${this.anchor}', position: '${n}')`}},ki=class{},Na=class{},Fi=class{url;navigationBehaviorOptions;constructor(n,e){this.url=n,this.navigationBehaviorOptions=e}};function BP(t){return!(t instanceof ki)&&!(t instanceof Fi)&&!(t instanceof Na)}var Td=class{rootInjector;outlet=null;route=null;children;attachRef=null;get injector(){return this.route?.snapshot._environmentInjector??this.rootInjector}constructor(n){this.rootInjector=n,this.children=new wo(this.rootInjector)}},wo=(()=>{class t{rootInjector;contexts=new Map;constructor(e){this.rootInjector=e}onChildOutletCreated(e,r){let o=this.getOrCreateContext(e);o.outlet=r,this.contexts.set(e,o)}onChildOutletDestroyed(e){let r=this.getContext(e);r&&(r.outlet=null,r.attachRef=null)}onOutletDeactivated(){let e=this.contexts;return this.contexts=new Map,e}onOutletReAttached(e){this.contexts=e}getOrCreateContext(e){let r=this.getContext(e);return r||(r=new Td(this.rootInjector),this.contexts.set(e,r)),r}getContext(e){return this.contexts.get(e)||null}static \u0275fac=function(r){return new(r||t)(w(re))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),xd=class{_root;constructor(n){this._root=n}get root(){return this._root.value}parent(n){let e=this.pathFromRoot(n);return e.length>1?e[e.length-2]:null}children(n){let e=sv(n,this._root);return e?e.children.map(r=>r.value):[]}firstChild(n){let e=sv(n,this._root);return e&&e.children.length>0?e.children[0].value:null}siblings(n){let e=av(n,this._root);return e.length<2?[]:e[e.length-2].children.map(o=>o.value).filter(o=>o!==n)}pathFromRoot(n){return av(n,this._root).map(e=>e.value)}};function sv(t,n){if(t===n.value)return n;for(let e of n.children){let r=sv(t,e);if(r)return r}return null}function av(t,n){if(t===n.value)return[n];for(let e of n.children){let r=av(t,e);if(r.length)return r.unshift(n),r}return[]}var _t=class{value;children;constructor(n,e){this.value=n,this.children=e}toString(){return`TreeNode(${this.value})`}};function Ti(t){let n={};return t&&t.children.forEach(e=>n[e.value.outlet]=e),n}var Oa=class extends xd{snapshot;constructor(n,e){super(n),this.snapshot=e,yv(this,n)}toString(){return this.snapshot.toString()}};function uS(t,n){let e=UP(t,n),r=new Ae([new Ir("",{})]),o=new Ae({}),i=new Ae({}),s=new Ae({}),a=new Ae(""),c=new Jn(r,o,s,a,i,B,t,e.root);return c.snapshot=e.root,new Oa(new _t(c,[]),e)}function UP(t,n){let e={},r={},o={},s=new Pi([],e,o,"",r,B,t,null,{},n);return new ka("",new _t(s,[]))}var Jn=class{urlSubject;paramsSubject;queryParamsSubject;fragmentSubject;dataSubject;outlet;component;snapshot;_futureSnapshot;_routerState;_paramMap;_queryParamMap;title;url;params;queryParams;fragment;data;constructor(n,e,r,o,i,s,a,c){this.urlSubject=n,this.paramsSubject=e,this.queryParamsSubject=r,this.fragmentSubject=o,this.dataSubject=i,this.outlet=s,this.component=a,this._futureSnapshot=c,this.title=this.dataSubject?.pipe(V(l=>l[La]))??S(void 0),this.url=n,this.params=e,this.queryParams=r,this.fragment=o,this.data=i}get routeConfig(){return this._futureSnapshot.routeConfig}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=this.params.pipe(V(n=>_o(n))),this._paramMap}get queryParamMap(){return this._queryParamMap??=this.queryParams.pipe(V(n=>_o(n))),this._queryParamMap}toString(){return this.snapshot?this.snapshot.toString():`Future(${this._futureSnapshot})`}};function vv(t,n,e="emptyOnly"){let r,{routeConfig:o}=t;return n!==null&&(e==="always"||o?.path===""||!n.component&&!n.routeConfig?.loadComponent)?r={params:g(g({},n.params),t.params),data:g(g({},n.data),t.data),resolve:g(g(g(g({},t.data),n.data),o?.data),t._resolvedData)}:r={params:g({},t.params),data:g({},t.data),resolve:g(g({},t.data),t._resolvedData??{})},o&&fS(o)&&(r.resolve[La]=o.title),r}var Pi=class{url;params;queryParams;fragment;data;outlet;component;routeConfig;_resolve;_resolvedData;_routerState;_paramMap;_queryParamMap;_environmentInjector;get title(){return this.data?.[La]}constructor(n,e,r,o,i,s,a,c,l,u){this.url=n,this.params=e,this.queryParams=r,this.fragment=o,this.data=i,this.outlet=s,this.component=a,this.routeConfig=c,this._resolve=l,this._environmentInjector=u}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=_o(this.params),this._paramMap}get queryParamMap(){return this._queryParamMap??=_o(this.queryParams),this._queryParamMap}toString(){let n=this.url.map(r=>r.toString()).join("/"),e=this.routeConfig?this.routeConfig.path:"";return`Route(url:'${n}', path:'${e}')`}},ka=class extends xd{url;constructor(n,e){super(e),this.url=n,yv(this,e)}toString(){return dS(this._root)}};function yv(t,n){n.value._routerState=t,n.children.forEach(e=>yv(t,e))}function dS(t){let n=t.children.length>0?` { ${t.children.map(dS).join(", ")} } `:"";return`${t.value}${n}`}function Jg(t){if(t.snapshot){let n=t.snapshot,e=t._futureSnapshot;t.snapshot=e,Sn(n.queryParams,e.queryParams)||t.queryParamsSubject.next(e.queryParams),n.fragment!==e.fragment&&t.fragmentSubject.next(e.fragment),Sn(n.params,e.params)||t.paramsSubject.next(e.params),mP(n.url,e.url)||t.urlSubject.next(e.url),Sn(n.data,e.data)||t.dataSubject.next(e.data)}else t.snapshot=t._futureSnapshot,t.dataSubject.next(t._futureSnapshot.data)}function cv(t,n){let e=Sn(t.params,n.params)&&_P(t.url,n.url),r=!t.parent!=!n.parent;return e&&!r&&(!t.parent||cv(t.parent,n.parent))}function fS(t){return typeof t.title=="string"||t.title===null}var hS=new y(""),bv=(()=>{class t{activated=null;get activatedComponentRef(){return this.activated}_activatedRoute=null;name=B;activateEvents=new H;deactivateEvents=new H;attachEvents=new H;detachEvents=new H;routerOutletData=zE();parentContexts=f(wo);location=f(Qe);changeDetector=f(Jt);inputBinder=f(ja,{optional:!0});supportsBindingToComponentInputs=!0;ngOnChanges(e){if(e.name){let{firstChange:r,previousValue:o}=e.name;if(r)return;this.isTrackedInParentContexts(o)&&(this.deactivate(),this.parentContexts.onChildOutletDestroyed(o)),this.initializeOutletWithName()}}ngOnDestroy(){this.isTrackedInParentContexts(this.name)&&this.parentContexts.onChildOutletDestroyed(this.name),this.inputBinder?.unsubscribeFromRouteData(this)}isTrackedInParentContexts(e){return this.parentContexts.getContext(e)?.outlet===this}ngOnInit(){this.initializeOutletWithName()}initializeOutletWithName(){if(this.parentContexts.onChildOutletCreated(this.name,this),this.activated)return;let e=this.parentContexts.getContext(this.name);e?.route&&(e.attachRef?this.attach(e.attachRef,e.route):this.activateWith(e.route,e.injector))}get isActivated(){return!!this.activated}get component(){if(!this.activated)throw new b(4012,!1);return this.activated.instance}get activatedRoute(){if(!this.activated)throw new b(4012,!1);return this._activatedRoute}get activatedRouteData(){return this._activatedRoute?this._activatedRoute.snapshot.data:{}}detach(){if(!this.activated)throw new b(4012,!1);this.location.detach();let e=this.activated;return this.activated=null,this._activatedRoute=null,this.detachEvents.emit(e.instance),e}attach(e,r){this.activated=e,this._activatedRoute=r,this.location.insert(e.hostView),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.attachEvents.emit(e.instance)}deactivate(){if(this.activated){let e=this.component;this.activated.destroy(),this.activated=null,this._activatedRoute=null,this.deactivateEvents.emit(e)}}activateWith(e,r){if(this.isActivated)throw new b(4013,!1);this._activatedRoute=e;let o=this.location,s=e.snapshot.component,a=this.parentContexts.getOrCreateContext(this.name).children,c=new lv(e,a,o.injector,this.routerOutletData);this.activated=o.createComponent(s,{index:o.length,injector:c,environmentInjector:r}),this.changeDetector.markForCheck(),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.activateEvents.emit(this.activated.instance)}static \u0275fac=function(r){return new(r||t)};static \u0275dir=T({type:t,selectors:[["router-outlet"]],inputs:{name:"name",routerOutletData:[1,"routerOutletData"]},outputs:{activateEvents:"activate",deactivateEvents:"deactivate",attachEvents:"attach",detachEvents:"detach"},exportAs:["outlet"],features:[$e]})}return t})(),lv=class{route;childContexts;parent;outletData;constructor(n,e,r,o){this.route=n,this.childContexts=e,this.parent=r,this.outletData=o}get(n,e){return n===Jn?this.route:n===wo?this.childContexts:n===hS?this.outletData:this.parent.get(n,e)}},ja=new y(""),_v=(()=>{class t{outletDataSubscriptions=new Map;bindActivatedRouteToOutletComponent(e){this.unsubscribeFromRouteData(e),this.subscribeToRouteData(e)}unsubscribeFromRouteData(e){this.outletDataSubscriptions.get(e)?.unsubscribe(),this.outletDataSubscriptions.delete(e)}subscribeToRouteData(e){let{activatedRoute:r}=e,o=Ro([r.queryParams,r.params,r.data]).pipe(Ve(([i,s,a],c)=>(a=g(g(g({},i),s),a),c===0?S(a):Promise.resolve(a)))).subscribe(i=>{if(!e.isActivated||!e.activatedComponentRef||e.activatedRoute!==r||r.component===null){this.unsubscribeFromRouteData(e);return}let s=ZE(r.component);if(!s){this.unsubscribeFromRouteData(e);return}for(let{templateName:a}of s.inputs)e.activatedComponentRef.setInput(a,i[a])});this.outletDataSubscriptions.set(e,o)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),Dv=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=Te({type:t,selectors:[["ng-component"]],exportAs:["emptyRouterOutlet"],decls:1,vars:0,template:function(r,o){r&1&&zl(0,"router-outlet")},dependencies:[bv],encapsulation:2})}return t})();function Ev(t){let n=t.children&&t.children.map(Ev),e=n?F(g({},t),{children:n}):g({},t);return!e.component&&!e.loadComponent&&(n||e.loadChildren)&&e.outlet&&e.outlet!==B&&(e.component=Dv),e}function HP(t,n,e){let r=Fa(t,n._root,e?e._root:void 0);return new Oa(r,n)}function Fa(t,n,e){if(e&&t.shouldReuseRoute(n.value,e.value.snapshot)){let r=e.value;r._futureSnapshot=n.value;let o=$P(t,n,e);return new _t(r,o)}else{if(t.shouldAttach(n.value)){let i=t.retrieve(n.value);if(i!==null){let s=i.route;return s.value._futureSnapshot=n.value,s.children=n.children.map(a=>Fa(t,a)),s}}let r=zP(n.value),o=n.children.map(i=>Fa(t,i));return new _t(r,o)}}function $P(t,n,e){return n.children.map(r=>{for(let o of e.children)if(t.shouldReuseRoute(r.value,o.value.snapshot))return Fa(t,r,o);return Fa(t,r)})}function zP(t){return new Jn(new Ae(t.url),new Ae(t.params),new Ae(t.queryParams),new Ae(t.fragment),new Ae(t.data),t.outlet,t.component,t)}var Li=class{redirectTo;navigationBehaviorOptions;constructor(n,e){this.redirectTo=n,this.navigationBehaviorOptions=e}},pS="ngNavigationCancelingError";function Ad(t,n){let{redirectTo:e,navigationBehaviorOptions:r}=Sr(n)?{redirectTo:n,navigationBehaviorOptions:void 0}:n,o=mS(!1,et.Redirect);return o.url=e,o.navigationBehaviorOptions=r,o}function mS(t,n){let e=new Error(`NavigationCancelingError: ${t||""}`);return e[pS]=!0,e.cancellationCode=n,e}function GP(t){return gS(t)&&Sr(t.url)}function gS(t){return!!t&&t[pS]}var uv=class{routeReuseStrategy;futureState;currState;forwardEvent;inputBindingEnabled;constructor(n,e,r,o,i){this.routeReuseStrategy=n,this.futureState=e,this.currState=r,this.forwardEvent=o,this.inputBindingEnabled=i}activate(n){let e=this.futureState._root,r=this.currState?this.currState._root:null;this.deactivateChildRoutes(e,r,n),Jg(this.futureState.root),this.activateChildRoutes(e,r,n)}deactivateChildRoutes(n,e,r){let o=Ti(e);n.children.forEach(i=>{let s=i.value.outlet;this.deactivateRoutes(i,o[s],r),delete o[s]}),Object.values(o).forEach(i=>{this.deactivateRouteAndItsChildren(i,r)})}deactivateRoutes(n,e,r){let o=n.value,i=e?e.value:null;if(o===i)if(o.component){let s=r.getContext(o.outlet);s&&this.deactivateChildRoutes(n,e,s.children)}else this.deactivateChildRoutes(n,e,r);else i&&this.deactivateRouteAndItsChildren(e,r)}deactivateRouteAndItsChildren(n,e){n.value.component&&this.routeReuseStrategy.shouldDetach(n.value.snapshot)?this.detachAndStoreRouteSubtree(n,e):this.deactivateRouteAndOutlet(n,e)}detachAndStoreRouteSubtree(n,e){let r=e.getContext(n.value.outlet),o=r&&n.value.component?r.children:e,i=Ti(n);for(let s of Object.values(i))this.deactivateRouteAndItsChildren(s,o);if(r&&r.outlet){let s=r.outlet.detach(),a=r.children.onOutletDeactivated();this.routeReuseStrategy.store(n.value.snapshot,{componentRef:s,route:n,contexts:a})}}deactivateRouteAndOutlet(n,e){let r=e.getContext(n.value.outlet),o=r&&n.value.component?r.children:e,i=Ti(n);for(let s of Object.values(i))this.deactivateRouteAndItsChildren(s,o);r&&(r.outlet&&(r.outlet.deactivate(),r.children.onOutletDeactivated()),r.attachRef=null,r.route=null)}activateChildRoutes(n,e,r){let o=Ti(e);n.children.forEach(i=>{this.activateRoutes(i,o[i.value.outlet],r),this.forwardEvent(new Md(i.value.snapshot))}),n.children.length&&this.forwardEvent(new Id(n.value.snapshot))}activateRoutes(n,e,r){let o=n.value,i=e?e.value:null;if(Jg(o),o===i)if(o.component){let s=r.getOrCreateContext(o.outlet);this.activateChildRoutes(n,e,s.children)}else this.activateChildRoutes(n,e,r);else if(o.component){let s=r.getOrCreateContext(o.outlet);if(this.routeReuseStrategy.shouldAttach(o.snapshot)){let a=this.routeReuseStrategy.retrieve(o.snapshot);this.routeReuseStrategy.store(o.snapshot,null),s.children.onOutletReAttached(a.contexts),s.attachRef=a.componentRef,s.route=a.route.value,s.outlet&&s.outlet.attach(a.componentRef,a.route.value),Jg(a.route.value),this.activateChildRoutes(n,null,s.children)}else s.attachRef=null,s.route=o,s.outlet&&s.outlet.activateWith(o,s.injector),this.activateChildRoutes(n,null,s.children)}else this.activateChildRoutes(n,null,r)}},Rd=class{path;route;constructor(n){this.path=n,this.route=this.path[this.path.length-1]}},Ri=class{component;route;constructor(n,e){this.component=n,this.route=e}};function WP(t,n,e){let r=t._root,o=n?n._root:null;return Sa(r,o,e,[r.value])}function qP(t){let n=t.routeConfig?t.routeConfig.canActivateChild:null;return!n||n.length===0?null:{node:t,guards:n}}function Vi(t,n){let e=Symbol(),r=n.get(t,e);return r===e?typeof t=="function"&&!Af(t)?t:n.get(t):r}function Sa(t,n,e,r,o={canDeactivateChecks:[],canActivateChecks:[]}){let i=Ti(n);return t.children.forEach(s=>{YP(s,i[s.value.outlet],e,r.concat([s.value]),o),delete i[s.value.outlet]}),Object.entries(i).forEach(([s,a])=>Ta(a,e.getContext(s),o)),o}function YP(t,n,e,r,o={canDeactivateChecks:[],canActivateChecks:[]}){let i=t.value,s=n?n.value:null,a=e?e.getContext(t.value.outlet):null;if(s&&i.routeConfig===s.routeConfig){let c=ZP(s,i,i.routeConfig.runGuardsAndResolvers);c?o.canActivateChecks.push(new Rd(r)):(i.data=s.data,i._resolvedData=s._resolvedData),i.component?Sa(t,n,a?a.children:null,r,o):Sa(t,n,e,r,o),c&&a&&a.outlet&&a.outlet.isActivated&&o.canDeactivateChecks.push(new Ri(a.outlet.component,s))}else s&&Ta(n,a,o),o.canActivateChecks.push(new Rd(r)),i.component?Sa(t,null,a?a.children:null,r,o):Sa(t,null,e,r,o);return o}function ZP(t,n,e){if(typeof e=="function")return Fe(n._environmentInjector,()=>e(t,n));switch(e){case"pathParamsChange":return!bo(t.url,n.url);case"pathParamsOrQueryParamsChange":return!bo(t.url,n.url)||!Sn(t.queryParams,n.queryParams);case"always":return!0;case"paramsOrQueryParamsChange":return!cv(t,n)||!Sn(t.queryParams,n.queryParams);default:return!cv(t,n)}}function Ta(t,n,e){let r=Ti(t),o=t.value;Object.entries(r).forEach(([i,s])=>{o.component?n?Ta(s,n.children.getContext(i),e):Ta(s,null,e):Ta(s,n,e)}),o.component?n&&n.outlet&&n.outlet.isActivated?e.canDeactivateChecks.push(new Ri(n.outlet.component,o)):e.canDeactivateChecks.push(new Ri(null,o)):e.canDeactivateChecks.push(new Ri(null,o))}function Va(t){return typeof t=="function"}function KP(t){return typeof t=="boolean"}function QP(t){return t&&Va(t.canLoad)}function XP(t){return t&&Va(t.canActivate)}function JP(t){return t&&Va(t.canActivateChild)}function eL(t){return t&&Va(t.canDeactivate)}function tL(t){return t&&Va(t.canMatch)}function vS(t){return t instanceof An||t?.name==="EmptyError"}var dd=Symbol("INITIAL_VALUE");function ji(){return Ve(t=>Ro(t.map(n=>n.pipe(Ne(1),No(dd)))).pipe(V(n=>{for(let e of n)if(e!==!0){if(e===dd)return dd;if(e===!1||nL(e))return e}return!0}),he(n=>n!==dd),Ne(1)))}function nL(t){return Sr(t)||t instanceof Li}function yS(t){return t.aborted?S(void 0).pipe(Ne(1)):new P(n=>{let e=()=>{n.next(),n.complete()};return t.addEventListener("abort",e),()=>t.removeEventListener("abort",e)})}function bS(t){return jt(yS(t))}function rL(t){return Ee(n=>{let{targetSnapshot:e,currentSnapshot:r,guards:{canActivateChecks:o,canDeactivateChecks:i}}=n;return i.length===0&&o.length===0?S(F(g({},n),{guardsResult:!0})):oL(i,e,r).pipe(Ee(s=>s&&KP(s)?iL(e,o,t):S(s)),V(s=>F(g({},n),{guardsResult:s})))})}function oL(t,n,e){return ae(t).pipe(Ee(r=>uL(r.component,r.route,e,n)),Nn(r=>r!==!0,!0))}function iL(t,n,e){return ae(n).pipe(or(r=>ln(aL(r.route.parent,e),sL(r.route,e),lL(t,r.path),cL(t,r.route))),Nn(r=>r!==!0,!0))}function sL(t,n){return t!==null&&n&&n(new Sd(t)),S(!0)}function aL(t,n){return t!==null&&n&&n(new Cd(t)),S(!0)}function cL(t,n){let e=n.routeConfig?n.routeConfig.canActivate:null;if(!e||e.length===0)return S(!0);let r=e.map(o=>qi(()=>{let i=n._environmentInjector,s=Vi(o,i),a=XP(s)?s.canActivate(n,t):Fe(i,()=>s(n,t));return Eo(a).pipe(Nn())}));return S(r).pipe(ji())}function lL(t,n){let e=n[n.length-1],o=n.slice(0,n.length-1).reverse().map(i=>qP(i)).filter(i=>i!==null).map(i=>qi(()=>{let s=i.guards.map(a=>{let c=i.node._environmentInjector,l=Vi(a,c),u=JP(l)?l.canActivateChild(e,t):Fe(c,()=>l(e,t));return Eo(u).pipe(Nn())});return S(s).pipe(ji())}));return S(o).pipe(ji())}function uL(t,n,e,r){let o=n&&n.routeConfig?n.routeConfig.canDeactivate:null;if(!o||o.length===0)return S(!0);let i=o.map(s=>{let a=n._environmentInjector,c=Vi(s,a),l=eL(c)?c.canDeactivate(t,n,e,r):Fe(a,()=>c(t,n,e,r));return Eo(l).pipe(Nn())});return S(i).pipe(ji())}function dL(t,n,e,r,o){let i=n.canLoad;if(i===void 0||i.length===0)return S(!0);let s=i.map(a=>{let c=Vi(a,t),l=QP(c)?c.canLoad(n,e):Fe(t,()=>c(n,e)),u=Eo(l);return o?u.pipe(bS(o)):u});return S(s).pipe(ji(),_S(r))}function _S(t){return qd(Be(n=>{if(typeof n!="boolean")throw Ad(t,n)}),V(n=>n===!0))}function fL(t,n,e,r,o,i){let s=n.canMatch;if(!s||s.length===0)return S(!0);let a=s.map(c=>{let l=Vi(c,t),u=tL(l)?l.canMatch(n,e,o):Fe(t,()=>l(n,e,o));return Eo(u).pipe(bS(i))});return S(a).pipe(ji(),_S(r))}var Qn=class t extends Error{segmentGroup;constructor(n){super(),this.segmentGroup=n||null,Object.setPrototypeOf(this,t.prototype)}},Pa=class t extends Error{urlTree;constructor(n){super(),this.urlTree=n,Object.setPrototypeOf(this,t.prototype)}};function hL(t){throw new b(4e3,!1)}function pL(t){throw mS(!1,et.GuardRejected)}var dv=class{urlSerializer;urlTree;constructor(n,e){this.urlSerializer=n,this.urlTree=e}async lineralizeSegments(n,e){let r=[],o=e.root;for(;;){if(r=r.concat(o.segments),o.numberOfChildren===0)return r;if(o.numberOfChildren>1||!o.children[B])throw hL(`${n.redirectTo}`);o=o.children[B]}}async applyRedirectCommands(n,e,r,o,i){let s=await mL(e,o,i);if(s instanceof it)throw new Pa(s);let a=this.applyRedirectCreateUrlTree(s,this.urlSerializer.parse(s),n,r);if(s[0]==="/")throw new Pa(a);return a}applyRedirectCreateUrlTree(n,e,r,o){let i=this.createSegmentGroup(n,e.root,r,o);return new it(i,this.createQueryParams(e.queryParams,this.urlTree.queryParams),e.fragment)}createQueryParams(n,e){let r={};return Object.entries(n).forEach(([o,i])=>{if(typeof i=="string"&&i[0]===":"){let a=i.substring(1);r[o]=e[a]}else r[o]=i}),r}createSegmentGroup(n,e,r,o){let i=this.createSegments(n,e.segments,r,o),s={};return Object.entries(e.children).forEach(([a,c])=>{s[a]=this.createSegmentGroup(n,c,r,o)}),new te(i,s)}createSegments(n,e,r,o){return e.map(i=>i.path[0]===":"?this.findPosParam(n,i,o):this.findOrReturn(i,r))}findPosParam(n,e,r){let o=r[e.path.substring(1)];if(!o)throw new b(4001,!1);return o}findOrReturn(n,e){let r=0;for(let o of e){if(o.path===n.path)return e.splice(r),o;r++}return n}};function mL(t,n,e){if(typeof t=="string")return Promise.resolve(t);let r=t;return md(Eo(Fe(e,()=>r(n))))}function gL(t,n){return t.providers&&!t._injector&&(t._injector=oi(t.providers,n,`Route: ${t.path}`)),t._injector??n}function sn(t){return t.outlet||B}function vL(t,n){let e=t.filter(r=>sn(r)===n);return e.push(...t.filter(r=>sn(r)!==n)),e}var fv={matched:!1,consumedSegments:[],remainingSegments:[],parameters:{},positionalParamSegments:{}};function DS(t){return{routeConfig:t.routeConfig,url:t.url,params:t.params,queryParams:t.queryParams,fragment:t.fragment,data:t.data,outlet:t.outlet,title:t.title,paramMap:t.paramMap,queryParamMap:t.queryParamMap}}function yL(t,n,e,r,o,i,s){let a=ES(t,n,e);if(!a.matched)return S(a);let c=DS(i(a));return r=gL(n,r),fL(r,n,e,o,c,s).pipe(V(l=>l===!0?a:g({},fv)))}function ES(t,n,e){if(n.path==="")return n.pathMatch==="full"&&(t.hasChildren()||e.length>0)?g({},fv):{matched:!0,consumedSegments:[],remainingSegments:e,parameters:{},positionalParamSegments:{}};let o=(n.matcher||qI)(e,t,n);if(!o)return g({},fv);let i={};Object.entries(o.posParams??{}).forEach(([a,c])=>{i[a]=c.path});let s=o.consumed.length>0?g(g({},i),o.consumed[o.consumed.length-1].parameters):i;return{matched:!0,consumedSegments:o.consumed,remainingSegments:e.slice(o.consumed.length),parameters:s,positionalParamSegments:o.posParams??{}}}function GI(t,n,e,r,o){return e.length>0&&DL(t,e,r,o)?{segmentGroup:new te(n,_L(r,new te(e,t.children))),slicedSegments:[]}:e.length===0&&EL(t,e,r)?{segmentGroup:new te(t.segments,bL(t,e,r,t.children)),slicedSegments:e}:{segmentGroup:new te(t.segments,t.children),slicedSegments:e}}function bL(t,n,e,r){let o={};for(let i of e)if(Od(t,n,i)&&!r[sn(i)]){let s=new te([],{});o[sn(i)]=s}return g(g({},r),o)}function _L(t,n){let e={};e[B]=n;for(let r of t)if(r.path===""&&sn(r)!==B){let o=new te([],{});e[sn(r)]=o}return e}function DL(t,n,e,r){return e.some(o=>!Od(t,n,o)||!(sn(o)!==B)?!1:!(r!==void 0&&sn(o)===r))}function EL(t,n,e){return e.some(r=>Od(t,n,r))}function Od(t,n,e){return(t.hasChildren()||n.length>0)&&e.pathMatch==="full"?!1:e.path===""}function wL(t,n,e){return n.length===0&&!t.children[e]}var hv=class{};async function CL(t,n,e,r,o,i,s="emptyOnly",a){return new pv(t,n,e,r,o,s,i,a).recognize()}var IL=31,pv=class{injector;configLoader;rootComponentType;config;urlTree;paramsInheritanceStrategy;urlSerializer;abortSignal;applyRedirects;absoluteRedirectCount=0;allowRedirects=!0;constructor(n,e,r,o,i,s,a,c){this.injector=n,this.configLoader=e,this.rootComponentType=r,this.config=o,this.urlTree=i,this.paramsInheritanceStrategy=s,this.urlSerializer=a,this.abortSignal=c,this.applyRedirects=new dv(this.urlSerializer,this.urlTree)}noMatchError(n){return new b(4002,`'${n.segmentGroup}'`)}async recognize(){let n=GI(this.urlTree.root,[],[],this.config).segmentGroup,{children:e,rootSnapshot:r}=await this.match(n),o=new _t(r,e),i=new ka("",o),s=oS(r,[],this.urlTree.queryParams,this.urlTree.fragment);return s.queryParams=this.urlTree.queryParams,i.url=this.urlSerializer.serialize(s),{state:i,tree:s}}async match(n){let e=new Pi([],Object.freeze({}),Object.freeze(g({},this.urlTree.queryParams)),this.urlTree.fragment,Object.freeze({}),B,this.rootComponentType,null,{},this.injector);try{return{children:await this.processSegmentGroup(this.injector,this.config,n,B,e),rootSnapshot:e}}catch(r){if(r instanceof Pa)return this.urlTree=r.urlTree,this.match(r.urlTree.root);throw r instanceof Qn?this.noMatchError(r):r}}async processSegmentGroup(n,e,r,o,i){if(r.segments.length===0&&r.hasChildren())return this.processChildren(n,e,r,i);let s=await this.processSegment(n,e,r,r.segments,o,!0,i);return s instanceof _t?[s]:[]}async processChildren(n,e,r,o){let i=[];for(let c of Object.keys(r.children))c==="primary"?i.unshift(c):i.push(c);let s=[];for(let c of i){let l=r.children[c],u=vL(e,c),d=await this.processSegmentGroup(n,u,l,c,o);s.push(...d)}let a=wS(s);return SL(a),a}async processSegment(n,e,r,o,i,s,a){for(let c of e)try{return await this.processSegmentAgainstRoute(c._injector??n,e,c,r,o,i,s,a)}catch(l){if(l instanceof Qn||vS(l))continue;throw l}if(wL(r,o,i))return new hv;throw new Qn(r)}async processSegmentAgainstRoute(n,e,r,o,i,s,a,c){if(sn(r)!==s&&(s===B||!Od(o,i,r)))throw new Qn(o);if(r.redirectTo===void 0)return this.matchSegmentAgainstRoute(n,o,r,i,s,c);if(this.allowRedirects&&a)return this.expandSegmentAgainstRouteUsingRedirect(n,o,e,r,i,s,c);throw new Qn(o)}async expandSegmentAgainstRouteUsingRedirect(n,e,r,o,i,s,a){let{matched:c,parameters:l,consumedSegments:u,positionalParamSegments:d,remainingSegments:h}=ES(e,o,i);if(!c)throw new Qn(e);typeof o.redirectTo=="string"&&o.redirectTo[0]==="/"&&(this.absoluteRedirectCount++,this.absoluteRedirectCount>IL&&(this.allowRedirects=!1));let p=this.createSnapshot(n,o,i,l,a);if(this.abortSignal.aborted)throw new Error(this.abortSignal.reason);let m=await this.applyRedirects.applyRedirectCommands(u,o.redirectTo,d,DS(p),n),_=await this.applyRedirects.lineralizeSegments(o,m);return this.processSegment(n,r,e,_.concat(h),s,!1,a)}createSnapshot(n,e,r,o,i){let s=new Pi(r,o,Object.freeze(g({},this.urlTree.queryParams)),this.urlTree.fragment,TL(e),sn(e),e.component??e._loadedComponent??null,e,xL(e),n),a=vv(s,i,this.paramsInheritanceStrategy);return s.params=Object.freeze(a.params),s.data=Object.freeze(a.data),s}async matchSegmentAgainstRoute(n,e,r,o,i,s){if(this.abortSignal.aborted)throw new Error(this.abortSignal.reason);let a=De=>this.createSnapshot(n,r,De.consumedSegments,De.parameters,s),c=await md(yL(e,r,o,n,this.urlSerializer,a,this.abortSignal));if(r.path==="**"&&(e.children={}),!c?.matched)throw new Qn(e);n=r._injector??n;let{routes:l}=await this.getChildConfig(n,r,o),u=r._loadedInjector??n,{parameters:d,consumedSegments:h,remainingSegments:p}=c,m=this.createSnapshot(n,r,h,d,s),{segmentGroup:_,slicedSegments:D}=GI(e,h,p,l,i);if(D.length===0&&_.hasChildren()){let De=await this.processChildren(u,l,_,m);return new _t(m,De)}if(l.length===0&&D.length===0)return new _t(m,[]);let C=sn(r)===i,z=await this.processSegment(u,l,_,D,C?B:i,!0,m);return new _t(m,z instanceof _t?[z]:[])}async getChildConfig(n,e,r){if(e.children)return{routes:e.children,injector:n};if(e.loadChildren){if(e._loadedRoutes!==void 0){let i=e._loadedNgModuleFactory;return i&&!e._loadedInjector&&(e._loadedInjector=i.create(n).injector),{routes:e._loadedRoutes,injector:e._loadedInjector}}if(this.abortSignal.aborted)throw new Error(this.abortSignal.reason);if(await md(dL(n,e,r,this.urlSerializer,this.abortSignal))){let i=await this.configLoader.loadChildren(n,e);return e._loadedRoutes=i.routes,e._loadedInjector=i.injector,e._loadedNgModuleFactory=i.factory,i}throw pL(e)}return{routes:[],injector:n}}};function SL(t){t.sort((n,e)=>n.value.outlet===B?-1:e.value.outlet===B?1:n.value.outlet.localeCompare(e.value.outlet))}function ML(t){let n=t.value.routeConfig;return n&&n.path===""}function wS(t){let n=[],e=new Set;for(let r of t){if(!ML(r)){n.push(r);continue}let o=n.find(i=>r.value.routeConfig===i.value.routeConfig);o!==void 0?(o.children.push(...r.children),e.add(o)):n.push(r)}for(let r of e){let o=wS(r.children);n.push(new _t(r.value,o))}return n.filter(r=>!e.has(r))}function TL(t){return t.data||{}}function xL(t){return t.resolve||{}}function AL(t,n,e,r,o,i,s){return Ee(async a=>{let{state:c,tree:l}=await CL(t,n,e,r,a.extractedUrl,o,i,s);return F(g({},a),{targetSnapshot:c,urlAfterRedirects:l})})}function RL(t){return Ee(n=>{let{targetSnapshot:e,guards:{canActivateChecks:r}}=n;if(!r.length)return S(n);let o=new Set(r.map(a=>a.route)),i=new Set;for(let a of o)if(!i.has(a))for(let c of CS(a))i.add(c);let s=0;return ae(i).pipe(or(a=>o.has(a)?NL(a,e,t):(a.data=vv(a,a.parent,t).resolve,S(void 0))),Be(()=>s++),vc(1),Ee(a=>s===i.size?S(n):Re))})}function CS(t){let n=t.children.map(e=>CS(e)).flat();return[t,...n]}function NL(t,n,e){let r=t.routeConfig,o=t._resolve;return r?.title!==void 0&&!fS(r)&&(o[La]=r.title),qi(()=>(t.data=vv(t,t.parent,e).resolve,OL(o,t,n).pipe(V(i=>(t._resolvedData=i,t.data=g(g({},t.data),i),null)))))}function OL(t,n,e){let r=tv(t);if(r.length===0)return S({});let o={};return ae(r).pipe(Ee(i=>kL(t[i],n,e).pipe(Nn(),Be(s=>{if(s instanceof Li)throw Ad(new Xn,s);o[i]=s}))),vc(1),V(()=>o),It(i=>vS(i)?Re:xn(i)))}function kL(t,n,e){let r=n._environmentInjector,o=Vi(t,r),i=o.resolve?o.resolve(n,e):Fe(r,()=>o(n,e));return Eo(i)}function WI(t){return Ve(n=>{let e=t(n);return e?ae(e).pipe(V(()=>n)):S(n)})}var wv=(()=>{class t{buildTitle(e){let r,o=e.root;for(;o!==void 0;)r=this.getResolvedTitleForRoute(o)??r,o=o.children.find(i=>i.outlet===B);return r}getResolvedTitleForRoute(e){return e.data[La]}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(IS),providedIn:"root"})}return t})(),IS=(()=>{class t extends wv{title;constructor(e){super(),this.title=e}updateTitle(e){let r=this.buildTitle(e);r!==void 0&&this.title.setTitle(r)}static \u0275fac=function(r){return new(r||t)(w(qw))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),xr=new y("",{factory:()=>({})}),Bi=new y(""),kd=(()=>{class t{componentLoaders=new WeakMap;childrenLoaders=new WeakMap;onLoadStartListener;onLoadEndListener;compiler=f(cm);async loadComponent(e,r){if(this.componentLoaders.get(r))return this.componentLoaders.get(r);if(r._loadedComponent)return Promise.resolve(r._loadedComponent);this.onLoadStartListener&&this.onLoadStartListener(r);let o=(async()=>{try{let i=await ZI(Fe(e,()=>r.loadComponent())),s=await TS(MS(i));return this.onLoadEndListener&&this.onLoadEndListener(r),r._loadedComponent=s,s}finally{this.componentLoaders.delete(r)}})();return this.componentLoaders.set(r,o),o}loadChildren(e,r){if(this.childrenLoaders.get(r))return this.childrenLoaders.get(r);if(r._loadedRoutes)return Promise.resolve({routes:r._loadedRoutes,injector:r._loadedInjector});this.onLoadStartListener&&this.onLoadStartListener(r);let o=(async()=>{try{let i=await SS(r,this.compiler,e,this.onLoadEndListener);return r._loadedRoutes=i.routes,r._loadedInjector=i.injector,r._loadedNgModuleFactory=i.factory,i}finally{this.childrenLoaders.delete(r)}})();return this.childrenLoaders.set(r,o),o}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();async function SS(t,n,e,r){let o=await ZI(Fe(e,()=>t.loadChildren())),i=await TS(MS(o)),s;i instanceof Ll||Array.isArray(i)?s=i:s=await n.compileModuleAsync(i),r&&r(t);let a,c,l=!1,u;return Array.isArray(s)?(c=s,l=!0):(a=s.create(e).injector,u=s,c=a.get(Bi,[],{optional:!0,self:!0}).flat()),{routes:c.map(Ev),injector:a,factory:u}}function FL(t){return t&&typeof t=="object"&&"default"in t}function MS(t){return FL(t)?t.default:t}async function TS(t){return t}var Fd=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(PL),providedIn:"root"})}return t})(),PL=(()=>{class t{shouldProcessUrl(e){return!0}extract(e){return e}merge(e,r){return e}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Cv=new y(""),Iv=new y("");function xS(t,n,e){let r=t.get(Iv),o=t.get(O);if(!o.startViewTransition||r.skipNextTransition)return r.skipNextTransition=!1,new Promise(l=>setTimeout(l));let i,s=new Promise(l=>{i=l}),a=o.startViewTransition(()=>(i(),LL(t)));a.updateCallbackDone.catch(l=>{}),a.ready.catch(l=>{}),a.finished.catch(l=>{});let{onViewTransitionCreated:c}=r;return c&&Fe(t,()=>c({transition:a,from:n,to:e})),s}function LL(t){return new Promise(n=>{At({read:()=>setTimeout(n)},{injector:t})})}var jL=()=>{},Sv=new y(""),Pd=(()=>{class t{currentNavigation=K(null,{equal:()=>!1});currentTransition=null;lastSuccessfulNavigation=K(null);events=new x;transitionAbortWithErrorSubject=new x;configLoader=f(kd);environmentInjector=f(re);destroyRef=f(Ue);urlSerializer=f(Tr);rootContexts=f(wo);location=f(Dn);inputBindingEnabled=f(ja,{optional:!0})!==null;titleStrategy=f(wv);options=f(xr,{optional:!0})||{};paramsInheritanceStrategy=this.options.paramsInheritanceStrategy||"emptyOnly";urlHandlingStrategy=f(Fd);createViewTransition=f(Cv,{optional:!0});navigationErrorHandler=f(Sv,{optional:!0});navigationId=0;get hasRequestedNavigation(){return this.navigationId!==0}transitions;afterPreactivation=()=>S(void 0);rootComponentType=null;destroyed=!1;constructor(){let e=o=>this.events.next(new Ed(o)),r=o=>this.events.next(new wd(o));this.configLoader.onLoadEndListener=r,this.configLoader.onLoadStartListener=e,this.destroyRef.onDestroy(()=>{this.destroyed=!0})}complete(){this.transitions?.complete()}handleNavigationRequest(e){let r=++this.navigationId;Y(()=>{this.transitions?.next(F(g({},e),{extractedUrl:this.urlHandlingStrategy.extract(e.rawUrl),targetSnapshot:null,targetRouterState:null,guards:{canActivateChecks:[],canDeactivateChecks:[]},guardsResult:null,id:r,routesRecognizeHandler:{},beforeActivateHandler:{}}))})}setupNavigations(e){return this.transitions=new Ae(null),this.transitions.pipe(he(r=>r!==null),Ve(r=>{let o=!1,i=new AbortController,s=()=>!o&&this.currentTransition?.id===r.id;return S(r).pipe(Ve(a=>{if(this.navigationId>r.id)return this.cancelNavigationTransition(r,"",et.SupersededByNewNavigation),Re;this.currentTransition=r;let c=this.lastSuccessfulNavigation();this.currentNavigation.set({id:a.id,initialUrl:a.rawUrl,extractedUrl:a.extractedUrl,targetBrowserUrl:typeof a.extras.browserUrl=="string"?this.urlSerializer.parse(a.extras.browserUrl):a.extras.browserUrl,trigger:a.source,extras:a.extras,previousNavigation:c?F(g({},c),{previousNavigation:null}):null,abort:()=>i.abort(),routesRecognizeHandler:a.routesRecognizeHandler,beforeActivateHandler:a.beforeActivateHandler});let l=!e.navigated||this.isUpdatingInternalState()||this.isUpdatedBrowserUrl(),u=a.extras.onSameUrlNavigation??e.onSameUrlNavigation;if(!l&&u!=="reload")return this.events.next(new Mn(a.id,this.urlSerializer.serialize(a.rawUrl),"",Ni.IgnoredSameUrlNavigation)),a.resolve(!1),Re;if(this.urlHandlingStrategy.shouldProcessUrl(a.rawUrl))return S(a).pipe(Ve(d=>(this.events.next(new Mr(d.id,this.urlSerializer.serialize(d.extractedUrl),d.source,d.restoredState)),d.id!==this.navigationId?Re:Promise.resolve(d))),AL(this.environmentInjector,this.configLoader,this.rootComponentType,e.config,this.urlSerializer,this.paramsInheritanceStrategy,i.signal),Be(d=>{r.targetSnapshot=d.targetSnapshot,r.urlAfterRedirects=d.urlAfterRedirects,this.currentNavigation.update(h=>(h.finalUrl=d.urlAfterRedirects,h)),this.events.next(new Na)}),Ve(d=>ae(r.routesRecognizeHandler.deferredHandle??S(void 0)).pipe(V(()=>d))),Be(()=>{let d=new Ra(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);this.events.next(d)}));if(l&&this.urlHandlingStrategy.shouldProcessUrl(a.currentRawUrl)){let{id:d,extractedUrl:h,source:p,restoredState:m,extras:_}=a,D=new Mr(d,this.urlSerializer.serialize(h),p,m);this.events.next(D);let C=uS(this.rootComponentType,this.environmentInjector).snapshot;return this.currentTransition=r=F(g({},a),{targetSnapshot:C,urlAfterRedirects:h,extras:F(g({},_),{skipLocationChange:!1,replaceUrl:!1})}),this.currentNavigation.update(z=>(z.finalUrl=h,z)),S(r)}else return this.events.next(new Mn(a.id,this.urlSerializer.serialize(a.extractedUrl),"",Ni.IgnoredByUrlHandlingStrategy)),a.resolve(!1),Re}),V(a=>{let c=new yd(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);return this.events.next(c),this.currentTransition=r=F(g({},a),{guards:WP(a.targetSnapshot,a.currentSnapshot,this.rootContexts)}),r}),rL(a=>this.events.next(a)),Ve(a=>{if(r.guardsResult=a.guardsResult,a.guardsResult&&typeof a.guardsResult!="boolean")throw Ad(this.urlSerializer,a.guardsResult);let c=new bd(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot,!!a.guardsResult);if(this.events.next(c),!s())return Re;if(!a.guardsResult)return this.cancelNavigationTransition(a,"",et.GuardRejected),Re;if(a.guards.canActivateChecks.length===0)return S(a);let l=new _d(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);if(this.events.next(l),!s())return Re;let u=!1;return S(a).pipe(RL(this.paramsInheritanceStrategy),Be({next:()=>{u=!0;let d=new Dd(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);this.events.next(d)},complete:()=>{u||this.cancelNavigationTransition(a,"",et.NoDataFromResolver)}}))}),WI(a=>{let c=u=>{let d=[];if(u.routeConfig?._loadedComponent)u.component=u.routeConfig?._loadedComponent;else if(u.routeConfig?.loadComponent){let h=u._environmentInjector;d.push(this.configLoader.loadComponent(h,u.routeConfig).then(p=>{u.component=p}))}for(let h of u.children)d.push(...c(h));return d},l=c(a.targetSnapshot.root);return l.length===0?S(a):ae(Promise.all(l).then(()=>a))}),WI(()=>this.afterPreactivation()),Ve(()=>{let{currentSnapshot:a,targetSnapshot:c}=r,l=this.createViewTransition?.(this.environmentInjector,a.root,c.root);return l?ae(l).pipe(V(()=>r)):S(r)}),Ne(1),Ve(a=>{let c=HP(e.routeReuseStrategy,a.targetSnapshot,a.currentRouterState);this.currentTransition=r=a=F(g({},a),{targetRouterState:c}),this.currentNavigation.update(u=>(u.targetRouterState=c,u)),this.events.next(new ki);let l=r.beforeActivateHandler.deferredHandle;return l?ae(l.then(()=>a)):S(a)}),Be(a=>{new uv(e.routeReuseStrategy,r.targetRouterState,r.currentRouterState,c=>this.events.next(c),this.inputBindingEnabled).activate(this.rootContexts),s()&&(o=!0,this.currentNavigation.update(c=>(c.abort=jL,c)),this.lastSuccessfulNavigation.set(Y(this.currentNavigation)),this.events.next(new Et(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects))),this.titleStrategy?.updateTitle(a.targetRouterState.snapshot),a.resolve(!0))}),jt(yS(i.signal).pipe(he(()=>!o&&!r.targetRouterState),Be(()=>{this.cancelNavigationTransition(r,i.signal.reason+"",et.Aborted)}))),Be({complete:()=>{o=!0}}),jt(this.transitionAbortWithErrorSubject.pipe(Be(a=>{throw a}))),Rn(()=>{i.abort(),o||this.cancelNavigationTransition(r,"",et.SupersededByNewNavigation),this.currentTransition?.id===r.id&&(this.currentNavigation.set(null),this.currentTransition=null)}),It(a=>{if(o=!0,this.destroyed)return r.resolve(!1),Re;if(gS(a))this.events.next(new Ft(r.id,this.urlSerializer.serialize(r.extractedUrl),a.message,a.cancellationCode)),GP(a)?this.events.next(new Fi(a.url,a.navigationBehaviorOptions)):r.resolve(!1);else{let c=new Do(r.id,this.urlSerializer.serialize(r.extractedUrl),a,r.targetSnapshot??void 0);try{let l=Fe(this.environmentInjector,()=>this.navigationErrorHandler?.(c));if(l instanceof Li){let{message:u,cancellationCode:d}=Ad(this.urlSerializer,l);this.events.next(new Ft(r.id,this.urlSerializer.serialize(r.extractedUrl),u,d)),this.events.next(new Fi(l.redirectTo,l.navigationBehaviorOptions))}else throw this.events.next(c),a}catch(l){this.options.resolveNavigationPromiseOnError?r.resolve(!1):r.reject(l)}}return Re}))}))}cancelNavigationTransition(e,r,o){let i=new Ft(e.id,this.urlSerializer.serialize(e.extractedUrl),r,o);this.events.next(i),e.resolve(!1)}isUpdatingInternalState(){return this.currentTransition?.extractedUrl.toString()!==this.currentTransition?.currentUrlTree.toString()}isUpdatedBrowserUrl(){let e=this.urlHandlingStrategy.extract(this.urlSerializer.parse(this.location.path(!0))),r=Y(this.currentNavigation),o=r?.targetBrowserUrl??r?.extractedUrl;return e.toString()!==o?.toString()&&!r?.extras.skipLocationChange}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function VL(t){return t!==Ai}var AS=new y("");var RS=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(BL),providedIn:"root"})}return t})(),Nd=class{shouldDetach(n){return!1}store(n,e){}shouldAttach(n){return!1}retrieve(n){return null}shouldReuseRoute(n,e){return n.routeConfig===e.routeConfig}shouldDestroyInjector(n){return!0}},BL=(()=>{class t extends Nd{static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Ld=(()=>{class t{urlSerializer=f(Tr);options=f(xr,{optional:!0})||{};canceledNavigationResolution=this.options.canceledNavigationResolution||"replace";location=f(Dn);urlHandlingStrategy=f(Fd);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";currentUrlTree=new it;getCurrentUrlTree(){return this.currentUrlTree}rawUrlTree=this.currentUrlTree;getRawUrlTree(){return this.rawUrlTree}createBrowserPath({finalUrl:e,initialUrl:r,targetBrowserUrl:o}){let i=e!==void 0?this.urlHandlingStrategy.merge(e,r):r,s=o??i;return s instanceof it?this.urlSerializer.serialize(s):s}routerUrlState(e){return e?.targetBrowserUrl===void 0||e?.finalUrl===void 0?{}:{\u0275routerUrl:this.urlSerializer.serialize(e.finalUrl)}}commitTransition({targetRouterState:e,finalUrl:r,initialUrl:o}){r&&e?(this.currentUrlTree=r,this.rawUrlTree=this.urlHandlingStrategy.merge(r,o),this.routerState=e):this.rawUrlTree=o}routerState=uS(null,f(re));getRouterState(){return this.routerState}_stateMemento=this.createStateMemento();get stateMemento(){return this._stateMemento}updateStateMemento(){this._stateMemento=this.createStateMemento()}createStateMemento(){return{rawUrlTree:this.rawUrlTree,currentUrlTree:this.currentUrlTree,routerState:this.routerState}}restoredState(){return this.location.getState()}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(UL),providedIn:"root"})}return t})(),UL=(()=>{class t extends Ld{currentPageId=0;lastSuccessfulId=-1;get browserPageId(){return this.canceledNavigationResolution!=="computed"?this.currentPageId:this.restoredState()?.\u0275routerPageId??this.currentPageId}registerNonRouterCurrentEntryChangeListener(e){return this.location.subscribe(r=>{r.type==="popstate"&&setTimeout(()=>{e(r.url,r.state,"popstate",{replaceUrl:!0})})})}handleRouterEvent(e,r){e instanceof Mr?this.updateStateMemento():e instanceof Mn?this.commitTransition(r):e instanceof Ra?this.urlUpdateStrategy==="eager"&&(r.extras.skipLocationChange||this.setBrowserUrl(this.createBrowserPath(r),r)):e instanceof ki?(this.commitTransition(r),this.urlUpdateStrategy==="deferred"&&!r.extras.skipLocationChange&&this.setBrowserUrl(this.createBrowserPath(r),r)):e instanceof Ft&&!lS(e)?this.restoreHistory(r):e instanceof Do?this.restoreHistory(r,!0):e instanceof Et&&(this.lastSuccessfulId=e.id,this.currentPageId=this.browserPageId)}setBrowserUrl(e,r){let{extras:o,id:i}=r,{replaceUrl:s,state:a}=o;if(this.location.isCurrentPathEqualTo(e)||s){let c=this.browserPageId,l=g(g({},a),this.generateNgRouterState(i,c,r));this.location.replaceState(e,"",l)}else{let c=g(g({},a),this.generateNgRouterState(i,this.browserPageId+1,r));this.location.go(e,"",c)}}restoreHistory(e,r=!1){if(this.canceledNavigationResolution==="computed"){let o=this.browserPageId,i=this.currentPageId-o;i!==0?this.location.historyGo(i):this.getCurrentUrlTree()===e.finalUrl&&i===0&&(this.resetInternalState(e),this.resetUrlToCurrentUrlTree())}else this.canceledNavigationResolution==="replace"&&(r&&this.resetInternalState(e),this.resetUrlToCurrentUrlTree())}resetInternalState({finalUrl:e}){this.routerState=this.stateMemento.routerState,this.currentUrlTree=this.stateMemento.currentUrlTree,this.rawUrlTree=this.urlHandlingStrategy.merge(this.currentUrlTree,e??this.rawUrlTree)}resetUrlToCurrentUrlTree(){this.location.replaceState(this.urlSerializer.serialize(this.getRawUrlTree()),"",this.generateNgRouterState(this.lastSuccessfulId,this.currentPageId))}generateNgRouterState(e,r,o){return this.canceledNavigationResolution==="computed"?g({navigationId:e,\u0275routerPageId:r},this.routerUrlState(o)):g({navigationId:e},this.routerUrlState(o))}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Pe(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function jd(t,n){t.events.pipe(he(e=>e instanceof Et||e instanceof Ft||e instanceof Do||e instanceof Mn),V(e=>e instanceof Et||e instanceof Mn?0:(e instanceof Ft?e.code===et.Redirect||e.code===et.SupersededByNewNavigation:!1)?2:1),he(e=>e!==2),Ne(1)).subscribe(()=>{n()})}var wt=(()=>{class t{get currentUrlTree(){return this.stateManager.getCurrentUrlTree()}get rawUrlTree(){return this.stateManager.getRawUrlTree()}disposed=!1;nonRouterCurrentEntryChangeSubscription;console=f(jl);stateManager=f(Ld);options=f(xr,{optional:!0})||{};pendingTasks=f(Bn);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";navigationTransitions=f(Pd);urlSerializer=f(Tr);location=f(Dn);urlHandlingStrategy=f(Fd);injector=f(re);_events=new x;get events(){return this._events}get routerState(){return this.stateManager.getRouterState()}navigated=!1;routeReuseStrategy=f(RS);injectorCleanup=f(AS,{optional:!0});onSameUrlNavigation=this.options.onSameUrlNavigation||"ignore";config=f(Bi,{optional:!0})?.flat()??[];componentInputBindingEnabled=!!f(ja,{optional:!0});currentNavigation=this.navigationTransitions.currentNavigation.asReadonly();constructor(){this.resetConfig(this.config),this.navigationTransitions.setupNavigations(this).subscribe({error:e=>{}}),this.subscribeToNavigationEvents()}eventsSubscription=new G;subscribeToNavigationEvents(){let e=this.navigationTransitions.events.subscribe(r=>{try{let o=this.navigationTransitions.currentTransition,i=Y(this.navigationTransitions.currentNavigation);if(o!==null&&i!==null){if(this.stateManager.handleRouterEvent(r,i),r instanceof Ft&&r.code!==et.Redirect&&r.code!==et.SupersededByNewNavigation)this.navigated=!0;else if(r instanceof Et)this.navigated=!0,this.injectorCleanup?.(this.routeReuseStrategy,this.routerState,this.config);else if(r instanceof Fi){let s=r.navigationBehaviorOptions,a=this.urlHandlingStrategy.merge(r.url,o.currentRawUrl),c=g({scroll:o.extras.scroll,browserUrl:o.extras.browserUrl,info:o.extras.info,skipLocationChange:o.extras.skipLocationChange,replaceUrl:o.extras.replaceUrl||this.urlUpdateStrategy==="eager"||VL(o.source)},s);this.scheduleNavigation(a,Ai,null,c,{resolve:o.resolve,reject:o.reject,promise:o.promise})}}BP(r)&&this._events.next(r)}catch(o){this.navigationTransitions.transitionAbortWithErrorSubject.next(o)}});this.eventsSubscription.add(e)}resetRootComponentType(e){this.routerState.root.component=e,this.navigationTransitions.rootComponentType=e}initialNavigation(){this.setUpLocationChangeListener(),this.navigationTransitions.hasRequestedNavigation||this.navigateToSyncWithBrowser(this.location.path(!0),Ai,this.stateManager.restoredState(),{replaceUrl:!0})}setUpLocationChangeListener(){this.nonRouterCurrentEntryChangeSubscription??=this.stateManager.registerNonRouterCurrentEntryChangeListener((e,r,o,i)=>{this.navigateToSyncWithBrowser(e,o,r,i)})}navigateToSyncWithBrowser(e,r,o,i){let s=o?.navigationId?o:null,a=o?.\u0275routerUrl??e;if(o?.\u0275routerUrl&&(i=F(g({},i),{browserUrl:e})),o){let l=g({},o);delete l.navigationId,delete l.\u0275routerPageId,delete l.\u0275routerUrl,Object.keys(l).length!==0&&(i.state=l)}let c=this.parseUrl(a);this.scheduleNavigation(c,r,s,i).catch(l=>{this.disposed||this.injector.get(mt)(l)})}get url(){return this.serializeUrl(this.currentUrlTree)}getCurrentNavigation(){return Y(this.navigationTransitions.currentNavigation)}get lastSuccessfulNavigation(){return this.navigationTransitions.lastSuccessfulNavigation}resetConfig(e){this.config=e.map(Ev),this.navigated=!1}ngOnDestroy(){this.dispose()}dispose(){this._events.unsubscribe(),this.navigationTransitions.complete(),this.nonRouterCurrentEntryChangeSubscription?.unsubscribe(),this.nonRouterCurrentEntryChangeSubscription=void 0,this.disposed=!0,this.eventsSubscription.unsubscribe()}createUrlTree(e,r={}){let{relativeTo:o,queryParams:i,fragment:s,queryParamsHandling:a,preserveFragment:c}=r,l=c?this.currentUrlTree.fragment:s,u=null;switch(a??this.options.defaultQueryParamsHandling){case"merge":u=g(g({},this.currentUrlTree.queryParams),i);break;case"preserve":u=this.currentUrlTree.queryParams;break;default:u=i||null}u!==null&&(u=this.removeEmptyProps(u));let d;try{let h=o?o.snapshot:this.routerState.snapshot.root;d=iS(h)}catch{(typeof e[0]!="string"||e[0][0]!=="/")&&(e=[]),d=this.currentUrlTree.root}return sS(d,e,u,l??null,this.urlSerializer)}navigateByUrl(e,r={skipLocationChange:!1}){let o=Sr(e)?e:this.parseUrl(e),i=this.urlHandlingStrategy.merge(o,this.rawUrlTree);return this.scheduleNavigation(i,Ai,null,r)}navigate(e,r={skipLocationChange:!1}){return HL(e),this.navigateByUrl(this.createUrlTree(e,r),r)}serializeUrl(e){return this.urlSerializer.serialize(e)}parseUrl(e){try{return this.urlSerializer.parse(e)}catch{return this.console.warn(St(4018,!1)),this.urlSerializer.parse("/")}}isActive(e,r){let o;if(r===!0?o=g({},mv):r===!1?o=g({},xa):o=g(g({},xa),r),Sr(e))return nv(this.currentUrlTree,e,o);let i=this.parseUrl(e);return nv(this.currentUrlTree,i,o)}removeEmptyProps(e){return Object.entries(e).reduce((r,[o,i])=>(i!=null&&(r[o]=i),r),{})}scheduleNavigation(e,r,o,i,s){if(this.disposed)return Promise.resolve(!1);let a,c,l;s?(a=s.resolve,c=s.reject,l=s.promise):l=new Promise((d,h)=>{a=d,c=h});let u=this.pendingTasks.add();return jd(this,()=>{queueMicrotask(()=>this.pendingTasks.remove(u))}),this.navigationTransitions.handleNavigationRequest({source:r,restoredState:o,currentUrlTree:this.currentUrlTree,currentRawUrl:this.currentUrlTree,rawUrl:e,extras:i,resolve:a,reject:c,promise:l,currentSnapshot:this.routerState.snapshot,currentRouterState:this.routerState}),l.catch(Promise.reject.bind(Promise))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function HL(t){for(let n=0;n{class t{router=f(wt);stateManager=f(Ld);fragment=K("");queryParams=K({});path=K("");serializer=f(Tr);constructor(){this.updateState(),this.router.events?.subscribe(e=>{e instanceof Et&&this.updateState()})}updateState(){let{fragment:e,root:r,queryParams:o}=this.stateManager.getCurrentUrlTree();this.fragment.set(e),this.queryParams.set(o),this.path.set(this.serializer.serialize(new it(r)))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Vd=(()=>{class t{router;route;tabIndexAttribute;renderer;el;locationStrategy;hrefAttributeValue=f(new ui("href"),{optional:!0});reactiveHref=um(()=>this.isAnchorElement?this.computeHref(this._urlTree()):this.hrefAttributeValue);get href(){return Y(this.reactiveHref)}set href(e){this.reactiveHref.set(e)}set target(e){this._target.set(e)}get target(){return Y(this._target)}_target=K(void 0);set queryParams(e){this._queryParams.set(e)}get queryParams(){return Y(this._queryParams)}_queryParams=K(void 0,{equal:()=>!1});set fragment(e){this._fragment.set(e)}get fragment(){return Y(this._fragment)}_fragment=K(void 0);set queryParamsHandling(e){this._queryParamsHandling.set(e)}get queryParamsHandling(){return Y(this._queryParamsHandling)}_queryParamsHandling=K(void 0);set state(e){this._state.set(e)}get state(){return Y(this._state)}_state=K(void 0,{equal:()=>!1});set info(e){this._info.set(e)}get info(){return Y(this._info)}_info=K(void 0,{equal:()=>!1});set relativeTo(e){this._relativeTo.set(e)}get relativeTo(){return Y(this._relativeTo)}_relativeTo=K(void 0);set preserveFragment(e){this._preserveFragment.set(e)}get preserveFragment(){return Y(this._preserveFragment)}_preserveFragment=K(!1);set skipLocationChange(e){this._skipLocationChange.set(e)}get skipLocationChange(){return Y(this._skipLocationChange)}_skipLocationChange=K(!1);set replaceUrl(e){this._replaceUrl.set(e)}get replaceUrl(){return Y(this._replaceUrl)}_replaceUrl=K(!1);isAnchorElement;onChanges=new x;applicationErrorHandler=f(mt);options=f(xr,{optional:!0});reactiveRouterState=f($L);constructor(e,r,o,i,s,a){this.router=e,this.route=r,this.tabIndexAttribute=o,this.renderer=i,this.el=s,this.locationStrategy=a;let c=s.nativeElement.tagName?.toLowerCase();this.isAnchorElement=c==="a"||c==="area"||!!(typeof customElements=="object"&&customElements.get(c)?.observedAttributes?.includes?.("href"))}setTabIndexIfNotOnNativeEl(e){this.tabIndexAttribute!=null||this.isAnchorElement||this.applyAttributeValue("tabindex",e)}ngOnChanges(e){this.onChanges.next(this)}routerLinkInput=K(null);set routerLink(e){e==null?(this.routerLinkInput.set(null),this.setTabIndexIfNotOnNativeEl(null)):(Sr(e)?this.routerLinkInput.set(e):this.routerLinkInput.set(Array.isArray(e)?e:[e]),this.setTabIndexIfNotOnNativeEl("0"))}onClick(e,r,o,i,s){let a=this._urlTree();if(a===null||this.isAnchorElement&&(e!==0||r||o||i||s||typeof this.target=="string"&&this.target!="_self"))return!0;let c={skipLocationChange:this.skipLocationChange,replaceUrl:this.replaceUrl,state:this.state,info:this.info};return this.router.navigateByUrl(a,c)?.catch(l=>{this.applicationErrorHandler(l)}),!this.isAnchorElement}ngOnDestroy(){}applyAttributeValue(e,r){let o=this.renderer,i=this.el.nativeElement;r!==null?o.setAttribute(i,e,r):o.removeAttribute(i,e)}_urlTree=Xt(()=>{this.reactiveRouterState.path(),this._preserveFragment()&&this.reactiveRouterState.fragment();let e=o=>o==="preserve"||o==="merge";(e(this._queryParamsHandling())||e(this.options?.defaultQueryParamsHandling))&&this.reactiveRouterState.queryParams();let r=this.routerLinkInput();return r===null||!this.router.createUrlTree?null:Sr(r)?r:this.router.createUrlTree(r,{relativeTo:this._relativeTo()!==void 0?this._relativeTo():this.route,queryParams:this._queryParams(),fragment:this._fragment(),queryParamsHandling:this._queryParamsHandling(),preserveFragment:this._preserveFragment()})},{equal:(e,r)=>this.computeHref(e)===this.computeHref(r)});get urlTree(){return Y(this._urlTree)}computeHref(e){return e!==null&&this.locationStrategy?this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(e))??"":null}static \u0275fac=function(r){return new(r||t)(E(wt),E(Jn),ks("tabindex"),E(Le),E($),E(tn))};static \u0275dir=T({type:t,selectors:[["","routerLink",""]],hostVars:2,hostBindings:function(r,o){r&1&&Qt("click",function(s){return o.onClick(s.button,s.ctrlKey,s.shiftKey,s.altKey,s.metaKey)}),r&2&&yt("href",o.reactiveHref(),wp)("target",o._target())},inputs:{target:"target",queryParams:"queryParams",fragment:"fragment",queryParamsHandling:"queryParamsHandling",state:"state",info:"info",relativeTo:"relativeTo",preserveFragment:[2,"preserveFragment","preserveFragment",ue],skipLocationChange:[2,"skipLocationChange","skipLocationChange",ue],replaceUrl:[2,"replaceUrl","replaceUrl",ue],routerLink:"routerLink"},features:[$e]})}return t})(),zL=(()=>{class t{router;element;renderer;cdr;links;classes=[];routerEventsSubscription;linkInputChangesSubscription;_isActive=!1;get isActive(){return this._isActive}routerLinkActiveOptions={exact:!1};ariaCurrentWhenActive;isActiveChange=new H;link=f(Vd,{optional:!0});constructor(e,r,o,i){this.router=e,this.element=r,this.renderer=o,this.cdr=i,this.routerEventsSubscription=e.events.subscribe(s=>{s instanceof Et&&this.update()})}ngAfterContentInit(){S(this.links.changes,S(null)).pipe(cn()).subscribe(e=>{this.update(),this.subscribeToEachLinkOnChanges()})}subscribeToEachLinkOnChanges(){this.linkInputChangesSubscription?.unsubscribe();let e=[...this.links.toArray(),this.link].filter(r=>!!r).map(r=>r.onChanges);this.linkInputChangesSubscription=ae(e).pipe(cn()).subscribe(r=>{this._isActive!==this.isLinkActive(this.router)(r)&&this.update()})}set routerLinkActive(e){let r=Array.isArray(e)?e:e.split(" ");this.classes=r.filter(o=>!!o)}ngOnChanges(e){this.update()}ngOnDestroy(){this.routerEventsSubscription.unsubscribe(),this.linkInputChangesSubscription?.unsubscribe()}update(){!this.links||!this.router.navigated||queueMicrotask(()=>{let e=this.hasActiveLinks();this.classes.forEach(r=>{e?this.renderer.addClass(this.element.nativeElement,r):this.renderer.removeClass(this.element.nativeElement,r)}),e&&this.ariaCurrentWhenActive!==void 0?this.renderer.setAttribute(this.element.nativeElement,"aria-current",this.ariaCurrentWhenActive.toString()):this.renderer.removeAttribute(this.element.nativeElement,"aria-current"),this._isActive!==e&&(this._isActive=e,this.cdr.markForCheck(),this.isActiveChange.emit(e))})}isLinkActive(e){let r=GL(this.routerLinkActiveOptions)?this.routerLinkActiveOptions:this.routerLinkActiveOptions.exact??!1?g({},mv):g({},xa);return o=>{let i=o.urlTree;return i?Y(gv(i,e,r)):!1}}hasActiveLinks(){let e=this.isLinkActive(this.router);return this.link&&e(this.link)||this.links.some(e)}static \u0275fac=function(r){return new(r||t)(E(wt),E($),E(Le),E(Jt))};static \u0275dir=T({type:t,selectors:[["","routerLinkActive",""]],contentQueries:function(r,o,i){if(r&1&&Gl(i,Vd,5),r&2){let s;Wl(s=ql())&&(o.links=s)}},inputs:{routerLinkActiveOptions:"routerLinkActiveOptions",ariaCurrentWhenActive:"ariaCurrentWhenActive",routerLinkActive:"routerLinkActive"},outputs:{isActiveChange:"isActiveChange"},exportAs:["routerLinkActive"],features:[$e]})}return t})();function GL(t){let n=t;return!!(n.paths||n.matrixParams||n.queryParams||n.fragment)}var Ba=class{};var NS=(()=>{class t{router;injector;preloadingStrategy;loader;subscription;constructor(e,r,o,i){this.router=e,this.injector=r,this.preloadingStrategy=o,this.loader=i}setUpPreloading(){this.subscription=this.router.events.pipe(he(e=>e instanceof Et),or(()=>this.preload())).subscribe(()=>{})}preload(){return this.processRoutes(this.injector,this.router.config)}ngOnDestroy(){this.subscription?.unsubscribe()}processRoutes(e,r){let o=[];for(let i of r){i.providers&&!i._injector&&(i._injector=oi(i.providers,e,""));let s=i._injector??e;i._loadedNgModuleFactory&&!i._loadedInjector&&(i._loadedInjector=i._loadedNgModuleFactory.create(s).injector);let a=i._loadedInjector??s;(i.loadChildren&&!i._loadedRoutes&&i.canLoad===void 0||i.loadComponent&&!i._loadedComponent)&&o.push(this.preloadConfig(s,i)),(i.children||i._loadedRoutes)&&o.push(this.processRoutes(a,i.children??i._loadedRoutes))}return ae(o).pipe(cn())}preloadConfig(e,r){return this.preloadingStrategy.preload(r,()=>{if(e.destroyed)return S(null);let o;r.loadChildren&&r.canLoad===void 0?o=ae(this.loader.loadChildren(e,r)):o=S(null);let i=o.pipe(Ee(s=>s===null?S(void 0):(r._loadedRoutes=s.routes,r._loadedInjector=s.injector,r._loadedNgModuleFactory=s.factory,this.processRoutes(s.injector??e,s.routes))));if(r.loadComponent&&!r._loadedComponent){let s=this.loader.loadComponent(e,r);return ae([i,s]).pipe(cn())}else return i})}static \u0275fac=function(r){return new(r||t)(w(wt),w(re),w(Ba),w(kd))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),OS=new y(""),WL=(()=>{class t{options;routerEventsSubscription;scrollEventsSubscription;lastId=0;lastSource=Ai;restoredId=0;store={};isHydrating=f(gp,{optional:!0})??!1;urlSerializer=f(Tr);zone=f(j);viewportScroller=f(Vm);transitions=f(Pd);constructor(e){this.options=e,this.options.scrollPositionRestoration||="disabled",this.options.anchorScrolling||="disabled",this.isHydrating&&f(Ge).whenStable().then(()=>{this.isHydrating=!1})}init(){this.options.scrollPositionRestoration!=="disabled"&&this.viewportScroller.setHistoryScrollRestoration("manual"),this.routerEventsSubscription=this.createScrollEvents(),this.scrollEventsSubscription=this.consumeScrollEvents()}createScrollEvents(){return this.transitions.events.subscribe(e=>{e instanceof Mr?(this.store[this.lastId]=this.viewportScroller.getScrollPosition(),this.lastSource=e.navigationTrigger,this.restoredId=e.restoredState?e.restoredState.navigationId:0):e instanceof Et?(this.lastId=e.id,this.scheduleScrollEvent(e,this.urlSerializer.parse(e.urlAfterRedirects).fragment)):e instanceof Mn&&e.code===Ni.IgnoredSameUrlNavigation&&(this.lastSource=void 0,this.restoredId=0,this.scheduleScrollEvent(e,this.urlSerializer.parse(e.url).fragment))})}consumeScrollEvents(){return this.transitions.events.subscribe(e=>{if(!(e instanceof Oi)||e.scrollBehavior==="manual")return;let r={behavior:"instant"};e.position?this.options.scrollPositionRestoration==="top"?this.viewportScroller.scrollToPosition([0,0],r):this.options.scrollPositionRestoration==="enabled"&&this.viewportScroller.scrollToPosition(e.position,r):e.anchor&&this.options.anchorScrolling==="enabled"?this.viewportScroller.scrollToAnchor(e.anchor):this.options.scrollPositionRestoration!=="disabled"&&this.viewportScroller.scrollToPosition([0,0])})}scheduleScrollEvent(e,r){if(this.isHydrating)return;let o=Y(this.transitions.currentNavigation)?.extras.scroll;this.zone.runOutsideAngular(async()=>{await new Promise(i=>{setTimeout(i),typeof requestAnimationFrame<"u"&&requestAnimationFrame(i)}),this.zone.run(()=>{this.transitions.events.next(new Oi(e,this.lastSource==="popstate"?this.store[this.restoredId]:null,r,o))})})}ngOnDestroy(){this.routerEventsSubscription?.unsubscribe(),this.scrollEventsSubscription?.unsubscribe()}static \u0275fac=function(r){Bp()};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})();function qL(){return f(wt).routerState.root}function Ua(t,n){return{\u0275kind:t,\u0275providers:n}}function YL(){let t=f(W);return n=>{let e=t.get(Ge);if(n!==e.components[0])return;let r=t.get(wt),o=t.get(kS);t.get(Tv)===1&&r.initialNavigation(),t.get(LS,null,{optional:!0})?.setUpPreloading(),t.get(OS,null,{optional:!0})?.init(),r.resetRootComponentType(e.componentTypes[0]),o.closed||(o.next(),o.complete(),o.unsubscribe())}}var kS=new y("",{factory:()=>new x}),Tv=new y("",{factory:()=>1});function FS(){let t=[{provide:wl,useValue:!0},{provide:Tv,useValue:0},Hl(()=>{let n=f(W);return n.get(Im,Promise.resolve()).then(()=>new Promise(r=>{let o=n.get(wt),i=n.get(kS);jd(o,()=>{r(!0)}),n.get(Pd).afterPreactivation=()=>(r(!0),i.closed?S(void 0):i),o.initialNavigation()}))})];return Ua(2,t)}function PS(){let t=[Hl(()=>{f(wt).setUpLocationChangeListener()}),{provide:Tv,useValue:2}];return Ua(3,t)}var LS=new y("");function jS(t){return Ua(0,[{provide:LS,useExisting:NS},{provide:Ba,useExisting:t}])}function VS(){return Ua(8,[_v,{provide:ja,useExisting:_v}])}function BS(t){Kt("NgRouterViewTransitions");let n=[{provide:Cv,useValue:xS},{provide:Iv,useValue:g({skipNextTransition:!!t?.skipInitialTransition},t)}];return Ua(9,n)}var US=[Dn,{provide:Tr,useClass:Xn},wt,wo,{provide:Jn,useFactory:qL},kd,[]],ZL=(()=>{class t{constructor(){}static forRoot(e,r){return{ngModule:t,providers:[US,[],{provide:Bi,multi:!0,useValue:e},[],r?.errorHandler?{provide:Sv,useValue:r.errorHandler}:[],{provide:xr,useValue:r||{}},r?.useHash?QL():XL(),KL(),r?.preloadingStrategy?jS(r.preloadingStrategy).\u0275providers:[],r?.initialNavigation?JL(r):[],r?.bindToComponentInputs?VS().\u0275providers:[],r?.enableViewTransitions?BS().\u0275providers:[],e1()]}}static forChild(e){return{ngModule:t,providers:[{provide:Bi,multi:!0,useValue:e}]}}static \u0275fac=function(r){return new(r||t)};static \u0275mod=Q({type:t});static \u0275inj=q({})}return t})();function KL(){return{provide:OS,useFactory:()=>{let t=f(Vm),n=f(xr);return n.scrollOffset&&t.setOffset(n.scrollOffset),new WL(n)}}}function QL(){return{provide:tn,useClass:km}}function XL(){return{provide:tn,useClass:nu}}function JL(t){return[t.initialNavigation==="disabled"?PS().\u0275providers:[],t.initialNavigation==="enabledBlocking"?FS().\u0275providers:[]]}var Mv=new y("");function e1(){return[{provide:Mv,useFactory:YL},{provide:$l,multi:!0,useExisting:Mv}]}var Bd=class t extends Error{originalError;constructor(n){super(n)}static fromError(n,e){let r=new t(n);return r.originalError=e,r}},n1=(()=>{class t{handleError(e){let r=e;return e.name==="HttpErrorResponse"&&e.status===0?r=Bd.fromError("Controller is unreachable",e):e.error?.message&&(r=Bd.fromError(e.error.message,e)),xn(()=>r)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),eee=(()=>{class t{http;errorHandler;router;requestsNotificationEmitter=new H;isRefreshing=!1;failedQueue=[];constructor(e,r,o){this.http=e,this.errorHandler=r,this.router=o}get(e,r,o){o=this.getJsonOptions(o);let i=this.getOptionsForController(e,r,o);return this.requestsNotificationEmitter.emit(`GET ${i.url}`),this.handleResponse(e,this.http.get(i.url,i.options),"GET",r,null,o)}getText(e,r,o){o=this.getTextOptions(o);let i=this.getOptionsForController(e,r,o);return this.requestsNotificationEmitter.emit(`GET ${i.url}`),this.handleResponse(e,this.http.get(i.url,i.options),"GET",r,null,o)}getBlob(e,r,o){o=this.getBlobOptions(o);let i=this.getOptionsForController(e,r,o);return this.requestsNotificationEmitter.emit(`GET ${i.url}`),this.handleResponse(e,this.http.get(i.url,i.options),"GET",r,null,o)}post(e,r,o,i){i=this.getJsonOptions(i);let s=this.getOptionsForController(e,r,i);return this.requestsNotificationEmitter.emit(`POST ${s.url}`),this.handleResponse(e,this.http.post(s.url,o,s.options),"POST",r,o,i)}postBlob(e,r,o){let i={responseType:"blob",headers:{}},s=this.getOptionsForController(e,r,i);return this.requestsNotificationEmitter.emit(`POST ${s.url}`),this.handleResponse(e,this.http.post(s.url,o,s.options),"POST",r,o,i)}put(e,r,o,i){i=this.getJsonOptions(i);let s=this.getOptionsForController(e,r,i);return this.requestsNotificationEmitter.emit(`PUT ${s.url}`),this.handleResponse(e,this.http.put(s.url,o,s.options),"PUT",r,o,i)}delete(e,r,o){o=this.getJsonOptions(o);let i=this.getOptionsForController(e,r,o);return this.requestsNotificationEmitter.emit(`DELETE ${i.url}`),this.handleResponse(e,this.http.delete(i.url,i.options),"DELETE",r,null,o)}patch(e,r,o,i){i=this.getJsonOptions(i);let s=this.getOptionsForController(e,r,i);return this.handleResponse(e,this.http.patch(s.url,o,s.options),"PATCH",r,o,i)}head(e,r,o){o=this.getJsonOptions(o);let i=this.getOptionsForController(e,r,o);return this.handleResponse(e,this.http.head(i.url,i.options),"HEAD",r,null,o)}options(e,r,o){o=this.getJsonOptions(o);let i=this.getOptionsForController(e,r,o);return this.handleResponse(e,this.http.options(i.url,i.options),"OPTIONS",r,null,o)}getJsonOptions(e){return e||{responseType:"json"}}getTextOptions(e){return e||{responseType:"text"}}getBlobOptions(e){return e||{responseType:"blob"}}getOptionsForController(e,r,o){return e&&e.host&&e.port?(e.protocol||(e.protocol=location.protocol),r=`${e.protocol}//${e.host}:${e.port}/${ed.current_version}${r}`):r=`/${ed.current_version}${r}`,o.headers||(o.headers={}),e&&e.authToken&&!e.tokenExpired&&(o.headers.Authorization=`Bearer ${e.authToken}`),{url:r,options:o}}handleResponse(e,r,o,i,s,a){return r.pipe(It(c=>{if(c.status!==401)return this.errorHandler.handleError(c);if(i.endsWith("/access/users/login")||i.endsWith("/access/users/authenticate")||i.endsWith("/access/users/refresh"))return this.errorHandler.handleError(c);let l=localStorage.getItem(`refresh_token_${e.id}`);return l?this.retryAfterRefresh(e,o,i,s,a,l):(this.redirectToLogin(e),xn(()=>c))}))}retryAfterRefresh(e,r,o,i,s,a){return this.isRefreshing?new P(c=>{this.failedQueue.push({resolve:()=>{this.executeRequest(e,r,o,i,s).subscribe({next:l=>{c.next(l),c.complete()},error:l=>c.error(l)})},reject:l=>c.error(l)})}):(this.isRefreshing=!0,this.doRefreshToken(e,a).pipe(Ve(c=>(e.authToken=c.access_token,localStorage.setItem(`controller-${e.id}`,JSON.stringify(e)),c.refresh_token&&localStorage.setItem(`refresh_token_${e.id}`,c.refresh_token),this.isRefreshing=!1,this.processQueue(null),this.executeRequest(e,r,o,i,s))),It(c=>(this.isRefreshing=!1,this.processQueue(c),c.status===401&&this.redirectToLogin(e),xn(()=>c)))))}doRefreshToken(e,r){let o=`${e.protocol}//${e.host}:${e.port}/${ed.current_version}/access/users/refresh`;return this.http.post(o,{refresh_token:r},{headers:new wn({"Content-Type":"application/json"})})}executeRequest(e,r,o,i,s){let a=this.getOptionsForController(e,o,s);return this.requestsNotificationEmitter.emit(`${r} ${a.url}`),this.http.request(r,a.url,{body:i,headers:a.options.headers,params:a.options.params,responseType:a.options.responseType||"json"})}processQueue(e){e?this.failedQueue.forEach(r=>r.reject(e)):this.failedQueue.forEach(r=>r.resolve()),this.failedQueue=[]}clearTokens(e){localStorage.removeItem(`refresh_token_${e.id}`),e.authToken=null,localStorage.setItem(`controller-${e.id}`,JSON.stringify(e))}redirectToLogin(e){this.clearTokens(e),e.tokenExpired=!0,localStorage.setItem(`controller-${e.id}`,JSON.stringify(e)),this.isRefreshing=!1,this.processQueue(new Error("Session expired, redirecting to login")),this.router.navigate(["/controller",e.id,"login"])}static \u0275fac=function(r){return new(r||t)(w(pi),w(n1),w(wt))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})();var xv=class{_multiple;_emitChanges;compareWith;_selection=new Set;_deselectedToEmit=[];_selectedToEmit=[];_selected=null;get selected(){return this._selected||(this._selected=Array.from(this._selection.values())),this._selected}changed=new x;constructor(n=!1,e,r=!0,o){this._multiple=n,this._emitChanges=r,this.compareWith=o,e&&e.length&&(n?e.forEach(i=>this._markSelected(i)):this._markSelected(e[0]),this._selectedToEmit.length=0)}select(...n){this._verifyValueAssignment(n),n.forEach(r=>this._markSelected(r));let e=this._hasQueuedChanges();return this._emitChangeEvent(),e}deselect(...n){this._verifyValueAssignment(n),n.forEach(r=>this._unmarkSelected(r));let e=this._hasQueuedChanges();return this._emitChangeEvent(),e}setSelection(...n){this._verifyValueAssignment(n);let e=this.selected,r=new Set(n.map(i=>this._getConcreteValue(i)));n.forEach(i=>this._markSelected(i)),e.filter(i=>!r.has(this._getConcreteValue(i,r))).forEach(i=>this._unmarkSelected(i));let o=this._hasQueuedChanges();return this._emitChangeEvent(),o}toggle(n){return this.isSelected(n)?this.deselect(n):this.select(n)}clear(n=!0){this._unmarkAll();let e=this._hasQueuedChanges();return n&&this._emitChangeEvent(),e}isSelected(n){return this._selection.has(this._getConcreteValue(n))}isEmpty(){return this._selection.size===0}hasValue(){return!this.isEmpty()}sort(n){this._multiple&&this.selected&&this._selected.sort(n)}isMultipleSelection(){return this._multiple}_emitChangeEvent(){this._selected=null,(this._selectedToEmit.length||this._deselectedToEmit.length)&&(this.changed.next({source:this,added:this._selectedToEmit,removed:this._deselectedToEmit}),this._deselectedToEmit=[],this._selectedToEmit=[])}_markSelected(n){n=this._getConcreteValue(n),this.isSelected(n)||(this._multiple||this._unmarkAll(),this.isSelected(n)||this._selection.add(n),this._emitChanges&&this._selectedToEmit.push(n))}_unmarkSelected(n){n=this._getConcreteValue(n),this.isSelected(n)&&(this._selection.delete(n),this._emitChanges&&this._deselectedToEmit.push(n))}_unmarkAll(){this.isEmpty()||this._selection.forEach(n=>this._unmarkSelected(n))}_verifyValueAssignment(n){n.length>1&&this._multiple}_hasQueuedChanges(){return!!(this._deselectedToEmit.length||this._selectedToEmit.length)}_getConcreteValue(n,e){if(this.compareWith){e=e??this._selection;for(let r of e)if(this.compareWith(n,r))return r;return n}else return n}};var r1=(()=>{class t{_listeners=[];notify(e,r){for(let o of this._listeners)o(e,r)}listen(e){return this._listeners.push(e),()=>{this._listeners=this._listeners.filter(r=>e!==r)}}ngOnDestroy(){this._listeners=[]}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var HS=class{applyChanges(n,e,r,o,i){n.forEachOperation((s,a,c)=>{let l,u;if(s.previousIndex==null){let d=r(s,a,c);l=e.createEmbeddedView(d.templateRef,d.context,d.index),u=In.INSERTED}else c==null?(e.remove(a),u=In.REMOVED):(l=e.get(a),e.move(l,c),u=In.MOVED);i&&i({context:l?.context,operation:u,record:s})})}detach(){}};var pee=(()=>{class t{_animationsDisabled=Cr();state="unchecked";disabled=!1;appearance="full";constructor(){}static \u0275fac=function(r){return new(r||t)};static \u0275cmp=Te({type:t,selectors:[["mat-pseudo-checkbox"]],hostAttrs:[1,"mat-pseudo-checkbox"],hostVars:12,hostBindings:function(r,o){r&2&&Xe("mat-pseudo-checkbox-indeterminate",o.state==="indeterminate")("mat-pseudo-checkbox-checked",o.state==="checked")("mat-pseudo-checkbox-disabled",o.disabled)("mat-pseudo-checkbox-minimal",o.appearance==="minimal")("mat-pseudo-checkbox-full",o.appearance==="full")("_mat-animation-noopable",o._animationsDisabled)},inputs:{state:"state",disabled:"disabled",appearance:"appearance"},decls:0,vars:0,template:function(r,o){},styles:[`.mat-pseudo-checkbox { + border-radius: 2px; + cursor: pointer; + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + position: relative; + flex-shrink: 0; + transition: border-color 90ms cubic-bezier(0, 0, 0.2, 0.1), background-color 90ms cubic-bezier(0, 0, 0.2, 0.1); +} +.mat-pseudo-checkbox::after { + position: absolute; + opacity: 0; + content: ""; + border-bottom: 2px solid currentColor; + transition: opacity 90ms cubic-bezier(0, 0, 0.2, 0.1); +} +.mat-pseudo-checkbox._mat-animation-noopable { + transition: none !important; + animation: none !important; +} +.mat-pseudo-checkbox._mat-animation-noopable::after { + transition: none; +} + +.mat-pseudo-checkbox-disabled { + cursor: default; +} + +.mat-pseudo-checkbox-indeterminate::after { + left: 1px; + opacity: 1; + border-radius: 2px; +} + +.mat-pseudo-checkbox-checked::after { + left: 1px; + border-left: 2px solid currentColor; + transform: rotate(-45deg); + opacity: 1; + box-sizing: content-box; +} + +.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-checked::after, .mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-indeterminate::after { + color: var(--mat-pseudo-checkbox-minimal-selected-checkmark-color, var(--mat-sys-primary)); +} +.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled::after, .mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled::after { + color: var(--mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} + +.mat-pseudo-checkbox-full { + border-color: var(--mat-pseudo-checkbox-full-unselected-icon-color, var(--mat-sys-on-surface-variant)); + border-width: 2px; + border-style: solid; +} +.mat-pseudo-checkbox-full.mat-pseudo-checkbox-disabled { + border-color: var(--mat-pseudo-checkbox-full-disabled-unselected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked, .mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate { + background-color: var(--mat-pseudo-checkbox-full-selected-icon-color, var(--mat-sys-primary)); + border-color: transparent; +} +.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked::after, .mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate::after { + color: var(--mat-pseudo-checkbox-full-selected-checkmark-color, var(--mat-sys-on-primary)); +} +.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled, .mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled { + background-color: var(--mat-pseudo-checkbox-full-disabled-selected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled::after, .mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled::after { + color: var(--mat-pseudo-checkbox-full-disabled-selected-checkmark-color, var(--mat-sys-surface)); +} + +.mat-pseudo-checkbox { + width: 18px; + height: 18px; +} + +.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-checked::after { + width: 14px; + height: 6px; + transform-origin: center; + top: -4.2426406871px; + left: 0; + bottom: 0; + right: 0; + margin: auto; +} +.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-indeterminate::after { + top: 8px; + width: 16px; +} + +.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked::after { + width: 10px; + height: 4px; + transform-origin: center; + top: -2.8284271247px; + left: 0; + bottom: 0; + right: 0; + margin: auto; +} +.mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate::after { + top: 6px; + width: 12px; +} +`],encapsulation:2,changeDetection:0})}return t})();export{g as a,F as b,ZS as c,o1 as d,i1 as e,s1 as f,G as g,P as h,x as i,Ae as j,$i as k,qv as l,Ct as m,Yv as n,Re as o,Wi as p,ae as q,S as r,xn as s,Vt as t,fM as u,pM as v,V as w,Ro as x,Ee as y,ln as z,qi as A,Yi as B,tf as C,Pr as D,AM as E,RM as F,he as G,mc as H,It as I,NM as J,or as K,Lr as L,Ne as M,nf as N,OM as O,gc as P,Rn as Q,dy as R,Zi as S,fy as T,Ki as U,No as V,Ve as W,jt as X,Be as Y,b as Z,Ie as _,v as $,q as aa,y as ba,w as ca,f as da,Ly as ea,re as fa,J as ga,Qy as ha,Xy as ia,ub as ja,db as ka,W as la,O as ma,Ue as na,H as oa,j as pa,Ze as qa,K as ra,Es as sa,$e as ta,Pe as ua,$ as va,Un as wa,so as xa,Fs as ya,P0 as za,$_ as Aa,z_ as Ba,V0 as Ca,B0 as Da,Q_ as Ea,At as Fa,gt as Ga,He as Ha,Le as Ia,E as Ja,Bp as Ka,Qe as La,Te as Ma,Q as Na,T as Oa,ii as Pa,UA as Qa,ie as Ra,ZD as Sa,KD as Ta,si as Ua,Ge as Va,tE as Wa,yt as Xa,eR as Ya,nR as Za,em as _a,rR as $a,oR as ab,iR as bb,sR as cb,aR as db,nE as eb,bl as fb,tm as gb,zl as hb,ai as ib,ci as jb,Gn as kb,nm as lb,rm as mb,oE as nb,hR as ob,iE as pb,Qt as qb,aE as rb,yR as sb,Dr as tb,_n as ub,Gl as vb,om as wb,Wl as xb,ql as yb,lE as zb,uE as Ab,DR as Bb,ER as Cb,im as Db,Xe as Eb,zs as Fb,$R as Gb,bE as Hb,sm as Ib,_E as Jb,DE as Kb,EE as Lb,wE as Mb,CE as Nb,ZR as Ob,IE as Pb,KR as Qb,QR as Rb,ze as Sb,tN as Tb,nN as Ub,rN as Vb,oN as Wb,iN as Xb,aN as Yb,lN as Zb,uN as _b,dN as $b,fN as ac,hN as bc,Y as cc,Xt as dc,ui as ec,M5 as fc,T5 as gc,zE as hc,x5 as ic,A5 as jc,R5 as kc,N5 as lc,Jt as mc,Xl as nc,ue as oc,wm as pc,k5 as qc,Dn as rc,hO as sc,yw as tc,pO as uc,mO as vc,gO as wc,bO as xc,DO as yc,CO as zc,IO as Ac,Pm as Bc,Dw as Cc,Wm as Dc,HO as Ec,ZO as Fc,wn as Gc,Yn as Hc,di as Ic,lo as Jc,fi as Kc,Hw as Lc,pi as Mc,gk as Nc,qw as Oc,cY as Pc,ia as Qc,Nt as Rc,gg as Sc,EF as Tc,ot as Uc,ha as Vc,pa as Wc,$u as Xc,HC as Yc,Ot as Zc,_e as _c,_i as $c,_a as ad,iP as bd,Fg as cd,In as dd,Pg as ed,rn as fd,Da as gd,aP as hd,Si as id,XQ as jd,Lg as kd,jg as ld,va as md,mo as nd,Vg as od,Mi as pd,td as qd,nd as rd,iX as sd,sX as td,EI as ud,Yu as vd,AI as wd,zg as xd,Ca as yd,Gg as zd,ad as Ad,qg as Bd,LI as Cd,Yg as Dd,Zg as Ed,$g as Fd,hP as Gd,pP as Hd,HS as Id,fo as Jd,iC as Kd,Cn as Ld,Zw as Md,rt as Nd,Zn as Od,IY as Pd,SY as Qd,uo as Rd,MY as Sd,Bk as Td,vi as Ud,xY as Vd,zk as Wd,AY as Xd,Wk as Yd,Yk as Zd,CC as _d,IC as $d,Xk as ae,eF as be,RY as ce,NY as de,sF as ee,cF as fe,uF as ge,dF as he,OY as ie,kY as je,FY as ke,Gu as le,wF as me,Uu as ne,Wu as oe,bg as pe,l7 as qe,XC as re,nI as se,LF as te,VF as ue,BF as ve,Dg as we,Eg as xe,wg as ye,nK as ze,HF as Ae,$F as Be,gK as Ce,jK as De,SK as Ee,RK as Fe,GF as Ge,Cr as He,Mr as Ie,Et as Je,Ft as Ke,Do as Le,Jn as Me,bv as Ne,wt as Oe,Vd as Pe,zL as Qe,ZL as Re,LC as Se,gZ as Te,vZ as Ue,ba as Ve,Rg as We,JK as Xe,vI as Ye,yI as Ze,eP as _e,_I as $e,SQ as af,MQ as bf,ed as cf,n1 as df,eee as ef,xv as ff,r1 as gf,pee as hf}; diff --git a/gns3server/static/web-ui/chunk-JVPSP3B2.js b/gns3server/static/web-ui/chunk-JVPSP3B2.js deleted file mode 100644 index ea79498e6..000000000 --- a/gns3server/static/web-ui/chunk-JVPSP3B2.js +++ /dev/null @@ -1 +0,0 @@ -import{aa as a}from"./chunk-EXPJ4N52.js";import"./chunk-TYGV4UPE.js";export{a as TopologySummaryComponent}; diff --git a/gns3server/static/web-ui/chunk-TYGV4UPE.js b/gns3server/static/web-ui/chunk-TYGV4UPE.js deleted file mode 100644 index feb876c34..000000000 --- a/gns3server/static/web-ui/chunk-TYGV4UPE.js +++ /dev/null @@ -1,1049 +0,0 @@ -var bS=Object.create;var ka=Object.defineProperty,_S=Object.defineProperties,DS=Object.getOwnPropertyDescriptor,ES=Object.getOwnPropertyDescriptors,wS=Object.getOwnPropertyNames,Oa=Object.getOwnPropertySymbols,CS=Object.getPrototypeOf,Sd=Object.prototype.hasOwnProperty,Cv=Object.prototype.propertyIsEnumerable;var wv=(t,n,e)=>n in t?ka(t,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[n]=e,g=(t,n)=>{for(var e in n||={})Sd.call(n,e)&&wv(t,e,n[e]);if(Oa)for(var e of Oa(n))Cv.call(n,e)&&wv(t,e,n[e]);return t},F=(t,n)=>_S(t,ES(n));var IS=(t,n)=>{var e={};for(var r in t)Sd.call(t,r)&&n.indexOf(r)<0&&(e[r]=t[r]);if(t!=null&&Oa)for(var r of Oa(t))n.indexOf(r)<0&&Cv.call(t,r)&&(e[r]=t[r]);return e};var CL=(t,n)=>()=>{try{return n||t((n={exports:{}}).exports,n),n.exports}catch(e){throw n=0,e}},IL=(t,n)=>{for(var e in n)ka(t,e,{get:n[e],enumerable:!0})},SS=(t,n,e,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of wS(n))!Sd.call(t,o)&&o!==e&&ka(t,o,{get:()=>n[o],enumerable:!(r=DS(n,o))||r.enumerable});return t};var SL=(t,n,e)=>(e=t!=null?bS(CS(t)):{},SS(n||!t||!t.__esModule?ka(e,"default",{value:t,enumerable:!0}):e,t));function R(t){return typeof t=="function"}function Zn(t){let e=t(r=>{Error.call(r),r.stack=new Error().stack});return e.prototype=Object.create(Error.prototype),e.prototype.constructor=e,e}var Fa=Zn(t=>function(e){t(this),this.message=e?`${e.length} errors occurred during unsubscription: -${e.map((r,o)=>`${o+1}) ${r.toString()}`).join(` - `)}`:"",this.name="UnsubscriptionError",this.errors=e});function wr(t,n){if(t){let e=t.indexOf(n);0<=e&&t.splice(e,1)}}var G=class t{constructor(n){this.initialTeardown=n,this.closed=!1,this._parentage=null,this._finalizers=null}unsubscribe(){let n;if(!this.closed){this.closed=!0;let{_parentage:e}=this;if(e)if(this._parentage=null,Array.isArray(e))for(let i of e)i.remove(this);else e.remove(this);let{initialTeardown:r}=this;if(R(r))try{r()}catch(i){n=i instanceof Fa?i.errors:[i]}let{_finalizers:o}=this;if(o){this._finalizers=null;for(let i of o)try{Iv(i)}catch(s){n=n??[],s instanceof Fa?n=[...n,...s.errors]:n.push(s)}}if(n)throw new Fa(n)}}add(n){var e;if(n&&n!==this)if(this.closed)Iv(n);else{if(n instanceof t){if(n.closed||n._hasParent(this))return;n._addParent(this)}(this._finalizers=(e=this._finalizers)!==null&&e!==void 0?e:[]).push(n)}}_hasParent(n){let{_parentage:e}=this;return e===n||Array.isArray(e)&&e.includes(n)}_addParent(n){let{_parentage:e}=this;this._parentage=Array.isArray(e)?(e.push(n),e):e?[e,n]:n}_removeParent(n){let{_parentage:e}=this;e===n?this._parentage=null:Array.isArray(e)&&wr(e,n)}remove(n){let{_finalizers:e}=this;e&&wr(e,n),n instanceof t&&n._removeParent(this)}};G.EMPTY=(()=>{let t=new G;return t.closed=!0,t})();var Md=G.EMPTY;function Pa(t){return t instanceof G||t&&"closed"in t&&R(t.remove)&&R(t.add)&&R(t.unsubscribe)}function Iv(t){R(t)?t():t.unsubscribe()}var Nt={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Do={setTimeout(t,n,...e){let{delegate:r}=Do;return r?.setTimeout?r.setTimeout(t,n,...e):setTimeout(t,n,...e)},clearTimeout(t){let{delegate:n}=Do;return(n?.clearTimeout||clearTimeout)(t)},delegate:void 0};function La(t){Do.setTimeout(()=>{let{onUnhandledError:n}=Nt;if(n)n(t);else throw t})}function Cr(){}var Sv=Td("C",void 0,void 0);function Mv(t){return Td("E",void 0,t)}function Tv(t){return Td("N",t,void 0)}function Td(t,n,e){return{kind:t,value:n,error:e}}var Ir=null;function Eo(t){if(Nt.useDeprecatedSynchronousErrorHandling){let n=!Ir;if(n&&(Ir={errorThrown:!1,error:null}),t(),n){let{errorThrown:e,error:r}=Ir;if(Ir=null,e)throw r}}else t()}function xv(t){Nt.useDeprecatedSynchronousErrorHandling&&Ir&&(Ir.errorThrown=!0,Ir.error=t)}var Sr=class extends G{constructor(n){super(),this.isStopped=!1,n?(this.destination=n,Pa(n)&&n.add(this)):this.destination=xS}static create(n,e,r){return new Ot(n,e,r)}next(n){this.isStopped?Ad(Tv(n),this):this._next(n)}error(n){this.isStopped?Ad(Mv(n),this):(this.isStopped=!0,this._error(n))}complete(){this.isStopped?Ad(Sv,this):(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe(),this.destination=null)}_next(n){this.destination.next(n)}_error(n){try{this.destination.error(n)}finally{this.unsubscribe()}}_complete(){try{this.destination.complete()}finally{this.unsubscribe()}}},MS=Function.prototype.bind;function xd(t,n){return MS.call(t,n)}var Rd=class{constructor(n){this.partialObserver=n}next(n){let{partialObserver:e}=this;if(e.next)try{e.next(n)}catch(r){ja(r)}}error(n){let{partialObserver:e}=this;if(e.error)try{e.error(n)}catch(r){ja(r)}else ja(n)}complete(){let{partialObserver:n}=this;if(n.complete)try{n.complete()}catch(e){ja(e)}}},Ot=class extends Sr{constructor(n,e,r){super();let o;if(R(n)||!n)o={next:n??void 0,error:e??void 0,complete:r??void 0};else{let i;this&&Nt.useDeprecatedNextContext?(i=Object.create(n),i.unsubscribe=()=>this.unsubscribe(),o={next:n.next&&xd(n.next,i),error:n.error&&xd(n.error,i),complete:n.complete&&xd(n.complete,i)}):o=n}this.destination=new Rd(o)}};function ja(t){Nt.useDeprecatedSynchronousErrorHandling?xv(t):La(t)}function TS(t){throw t}function Ad(t,n){let{onStoppedNotification:e}=Nt;e&&Do.setTimeout(()=>e(t,n))}var xS={closed:!0,next:Cr,error:TS,complete:Cr};var wo=typeof Symbol=="function"&&Symbol.observable||"@@observable";function st(t){return t}function Nd(...t){return Od(t)}function Od(t){return t.length===0?st:t.length===1?t[0]:function(e){return t.reduce((r,o)=>o(r),e)}}var O=(()=>{class t{constructor(e){e&&(this._subscribe=e)}lift(e){let r=new t;return r.source=this,r.operator=e,r}subscribe(e,r,o){let i=RS(e)?e:new Ot(e,r,o);return Eo(()=>{let{operator:s,source:a}=this;i.add(s?s.call(i,a):a?this._subscribe(i):this._trySubscribe(i))}),i}_trySubscribe(e){try{return this._subscribe(e)}catch(r){e.error(r)}}forEach(e,r){return r=Av(r),new r((o,i)=>{let s=new Ot({next:a=>{try{e(a)}catch(c){i(c),s.unsubscribe()}},error:i,complete:o});this.subscribe(s)})}_subscribe(e){var r;return(r=this.source)===null||r===void 0?void 0:r.subscribe(e)}[wo](){return this}pipe(...e){return Od(e)(this)}toPromise(e){return e=Av(e),new e((r,o)=>{let i;this.subscribe(s=>i=s,s=>o(s),()=>r(i))})}}return t.create=n=>new t(n),t})();function Av(t){var n;return(n=t??Nt.Promise)!==null&&n!==void 0?n:Promise}function AS(t){return t&&R(t.next)&&R(t.error)&&R(t.complete)}function RS(t){return t&&t instanceof Sr||AS(t)&&Pa(t)}var Rv=Zn(t=>function(){t(this),this.name="ObjectUnsubscribedError",this.message="object unsubscribed"});var S=(()=>{class t extends O{constructor(){super(),this.closed=!1,this.currentObservers=null,this.observers=[],this.isStopped=!1,this.hasError=!1,this.thrownError=null}lift(e){let r=new Va(this,this);return r.operator=e,r}_throwIfClosed(){if(this.closed)throw new Rv}next(e){Eo(()=>{if(this._throwIfClosed(),!this.isStopped){this.currentObservers||(this.currentObservers=Array.from(this.observers));for(let r of this.currentObservers)r.next(e)}})}error(e){Eo(()=>{if(this._throwIfClosed(),!this.isStopped){this.hasError=this.isStopped=!0,this.thrownError=e;let{observers:r}=this;for(;r.length;)r.shift().error(e)}})}complete(){Eo(()=>{if(this._throwIfClosed(),!this.isStopped){this.isStopped=!0;let{observers:e}=this;for(;e.length;)e.shift().complete()}})}unsubscribe(){this.isStopped=this.closed=!0,this.observers=this.currentObservers=null}get observed(){var e;return((e=this.observers)===null||e===void 0?void 0:e.length)>0}_trySubscribe(e){return this._throwIfClosed(),super._trySubscribe(e)}_subscribe(e){return this._throwIfClosed(),this._checkFinalizedStatuses(e),this._innerSubscribe(e)}_innerSubscribe(e){let{hasError:r,isStopped:o,observers:i}=this;return r||o?Md:(this.currentObservers=null,i.push(e),new G(()=>{this.currentObservers=null,wr(i,e)}))}_checkFinalizedStatuses(e){let{hasError:r,thrownError:o,isStopped:i}=this;r?e.error(o):i&&e.complete()}asObservable(){let e=new O;return e.source=this,e}}return t.create=(n,e)=>new Va(n,e),t})(),Va=class extends S{constructor(n,e){super(),this.destination=n,this.source=e}next(n){var e,r;(r=(e=this.destination)===null||e===void 0?void 0:e.next)===null||r===void 0||r.call(e,n)}error(n){var e,r;(r=(e=this.destination)===null||e===void 0?void 0:e.error)===null||r===void 0||r.call(e,n)}complete(){var n,e;(e=(n=this.destination)===null||n===void 0?void 0:n.complete)===null||e===void 0||e.call(n)}_subscribe(n){var e,r;return(r=(e=this.source)===null||e===void 0?void 0:e.subscribe(n))!==null&&r!==void 0?r:Md}};function kd(t){return R(t?.lift)}function k(t){return n=>{if(kd(n))return n.lift(function(e){try{return t(e,this)}catch(r){this.error(r)}});throw new TypeError("Unable to lift unknown Observable type")}}function N(t,n,e,r,o){return new Fd(t,n,e,r,o)}var Fd=class extends Sr{constructor(n,e,r,o,i,s){super(n),this.onFinalize=i,this.shouldUnsubscribe=s,this._next=e?function(a){try{e(a)}catch(c){n.error(c)}}:super._next,this._error=o?function(a){try{o(a)}catch(c){n.error(c)}finally{this.unsubscribe()}}:super._error,this._complete=r?function(){try{r()}catch(a){n.error(a)}finally{this.unsubscribe()}}:super._complete}unsubscribe(){var n;if(!this.shouldUnsubscribe||this.shouldUnsubscribe()){let{closed:e}=this;super.unsubscribe(),!e&&((n=this.onFinalize)===null||n===void 0||n.call(this))}}};function Ov(t,n,e,r){function o(i){return i instanceof e?i:new e(function(s){s(i)})}return new(e||(e=Promise))(function(i,s){function a(u){try{l(r.next(u))}catch(d){s(d)}}function c(u){try{l(r.throw(u))}catch(d){s(d)}}function l(u){u.done?i(u.value):o(u.value).then(a,c)}l((r=r.apply(t,n||[])).next())})}function Nv(t){var n=typeof Symbol=="function"&&Symbol.iterator,e=n&&t[n],r=0;if(e)return e.call(t);if(t&&typeof t.length=="number")return{next:function(){return t&&r>=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}};throw new TypeError(n?"Object is not iterable.":"Symbol.iterator is not defined.")}function Mr(t){return this instanceof Mr?(this.v=t,this):new Mr(t)}function kv(t,n,e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var r=e.apply(t,n||[]),o,i=[];return o=Object.create((typeof AsyncIterator=="function"?AsyncIterator:Object).prototype),a("next"),a("throw"),a("return",s),o[Symbol.asyncIterator]=function(){return this},o;function s(p){return function(m){return Promise.resolve(m).then(p,d)}}function a(p,m){r[p]&&(o[p]=function(_){return new Promise(function(E,I){i.push([p,_,E,I])>1||c(p,_)})},m&&(o[p]=m(o[p])))}function c(p,m){try{l(r[p](m))}catch(_){h(i[0][3],_)}}function l(p){p.value instanceof Mr?Promise.resolve(p.value.v).then(u,d):h(i[0][2],p)}function u(p){c("next",p)}function d(p){c("throw",p)}function h(p,m){p(m),i.shift(),i.length&&c(i[0][0],i[0][1])}}function Fv(t){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var n=t[Symbol.asyncIterator],e;return n?n.call(t):(t=typeof Nv=="function"?Nv(t):t[Symbol.iterator](),e={},r("next"),r("throw"),r("return"),e[Symbol.asyncIterator]=function(){return this},e);function r(i){e[i]=t[i]&&function(s){return new Promise(function(a,c){s=t[i](s),o(a,c,s.done,s.value)})}}function o(i,s,a,c){Promise.resolve(c).then(function(l){i({value:l,done:a})},s)}}var Co=t=>t&&typeof t.length=="number"&&typeof t!="function";function Ba(t){return R(t?.then)}function Ua(t){return R(t[wo])}function Ha(t){return Symbol.asyncIterator&&R(t?.[Symbol.asyncIterator])}function $a(t){return new TypeError(`You provided ${t!==null&&typeof t=="object"?"an invalid object":`'${t}'`} where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`)}function NS(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var za=NS();function Ga(t){return R(t?.[za])}function Wa(t){return kv(this,arguments,function*(){let e=t.getReader();try{for(;;){let{value:r,done:o}=yield Mr(e.read());if(o)return yield Mr(void 0);yield yield Mr(r)}}finally{e.releaseLock()}})}function qa(t){return R(t?.getReader)}function Y(t){if(t instanceof O)return t;if(t!=null){if(Ua(t))return OS(t);if(Co(t))return kS(t);if(Ba(t))return FS(t);if(Ha(t))return Pv(t);if(Ga(t))return PS(t);if(qa(t))return LS(t)}throw $a(t)}function OS(t){return new O(n=>{let e=t[wo]();if(R(e.subscribe))return e.subscribe(n);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function kS(t){return new O(n=>{for(let e=0;e{t.then(e=>{n.closed||(n.next(e),n.complete())},e=>n.error(e)).then(null,La)})}function PS(t){return new O(n=>{for(let e of t)if(n.next(e),n.closed)return;n.complete()})}function Pv(t){return new O(n=>{jS(t,n).catch(e=>n.error(e))})}function LS(t){return Pv(Wa(t))}function jS(t,n){var e,r,o,i;return Ov(this,void 0,void 0,function*(){try{for(e=Fv(t);r=yield e.next(),!r.done;){let s=r.value;if(n.next(s),n.closed)return}}catch(s){o={error:s}}finally{try{r&&!r.done&&(i=e.return)&&(yield i.call(e))}finally{if(o)throw o.error}}n.complete()})}function at(t){return k((n,e)=>{Y(t).subscribe(N(e,()=>e.complete(),Cr)),!e.closed&&n.subscribe(e)})}function Lv(){return k((t,n)=>{let e=null;t._refCount++;let r=N(n,void 0,void 0,void 0,()=>{if(!t||t._refCount<=0||0<--t._refCount){e=null;return}let o=t._connection,i=e;e=null,o&&(!i||o===i)&&o.unsubscribe(),n.unsubscribe()});t.subscribe(r),r.closed||(e=t.connect())})}var ki=class extends O{constructor(n,e){super(),this.source=n,this.subjectFactory=e,this._subject=null,this._refCount=0,this._connection=null,kd(n)&&(this.lift=n.lift)}_subscribe(n){return this.getSubject().subscribe(n)}getSubject(){let n=this._subject;return(!n||n.isStopped)&&(this._subject=this.subjectFactory()),this._subject}_teardown(){this._refCount=0;let{_connection:n}=this;this._subject=this._connection=null,n?.unsubscribe()}connect(){let n=this._connection;if(!n){n=this._connection=new G;let e=this.getSubject();n.add(this.source.subscribe(N(e,void 0,()=>{this._teardown(),e.complete()},r=>{this._teardown(),e.error(r)},()=>this._teardown()))),n.closed&&(this._connection=null,n=G.EMPTY)}return n}refCount(){return Lv()(this)}};var Io={schedule(t){let n=requestAnimationFrame,e=cancelAnimationFrame,{delegate:r}=Io;r&&(n=r.requestAnimationFrame,e=r.cancelAnimationFrame);let o=n(i=>{e=void 0,t(i)});return new G(()=>e?.(o))},requestAnimationFrame(...t){let{delegate:n}=Io;return(n?.requestAnimationFrame||requestAnimationFrame)(...t)},cancelAnimationFrame(...t){let{delegate:n}=Io;return(n?.cancelAnimationFrame||cancelAnimationFrame)(...t)},delegate:void 0};var Ie=class extends S{constructor(n){super(),this._value=n}get value(){return this.getValue()}_subscribe(n){let e=super._subscribe(n);return!e.closed&&n.next(this._value),e}getValue(){let{hasError:n,thrownError:e,_value:r}=this;if(n)throw e;return this._throwIfClosed(),r}next(n){super.next(this._value=n)}};var Fi={now(){return(Fi.delegate||Date).now()},delegate:void 0};var Pi=class extends S{constructor(n=1/0,e=1/0,r=Fi){super(),this._bufferSize=n,this._windowTime=e,this._timestampProvider=r,this._buffer=[],this._infiniteTimeWindow=!0,this._infiniteTimeWindow=e===1/0,this._bufferSize=Math.max(1,n),this._windowTime=Math.max(1,e)}next(n){let{isStopped:e,_buffer:r,_infiniteTimeWindow:o,_timestampProvider:i,_windowTime:s}=this;e||(r.push(n),!o&&r.push(i.now()+s)),this._trimBuffer(),super.next(n)}_subscribe(n){this._throwIfClosed(),this._trimBuffer();let e=this._innerSubscribe(n),{_infiniteTimeWindow:r,_buffer:o}=this,i=o.slice();for(let s=0;sjv(n)&&t()),n},clearImmediate(t){jv(t)}};var{setImmediate:BS,clearImmediate:US}=Vv,ji={setImmediate(...t){let{delegate:n}=ji;return(n?.setImmediate||BS)(...t)},clearImmediate(t){let{delegate:n}=ji;return(n?.clearImmediate||US)(t)},delegate:void 0};var Za=class extends Kn{constructor(n,e){super(n,e),this.scheduler=n,this.work=e}requestAsyncId(n,e,r=0){return r!==null&&r>0?super.requestAsyncId(n,e,r):(n.actions.push(this),n._scheduled||(n._scheduled=ji.setImmediate(n.flush.bind(n,void 0))))}recycleAsyncId(n,e,r=0){var o;if(r!=null?r>0:this.delay>0)return super.recycleAsyncId(n,e,r);let{actions:i}=n;e!=null&&((o=i[i.length-1])===null||o===void 0?void 0:o.id)!==e&&(ji.clearImmediate(e),n._scheduled===e&&(n._scheduled=void 0))}};var So=class t{constructor(n,e=t.now){this.schedulerActionCtor=n,this.now=e}schedule(n,e=0,r){return new this.schedulerActionCtor(this,n).schedule(r,e)}};So.now=Fi.now;var Qn=class extends So{constructor(n,e=So.now){super(n,e),this.actions=[],this._active=!1}flush(n){let{actions:e}=this;if(this._active){e.push(n);return}let r;this._active=!0;do if(r=n.execute(n.state,n.delay))break;while(n=e.shift());if(this._active=!1,r){for(;n=e.shift();)n.unsubscribe();throw r}}};var Ka=class extends Qn{flush(n){this._active=!0;let e=this._scheduled;this._scheduled=void 0;let{actions:r}=this,o;n=n||r.shift();do if(o=n.execute(n.state,n.delay))break;while((n=r[0])&&n.id===e&&r.shift());if(this._active=!1,o){for(;(n=r[0])&&n.id===e&&r.shift();)n.unsubscribe();throw o}}};var jd=new Ka(Za);var kt=new Qn(Kn),Bv=kt;var Qa=class extends Kn{constructor(n,e){super(n,e),this.scheduler=n,this.work=e}requestAsyncId(n,e,r=0){return r!==null&&r>0?super.requestAsyncId(n,e,r):(n.actions.push(this),n._scheduled||(n._scheduled=Io.requestAnimationFrame(()=>n.flush(void 0))))}recycleAsyncId(n,e,r=0){var o;if(r!=null?r>0:this.delay>0)return super.recycleAsyncId(n,e,r);let{actions:i}=n;e!=null&&e===n._scheduled&&((o=i[i.length-1])===null||o===void 0?void 0:o.id)!==e&&(Io.cancelAnimationFrame(e),n._scheduled=void 0)}};var Xa=class extends Qn{flush(n){this._active=!0;let e;n?e=n.id:(e=this._scheduled,this._scheduled=void 0);let{actions:r}=this,o;n=n||r.shift();do if(o=n.execute(n.state,n.delay))break;while((n=r[0])&&n.id===e&&r.shift());if(this._active=!1,o){for(;(n=r[0])&&n.id===e&&r.shift();)n.unsubscribe();throw o}}};var Vd=new Xa(Qa);var Se=new O(t=>t.complete());function Ja(t){return t&&R(t.schedule)}function Bd(t){return t[t.length-1]}function ec(t){return R(Bd(t))?t.pop():void 0}function nn(t){return Ja(Bd(t))?t.pop():void 0}function Uv(t,n){return typeof Bd(t)=="number"?t.pop():n}function $e(t,n,e,r=0,o=!1){let i=n.schedule(function(){e(),o?t.add(this.schedule(null,r)):this.unsubscribe()},r);if(t.add(i),!o)return i}function tc(t,n=0){return k((e,r)=>{e.subscribe(N(r,o=>$e(r,t,()=>r.next(o),n),()=>$e(r,t,()=>r.complete(),n),o=>$e(r,t,()=>r.error(o),n)))})}function nc(t,n=0){return k((e,r)=>{r.add(t.schedule(()=>e.subscribe(r),n))})}function Hv(t,n){return Y(t).pipe(nc(n),tc(n))}function $v(t,n){return Y(t).pipe(nc(n),tc(n))}function zv(t,n){return new O(e=>{let r=0;return n.schedule(function(){r===t.length?e.complete():(e.next(t[r++]),e.closed||this.schedule())})})}function Gv(t,n){return new O(e=>{let r;return $e(e,n,()=>{r=t[za](),$e(e,n,()=>{let o,i;try{({value:o,done:i}=r.next())}catch(s){e.error(s);return}i?e.complete():e.next(o)},0,!0)}),()=>R(r?.return)&&r.return()})}function rc(t,n){if(!t)throw new Error("Iterable cannot be null");return new O(e=>{$e(e,n,()=>{let r=t[Symbol.asyncIterator]();$e(e,n,()=>{r.next().then(o=>{o.done?e.complete():e.next(o.value)})},0,!0)})})}function Wv(t,n){return rc(Wa(t),n)}function qv(t,n){if(t!=null){if(Ua(t))return Hv(t,n);if(Co(t))return zv(t,n);if(Ba(t))return $v(t,n);if(Ha(t))return rc(t,n);if(Ga(t))return Gv(t,n);if(qa(t))return Wv(t,n)}throw $a(t)}function se(t,n){return n?qv(t,n):Y(t)}function T(...t){let n=nn(t);return se(t,n)}function Tr(t,n){let e=R(t)?t:()=>t,r=o=>o.error(e());return new O(n?o=>n.schedule(r,0,o):r)}function Ft(t){return!!t&&(t instanceof O||R(t.lift)&&R(t.subscribe))}var In=Zn(t=>function(){t(this),this.name="EmptyError",this.message="no elements in sequence"});function HS(t,n){let e=typeof n=="object";return new Promise((r,o)=>{let i=new Ot({next:s=>{r(s),i.unsubscribe()},error:o,complete:()=>{e?r(n.defaultValue):o(new In)}});t.subscribe(i)})}function oc(t){return t instanceof Date&&!isNaN(t)}var $S=Zn(t=>function(e=null){t(this),this.message="Timeout has occurred",this.name="TimeoutError",this.info=e});function zS(t,n){let{first:e,each:r,with:o=GS,scheduler:i=n??kt,meta:s=null}=oc(t)?{first:t}:typeof t=="number"?{each:t}:t;if(e==null&&r==null)throw new TypeError("No timeout provided.");return k((a,c)=>{let l,u,d=null,h=0,p=m=>{u=$e(c,i,()=>{try{l.unsubscribe(),Y(o({meta:s,lastValue:d,seen:h})).subscribe(c)}catch(_){c.error(_)}},m)};l=a.subscribe(N(c,m=>{u?.unsubscribe(),h++,c.next(d=m),r>0&&p(r)},void 0,void 0,()=>{u?.closed||u?.unsubscribe(),d=null})),!h&&p(e!=null?typeof e=="number"?e:+e-i.now():r)})}function GS(t){throw new $S(t)}function H(t,n){return k((e,r)=>{let o=0;e.subscribe(N(r,i=>{r.next(t.call(n,i,o++))}))})}var{isArray:WS}=Array;function qS(t,n){return WS(n)?t(...n):t(n)}function Mo(t){return H(n=>qS(t,n))}var{isArray:YS}=Array,{getPrototypeOf:ZS,prototype:KS,keys:QS}=Object;function ic(t){if(t.length===1){let n=t[0];if(YS(n))return{args:n,keys:null};if(XS(n)){let e=QS(n);return{args:e.map(r=>n[r]),keys:e}}}return{args:t,keys:null}}function XS(t){return t&&typeof t=="object"&&ZS(t)===KS}function sc(t,n){return t.reduce((e,r,o)=>(e[r]=n[o],e),{})}function To(...t){let n=nn(t),e=ec(t),{args:r,keys:o}=ic(t);if(r.length===0)return se([],n);let i=new O(JS(r,n,o?s=>sc(o,s):st));return e?i.pipe(Mo(e)):i}function JS(t,n,e=st){return r=>{Yv(n,()=>{let{length:o}=t,i=new Array(o),s=o,a=o;for(let c=0;c{let l=se(t[c],n),u=!1;l.subscribe(N(r,d=>{i[c]=d,u||(u=!0,a--),a||r.next(e(i.slice()))},()=>{--s||r.complete()}))},r)},r)}}function Yv(t,n,e){t?$e(e,t,n):n()}function Zv(t,n,e,r,o,i,s,a){let c=[],l=0,u=0,d=!1,h=()=>{d&&!c.length&&!l&&n.complete()},p=_=>l{i&&n.next(_),l++;let E=!1;Y(e(_,u++)).subscribe(N(n,I=>{o?.(I),i?p(I):n.next(I)},()=>{E=!0},void 0,()=>{if(E)try{for(l--;c.length&&lm(I)):m(I)}h()}catch(I){n.error(I)}}))};return t.subscribe(N(n,p,()=>{d=!0,h()})),()=>{a?.()}}function ve(t,n,e=1/0){return R(n)?ve((r,o)=>H((i,s)=>n(r,i,o,s))(Y(t(r,o))),e):(typeof n=="number"&&(e=n),k((r,o)=>Zv(r,o,t,e)))}function rn(t=1/0){return ve(st,t)}function Kv(){return rn(1)}function on(...t){return Kv()(se(t,nn(t)))}function Vi(t){return new O(n=>{Y(t()).subscribe(n)})}function Ud(...t){let n=ec(t),{args:e,keys:r}=ic(t),o=new O(i=>{let{length:s}=e;if(!s){i.complete();return}let a=new Array(s),c=s,l=s;for(let u=0;u{d||(d=!0,l--),a[u]=h},()=>c--,void 0,()=>{(!c||!d)&&(l||i.next(r?sc(r,a):a),i.complete())}))}});return n?o.pipe(Mo(n)):o}var eM=["addListener","removeListener"],tM=["addEventListener","removeEventListener"],nM=["on","off"];function Hd(t,n,e,r){if(R(e)&&(r=e,e=void 0),r)return Hd(t,n,e).pipe(Mo(r));let[o,i]=iM(t)?tM.map(s=>a=>t[s](n,a,e)):rM(t)?eM.map(Qv(t,n)):oM(t)?nM.map(Qv(t,n)):[];if(!o&&Co(t))return ve(s=>Hd(s,n,e))(Y(t));if(!o)throw new TypeError("Invalid event target");return new O(s=>{let a=(...c)=>s.next(1i(a)})}function Qv(t,n){return e=>r=>t[e](n,r)}function rM(t){return R(t.addListener)&&R(t.removeListener)}function oM(t){return R(t.on)&&R(t.off)}function iM(t){return R(t.addEventListener)&&R(t.removeEventListener)}function xr(t=0,n,e=Bv){let r=-1;return n!=null&&(Ja(n)?e=n:r=n),new O(o=>{let i=oc(t)?+t-e.now():t;i<0&&(i=0);let s=0;return e.schedule(function(){o.closed||(o.next(s++),0<=r?this.schedule(void 0,r):o.complete())},i)})}function sM(t=0,n=kt){return t<0&&(t=0),xr(t,t,n)}function aM(...t){let n=nn(t),e=Uv(t,1/0),r=t;return r.length?r.length===1?Y(r[0]):rn(e)(se(r,n)):Se}function fe(t,n){return k((e,r)=>{let o=0;e.subscribe(N(r,i=>t.call(n,i,o++)&&r.next(i)))})}function Xv(t){return k((n,e)=>{let r=!1,o=null,i=null,s=!1,a=()=>{if(i?.unsubscribe(),i=null,r){r=!1;let l=o;o=null,e.next(l)}s&&e.complete()},c=()=>{i=null,s&&e.complete()};n.subscribe(N(e,l=>{r=!0,o=l,i||Y(t(l)).subscribe(i=N(e,a,c))},()=>{s=!0,(!r||!i||i.closed)&&e.complete()}))})}function Bi(t,n=kt){return Xv(()=>xr(t,n))}function sn(t){return k((n,e)=>{let r=null,o=!1,i;r=n.subscribe(N(e,void 0,void 0,s=>{i=Y(t(s,sn(t)(n))),r?(r.unsubscribe(),r=null,i.subscribe(e)):o=!0})),o&&(r.unsubscribe(),r=null,i.subscribe(e))})}function Jv(t,n,e,r,o){return(i,s)=>{let a=e,c=n,l=0;i.subscribe(N(s,u=>{let d=l++;c=a?t(c,u,d):(a=!0,u),r&&s.next(c)},o&&(()=>{a&&s.next(c),s.complete()})))}}function $d(t,n){return k(Jv(t,n,arguments.length>=2,!1,!0))}function Xn(t,n){return R(n)?ve(t,n,1):ve(t,1)}function cM(t){return $d((n,e,r)=>!t||t(e,r)?n+1:n,0)}function Ar(t,n=kt){return k((e,r)=>{let o=null,i=null,s=null,a=()=>{if(o){o.unsubscribe(),o=null;let l=i;i=null,r.next(l)}};function c(){let l=s+t,u=n.now();if(u{i=l,s=n.now(),o||(o=n.schedule(c,t),r.add(o))},()=>{a(),r.complete()},void 0,()=>{i=o=null}))})}function ey(t){return k((n,e)=>{let r=!1;n.subscribe(N(e,o=>{r=!0,e.next(o)},()=>{r||e.next(t),e.complete()}))})}function Ue(t){return t<=0?()=>Se:k((n,e)=>{let r=0;n.subscribe(N(e,o=>{++r<=t&&(e.next(o),t<=r&&e.complete())}))})}function ty(){return k((t,n)=>{t.subscribe(N(n,Cr))})}function zd(t){return H(()=>t)}function Gd(t,n){return n?e=>on(n.pipe(Ue(1),ty()),e.pipe(Gd(t))):ve((e,r)=>Y(t(e,r)).pipe(Ue(1),zd(e)))}function lM(t,n=kt){let e=xr(t,n);return Gd(()=>e)}function xo(t,n=st){return t=t??uM,k((e,r)=>{let o,i=!0;e.subscribe(N(r,s=>{let a=n(s);(i||!t(o,a))&&(i=!1,o=a,r.next(s))}))})}function uM(t,n){return t===n}function ny(t=dM){return k((n,e)=>{let r=!1;n.subscribe(N(e,o=>{r=!0,e.next(o)},()=>r?e.complete():e.error(t())))})}function dM(){return new In}function Rr(t){return k((n,e)=>{try{n.subscribe(e)}finally{e.add(t)}})}function Sn(t,n){let e=arguments.length>=2;return r=>r.pipe(t?fe((o,i)=>t(o,i,r)):st,Ue(1),e?ey(n):ny(()=>new In))}function ac(t){return t<=0?()=>Se:k((n,e)=>{let r=[];n.subscribe(N(e,o=>{r.push(o),t{for(let o of r)e.next(o);e.complete()},void 0,()=>{r=null}))})}function ry(){return k((t,n)=>{let e,r=!1;t.subscribe(N(n,o=>{let i=e;e=o,r&&n.next([i,o]),r=!0}))})}function qd(t={}){let{connector:n=()=>new S,resetOnError:e=!0,resetOnComplete:r=!0,resetOnRefCountZero:o=!0}=t;return i=>{let s,a,c,l=0,u=!1,d=!1,h=()=>{a?.unsubscribe(),a=void 0},p=()=>{h(),s=c=void 0,u=d=!1},m=()=>{let _=s;p(),_?.unsubscribe()};return k((_,E)=>{l++,!d&&!u&&h();let I=c=c??n();E.add(()=>{l--,l===0&&!d&&!u&&(a=Wd(m,o))}),I.subscribe(E),!s&&l>0&&(s=new Ot({next:ee=>I.next(ee),error:ee=>{d=!0,h(),a=Wd(p,e,ee),I.error(ee)},complete:()=>{u=!0,h(),a=Wd(p,r),I.complete()}}),Y(_).subscribe(s))})(i)}}function Wd(t,n,...e){if(n===!0){t();return}if(n===!1)return;let r=new Ot({next:()=>{r.unsubscribe(),t()}});return Y(n(...e)).subscribe(r)}function oy(t,n,e){let r,o=!1;return t&&typeof t=="object"?{bufferSize:r=1/0,windowTime:n=1/0,refCount:o=!1,scheduler:e}=t:r=t??1/0,qd({connector:()=>new Pi(r,n,e),resetOnError:!0,resetOnComplete:!1,resetOnRefCountZero:o})}function Ui(t){return fe((n,e)=>t<=e)}function Nr(...t){let n=nn(t);return k((e,r)=>{(n?on(t,e,n):on(t,e)).subscribe(r)})}function He(t,n){return k((e,r)=>{let o=null,i=0,s=!1,a=()=>s&&!o&&r.complete();e.subscribe(N(r,c=>{o?.unsubscribe();let l=0,u=i++;Y(t(c,u)).subscribe(o=N(r,d=>r.next(n?n(c,d,u,l++):d),()=>{o=null,a()}))},()=>{s=!0,a()}))})}function Yd(t,n=!1){return k((e,r)=>{let o=0;e.subscribe(N(r,i=>{let s=t(i,o++);(s||n)&&r.next(i),!s&&r.complete()}))})}function nt(t,n,e){let r=R(t)||n||e?{next:t,error:n,complete:e}:t;return r?k((o,i)=>{var s;(s=r.subscribe)===null||s===void 0||s.call(r);let a=!0;o.subscribe(N(i,c=>{var l;(l=r.next)===null||l===void 0||l.call(r,c),i.next(c)},()=>{var c;a=!1,(c=r.complete)===null||c===void 0||c.call(r),i.complete()},c=>{var l;a=!1,(l=r.error)===null||l===void 0||l.call(r,c),i.error(c)},()=>{var c,l;a&&((c=r.unsubscribe)===null||c===void 0||c.call(r)),(l=r.finalize)===null||l===void 0||l.call(r)}))}):st}var ze=null,cc=!1,Zd=1,fM=null,pe=Symbol("SIGNAL");function x(t){let n=ze;return ze=t,n}function lc(){return ze}var Jn={version:0,lastCleanEpoch:0,dirty:!1,producers:void 0,producersTail:void 0,consumers:void 0,consumersTail:void 0,recomputing:!1,consumerAllowSignalWrites:!1,consumerIsAlwaysLive:!1,kind:"unknown",producerMustRecompute:()=>!1,producerRecomputeValue:()=>{},consumerMarkedDirty:()=>{},consumerOnSignalRead:()=>{}};function Mn(t){if(cc)throw new Error("");if(ze===null)return;ze.consumerOnSignalRead(t);let n=ze.producersTail;if(n!==void 0&&n.producer===t)return;let e,r=ze.recomputing;if(r&&(e=n!==void 0?n.nextProducer:ze.producers,e!==void 0&&e.producer===t)){ze.producersTail=e,e.lastReadVersion=t.version;return}let o=t.consumersTail;if(o!==void 0&&o.consumer===ze&&(!r||pM(o,ze)))return;let i=No(ze),s={producer:t,consumer:ze,nextProducer:e,prevConsumer:o,lastReadVersion:t.version,nextConsumer:void 0};ze.producersTail=s,n!==void 0?n.nextProducer=s:ze.producers=s,i&&cy(t,s)}function iy(){Zd++}function Fr(t){if(!(No(t)&&!t.dirty)&&!(!t.dirty&&t.lastCleanEpoch===Zd)){if(!t.producerMustRecompute(t)&&!Ro(t)){Ao(t);return}t.producerRecomputeValue(t),Ao(t)}}function Kd(t){if(t.consumers===void 0)return;let n=cc;cc=!0;try{for(let e=t.consumers;e!==void 0;e=e.nextConsumer){let r=e.consumer;r.dirty||hM(r)}}finally{cc=n}}function Qd(){return ze?.consumerAllowSignalWrites!==!1}function hM(t){t.dirty=!0,Kd(t),t.consumerMarkedDirty?.(t)}function Ao(t){t.dirty=!1,t.lastCleanEpoch=Zd}function Tn(t){return t&&sy(t),x(t)}function sy(t){t.producersTail=void 0,t.recomputing=!0}function er(t,n){x(n),t&&ay(t)}function ay(t){t.recomputing=!1;let n=t.producersTail,e=n!==void 0?n.nextProducer:t.producers;if(e!==void 0){if(No(t))do e=Xd(e);while(e!==void 0);n!==void 0?n.nextProducer=void 0:t.producers=void 0}}function Ro(t){for(let n=t.producers;n!==void 0;n=n.nextProducer){let e=n.producer,r=n.lastReadVersion;if(r!==e.version||(Fr(e),r!==e.version))return!0}return!1}function tr(t){if(No(t)){let n=t.producers;for(;n!==void 0;)n=Xd(n)}t.producers=void 0,t.producersTail=void 0,t.consumers=void 0,t.consumersTail=void 0}function cy(t,n){let e=t.consumersTail,r=No(t);if(e!==void 0?(n.nextConsumer=e.nextConsumer,e.nextConsumer=n):(n.nextConsumer=void 0,t.consumers=n),n.prevConsumer=e,t.consumersTail=n,!r)for(let o=t.producers;o!==void 0;o=o.nextProducer)cy(o.producer,o)}function Xd(t){let n=t.producer,e=t.nextProducer,r=t.nextConsumer,o=t.prevConsumer;if(t.nextConsumer=void 0,t.prevConsumer=void 0,r!==void 0?r.prevConsumer=o:n.consumersTail=o,o!==void 0)o.nextConsumer=r;else if(n.consumers=r,!No(n)){let i=n.producers;for(;i!==void 0;)i=Xd(i)}return e}function No(t){return t.consumerIsAlwaysLive||t.consumers!==void 0}function Hi(t){fM?.(t)}function pM(t,n){let e=n.producersTail;if(e!==void 0){let r=n.producers;do{if(r===t)return!0;if(r===e)break;r=r.nextProducer}while(r!==void 0)}return!1}function $i(t,n){return Object.is(t,n)}function zi(t,n){let e=Object.create(mM);e.computation=t,n!==void 0&&(e.equal=n);let r=()=>{if(Fr(e),Mn(e),e.value===an)throw e.error;return e.value};return r[pe]=e,Hi(e),r}var Or=Symbol("UNSET"),kr=Symbol("COMPUTING"),an=Symbol("ERRORED"),mM=F(g({},Jn),{value:Or,dirty:!0,error:null,equal:$i,kind:"computed",producerMustRecompute(t){return t.value===Or||t.value===kr},producerRecomputeValue(t){if(t.value===kr)throw new Error("");let n=t.value;t.value=kr;let e=Tn(t),r,o=!1;try{r=t.computation(),x(null),o=n!==Or&&n!==an&&r!==an&&t.equal(n,r)}catch(i){r=an,t.error=i}finally{er(t,e)}if(o){t.value=n;return}t.value=r,t.version++}});function gM(){throw new Error}var ly=gM;function uy(t){ly(t)}function Jd(t){ly=t}var vM=null;function ef(t,n){let e=Object.create(Gi);e.value=t,n!==void 0&&(e.equal=n);let r=()=>dy(e);return r[pe]=e,Hi(e),[r,s=>nr(e,s),s=>uc(e,s)]}function dy(t){return Mn(t),t.value}function nr(t,n){Qd()||uy(t),t.equal(t.value,n)||(t.value=n,yM(t))}function uc(t,n){Qd()||uy(t),nr(t,n(t.value))}var Gi=F(g({},Jn),{equal:$i,value:void 0,kind:"signal"});function yM(t){t.version++,iy(),Kd(t),vM?.(t)}var tf=F(g({},Jn),{consumerIsAlwaysLive:!0,consumerAllowSignalWrites:!0,dirty:!0,kind:"effect"});function nf(t){if(t.dirty=!1,t.version>0&&!Ro(t))return;t.version++;let n=Tn(t);try{t.cleanup(),t.fn()}finally{er(t,n)}}var rf;function dc(){return rf}function cn(t){let n=rf;return rf=t,n}var fy=Symbol("NotFound");function Oo(t){return t===fy||t?.name==="\u0275NotFound"}function of(t,n,e){let r=Object.create(bM);r.source=t,r.computation=n,e!=null&&(r.equal=e);let i=()=>{if(Fr(r),Mn(r),r.value===an)throw r.error;return r.value};return i[pe]=r,Hi(r),i}function hy(t,n){Fr(t),nr(t,n),Ao(t)}function py(t,n){if(Fr(t),t.value===an)throw t.error;uc(t,n),Ao(t)}var bM=F(g({},Jn),{value:Or,dirty:!0,error:null,equal:$i,kind:"linkedSignal",producerMustRecompute(t){return t.value===Or||t.value===kr},producerRecomputeValue(t){if(t.value===kr)throw new Error("");let n=t.value;t.value=kr;let e=Tn(t),r,o=!1;try{let i=t.source(),s=n!==Or&&n!==an,a=s?{source:t.sourceValue,value:n}:void 0;r=t.computation(i,a),t.sourceValue=i,x(null),o=s&&r!==an&&t.equal(n,r)}catch(i){r=an,t.error=i}finally{er(t,e)}if(o){t.value=n;return}t.value=r,t.version++}});function my(t){let n=x(null);try{return t()}finally{x(n)}}var yc="https://angular.dev/best-practices/security#preventing-cross-site-scripting-xss",b=class extends Error{code;constructor(n,e){super(Dt(n,e)),this.code=n}};function _M(t){return`NG0${Math.abs(t)}`}function Dt(t,n){return`${_M(t)}${n?": "+n:""}`}var ye=globalThis;function ne(t){for(let n in t)if(t[n]===ne)return n;throw Error("")}function _y(t,n){for(let e in n)n.hasOwnProperty(e)&&!t.hasOwnProperty(e)&&(t[e]=n[e])}function Xi(t){if(typeof t=="string")return t;if(Array.isArray(t))return`[${t.map(Xi).join(", ")}]`;if(t==null)return""+t;let n=t.overriddenName||t.name;if(n)return`${n}`;let e=t.toString();if(e==null)return""+e;let r=e.indexOf(` -`);return r>=0?e.slice(0,r):e}function bc(t,n){return t?n?`${t} ${n}`:t:n||""}var DM=ne({__forward_ref__:ne});function be(t){return t.__forward_ref__=be,t}function Me(t){return yf(t)?t():t}function yf(t){return typeof t=="function"&&t.hasOwnProperty(DM)&&t.__forward_ref__===be}function v(t){return{token:t.token,providedIn:t.providedIn||null,factory:t.factory,value:void 0}}function Z(t){return{providers:t.providers||[],imports:t.imports||[]}}function Ji(t){return EM(t,_c)}function bf(t){return Ji(t)!==null}function EM(t,n){return t.hasOwnProperty(n)&&t[n]||null}function wM(t){let n=t?.[_c]??null;return n||null}function af(t){return t&&t.hasOwnProperty(hc)?t[hc]:null}var _c=ne({\u0275prov:ne}),hc=ne({\u0275inj:ne}),y=class{_desc;ngMetadataName="InjectionToken";\u0275prov;constructor(n,e){this._desc=n,this.\u0275prov=void 0,typeof e=="number"?this.__NG_ELEMENT_ID__=e:e!==void 0&&(this.\u0275prov=v({token:this,providedIn:e.providedIn||"root",factory:e.factory}))}get multi(){return this}toString(){return`InjectionToken ${this._desc}`}};function _f(t){return t&&!!t.\u0275providers}var es=ne({\u0275cmp:ne}),ts=ne({\u0275dir:ne}),Df=ne({\u0275pipe:ne}),Ef=ne({\u0275mod:ne}),qi=ne({\u0275fac:ne}),Br=ne({__NG_ELEMENT_ID__:ne}),gy=ne({__NG_ENV_ID__:ne});function wf(t){return Ec(t,"@NgModule"),t[Ef]||null}function un(t){return Ec(t,"@Component"),t[es]||null}function Dc(t){return Ec(t,"@Directive"),t[ts]||null}function Dy(t){return Ec(t,"@Pipe"),t[Df]||null}function Ec(t,n){if(t==null)throw new b(-919,!1)}function dn(t){return typeof t=="string"?t:t==null?"":String(t)}var Ey=ne({ngErrorCode:ne}),CM=ne({ngErrorMessage:ne}),IM=ne({ngTokenPath:ne});function Cf(t,n){return wy("",-200,n)}function wc(t,n){throw new b(-201,!1)}function wy(t,n,e){let r=new b(n,t);return r[Ey]=n,r[CM]=t,e&&(r[IM]=e),r}function SM(t){return t[Ey]}var cf;function Cy(){return cf}function Ke(t){let n=cf;return cf=t,n}function If(t,n,e){let r=Ji(t);if(r&&r.providedIn=="root")return r.value===void 0?r.value=r.factory():r.value;if(e&8)return null;if(n!==void 0)return n;wc(t,"")}var MM={},Pr=MM,TM="__NG_DI_FLAG__",lf=class{injector;constructor(n){this.injector=n}retrieve(n,e){let r=Lr(e)||0;try{return this.injector.get(n,r&8?null:Pr,r)}catch(o){if(Oo(o))return o;throw o}}};function xM(t,n=0){let e=dc();if(e===void 0)throw new b(-203,!1);if(e===null)return If(t,void 0,n);{let r=AM(n),o=e.retrieve(t,r);if(Oo(o)){if(r.optional)return null;throw o}return o}}function w(t,n=0){return(Cy()||xM)(Me(t),n)}function f(t,n){return w(t,Lr(n))}function Lr(t){return typeof t>"u"||typeof t=="number"?t:0|(t.optional&&8)|(t.host&&1)|(t.self&&2)|(t.skipSelf&&4)}function AM(t){return{optional:!!(t&8),host:!!(t&1),self:!!(t&2),skipSelf:!!(t&4)}}function uf(t){let n=[];for(let e=0;eArray.isArray(e)?Cc(e,n):n(e))}function Sf(t,n,e){n>=t.length?t.push(e):t.splice(n,0,e)}function ns(t,n){return n>=t.length-1?t.pop():t.splice(n,1)[0]}function My(t,n){let e=[];for(let r=0;rn;){let i=o-2;t[o]=t[i],o--}t[n]=e,t[n+1]=r}}function Ic(t,n,e){let r=Fo(t,n);return r>=0?t[r|1]=e:(r=~r,Ty(t,r,n,e)),r}function Sc(t,n){let e=Fo(t,n);if(e>=0)return t[e|1]}function Fo(t,n){return NM(t,n,1)}function NM(t,n,e){let r=0,o=t.length>>e;for(;o!==r;){let i=r+(o-r>>1),s=t[i<n?o=i:r=i+1}return~(o<{e.push(s)};return Cc(n,s=>{let a=s;pc(a,i,[],r)&&(o||=[],o.push(a))}),o!==void 0&&Ay(o,i),e}function Ay(t,n){for(let e=0;e{n(i,r)})}}function pc(t,n,e,r){if(t=Me(t),!t)return!1;let o=null,i=af(t),s=!i&&un(t);if(!i&&!s){let c=t.ngModule;if(i=af(c),i)o=c;else return!1}else{if(s&&!s.standalone)return!1;o=t}let a=r.has(o);if(s){if(a)return!1;if(r.add(o),s.dependencies){let c=typeof s.dependencies=="function"?s.dependencies():s.dependencies;for(let l of c)pc(l,n,e,r)}}else if(i){if(i.imports!=null&&!a){r.add(o);let l;Cc(i.imports,u=>{pc(u,n,e,r)&&(l||=[],l.push(u))}),l!==void 0&&Ay(l,n)}if(!a){let l=rr(o)||(()=>new o);n({provide:o,useFactory:l,deps:Ge},o),n({provide:Tf,useValue:o,multi:!0},o),n({provide:Ur,useValue:()=>w(o),multi:!0},o)}let c=i.providers;if(c!=null&&!a){let l=t;Af(c,u=>{n(u,l)})}}else return!1;return o!==t&&t.providers!==void 0}function Af(t,n){for(let e of t)_f(e)&&(e=e.\u0275providers),Array.isArray(e)?Af(e,n):n(e)}var OM=ne({provide:String,useValue:ne});function Ry(t){return t!==null&&typeof t=="object"&&OM in t}function kM(t){return!!(t&&t.useExisting)}function FM(t){return!!(t&&t.useFactory)}function jr(t){return typeof t=="function"}function Ny(t){return!!t.useClass}var rs=new y(""),fc={},vy={},sf;function Po(){return sf===void 0&&(sf=new Yi),sf}var re=class{},Vr=class extends re{parent;source;scopes;records=new Map;_ngOnDestroyHooks=new Set;_onDestroyHooks=[];get destroyed(){return this._destroyed}_destroyed=!1;injectorDefTypes;constructor(n,e,r,o){super(),this.parent=e,this.source=r,this.scopes=o,ff(n,s=>this.processProvider(s)),this.records.set(Mf,ko(void 0,this)),o.has("environment")&&this.records.set(re,ko(void 0,this));let i=this.records.get(rs);i!=null&&typeof i.value=="string"&&this.scopes.add(i.value),this.injectorDefTypes=new Set(this.get(Tf,Ge,{self:!0}))}retrieve(n,e){let r=Lr(e)||0;try{return this.get(n,Pr,r)}catch(o){if(Oo(o))return o;throw o}}destroy(){Wi(this),this._destroyed=!0;let n=x(null);try{for(let r of this._ngOnDestroyHooks)r.ngOnDestroy();let e=this._onDestroyHooks;this._onDestroyHooks=[];for(let r of e)r()}finally{this.records.clear(),this._ngOnDestroyHooks.clear(),this.injectorDefTypes.clear(),x(n)}}onDestroy(n){return Wi(this),this._onDestroyHooks.push(n),()=>this.removeOnDestroy(n)}runInContext(n){Wi(this);let e=cn(this),r=Ke(void 0),o;try{return n()}finally{cn(e),Ke(r)}}get(n,e=Pr,r){if(Wi(this),n.hasOwnProperty(gy))return n[gy](this);let o=Lr(r),i,s=cn(this),a=Ke(void 0);try{if(!(o&4)){let l=this.records.get(n);if(l===void 0){let u=BM(n)&&Ji(n);u&&this.injectableDefInScope(u)?l=ko(df(n),fc):l=null,this.records.set(n,l)}if(l!=null)return this.hydrate(n,l,o)}let c=o&2?Po():this.parent;return e=o&8&&e===Pr?null:e,c.get(n,e)}catch(c){let l=SM(c);throw l===-200||l===-201?new b(l,null):c}finally{Ke(a),cn(s)}}resolveInjectorInitializers(){let n=x(null),e=cn(this),r=Ke(void 0),o;try{let i=this.get(Ur,Ge,{self:!0});for(let s of i)s()}finally{cn(e),Ke(r),x(n)}}toString(){return"R3Injector[...]"}processProvider(n){n=Me(n);let e=jr(n)?n:Me(n&&n.provide),r=LM(n);if(!jr(n)&&n.multi===!0){let o=this.records.get(e);o||(o=ko(void 0,fc,!0),o.factory=()=>uf(o.multi),this.records.set(e,o)),e=n,o.multi.push(n)}this.records.set(e,r)}hydrate(n,e,r){let o=x(null);try{if(e.value===vy)throw Cf("");return e.value===fc&&(e.value=vy,e.value=e.factory(void 0,r)),typeof e.value=="object"&&e.value&&VM(e.value)&&this._ngOnDestroyHooks.add(e.value),e.value}finally{x(o)}}injectableDefInScope(n){if(!n.providedIn)return!1;let e=Me(n.providedIn);return typeof e=="string"?e==="any"||this.scopes.has(e):this.injectorDefTypes.has(e)}removeOnDestroy(n){let e=this._onDestroyHooks.indexOf(n);e!==-1&&this._onDestroyHooks.splice(e,1)}};function df(t){let n=Ji(t),e=n!==null?n.factory:rr(t);if(e!==null)return e;if(t instanceof y)throw new b(-204,!1);if(t instanceof Function)return PM(t);throw new b(-204,!1)}function PM(t){if(t.length>0)throw new b(-204,!1);let e=wM(t);return e!==null?()=>e.factory(t):()=>new t}function LM(t){if(Ry(t))return ko(void 0,t.useValue);{let n=Rf(t);return ko(n,fc)}}function Rf(t,n,e){let r;if(jr(t)){let o=Me(t);return rr(o)||df(o)}else if(Ry(t))r=()=>Me(t.useValue);else if(FM(t))r=()=>t.useFactory(...uf(t.deps||[]));else if(kM(t))r=(o,i)=>w(Me(t.useExisting),i!==void 0&&i&8?8:void 0);else{let o=Me(t&&(t.useClass||t.provide));if(jM(t))r=()=>new o(...uf(t.deps));else return rr(o)||df(o)}return r}function Wi(t){if(t.destroyed)throw new b(-205,!1)}function ko(t,n,e=!1){return{factory:t,value:n,multi:e?[]:void 0}}function jM(t){return!!t.deps}function VM(t){return t!==null&&typeof t=="object"&&typeof t.ngOnDestroy=="function"}function BM(t){return typeof t=="function"||typeof t=="object"&&t.ngMetadataName==="InjectionToken"}function ff(t,n){for(let e of t)Array.isArray(e)?ff(e,n):e&&_f(e)?ff(e.\u0275providers,n):n(e)}function xe(t,n){let e;t instanceof Vr?(Wi(t),e=t):e=new lf(t);let r,o=cn(e),i=Ke(void 0);try{return n()}finally{cn(o),Ke(i)}}function Nf(){return Cy()!==void 0||dc()!=null}var Lt=0,A=1,P=2,Te=3,Et=4,Qe=5,Hr=6,Lo=7,me=8,An=9,jt=10,ie=11,jo=12,Of=13,$r=14,We=15,ar=16,zr=17,fn=18,Rn=19,kf=20,xn=21,Mc=22,or=23,ct=24,Gr=25,cr=26,le=27,Oy=1,Ff=6,lr=7,os=8,Wr=9,ge=10;function Nn(t){return Array.isArray(t)&&typeof t[Oy]=="object"}function Vt(t){return Array.isArray(t)&&t[Oy]===!0}function Pf(t){return(t.flags&4)!==0}function hn(t){return t.componentOffset>-1}function Vo(t){return(t.flags&1)===1}function Bt(t){return!!t.template}function Bo(t){return(t[P]&512)!==0}function qr(t){return(t[P]&256)===256}var Lf="svg",ky="math";function wt(t){for(;Array.isArray(t);)t=t[Lt];return t}function jf(t,n){return wt(n[t])}function Ct(t,n){return wt(n[t.index])}function Tc(t,n){return t.data[n]}function is(t,n){return t[n]}function Vf(t,n,e,r){e>=t.data.length&&(t.data[e]=null,t.blueprint[e]=null),n[e]=r}function It(t,n){let e=n[t];return Nn(e)?e:e[Lt]}function Fy(t){return(t[P]&4)===4}function xc(t){return(t[P]&128)===128}function Py(t){return Vt(t[Te])}function lt(t,n){return n==null?null:t[n]}function Bf(t){t[zr]=0}function Uf(t){t[P]&1024||(t[P]|=1024,xc(t)&&Yr(t))}function Ly(t,n){for(;t>0;)n=n[$r],t--;return n}function ss(t){return!!(t[P]&9216||t[ct]?.dirty)}function Ac(t){t[jt].changeDetectionScheduler?.notify(8),t[P]&64&&(t[P]|=1024),ss(t)&&Yr(t)}function Yr(t){t[jt].changeDetectionScheduler?.notify(0);let n=ir(t);for(;n!==null&&!(n[P]&8192||(n[P]|=8192,!xc(n)));)n=ir(n)}function Hf(t,n){if(qr(t))throw new b(911,!1);t[xn]===null&&(t[xn]=[]),t[xn].push(n)}function jy(t,n){if(t[xn]===null)return;let e=t[xn].indexOf(n);e!==-1&&t[xn].splice(e,1)}function ir(t){let n=t[Te];return Vt(n)?n[Te]:n}function $f(t){return t[Lo]??=[]}function zf(t){return t.cleanup??=[]}function Vy(t,n,e,r){let o=$f(n);o.push(e),t.firstCreatePass&&zf(t).push(r,o.length-1)}var B={lFrame:Xy(null),bindingsEnabled:!0,skipHydrationRootTNode:null};var hf=!1;function By(){return B.lFrame.elementDepthCount}function Uy(){B.lFrame.elementDepthCount++}function Gf(){B.lFrame.elementDepthCount--}function Rc(){return B.bindingsEnabled}function Wf(){return B.skipHydrationRootTNode!==null}function qf(t){return B.skipHydrationRootTNode===t}function Yf(){B.skipHydrationRootTNode=null}function C(){return B.lFrame.lView}function ce(){return B.lFrame.tView}function Hy(t){return B.lFrame.contextLView=t,t[me]}function $y(t){return B.lFrame.contextLView=null,t}function Ee(){let t=Zf();for(;t!==null&&t.type===64;)t=t.parent;return t}function Zf(){return B.lFrame.currentTNode}function zy(){let t=B.lFrame,n=t.currentTNode;return t.isParent?n:n.parent}function Uo(t,n){let e=B.lFrame;e.currentTNode=t,e.isParent=n}function Kf(){return B.lFrame.isParent}function Qf(){B.lFrame.isParent=!1}function Gy(){return B.lFrame.contextLView}function Xf(){return hf}function Zi(t){let n=hf;return hf=t,n}function pn(){let t=B.lFrame,n=t.bindingRootIndex;return n===-1&&(n=t.bindingRootIndex=t.tView.bindingStartIndex),n}function Jf(){return B.lFrame.bindingIndex}function Wy(t){return B.lFrame.bindingIndex=t}function On(){return B.lFrame.bindingIndex++}function as(t){let n=B.lFrame,e=n.bindingIndex;return n.bindingIndex=n.bindingIndex+t,e}function qy(){return B.lFrame.inI18n}function Yy(t,n){let e=B.lFrame;e.bindingIndex=e.bindingRootIndex=t,Nc(n)}function Zy(){return B.lFrame.currentDirectiveIndex}function Nc(t){B.lFrame.currentDirectiveIndex=t}function Ky(t){let n=B.lFrame.currentDirectiveIndex;return n===-1?null:t[n]}function Oc(){return B.lFrame.currentQueryIndex}function cs(t){B.lFrame.currentQueryIndex=t}function UM(t){let n=t[A];return n.type===2?n.declTNode:n.type===1?t[Qe]:null}function eh(t,n,e){if(e&4){let o=n,i=t;for(;o=o.parent,o===null&&!(e&1);)if(o=UM(i),o===null||(i=i[$r],o.type&10))break;if(o===null)return!1;n=o,t=i}let r=B.lFrame=Qy();return r.currentTNode=n,r.lView=t,!0}function kc(t){let n=Qy(),e=t[A];B.lFrame=n,n.currentTNode=e.firstChild,n.lView=t,n.tView=e,n.contextLView=t,n.bindingIndex=e.bindingStartIndex,n.inI18n=!1}function Qy(){let t=B.lFrame,n=t===null?null:t.child;return n===null?Xy(t):n}function Xy(t){let n={currentTNode:null,isParent:!0,lView:null,tView:null,selectedIndex:-1,contextLView:null,elementDepthCount:0,currentNamespace:null,currentDirectiveIndex:-1,bindingRootIndex:-1,bindingIndex:-1,currentQueryIndex:0,parent:t,child:null,inI18n:!1};return t!==null&&(t.child=n),n}function Jy(){let t=B.lFrame;return B.lFrame=t.parent,t.currentTNode=null,t.lView=null,t}var th=Jy;function Fc(){let t=Jy();t.isParent=!0,t.tView=null,t.selectedIndex=-1,t.contextLView=null,t.elementDepthCount=0,t.currentDirectiveIndex=-1,t.currentNamespace=null,t.bindingRootIndex=-1,t.bindingIndex=-1,t.currentQueryIndex=0}function eb(t){return(B.lFrame.contextLView=Ly(t,B.lFrame.contextLView))[me]}function Ut(){return B.lFrame.selectedIndex}function ur(t){B.lFrame.selectedIndex=t}function Ho(){let t=B.lFrame;return Tc(t.tView,t.selectedIndex)}function tb(){B.lFrame.currentNamespace=Lf}function nb(){HM()}function HM(){B.lFrame.currentNamespace=null}function nh(){return B.lFrame.currentNamespace}var rb=!0;function Pc(){return rb}function ls(t){rb=t}function pf(t,n=null,e=null,r){let o=rh(t,n,e,r);return o.resolveInjectorInitializers(),o}function rh(t,n=null,e=null,r,o=new Set){let i=[e||Ge,xy(t)],s;return new Vr(i,n||Po(),s||null,o)}var $=class t{static THROW_IF_NOT_FOUND=Pr;static NULL=new Yi;static create(n,e){if(Array.isArray(n))return pf({name:""},e,n,"");{let r=n.name??"";return pf({name:r},n.parent,n.providers,r)}}static \u0275prov=v({token:t,providedIn:"any",factory:()=>w(Mf)});static __NG_ELEMENT_ID__=-1},L=new y(""),Ae=(()=>{class t{static __NG_ELEMENT_ID__=$M;static __NG_ENV_ID__=e=>e}return t})(),mc=class extends Ae{_lView;constructor(n){super(),this._lView=n}get destroyed(){return qr(this._lView)}onDestroy(n){let e=this._lView;return Hf(e,n),()=>jy(e,n)}};function $M(){return new mc(C())}var ob=!1,ib=new y(""),kn=(()=>{class t{taskId=0;pendingTasks=new Set;destroyed=!1;pendingTask=new Ie(!1);debugTaskTracker=f(ib,{optional:!0});get hasPendingTasks(){return this.destroyed?!1:this.pendingTask.value}get hasPendingTasksObservable(){return this.destroyed?new O(e=>{e.next(!1),e.complete()}):this.pendingTask}add(){!this.hasPendingTasks&&!this.destroyed&&this.pendingTask.next(!0);let e=this.taskId++;return this.pendingTasks.add(e),this.debugTaskTracker?.add(e),e}has(e){return this.pendingTasks.has(e)}remove(e){this.pendingTasks.delete(e),this.debugTaskTracker?.remove(e),this.pendingTasks.size===0&&this.hasPendingTasks&&this.pendingTask.next(!1)}ngOnDestroy(){this.pendingTasks.clear(),this.hasPendingTasks&&this.pendingTask.next(!1),this.destroyed=!0,this.pendingTask.unsubscribe()}static \u0275prov=v({token:t,providedIn:"root",factory:()=>new t})}return t})(),mf=class extends S{__isAsync;destroyRef=void 0;pendingTasks=void 0;constructor(n=!1){super(),this.__isAsync=n,Nf()&&(this.destroyRef=f(Ae,{optional:!0})??void 0,this.pendingTasks=f(kn,{optional:!0})??void 0)}emit(n){let e=x(null);try{super.next(n)}finally{x(e)}}subscribe(n,e,r){let o=n,i=e||(()=>null),s=r;if(n&&typeof n=="object"){let c=n;o=c.next?.bind(c),i=c.error?.bind(c),s=c.complete?.bind(c)}this.__isAsync&&(i=this.wrapInTimeout(i),o&&(o=this.wrapInTimeout(o)),s&&(s=this.wrapInTimeout(s)));let a=super.subscribe({next:o,error:i,complete:s});return n instanceof G&&n.add(a),a}wrapInTimeout(n){return e=>{let r=this.pendingTasks?.add();setTimeout(()=>{try{n(e)}finally{r!==void 0&&this.pendingTasks?.remove(r)}})}}},U=mf;function gc(...t){}function oh(t){let n,e;function r(){t=gc;try{e!==void 0&&typeof cancelAnimationFrame=="function"&&cancelAnimationFrame(e),n!==void 0&&clearTimeout(n)}catch{}}return n=setTimeout(()=>{t(),r()}),typeof requestAnimationFrame=="function"&&(e=requestAnimationFrame(()=>{t(),r()})),()=>r()}function sb(t){return queueMicrotask(()=>t()),()=>{t=gc}}var ih="isAngularZone",Ki=ih+"_ID",zM=0,j=class t{hasPendingMacrotasks=!1;hasPendingMicrotasks=!1;isStable=!0;onUnstable=new U(!1);onMicrotaskEmpty=new U(!1);onStable=new U(!1);onError=new U(!1);constructor(n){let{enableLongStackTrace:e=!1,shouldCoalesceEventChangeDetection:r=!1,shouldCoalesceRunChangeDetection:o=!1,scheduleInRootZone:i=ob}=n;if(typeof Zone>"u")throw new b(908,!1);Zone.assertZonePatched();let s=this;s._nesting=0,s._outer=s._inner=Zone.current,Zone.TaskTrackingZoneSpec&&(s._inner=s._inner.fork(new Zone.TaskTrackingZoneSpec)),e&&Zone.longStackTraceZoneSpec&&(s._inner=s._inner.fork(Zone.longStackTraceZoneSpec)),s.shouldCoalesceEventChangeDetection=!o&&r,s.shouldCoalesceRunChangeDetection=o,s.callbackScheduled=!1,s.scheduleInRootZone=i,qM(s)}static isInAngularZone(){return typeof Zone<"u"&&Zone.current.get(ih)===!0}static assertInAngularZone(){if(!t.isInAngularZone())throw new b(909,!1)}static assertNotInAngularZone(){if(t.isInAngularZone())throw new b(909,!1)}run(n,e,r){return this._inner.run(n,e,r)}runTask(n,e,r,o){let i=this._inner,s=i.scheduleEventTask("NgZoneEvent: "+o,n,GM,gc,gc);try{return i.runTask(s,e,r)}finally{i.cancelTask(s)}}runGuarded(n,e,r){return this._inner.runGuarded(n,e,r)}runOutsideAngular(n){return this._outer.run(n)}},GM={};function sh(t){if(t._nesting==0&&!t.hasPendingMicrotasks&&!t.isStable)try{t._nesting++,t.onMicrotaskEmpty.emit(null)}finally{if(t._nesting--,!t.hasPendingMicrotasks)try{t.runOutsideAngular(()=>t.onStable.emit(null))}finally{t.isStable=!0}}}function WM(t){if(t.isCheckStableRunning||t.callbackScheduled)return;t.callbackScheduled=!0;function n(){oh(()=>{t.callbackScheduled=!1,gf(t),t.isCheckStableRunning=!0,sh(t),t.isCheckStableRunning=!1})}t.scheduleInRootZone?Zone.root.run(()=>{n()}):t._outer.run(()=>{n()}),gf(t)}function qM(t){let n=()=>{WM(t)},e=zM++;t._inner=t._inner.fork({name:"angular",properties:{[ih]:!0,[Ki]:e,[Ki+e]:!0},onInvokeTask:(r,o,i,s,a,c)=>{if(YM(c))return r.invokeTask(i,s,a,c);try{return yy(t),r.invokeTask(i,s,a,c)}finally{(t.shouldCoalesceEventChangeDetection&&s.type==="eventTask"||t.shouldCoalesceRunChangeDetection)&&n(),by(t)}},onInvoke:(r,o,i,s,a,c,l)=>{try{return yy(t),r.invoke(i,s,a,c,l)}finally{t.shouldCoalesceRunChangeDetection&&!t.callbackScheduled&&!ZM(c)&&n(),by(t)}},onHasTask:(r,o,i,s)=>{r.hasTask(i,s),o===i&&(s.change=="microTask"?(t._hasPendingMicrotasks=s.microTask,gf(t),sh(t)):s.change=="macroTask"&&(t.hasPendingMacrotasks=s.macroTask))},onHandleError:(r,o,i,s)=>(r.handleError(i,s),t.runOutsideAngular(()=>t.onError.emit(s)),!1)})}function gf(t){t._hasPendingMicrotasks||(t.shouldCoalesceEventChangeDetection||t.shouldCoalesceRunChangeDetection)&&t.callbackScheduled===!0?t.hasPendingMicrotasks=!0:t.hasPendingMicrotasks=!1}function yy(t){t._nesting++,t.isStable&&(t.isStable=!1,t.onUnstable.emit(null))}function by(t){t._nesting--,sh(t)}var Qi=class{hasPendingMicrotasks=!1;hasPendingMacrotasks=!1;isStable=!0;onUnstable=new U;onMicrotaskEmpty=new U;onStable=new U;onError=new U;run(n,e,r){return n.apply(e,r)}runGuarded(n,e,r){return n.apply(e,r)}runOutsideAngular(n){return n()}runTask(n,e,r,o){return n.apply(e,r)}};function YM(t){return ab(t,"__ignore_ng_zone__")}function ZM(t){return ab(t,"__scheduler_tick__")}function ab(t,n){return!Array.isArray(t)||t.length!==1?!1:t[0]?.data?.[n]===!0}var _t=class{_console=console;handleError(n){this._console.error("ERROR",n)}},ut=new y("",{factory:()=>{let t=f(j),n=f(re),e;return r=>{t.runOutsideAngular(()=>{n.destroyed&&!e?setTimeout(()=>{throw r}):(e??=n.get(_t),e.handleError(r))})}}}),cb={provide:Ur,useValue:()=>{let t=f(_t,{optional:!0})},multi:!0};function W(t,n){let[e,r,o]=ef(t,n?.equal),i=e,s=i[pe];return i.set=r,i.update=o,i.asReadonly=us.bind(i),i}function us(){let t=this[pe];if(t.readonlyFn===void 0){let n=()=>this();n[pe]=t,t.readonlyFn=n}return t.readonlyFn}var $o=(()=>{class t{view;node;constructor(e,r){this.view=e,this.node=r}static __NG_ELEMENT_ID__=KM}return t})();function KM(){return new $o(C(),Ee())}var ln=class{},ds=new y("",{factory:()=>!0});var ah=new y(""),zo=(()=>{class t{internalPendingTasks=f(kn);scheduler=f(ln);errorHandler=f(ut);add(){let e=this.internalPendingTasks.add();return()=>{this.internalPendingTasks.has(e)&&(this.scheduler.notify(11),this.internalPendingTasks.remove(e))}}run(e){let r=this.add();e().catch(this.errorHandler).finally(r)}static \u0275prov=v({token:t,providedIn:"root",factory:()=>new t})}return t})(),Lc=(()=>{class t{static \u0275prov=v({token:t,providedIn:"root",factory:()=>new vf})}return t})(),vf=class{dirtyEffectCount=0;queues=new Map;add(n){this.enqueue(n),this.schedule(n)}schedule(n){n.dirty&&this.dirtyEffectCount++}remove(n){let e=n.zone,r=this.queues.get(e);r.has(n)&&(r.delete(n),n.dirty&&this.dirtyEffectCount--)}enqueue(n){let e=n.zone;this.queues.has(e)||this.queues.set(e,new Set);let r=this.queues.get(e);r.has(n)||r.add(n)}flush(){for(;this.dirtyEffectCount>0;){let n=!1;for(let[e,r]of this.queues)e===null?n||=this.flushQueue(r):n||=e.run(()=>this.flushQueue(r));n||(this.dirtyEffectCount=0)}}flushQueue(n){let e=!1;for(let r of n)r.dirty&&(this.dirtyEffectCount--,e=!0,r.run());return e}},vc=class{[pe];constructor(n){this[pe]=n}destroy(){this[pe].destroy()}};function Go(t,n){let e=n?.injector??f($),r=n?.manualCleanup!==!0?e.get(Ae):null,o,i=e.get($o,null,{optional:!0}),s=e.get(ln);return i!==null?(o=JM(i.view,s,t),r instanceof mc&&r._lView===i.view&&(r=null)):o=eT(t,e.get(Lc),s),o.injector=e,r!==null&&(o.onDestroyFns=[r.onDestroy(()=>o.destroy())]),new vc(o)}var lb=F(g({},tf),{cleanupFns:void 0,zone:null,onDestroyFns:null,run(){let t=Zi(!1);try{nf(this)}finally{Zi(t)}},cleanup(){if(!this.cleanupFns?.length)return;let t=x(null);try{for(;this.cleanupFns.length;)this.cleanupFns.pop()()}finally{this.cleanupFns=[],x(t)}}}),QM=F(g({},lb),{consumerMarkedDirty(){this.scheduler.schedule(this),this.notifier.notify(12)},destroy(){if(tr(this),this.onDestroyFns!==null)for(let t of this.onDestroyFns)t();this.cleanup(),this.scheduler.remove(this)}}),XM=F(g({},lb),{consumerMarkedDirty(){this.view[P]|=8192,Yr(this.view),this.notifier.notify(13)},destroy(){if(tr(this),this.onDestroyFns!==null)for(let t of this.onDestroyFns)t();this.cleanup(),this.view[or]?.delete(this)}});function JM(t,n,e){let r=Object.create(XM);return r.view=t,r.zone=typeof Zone<"u"?Zone.current:null,r.notifier=n,r.fn=ub(r,e),t[or]??=new Set,t[or].add(r),r.consumerMarkedDirty(r),r}function eT(t,n,e){let r=Object.create(QM);return r.fn=ub(r,t),r.scheduler=n,r.notifier=e,r.zone=typeof Zone<"u"?Zone.current:null,r.scheduler.add(r),r.notifier.notify(12),r}function ub(t,n){return()=>{n(e=>(t.cleanupFns??=[]).push(e))}}function ws(t){return{toString:t}.toString()}function aT(t){return typeof t=="function"}function Yb(t,n,e,r){n!==null?n.applyValueToInputSignal(n,r):t[e]=r}var qc=class{previousValue;currentValue;firstChange;constructor(n,e,r){this.previousValue=n,this.currentValue=e,this.firstChange=r}isFirstChange(){return this.firstChange}},Re=(()=>{let t=()=>Zb;return t.ngInherit=!0,t})();function Zb(t){return t.type.prototype.ngOnChanges&&(t.setInput=lT),cT}function cT(){let t=Qb(this),n=t?.current;if(n){let e=t.previous;if(e===Pt)t.previous=n;else for(let r in n)e[r]=n[r];t.current=null,this.ngOnChanges(n)}}function lT(t,n,e,r,o){let i=this.declaredInputs[r],s=Qb(t)||uT(t,{previous:Pt,current:null}),a=s.current||(s.current={}),c=s.previous,l=c[i];a[i]=new qc(l&&l.currentValue,e,c===Pt),Yb(t,n,o,e)}var Kb="__ngSimpleChanges__";function Qb(t){return t[Kb]||null}function uT(t,n){return t[Kb]=n}var db=[];var oe=function(t,n=null,e){for(let r=0;r=r)break}else n[c]<0&&(t[zr]+=65536),(a>14>16&&(t[P]&3)===n&&(t[P]+=16384,fb(a,i)):fb(a,i)}var qo=-1,Kr=class{factory;name;injectImpl;resolving=!1;canSeeViewProviders;multi;componentProviders;index;providerFactory;constructor(n,e,r,o){this.factory=n,this.name=o,this.canSeeViewProviders=e,this.injectImpl=r}};function hT(t){return(t.flags&8)!==0}function pT(t){return(t.flags&16)!==0}function mT(t,n,e){let r=0;for(;rn){s=i-1;break}}}for(;i>16}function Zc(t,n){let e=vT(t),r=n;for(;e>0;)r=r[$r],e--;return r}var bh=!0;function Kc(t){let n=bh;return bh=t,n}var yT=256,n_=yT-1,r_=5,bT=0,mn={};function _T(t,n,e){let r;typeof e=="string"?r=e.charCodeAt(0)||0:e.hasOwnProperty(Br)&&(r=e[Br]),r==null&&(r=e[Br]=bT++);let o=r&n_,i=1<>r_)]|=i}function Qc(t,n){let e=o_(t,n);if(e!==-1)return e;let r=n[A];r.firstCreatePass&&(t.injectorIndex=n.length,lh(r.data,t),lh(n,null),lh(r.blueprint,null));let o=rp(t,n),i=t.injectorIndex;if(t_(o)){let s=Yc(o),a=Zc(o,n),c=a[A].data;for(let l=0;l<8;l++)n[i+l]=a[s+l]|c[s+l]}return n[i+8]=o,i}function lh(t,n){t.push(0,0,0,0,0,0,0,0,n)}function o_(t,n){return t.injectorIndex===-1||t.parent&&t.parent.injectorIndex===t.injectorIndex||n[t.injectorIndex+8]===null?-1:t.injectorIndex}function rp(t,n){if(t.parent&&t.parent.injectorIndex!==-1)return t.parent.injectorIndex;let e=0,r=null,o=n;for(;o!==null;){if(r=l_(o),r===null)return qo;if(e++,o=o[$r],r.injectorIndex!==-1)return r.injectorIndex|e<<16}return qo}function _h(t,n,e){_T(t,n,e)}function DT(t,n){if(n==="class")return t.classes;if(n==="style")return t.styles;let e=t.attrs;if(e){let r=e.length,o=0;for(;o>20,d=r?a:a+u,h=o?a+u:l;for(let p=d;p=c&&m.type===e)return p}if(o){let p=s[c];if(p&&Bt(p)&&p.type===e)return c}return null}function gs(t,n,e,r,o){let i=t[e],s=n.data;if(i instanceof Kr){let a=i;if(a.resolving)throw Cf("");let c=Kc(a.canSeeViewProviders);a.resolving=!0;let l=s[e].type||s[e],u,d=a.injectImpl?Ke(a.injectImpl):null,h=eh(t,r,0);try{i=t[e]=a.factory(void 0,o,s,t,r),n.firstCreatePass&&e>=r.directiveStart&&dT(e,s[e],n)}finally{d!==null&&Ke(d),Kc(c),a.resolving=!1,th()}}return i}function wT(t){if(typeof t=="string")return t.charCodeAt(0)||0;let n=t.hasOwnProperty(Br)?t[Br]:void 0;return typeof n=="number"?n>=0?n&n_:CT:n}function pb(t,n,e){let r=1<>r_)]&r)}function mb(t,n){return!(t&2)&&!(t&1&&n)}var Zr=class{_tNode;_lView;constructor(n,e){this._tNode=n,this._lView=e}get(n,e,r){return a_(this._tNode,this._lView,n,Lr(r),e)}};function CT(){return new Zr(Ee(),C())}function Ne(t){return ws(()=>{let n=t.prototype.constructor,e=n[qi]||Dh(n),r=Object.prototype,o=Object.getPrototypeOf(t.prototype).constructor;for(;o&&o!==r;){let i=o[qi]||Dh(o);if(i&&i!==e)return i;o=Object.getPrototypeOf(o)}return i=>new i})}function Dh(t){return yf(t)?()=>{let n=Dh(Me(t));return n&&n()}:rr(t)}function IT(t,n,e,r,o){let i=t,s=n;for(;i!==null&&s!==null&&s[P]&2048&&!Bo(s);){let a=c_(i,s,e,r|2,mn);if(a!==mn)return a;let c=i.parent;if(!c){let l=s[kf];if(l){let u=l.get(e,mn,r&-5);if(u!==mn)return u}c=l_(s),s=s[$r]}i=c}return o}function l_(t){let n=t[A],e=n.type;return e===2?n.declTNode:e===1?t[Qe]:null}function Cs(t){return DT(Ee(),t)}function ST(){return Jo(Ee(),C())}function Jo(t,n){return new z(Ct(t,n))}var z=(()=>{class t{nativeElement;constructor(e){this.nativeElement=e}static __NG_ELEMENT_ID__=ST}return t})();function u_(t){return t instanceof z?t.nativeElement:t}function MT(){return this._results[Symbol.iterator]()}var Fn=class{_emitDistinctChangesOnly;dirty=!0;_onDirty=void 0;_results=[];_changesDetected=!1;_changes=void 0;length=0;first=void 0;last=void 0;get changes(){return this._changes??=new S}constructor(n=!1){this._emitDistinctChangesOnly=n}get(n){return this._results[n]}map(n){return this._results.map(n)}filter(n){return this._results.filter(n)}find(n){return this._results.find(n)}reduce(n,e){return this._results.reduce(n,e)}forEach(n){this._results.forEach(n)}some(n){return this._results.some(n)}toArray(){return this._results.slice()}toString(){return this._results.toString()}reset(n,e){this.dirty=!1;let r=Sy(n);(this._changesDetected=!Iy(this._results,r,e))&&(this._results=r,this.length=r.length,this.last=r[this.length-1],this.first=r[0])}notifyOnChanges(){this._changes!==void 0&&(this._changesDetected||!this._emitDistinctChangesOnly)&&this._changes.next(this)}onDirty(n){this._onDirty=n}setDirty(){this.dirty=!0,this._onDirty?.()}destroy(){this._changes!==void 0&&(this._changes.complete(),this._changes.unsubscribe())}[Symbol.iterator]=MT};function d_(t){return(t.flags&128)===128}var op=(function(t){return t[t.OnPush=0]="OnPush",t[t.Eager=1]="Eager",t[t.Default=1]="Default",t})(op||{}),f_=new Map,TT=0;function xT(){return TT++}function AT(t){f_.set(t[Rn],t)}function Eh(t){f_.delete(t[Rn])}var gb="__ngContext__";function Zo(t,n){Nn(n)?(t[gb]=n[Rn],AT(n)):t[gb]=n}function h_(t){return m_(t[jo])}function p_(t){return m_(t[Et])}function m_(t){for(;t!==null&&!Vt(t);)t=t[Et];return t}var wh;function ip(t){wh=t}function g_(){if(wh!==void 0)return wh;if(typeof document<"u")return document;throw new b(210,!1)}var hr=new y("",{factory:()=>RT}),RT="ng";var dl=new y(""),Jr=new y("",{providedIn:"platform",factory:()=>"unknown"}),Is=new y(""),eo=new y("",{factory:()=>f(L).body?.querySelector("[ngCspNonce]")?.getAttribute("ngCspNonce")||null});var v_="r";var y_="di";var sp=new y(""),b_=!1,__=new y("",{factory:()=>b_});var fl=new y("");var vb=new WeakMap;function NT(t,n){if(t==null||typeof t!="object")return;let e=vb.get(t);e||(e=new WeakSet,vb.set(t,e)),e.add(n)}var OT=(t,n,e,r)=>{};function kT(t,n,e,r){OT(t,n,e,r)}function hl(t){return(t.flags&32)===32}var FT=()=>null;function D_(t,n,e=!1){return FT(t,n,e)}function E_(t,n){let e=t.contentQueries;if(e!==null){let r=x(null);try{for(let o=0;ot,createScript:t=>t,createScriptURL:t=>t})}catch{}return jc}function pl(t){return PT()?.createHTML(t)||t}var Vc;function w_(){if(Vc===void 0&&(Vc=null,ye.trustedTypes))try{Vc=ye.trustedTypes.createPolicy("angular#unsafe-bypass",{createHTML:t=>t,createScript:t=>t,createScriptURL:t=>t})}catch{}return Vc}function yb(t){return w_()?.createHTML(t)||t}function bb(t){return w_()?.createScriptURL(t)||t}var Pn=class{changingThisBreaksApplicationSecurity;constructor(n){this.changingThisBreaksApplicationSecurity=n}toString(){return`SafeValue must use [property]=binding: ${this.changingThisBreaksApplicationSecurity} (see ${yc})`}},Ih=class extends Pn{getTypeName(){return"HTML"}},Sh=class extends Pn{getTypeName(){return"Style"}},Mh=class extends Pn{getTypeName(){return"Script"}},Th=class extends Pn{getTypeName(){return"URL"}},xh=class extends Pn{getTypeName(){return"ResourceURL"}};function ft(t){return t instanceof Pn?t.changingThisBreaksApplicationSecurity:t}function vn(t,n){let e=C_(t);if(e!=null&&e!==n){if(e==="ResourceURL"&&n==="URL")return!0;throw new Error(`Required a safe ${n}, got a ${e} (see ${yc})`)}return e===n}function C_(t){return t instanceof Pn&&t.getTypeName()||null}function cp(t){return new Ih(t)}function lp(t){return new Sh(t)}function up(t){return new Mh(t)}function dp(t){return new Th(t)}function fp(t){return new xh(t)}function LT(t){let n=new Rh(t);return jT()?new Ah(n):n}var Ah=class{inertDocumentHelper;constructor(n){this.inertDocumentHelper=n}getInertBodyElement(n){n=""+n;try{let e=new window.DOMParser().parseFromString(pl(n),"text/html").body;return e===null?this.inertDocumentHelper.getInertBodyElement(n):(e.firstChild?.remove(),e)}catch{return null}}},Rh=class{defaultDoc;inertDocument;constructor(n){this.defaultDoc=n,this.inertDocument=this.defaultDoc.implementation.createHTMLDocument("sanitization-inert")}getInertBodyElement(n){let e=this.inertDocument.createElement("template");return e.innerHTML=pl(n),e}};function jT(){try{return!!new window.DOMParser().parseFromString(pl(""),"text/html")}catch{return!1}}var VT=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:\/?#]*(?:[\/?#]|$))/i;function Ss(t){return t=String(t),t.match(VT)?t:"unsafe:"+t}function Ln(t){let n={};for(let e of t.split(","))n[e]=!0;return n}function Ms(...t){let n={};for(let e of t)for(let r in e)e.hasOwnProperty(r)&&(n[r]=!0);return n}var I_=Ln("area,br,col,hr,img,wbr"),S_=Ln("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),M_=Ln("rp,rt"),BT=Ms(M_,S_),UT=Ms(S_,Ln("address,article,aside,blockquote,caption,center,del,details,dialog,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,main,map,menu,nav,ol,pre,section,summary,table,ul")),HT=Ms(M_,Ln("a,abbr,acronym,audio,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,picture,q,ruby,rp,rt,s,samp,small,source,span,strike,strong,sub,sup,time,track,tt,u,var,video")),_b=Ms(I_,UT,HT,BT),T_=Ln("background,cite,href,itemtype,longdesc,poster,src,xlink:href"),$T=Ln("abbr,accesskey,align,alt,autoplay,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,controls,coords,datetime,default,dir,download,face,headers,height,hidden,hreflang,hspace,ismap,itemscope,itemprop,kind,label,lang,language,loop,media,muted,nohref,nowrap,open,preload,rel,rev,role,rows,rowspan,rules,scope,scrolling,shape,size,sizes,span,srclang,srcset,start,summary,tabindex,target,title,translate,type,usemap,valign,value,vspace,width"),zT=Ln("aria-activedescendant,aria-atomic,aria-autocomplete,aria-busy,aria-checked,aria-colcount,aria-colindex,aria-colspan,aria-controls,aria-current,aria-describedby,aria-details,aria-disabled,aria-dropeffect,aria-errormessage,aria-expanded,aria-flowto,aria-grabbed,aria-haspopup,aria-hidden,aria-invalid,aria-keyshortcuts,aria-label,aria-labelledby,aria-level,aria-live,aria-modal,aria-multiline,aria-multiselectable,aria-orientation,aria-owns,aria-placeholder,aria-posinset,aria-pressed,aria-readonly,aria-relevant,aria-required,aria-roledescription,aria-rowcount,aria-rowindex,aria-rowspan,aria-selected,aria-setsize,aria-sort,aria-valuemax,aria-valuemin,aria-valuenow,aria-valuetext"),GT=Ms(T_,$T,zT),WT=Ln("script,style,template"),Nh=class{sanitizedSomething=!1;buf=[];sanitizeChildren(n){let e=n.firstChild,r=!0,o=[];for(;e;){if(e.nodeType===Node.ELEMENT_NODE?r=this.startElement(e):e.nodeType===Node.TEXT_NODE?this.chars(e.nodeValue):this.sanitizedSomething=!0,r&&e.firstChild){o.push(e),e=ZT(e);continue}for(;e;){e.nodeType===Node.ELEMENT_NODE&&this.endElement(e);let i=YT(e);if(i){e=i;break}e=o.pop()}}return this.buf.join("")}startElement(n){let e=Db(n).toLowerCase();if(!_b.hasOwnProperty(e))return this.sanitizedSomething=!0,!WT.hasOwnProperty(e);this.buf.push("<"),this.buf.push(e);let r=n.attributes;for(let o=0;o"),!0}endElement(n){let e=Db(n).toLowerCase();_b.hasOwnProperty(e)&&!I_.hasOwnProperty(e)&&(this.buf.push(""))}chars(n){this.buf.push(Eb(n))}};function qT(t,n){return(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_CONTAINED_BY)!==Node.DOCUMENT_POSITION_CONTAINED_BY}function YT(t){let n=t.nextSibling;if(n&&t!==n.previousSibling)throw x_(n);return n}function ZT(t){let n=t.firstChild;if(n&&qT(t,n))throw x_(n);return n}function Db(t){let n=t.nodeName;return typeof n=="string"?n:"FORM"}function x_(t){return new Error(`Failed to sanitize html because the element is clobbered: ${t.outerHTML}`)}var KT=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,QT=/([^\#-~ |!])/g;function Eb(t){return t.replace(/&/g,"&").replace(KT,function(n){let e=n.charCodeAt(0),r=n.charCodeAt(1);return"&#"+((e-55296)*1024+(r-56320)+65536)+";"}).replace(QT,function(n){return"&#"+n.charCodeAt(0)+";"}).replace(//g,">")}var Bc;function ml(t,n){let e=null;try{Bc=Bc||LT(t);let r=n?String(n):"";e=Bc.getInertBodyElement(r);let o=5,i=r;do{if(o===0)throw new Error("Failed to sanitize html because the input is unstable");o--,r=i,i=e.innerHTML,e=Bc.getInertBodyElement(r)}while(r!==i);let a=new Nh().sanitizeChildren(wb(e)||e);return pl(a)}finally{if(e){let r=wb(e)||e;for(;r.firstChild;)r.firstChild.remove()}}}function wb(t){return"content"in t&&XT(t)?t.content:null}function XT(t){return t.nodeType===Node.ELEMENT_NODE&&t.nodeName==="TEMPLATE"}var JT=/^>|^->||--!>|)/g,t0="\u200B$1\u200B";function n0(t){return t.replace(JT,n=>n.replace(e0,t0))}function r0(t,n){return t.createText(n)}function o0(t,n,e){t.setValue(n,e)}function i0(t,n){return t.createComment(n0(n))}function A_(t,n,e){return t.createElement(n,e)}function Xc(t,n,e,r,o){t.insertBefore(n,e,r,o)}function R_(t,n,e){t.appendChild(n,e)}function Cb(t,n,e,r,o){r!==null?Xc(t,n,e,r,o):R_(t,n,e)}function N_(t,n,e,r){t.removeChild(null,n,e,r)}function s0(t,n,e){t.setAttribute(n,"style",e)}function a0(t,n,e){e===""?t.removeAttribute(n,"class"):t.setAttribute(n,"class",e)}function O_(t,n,e){let{mergedAttrs:r,classes:o,styles:i}=e;r!==null&&mT(t,n,r),o!==null&&a0(t,n,o),i!==null&&s0(t,n,i)}var ot=(function(t){return t[t.NONE=0]="NONE",t[t.HTML=1]="HTML",t[t.STYLE=2]="STYLE",t[t.SCRIPT=3]="SCRIPT",t[t.URL=4]="URL",t[t.RESOURCE_URL=5]="RESOURCE_URL",t[t.ATTRIBUTE_NO_BINDING=6]="ATTRIBUTE_NO_BINDING",t})(ot||{});function c0(t){let n=pp();return n?yb(n.sanitize(ot.HTML,t)||""):vn(t,"HTML")?yb(ft(t)):ml(g_(),dn(t))}function k_(t){let n=pp();return n?n.sanitize(ot.URL,t)||"":vn(t,"URL")?ft(t):Ss(dn(t))}function F_(t){let n=pp();if(n)return bb(n.sanitize(ot.RESOURCE_URL,t)||"");if(vn(t,"ResourceURL"))return bb(ft(t));throw new b(904,!1)}var l0={embed:{src:!0},frame:{src:!0},iframe:{src:!0},media:{src:!0},base:{href:!0},link:{href:!0},object:{data:!0,codebase:!0}};function u0(t,n){return l0[t.toLowerCase()]?.[n.toLowerCase()]===!0?F_:k_}function hp(t,n,e){return u0(n,e)(t)}function pp(){let t=C();return t&&t[jt].sanitizer}function d0(t){return t.ownerDocument.defaultView}function f0(t){return t.ownerDocument}function P_(t){return t instanceof Function?t():t}function h0(t,n,e){let r=t.length;for(;;){let o=t.indexOf(n,e);if(o===-1)return o;if(o===0||t.charCodeAt(o-1)<=32){let i=n.length;if(o+i===r||t.charCodeAt(o+i)<=32)return o}e=o+1}}var L_="ng-template";function p0(t,n,e,r){let o=0;if(r){for(;o-1){let i;for(;++oi?d="":d=o[u+1].toLowerCase(),r&2&&l!==d){if(Ht(r))return!1;s=!0}}}}return Ht(r)||s}function Ht(t){return(t&1)===0}function v0(t,n,e,r){if(n===null)return-1;let o=0;if(r||!e){let i=!1;for(;o-1)for(e++;e0?'="'+a+'"':"")+"]"}else r&8?o+="."+s:r&4&&(o+=" "+s);else o!==""&&!Ht(s)&&(n+=Ib(i,o),o=""),r=s,i=i||!Ht(r);e++}return o!==""&&(n+=Ib(i,o)),n}function w0(t){return t.map(E0).join(",")}function C0(t){let n=[],e=[],r=1,o=2;for(;r=0;i--){let s=e[i],a=s.parentNode;s===n?(e.splice(i,1),hs.add(s),s.dispatchEvent(new CustomEvent("animationend",{detail:{cancel:!0}}))):(o&&s===o||a&&r&&a!==r)&&(e.splice(i,1),s.dispatchEvent(new CustomEvent("animationend",{detail:{cancel:!0}})),s.parentNode?.removeChild(s))}}function A0(t,n){let e=kh.get(t);e?e.includes(n)||e.push(n):kh.set(t,[n])}var Qr=new Set,vl=(function(t){return t[t.CHANGE_DETECTION=0]="CHANGE_DETECTION",t[t.AFTER_NEXT_RENDER=1]="AFTER_NEXT_RENDER",t})(vl||{}),Wt=new y(""),Sb=new Set;function qt(t){Sb.has(t)||(Sb.add(t),performance?.mark?.("mark_feature_usage",{detail:{feature:t}}))}var yl=(()=>{class t{impl=null;execute(){this.impl?.execute()}static \u0275prov=v({token:t,providedIn:"root",factory:()=>new t})}return t})(),Dp=[0,1,2,3],Ep=(()=>{class t{ngZone=f(j);scheduler=f(ln);errorHandler=f(_t,{optional:!0});sequences=new Set;deferredRegistrations=new Set;executing=!1;constructor(){f(Wt,{optional:!0})}execute(){let e=this.sequences.size>0;e&&oe(K.AfterRenderHooksStart),this.executing=!0;for(let r of Dp)for(let o of this.sequences)if(!(o.erroredOrDestroyed||!o.hooks[r]))try{o.pipelinedValue=this.ngZone.runOutsideAngular(()=>this.maybeTrace(()=>{let i=o.hooks[r];return i(o.pipelinedValue)},o.snapshot))}catch(i){o.erroredOrDestroyed=!0,this.errorHandler?.handleError(i)}this.executing=!1;for(let r of this.sequences)r.afterRun(),r.once&&(this.sequences.delete(r),r.destroy());for(let r of this.deferredRegistrations)this.sequences.add(r);this.deferredRegistrations.size>0&&this.scheduler.notify(7),this.deferredRegistrations.clear(),e&&oe(K.AfterRenderHooksEnd)}register(e){let{view:r}=e;r!==void 0?((r[Gr]??=[]).push(e),Yr(r),r[P]|=8192):this.executing?this.deferredRegistrations.add(e):this.addSequence(e)}addSequence(e){this.sequences.add(e),this.scheduler.notify(7)}unregister(e){this.executing&&this.sequences.has(e)?(e.erroredOrDestroyed=!0,e.pipelinedValue=void 0,e.once=!0):(this.sequences.delete(e),this.deferredRegistrations.delete(e))}maybeTrace(e,r){return r?r.run(vl.AFTER_NEXT_RENDER,e):e()}static \u0275prov=v({token:t,providedIn:"root",factory:()=>new t})}return t})(),vs=class{impl;hooks;view;once;snapshot;erroredOrDestroyed=!1;pipelinedValue=void 0;unregisterOnDestroy;constructor(n,e,r,o,i,s=null){this.impl=n,this.hooks=e,this.view=r,this.once=o,this.snapshot=s,this.unregisterOnDestroy=i?.onDestroy(()=>this.destroy())}afterRun(){this.erroredOrDestroyed=!1,this.pipelinedValue=void 0,this.snapshot?.dispose(),this.snapshot=null}destroy(){this.impl.unregister(this),this.unregisterOnDestroy?.();let n=this.view?.[Gr];n&&(this.view[Gr]=n.filter(e=>e!==this))}};function ht(t,n){let e=n?.injector??f($);return qt("NgAfterNextRender"),N0(t,e,n,!0)}function R0(t){return t instanceof Function?[void 0,void 0,t,void 0]:[t.earlyRead,t.write,t.mixedReadWrite,t.read]}function N0(t,n,e,r){let o=n.get(yl);o.impl??=n.get(Ep);let i=n.get(Wt,null,{optional:!0}),s=e?.manualCleanup!==!0?n.get(Ae):null,a=n.get($o,null,{optional:!0}),c=new vs(o.impl,R0(t),a?.view,r,s,i?.snapshot(null));return o.impl.register(c),c}var H_=new y("",{factory:()=>({queue:new Set,isScheduled:!1,scheduler:null,injector:f(re)})});function $_(t,n,e){let r=t.get(H_);if(Array.isArray(n))for(let o of n)r.queue.add(o),e?.detachedLeaveAnimationFns?.push(o);else r.queue.add(n),e?.detachedLeaveAnimationFns?.push(n);r.scheduler&&r.scheduler(t)}function O0(t,n){let e=t.get(H_);if(n.detachedLeaveAnimationFns){for(let r of n.detachedLeaveAnimationFns)e.queue.delete(r);n.detachedLeaveAnimationFns=void 0}}function k0(t,n){for(let[e,r]of n)$_(t,r.animateFns)}function Mb(t,n,e,r){let o=t?.[cr]?.enter;n!==null&&o&&o.has(e.index)&&k0(r,o)}function Wo(t,n,e,r,o,i,s,a){if(o!=null){let c,l=!1;Vt(o)?c=o:Nn(o)&&(l=!0,o=o[Lt]);let u=wt(o);t===0&&r!==null?(Mb(a,r,i,e),s==null?R_(n,r,u):Xc(n,r,u,s||null,!0)):t===1&&r!==null?(Mb(a,r,i,e),Xc(n,r,u,s||null,!0),x0(i,u)):t===2?(a?.[cr]?.leave?.has(i.index)&&A0(i,u),hs.delete(u),Tb(a,i,e,d=>{if(hs.has(u)){hs.delete(u);return}N_(n,u,l,d)})):t===3&&(hs.delete(u),Tb(a,i,e,()=>{n.destroyNode(u)})),c!=null&&G0(n,t,e,c,i,r,s)}}function F0(t,n){z_(t,n),n[Lt]=null,n[Qe]=null}function P0(t,n,e,r,o,i){r[Lt]=o,r[Qe]=n,_l(t,r,e,1,o,i)}function z_(t,n){n[jt].changeDetectionScheduler?.notify(9),_l(t,n,n[ie],2,null,null)}function L0(t){let n=t[jo];if(!n)return uh(t[A],t);for(;n;){let e=null;if(Nn(n))e=n[jo];else{let r=n[ge];r&&(e=r)}if(!e){for(;n&&!n[Et]&&n!==t;)Nn(n)&&uh(n[A],n),n=n[Te];n===null&&(n=t),Nn(n)&&uh(n[A],n),e=n&&n[Et]}n=e}}function wp(t,n){let e=t[Wr],r=e.indexOf(n);e.splice(r,1)}function bl(t,n){if(qr(n))return;let e=n[ie];e.destroyNode&&_l(t,n,e,3,null,null),L0(n)}function uh(t,n){if(qr(n))return;let e=x(null);try{n[P]&=-129,n[P]|=256,n[ct]&&tr(n[ct]),B0(t,n),V0(t,n),n[A].type===1&&n[ie].destroy();let r=n[ar];if(r!==null&&Vt(n[Te])){r!==n[Te]&&wp(r,n);let o=n[fn];o!==null&&o.detachView(t)}Eh(n)}finally{x(e)}}function Tb(t,n,e,r){let o=t?.[cr];if(o==null||o.leave==null||!o.leave.has(n.index))return r(!1);t&&Qr.add(t[Rn]),$_(e,()=>{if(o.leave&&o.leave.has(n.index)){let s=o.leave.get(n.index),a=[];if(s){for(let c=0;c{t[cr].running=void 0,Qr.delete(t[Rn]),n(!0)});return}n(!1)}function V0(t,n){let e=t.cleanup,r=n[Lo];if(e!==null)for(let s=0;s=0?r[a]():r[-a].unsubscribe(),s+=2}else{let a=r[e[s+1]];e[s].call(a)}r!==null&&(n[Lo]=null);let o=n[xn];if(o!==null){n[xn]=null;for(let s=0;sle&&U_(t,n,le,!1);let a=s?K.TemplateUpdateStart:K.TemplateCreateStart;oe(a,o,e),e(r,o)}finally{ur(i);let a=s?K.TemplateUpdateEnd:K.TemplateCreateEnd;oe(a,o,e)}}function Dl(t,n,e){Q0(t,n,e),(e.flags&64)===64&&X0(t,n,e)}function Ts(t,n,e=Ct){let r=n.localNames;if(r!==null){let o=n.index+1;for(let i=0;inull;function K0(t){return t==="class"?"className":t==="for"?"htmlFor":t==="formaction"?"formAction":t==="innerHtml"?"innerHTML":t==="readonly"?"readOnly":t==="tabindex"?"tabIndex":t}function K_(t,n,e,r,o,i){let s=n[A];if(El(t,s,n,e,r)){hn(t)&&X_(n,t.index);return}t.type&3&&(e=K0(e)),Q_(t,n,e,r,o,i)}function Q_(t,n,e,r,o,i){if(t.type&3){let s=Ct(t,n);r=i!=null?i(r,t.value||"",e):r,o.setProperty(s,e,r)}else t.type&12}function X_(t,n){let e=It(n,t);e[P]&16||(e[P]|=64)}function Q0(t,n,e){let r=e.directiveStart,o=e.directiveEnd;hn(e)&&M0(n,e,t.data[r+e.componentOffset]),t.firstCreatePass||Qc(e,n);let i=e.initialInputs;for(let s=r;s{Yr(t.lView)},consumerOnSignalRead(){this.lView[ct]=this}});function ux(t){let n=t[ct]??Object.create(dx);return n.lView=t,n}var dx=F(g({},Jn),{consumerIsAlwaysLive:!0,kind:"template",consumerMarkedDirty:t=>{let n=ir(t.lView);for(;n&&!rD(n[A]);)n=ir(n);n&&Uf(n)},consumerOnSignalRead(){this.lView[ct]=this}});function rD(t){return t.type!==2}function oD(t){if(t[or]===null)return;let n=!0;for(;n;){let e=!1;for(let r of t[or])r.dirty&&(e=!0,r.zone===null||Zone.current===r.zone?r.run():r.zone.run(()=>r.run()));n=e&&!!(t[P]&8192)}}var fx=100;function iD(t,n=0){let r=t[jt].rendererFactory,o=!1;o||r.begin?.();try{hx(t,n)}finally{o||r.end?.()}}function hx(t,n){let e=Xf();try{Zi(!0),Ph(t,n);let r=0;for(;ss(t);){if(r===fx)throw new b(103,!1);r++,Ph(t,1)}}finally{Zi(e)}}function px(t,n,e,r){if(qr(n))return;let o=n[P],i=!1,s=!1;kc(n);let a=!0,c=null,l=null;i||(rD(t)?(l=sx(n),c=Tn(l)):lc()===null?(a=!1,l=ux(n),c=Tn(l)):n[ct]&&(tr(n[ct]),n[ct]=null));try{Bf(n),Wy(t.bindingStartIndex),e!==null&&Z_(t,n,e,2,r);let u=(o&3)===3;if(!i)if(u){let p=t.preOrderCheckHooks;p!==null&&Hc(n,p,null)}else{let p=t.preOrderHooks;p!==null&&$c(n,p,0,null),ch(n,0)}if(s||mx(n),oD(n),sD(n,0),t.contentQueries!==null&&E_(t,n),!i)if(u){let p=t.contentCheckHooks;p!==null&&Hc(n,p)}else{let p=t.contentHooks;p!==null&&$c(n,p,1),ch(n,1)}vx(t,n);let d=t.components;d!==null&&cD(n,d,0);let h=t.viewQuery;if(h!==null&&Ch(2,h,r),!i)if(u){let p=t.viewCheckHooks;p!==null&&Hc(n,p)}else{let p=t.viewHooks;p!==null&&$c(n,p,2),ch(n,2)}if(t.firstUpdatePass===!0&&(t.firstUpdatePass=!1),n[Mc]){for(let p of n[Mc])p();n[Mc]=null}i||(tD(n),n[P]&=-73)}catch(u){throw i||Yr(n),u}finally{l!==null&&(er(l,c),a&&cx(l)),Fc()}}function sD(t,n){for(let e=h_(t);e!==null;e=p_(e))for(let r=ge;r0&&(t[e-1][Et]=r[Et]);let i=ns(t,ge+n);F0(r[A],r);let s=i[fn];s!==null&&s.detachView(i[A]),r[Te]=null,r[Et]=null,r[P]&=-129}return r}function yx(t,n,e,r){let o=ge+r,i=e.length;r>0&&(e[o-1][Et]=n),r-1&&(bs(n,r),ns(e,r))}this._attachedToViewContainer=!1}bl(this._lView[A],this._lView)}onDestroy(n){Hf(this._lView,n)}markForCheck(){Ap(this._cdRefInjectingView||this._lView,4)}detach(){this._lView[P]&=-129}reattach(){Ac(this._lView),this._lView[P]|=128}detectChanges(){this._lView[P]|=1024,iD(this._lView)}checkNoChanges(){}attachToViewContainerRef(){if(this._appRef)throw new b(902,!1);this._attachedToViewContainer=!0}detachFromAppRef(){this._appRef=null;let n=Bo(this._lView),e=this._lView[ar];e!==null&&!n&&wp(e,this._lView),z_(this._lView[A],this._lView)}attachToAppRef(n){if(this._attachedToViewContainer)throw new b(902,!1);this._appRef=n;let e=Bo(this._lView),r=this._lView[ar];r!==null&&!e&&fD(r,this._lView),Ac(this._lView)}};var dt=(()=>{class t{_declarationLView;_declarationTContainer;elementRef;static __NG_ELEMENT_ID__=bx;constructor(e,r,o){this._declarationLView=e,this._declarationTContainer=r,this.elementRef=o}get ssrId(){return this._declarationTContainer.tView?.ssrId||null}createEmbeddedView(e,r){return this.createEmbeddedViewImpl(e,r)}createEmbeddedViewImpl(e,r,o){let i=xs(this._declarationLView,this._declarationTContainer,e,{embeddedViewInjector:r,dehydratedView:o});return new dr(i)}}return t})();function bx(){return wl(Ee(),C())}function wl(t,n){return t.type&4?new dt(n,t,Jo(t,n)):null}function ei(t,n,e,r,o){let i=t.data[n];if(i===null)i=_x(t,n,e,r,o),qy()&&(i.flags|=32);else if(i.type&64){i.type=e,i.value=r,i.attrs=o;let s=zy();i.injectorIndex=s===null?-1:s.injectorIndex}return Uo(i,!0),i}function _x(t,n,e,r,o){let i=Zf(),s=Kf(),a=s?i:i&&i.parent,c=t.data[n]=Ex(t,a,e,n,r,o);return Dx(t,c,i,s),c}function Dx(t,n,e,r){t.firstChild===null&&(t.firstChild=n),e!==null&&(r?e.child==null&&n.parent!==null&&(e.child=n):e.next===null&&(e.next=n,n.prev=e))}function Ex(t,n,e,r,o,i){let s=n?n.injectorIndex:-1,a=0;return Wf()&&(a|=128),{type:e,index:r,insertBeforeIndex:null,injectorIndex:s,directiveStart:-1,directiveEnd:-1,directiveStylingLast:-1,componentOffset:-1,controlDirectiveIndex:-1,customControlIndex:-1,propertyBindings:null,flags:a,providerIndexes:0,value:o,namespace:nh(),attrs:i,mergedAttrs:null,localNames:null,initialInputs:null,inputs:null,hostDirectiveInputs:null,outputs:null,hostDirectiveOutputs:null,directiveToIndex:null,tView:null,next:null,prev:null,projectionNext:null,child:null,parent:n,projection:null,styles:null,stylesWithoutHost:null,residualStyles:void 0,classes:null,classesWithoutHost:null,residualClasses:void 0,classBindings:0,styleBindings:0}}function wx(t){let n=t[Ff]??[],r=t[Te][ie],o=[];for(let i of n)i.data[y_]!==void 0?o.push(i):Cx(i,r);t[Ff]=o}function Cx(t,n){let e=0,r=t.firstChild;if(r){let o=t.data[v_];for(;enull,Sx=()=>null;function Jc(t,n){return Ix(t,n)}function hD(t,n,e){return Sx(t,n,e)}var pD=class{},Cl=class{},Lh=class{resolveComponentFactory(n){throw new b(917,!1)}},Rs=class{static NULL=new Lh},je=class{},Oe=(()=>{class t{destroyNode=null;static __NG_ELEMENT_ID__=()=>Mx()}return t})();function Mx(){let t=C(),n=Ee(),e=It(n.index,t);return(Nn(e)?e:t)[ie]}var mD=(()=>{class t{static \u0275prov=v({token:t,providedIn:"root",factory:()=>null})}return t})();var Gc={},jh=class{injector;parentInjector;constructor(n,e){this.injector=n,this.parentInjector=e}get(n,e,r){let o=this.injector.get(n,Gc,r);return o!==Gc||e===Gc?o:this.parentInjector.get(n,e,r)}};function el(t,n,e){let r=e?t.styles:null,o=e?t.classes:null,i=0;if(n!==null)for(let s=0;s0&&(e.directiveToIndex=new Map);for(let h=0;h0;){let e=t[--n];if(typeof e=="number"&&e<0)return e}return 0}function Fx(t,n,e){if(e){if(n.exportAs)for(let r=0;rr(wt(_[t.index])):t.index;ED(m,n,e,i,a,p,!1)}}return l}function Vx(t){return t.startsWith("animation")||t.startsWith("transition")}function Bx(t,n,e,r){let o=t.cleanup;if(o!=null)for(let i=0;ic?a[c]:null}typeof s=="string"&&(i+=2)}return null}function ED(t,n,e,r,o,i,s){let a=n.firstCreatePass?zf(n):null,c=$f(e),l=c.length;c.push(o,i),a&&a.push(r,t,l,(l+1)*(s?-1:1))}function kb(t,n,e,r,o,i){let s=n[e],a=n[A],l=a.data[e].outputs[r],d=s[l].subscribe(i);ED(t.index,a,n,o,i,d,!0)}var Vh=Symbol("BINDING");function wD(t){return t.debugInfo?.className||t.type.name||null}var tl=class extends Rs{ngModule;constructor(n){super(),this.ngModule=n}resolveComponentFactory(n){let e=un(n);return new fr(e,this.ngModule)}};function Ux(t){return Object.keys(t).map(n=>{let[e,r,o]=t[n],i={propName:e,templateName:n,isSignal:(r&gl.SignalBased)!==0};return o&&(i.transform=o),i})}function Hx(t){return Object.keys(t).map(n=>({propName:t[n],templateName:n}))}function $x(t,n,e){let r=n instanceof re?n:n?.injector;return r&&t.getStandaloneInjector!==null&&(r=t.getStandaloneInjector(r)||r),r?new jh(e,r):e}function zx(t){let n=t.get(je,null);if(n===null)throw new b(407,!1);let e=t.get(mD,null),r=t.get(ln,null),o=t.get(Wt,null,{optional:!0});return{rendererFactory:n,sanitizer:e,changeDetectionScheduler:r,ngReflect:!1,tracingService:o}}function Gx(t,n){let e=CD(t);return A_(n,e,e==="svg"?Lf:e==="math"?ky:null)}function Wx(t){if(t?.toLowerCase()==="script")throw new b(905,!1)}function CD(t){return(t.selectors[0][0]||"div").toLowerCase()}var fr=class extends Cl{componentDef;ngModule;selector;componentType;ngContentSelectors;isBoundToModule;cachedInputs=null;cachedOutputs=null;get inputs(){return this.cachedInputs??=Ux(this.componentDef.inputs),this.cachedInputs}get outputs(){return this.cachedOutputs??=Hx(this.componentDef.outputs),this.cachedOutputs}constructor(n,e){super(),this.componentDef=n,this.ngModule=e,this.componentType=n.type,this.selector=w0(n.selectors),this.ngContentSelectors=n.ngContentSelectors??[],this.isBoundToModule=!!e}create(n,e,r,o,i,s){oe(K.DynamicComponentStart);let a=x(null);try{let c=this.componentDef,l=$x(c,o||this.ngModule,n),u=zx(l),d=u.tracingService;return d&&d.componentCreate?d.componentCreate(wD(c),()=>this.createComponentRef(u,l,e,r,i,s)):this.createComponentRef(u,l,e,r,i,s)}finally{x(a)}}createComponentRef(n,e,r,o,i,s){let a=this.componentDef,c=qx(o,a,s,i),l=n.rendererFactory.createRenderer(null,a),u=o?q0(l,o,a.encapsulation,e):Gx(a,l);Wx(u?.tagName);let d=s?.some(Fb)||i?.some(m=>typeof m!="function"&&m.bindings.some(Fb)),h=vp(null,c,null,512|V_(a),null,null,n,l,e,null,D_(u,e,!0));h[le]=u,kc(h);let p=null;try{let m=Np(le,h,2,"#host",()=>c.directiveRegistry,!0,0);O_(l,u,m),Zo(u,h),Dl(c,h,m),ap(c,m,h),Op(c,m),r!==void 0&&Zx(m,this.ngContentSelectors,r),p=It(m.index,h),h[me]=p[me],xp(c,h,null)}catch(m){throw p!==null&&Eh(p),Eh(h),m}finally{oe(K.DynamicComponentEnd),Fc()}return new nl(this.componentType,h,!!d)}};function qx(t,n,e,r){let o=t?["ng-version","21.2.18"]:C0(n.selectors[0]),i=null,s=null,a=0;if(e)for(let u of e)a+=u[Vh].requiredVars,u.create&&(u.targetIdx=0,(i??=[]).push(u)),u.update&&(u.targetIdx=0,(s??=[]).push(u));if(r)for(let u=0;u{if(e&1&&t)for(let r of t)r.create();if(e&2&&n)for(let r of n)r.update()}}function Fb(t){let n=t[Vh].kind;return n==="input"||n==="twoWay"}var nl=class extends pD{_rootLView;_hasInputBindings;instance;hostView;changeDetectorRef;componentType;location;previousInputValues=null;_tNode;constructor(n,e,r){super(),this._rootLView=e,this._hasInputBindings=r,this._tNode=Tc(e[A],le),this.location=Jo(this._tNode,e),this.instance=It(this._tNode.index,e)[me],this.hostView=this.changeDetectorRef=new dr(e,void 0),this.componentType=n}setInput(n,e){this._hasInputBindings;let r=this._tNode;if(this.previousInputValues??=new Map,this.previousInputValues.has(n)&&Object.is(this.previousInputValues.get(n),e))return;let o=this._rootLView,i=El(r,o[A],o,n,e);this.previousInputValues.set(n,e);let s=It(r.index,o);Ap(s,1)}get injector(){return new Zr(this._tNode,this._rootLView)}destroy(){this.hostView.destroy()}onDestroy(n){this.hostView.onDestroy(n)}};function Zx(t,n,e){let r=t.projection=[];for(let o=0;o{class t{static __NG_ELEMENT_ID__=Kx}return t})();function Kx(){let t=Ee();return ID(t,C())}var Bh=class t extends qe{_lContainer;_hostTNode;_hostLView;constructor(n,e,r){super(),this._lContainer=n,this._hostTNode=e,this._hostLView=r}get element(){return Jo(this._hostTNode,this._hostLView)}get injector(){return new Zr(this._hostTNode,this._hostLView)}get parentInjector(){let n=rp(this._hostTNode,this._hostLView);if(t_(n)){let e=Zc(n,this._hostLView),r=Yc(n),o=e[A].data[r+8];return new Zr(o,e)}else return new Zr(null,this._hostLView)}clear(){for(;this.length>0;)this.remove(this.length-1)}get(n){let e=Pb(this._lContainer);return e!==null&&e[n]||null}get length(){return this._lContainer.length-ge}createEmbeddedView(n,e,r){let o,i;typeof r=="number"?o=r:r!=null&&(o=r.index,i=r.injector);let s=Jc(this._lContainer,n.ssrId),a=n.createEmbeddedViewImpl(e||{},i,s);return this.insertImpl(a,o,Ko(this._hostTNode,s)),a}createComponent(n,e,r,o,i,s,a){let c=n&&!aT(n),l;if(c)l=e;else{let E=e||{};l=E.index,r=E.injector,o=E.projectableNodes,i=E.environmentInjector||E.ngModuleRef,s=E.directives,a=E.bindings}let u=c?n:new fr(un(n)),d=r||this.parentInjector;if(!i&&u.ngModule==null){let I=(c?d:this.parentInjector).get(re,null);I&&(i=I)}let h=un(u.componentType??{}),p=Jc(this._lContainer,h?.id??null),m=p?.firstChild??null,_=u.create(d,o,m,i,s,a);return this.insertImpl(_.hostView,l,Ko(this._hostTNode,p)),_}insert(n,e){return this.insertImpl(n,e,!0)}insertImpl(n,e,r){let o=n._lView;if(Py(o)){let a=this.indexOf(n);if(a!==-1)this.detach(a);else{let c=o[Te],l=new t(c,c[Qe],c[Te]);l.detach(l.indexOf(n))}}let i=this._adjustIndex(e),s=this._lContainer;return As(s,o,i,r),n.attachToViewContainerRef(),Sf(dh(s),i,n),n}move(n,e){return this.insert(n,e)}indexOf(n){let e=Pb(this._lContainer);return e!==null?e.indexOf(n):-1}remove(n){let e=this._adjustIndex(n,-1),r=bs(this._lContainer,e);r&&(ns(dh(this._lContainer),e),bl(r[A],r))}detach(n){let e=this._adjustIndex(n,-1),r=bs(this._lContainer,e);return r&&ns(dh(this._lContainer),e)!=null?new dr(r):null}_adjustIndex(n,e=0){return n??this.length+e}};function Pb(t){return t[os]}function dh(t){return t[os]||(t[os]=[])}function ID(t,n){let e,r=n[t.index];return Vt(r)?e=r:(e=lD(r,n,null,t),n[t.index]=e,yp(n,e)),Xx(e,n,t,r),new Bh(e,t,n)}function Qx(t,n){let e=t[ie],r=e.createComment(""),o=Ct(n,t),i=e.parentNode(o);return Xc(e,i,r,e.nextSibling(o),!1),r}var Xx=tA,Jx=()=>!1;function eA(t,n,e){return Jx(t,n,e)}function tA(t,n,e,r){if(t[lr])return;let o;e.type&8?o=wt(r):o=Qx(n,e),t[lr]=o}var Uh=class t{queryList;matches=null;constructor(n){this.queryList=n}clone(){return new t(this.queryList)}setDirty(){this.queryList.setDirty()}},Hh=class t{queries;constructor(n=[]){this.queries=n}createEmbeddedView(n){let e=n.queries;if(e!==null){let r=n.contentQueries!==null?n.contentQueries[0]:e.length,o=[];for(let i=0;i0)r.push(s[a/2]);else{let l=i[a+1],u=n[-c];for(let d=ge;dn.trim())}function AD(t,n,e){t.queries===null&&(t.queries=new $h),t.queries.track(new zh(n,e))}function aA(t,n){let e=t.contentQueries||(t.contentQueries=[]),r=e.length?e[e.length-1]:-1;n!==r&&e.push(t.queries.length-1,n)}function Pp(t,n){return t.queries.getByIndex(n)}function RD(t,n){let e=t[A],r=Pp(e,n);return r.crossesNgTemplate?Gh(e,t,n,[]):SD(e,t,r,n)}function Lp(t,n,e){let r,o=zi(()=>{r._dirtyCounter();let i=cA(r,t);if(n&&i===void 0)throw new b(-951,!1);return i});return r=o[pe],r._dirtyCounter=W(0),r._flatValue=void 0,o}function jp(t){return Lp(!0,!1,t)}function Vp(t){return Lp(!0,!0,t)}function ND(t){return Lp(!1,!1,t)}function OD(t,n){let e=t[pe];e._lView=C(),e._queryIndex=n,e._queryList=Fp(e._lView,n),e._queryList.onDirty(()=>e._dirtyCounter.update(r=>r+1))}function cA(t,n){let e=t._lView,r=t._queryIndex;if(e===void 0||r===void 0||e[P]&4)return n?void 0:Ge;let o=Fp(e,r),i=RD(e,r);return o.reset(i,u_),n?o.first:o._changesDetected||t._flatValue===void 0?t._flatValue=o.toArray():t._flatValue}var gn=class{},Sl=class{};var ol=class extends gn{ngModuleType;_parent;_bootstrapComponents=[];_r3Injector;instance;destroyCbs=[];componentFactoryResolver=new tl(this);constructor(n,e,r,o=!0){super(),this.ngModuleType=n,this._parent=e;let i=wf(n);this._bootstrapComponents=P_(i.bootstrap),this._r3Injector=rh(n,e,[{provide:gn,useValue:this},{provide:Rs,useValue:this.componentFactoryResolver},...r],Xi(n),new Set(["environment"])),o&&this.resolveInjectorInitializers()}resolveInjectorInitializers(){this._r3Injector.resolveInjectorInitializers(),this.instance=this._r3Injector.get(this.ngModuleType)}get injector(){return this._r3Injector}destroy(){let n=this._r3Injector;!n.destroyed&&n.destroy(),this.destroyCbs.forEach(e=>e()),this.destroyCbs=null}onDestroy(n){this.destroyCbs.push(n)}},il=class extends Sl{moduleType;constructor(n){super(),this.moduleType=n}create(n){return new ol(this.moduleType,n,[])}};var Ds=class extends gn{injector;componentFactoryResolver=new tl(this);instance=null;constructor(n){super();let e=new Vr([...n.providers,{provide:gn,useValue:this},{provide:Rs,useValue:this.componentFactoryResolver}],n.parent||Po(),n.debugName,new Set(["environment"]));this.injector=e,n.runEnvironmentInitializers&&e.resolveInjectorInitializers()}destroy(){this.injector.destroy()}onDestroy(n){this.injector.onDestroy(n)}};function ni(t,n,e=null){return new Ds({providers:t,parent:n,debugName:e,runEnvironmentInitializers:!0}).injector}var lA=(()=>{class t{_injector;cachedInjectors=new Map;constructor(e){this._injector=e}getOrCreateStandaloneInjector(e){if(!e.standalone)return null;if(!this.cachedInjectors.has(e)){let r=xf(!1,e.type),o=r.length>0?ni([r],this._injector,""):null;this.cachedInjectors.set(e,o)}return this.cachedInjectors.get(e)}ngOnDestroy(){try{for(let e of this.cachedInjectors.values())e!==null&&e.destroy()}finally{this.cachedInjectors.clear()}}static \u0275prov=v({token:t,providedIn:"environment",factory:()=>new t(w(re))})}return t})();function ke(t){return ws(()=>{let n=kD(t),e=F(g({},n),{decls:t.decls,vars:t.vars,template:t.template,consts:t.consts||null,ngContentSelectors:t.ngContentSelectors,onPush:t.changeDetection===op.OnPush,directiveDefs:null,pipeDefs:null,dependencies:n.standalone&&t.dependencies||null,getStandaloneInjector:n.standalone?o=>o.get(lA).getOrCreateStandaloneInjector(e):null,getExternalStyles:null,signals:t.signals??!1,data:t.data||{},encapsulation:t.encapsulation||zt.Emulated,styles:t.styles||Ge,_:null,schemas:t.schemas||null,tView:null,id:""});n.standalone&&qt("NgStandalone"),FD(e);let r=t.dependencies;return e.directiveDefs=Lb(r,uA),e.pipeDefs=Lb(r,Dy),e.id=hA(e),e})}function uA(t){return un(t)||Dc(t)}function X(t){return ws(()=>({type:t.type,bootstrap:t.bootstrap||Ge,declarations:t.declarations||Ge,imports:t.imports||Ge,exports:t.exports||Ge,transitiveCompileScopes:null,schemas:t.schemas||null,id:t.id||null}))}function dA(t,n){if(t==null)return Pt;let e={};for(let r in t)if(t.hasOwnProperty(r)){let o=t[r],i,s,a,c;Array.isArray(o)?(a=o[0],i=o[1],s=o[2]??i,c=o[3]||null):(i=o,s=o,a=gl.None,c=null),e[i]=[r,a,c],n[i]=s}return e}function fA(t){if(t==null)return Pt;let n={};for(let e in t)t.hasOwnProperty(e)&&(n[t[e]]=e);return n}function M(t){return ws(()=>{let n=kD(t);return FD(n),n})}function Ns(t){return{type:t.type,name:t.name,factory:null,pure:t.pure!==!1,standalone:t.standalone??!0,onDestroy:t.type.prototype.ngOnDestroy||null}}function kD(t){let n={};return{type:t.type,providersResolver:null,viewProvidersResolver:null,factory:null,hostBindings:t.hostBindings||null,hostVars:t.hostVars||0,hostAttrs:t.hostAttrs||null,contentQueries:t.contentQueries||null,declaredInputs:n,inputConfig:t.inputs||Pt,exportAs:t.exportAs||null,standalone:t.standalone??!0,signals:t.signals===!0,selectors:t.selectors||Ge,viewQuery:t.viewQuery||null,features:t.features||null,setInput:null,resolveHostDirectives:null,hostDirectives:null,controlDef:null,inputs:dA(t.inputs,n),outputs:fA(t.outputs),debugInfo:null}}function FD(t){t.features?.forEach(n=>n(t))}function Lb(t,n){return t?()=>{let e=typeof t=="function"?t():t,r=[];for(let o of e){let i=n(o);i!==null&&r.push(i)}return r}:null}function hA(t){let n=0,e=typeof t.consts=="function"?"":t.consts,r=[t.selectors,t.ngContentSelectors,t.hostVars,t.hostAttrs,e,t.vars,t.decls,t.encapsulation,t.standalone,t.signals,t.exportAs,JSON.stringify(t.inputs),JSON.stringify(t.outputs),Object.getOwnPropertyNames(t.type.prototype),!!t.contentQueries,!!t.viewQuery];for(let i of r.join("|"))n=Math.imul(31,n)+i.charCodeAt(0)<<0;return n+=2147483648,"c"+n}function pA(t){let n=e=>{let r=Array.isArray(t);e.hostDirectives===null?(e.resolveHostDirectives=mA,e.hostDirectives=r?t.map(Wh):[t]):r?e.hostDirectives.unshift(...t.map(Wh)):e.hostDirectives.unshift(t)};return n.ngInherit=!0,n}function mA(t){let n=[],e=!1,r=null,o=null;for(let i=0;i=0;r--){let o=t[r];o.hostVars=n+=o.hostVars,o.hostAttrs=Yo(o.hostAttrs,e=Yo(e,o.hostAttrs))}}function fh(t){return t===Pt?{}:t===Ge?[]:t}function _A(t,n){let e=t.viewQuery;e?t.viewQuery=(r,o)=>{n(r,o),e(r,o)}:t.viewQuery=n}function DA(t,n){let e=t.contentQueries;e?t.contentQueries=(r,o,i)=>{n(r,o,i),e(r,o,i)}:t.contentQueries=n}function EA(t,n){let e=t.hostBindings;e?t.hostBindings=(r,o)=>{n(r,o),e(r,o)}:t.hostBindings=n}function LD(t,n,e,r,o,i,s,a){if(e.firstCreatePass){t.mergedAttrs=Yo(t.mergedAttrs,t.attrs);let u=t.tView=gp(2,t,o,i,s,e.directiveRegistry,e.pipeRegistry,null,e.schemas,e.consts,null);e.queries!==null&&(e.queries.template(e,t),u.queries=e.queries.embeddedTView(t))}a&&(t.flags|=a),Uo(t,!1);let c=CA(e,n,t,r);Pc()&&Cp(e,n,c,t),Zo(c,n);let l=lD(c,n,c,t);n[r+le]=l,yp(n,l),eA(l,t,n)}function wA(t,n,e,r,o,i,s,a,c,l,u){let d=e+le,h;return n.firstCreatePass?(h=ei(n,d,4,s||null,a||null),Rc()&&gD(n,t,h,lt(n.consts,l),Sp),Xb(n,h)):h=n.data[d],LD(h,t,n,e,r,o,i,c),Vo(h)&&Dl(n,t,h),l!=null&&Ts(t,h,u),h}function Qo(t,n,e,r,o,i,s,a,c,l,u){let d=e+le,h;if(n.firstCreatePass){if(h=ei(n,d,4,s||null,a||null),l!=null){let p=lt(n.consts,l);h.localNames=[];for(let m=0;m{class t{log(e){console.log(e)}warn(e){console.warn(e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"platform"})}return t})();function ri(t){return typeof t=="function"&&t[pe]!==void 0}function Bp(t){return ri(t)&&typeof t.set=="function"}var Tl=new y(""),xl=new y(""),Os=(()=>{class t{_ngZone;registry;_isZoneStable=!0;_callbacks=[];_taskTrackingZone=null;_destroyRef;constructor(e,r,o){this._ngZone=e,this.registry=r,Nf()&&(this._destroyRef=f(Ae,{optional:!0})??void 0),Up||(UD(o),o.addToWindow(r)),this._watchAngularEvents(),e.run(()=>{this._taskTrackingZone=typeof Zone>"u"?null:Zone.current.get("TaskTrackingZone")})}_watchAngularEvents(){let e=this._ngZone.onUnstable.subscribe({next:()=>{this._isZoneStable=!1}}),r=this._ngZone.runOutsideAngular(()=>this._ngZone.onStable.subscribe({next:()=>{j.assertNotInAngularZone(),queueMicrotask(()=>{this._isZoneStable=!0,this._runCallbacksIfReady()})}}));this._destroyRef?.onDestroy(()=>{e.unsubscribe(),r.unsubscribe()})}isStable(){return this._isZoneStable&&!this._ngZone.hasPendingMacrotasks}_runCallbacksIfReady(){if(this.isStable())queueMicrotask(()=>{for(;this._callbacks.length!==0;){let e=this._callbacks.pop();clearTimeout(e.timeoutId),e.doneCb()}});else{let e=this.getPendingTasks();this._callbacks=this._callbacks.filter(r=>r.updateCb&&r.updateCb(e)?(clearTimeout(r.timeoutId),!1):!0)}}getPendingTasks(){return this._taskTrackingZone?this._taskTrackingZone.macroTasks.map(e=>({source:e.source,creationLocation:e.creationLocation,data:e.data})):[]}addCallback(e,r,o){let i=-1;r&&r>0&&(i=setTimeout(()=>{this._callbacks=this._callbacks.filter(s=>s.timeoutId!==i),e()},r)),this._callbacks.push({doneCb:e,timeoutId:i,updateCb:o})}whenStable(e,r,o){if(o&&!this._taskTrackingZone)throw new Error('Task tracking zone is required when passing an update callback to whenStable(). Is "zone.js/plugins/task-tracking" loaded?');this.addCallback(e,r,o),this._runCallbacksIfReady()}registerApplication(e){this.registry.registerApplication(e,this)}unregisterApplication(e){this.registry.unregisterApplication(e)}findProviders(e,r,o){return[]}static \u0275fac=function(r){return new(r||t)(w(j),w(BD),w(xl))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),BD=(()=>{class t{_applications=new Map;registerApplication(e,r){this._applications.set(e,r)}unregisterApplication(e){this._applications.delete(e)}unregisterAllApplications(){this._applications.clear()}getTestability(e){return this._applications.get(e)||null}getAllTestabilities(){return Array.from(this._applications.values())}getAllRootElements(){return Array.from(this._applications.keys())}findTestabilityInTree(e,r=!0){return Up?.findTestabilityInTree(this,e,r)??null}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"platform"})}return t})();function UD(t){Up=t}var Up;function jn(t){return!!t&&typeof t.then=="function"}function Al(t){return!!t&&typeof t.subscribe=="function"}var Hp=new y("");function Rl(t){return sr([{provide:Hp,multi:!0,useValue:t}])}var $p=(()=>{class t{resolve;reject;initialized=!1;done=!1;donePromise=new Promise((e,r)=>{this.resolve=e,this.reject=r});appInits=f(Hp,{optional:!0})??[];injector=f($);constructor(){}runInitializers(){if(this.initialized)return;let e=[];for(let o of this.appInits){let i=xe(this.injector,o);if(jn(i))e.push(i);else if(Al(i)){let s=new Promise((a,c)=>{i.subscribe({complete:a,error:c})});e.push(s)}}let r=()=>{this.done=!0,this.resolve()};Promise.all(e).then(()=>{r()}).catch(o=>{this.reject(o)}),e.length===0&&r(),this.initialized=!0}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Nl=new y("");function HD(){Jd(()=>{let t="";throw new b(600,t)})}function $D(t){return t.isBoundToModule}var SA=10;var Be=(()=>{class t{_runningTick=!1;_destroyed=!1;_destroyListeners=[];_views=[];internalErrorHandler=f(ut);afterRenderManager=f(yl);zonelessEnabled=f(ds);rootEffectScheduler=f(Lc);dirtyFlags=0;tracingSnapshot=null;allTestViews=new Set;autoDetectTestViews=new Set;includeAllTestViews=!1;afterTick=new S;get allViews(){return[...(this.includeAllTestViews?this.allTestViews:this.autoDetectTestViews).keys(),...this._views]}get destroyed(){return this._destroyed}componentTypes=[];components=[];internalPendingTask=f(kn);get isStable(){return this.internalPendingTask.hasPendingTasksObservable.pipe(H(e=>!e))}constructor(){f(Wt,{optional:!0})}whenStable(){let e;return new Promise(r=>{e=this.isStable.subscribe({next:o=>{o&&r()}})}).finally(()=>{e.unsubscribe()})}_injector=f(re);_rendererFactory=null;get injector(){return this._injector}bootstrap(e,r){return this.bootstrapImpl(e,r)}bootstrapImpl(e,r,o=$.NULL){return this._injector.get(j).run(()=>{oe(K.BootstrapComponentStart);let s=e instanceof Cl;if(!this._injector.get($p).done){let m="";throw new b(405,m)}let c;s?c=e:c=this._injector.get(Rs).resolveComponentFactory(e),this.componentTypes.push(c.componentType);let l=$D(c)?void 0:this._injector.get(gn),u=r||c.selector,d=c.create(o,[],u,l),h=d.location.nativeElement,p=d.injector.get(Tl,null);return p?.registerApplication(h),d.onDestroy(()=>{this.detachView(d.hostView),ms(this.components,d),p?.unregisterApplication(h)}),this._loadComponent(d),oe(K.BootstrapComponentEnd,d),d})}tick(){this.zonelessEnabled||(this.dirtyFlags|=1),this._tick()}_tick(){oe(K.ChangeDetectionStart),this.tracingSnapshot!==null?this.tracingSnapshot.run(vl.CHANGE_DETECTION,this.tickImpl):this.tickImpl()}tickImpl=()=>{if(this._runningTick)throw oe(K.ChangeDetectionEnd),new b(101,!1);let e=x(null);try{this._runningTick=!0,this.synchronize()}finally{this._runningTick=!1,this.tracingSnapshot?.dispose(),this.tracingSnapshot=null,x(e),this.afterTick.next(),oe(K.ChangeDetectionEnd)}};synchronize(){this._rendererFactory===null&&!this._injector.destroyed&&(this._rendererFactory=this._injector.get(je,null,{optional:!0}));let e=0;for(;this.dirtyFlags!==0&&e++ss(e))){this.dirtyFlags|=2;return}else this.dirtyFlags&=-8}attachView(e){let r=e;this._views.push(r),r.attachToAppRef(this)}detachView(e){let r=e;ms(this._views,r),r.detachFromAppRef()}_loadComponent(e){this.attachView(e.hostView);try{this.tick()}catch(o){this.internalErrorHandler(o)}this.components.push(e),this._injector.get(Nl,[]).forEach(o=>o(e))}ngOnDestroy(){if(!this._destroyed)try{this._destroyListeners.forEach(e=>e()),this._views.slice().forEach(e=>e.destroy())}finally{this._destroyed=!0,this._views=[],this._destroyListeners=[]}}onDestroy(e){return this._destroyListeners.push(e),()=>ms(this._destroyListeners,e)}destroy(){if(this._destroyed)throw new b(406,!1);let e=this._injector;e.destroy&&!e.destroyed&&e.destroy()}get viewCount(){return this._views.length}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function ms(t,n){let e=t.indexOf(n);e>-1&&t.splice(e,1)}function zD(t,n){let e=C(),r=On();if(rt(e,r,n)){let o=ce(),i=Ho();if(El(i,o,e,t,n))hn(i)&&X_(e,i.index);else{let a=Ct(i,e);J_(e[ie],a,null,i.value,t,n,null)}}return zD}function Yt(t,n,e,r){let o=C(),i=On();if(rt(o,i,n)){let s=ce(),a=Ho();ex(a,o,t,n,e,r)}return Yt}function MA(){return C()[We][me]}var qh=class{destroy(n){}updateValue(n,e){}swap(n,e){let r=Math.min(n,e),o=Math.max(n,e),i=this.detach(o);if(o-r>1){let s=this.detach(r);this.attach(r,i),this.attach(o,s)}else this.attach(r,i)}move(n,e){this.attach(e,this.detach(n))}};function hh(t,n,e,r,o){return t===e&&Object.is(n,r)?1:Object.is(o(t,n),o(e,r))?-1:0}function TA(t,n,e,r){let o,i,s=0,a=t.length-1,c=void 0;if(Array.isArray(n)){x(r);let l=n.length-1;for(x(null);s<=a&&s<=l;){let u=t.at(s),d=n[s],h=hh(s,u,s,d,e);if(h!==0){h<0&&t.updateValue(s,d),s++;continue}let p=t.at(a),m=n[l],_=hh(a,p,l,m,e);if(_!==0){_<0&&t.updateValue(a,m),a--,l--;continue}let E=e(s,u),I=e(a,p),ee=e(s,d);if(Object.is(ee,I)){let Pe=e(l,m);Object.is(Pe,E)?(t.swap(s,a),t.updateValue(a,m),l--,a--):t.move(a,s),t.updateValue(s,d),s++;continue}if(o??=new sl,i??=Ub(t,s,a,e),Yh(t,o,s,ee))t.updateValue(s,d),s++,a++;else if(i.has(ee))o.set(E,t.detach(s)),a--;else{let Pe=t.create(s,n[s]);t.attach(s,Pe),s++,a++}}for(;s<=l;)Bb(t,o,e,s,n[s]),s++}else if(n!=null){x(r);let l=n[Symbol.iterator]();x(null);let u=l.next();for(;!u.done&&s<=a;){let d=t.at(s),h=u.value,p=hh(s,d,s,h,e);if(p!==0)p<0&&t.updateValue(s,h),s++,u=l.next();else{o??=new sl,i??=Ub(t,s,a,e);let m=e(s,h);if(Yh(t,o,s,m))t.updateValue(s,h),s++,a++,u=l.next();else if(!i.has(m))t.attach(s,t.create(s,h)),s++,a++,u=l.next();else{let _=e(s,d);o.set(_,t.detach(s)),a--}}}for(;!u.done;)Bb(t,o,e,t.length,u.value),u=l.next()}for(;s<=a;)t.destroy(t.detach(a--));o?.forEach(l=>{t.destroy(l)})}function Yh(t,n,e,r){return n!==void 0&&n.has(r)?(t.attach(e,n.get(r)),n.delete(r),!0):!1}function Bb(t,n,e,r,o){if(Yh(t,n,r,e(r,o)))t.updateValue(r,o);else{let i=t.create(r,o);t.attach(r,i)}}function Ub(t,n,e,r){let o=new Set;for(let i=n;i<=e;i++)o.add(r(i,t.at(i)));return o}var sl=class{kvMap=new Map;_vMap=void 0;has(n){return this.kvMap.has(n)}delete(n){if(!this.has(n))return!1;let e=this.kvMap.get(n);return this._vMap!==void 0&&this._vMap.has(e)?(this.kvMap.set(n,this._vMap.get(e)),this._vMap.delete(e)):this.kvMap.delete(n),!0}get(n){return this.kvMap.get(n)}set(n,e){if(this.kvMap.has(n)){let r=this.kvMap.get(n);this._vMap===void 0&&(this._vMap=new Map);let o=this._vMap;for(;o.has(r);)r=o.get(r);o.set(r,e)}else this.kvMap.set(n,e)}forEach(n){for(let[e,r]of this.kvMap)if(n(r,e),this._vMap!==void 0){let o=this._vMap;for(;o.has(r);)r=o.get(r),n(r,e)}}};function xA(t,n,e,r,o,i,s,a){qt("NgControlFlow");let c=C(),l=ce(),u=lt(l.consts,i);return Qo(c,l,t,n,e,r,o,u,256,s,a),zp}function zp(t,n,e,r,o,i,s,a){qt("NgControlFlow");let c=C(),l=ce(),u=lt(l.consts,i);return Qo(c,l,t,n,e,r,o,u,512,s,a),zp}function AA(t,n){qt("NgControlFlow");let e=C(),r=On(),o=e[r]!==Ve?e[r]:-1,i=o!==-1?al(e,le+o):void 0,s=0;if(rt(e,r,t)){let a=x(null);try{if(i!==void 0&&dD(i,s),t!==-1){let c=le+t,l=al(e,c),u=Xh(e[A],c),d=hD(l,u,e),h=xs(e,u,n,{dehydratedView:d});As(l,h,s,Ko(u,d))}}finally{x(a)}}else if(i!==void 0){let a=uD(i,s);a!==void 0&&(a[me]=n)}}var Zh=class{lContainer;$implicit;$index;constructor(n,e,r){this.lContainer=n,this.$implicit=e,this.$index=r}get $count(){return this.lContainer.length-ge}};function RA(t){return t}function NA(t,n){return n}var Kh=class{hasEmptyBlock;trackByFn;liveCollection;constructor(n,e,r){this.hasEmptyBlock=n,this.trackByFn=e,this.liveCollection=r}};function OA(t,n,e,r,o,i,s,a,c,l,u,d,h){qt("NgControlFlow");let p=C(),m=ce(),_=c!==void 0,E=C(),I=a?s.bind(E[We][me]):s,ee=new Kh(_,I);E[le+t]=ee,Qo(p,m,t+1,n,e,r,o,lt(m.consts,i),256),_&&Qo(p,m,t+2,c,l,u,d,lt(m.consts,h),512)}var Qh=class extends qh{lContainer;hostLView;templateTNode;operationsCounter=void 0;needsIndexUpdate=!1;constructor(n,e,r){super(),this.lContainer=n,this.hostLView=e,this.templateTNode=r}get length(){return this.lContainer.length-ge}at(n){return this.getLView(n)[me].$implicit}attach(n,e){let r=e[Hr];this.needsIndexUpdate||=n!==this.length,As(this.lContainer,e,n,Ko(this.templateTNode,r)),FA(this.lContainer,n)}detach(n){return this.needsIndexUpdate||=n!==this.length-1,PA(this.lContainer,n),LA(this.lContainer,n)}create(n,e){let r=Jc(this.lContainer,this.templateTNode.tView.ssrId);return xs(this.hostLView,this.templateTNode,new Zh(this.lContainer,e,n),{dehydratedView:r})}destroy(n){bl(n[A],n)}updateValue(n,e){this.getLView(n)[me].$implicit=e}reset(){this.needsIndexUpdate=!1}updateIndexes(){if(this.needsIndexUpdate)for(let n=0;n0){let i=r[An];O0(i,o),Qr.delete(r[Rn]),o.detachedLeaveAnimationFns=void 0}}function PA(t,n){if(t.length<=ge)return;let e=ge+n,r=t[e],o=r?r[cr]:void 0;o&&o.leave&&o.leave.size>0&&(o.detachedLeaveAnimationFns=[])}function LA(t,n){return bs(t,n)}function jA(t,n){return uD(t,n)}function Xh(t,n){return Tc(t,n)}function GD(t,n,e){let r=C(),o=On();if(rt(r,o,n)){let i=ce(),s=Ho();K_(s,r,t,n,r[ie],e)}return GD}function Jh(t,n,e,r,o){El(n,t,e,o?"class":"style",r)}function cl(t,n,e,r){let o=C(),i=o[A],s=t+le,a=i.firstCreatePass?Np(s,o,2,n,Sp,Rc(),e,r):i.data[s];if(hn(a)){let c=o[jt].tracingService;if(c&&c.componentCreate){let l=i.data[a.directiveStart+a.componentOffset];return c.componentCreate(wD(l),()=>(Hb(t,n,o,a,r),cl))}}return Hb(t,n,o,a,r),cl}function Hb(t,n,e,r,o){if(Mp(r,e,t,n,WD),Vo(r)){let i=e[A];Dl(i,e,r),ap(i,r,e)}o!=null&&Ts(e,r)}function Gp(){let t=ce(),n=Ee(),e=Tp(n);return t.firstCreatePass&&Op(t,e),qf(e)&&Yf(),Gf(),e.classesWithoutHost!=null&&hT(e)&&Jh(t,e,C(),e.classesWithoutHost,!0),e.stylesWithoutHost!=null&&pT(e)&&Jh(t,e,C(),e.stylesWithoutHost,!1),Gp}function Ol(t,n,e,r){return cl(t,n,e,r),Gp(),Ol}function to(t,n,e,r){let o=C(),i=o[A],s=t+le,a=i.firstCreatePass?Lx(s,i,2,n,e,r):i.data[s];return Mp(a,o,t,n,WD),r!=null&&Ts(o,a),to}function no(){let t=Ee(),n=Tp(t);return qf(n)&&Yf(),Gf(),no}function yn(t,n,e,r){return to(t,n,e,r),no(),yn}var WD=(t,n,e,r,o)=>(ls(!0),A_(n[ie],r,nh()));function Wp(t,n,e){let r=C(),o=r[A],i=t+le,s=o.firstCreatePass?Np(i,r,8,"ng-container",Sp,Rc(),n,e):o.data[i];if(Mp(s,r,t,"ng-container",VA),Vo(s)){let a=r[A];Dl(a,r,s),ap(a,s,r)}return e!=null&&Ts(r,s),Wp}function qp(){let t=ce(),n=Ee(),e=Tp(n);return t.firstCreatePass&&Op(t,e),qp}function qD(t,n,e){return Wp(t,n,e),qp(),qD}var VA=(t,n,e,r,o)=>(ls(!0),i0(n[ie],""));function BA(){return C()}function YD(t,n,e){let r=C(),o=On();if(rt(r,o,n)){let i=ce(),s=Ho();Q_(s,r,t,n,r[ie],e)}return YD}var fs=void 0;function UA(t){let n=Math.floor(Math.abs(t)),e=t.toString().replace(/^[^.]*\.?/,"").length;return n===1&&e===0?1:5}var HA=["en",[["a","p"],["AM","PM"]],[["AM","PM"]],[["S","M","T","W","T","F","S"],["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],["Su","Mo","Tu","We","Th","Fr","Sa"]],fs,[["J","F","M","A","M","J","J","A","S","O","N","D"],["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],["January","February","March","April","May","June","July","August","September","October","November","December"]],fs,[["B","A"],["BC","AD"],["Before Christ","Anno Domini"]],0,[6,0],["M/d/yy","MMM d, y","MMMM d, y","EEEE, MMMM d, y"],["h:mm\u202Fa","h:mm:ss\u202Fa","h:mm:ss\u202Fa z","h:mm:ss\u202Fa zzzz"],["{1}, {0}",fs,fs,fs],[".",",",";","%","+","-","E","\xD7","\u2030","\u221E","NaN",":"],["#,##0.###","#,##0%","\xA4#,##0.00","#E0"],"USD","$","US Dollar",{},"ltr",UA],ph=Object.create(null);function pt(t){let n=$A(t),e=$b(n);if(e)return e;let r=n.split("-")[0];if(e=$b(r),e)return e;if(r==="en")return HA;throw new b(701,!1)}function $b(t){if(!(t in ph)){let n=ye.ng&&ye.ng.common&&ye.ng.common.locales&&ye.ng.common.locales[t];return n!==void 0&&(ph[t]=n),n}return ph[t]}var _e=(function(t){return t[t.LocaleId=0]="LocaleId",t[t.DayPeriodsFormat=1]="DayPeriodsFormat",t[t.DayPeriodsStandalone=2]="DayPeriodsStandalone",t[t.DaysFormat=3]="DaysFormat",t[t.DaysStandalone=4]="DaysStandalone",t[t.MonthsFormat=5]="MonthsFormat",t[t.MonthsStandalone=6]="MonthsStandalone",t[t.Eras=7]="Eras",t[t.FirstDayOfWeek=8]="FirstDayOfWeek",t[t.WeekendRange=9]="WeekendRange",t[t.DateFormat=10]="DateFormat",t[t.TimeFormat=11]="TimeFormat",t[t.DateTimeFormat=12]="DateTimeFormat",t[t.NumberSymbols=13]="NumberSymbols",t[t.NumberFormats=14]="NumberFormats",t[t.CurrencyCode=15]="CurrencyCode",t[t.CurrencySymbol=16]="CurrencySymbol",t[t.CurrencyName=17]="CurrencyName",t[t.Currencies=18]="Currencies",t[t.Directionality=19]="Directionality",t[t.PluralCase=20]="PluralCase",t[t.ExtraData=21]="ExtraData",t})(_e||{});function $A(t){return t.toLowerCase().replace(/_/g,"-")}var ks="en-US";var zA=ks;function ZD(t){typeof t=="string"&&(zA=t.toLowerCase().replace(/_/g,"-"))}function Zt(t,n,e){let r=C(),o=ce(),i=Ee();return QD(o,r,r[ie],i,t,n,e),Zt}function KD(t,n,e){let r=C(),o=ce(),i=Ee();return(i.type&3||e)&&DD(i,o,r,e,r[ie],t,n,Wc(i,r,n)),KD}function QD(t,n,e,r,o,i,s){let a=!0,c=null;if((r.type&3||s)&&(c??=Wc(r,n,i),DD(r,t,n,s,e,o,i,c)&&(a=!1)),a){let l=r.outputs?.[o],u=r.hostDirectiveOutputs?.[o];if(u&&u.length)for(let d=0;d>17&32767}function KA(t){return(t&2)==2}function QA(t,n){return t&131071|n<<17}function ep(t){return t|2}function Xo(t){return(t&131068)>>2}function mh(t,n){return t&-131069|n<<2}function XA(t){return(t&1)===1}function tp(t){return t|1}function JA(t,n,e,r,o,i){let s=i?n.classBindings:n.styleBindings,a=Xr(s),c=Xo(s);t[r]=e;let l=!1,u;if(Array.isArray(e)){let d=e;u=d[1],(u===null||Fo(d,u)>0)&&(l=!0)}else u=e;if(o)if(c!==0){let h=Xr(t[a+1]);t[r+1]=Uc(h,a),h!==0&&(t[h+1]=mh(t[h+1],r)),t[a+1]=QA(t[a+1],r)}else t[r+1]=Uc(a,0),a!==0&&(t[a+1]=mh(t[a+1],r)),a=r;else t[r+1]=Uc(c,0),a===0?a=r:t[c+1]=mh(t[c+1],r),c=r;l&&(t[r+1]=ep(t[r+1])),zb(t,u,r,!0),zb(t,u,r,!1),eR(n,u,t,r,i),s=Uc(a,c),i?n.classBindings=s:n.styleBindings=s}function eR(t,n,e,r,o){let i=o?t.residualClasses:t.residualStyles;i!=null&&typeof n=="string"&&Fo(i,n)>=0&&(e[r+1]=tp(e[r+1]))}function zb(t,n,e,r){let o=t[e+1],i=n===null,s=r?Xr(o):Xo(o),a=!1;for(;s!==0&&(a===!1||i);){let c=t[s],l=t[s+1];tR(c,n)&&(a=!0,t[s+1]=r?tp(l):ep(l)),s=r?Xr(l):Xo(l)}a&&(t[e+1]=r?ep(o):tp(o))}function tR(t,n){return t===null||n==null||(Array.isArray(t)?t[1]:t)===n?!0:Array.isArray(t)&&typeof n=="string"?Fo(t,n)>=0:!1}var $t={textEnd:0,key:0,keyEnd:0,value:0,valueEnd:0};function nR(t){return t.substring($t.key,$t.keyEnd)}function rR(t){return oR(t),eE(t,tE(t,0,$t.textEnd))}function eE(t,n){let e=$t.textEnd;return e===n?-1:(n=$t.keyEnd=iR(t,$t.key=n,e),tE(t,n,e))}function oR(t){$t.key=0,$t.keyEnd=0,$t.value=0,$t.valueEnd=0,$t.textEnd=t.length}function tE(t,n,e){for(;n32;)n++;return n}function Pl(t,n,e){return nE(t,n,e,!1),Pl}function Xe(t,n){return nE(t,n,null,!0),Xe}function Yp(t){aR(hR,sR,t,!0)}function sR(t,n){for(let e=rR(n);e>=0;e=eE(n,e))Ic(t,nR(n),!0)}function nE(t,n,e,r){let o=C(),i=ce(),s=as(2);if(i.firstUpdatePass&&oE(i,t,s,r),n!==Ve&&rt(o,s,n)){let a=i.data[Ut()];iE(i,a,o,o[ie],t,o[s+1]=mR(n,e),r,s)}}function aR(t,n,e,r){let o=ce(),i=as(2);o.firstUpdatePass&&oE(o,null,i,r);let s=C();if(e!==Ve&&rt(s,i,e)){let a=o.data[Ut()];if(sE(a,r)&&!rE(o,i)){let c=r?a.classesWithoutHost:a.stylesWithoutHost;c!==null&&(e=bc(c,e||"")),Jh(o,a,s,e,r)}else pR(o,a,s,s[ie],s[i+1],s[i+1]=fR(t,n,e),r,i)}}function rE(t,n){return n>=t.expandoStartIndex}function oE(t,n,e,r){let o=t.data;if(o[e+1]===null){let i=o[Ut()],s=rE(t,e);sE(i,r)&&n===null&&!s&&(n=!1),n=cR(o,i,n,r),JA(o,i,n,e,s,r)}}function cR(t,n,e,r){let o=Ky(t),i=r?n.residualClasses:n.residualStyles;if(o===null)(r?n.classBindings:n.styleBindings)===0&&(e=gh(null,t,n,e,r),e=Es(e,n.attrs,r),i=null);else{let s=n.directiveStylingLast;if(s===-1||t[s]!==o)if(e=gh(o,t,n,e,r),i===null){let c=lR(t,n,r);c!==void 0&&Array.isArray(c)&&(c=gh(null,t,n,c[1],r),c=Es(c,n.attrs,r),uR(t,n,r,c))}else i=dR(t,n,r)}return i!==void 0&&(r?n.residualClasses=i:n.residualStyles=i),e}function lR(t,n,e){let r=e?n.classBindings:n.styleBindings;if(Xo(r)!==0)return t[Xr(r)]}function uR(t,n,e,r){let o=e?n.classBindings:n.styleBindings;t[Xr(o)]=r}function dR(t,n,e){let r,o=n.directiveEnd;for(let i=1+n.directiveStylingLast;i0;){let c=t[o],l=Array.isArray(c),u=l?c[1]:c,d=u===null,h=e[o+1];h===Ve&&(h=d?Ge:void 0);let p=d?Sc(h,r):u===r?h:void 0;if(l&&!ll(p)&&(p=Sc(c,r)),ll(p)&&(a=p,s))return a;let m=t[o+1];o=s?Xr(m):Xo(m)}if(n!==null){let c=i?n.residualClasses:n.residualStyles;c!=null&&(a=Sc(c,r))}return a}function ll(t){return t!==void 0}function mR(t,n){return t==null||t===""||(typeof n=="string"?t=t+n:typeof t=="object"&&(t=Xi(ft(t)))),t}function sE(t,n){return(t.flags&(n?8:16))!==0}function gR(t,n=""){let e=C(),r=ce(),o=t+le,i=r.firstCreatePass?ei(r,o,1,n,null):r.data[o],s=vR(r,e,i,n);e[o]=s,Pc()&&Cp(r,e,s,i),Uo(i,!1)}var vR=(t,n,e,r)=>(ls(!0),r0(n[ie],r));function aE(t,n,e,r=""){return rt(t,On(),e)?n+dn(e)+r:Ve}function yR(t,n,e,r,o,i=""){let s=Jf(),a=_s(t,s,e,o);return as(2),a?n+dn(e)+r+dn(o)+i:Ve}function bR(t,n,e,r,o,i,s,a=""){let c=Jf(),l=_D(t,c,e,o,s);return as(3),l?n+dn(e)+r+dn(o)+i+dn(s)+a:Ve}function cE(t){return Zp("",t),cE}function Zp(t,n,e){let r=C(),o=aE(r,t,n,e);return o!==Ve&&Kp(r,Ut(),o),Zp}function lE(t,n,e,r,o){let i=C(),s=yR(i,t,n,e,r,o);return s!==Ve&&Kp(i,Ut(),s),lE}function uE(t,n,e,r,o,i,s){let a=C(),c=bR(a,t,n,e,r,o,i,s);return c!==Ve&&Kp(a,Ut(),c),uE}function Kp(t,n,e){let r=jf(n,t);o0(t[ie],r,e)}function dE(t,n,e){Bp(n)&&(n=n());let r=C(),o=On();if(rt(r,o,n)){let i=ce(),s=Ho();K_(s,r,t,n,r[ie],e)}return dE}function _R(t,n){let e=Bp(t);return e&&t.set(n),e}function fE(t,n){let e=C(),r=ce(),o=Ee();return QD(r,e,e[ie],o,t,n),fE}function DR(t,n,e=""){return aE(C(),t,n,e)}function ER(t,n,e){let r=pn()+t,o=C();return o[r]===Ve?ti(o,r,n(e,o)):bD(o,r)}function Wb(t,n,e){let r=ce();r.firstCreatePass&&hE(n,r.data,r.blueprint,Bt(t),e)}function hE(t,n,e,r,o){if(t=Me(t),Array.isArray(t))for(let i=0;i>20;if(jr(t)||!t.multi){let p=new Kr(l,o,D,null),m=yh(c,n,o?u:u+h,d);m===-1?(_h(Qc(a,s),i,c),vh(i,t,n.length),n.push(c),a.directiveStart++,a.directiveEnd++,o&&(a.providerIndexes+=1048576),e.push(p),s.push(p)):(e[m]=p,s[m]=p)}else{let p=yh(c,n,u+h,d),m=yh(c,n,u,u+h),_=p>=0&&e[p],E=m>=0&&e[m];if(o&&!E||!o&&!_){_h(Qc(a,s),i,c);let I=IR(o?CR:wR,e.length,o,r,l,t);!o&&E&&(e[m].providerFactory=I),vh(i,t,n.length,0),n.push(c),a.directiveStart++,a.directiveEnd++,o&&(a.providerIndexes+=1048576),e.push(I),s.push(I)}else{let I=pE(e[o?m:p],l,!o&&r);vh(i,t,p>-1?p:m,I)}!o&&r&&E&&e[m].componentProviders++}}}function vh(t,n,e,r){let o=jr(n),i=Ny(n);if(o||i){let c=(i?Me(n.useClass):n).prototype.ngOnDestroy;if(c){let l=t.destroyHooks||(t.destroyHooks=[]);if(!o&&n.multi){let u=l.indexOf(e);u===-1?l.push(e,[r,c]):l[u+1].push(r,c)}else l.push(e,c)}}}function pE(t,n,e){return e&&t.componentProviders++,t.multi.push(n)-1}function yh(t,n,e,r){for(let o=e;o{e.providersResolver=(r,o)=>Wb(r,o?o(t):t,!1),n&&(e.viewProvidersResolver=(r,o)=>Wb(r,o?o(n):n,!0))}}function SR(t,n){let e=pn()+t,r=C();return r[e]===Ve?ti(r,e,n()):bD(r,e)}function MR(t,n,e){return mE(C(),pn(),t,n,e)}function TR(t,n,e,r){return gE(C(),pn(),t,n,e,r)}function xR(t,n,e,r,o){return vE(C(),pn(),t,n,e,r,o)}function AR(t,n,e,r,o,i,s){return RR(C(),pn(),t,n,e,r,o,i)}function Ll(t,n){let e=t[n];return e===Ve?void 0:e}function mE(t,n,e,r,o,i){let s=n+e;return rt(t,s,o)?ti(t,s+1,i?r.call(i,o):r(o)):Ll(t,s+1)}function gE(t,n,e,r,o,i,s){let a=n+e;return _s(t,a,o,i)?ti(t,a+2,s?r.call(s,o,i):r(o,i)):Ll(t,a+2)}function vE(t,n,e,r,o,i,s,a){let c=n+e;return _D(t,c,o,i,s)?ti(t,c+3,a?r.call(a,o,i,s):r(o,i,s)):Ll(t,c+3)}function RR(t,n,e,r,o,i,s,a,c){let l=n+e;return jx(t,l,o,i,s,a)?ti(t,l+4,c?r.call(c,o,i,s,a):r(o,i,s,a)):Ll(t,l+4)}function NR(t,n){let e=ce(),r,o=t+le;e.firstCreatePass?(r=OR(n,e.pipeRegistry),e.data[o]=r,r.onDestroy&&(e.destroyHooks??=[]).push(o,r.onDestroy)):r=e.data[o];let i=r.factory||(r.factory=rr(r.type,!0)),s,a=Ke(D);try{let c=Kc(!1),l=i();return Kc(c),Vf(e,C(),o,l),l}finally{Ke(a)}}function OR(t,n){if(n)for(let e=n.length-1;e>=0;e--){let r=n[e];if(t===r.name)return r}}function kR(t,n,e){let r=t+le,o=C(),i=is(o,r);return Qp(o,r)?mE(o,pn(),n,i.transform,e,i):i.transform(e)}function FR(t,n,e,r){let o=t+le,i=C(),s=is(i,o);return Qp(i,o)?gE(i,pn(),n,s.transform,e,r,s):s.transform(e,r)}function PR(t,n,e,r,o){let i=t+le,s=C(),a=is(s,i);return Qp(s,i)?vE(s,pn(),n,a.transform,e,r,o,a):a.transform(e,r,o)}function Qp(t,n){return t[A].data[n].pure}function LR(t,n){return wl(t,n)}var ul=class{ngModuleFactory;componentFactories;constructor(n,e){this.ngModuleFactory=n,this.componentFactories=e}},Xp=(()=>{class t{compileModuleSync(e){return new il(e)}compileModuleAsync(e){return Promise.resolve(this.compileModuleSync(e))}compileModuleAndAllComponentsSync(e){let r=this.compileModuleSync(e),o=wf(e),i=P_(o.declarations).reduce((s,a)=>{let c=un(a);return c&&s.push(new fr(c)),s},[]);return new ul(r,i)}compileModuleAndAllComponentsAsync(e){return Promise.resolve(this.compileModuleAndAllComponentsSync(e))}clearCache(){}clearCacheFor(e){}getModuleId(e){}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var yE=(()=>{class t{applicationErrorHandler=f(ut);appRef=f(Be);taskService=f(kn);ngZone=f(j);zonelessEnabled=f(ds);tracing=f(Wt,{optional:!0});zoneIsDefined=typeof Zone<"u"&&!!Zone.root.run;schedulerTickApplyArgs=[{data:{__scheduler_tick__:!0}}];subscriptions=new G;angularZoneId=this.zoneIsDefined?this.ngZone._inner?.get(Ki):null;scheduleInRootZone=!this.zonelessEnabled&&this.zoneIsDefined&&(f(ah,{optional:!0})??!1);cancelScheduledCallback=null;useMicrotaskScheduler=!1;runningTick=!1;pendingRenderTaskId=null;constructor(){this.subscriptions.add(this.appRef.afterTick.subscribe(()=>{let e=this.taskService.add();if(!this.runningTick&&(this.cleanup(),!this.zonelessEnabled||this.appRef.includeAllTestViews)){this.taskService.remove(e);return}this.switchToMicrotaskScheduler(),this.taskService.remove(e)})),this.subscriptions.add(this.ngZone.onUnstable.subscribe(()=>{this.runningTick||this.cleanup()}))}switchToMicrotaskScheduler(){this.ngZone.runOutsideAngular(()=>{let e=this.taskService.add();this.useMicrotaskScheduler=!0,queueMicrotask(()=>{this.useMicrotaskScheduler=!1,this.taskService.remove(e)})})}notify(e){if(!this.zonelessEnabled&&e===5)return;switch(e){case 0:{this.appRef.dirtyFlags|=2;break}case 3:case 2:case 4:case 5:case 1:{this.appRef.dirtyFlags|=4;break}case 6:{this.appRef.dirtyFlags|=2;break}case 12:{this.appRef.dirtyFlags|=16;break}case 13:{this.appRef.dirtyFlags|=2;break}case 11:break;default:this.appRef.dirtyFlags|=8}if(this.appRef.tracingSnapshot=this.tracing?.snapshot(this.appRef.tracingSnapshot)??null,!this.shouldScheduleTick())return;let r=this.useMicrotaskScheduler?sb:oh;this.pendingRenderTaskId=this.taskService.add(),this.scheduleInRootZone?this.cancelScheduledCallback=Zone.root.run(()=>r(()=>this.tick())):this.cancelScheduledCallback=this.ngZone.runOutsideAngular(()=>r(()=>this.tick()))}shouldScheduleTick(){return!(this.appRef.destroyed||this.pendingRenderTaskId!==null||this.runningTick||this.appRef._runningTick||!this.zonelessEnabled&&this.zoneIsDefined&&Zone.current.get(Ki+this.angularZoneId))}tick(){if(this.runningTick||this.appRef.destroyed)return;if(this.appRef.dirtyFlags===0){this.cleanup();return}!this.zonelessEnabled&&this.appRef.dirtyFlags&7&&(this.appRef.dirtyFlags|=1);let e=this.taskService.add();try{this.ngZone.run(()=>{this.runningTick=!0,this.appRef._tick()},void 0,this.schedulerTickApplyArgs)}catch(r){this.applicationErrorHandler(r)}finally{this.taskService.remove(e),this.cleanup()}}ngOnDestroy(){this.subscriptions.unsubscribe(),this.cleanup()}cleanup(){if(this.runningTick=!1,this.cancelScheduledCallback?.(),this.cancelScheduledCallback=null,this.pendingRenderTaskId!==null){let e=this.pendingRenderTaskId;this.pendingRenderTaskId=null,this.taskService.remove(e)}}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function jR(){return qt("NgZoneless"),sr([...Jp(),[]])}function Jp(){return[{provide:ln,useExisting:yE},{provide:j,useClass:Qi},{provide:ds,useValue:!0}]}function VR(){return typeof $localize<"u"&&$localize.locale||ks}var Ls=new y("",{factory:()=>f(Ls,{optional:!0,skipSelf:!0})||VR()});var js=class{destroyed=!1;listeners=null;errorHandler=f(_t,{optional:!0});destroyRef=f(Ae);constructor(){this.destroyRef.onDestroy(()=>{this.destroyed=!0,this.listeners=null})}subscribe(n){if(this.destroyed)throw new b(953,!1);return(this.listeners??=[]).push(n),{unsubscribe:()=>{let e=this.listeners?.indexOf(n);e!==void 0&&e!==-1&&this.listeners?.splice(e,1)}}}emit(n){if(this.destroyed){console.warn(Dt(953,!1));return}if(this.listeners===null)return;let e=x(null);try{for(let r of this.listeners)try{r(n)}catch(o){this.errorHandler?.handleError(o)}}finally{x(e)}}};function q(t){return my(t)}function Kt(t,n){return zi(t,n?.equal)}var BR=t=>t;function em(t,n){if(typeof t=="function"){let e=of(t,BR,n?.equal);return bE(e,n?.debugName)}else{let e=of(t.source,t.computation,t.equal);return bE(e,t.debugName)}}function bE(t,n){let e=t[pe],r=t;return r.set=o=>hy(e,o),r.update=o=>py(e,o),r.asReadonly=us.bind(t),r}var Ul=Symbol("InputSignalNode#UNSET"),xE=F(g({},Gi),{transformFn:void 0,applyValueToInputSignal(t,n){nr(t,n)}});function AE(t,n){let e=Object.create(xE);e.value=t,e.transformFn=n?.transform;function r(){if(Mn(e),e.value===Ul){let o=null;throw new b(-950,o)}return e.value}return r[pe]=e,r}var Vl=class{attributeName;constructor(n){this.attributeName=n}__NG_ELEMENT_ID__=()=>Cs(this.attributeName);toString(){return`HostAttributeToken ${this.attributeName}`}},Zq=(()=>{let t=new y("");return t.__NG_ELEMENT_ID__=n=>{let e=Ee();if(e===null)throw new b(-204,!1);if(e.type&2)return e.value;if(n&8)return null;throw new b(-204,!1)},t})();function Kq(t){return new js}function _E(t,n){return AE(t,n)}function QR(t){return AE(Ul,t)}var RE=(_E.required=QR,_E);function DE(t,n){return jp(n)}function XR(t,n){return Vp(n)}var Qq=(DE.required=XR,DE);function Xq(t,n){return ND(n)}function EE(t,n){return jp(n)}function JR(t,n){return Vp(n)}var Jq=(EE.required=JR,EE);function NE(t,n){let e=Object.create(xE),r=new js;e.value=t;function o(){return Mn(e),wE(e.value),e.value}return o[pe]=e,o.asReadonly=us.bind(o),o.set=i=>{e.equal(e.value,i)||(nr(e,i),r.emit(i))},o.update=i=>{wE(e.value),o.set(i(e.value))},o.subscribe=r.subscribe.bind(r),o.destroyRef=r.destroyRef,o}function wE(t){if(t===Ul)throw new b(952,!1)}function CE(t,n){return NE(t,n)}function eN(t){return NE(Ul,t)}var e5=(CE.required=eN,CE);var nm=new y(""),tN=new y("");function Vs(t){return!t.moduleRef}function nN(t){let n=Vs(t)?t.r3Injector:t.moduleRef.injector,e=n.get(j);return e.run(()=>{Vs(t)?t.r3Injector.resolveInjectorInitializers():t.moduleRef.resolveInjectorInitializers();let r=n.get(ut),o;if(e.runOutsideAngular(()=>{o=e.onError.subscribe({next:r})}),Vs(t)){let i=()=>n.destroy(),s=t.platformInjector.get(nm);s.add(i),n.onDestroy(()=>{o.unsubscribe(),s.delete(i)})}else{let i=()=>t.moduleRef.destroy(),s=t.platformInjector.get(nm);s.add(i),t.moduleRef.onDestroy(()=>{ms(t.allPlatformModules,t.moduleRef),o.unsubscribe(),s.delete(i)})}return oN(r,e,()=>{let i=n.get(kn),s=i.add(),a=n.get($p);return a.runInitializers(),a.donePromise.then(()=>{let c=n.get(Ls,ks);if(ZD(c||ks),!n.get(tN,!0))return Vs(t)?n.get(Be):(t.allPlatformModules.push(t.moduleRef),t.moduleRef);if(Vs(t)){let u=n.get(Be);return t.rootComponent!==void 0&&u.bootstrap(t.rootComponent),u}else return rN?.(t.moduleRef,t.allPlatformModules),t.moduleRef}).finally(()=>{i.remove(s)})})})}var rN;function oN(t,n,e){try{let r=e();return jn(r)?r.catch(o=>{throw n.runOutsideAngular(()=>t(o)),o}):r}catch(r){throw n.runOutsideAngular(()=>t(r)),r}}var jl=null;function iN(t=[],n){return $.create({name:n,providers:[{provide:rs,useValue:"platform"},{provide:nm,useValue:new Set([()=>jl=null])},...t]})}function sN(t=[]){if(jl)return jl;let n=iN(t);return jl=n,HD(),aN(n),n}function aN(t){let n=t.get(dl,null);xe(t,()=>{n?.forEach(e=>e())})}var cN=1e4;var t5=cN-1e3;var St=(()=>{class t{static __NG_ELEMENT_ID__=lN}return t})();function lN(t){return uN(Ee(),C(),(t&16)===16)}function uN(t,n,e){if(hn(t)&&!e){let r=It(t.index,n);return new dr(r,r)}else if(t.type&175){let r=n[We];return new dr(r,n)}return null}var rm=class{supports(n){return kp(n)}create(n){return new om(n)}},dN=(t,n)=>n,om=class{length=0;collection;_linkedRecords=null;_unlinkedRecords=null;_previousItHead=null;_itHead=null;_itTail=null;_additionsHead=null;_additionsTail=null;_movesHead=null;_movesTail=null;_removalsHead=null;_removalsTail=null;_identityChangesHead=null;_identityChangesTail=null;_trackByFn;constructor(n){this._trackByFn=n||dN}forEachItem(n){let e;for(e=this._itHead;e!==null;e=e._next)n(e)}forEachOperation(n){let e=this._itHead,r=this._removalsHead,o=0,i=null;for(;e||r;){let s=!r||e&&e.currentIndex{s=this._trackByFn(o,a),e===null||!Object.is(e.trackById,s)?(e=this._mismatch(e,a,s,o),r=!0):(r&&(e=this._verifyReinsertion(e,a,s,o)),Object.is(e.item,a)||this._addIdentityChange(e,a)),e=e._next,o++}),this.length=o;return this._truncate(e),this.collection=n,this.isDirty}get isDirty(){return this._additionsHead!==null||this._movesHead!==null||this._removalsHead!==null||this._identityChangesHead!==null}_reset(){if(this.isDirty){let n;for(n=this._previousItHead=this._itHead;n!==null;n=n._next)n._nextPrevious=n._next;for(n=this._additionsHead;n!==null;n=n._nextAdded)n.previousIndex=n.currentIndex;for(this._additionsHead=this._additionsTail=null,n=this._movesHead;n!==null;n=n._nextMoved)n.previousIndex=n.currentIndex;this._movesHead=this._movesTail=null,this._removalsHead=this._removalsTail=null,this._identityChangesHead=this._identityChangesTail=null}}_mismatch(n,e,r,o){let i;return n===null?i=this._itTail:(i=n._prev,this._remove(n)),n=this._unlinkedRecords===null?null:this._unlinkedRecords.get(r,null),n!==null?(Object.is(n.item,e)||this._addIdentityChange(n,e),this._reinsertAfter(n,i,o)):(n=this._linkedRecords===null?null:this._linkedRecords.get(r,o),n!==null?(Object.is(n.item,e)||this._addIdentityChange(n,e),this._moveAfter(n,i,o)):n=this._addAfter(new im(e,r),i,o)),n}_verifyReinsertion(n,e,r,o){let i=this._unlinkedRecords===null?null:this._unlinkedRecords.get(r,null);return i!==null?n=this._reinsertAfter(i,n._prev,o):n.currentIndex!=o&&(n.currentIndex=o,this._addToMoves(n,o)),n}_truncate(n){for(;n!==null;){let e=n._next;this._addToRemovals(this._unlink(n)),n=e}this._unlinkedRecords!==null&&this._unlinkedRecords.clear(),this._additionsTail!==null&&(this._additionsTail._nextAdded=null),this._movesTail!==null&&(this._movesTail._nextMoved=null),this._itTail!==null&&(this._itTail._next=null),this._removalsTail!==null&&(this._removalsTail._nextRemoved=null),this._identityChangesTail!==null&&(this._identityChangesTail._nextIdentityChange=null)}_reinsertAfter(n,e,r){this._unlinkedRecords!==null&&this._unlinkedRecords.remove(n);let o=n._prevRemoved,i=n._nextRemoved;return o===null?this._removalsHead=i:o._nextRemoved=i,i===null?this._removalsTail=o:i._prevRemoved=o,this._insertAfter(n,e,r),this._addToMoves(n,r),n}_moveAfter(n,e,r){return this._unlink(n),this._insertAfter(n,e,r),this._addToMoves(n,r),n}_addAfter(n,e,r){return this._insertAfter(n,e,r),this._additionsTail===null?this._additionsTail=this._additionsHead=n:this._additionsTail=this._additionsTail._nextAdded=n,n}_insertAfter(n,e,r){let o=e===null?this._itHead:e._next;return n._next=o,n._prev=e,o===null?this._itTail=n:o._prev=n,e===null?this._itHead=n:e._next=n,this._linkedRecords===null&&(this._linkedRecords=new Bl),this._linkedRecords.put(n),n.currentIndex=r,n}_remove(n){return this._addToRemovals(this._unlink(n))}_unlink(n){this._linkedRecords!==null&&this._linkedRecords.remove(n);let e=n._prev,r=n._next;return e===null?this._itHead=r:e._next=r,r===null?this._itTail=e:r._prev=e,n}_addToMoves(n,e){return n.previousIndex===e||(this._movesTail===null?this._movesTail=this._movesHead=n:this._movesTail=this._movesTail._nextMoved=n),n}_addToRemovals(n){return this._unlinkedRecords===null&&(this._unlinkedRecords=new Bl),this._unlinkedRecords.put(n),n.currentIndex=null,n._nextRemoved=null,this._removalsTail===null?(this._removalsTail=this._removalsHead=n,n._prevRemoved=null):(n._prevRemoved=this._removalsTail,this._removalsTail=this._removalsTail._nextRemoved=n),n}_addIdentityChange(n,e){return n.item=e,this._identityChangesTail===null?this._identityChangesTail=this._identityChangesHead=n:this._identityChangesTail=this._identityChangesTail._nextIdentityChange=n,n}},im=class{item;trackById;currentIndex=null;previousIndex=null;_nextPrevious=null;_prev=null;_next=null;_prevDup=null;_nextDup=null;_prevRemoved=null;_nextRemoved=null;_nextAdded=null;_nextMoved=null;_nextIdentityChange=null;constructor(n,e){this.item=n,this.trackById=e}},sm=class{_head=null;_tail=null;add(n){this._head===null?(this._head=this._tail=n,n._nextDup=null,n._prevDup=null):(this._tail._nextDup=n,n._prevDup=this._tail,n._nextDup=null,this._tail=n)}get(n,e){let r;for(r=this._head;r!==null;r=r._nextDup)if((e===null||e<=r.currentIndex)&&Object.is(r.trackById,n))return r;return null}remove(n){let e=n._prevDup,r=n._nextDup;return e===null?this._head=r:e._nextDup=r,r===null?this._tail=e:r._prevDup=e,this._head===null}},Bl=class{map=new Map;put(n){let e=n.trackById,r=this.map.get(e);r||(r=new sm,this.map.set(e,r)),r.add(n)}get(n,e){let r=n,o=this.map.get(r);return o?o.get(n,e):null}remove(n){let e=n.trackById;return this.map.get(e).remove(n)&&this.map.delete(e),n}get isEmpty(){return this.map.size===0}clear(){this.map.clear()}};function IE(t,n,e){let r=t.previousIndex;if(r===null)return r;let o=0;return e&&r{if(e&&e.key===o)this._maybeAddToChanges(e,r),this._appendAfter=e,e=e._next;else{let i=this._getOrCreateRecordForKey(o,r);e=this._insertBeforeOrAppend(e,i)}}),e){e._prev&&(e._prev._next=null),this._removalsHead=e;for(let r=e;r!==null;r=r._nextRemoved)r===this._mapHead&&(this._mapHead=null),this._records.delete(r.key),r._nextRemoved=r._next,r.previousValue=r.currentValue,r.currentValue=null,r._prev=null,r._next=null}return this._changesTail&&(this._changesTail._nextChanged=null),this._additionsTail&&(this._additionsTail._nextAdded=null),this.isDirty}_insertBeforeOrAppend(n,e){if(n){let r=n._prev;return e._next=n,e._prev=r,n._prev=e,r&&(r._next=e),n===this._mapHead&&(this._mapHead=e),this._appendAfter=n,n}return this._appendAfter?(this._appendAfter._next=e,e._prev=this._appendAfter):this._mapHead=e,this._appendAfter=e,null}_getOrCreateRecordForKey(n,e){if(this._records.has(n)){let o=this._records.get(n);this._maybeAddToChanges(o,e);let i=o._prev,s=o._next;return i&&(i._next=s),s&&(s._prev=i),o._next=null,o._prev=null,o}let r=new lm(n);return this._records.set(n,r),r.currentValue=e,this._addToAdditions(r),r}_reset(){if(this.isDirty){let n;for(this._previousMapHead=this._mapHead,n=this._previousMapHead;n!==null;n=n._next)n._nextPrevious=n._next;for(n=this._changesHead;n!==null;n=n._nextChanged)n.previousValue=n.currentValue;for(n=this._additionsHead;n!=null;n=n._nextAdded)n.previousValue=n.currentValue;this._changesHead=this._changesTail=null,this._additionsHead=this._additionsTail=null,this._removalsHead=null}}_maybeAddToChanges(n,e){Object.is(e,n.currentValue)||(n.previousValue=n.currentValue,n.currentValue=e,this._addToChanges(n))}_addToAdditions(n){this._additionsHead===null?this._additionsHead=this._additionsTail=n:(this._additionsTail._nextAdded=n,this._additionsTail=n)}_addToChanges(n){this._changesHead===null?this._changesHead=this._changesTail=n:(this._changesTail._nextChanged=n,this._changesTail=n)}_forEach(n,e){n instanceof Map?n.forEach(e):Object.keys(n).forEach(r=>e(n[r],r))}},lm=class{key;previousValue=null;currentValue=null;_nextPrevious=null;_next=null;_prev=null;_nextAdded=null;_nextRemoved=null;_nextChanged=null;constructor(n){this.key=n}};function SE(){return new Hl([new rm])}var Hl=(()=>{class t{factories;static \u0275prov=v({token:t,providedIn:"root",factory:SE});constructor(e){this.factories=e}static create(e,r){if(r!=null){let o=r.factories.slice();e=e.concat(o)}return new t(e)}static extend(e){return{provide:t,useFactory:()=>{let r=f(t,{optional:!0,skipSelf:!0});return t.create(e,r||SE())}}}find(e){let r=this.factories.find(o=>o.supports(e));if(r!=null)return r;throw new b(901,!1)}}return t})();function ME(){return new fm([new am])}var fm=(()=>{class t{static \u0275prov=v({token:t,providedIn:"root",factory:ME});factories;constructor(e){this.factories=e}static create(e,r){if(r){let o=r.factories.slice();e=e.concat(o)}return new t(e)}static extend(e){return{provide:t,useFactory:()=>{let r=f(t,{optional:!0,skipSelf:!0});return t.create(e,r||ME())}}}find(e){let r=this.factories.find(o=>o.supports(e));if(r)return r;throw new b(901,!1)}}return t})();var OE=(()=>{class t{constructor(e){}static \u0275fac=function(r){return new(r||t)(w(Be))};static \u0275mod=X({type:t});static \u0275inj=Z({})}return t})();function kE(t){let{rootComponent:n,appProviders:e,platformProviders:r,platformRef:o}=t;oe(K.BootstrapApplicationStart);try{let i=o?.injector??sN(r),s=[Jp(),cb,...e||[]],a=new Ds({providers:s,parent:i,debugName:"",runEnvironmentInitializers:!1});return nN({r3Injector:a.injector,platformInjector:i,rootComponent:n})}catch(i){return Promise.reject(i)}finally{oe(K.BootstrapApplicationEnd)}}function ue(t){return typeof t=="boolean"?t:t!=null&&t!=="false"}function hm(t,n=NaN){return!isNaN(parseFloat(t))&&!isNaN(Number(t))?Number(t):n}var tm=Symbol("NOT_SET"),FE=new Set,fN=F(g({},Gi),{kind:"afterRenderEffectPhase",consumerIsAlwaysLive:!0,consumerAllowSignalWrites:!0,value:tm,cleanup:null,consumerMarkedDirty(){if(this.sequence.impl.executing){if(this.sequence.lastPhase===null||this.sequence.lastPhase(Mn(l),l.value),l.signal[pe]=l,l.registerCleanupFn=u=>(l.cleanup??=new Set).add(u),this.nodes[a]=l,this.hooks[a]=u=>l.phaseFn(u)}}afterRun(){super.afterRun(),this.lastPhase=null}destroy(){if(this.onDestroyFns!==null)for(let n of this.onDestroyFns)n();super.destroy();for(let n of this.nodes)if(n)try{for(let e of n.cleanup??FE)e()}finally{tr(n)}}};function n5(t,n){let e=n?.injector??f($),r=e.get(ln),o=e.get(yl),i=e.get(Wt,null,{optional:!0});o.impl??=e.get(Ep);let s=t;typeof s=="function"&&(s={mixedReadWrite:t});let a=e.get($o,null,{optional:!0}),c=new um(o.impl,[s.earlyRead,s.write,s.mixedReadWrite,s.read],a?.view,r,e,i?.snapshot(null));return o.impl.register(c),c}function $l(t,n){let e=un(t),r=n.elementInjector||Po();return new fr(e).create(r,n.projectableNodes,n.hostElement,n.environmentInjector,n.directives,n.bindings)}function PE(t){let n=un(t);if(!n)return null;let e=new fr(n);return{get selector(){return e.selector},get type(){return e.componentType},get inputs(){return e.inputs},get outputs(){return e.outputs},get ngContentSelectors(){return e.ngContentSelectors},get isStandalone(){return n.standalone},get isSignal(){return n.signals}}}var LE=null;function mt(){return LE}function pm(t){LE??=t}var Bs=class{},Bn=(()=>{class t{historyGo(e){throw new Error("")}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(jE),providedIn:"platform"})}return t})(),mm=new y(""),jE=(()=>{class t extends Bn{_location;_history;_doc=f(L);constructor(){super(),this._location=window.location,this._history=window.history}getBaseHrefFromDOM(){return mt().getBaseHref(this._doc)}onPopState(e){let r=mt().getGlobalEventTarget(this._doc,"window");return r.addEventListener("popstate",e,!1),()=>r.removeEventListener("popstate",e)}onHashChange(e){let r=mt().getGlobalEventTarget(this._doc,"window");return r.addEventListener("hashchange",e,!1),()=>r.removeEventListener("hashchange",e)}get href(){return this._location.href}get protocol(){return this._location.protocol}get hostname(){return this._location.hostname}get port(){return this._location.port}get pathname(){return this._location.pathname}get search(){return this._location.search}get hash(){return this._location.hash}set pathname(e){this._location.pathname=e}pushState(e,r,o){this._history.pushState(e,r,o)}replaceState(e,r,o){this._history.replaceState(e,r,o)}forward(){this._history.forward()}back(){this._history.back()}historyGo(e=0){this._history.go(e)}getState(){return this._history.state}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>new t,providedIn:"platform"})}return t})();function zl(t,n){return t?n?t.endsWith("/")?n.startsWith("/")?t+n.slice(1):t+n:n.startsWith("/")?t+n:`${t}/${n}`:t:n}function VE(t){let n=t.search(/#|\?|$/);return t[n-1]==="/"?t.slice(0,n-1)+t.slice(n):t}function Qt(t){return t&&t[0]!=="?"?`?${t}`:t}var Xt=(()=>{class t{historyGo(e){throw new Error("")}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(Wl),providedIn:"root"})}return t})(),Gl=new y(""),Wl=(()=>{class t extends Xt{_platformLocation;_baseHref;_removeListenerFns=[];constructor(e,r){super(),this._platformLocation=e,this._baseHref=r??this._platformLocation.getBaseHrefFromDOM()??f(L).location?.origin??""}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(e){this._removeListenerFns.push(this._platformLocation.onPopState(e),this._platformLocation.onHashChange(e))}getBaseHref(){return this._baseHref}prepareExternalUrl(e){return zl(this._baseHref,e)}path(e=!1){let r=this._platformLocation.pathname+Qt(this._platformLocation.search),o=this._platformLocation.hash;return o&&e?`${r}${o}`:r}pushState(e,r,o,i){let s=this.prepareExternalUrl(o+Qt(i));this._platformLocation.pushState(e,r,s)}replaceState(e,r,o,i){let s=this.prepareExternalUrl(o+Qt(i));this._platformLocation.replaceState(e,r,s)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(e=0){this._platformLocation.historyGo?.(e)}static \u0275fac=function(r){return new(r||t)(w(Bn),w(Gl,8))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var bn=(()=>{class t{_subject=new S;_basePath;_locationStrategy;_urlChangeListeners=[];_urlChangeSubscription=null;constructor(e){this._locationStrategy=e;let r=this._locationStrategy.getBaseHref();this._basePath=mN(VE(BE(r))),this._locationStrategy.onPopState(o=>{this._subject.next({url:this.path(!0),pop:!0,state:o.state,type:o.type})})}ngOnDestroy(){this._urlChangeSubscription?.unsubscribe(),this._urlChangeListeners=[]}path(e=!1){return this.normalize(this._locationStrategy.path(e))}getState(){return this._locationStrategy.getState()}isCurrentPathEqualTo(e,r=""){return this.path()==this.normalize(e+Qt(r))}normalize(e){return t.stripTrailingSlash(pN(this._basePath,BE(e)))}prepareExternalUrl(e){return e&&e[0]!=="/"&&(e="/"+e),this._locationStrategy.prepareExternalUrl(e)}go(e,r="",o=null){this._locationStrategy.pushState(o,"",e,r),this._notifyUrlChangeListeners(this.prepareExternalUrl(e+Qt(r)),o)}replaceState(e,r="",o=null){this._locationStrategy.replaceState(o,"",e,r),this._notifyUrlChangeListeners(this.prepareExternalUrl(e+Qt(r)),o)}forward(){this._locationStrategy.forward()}back(){this._locationStrategy.back()}historyGo(e=0){this._locationStrategy.historyGo?.(e)}onUrlChange(e){return this._urlChangeListeners.push(e),this._urlChangeSubscription??=this.subscribe(r=>{this._notifyUrlChangeListeners(r.url,r.state)}),()=>{let r=this._urlChangeListeners.indexOf(e);this._urlChangeListeners.splice(r,1),this._urlChangeListeners.length===0&&(this._urlChangeSubscription?.unsubscribe(),this._urlChangeSubscription=null)}}_notifyUrlChangeListeners(e="",r){this._urlChangeListeners.forEach(o=>o(e,r))}subscribe(e,r,o){return this._subject.subscribe({next:e,error:r??void 0,complete:o??void 0})}static normalizeQueryParams=Qt;static joinWithSlash=zl;static stripTrailingSlash=VE;static \u0275fac=function(r){return new(r||t)(w(Xt))};static \u0275prov=v({token:t,factory:()=>hN(),providedIn:"root"})}return t})();function hN(){return new bn(w(Xt))}function pN(t,n){if(!t||!n.startsWith(t))return n;let e=n.substring(t.length);return e===""||["/",";","?","#"].includes(e[0])?e:n}function BE(t){return t.replace(/\/index\.html$/,"")}function mN(t){if(new RegExp("^(https?:)?//").test(t)){let[,e]=t.split(/\/\/[^\/]+/);return e}return t}var Em=(()=>{class t extends Xt{_platformLocation;_baseHref="";_removeListenerFns=[];constructor(e,r){super(),this._platformLocation=e,r!=null&&(this._baseHref=r)}ngOnDestroy(){for(;this._removeListenerFns.length;)this._removeListenerFns.pop()()}onPopState(e){this._removeListenerFns.push(this._platformLocation.onPopState(e),this._platformLocation.onHashChange(e))}getBaseHref(){return this._baseHref}path(e=!1){let r=this._platformLocation.hash??"#";return r.length>0?r.substring(1):r}prepareExternalUrl(e){let r=zl(this._baseHref,e);return r.length>0?"#"+r:r}pushState(e,r,o,i){let s=this.prepareExternalUrl(o+Qt(i))||this._platformLocation.pathname;this._platformLocation.pushState(e,r,s)}replaceState(e,r,o,i){let s=this.prepareExternalUrl(o+Qt(i))||this._platformLocation.pathname;this._platformLocation.replaceState(e,r,s)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}getState(){return this._platformLocation.getState()}historyGo(e=0){this._platformLocation.historyGo?.(e)}static \u0275fac=function(r){return new(r||t)(w(Bn),w(Gl,8))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})();var Ye=(function(t){return t[t.Format=0]="Format",t[t.Standalone=1]="Standalone",t})(Ye||{}),ae=(function(t){return t[t.Narrow=0]="Narrow",t[t.Abbreviated=1]="Abbreviated",t[t.Wide=2]="Wide",t[t.Short=3]="Short",t})(ae||{}),it=(function(t){return t[t.Short=0]="Short",t[t.Medium=1]="Medium",t[t.Long=2]="Long",t[t.Full=3]="Full",t})(it||{}),Hn={Decimal:0,Group:1,List:2,PercentSign:3,PlusSign:4,MinusSign:5,Exponential:6,SuperscriptingExponent:7,PerMille:8,Infinity:9,NaN:10,TimeSeparator:11,CurrencyDecimal:12,CurrencyGroup:13};function GE(t){return pt(t)[_e.LocaleId]}function WE(t,n,e){let r=pt(t),o=[r[_e.DayPeriodsFormat],r[_e.DayPeriodsStandalone]],i=Mt(o,n);return Mt(i,e)}function qE(t,n,e){let r=pt(t),o=[r[_e.DaysFormat],r[_e.DaysStandalone]],i=Mt(o,n);return Mt(i,e)}function YE(t,n,e){let r=pt(t),o=[r[_e.MonthsFormat],r[_e.MonthsStandalone]],i=Mt(o,n);return Mt(i,e)}function ZE(t,n){let r=pt(t)[_e.Eras];return Mt(r,n)}function Us(t,n){let e=pt(t);return Mt(e[_e.DateFormat],n)}function Hs(t,n){let e=pt(t);return Mt(e[_e.TimeFormat],n)}function $s(t,n){let r=pt(t)[_e.DateTimeFormat];return Mt(r,n)}function zs(t,n){let e=pt(t),r=e[_e.NumberSymbols][n];if(typeof r>"u"){if(n===Hn.CurrencyDecimal)return e[_e.NumberSymbols][Hn.Decimal];if(n===Hn.CurrencyGroup)return e[_e.NumberSymbols][Hn.Group]}return r}function KE(t){if(!t[_e.ExtraData])throw new b(2303,!1)}function QE(t){let n=pt(t);return KE(n),(n[_e.ExtraData][2]||[]).map(r=>typeof r=="string"?gm(r):[gm(r[0]),gm(r[1])])}function XE(t,n,e){let r=pt(t);KE(r);let o=[r[_e.ExtraData][0],r[_e.ExtraData][1]],i=Mt(o,n)||[];return Mt(i,e)||[]}function Mt(t,n){for(let e=n;e>-1;e--)if(typeof t[e]<"u")return t[e];throw new b(2304,!1)}function gm(t){let[n,e]=t.split(":");return{hours:+n,minutes:+e}}var gN=/^(\d{4,})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/,ql={},vN=/((?:[^BEGHLMOSWYZabcdhmswyz']+)|(?:'(?:[^']|'')*')|(?:G{1,5}|y{1,4}|Y{1,4}|M{1,5}|L{1,5}|w{1,2}|W{1}|d{1,2}|E{1,6}|c{1,6}|a{1,5}|b{1,5}|B{1,5}|h{1,2}|H{1,2}|m{1,2}|s{1,2}|S{1,3}|z{1,4}|Z{1,5}|O{1,4}))([\s\S]*)/,yN=256;function JE(t,n,e,r){let o=TN(t);bN(n),n=Un(e,n)||n;let s=[],a;for(;n;)if(a=vN.exec(n),a){s=s.concat(a.slice(1));let u=s.pop();if(!u)break;n=u}else{s.push(n);break}let c=o.getTimezoneOffset();r&&(c=tw(r,c),o=MN(o,r));let l="";return s.forEach(u=>{let d=IN(u);l+=d?d(o,e,c):u==="''"?"'":u.replace(/(^'|'$)/g,"").replace(/''/g,"'")}),l}function bN(t){if(t.length>yN)throw new b(2300,!1)}function Xl(t,n,e){let r=new Date(0);return r.setFullYear(t,n,e),r.setHours(0,0,0),r}function Un(t,n){let e=GE(t);if(ql[e]??={},ql[e][n])return ql[e][n];let r="";switch(n){case"shortDate":r=Us(t,it.Short);break;case"mediumDate":r=Us(t,it.Medium);break;case"longDate":r=Us(t,it.Long);break;case"fullDate":r=Us(t,it.Full);break;case"shortTime":r=Hs(t,it.Short);break;case"mediumTime":r=Hs(t,it.Medium);break;case"longTime":r=Hs(t,it.Long);break;case"fullTime":r=Hs(t,it.Full);break;case"short":let o=Un(t,"shortTime"),i=Un(t,"shortDate");r=Yl($s(t,it.Short),[o,i]);break;case"medium":let s=Un(t,"mediumTime"),a=Un(t,"mediumDate");r=Yl($s(t,it.Medium),[s,a]);break;case"long":let c=Un(t,"longTime"),l=Un(t,"longDate");r=Yl($s(t,it.Long),[c,l]);break;case"full":let u=Un(t,"fullTime"),d=Un(t,"fullDate");r=Yl($s(t,it.Full),[u,d]);break}return r&&(ql[e][n]=r),r}function Yl(t,n){return n&&(t=t.replace(/\{([^}]+)}/g,function(e,r){return n!=null&&r in n?n[r]:e})),t}function Jt(t,n,e="-",r,o){let i="";(t<0||o&&t<=0)&&(o?t=-t+1:(t=-t,i=e));let s=String(t);for(;s.length0||a>-e)&&(a+=e),t===3)a===0&&e===-12&&(a=12);else if(t===6)return _N(a,n);let c=zs(s,Hn.MinusSign);return Jt(a,n,c,r,o)}}function DN(t,n){switch(t){case 0:return n.getFullYear();case 1:return n.getMonth();case 2:return n.getDate();case 3:return n.getHours();case 4:return n.getMinutes();case 5:return n.getSeconds();case 6:return n.getMilliseconds();case 7:return n.getDay();default:throw new b(2301,!1)}}function de(t,n,e=Ye.Format,r=!1){return function(o,i){return EN(o,i,t,n,e,r)}}function EN(t,n,e,r,o,i){switch(e){case 2:return YE(n,o,r)[t.getMonth()];case 1:return qE(n,o,r)[t.getDay()];case 0:let s=t.getHours(),a=t.getMinutes();if(i){let l=QE(n),u=XE(n,o,r),d=l.findIndex(h=>{if(Array.isArray(h)){let[p,m]=h,_=s>=p.hours&&a>=p.minutes,E=s0?Math.floor(o/60):Math.ceil(o/60);switch(t){case 0:return(o>=0?"+":"")+Jt(s,2,i)+Jt(Math.abs(o%60),2,i);case 1:return"GMT"+(o>=0?"+":"")+Jt(s,1,i);case 2:return"GMT"+(o>=0?"+":"")+Jt(s,2,i)+":"+Jt(Math.abs(o%60),2,i);case 3:return r===0?"Z":(o>=0?"+":"")+Jt(s,2,i)+":"+Jt(Math.abs(o%60),2,i);default:throw new b(2310,!1)}}}var wN=0,Ql=4;function CN(t){let n=Xl(t,wN,1).getDay();return Xl(t,0,1+(n<=Ql?Ql:Ql+7)-n)}function ew(t){let n=t.getDay(),e=n===0?-3:Ql-n;return Xl(t.getFullYear(),t.getMonth(),t.getDate()+e)}function vm(t,n=!1){return function(e,r){let o;if(n){let i=new Date(e.getFullYear(),e.getMonth(),1).getDay()-1,s=e.getDate();o=1+Math.floor((s+i)/7)}else{let i=ew(e),s=CN(i.getFullYear()),a=i.getTime()-s.getTime();o=1+Math.round(a/6048e5)}return Jt(o,t,zs(r,Hn.MinusSign))}}function Kl(t,n=!1){return function(e,r){let i=ew(e).getFullYear();return Jt(i,t,zs(r,Hn.MinusSign),n)}}var ym={};function IN(t){if(ym[t])return ym[t];let n;switch(t){case"G":case"GG":case"GGG":n=de(3,ae.Abbreviated);break;case"GGGG":n=de(3,ae.Wide);break;case"GGGGG":n=de(3,ae.Narrow);break;case"y":n=Ce(0,1,0,!1,!0);break;case"yy":n=Ce(0,2,0,!0,!0);break;case"yyy":n=Ce(0,3,0,!1,!0);break;case"yyyy":n=Ce(0,4,0,!1,!0);break;case"Y":n=Kl(1);break;case"YY":n=Kl(2,!0);break;case"YYY":n=Kl(3);break;case"YYYY":n=Kl(4);break;case"M":case"L":n=Ce(1,1,1);break;case"MM":case"LL":n=Ce(1,2,1);break;case"MMM":n=de(2,ae.Abbreviated);break;case"MMMM":n=de(2,ae.Wide);break;case"MMMMM":n=de(2,ae.Narrow);break;case"LLL":n=de(2,ae.Abbreviated,Ye.Standalone);break;case"LLLL":n=de(2,ae.Wide,Ye.Standalone);break;case"LLLLL":n=de(2,ae.Narrow,Ye.Standalone);break;case"w":n=vm(1);break;case"ww":n=vm(2);break;case"W":n=vm(1,!0);break;case"d":n=Ce(2,1);break;case"dd":n=Ce(2,2);break;case"c":case"cc":n=Ce(7,1);break;case"ccc":n=de(1,ae.Abbreviated,Ye.Standalone);break;case"cccc":n=de(1,ae.Wide,Ye.Standalone);break;case"ccccc":n=de(1,ae.Narrow,Ye.Standalone);break;case"cccccc":n=de(1,ae.Short,Ye.Standalone);break;case"E":case"EE":case"EEE":n=de(1,ae.Abbreviated);break;case"EEEE":n=de(1,ae.Wide);break;case"EEEEE":n=de(1,ae.Narrow);break;case"EEEEEE":n=de(1,ae.Short);break;case"a":case"aa":case"aaa":n=de(0,ae.Abbreviated);break;case"aaaa":n=de(0,ae.Wide);break;case"aaaaa":n=de(0,ae.Narrow);break;case"b":case"bb":case"bbb":n=de(0,ae.Abbreviated,Ye.Standalone,!0);break;case"bbbb":n=de(0,ae.Wide,Ye.Standalone,!0);break;case"bbbbb":n=de(0,ae.Narrow,Ye.Standalone,!0);break;case"B":case"BB":case"BBB":n=de(0,ae.Abbreviated,Ye.Format,!0);break;case"BBBB":n=de(0,ae.Wide,Ye.Format,!0);break;case"BBBBB":n=de(0,ae.Narrow,Ye.Format,!0);break;case"h":n=Ce(3,1,-12);break;case"hh":n=Ce(3,2,-12);break;case"H":n=Ce(3,1);break;case"HH":n=Ce(3,2);break;case"m":n=Ce(4,1);break;case"mm":n=Ce(4,2);break;case"s":n=Ce(5,1);break;case"ss":n=Ce(5,2);break;case"S":n=Ce(6,1);break;case"SS":n=Ce(6,2);break;case"SSS":n=Ce(6,3);break;case"Z":case"ZZ":case"ZZZ":n=Zl(0);break;case"ZZZZZ":n=Zl(3);break;case"O":case"OO":case"OOO":case"z":case"zz":case"zzz":n=Zl(1);break;case"OOOO":case"ZZZZ":case"zzzz":n=Zl(2);break;default:return null}return ym[t]=n,n}function tw(t,n){t=t.replace(/:/g,"");let e=Date.parse("Jan 01, 1970 00:00:00 "+t)/6e4;return isNaN(e)?n:e}function SN(t,n){return t=new Date(t.getTime()),t.setMinutes(t.getMinutes()+n),t}function MN(t,n,e){let o=t.getTimezoneOffset(),i=tw(n,o);return SN(t,-1*(i-o))}function TN(t){if(UE(t))return t;if(typeof t=="number"&&!isNaN(t))return new Date(t);if(typeof t=="string"){if(t=t.trim(),/^(\d{4}(-\d{1,2}(-\d{1,2})?)?)$/.test(t)){let[o,i=1,s=1]=t.split("-").map(a=>+a);return Xl(o,i-1,s)}let e=parseFloat(t);if(!isNaN(t-e))return new Date(e);let r;if(r=t.match(gN))return xN(r)}let n=new Date(t);if(!UE(n))throw new b(2311,!1);return n}function xN(t){let n=new Date(0),e=0,r=0,o=t[8]?n.setUTCFullYear:n.setFullYear,i=t[8]?n.setUTCHours:n.setHours;t[9]&&(e=Number(t[9]+t[10]),r=Number(t[9]+t[11])),o.call(n,Number(t[1]),Number(t[2])-1,Number(t[3]));let s=Number(t[4]||0)-e,a=Number(t[5]||0)-r,c=Number(t[6]||0),l=Math.floor(parseFloat("0."+(t[7]||0))*1e3);return i.call(n,s,a,c,l),n}function UE(t){return t instanceof Date&&!isNaN(t.valueOf())}var bm=/\s+/,HE=[],AN=(()=>{class t{_ngEl;_renderer;initialClasses=HE;rawClass;stateMap=new Map;constructor(e,r){this._ngEl=e,this._renderer=r}set klass(e){this.initialClasses=e!=null?e.trim().split(bm):HE}set ngClass(e){this.rawClass=typeof e=="string"?e.trim().split(bm):e}ngDoCheck(){for(let r of this.initialClasses)this._updateState(r,!0);let e=this.rawClass;if(Array.isArray(e)||e instanceof Set)for(let r of e)this._updateState(r,!0);else if(e!=null)for(let r of Object.keys(e))this._updateState(r,!!e[r]);this._applyStateDiff()}_updateState(e,r){let o=this.stateMap.get(e);o!==void 0?(o.enabled!==r&&(o.changed=!0,o.enabled=r),o.touched=!0):this.stateMap.set(e,{enabled:r,changed:!0,touched:!0})}_applyStateDiff(){for(let e of this.stateMap){let r=e[0],o=e[1];o.changed?(this._toggleClass(r,o.enabled),o.changed=!1):o.touched||(o.enabled&&this._toggleClass(r,!1),this.stateMap.delete(r)),o.touched=!1}}_toggleClass(e,r){e=e.trim(),e.length>0&&e.split(bm).forEach(o=>{r?this._renderer.addClass(this._ngEl.nativeElement,o):this._renderer.removeClass(this._ngEl.nativeElement,o)})}static \u0275fac=function(r){return new(r||t)(D(z),D(Oe))};static \u0275dir=M({type:t,selectors:[["","ngClass",""]],inputs:{klass:[0,"class","klass"],ngClass:"ngClass"}})}return t})();var Jl=class{$implicit;ngForOf;index;count;constructor(n,e,r,o){this.$implicit=n,this.ngForOf=e,this.index=r,this.count=o}get first(){return this.index===0}get last(){return this.index===this.count-1}get even(){return this.index%2===0}get odd(){return!this.even}},nw=(()=>{class t{_viewContainer;_template;_differs;set ngForOf(e){this._ngForOf=e,this._ngForOfDirty=!0}set ngForTrackBy(e){this._trackByFn=e}get ngForTrackBy(){return this._trackByFn}_ngForOf=null;_ngForOfDirty=!0;_differ=null;_trackByFn;constructor(e,r,o){this._viewContainer=e,this._template=r,this._differs=o}set ngForTemplate(e){e&&(this._template=e)}ngDoCheck(){if(this._ngForOfDirty){this._ngForOfDirty=!1;let e=this._ngForOf;!this._differ&&e&&(this._differ=this._differs.find(e).create(this.ngForTrackBy))}if(this._differ){let e=this._differ.diff(this._ngForOf);e&&this._applyChanges(e)}}_applyChanges(e){let r=this._viewContainer;e.forEachOperation((o,i,s)=>{if(o.previousIndex==null)r.createEmbeddedView(this._template,new Jl(o.item,this._ngForOf,-1,-1),s===null?void 0:s);else if(s==null)r.remove(i===null?void 0:i);else if(i!==null){let a=r.get(i);r.move(a,s),$E(a,o)}});for(let o=0,i=r.length;o{let i=r.get(o.currentIndex);$E(i,o)})}static ngTemplateContextGuard(e,r){return!0}static \u0275fac=function(r){return new(r||t)(D(qe),D(dt),D(Hl))};static \u0275dir=M({type:t,selectors:[["","ngFor","","ngForOf",""]],inputs:{ngForOf:"ngForOf",ngForTrackBy:"ngForTrackBy",ngForTemplate:"ngForTemplate"}})}return t})();function $E(t,n){t.context.$implicit=n.item}var RN=(()=>{class t{_viewContainer;_context=new eu;_thenTemplateRef=null;_elseTemplateRef=null;_thenViewRef=null;_elseViewRef=null;constructor(e,r){this._viewContainer=e,this._thenTemplateRef=r}set ngIf(e){this._context.$implicit=this._context.ngIf=e,this._updateView()}set ngIfThen(e){zE(e,!1),this._thenTemplateRef=e,this._thenViewRef=null,this._updateView()}set ngIfElse(e){zE(e,!1),this._elseTemplateRef=e,this._elseViewRef=null,this._updateView()}_updateView(){this._context.$implicit?this._thenViewRef||(this._viewContainer.clear(),this._elseViewRef=null,this._thenTemplateRef&&(this._thenViewRef=this._viewContainer.createEmbeddedView(this._thenTemplateRef,this._context))):this._elseViewRef||(this._viewContainer.clear(),this._thenViewRef=null,this._elseTemplateRef&&(this._elseViewRef=this._viewContainer.createEmbeddedView(this._elseTemplateRef,this._context)))}static ngIfUseIfTypeGuard;static ngTemplateGuard_ngIf;static ngTemplateContextGuard(e,r){return!0}static \u0275fac=function(r){return new(r||t)(D(qe),D(dt))};static \u0275dir=M({type:t,selectors:[["","ngIf",""]],inputs:{ngIf:"ngIf",ngIfThen:"ngIfThen",ngIfElse:"ngIfElse"}})}return t})(),eu=class{$implicit=null;ngIf=null};function zE(t,n){if(t&&!t.createEmbeddedView)throw new b(2020,!1)}var NN=(()=>{class t{_ngEl;_differs;_renderer;_ngStyle=null;_differ=null;constructor(e,r,o){this._ngEl=e,this._differs=r,this._renderer=o}set ngStyle(e){this._ngStyle=e,!this._differ&&e&&(this._differ=this._differs.find(e).create())}ngDoCheck(){if(this._differ){let e=this._differ.diff(this._ngStyle);e&&this._applyChanges(e)}}_setStyle(e,r){let[o,i]=e.split("."),s=o.indexOf("-")===-1?void 0:Gt.DashCase;r!=null?this._renderer.setStyle(this._ngEl.nativeElement,o,i?`${r}${i}`:r,s):this._renderer.removeStyle(this._ngEl.nativeElement,o,s)}_applyChanges(e){e.forEachRemovedItem(r=>this._setStyle(r.key,null)),e.forEachAddedItem(r=>this._setStyle(r.key,r.currentValue)),e.forEachChangedItem(r=>this._setStyle(r.key,r.currentValue))}static \u0275fac=function(r){return new(r||t)(D(z),D(fm),D(Oe))};static \u0275dir=M({type:t,selectors:[["","ngStyle",""]],inputs:{ngStyle:"ngStyle"}})}return t})(),ON=(()=>{class t{_viewContainerRef;_viewRef=null;ngTemplateOutletContext=null;ngTemplateOutlet=null;ngTemplateOutletInjector=null;injector=f($);constructor(e){this._viewContainerRef=e}ngOnChanges(e){if(this._shouldRecreateView(e)){let r=this._viewContainerRef;if(this._viewRef&&r.remove(r.indexOf(this._viewRef)),!this.ngTemplateOutlet){this._viewRef=null;return}let o=this._createContextForwardProxy();this._viewRef=r.createEmbeddedView(this.ngTemplateOutlet,o,{injector:this._getInjector()})}}_getInjector(){return this.ngTemplateOutletInjector==="outlet"?this.injector:this.ngTemplateOutletInjector??void 0}_shouldRecreateView(e){return!!e.ngTemplateOutlet||!!e.ngTemplateOutletInjector}_createContextForwardProxy(){return new Proxy({},{set:(e,r,o)=>this.ngTemplateOutletContext?Reflect.set(this.ngTemplateOutletContext,r,o):!1,get:(e,r,o)=>{if(this.ngTemplateOutletContext)return Reflect.get(this.ngTemplateOutletContext,r,o)}})}static \u0275fac=function(r){return new(r||t)(D(qe))};static \u0275dir=M({type:t,selectors:[["","ngTemplateOutlet",""]],inputs:{ngTemplateOutletContext:"ngTemplateOutletContext",ngTemplateOutlet:"ngTemplateOutlet",ngTemplateOutletInjector:"ngTemplateOutletInjector"},features:[Re]})}return t})();function wm(t,n){return new b(2100,!1)}var _m=class{createSubscription(n,e,r){return q(()=>n.subscribe({next:e,error:r}))}dispose(n){q(()=>n.unsubscribe())}},Dm=class{createSubscription(n,e,r){return n.then(o=>e?.(o),o=>r?.(o)),{unsubscribe:()=>{e=null,r=null}}}dispose(n){n.unsubscribe()}},kN=new Dm,FN=new _m,PN=(()=>{class t{_ref;_latestValue=null;markForCheckOnValueUpdate=!0;_subscription=null;_obj=null;_strategy=null;applicationErrorHandler=f(ut);constructor(e){this._ref=e}ngOnDestroy(){this._subscription&&this._dispose(),this._ref=null}transform(e){if(!this._obj){if(e)try{this.markForCheckOnValueUpdate=!1,this._subscribe(e)}finally{this.markForCheckOnValueUpdate=!0}return this._latestValue}return e!==this._obj?(this._dispose(),this.transform(e)):this._latestValue}_subscribe(e){this._obj=e,this._strategy=this._selectStrategy(e),this._subscription=this._strategy.createSubscription(e,r=>this._updateLatestValue(e,r),r=>this.applicationErrorHandler(r))}_selectStrategy(e){if(jn(e))return kN;if(Al(e))return FN;throw wm(t,e)}_dispose(){this._strategy.dispose(this._subscription),this._latestValue=null,this._subscription=null,this._obj=null}_updateLatestValue(e,r){e===this._obj&&(this._latestValue=r,this.markForCheckOnValueUpdate&&this._ref?.markForCheck())}static \u0275fac=function(r){return new(r||t)(D(St,16))};static \u0275pipe=Ns({name:"async",type:t,pure:!1})}return t})();var LN=/(?:[0-9A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF1C\uDF27\uDF30-\uDF45\uDF70-\uDF81\uDFB0-\uDFC4\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDEB8\uDF00-\uDF1A\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2\uDFB0]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE70-\uDEBE\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD4B]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])\S*/g,jN=(()=>{class t{transform(e){return e==null?null:(VN(t,e),e.replace(LN,r=>r[0].toUpperCase()+r.slice(1).toLowerCase()))}static \u0275fac=function(r){return new(r||t)};static \u0275pipe=Ns({name:"titlecase",type:t,pure:!0})}return t})();function VN(t,n){if(typeof n!="string")throw wm(t,n)}var BN="mediumDate",rw=new y(""),ow=new y(""),UN=(()=>{class t{locale;defaultTimezone;defaultOptions;constructor(e,r,o){this.locale=e,this.defaultTimezone=r,this.defaultOptions=o}transform(e,r,o,i){if(e==null||e===""||e!==e)return null;try{let s=r??this.defaultOptions?.dateFormat??BN,a=o??this.defaultOptions?.timezone??this.defaultTimezone??void 0;return JE(e,s,i||this.locale,a)}catch(s){throw wm(t,s.message)}}static \u0275fac=function(r){return new(r||t)(D(Ls,16),D(rw,24),D(ow,24))};static \u0275pipe=Ns({name:"date",type:t,pure:!0})}return t})();var Cm=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({})}return t})();function Gs(t,n){n=encodeURIComponent(n);for(let e of t.split(";")){let r=e.indexOf("="),[o,i]=r==-1?[e,""]:[e.slice(0,r),e.slice(r+1)];if(o.trim()===n)return decodeURIComponent(i)}return null}var oo=class{};var Sm="browser";function iw(t){return t===Sm}var Mm=(()=>{class t{static \u0275prov=v({token:t,providedIn:"root",factory:()=>new Im(f(L),window)})}return t})(),Im=class{document;window;offset=()=>[0,0];constructor(n,e){this.document=n,this.window=e}setOffset(n){Array.isArray(n)?this.offset=()=>n:this.offset=n}getScrollPosition(){return[this.window.scrollX,this.window.scrollY]}scrollToPosition(n,e){this.window.scrollTo(F(g({},e),{left:n[0],top:n[1]}))}scrollToAnchor(n,e){let r=HN(this.document,n);r&&(this.scrollToElement(r,e),r.focus({preventScroll:!0}))}setHistoryScrollRestoration(n){try{this.window.history.scrollRestoration=n}catch{console.warn(Dt(2400,!1))}}scrollToElement(n,e){let r=n.getBoundingClientRect(),o=r.left+this.window.pageXOffset,i=r.top+this.window.pageYOffset,s=this.offset();this.window.scrollTo(F(g({},e),{left:o-s[0],top:i-s[1]}))}};function HN(t,n){let e=t.getElementById(n)||t.getElementsByName(n)[0];if(e)return e;if(typeof t.createTreeWalker=="function"&&t.body&&typeof t.body.attachShadow=="function"){let r=t.createTreeWalker(t.body,NodeFilter.SHOW_ELEMENT),o=r.currentNode;for(;o;){let i=o.shadowRoot;if(i){let s=i.getElementById(n)||i.querySelector(`[name="${n}"]`);if(s)return s}o=r.nextNode()}}return null}var Ws=class{_doc;constructor(n){this._doc=n}manager},tu=(()=>{class t extends Ws{constructor(e){super(e)}supports(e){return!0}addEventListener(e,r,o,i){return e.addEventListener(r,o,i),()=>this.removeEventListener(e,r,o,i)}removeEventListener(e,r,o,i){return e.removeEventListener(r,o,i)}static \u0275fac=function(r){return new(r||t)(w(L))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),ou=new y(""),Rm=(()=>{class t{_zone;_plugins;_eventNameToPlugin=new Map;constructor(e,r){this._zone=r,e.forEach(s=>{s.manager=this});let o=e.filter(s=>!(s instanceof tu));this._plugins=o.slice().reverse();let i=e.find(s=>s instanceof tu);i&&this._plugins.push(i)}addEventListener(e,r,o,i){return this._findPluginFor(r).addEventListener(e,r,o,i)}getZone(){return this._zone}_findPluginFor(e){let r=this._eventNameToPlugin.get(e);if(r)return r;if(r=this._plugins.find(i=>i.supports(e)),!r)throw new b(5101,!1);return this._eventNameToPlugin.set(e,r),r}static \u0275fac=function(r){return new(r||t)(w(ou),w(j))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),Tm="ng-app-id";function sw(t){for(let n of t)n.remove()}function aw(t,n){let e=n.createElement("style");return e.textContent=t,e}function zN(t,n,e,r){let o=t.head?.querySelectorAll(`style[${Tm}="${n}"],link[${Tm}="${n}"]`);if(o)for(let i of o)i.removeAttribute(Tm),i instanceof HTMLLinkElement?r.set(i.href.slice(i.href.lastIndexOf("/")+1),{usage:0,elements:[i]}):i.textContent&&e.set(i.textContent,{usage:0,elements:[i]})}function Am(t,n){let e=n.createElement("link");return e.setAttribute("rel","stylesheet"),e.setAttribute("href",t),e}var Nm=(()=>{class t{doc;appId;nonce;inline=new Map;external=new Map;hosts=new Set;constructor(e,r,o,i={}){this.doc=e,this.appId=r,this.nonce=o,zN(e,r,this.inline,this.external),this.hosts.add(e.head)}addStyles(e,r){for(let o of e)this.addUsage(o,this.inline,aw);r?.forEach(o=>this.addUsage(o,this.external,Am))}removeStyles(e,r){for(let o of e)this.removeUsage(o,this.inline);r?.forEach(o=>this.removeUsage(o,this.external))}addUsage(e,r,o){let i=r.get(e);i?i.usage++:r.set(e,{usage:1,elements:[...this.hosts].map(s=>this.addElement(s,o(e,this.doc)))})}removeUsage(e,r){let o=r.get(e);o&&(o.usage--,o.usage<=0&&(sw(o.elements),r.delete(e)))}ngOnDestroy(){for(let[,{elements:e}]of[...this.inline,...this.external])sw(e);this.hosts.clear()}addHost(e){this.hosts.add(e);for(let[r,{elements:o}]of this.inline)o.push(this.addElement(e,aw(r,this.doc)));for(let[r,{elements:o}]of this.external)o.push(this.addElement(e,Am(r,this.doc)))}removeHost(e){this.hosts.delete(e)}addElement(e,r){return this.nonce&&r.setAttribute("nonce",this.nonce),e.appendChild(r)}static \u0275fac=function(r){return new(r||t)(w(L),w(hr),w(eo,8),w(Jr))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),xm={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/",math:"http://www.w3.org/1998/Math/MathML"},Om=/%COMP%/g;var lw="%COMP%",GN=`_nghost-${lw}`,WN=`_ngcontent-${lw}`,qN=!0,YN=new y("",{factory:()=>qN});function ZN(t){return WN.replace(Om,t)}function KN(t){return GN.replace(Om,t)}function uw(t,n){return n.map(e=>e.replace(Om,t))}var km=(()=>{class t{eventManager;sharedStylesHost;appId;removeStylesOnCompDestroy;doc;ngZone;nonce;tracingService;rendererByCompId=new Map;defaultRenderer;constructor(e,r,o,i,s,a,c=null,l=null){this.eventManager=e,this.sharedStylesHost=r,this.appId=o,this.removeStylesOnCompDestroy=i,this.doc=s,this.ngZone=a,this.nonce=c,this.tracingService=l,this.defaultRenderer=new qs(e,s,a,this.tracingService)}createRenderer(e,r){if(!e||!r)return this.defaultRenderer;let o=this.getOrCreateRenderer(e,r);return o instanceof ru?o.applyToHost(e):o instanceof Ys&&o.applyStyles(),o}getOrCreateRenderer(e,r){let o=this.rendererByCompId,i=o.get(r.id);if(!i){let s=this.doc,a=this.ngZone,c=this.eventManager,l=this.sharedStylesHost,u=this.removeStylesOnCompDestroy,d=this.tracingService;switch(r.encapsulation){case zt.Emulated:i=new ru(c,l,r,this.appId,u,s,a,d);break;case zt.ShadowDom:return new nu(c,e,r,s,a,this.nonce,d,l);case zt.ExperimentalIsolatedShadowDom:return new nu(c,e,r,s,a,this.nonce,d);default:i=new Ys(c,l,r,u,s,a,d);break}o.set(r.id,i)}return i}ngOnDestroy(){this.rendererByCompId.clear()}componentReplaced(e){this.rendererByCompId.delete(e)}static \u0275fac=function(r){return new(r||t)(w(Rm),w(Nm),w(hr),w(YN),w(L),w(j),w(eo),w(Wt,8))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),qs=class{eventManager;doc;ngZone;tracingService;data=Object.create(null);throwOnSyntheticProps=!0;constructor(n,e,r,o){this.eventManager=n,this.doc=e,this.ngZone=r,this.tracingService=o}destroy(){}destroyNode=null;createElement(n,e){return e?this.doc.createElementNS(xm[e]||e,n):this.doc.createElement(n)}createComment(n){return this.doc.createComment(n)}createText(n){return this.doc.createTextNode(n)}appendChild(n,e){(cw(n)?n.content:n).appendChild(e)}insertBefore(n,e,r){n&&(cw(n)?n.content:n).insertBefore(e,r)}removeChild(n,e){e.remove()}selectRootElement(n,e){let r=typeof n=="string"?this.doc.querySelector(n):n;if(!r)throw new b(-5104,!1);return e||(r.textContent=""),r}parentNode(n){return n.parentNode}nextSibling(n){return n.nextSibling}setAttribute(n,e,r,o){if(o){e=o+":"+e;let i=xm[o];i?n.setAttributeNS(i,e,r):n.setAttribute(e,r)}else n.setAttribute(e,r)}removeAttribute(n,e,r){if(r){let o=xm[r];o?n.removeAttributeNS(o,e):n.removeAttribute(`${r}:${e}`)}else n.removeAttribute(e)}addClass(n,e){n.classList.add(e)}removeClass(n,e){n.classList.remove(e)}setStyle(n,e,r,o){o&(Gt.DashCase|Gt.Important)?n.style.setProperty(e,r,o&Gt.Important?"important":""):n.style[e]=r}removeStyle(n,e,r){r&Gt.DashCase?n.style.removeProperty(e):n.style[e]=""}setProperty(n,e,r){n!=null&&(n[e]=r)}setValue(n,e){n.nodeValue=e}listen(n,e,r,o){if(typeof n=="string"&&(n=mt().getGlobalEventTarget(this.doc,n),!n))throw new b(5102,!1);let i=this.decoratePreventDefault(r);return this.tracingService?.wrapEventListener&&(i=this.tracingService.wrapEventListener(n,e,i)),this.eventManager.addEventListener(n,e,i,o)}decoratePreventDefault(n){return e=>{if(e==="__ngUnwrap__")return n;n(e)===!1&&e.preventDefault()}}};function cw(t){return t.tagName==="TEMPLATE"&&t.content!==void 0}var nu=class extends qs{hostEl;sharedStylesHost;shadowRoot;constructor(n,e,r,o,i,s,a,c){super(n,o,i,a),this.hostEl=e,this.sharedStylesHost=c,this.shadowRoot=e.attachShadow({mode:"open"}),this.sharedStylesHost&&this.sharedStylesHost.addHost(this.shadowRoot);let l=r.styles;l=uw(r.id,l);for(let d of l){let h=document.createElement("style");s&&h.setAttribute("nonce",s),h.textContent=d,this.shadowRoot.appendChild(h)}let u=r.getExternalStyles?.();if(u)for(let d of u){let h=Am(d,o);s&&h.setAttribute("nonce",s),this.shadowRoot.appendChild(h)}}nodeOrShadowRoot(n){return n===this.hostEl?this.shadowRoot:n}appendChild(n,e){return super.appendChild(this.nodeOrShadowRoot(n),e)}insertBefore(n,e,r){return super.insertBefore(this.nodeOrShadowRoot(n),e,r)}removeChild(n,e){return super.removeChild(null,e)}parentNode(n){return this.nodeOrShadowRoot(super.parentNode(this.nodeOrShadowRoot(n)))}destroy(){this.sharedStylesHost&&this.sharedStylesHost.removeHost(this.shadowRoot)}},Ys=class extends qs{sharedStylesHost;removeStylesOnCompDestroy;styles;styleUrls;constructor(n,e,r,o,i,s,a,c){super(n,i,s,a),this.sharedStylesHost=e,this.removeStylesOnCompDestroy=o;let l=r.styles;this.styles=c?uw(c,l):l,this.styleUrls=r.getExternalStyles?.(c)}applyStyles(){this.sharedStylesHost.addStyles(this.styles,this.styleUrls)}destroy(){this.removeStylesOnCompDestroy&&Qr.size===0&&this.sharedStylesHost.removeStyles(this.styles,this.styleUrls)}},ru=class extends Ys{contentAttr;hostAttr;constructor(n,e,r,o,i,s,a,c){let l=o+"-"+r.id;super(n,e,r,i,s,a,c,l),this.contentAttr=ZN(l),this.hostAttr=KN(l)}applyToHost(n){this.applyStyles(),this.setAttribute(n,this.hostAttr,"")}createElement(n,e){let r=super.createElement(n,e);return super.setAttribute(r,this.contentAttr,""),r}};var iu=class t extends Bs{supportsDOMEvents=!0;static makeCurrent(){pm(new t)}onAndCancel(n,e,r,o){return n.addEventListener(e,r,o),()=>{n.removeEventListener(e,r,o)}}dispatchEvent(n,e){n.dispatchEvent(e)}remove(n){n.remove()}createElement(n,e){return e=e||this.getDefaultDocument(),e.createElement(n)}createHtmlDocument(){return document.implementation.createHTMLDocument("fakeTitle")}getDefaultDocument(){return document}isElementNode(n){return n.nodeType===Node.ELEMENT_NODE}isShadowRoot(n){return n instanceof DocumentFragment}getGlobalEventTarget(n,e){return e==="window"?window:e==="document"?n:e==="body"?n.body:null}getBaseHref(n){let e=QN();return e==null?null:XN(e)}resetBaseElement(){Zs=null}getUserAgent(){return window.navigator.userAgent}getCookie(n){return Gs(document.cookie,n)}},Zs=null;function QN(){return Zs=Zs||document.head.querySelector("base"),Zs?Zs.getAttribute("href"):null}function XN(t){return new URL(t,document.baseURI).pathname}var su=class{addToWindow(n){ye.getAngularTestability=(r,o=!0)=>{let i=n.findTestabilityInTree(r,o);if(i==null)throw new b(5103,!1);return i},ye.getAllAngularTestabilities=()=>n.getAllTestabilities(),ye.getAllAngularRootElements=()=>n.getAllRootElements();let e=r=>{let o=ye.getAllAngularTestabilities(),i=o.length,s=function(){i--,i==0&&r()};o.forEach(a=>{a.whenStable(s)})};ye.frameworkStabilizers||(ye.frameworkStabilizers=[]),ye.frameworkStabilizers.push(e)}findTestabilityInTree(n,e,r){if(e==null)return null;let o=n.getTestability(e);return o??(r?mt().isShadowRoot(e)?this.findTestabilityInTree(n,e.host,!0):this.findTestabilityInTree(n,e.parentElement,!0):null)}},JN=(()=>{class t{build(){return new XMLHttpRequest}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),dw=["alt","control","meta","shift"],eO={"\b":"Backspace"," ":"Tab","\x7F":"Delete","\x1B":"Escape",Del:"Delete",Esc:"Escape",Left:"ArrowLeft",Right:"ArrowRight",Up:"ArrowUp",Down:"ArrowDown",Menu:"ContextMenu",Scroll:"ScrollLock",Win:"OS"},tO={alt:t=>t.altKey,control:t=>t.ctrlKey,meta:t=>t.metaKey,shift:t=>t.shiftKey},fw=(()=>{class t extends Ws{constructor(e){super(e)}supports(e){return t.parseEventName(e)!=null}addEventListener(e,r,o,i){let s=t.parseEventName(r),a=t.eventCallback(s.fullKey,o,this.manager.getZone());return this.manager.getZone().runOutsideAngular(()=>mt().onAndCancel(e,s.domEventName,a,i))}static parseEventName(e){let r=e.toLowerCase().split("."),o=r.shift();if(r.length===0||!(o==="keydown"||o==="keyup"))return null;let i=t._normalizeKey(r.pop()),s="",a=r.indexOf("code");if(a>-1&&(r.splice(a,1),s="code."),dw.forEach(l=>{let u=r.indexOf(l);u>-1&&(r.splice(u,1),s+=l+".")}),s+=i,r.length!=0||i.length===0)return null;let c={};return c.domEventName=o,c.fullKey=s,c}static matchEventFullKeyCode(e,r){let o=eO[e.key]||e.key,i="";return r.indexOf("code.")>-1&&(o=e.code,i="code."),o==null||!o?!1:(o=o.toLowerCase(),o===" "?o="space":o==="."&&(o="dot"),dw.forEach(s=>{if(s!==o){let a=tO[s];a(e)&&(i+=s+".")}}),i+=o,i===r)}static eventCallback(e,r,o){return i=>{t.matchEventFullKeyCode(i,e)&&o.runGuarded(()=>r(i))}}static _normalizeKey(e){return e==="esc"?"escape":e}static \u0275fac=function(r){return new(r||t)(w(L))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})();async function nO(t,n,e){let r=g({rootComponent:t},rO(n,e));return kE(r)}function rO(t,n){return{platformRef:n?.platformRef,appProviders:[...hw,...t?.providers??[]],platformProviders:aO}}function oO(){iu.makeCurrent()}function iO(){return new _t}function sO(){return ip(document),document}var aO=[{provide:Jr,useValue:Sm},{provide:dl,useValue:oO,multi:!0},{provide:L,useFactory:sO}];var cO=[{provide:xl,useClass:su},{provide:Tl,useClass:Os},{provide:Os,useClass:Os}],hw=[{provide:rs,useValue:"root"},{provide:_t,useFactory:iO},{provide:ou,useClass:tu,multi:!0},{provide:ou,useClass:fw,multi:!0},km,Nm,Rm,{provide:je,useExisting:km},{provide:oo,useClass:JN},[]],lO=(()=>{class t{constructor(){}static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({providers:[...hw,...cO],imports:[Cm,OE]})}return t})();var _n=class t{headers;normalizedNames=new Map;lazyInit;lazyUpdate=null;constructor(n){n?typeof n=="string"?this.lazyInit=()=>{this.headers=new Map,n.split(` -`).forEach(e=>{let r=e.indexOf(":");if(r>0){let o=e.slice(0,r),i=e.slice(r+1).trim();this.addHeaderEntry(o,i)}})}:typeof Headers<"u"&&n instanceof Headers?(this.headers=new Map,n.forEach((e,r)=>{this.addHeaderEntry(r,e)})):this.lazyInit=()=>{this.headers=new Map,Object.entries(n).forEach(([e,r])=>{this.setHeaderEntries(e,r)})}:this.headers=new Map}has(n){return this.init(),this.headers.has(n.toLowerCase())}get(n){this.init();let e=this.headers.get(n.toLowerCase());return e&&e.length>0?e[0]:null}keys(){return this.init(),Array.from(this.normalizedNames.values())}getAll(n){return this.init(),this.headers.get(n.toLowerCase())||null}append(n,e){return this.clone({name:n,value:e,op:"a"})}set(n,e){return this.clone({name:n,value:e,op:"s"})}delete(n,e){return this.clone({name:n,value:e,op:"d"})}maybeSetNormalizedName(n,e){this.normalizedNames.has(e)||this.normalizedNames.set(e,n)}init(){this.lazyInit&&(this.lazyInit instanceof t?this.copyFrom(this.lazyInit):this.lazyInit(),this.lazyInit=null,this.lazyUpdate&&(this.lazyUpdate.forEach(n=>this.applyUpdate(n)),this.lazyUpdate=null))}copyFrom(n){n.init(),Array.from(n.headers.keys()).forEach(e=>{this.headers.set(e,n.headers.get(e)),this.normalizedNames.set(e,n.normalizedNames.get(e))})}clone(n){let e=new t;return e.lazyInit=this.lazyInit&&this.lazyInit instanceof t?this.lazyInit:this,e.lazyUpdate=(this.lazyUpdate||[]).concat([n]),e}applyUpdate(n){let e=n.name.toLowerCase();switch(n.op){case"a":case"s":let r=n.value;if(typeof r=="string"&&(r=[r]),r.length===0)return;this.maybeSetNormalizedName(n.name,e);let o=(n.op==="a"?this.headers.get(e):void 0)||[];o.push(...r),this.headers.set(e,o);break;case"d":let i=n.value;if(!i)this.headers.delete(e),this.normalizedNames.delete(e);else{let s=this.headers.get(e);if(!s)return;s=s.filter(a=>i.indexOf(a)===-1),s.length===0?(this.headers.delete(e),this.normalizedNames.delete(e)):this.headers.set(e,s)}break}}addHeaderEntry(n,e){let r=n.toLowerCase();this.maybeSetNormalizedName(n,r),this.headers.has(r)?this.headers.get(r).push(e):this.headers.set(r,[e])}setHeaderEntries(n,e){let r=(Array.isArray(e)?e:[e]).map(i=>i.toString()),o=n.toLowerCase();this.headers.set(o,r),this.maybeSetNormalizedName(n,o)}forEach(n){this.init(),Array.from(this.normalizedNames.keys()).forEach(e=>n(this.normalizedNames.get(e),this.headers.get(e)))}};var cu=class{map=new Map;set(n,e){return this.map.set(n,e),this}get(n){return this.map.has(n)||this.map.set(n,n.defaultValue()),this.map.get(n)}delete(n){return this.map.delete(n),this}has(n){return this.map.has(n)}keys(){return this.map.keys()}},lu=class{encodeKey(n){return pw(n)}encodeValue(n){return pw(n)}decodeKey(n){return decodeURIComponent(n)}decodeValue(n){return decodeURIComponent(n)}};function uO(t,n){let e=new Map;return t.length>0&&t.replace(/^\?/,"").split("&").forEach(o=>{let i=o.indexOf("="),[s,a]=i==-1?[n.decodeKey(o),""]:[n.decodeKey(o.slice(0,i)),n.decodeValue(o.slice(i+1))],c=e.get(s)||[];c.push(a),e.set(s,c)}),e}var dO=/%(\d[a-f0-9])/gi,fO={40:"@","3A":":",24:"$","2C":",","3B":";","3D":"=","3F":"?","2F":"/"};function pw(t){return encodeURIComponent(t).replace(dO,(n,e)=>fO[e]??n)}function au(t){return`${t}`}var $n=class t{map;encoder;updates=null;cloneFrom=null;constructor(n={}){if(this.encoder=n.encoder||new lu,n.fromString){if(n.fromObject)throw new b(2805,!1);this.map=uO(n.fromString,this.encoder)}else n.fromObject?(this.map=new Map,Object.keys(n.fromObject).forEach(e=>{let r=n.fromObject[e],o=Array.isArray(r)?r.map(au):[au(r)];this.map.set(e,o)})):this.map=null}has(n){return this.init(),this.map.has(n)}get(n){this.init();let e=this.map.get(n);return e?e[0]:null}getAll(n){return this.init(),this.map.get(n)||null}keys(){return this.init(),Array.from(this.map.keys())}append(n,e){return this.clone({param:n,value:e,op:"a"})}appendAll(n){let e=[];return Object.keys(n).forEach(r=>{let o=n[r];Array.isArray(o)?o.forEach(i=>{e.push({param:r,value:i,op:"a"})}):e.push({param:r,value:o,op:"a"})}),this.clone(e)}set(n,e){return this.clone({param:n,value:e,op:"s"})}delete(n,e){return this.clone({param:n,value:e,op:"d"})}toString(){return this.init(),this.keys().map(n=>{let e=this.encoder.encodeKey(n);return this.map.get(n).map(r=>e+"="+this.encoder.encodeValue(r)).join("&")}).filter(n=>n!=="").join("&")}clone(n){let e=new t({encoder:this.encoder});return e.cloneFrom=this.cloneFrom||this,e.updates=(this.updates||[]).concat(n),e}init(){this.map===null&&(this.map=new Map),this.cloneFrom!==null&&(this.cloneFrom.init(),this.cloneFrom.keys().forEach(n=>this.map.set(n,this.cloneFrom.map.get(n))),this.updates.forEach(n=>{switch(n.op){case"a":case"s":let e=(n.op==="a"?this.map.get(n.param):void 0)||[];e.push(au(n.value)),this.map.set(n.param,e);break;case"d":if(n.value!==void 0){let r=this.map.get(n.param)||[],o=r.indexOf(au(n.value));o!==-1&&r.splice(o,1),r.length>0?this.map.set(n.param,r):this.map.delete(n.param)}else{this.map.delete(n.param);break}}}),this.cloneFrom=this.updates=null)}};function hO(t){switch(t){case"DELETE":case"GET":case"HEAD":case"OPTIONS":case"JSONP":return!1;default:return!0}}function mw(t){return typeof ArrayBuffer<"u"&&t instanceof ArrayBuffer}function gw(t){return typeof Blob<"u"&&t instanceof Blob}function vw(t){return typeof FormData<"u"&&t instanceof FormData}function pO(t){return typeof URLSearchParams<"u"&&t instanceof URLSearchParams}var yw="Content-Type",bw="Accept",Dw="text/plain",Ew="application/json",mO=`${Ew}, ${Dw}, */*`,oi=class t{url;body=null;headers;context;reportProgress=!1;withCredentials=!1;credentials;keepalive=!1;cache;priority;mode;redirect;referrer;integrity;referrerPolicy;responseType="json";method;params;urlWithParams;transferCache;timeout;constructor(n,e,r,o){this.url=e,this.method=n.toUpperCase();let i;if(hO(this.method)||o?(this.body=r!==void 0?r:null,i=o):i=r,i){if(this.reportProgress=!!i.reportProgress,this.withCredentials=!!i.withCredentials,this.keepalive=!!i.keepalive,i.responseType&&(this.responseType=i.responseType),i.headers&&(this.headers=i.headers),i.context&&(this.context=i.context),i.params&&(this.params=i.params),i.priority&&(this.priority=i.priority),i.cache&&(this.cache=i.cache),i.credentials&&(this.credentials=i.credentials),typeof i.timeout=="number"){if(i.timeout<1||!Number.isInteger(i.timeout))throw new b(2822,"");this.timeout=i.timeout}i.mode&&(this.mode=i.mode),i.redirect&&(this.redirect=i.redirect),i.integrity&&(this.integrity=i.integrity),i.referrer!==void 0&&(this.referrer=i.referrer),i.referrerPolicy&&(this.referrerPolicy=i.referrerPolicy),this.transferCache=i.transferCache}if(this.headers??=new _n,this.context??=new cu,!this.params)this.params=new $n,this.urlWithParams=e;else{let s=this.params.toString();if(s.length===0)this.urlWithParams=e;else{let a=e.indexOf("?"),c=a===-1?"?":aOi.set(Er,n.setHeaders[Er]),Pe)),n.setParams&&(Le=Object.keys(n.setParams).reduce((Oi,Er)=>Oi.set(Er,n.setParams[Er]),Le)),new t(e,r,E,{params:Le,headers:Pe,context:Ni,reportProgress:ee,responseType:o,withCredentials:I,transferCache:m,keepalive:i,cache:a,priority:s,timeout:_,mode:c,redirect:l,credentials:u,referrer:d,integrity:h,referrerPolicy:p})}},io=(function(t){return t[t.Sent=0]="Sent",t[t.UploadProgress=1]="UploadProgress",t[t.ResponseHeader=2]="ResponseHeader",t[t.DownloadProgress=3]="DownloadProgress",t[t.Response=4]="Response",t[t.User=5]="User",t})(io||{}),si=class{headers;status;statusText;url;ok;type;redirected;responseType;constructor(n,e=200,r="OK"){this.headers=n.headers||new _n,this.status=n.status!==void 0?n.status:e,this.statusText=n.statusText||r,this.url=n.url||null,this.redirected=n.redirected,this.responseType=n.responseType,this.ok=this.status>=200&&this.status<300}},uu=class t extends si{constructor(n={}){super(n)}type=io.ResponseHeader;clone(n={}){return new t({headers:n.headers||this.headers,status:n.status!==void 0?n.status:this.status,statusText:n.statusText||this.statusText,url:n.url||this.url||void 0})}},Ks=class t extends si{body;constructor(n={}){super(n),this.body=n.body!==void 0?n.body:null}type=io.Response;clone(n={}){return new t({body:n.body!==void 0?n.body:this.body,headers:n.headers||this.headers,status:n.status!==void 0?n.status:this.status,statusText:n.statusText||this.statusText,url:n.url||this.url||void 0,redirected:n.redirected??this.redirected,responseType:n.responseType??this.responseType})}},ii=class extends si{name="HttpErrorResponse";message;error;ok=!1;constructor(n){super(n,0,"Unknown Error"),this.status>=200&&this.status<300?this.message=`Http failure during parsing for ${n.url||"(unknown url)"}`:this.message=`Http failure response for ${n.url||"(unknown url)"}: ${n.status} ${n.statusText}`,this.error=n.error||null}},gO=200,vO=204;var yO=new y("");var bO=/^\)\]\}',?\n/;var Pm=(()=>{class t{xhrFactory;tracingService=f(Wt,{optional:!0});constructor(e){this.xhrFactory=e}maybePropagateTrace(e){return this.tracingService?.propagate?this.tracingService.propagate(e):e}handle(e){if(e.method==="JSONP")throw new b(-2800,!1);let r=this.xhrFactory;return T(null).pipe(He(()=>new O(i=>{let s=r.build();if(s.open(e.method,e.urlWithParams),e.withCredentials&&(s.withCredentials=!0),e.headers.forEach((E,I)=>s.setRequestHeader(E,I.join(","))),e.headers.has(bw)||s.setRequestHeader(bw,mO),!e.headers.has(yw)){let E=e.detectContentTypeHeader();E!==null&&s.setRequestHeader(yw,E)}if(e.timeout&&(s.timeout=e.timeout),e.responseType){let E=e.responseType.toLowerCase();s.responseType=E!=="json"?E:"text"}let a=e.serializeBody(),c=null,l=()=>{if(c!==null)return c;let E=s.statusText||"OK",I=new _n(s.getAllResponseHeaders()),ee=s.responseURL||e.url;return c=new uu({headers:I,status:s.status,statusText:E,url:ee}),c},u=this.maybePropagateTrace(()=>{let{headers:E,status:I,statusText:ee,url:Pe}=l(),Le=null;I!==vO&&(Le=typeof s.response>"u"?s.responseText:s.response),I===0&&(I=Le?gO:0);let Ni=I>=200&&I<300;if(e.responseType==="json"&&typeof Le=="string"){let Oi=Le;Le=Le.replace(bO,"");try{Le=Le!==""?JSON.parse(Le):null}catch(Er){Le=Oi,Ni&&(Ni=!1,Le={error:Er,text:Le})}}Ni?(i.next(new Ks({body:Le,headers:E,status:I,statusText:ee,url:Pe||void 0})),i.complete()):i.error(new ii({error:Le,headers:E,status:I,statusText:ee,url:Pe||void 0}))}),d=this.maybePropagateTrace(E=>{let{url:I}=l(),ee=new ii({error:E,status:s.status||0,statusText:s.statusText||"Unknown Error",url:I||void 0});i.error(ee)}),h=d;e.timeout&&(h=this.maybePropagateTrace(E=>{let{url:I}=l(),ee=new ii({error:new DOMException("Request timed out","TimeoutError"),status:s.status||0,statusText:s.statusText||"Request timeout",url:I||void 0});i.error(ee)}));let p=!1,m=this.maybePropagateTrace(E=>{p||(i.next(l()),p=!0);let I={type:io.DownloadProgress,loaded:E.loaded};E.lengthComputable&&(I.total=E.total),e.responseType==="text"&&s.responseText&&(I.partialText=s.responseText),i.next(I)}),_=this.maybePropagateTrace(E=>{let I={type:io.UploadProgress,loaded:E.loaded};E.lengthComputable&&(I.total=E.total),i.next(I)});return s.addEventListener("load",u),s.addEventListener("error",d),s.addEventListener("timeout",h),s.addEventListener("abort",d),e.reportProgress&&(s.addEventListener("progress",m),a!==null&&s.upload&&s.upload.addEventListener("progress",_)),s.send(a),i.next({type:io.Sent}),()=>{s.removeEventListener("error",d),s.removeEventListener("abort",d),s.removeEventListener("load",u),s.removeEventListener("timeout",h),e.reportProgress&&(s.removeEventListener("progress",m),a!==null&&s.upload&&s.upload.removeEventListener("progress",_)),s.readyState!==s.DONE&&s.abort()}})))}static \u0275fac=function(r){return new(r||t)(w(oo))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function ww(t,n){return n(t)}function _O(t,n){return(e,r)=>n.intercept(e,{handle:o=>t(o,r)})}function DO(t,n,e){return(r,o)=>xe(e,()=>n(r,i=>t(i,o)))}var Cw=new y(""),Lm=new y("",{factory:()=>[]}),Iw=new y(""),jm=new y("",{factory:()=>!0});function EO(){let t=null;return(n,e)=>{t===null&&(t=(f(Cw,{optional:!0})??[]).reduceRight(_O,ww));let r=f(zo);if(f(jm)){let i=r.add();return t(n,e).pipe(Rr(i))}else return t(n,e)}}var Vm=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:function(r){let o=null;return r?o=new(r||t):o=w(Pm),o},providedIn:"root"})}return t})();var du=(()=>{class t{backend;injector;chain=null;pendingTasks=f(zo);contributeToStability=f(jm);constructor(e,r){this.backend=e,this.injector=r}handle(e){if(this.chain===null){let r=Array.from(new Set([...this.injector.get(Lm),...this.injector.get(Iw,[])]));this.chain=r.reduceRight((o,i)=>DO(o,i,this.injector),ww)}if(this.contributeToStability){let r=this.pendingTasks.add();return this.chain(e,o=>this.backend.handle(o)).pipe(Rr(r))}else return this.chain(e,r=>this.backend.handle(r))}static \u0275fac=function(r){return new(r||t)(w(Vm),w(re))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Bm=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:function(r){let o=null;return r?o=new(r||t):o=w(du),o},providedIn:"root"})}return t})();function Fm(t,n){return{body:n,headers:t.headers,context:t.context,observe:t.observe,params:t.params,reportProgress:t.reportProgress,responseType:t.responseType,withCredentials:t.withCredentials,credentials:t.credentials,transferCache:t.transferCache,timeout:t.timeout,keepalive:t.keepalive,priority:t.priority,cache:t.cache,mode:t.mode,redirect:t.redirect,integrity:t.integrity,referrer:t.referrer,referrerPolicy:t.referrerPolicy}}var fu=(()=>{class t{handler;constructor(e){this.handler=e}request(e,r,o={}){let i;if(e instanceof oi)i=e;else{let c;o.headers instanceof _n?c=o.headers:c=new _n(o.headers);let l;o.params&&(o.params instanceof $n?l=o.params:l=new $n({fromObject:o.params})),i=new oi(e,r,o.body!==void 0?o.body:null,{headers:c,context:o.context,params:l,reportProgress:o.reportProgress,responseType:o.responseType||"json",withCredentials:o.withCredentials,transferCache:o.transferCache,keepalive:o.keepalive,priority:o.priority,cache:o.cache,mode:o.mode,redirect:o.redirect,credentials:o.credentials,referrer:o.referrer,referrerPolicy:o.referrerPolicy,integrity:o.integrity,timeout:o.timeout})}let s=T(i).pipe(Xn(c=>this.handler.handle(c)));if(e instanceof oi||o.observe==="events")return s;let a=s.pipe(fe(c=>c instanceof Ks));switch(o.observe||"body"){case"body":switch(i.responseType){case"arraybuffer":return a.pipe(H(c=>{if(c.body!==null&&!(c.body instanceof ArrayBuffer))throw new b(2806,!1);return c.body}));case"blob":return a.pipe(H(c=>{if(c.body!==null&&!(c.body instanceof Blob))throw new b(2807,!1);return c.body}));case"text":return a.pipe(H(c=>{if(c.body!==null&&typeof c.body!="string")throw new b(2808,!1);return c.body}));default:return a.pipe(H(c=>c.body))}case"response":return a;default:throw new b(2809,!1)}}delete(e,r={}){return this.request("DELETE",e,r)}get(e,r={}){return this.request("GET",e,r)}head(e,r={}){return this.request("HEAD",e,r)}jsonp(e,r){return this.request("JSONP",e,{params:new $n().append(r,"JSONP_CALLBACK"),observe:"body",responseType:"json"})}options(e,r={}){return this.request("OPTIONS",e,r)}patch(e,r,o={}){return this.request("PATCH",e,Fm(o,r))}post(e,r,o={}){return this.request("POST",e,Fm(o,r))}put(e,r,o={}){return this.request("PUT",e,Fm(o,r))}static \u0275fac=function(r){return new(r||t)(w(Bm))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var wO=new y("",{factory:()=>!0}),CO="XSRF-TOKEN",IO=new y("",{factory:()=>CO}),SO="X-XSRF-TOKEN",MO=new y("",{factory:()=>SO}),TO=(()=>{class t{cookieName=f(IO);doc=f(L);lastCookieString="";lastToken=null;parseCount=0;getToken(){let e=this.doc.cookie||"";return e!==this.lastCookieString&&(this.parseCount++,this.lastToken=Gs(e,this.cookieName),this.lastCookieString=e),this.lastToken}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Sw=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:function(r){let o=null;return r?o=new(r||t):o=w(TO),o},providedIn:"root"})}return t})();function xO(t,n){if(!f(wO)||t.method==="GET"||t.method==="HEAD")return n(t);try{let o=f(Bn).href,{origin:i}=new URL(o),{origin:s}=new URL(t.url,i);if(i!==s)return n(t)}catch{return n(t)}let e=f(Sw).getToken(),r=f(MO);return e!=null&&!t.headers.has(r)&&(t=t.clone({headers:t.headers.set(r,e)})),n(t)}var Um=(function(t){return t[t.Interceptors=0]="Interceptors",t[t.LegacyInterceptors=1]="LegacyInterceptors",t[t.CustomXsrfConfiguration=2]="CustomXsrfConfiguration",t[t.NoXsrfProtection=3]="NoXsrfProtection",t[t.JsonpSupport=4]="JsonpSupport",t[t.RequestsMadeViaParent=5]="RequestsMadeViaParent",t[t.Fetch=6]="Fetch",t})(Um||{});function AO(t,n){return{\u0275kind:t,\u0275providers:n}}function Mw(...t){let n=[fu,du,{provide:Bm,useExisting:du},{provide:Vm,useFactory:()=>f(yO,{optional:!0})??f(Pm)},{provide:Lm,useValue:xO,multi:!0}];for(let e of t)n.push(...e.\u0275providers);return sr(n)}var _w=new y("");function Tw(){return AO(Um.LegacyInterceptors,[{provide:_w,useFactory:EO},{provide:Lm,useExisting:_w,multi:!0}])}var RO=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({providers:[Mw(Tw())]})}return t})();var xw=(()=>{class t{_doc;constructor(e){this._doc=e}getTitle(){return this._doc.title}setTitle(e){this._doc.title=e||""}static \u0275fac=function(r){return new(r||t)(w(L))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function OO(t,n){if(typeof COMPILED>"u"||!COMPILED){let e=ye.ng=ye.ng||{};e[t]=n}}var Hm=class{msPerTick;numTicks;constructor(n,e){this.msPerTick=n,this.numTicks=e}},$m=class{appRef;constructor(n){this.appRef=n.injector.get(Be)}timeChangeDetection(n){let e=n&&n.record,r="Change Detection";e&&"profile"in console&&typeof console.profile=="function"&&console.profile(r);let o=performance.now(),i=0;for(;i<5||performance.now()-o<500;)this.appRef.tick(),i++;let s=performance.now();e&&"profileEnd"in console&&typeof console.profileEnd=="function"&&console.profileEnd(r);let a=(s-o)/i;return console.log(`ran ${i} change detection cycles`),console.log(`${a.toFixed(2)} ms per check`),new Hm(a,i)}},kO="profiler";function k9(t){return OO(kO,new $m(t)),t}var zm=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:function(r){let o=null;return r?o=new(r||t):o=w(FO),o},providedIn:"root"})}return t})(),FO=(()=>{class t extends zm{_doc;constructor(e){super(),this._doc=e}sanitize(e,r){if(r==null)return null;switch(e){case ot.NONE:return r;case ot.HTML:return vn(r,"HTML")?ft(r):ml(this._doc,String(r)).toString();case ot.STYLE:return vn(r,"Style")?ft(r):r;case ot.SCRIPT:if(vn(r,"Script"))return ft(r);throw new b(5200,!1);case ot.URL:return vn(r,"URL")?ft(r):Ss(String(r));case ot.RESOURCE_URL:if(vn(r,"ResourceURL"))return ft(r);throw new b(5201,!1);default:throw new b(5202,!1)}}bypassSecurityTrustHtml(e){return cp(e)}bypassSecurityTrustStyle(e){return lp(e)}bypassSecurityTrustScript(e){return up(e)}bypassSecurityTrustUrl(e){return dp(e)}bypassSecurityTrustResourceUrl(e){return fp(e)}static \u0275fac=function(r){return new(r||t)(w(L))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function Qs(t){return t.buttons===0||t.detail===0}function Xs(t){let n=t.touches&&t.touches[0]||t.changedTouches&&t.changedTouches[0];return!!n&&n.identifier===-1&&(n.radiusX==null||n.radiusX===1)&&(n.radiusY==null||n.radiusY===1)}var Gm;function Aw(){if(Gm==null){let t=typeof document<"u"?document.head:null;Gm=!!(t&&(t.createShadowRoot||t.attachShadow))}return Gm}function Wm(t){if(Aw()){let n=t.getRootNode?t.getRootNode():null;if(typeof ShadowRoot<"u"&&ShadowRoot&&n instanceof ShadowRoot)return n}return null}function LO(){let t=typeof document<"u"&&document?document.activeElement:null;for(;t&&t.shadowRoot;){let n=t.shadowRoot.activeElement;if(n===t)break;t=n}return t}function Je(t){return t.composedPath?t.composedPath()[0]:t.target}var qm;try{qm=typeof Intl<"u"&&Intl.v8BreakIterator}catch{qm=!1}var he=(()=>{class t{_platformId=f(Jr);isBrowser=this._platformId?iw(this._platformId):typeof document=="object"&&!!document;EDGE=this.isBrowser&&/(edge)/i.test(navigator.userAgent);TRIDENT=this.isBrowser&&/(msie|trident)/i.test(navigator.userAgent);BLINK=this.isBrowser&&!!(window.chrome||qm)&&typeof CSS<"u"&&!this.EDGE&&!this.TRIDENT;WEBKIT=this.isBrowser&&/AppleWebKit/i.test(navigator.userAgent)&&!this.BLINK&&!this.EDGE&&!this.TRIDENT;IOS=this.isBrowser&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!("MSStream"in window);FIREFOX=this.isBrowser&&/(firefox|minefield)/i.test(navigator.userAgent);ANDROID=this.isBrowser&&/android/i.test(navigator.userAgent)&&!this.TRIDENT;SAFARI=this.isBrowser&&/safari/i.test(navigator.userAgent)&&this.WEBKIT;constructor(){}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var Js;function Rw(){if(Js==null&&typeof window<"u")try{window.addEventListener("test",null,Object.defineProperty({},"passive",{get:()=>Js=!0}))}finally{Js=Js||!1}return Js}function ai(t){return Rw()?t:!!t.capture}function so(t,n=0){return Nw(t)?Number(t):arguments.length===2?n:0}function Nw(t){return!isNaN(parseFloat(t))&&!isNaN(Number(t))}function Tt(t){return t instanceof z?t.nativeElement:t}var Ow=new y("cdk-input-modality-detector-options"),kw={ignoreKeys:[18,17,224,91,16]},Fw=650,Ym={passive:!0,capture:!0},Pw=(()=>{class t{_platform=f(he);_listenerCleanups;modalityDetected;modalityChanged;get mostRecentModality(){return this._modality.value}_mostRecentTarget=null;_modality=new Ie(null);_options;_lastTouchMs=0;_onKeydown=e=>{this._options?.ignoreKeys?.some(r=>r===e.keyCode)||(this._modality.next("keyboard"),this._mostRecentTarget=Je(e))};_onMousedown=e=>{Date.now()-this._lastTouchMs{if(Xs(e)){this._modality.next("keyboard");return}this._lastTouchMs=Date.now(),this._modality.next("touch"),this._mostRecentTarget=Je(e)};constructor(){let e=f(j),r=f(L),o=f(Ow,{optional:!0});if(this._options=g(g({},kw),o),this.modalityDetected=this._modality.pipe(Ui(1)),this.modalityChanged=this.modalityDetected.pipe(xo()),this._platform.isBrowser){let i=f(je).createRenderer(null,null);this._listenerCleanups=e.runOutsideAngular(()=>[i.listen(r,"keydown",this._onKeydown,Ym),i.listen(r,"mousedown",this._onMousedown,Ym),i.listen(r,"touchstart",this._onTouchstart,Ym)])}}ngOnDestroy(){this._modality.complete(),this._listenerCleanups?.forEach(e=>e())}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),ea=(function(t){return t[t.IMMEDIATE=0]="IMMEDIATE",t[t.EVENTUAL=1]="EVENTUAL",t})(ea||{}),Lw=new y("cdk-focus-monitor-default-options"),hu=ai({passive:!0,capture:!0}),pu=(()=>{class t{_ngZone=f(j);_platform=f(he);_inputModalityDetector=f(Pw);_origin=null;_lastFocusOrigin=null;_windowFocused=!1;_windowFocusTimeoutId;_originTimeoutId;_originFromTouchInteraction=!1;_elementInfo=new Map;_monitoredElementCount=0;_rootNodeFocusListenerCount=new Map;_detectionMode;_windowFocusListener=()=>{this._windowFocused=!0,this._windowFocusTimeoutId=setTimeout(()=>this._windowFocused=!1)};_document=f(L);_stopInputModalityDetector=new S;constructor(){let e=f(Lw,{optional:!0});this._detectionMode=e?.detectionMode||ea.IMMEDIATE}_rootNodeFocusAndBlurListener=e=>{let r=Je(e);for(let o=r;o;o=o.parentElement)e.type==="focus"?this._onFocus(e,o):this._onBlur(e,o)};monitor(e,r=!1){let o=Tt(e);if(!this._platform.isBrowser||o.nodeType!==1)return T();let i=Wm(o)||this._document,s=this._elementInfo.get(o);if(s)return r&&(s.checkChildren=!0),s.subject;let a={checkChildren:r,subject:new S,rootNode:i};return this._elementInfo.set(o,a),this._registerGlobalListeners(a),a.subject}stopMonitoring(e){let r=Tt(e),o=this._elementInfo.get(r);o&&(o.subject.complete(),this._setClasses(r),this._elementInfo.delete(r),this._removeGlobalListeners(o))}focusVia(e,r,o){let i=Tt(e),s=this._document.activeElement;i===s?this._getClosestElementsInfo(i).forEach(([a,c])=>this._originChanged(a,r,c)):(this._setOrigin(r),typeof i.focus=="function"&&i.focus(o))}ngOnDestroy(){this._elementInfo.forEach((e,r)=>this.stopMonitoring(r))}_getWindow(){return this._document.defaultView||window}_getFocusOrigin(e){return this._origin?this._originFromTouchInteraction?this._shouldBeAttributedToTouch(e)?"touch":"program":this._origin:this._windowFocused&&this._lastFocusOrigin?this._lastFocusOrigin:e&&this._isLastInteractionFromInputLabel(e)?"mouse":"program"}_shouldBeAttributedToTouch(e){return this._detectionMode===ea.EVENTUAL||!!e?.contains(this._inputModalityDetector._mostRecentTarget)}_setClasses(e,r){e.classList.toggle("cdk-focused",!!r),e.classList.toggle("cdk-touch-focused",r==="touch"),e.classList.toggle("cdk-keyboard-focused",r==="keyboard"),e.classList.toggle("cdk-mouse-focused",r==="mouse"),e.classList.toggle("cdk-program-focused",r==="program")}_setOrigin(e,r=!1){this._ngZone.runOutsideAngular(()=>{if(this._origin=e,this._originFromTouchInteraction=e==="touch"&&r,this._detectionMode===ea.IMMEDIATE){clearTimeout(this._originTimeoutId);let o=this._originFromTouchInteraction?Fw:1;this._originTimeoutId=setTimeout(()=>this._origin=null,o)}})}_onFocus(e,r){let o=this._elementInfo.get(r),i=Je(e);!o||!o.checkChildren&&r!==i||this._originChanged(r,this._getFocusOrigin(i),o)}_onBlur(e,r){let o=this._elementInfo.get(r);!o||o.checkChildren&&e.relatedTarget instanceof Node&&r.contains(e.relatedTarget)||(this._setClasses(r),this._emitOrigin(o,null))}_emitOrigin(e,r){e.subject.observers.length&&this._ngZone.run(()=>e.subject.next(r))}_registerGlobalListeners(e){if(!this._platform.isBrowser)return;let r=e.rootNode,o=this._rootNodeFocusListenerCount.get(r)||0;o||this._ngZone.runOutsideAngular(()=>{r.addEventListener("focus",this._rootNodeFocusAndBlurListener,hu),r.addEventListener("blur",this._rootNodeFocusAndBlurListener,hu)}),this._rootNodeFocusListenerCount.set(r,o+1),++this._monitoredElementCount===1&&(this._ngZone.runOutsideAngular(()=>{this._getWindow().addEventListener("focus",this._windowFocusListener)}),this._inputModalityDetector.modalityDetected.pipe(at(this._stopInputModalityDetector)).subscribe(i=>{this._setOrigin(i,!0)}))}_removeGlobalListeners(e){let r=e.rootNode;if(this._rootNodeFocusListenerCount.has(r)){let o=this._rootNodeFocusListenerCount.get(r);o>1?this._rootNodeFocusListenerCount.set(r,o-1):(r.removeEventListener("focus",this._rootNodeFocusAndBlurListener,hu),r.removeEventListener("blur",this._rootNodeFocusAndBlurListener,hu),this._rootNodeFocusListenerCount.delete(r))}--this._monitoredElementCount||(this._getWindow().removeEventListener("focus",this._windowFocusListener),this._stopInputModalityDetector.next(),clearTimeout(this._windowFocusTimeoutId),clearTimeout(this._originTimeoutId))}_originChanged(e,r,o){this._setClasses(e,r),this._emitOrigin(o,r),this._lastFocusOrigin=r}_getClosestElementsInfo(e){let r=[];return this._elementInfo.forEach((o,i)=>{(i===e||o.checkChildren&&i.contains(e))&&r.push([i,o])}),r}_isLastInteractionFromInputLabel(e){let{_mostRecentTarget:r,mostRecentModality:o}=this._inputModalityDetector;if(o!=="mouse"||!r||r===e||e.nodeName!=="INPUT"&&e.nodeName!=="TEXTAREA"||e.disabled)return!1;let i=e.labels;if(i){for(let s=0;s{class t{_elementRef=f(z);_focusMonitor=f(pu);_monitorSubscription;_focusOrigin=null;cdkFocusChange=new U;constructor(){}get focusOrigin(){return this._focusOrigin}ngAfterViewInit(){let e=this._elementRef.nativeElement;this._monitorSubscription=this._focusMonitor.monitor(e,e.nodeType===1&&e.hasAttribute("cdkMonitorSubtreeFocus")).subscribe(r=>{this._focusOrigin=r,this.cdkFocusChange.emit(r)})}ngOnDestroy(){this._focusMonitor.stopMonitoring(this._elementRef),this._monitorSubscription?.unsubscribe()}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["","cdkMonitorElementFocus",""],["","cdkMonitorSubtreeFocus",""]],outputs:{cdkFocusChange:"cdkFocusChange"},exportAs:["cdkMonitorFocus"]})}return t})();var mu=new WeakMap,xt=(()=>{class t{_appRef;_injector=f($);_environmentInjector=f(re);load(e){let r=this._appRef=this._appRef||this._injector.get(Be),o=mu.get(r);o||(o={loaders:new Set,refs:[]},mu.set(r,o),r.onDestroy(()=>{mu.get(r)?.refs.forEach(i=>i.destroy()),mu.delete(r)})),o.loaders.has(e)||(o.loaders.add(e),o.refs.push($l(e,{environmentInjector:this._environmentInjector})))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var vu=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=ke({type:t,selectors:[["ng-component"]],exportAs:["cdkVisuallyHidden"],decls:0,vars:0,template:function(r,o){},styles:[`.cdk-visually-hidden { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; - white-space: nowrap; - outline: 0; - -webkit-appearance: none; - -moz-appearance: none; - left: 0; -} -[dir=rtl] .cdk-visually-hidden { - left: auto; - right: 0; -} -`],encapsulation:2,changeDetection:0})}return t})(),gu;function VO(){if(gu===void 0&&(gu=null,typeof window<"u")){let t=window;t.trustedTypes!==void 0&&(gu=t.trustedTypes.createPolicy("angular#components",{createHTML:n=>n}))}return gu}function BO(t){return VO()?.createHTML(t)||t}function jw(t,n,e){let r=e.sanitize(ot.HTML,n);t.innerHTML=BO(r||"")}function ao(t){return Array.isArray(t)?t:[t]}var Vw=new Set,co,yu=(()=>{class t{_platform=f(he);_nonce=f(eo,{optional:!0});_matchMedia;constructor(){this._matchMedia=this._platform.isBrowser&&window.matchMedia?window.matchMedia.bind(window):HO}matchMedia(e){return(this._platform.WEBKIT||this._platform.BLINK)&&UO(e,this._nonce),this._matchMedia(e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function UO(t,n){if(!Vw.has(t))try{co||(co=document.createElement("style"),n&&co.setAttribute("nonce",n),co.setAttribute("type","text/css"),document.head.appendChild(co)),co.sheet&&(co.sheet.insertRule(`@media ${t} {body{ }}`,0),Vw.add(t))}catch(e){console.error(e)}}function HO(t){return{matches:t==="all"||t==="",media:t,addListener:()=>{},removeListener:()=>{}}}var Zm=(()=>{class t{_mediaMatcher=f(yu);_zone=f(j);_queries=new Map;_destroySubject=new S;constructor(){}ngOnDestroy(){this._destroySubject.next(),this._destroySubject.complete()}isMatched(e){return Bw(ao(e)).some(o=>this._registerQuery(o).mql.matches)}observe(e){let o=Bw(ao(e)).map(s=>this._registerQuery(s).observable),i=To(o);return i=on(i.pipe(Ue(1)),i.pipe(Ui(1),Ar(0))),i.pipe(H(s=>{let a={matches:!1,breakpoints:{}};return s.forEach(({matches:c,query:l})=>{a.matches=a.matches||c,a.breakpoints[l]=c}),a}))}_registerQuery(e){if(this._queries.has(e))return this._queries.get(e);let r=this._mediaMatcher.matchMedia(e),i={observable:new O(s=>{let a=c=>this._zone.run(()=>s.next(c));return r.addListener(a),()=>{r.removeListener(a)}}).pipe(Nr(r),H(({matches:s})=>({query:e,matches:s})),at(this._destroySubject)),mql:r};return this._queries.set(e,i),i}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function Bw(t){return t.map(n=>n.split(",")).reduce((n,e)=>n.concat(e)).map(n=>n.trim())}function $O(t){if(t.type==="characterData"&&t.target instanceof Comment)return!0;if(t.type==="childList"){for(let n=0;n{class t{create(e){return typeof MutationObserver>"u"?null:new MutationObserver(e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Hw=(()=>{class t{_mutationObserverFactory=f(Uw);_observedElements=new Map;_ngZone=f(j);constructor(){}ngOnDestroy(){this._observedElements.forEach((e,r)=>this._cleanupObserver(r))}observe(e){let r=Tt(e);return new O(o=>{let s=this._observeElement(r).pipe(H(a=>a.filter(c=>!$O(c))),fe(a=>!!a.length)).subscribe(a=>{this._ngZone.run(()=>{o.next(a)})});return()=>{s.unsubscribe(),this._unobserveElement(r)}})}_observeElement(e){return this._ngZone.runOutsideAngular(()=>{if(this._observedElements.has(e))this._observedElements.get(e).count++;else{let r=new S,o=this._mutationObserverFactory.create(i=>r.next(i));o&&o.observe(e,{characterData:!0,childList:!0,subtree:!0}),this._observedElements.set(e,{observer:o,stream:r,count:1})}return this._observedElements.get(e).stream})}_unobserveElement(e){this._observedElements.has(e)&&(this._observedElements.get(e).count--,this._observedElements.get(e).count||this._cleanupObserver(e))}_cleanupObserver(e){if(this._observedElements.has(e)){let{observer:r,stream:o}=this._observedElements.get(e);r&&r.disconnect(),o.complete(),this._observedElements.delete(e)}}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),FY=(()=>{class t{_contentObserver=f(Hw);_elementRef=f(z);event=new U;get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._disabled?this._unsubscribe():this._subscribe()}_disabled=!1;get debounce(){return this._debounce}set debounce(e){this._debounce=so(e),this._subscribe()}_debounce;_currentSubscription=null;constructor(){}ngAfterContentInit(){!this._currentSubscription&&!this.disabled&&this._subscribe()}ngOnDestroy(){this._unsubscribe()}_subscribe(){this._unsubscribe();let e=this._contentObserver.observe(this._elementRef);this._currentSubscription=(this.debounce?e.pipe(Ar(this.debounce)):e).subscribe(this.event)}_unsubscribe(){this._currentSubscription?.unsubscribe()}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["","cdkObserveContent",""]],inputs:{disabled:[2,"cdkObserveContentDisabled","disabled",ue],debounce:"debounce"},outputs:{event:"cdkObserveContent"},exportAs:["cdkObserveContent"]})}return t})(),$w=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({providers:[Uw]})}return t})();var zO=(()=>{class t{_platform=f(he);constructor(){}isDisabled(e){return e.hasAttribute("disabled")}isVisible(e){return WO(e)&&getComputedStyle(e).visibility==="visible"}isTabbable(e){if(!this._platform.isBrowser)return!1;let r=GO(ek(e));if(r&&(zw(r)===-1||!this.isVisible(r)))return!1;let o=e.nodeName.toLowerCase(),i=zw(e);return e.hasAttribute("contenteditable")?i!==-1:o==="iframe"||o==="object"||this._platform.WEBKIT&&this._platform.IOS&&!XO(e)?!1:o==="audio"?e.hasAttribute("controls")?i!==-1:!1:o==="video"?i===-1?!1:i!==null?!0:this._platform.FIREFOX||e.hasAttribute("controls"):e.tabIndex>=0}isFocusable(e,r){return JO(e)&&!this.isDisabled(e)&&(r?.ignoreVisibility||this.isVisible(e))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function GO(t){try{return t.frameElement}catch{return null}}function WO(t){return!!(t.offsetWidth||t.offsetHeight||typeof t.getClientRects=="function"&&t.getClientRects().length)}function qO(t){let n=t.nodeName.toLowerCase();return n==="input"||n==="select"||n==="button"||n==="textarea"}function YO(t){return KO(t)&&t.type=="hidden"}function ZO(t){return QO(t)&&t.hasAttribute("href")}function KO(t){return t.nodeName.toLowerCase()=="input"}function QO(t){return t.nodeName.toLowerCase()=="a"}function qw(t){if(!t.hasAttribute("tabindex")||t.tabIndex===void 0)return!1;let n=t.getAttribute("tabindex");return!!(n&&!isNaN(parseInt(n,10)))}function zw(t){if(!qw(t))return null;let n=parseInt(t.getAttribute("tabindex")||"",10);return isNaN(n)?-1:n}function XO(t){let n=t.nodeName.toLowerCase(),e=n==="input"&&t.type;return e==="text"||e==="password"||n==="select"||n==="textarea"}function JO(t){return YO(t)?!1:qO(t)||ZO(t)||t.hasAttribute("contenteditable")||qw(t)}function ek(t){return t.ownerDocument&&t.ownerDocument.defaultView||window}var Qm=class{_element;_checker;_ngZone;_document;_injector;_startAnchor=null;_endAnchor=null;_hasAttached=!1;startAnchorListener=()=>this.focusLastTabbableElement();endAnchorListener=()=>this.focusFirstTabbableElement();get enabled(){return this._enabled}set enabled(n){this._enabled=n,this._startAnchor&&this._endAnchor&&(this._toggleAnchorTabIndex(n,this._startAnchor),this._toggleAnchorTabIndex(n,this._endAnchor))}_enabled=!0;constructor(n,e,r,o,i=!1,s){this._element=n,this._checker=e,this._ngZone=r,this._document=o,this._injector=s,i||this.attachAnchors()}destroy(){let n=this._startAnchor,e=this._endAnchor;n&&(n.removeEventListener("focus",this.startAnchorListener),n.remove()),e&&(e.removeEventListener("focus",this.endAnchorListener),e.remove()),this._startAnchor=this._endAnchor=null,this._hasAttached=!1}attachAnchors(){return this._hasAttached?!0:(this._ngZone.runOutsideAngular(()=>{this._startAnchor||(this._startAnchor=this._createAnchor(),this._startAnchor.addEventListener("focus",this.startAnchorListener)),this._endAnchor||(this._endAnchor=this._createAnchor(),this._endAnchor.addEventListener("focus",this.endAnchorListener))}),this._element.parentNode&&(this._element.parentNode.insertBefore(this._startAnchor,this._element),this._element.parentNode.insertBefore(this._endAnchor,this._element.nextSibling),this._hasAttached=!0),this._hasAttached)}focusInitialElementWhenReady(n){return new Promise(e=>{this._executeOnStable(()=>e(this.focusInitialElement(n)))})}focusFirstTabbableElementWhenReady(n){return new Promise(e=>{this._executeOnStable(()=>e(this.focusFirstTabbableElement(n)))})}focusLastTabbableElementWhenReady(n){return new Promise(e=>{this._executeOnStable(()=>e(this.focusLastTabbableElement(n)))})}_getRegionBoundary(n){let e=this._element.querySelectorAll(`[cdk-focus-region-${n}], [cdkFocusRegion${n}], [cdk-focus-${n}]`);return n=="start"?e.length?e[0]:this._getFirstTabbableElement(this._element):e.length?e[e.length-1]:this._getLastTabbableElement(this._element)}focusInitialElement(n){let e=this._element.querySelector("[cdk-focus-initial], [cdkFocusInitial]");if(e){if(!this._checker.isFocusable(e)){let r=this._getFirstTabbableElement(e);return r?.focus(n),!!r}return e.focus(n),!0}return this.focusFirstTabbableElement(n)}focusFirstTabbableElement(n){let e=this._getRegionBoundary("start");return e&&e.focus(n),!!e}focusLastTabbableElement(n){let e=this._getRegionBoundary("end");return e&&e.focus(n),!!e}hasAttached(){return this._hasAttached}_getFirstTabbableElement(n){if(this._checker.isFocusable(n)&&this._checker.isTabbable(n))return n;let e=n.children;for(let r=0;r=0;r--){let o=e[r].nodeType===this._document.ELEMENT_NODE?this._getLastTabbableElement(e[r]):null;if(o)return o}return null}_createAnchor(){let n=this._document.createElement("div");return this._toggleAnchorTabIndex(this._enabled,n),n.classList.add("cdk-visually-hidden"),n.classList.add("cdk-focus-trap-anchor"),n.setAttribute("aria-hidden","true"),n}_toggleAnchorTabIndex(n,e){n?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")}toggleAnchors(n){this._startAnchor&&this._endAnchor&&(this._toggleAnchorTabIndex(n,this._startAnchor),this._toggleAnchorTabIndex(n,this._endAnchor))}_executeOnStable(n){this._injector?ht(n,{injector:this._injector}):setTimeout(n)}},tk=(()=>{class t{_checker=f(zO);_ngZone=f(j);_document=f(L);_injector=f($);constructor(){f(xt).load(vu)}create(e,r=!1){return new Qm(e,this._checker,this._ngZone,this._document,r,this._injector)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var Yw=new y("liveAnnouncerElement",{providedIn:"root",factory:()=>null}),Zw=new y("LIVE_ANNOUNCER_DEFAULT_OPTIONS"),nk=0,rk=(()=>{class t{_ngZone=f(j);_defaultOptions=f(Zw,{optional:!0});_liveElement;_document=f(L);_sanitizer=f(zm);_previousTimeout;_currentPromise;_currentResolve;constructor(){let e=f(Yw,{optional:!0});this._liveElement=e||this._createLiveElement()}announce(e,...r){let o=this._defaultOptions,i,s;return r.length===1&&typeof r[0]=="number"?s=r[0]:[i,s]=r,this.clear(),clearTimeout(this._previousTimeout),i||(i=o&&o.politeness?o.politeness:"polite"),s==null&&o&&(s=o.duration),this._liveElement.setAttribute("aria-live",i),this._liveElement.id&&this._exposeAnnouncerToModals(this._liveElement.id),this._ngZone.runOutsideAngular(()=>(this._currentPromise||(this._currentPromise=new Promise(a=>this._currentResolve=a)),clearTimeout(this._previousTimeout),this._previousTimeout=setTimeout(()=>{!e||typeof e=="string"?this._liveElement.textContent=e:jw(this._liveElement,e,this._sanitizer),typeof s=="number"&&(this._previousTimeout=setTimeout(()=>this.clear(),s)),this._currentResolve?.(),this._currentPromise=this._currentResolve=void 0},100),this._currentPromise))}clear(){this._liveElement&&(this._liveElement.textContent="")}ngOnDestroy(){clearTimeout(this._previousTimeout),this._liveElement?.remove(),this._liveElement=null,this._currentResolve?.(),this._currentPromise=this._currentResolve=void 0}_createLiveElement(){let e="cdk-live-announcer-element",r=this._document.getElementsByClassName(e),o=this._document.createElement("div");for(let i=0;i .cdk-overlay-container [aria-modal="true"]');for(let o=0;o{class t{_platform=f(he);_hasCheckedHighContrastMode=!1;_document=f(L);_breakpointSubscription;constructor(){this._breakpointSubscription=f(Zm).observe("(forced-colors: active)").subscribe(()=>{this._hasCheckedHighContrastMode&&(this._hasCheckedHighContrastMode=!1,this._applyBodyHighContrastModeCssClasses())})}getHighContrastMode(){if(!this._platform.isBrowser)return pr.NONE;let e=this._document.createElement("div");e.style.backgroundColor="rgb(1,2,3)",e.style.position="absolute",this._document.body.appendChild(e);let r=this._document.defaultView||window,o=r&&r.getComputedStyle?r.getComputedStyle(e):null,i=(o&&o.backgroundColor||"").replace(/ /g,"");switch(e.remove(),i){case"rgb(0,0,0)":case"rgb(45,50,54)":case"rgb(32,32,32)":return pr.WHITE_ON_BLACK;case"rgb(255,255,255)":case"rgb(255,250,239)":return pr.BLACK_ON_WHITE}return pr.NONE}ngOnDestroy(){this._breakpointSubscription.unsubscribe()}_applyBodyHighContrastModeCssClasses(){if(!this._hasCheckedHighContrastMode&&this._platform.isBrowser&&this._document.body){let e=this._document.body.classList;e.remove(Km,Gw,Ww),this._hasCheckedHighContrastMode=!0;let r=this.getHighContrastMode();r===pr.BLACK_ON_WHITE?e.add(Km,Gw):r===pr.WHITE_ON_BLACK&&e.add(Km,Ww)}}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),ok=(()=>{class t{constructor(){f(Kw)._applyBodyHighContrastModeCssClasses()}static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({imports:[$w]})}return t})();var ik=200,ci=class{_letterKeyStream=new S;_items=[];_selectedItemIndex=-1;_pressedLetters=[];_skipPredicateFn;_selectedItem=new S;selectedItem=this._selectedItem;constructor(n,e){let r=typeof e?.debounceInterval=="number"?e.debounceInterval:ik;e?.skipPredicate&&(this._skipPredicateFn=e.skipPredicate),this.setItems(n),this._setupKeyHandler(r)}destroy(){this._pressedLetters=[],this._letterKeyStream.complete(),this._selectedItem.complete()}setCurrentSelectedItemIndex(n){this._selectedItemIndex=n}setItems(n){this._items=n}handleKey(n){let e=n.keyCode;n.key&&n.key.length===1?this._letterKeyStream.next(n.key.toLocaleUpperCase()):(e>=65&&e<=90||e>=48&&e<=57)&&this._letterKeyStream.next(String.fromCharCode(e))}isTyping(){return this._pressedLetters.length>0}reset(){this._pressedLetters=[]}_setupKeyHandler(n){this._letterKeyStream.pipe(nt(e=>this._pressedLetters.push(e)),Ar(n),fe(()=>this._pressedLetters.length>0),H(()=>this._pressedLetters.join("").toLocaleUpperCase())).subscribe(e=>{for(let r=1;rt[e]):t.altKey||t.shiftKey||t.ctrlKey||t.metaKey}var li=class{_items;_activeItemIndex=W(-1);_activeItem=W(null);_wrap=!1;_typeaheadSubscription=G.EMPTY;_itemChangesSubscription;_vertical=!0;_horizontal=null;_allowedModifierKeys=[];_homeAndEnd=!1;_pageUpAndDown={enabled:!1,delta:10};_effectRef;_typeahead;_skipPredicateFn=n=>n.disabled;constructor(n,e){this._items=n,n instanceof Fn?this._itemChangesSubscription=n.changes.subscribe(r=>this._itemsChanged(r.toArray())):ri(n)&&(this._effectRef=Go(()=>this._itemsChanged(n()),{injector:e}))}tabOut=new S;change=new S;skipPredicate(n){return this._skipPredicateFn=n,this}withWrap(n=!0){return this._wrap=n,this}withVerticalOrientation(n=!0){return this._vertical=n,this}withHorizontalOrientation(n){return this._horizontal=n,this}withAllowedModifierKeys(n){return this._allowedModifierKeys=n,this}withTypeAhead(n=200){this._typeaheadSubscription.unsubscribe();let e=this._getItemsArray();return this._typeahead=new ci(e,{debounceInterval:typeof n=="number"?n:void 0,skipPredicate:r=>this._skipPredicateFn(r)}),this._typeaheadSubscription=this._typeahead.selectedItem.subscribe(r=>{this.setActiveItem(r)}),this}cancelTypeahead(){return this._typeahead?.reset(),this}withHomeAndEnd(n=!0){return this._homeAndEnd=n,this}withPageUpDown(n=!0,e=10){return this._pageUpAndDown={enabled:n,delta:e},this}setActiveItem(n){let e=this._activeItem();this.updateActiveItem(n),this._activeItem()!==e&&this.change.next(this._activeItemIndex())}onKeydown(n){let e=n.keyCode,o=["altKey","ctrlKey","metaKey","shiftKey"].every(i=>!n[i]||this._allowedModifierKeys.indexOf(i)>-1);switch(e){case 9:this.tabOut.next();return;case 40:if(this._vertical&&o){this.setNextItemActive();break}else return;case 38:if(this._vertical&&o){this.setPreviousItemActive();break}else return;case 39:if(this._horizontal&&o){this._horizontal==="rtl"?this.setPreviousItemActive():this.setNextItemActive();break}else return;case 37:if(this._horizontal&&o){this._horizontal==="rtl"?this.setNextItemActive():this.setPreviousItemActive();break}else return;case 36:if(this._homeAndEnd&&o){this.setFirstItemActive();break}else return;case 35:if(this._homeAndEnd&&o){this.setLastItemActive();break}else return;case 33:if(this._pageUpAndDown.enabled&&o){let i=this._activeItemIndex()-this._pageUpAndDown.delta;this._setActiveItemByIndex(i>0?i:0,1);break}else return;case 34:if(this._pageUpAndDown.enabled&&o){let i=this._activeItemIndex()+this._pageUpAndDown.delta,s=this._getItemsArray().length;this._setActiveItemByIndex(i-1&&r!==this._activeItemIndex()&&(this._activeItemIndex.set(r),this._typeahead?.setCurrentSelectedItemIndex(r))}}};var Xm=class extends li{setActiveItem(n){this.activeItem&&this.activeItem.setInactiveStyles(),super.setActiveItem(n),this.activeItem&&this.activeItem.setActiveStyles()}};var Jm=class extends li{_origin="program";setFocusOrigin(n){return this._origin=n,this}setActiveItem(n){super.setActiveItem(n),this.activeItem&&this.activeItem.focus(this._origin)}};function eg(t){return Ft(t)?t:T(t)}var tg=class{_activeItemIndex=-1;_activeItem=null;_shouldActivationFollowFocus=!1;_horizontalOrientation="ltr";_skipPredicateFn=n=>!1;_trackByFn=n=>n;_items=[];_typeahead;_typeaheadSubscription=G.EMPTY;_hasInitialFocused=!1;_initializeFocus(){if(this._hasInitialFocused||this._items.length===0)return;let n=0;for(let r=0;rthis._itemsChanged(r.toArray()))):Ft(n)?n.subscribe(r=>this._itemsChanged(r)):(this._items=n,this._initializeFocus()),typeof e.shouldActivationFollowFocus=="boolean"&&(this._shouldActivationFollowFocus=e.shouldActivationFollowFocus),e.horizontalOrientation&&(this._horizontalOrientation=e.horizontalOrientation),e.skipPredicate&&(this._skipPredicateFn=e.skipPredicate),e.trackBy&&(this._trackByFn=e.trackBy),typeof e.typeAheadDebounceInterval<"u"&&this._setTypeAhead(e.typeAheadDebounceInterval)}change=new S;destroy(){this._typeaheadSubscription.unsubscribe(),this._typeahead?.destroy(),this.change.complete()}onKeydown(n){switch(n.key){case"Tab":return;case"ArrowDown":this._focusNextItem();break;case"ArrowUp":this._focusPreviousItem();break;case"ArrowRight":this._horizontalOrientation==="rtl"?this._collapseCurrentItem():this._expandCurrentItem();break;case"ArrowLeft":this._horizontalOrientation==="rtl"?this._expandCurrentItem():this._collapseCurrentItem();break;case"Home":this._focusFirstItem();break;case"End":this._focusLastItem();break;case"Enter":case" ":this._activateCurrentItem();break;default:if(n.key==="*"){this._expandAllItemsAtCurrentItemLevel();break}this._typeahead?.handleKey(n);return}this._typeahead?.reset(),n.preventDefault()}getActiveItemIndex(){return this._activeItemIndex}getActiveItem(){return this._activeItem}_itemsChanged(n){this._hasInitialFocused&&this._activeItem&&!n.includes(this._activeItem)&&(this._activeItem=null,this._hasInitialFocused=!1),this._items=n,this._typeahead?.setItems(this._items),this._updateActiveItemIndex(this._items),this._initializeFocus()}_focusFirstItem(){this.focusItem(this._findNextAvailableItemIndex(-1))}_focusLastItem(){this.focusItem(this._findPreviousAvailableItemIndex(this._items.length))}_focusNextItem(){this.focusItem(this._findNextAvailableItemIndex(this._activeItemIndex))}_focusPreviousItem(){this.focusItem(this._findPreviousAvailableItemIndex(this._activeItemIndex))}focusItem(n,e={}){e.emitChangeEvent??=!0;let r=typeof n=="number"?n:this._items.findIndex(s=>this._trackByFn(s)===this._trackByFn(n));if(r<0||r>=this._items.length)return;let o=this._items[r];if(this._activeItem!==null&&this._trackByFn(o)===this._trackByFn(this._activeItem))return;let i=this._activeItem;this._activeItem=o??null,this._activeItemIndex=r,this._typeahead?.setCurrentSelectedItemIndex(r),this._activeItem?.focus(),i?.unfocus(),e.emitChangeEvent&&this.change.next(this._activeItem),this._shouldActivationFollowFocus&&this._activateCurrentItem()}_updateActiveItemIndex(n){let e=this._activeItem;if(!e)return;let r=n.findIndex(o=>this._trackByFn(o)===this._trackByFn(e));r>-1&&r!==this._activeItemIndex&&(this._activeItemIndex=r,this._typeahead?.setCurrentSelectedItemIndex(r))}_setTypeAhead(n){this._typeahead=new ci(this._items,{debounceInterval:typeof n=="number"?n:void 0,skipPredicate:e=>this._skipPredicateFn(e)}),this._typeaheadSubscription=this._typeahead.selectedItem.subscribe(e=>{this.focusItem(e)})}_findNextAvailableItemIndex(n){for(let e=n+1;e=0;e--)if(!this._skipPredicateFn(this._items[e]))return e;return n}_collapseCurrentItem(){if(this._activeItem)if(this._isCurrentItemExpanded())this._activeItem.collapse();else{let n=this._activeItem.getParent();if(!n||this._skipPredicateFn(n))return;this.focusItem(n)}}_expandCurrentItem(){this._activeItem&&(this._isCurrentItemExpanded()?eg(this._activeItem.getChildren()).pipe(Ue(1)).subscribe(n=>{let e=n.find(r=>!this._skipPredicateFn(r));e&&this.focusItem(e)}):this._activeItem.expand())}_isCurrentItemExpanded(){return this._activeItem?typeof this._activeItem.isExpanded=="boolean"?this._activeItem.isExpanded:this._activeItem.isExpanded():!1}_isItemDisabled(n){return typeof n.isDisabled=="boolean"?n.isDisabled:n.isDisabled?.()}_expandAllItemsAtCurrentItemLevel(){if(!this._activeItem)return;let n=this._activeItem.getParent(),e;n?e=eg(n.getChildren()):e=T(this._items.filter(r=>r.getParent()===null)),e.pipe(Ue(1)).subscribe(r=>{for(let o of r)o.expand()})}_activateCurrentItem(){this._activeItem?.activate()}},TZ=new y("tree-key-manager",{providedIn:"root",factory:()=>(t,n)=>new tg(t,n)});var ng={},ta=class t{_appId=f(hr);static _infix=`a${Math.floor(Math.random()*1e5).toString()}`;getId(n,e=!1){return this._appId!=="ng"&&(n+=this._appId),ng.hasOwnProperty(n)||(ng[n]=0),`${n}${e?t._infix+"-":""}${ng[n]++}`}static \u0275fac=function(e){return new(e||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})};var Xw=" ";function sk(t,n,e){let r=Du(t,n);e=e.trim(),!r.some(o=>o.trim()===e)&&(r.push(e),t.setAttribute(n,r.join(Xw)))}function ak(t,n,e){let r=Du(t,n);e=e.trim();let o=r.filter(i=>i!==e);o.length?t.setAttribute(n,o.join(Xw)):t.removeAttribute(n)}function Du(t,n){return t.getAttribute(n)?.match(/\S+/g)??[]}var Jw="cdk-describedby-message",_u="cdk-describedby-host",og=0,UZ=(()=>{class t{_platform=f(he);_document=f(L);_messageRegistry=new Map;_messagesContainer=null;_id=`${og++}`;constructor(){f(xt).load(vu),this._id=f(hr)+"-"+og++}describe(e,r,o){if(!this._canBeDescribed(e,r))return;let i=rg(r,o);typeof r!="string"?(Qw(r,this._id),this._messageRegistry.set(i,{messageElement:r,referenceCount:0})):this._messageRegistry.has(i)||this._createMessageElement(r,o),this._isElementDescribedByMessage(e,i)||this._addMessageReference(e,i)}removeDescription(e,r,o){if(!r||!this._isElementNode(e))return;let i=rg(r,o);if(this._isElementDescribedByMessage(e,i)&&this._removeMessageReference(e,i),typeof r=="string"){let s=this._messageRegistry.get(i);s&&s.referenceCount===0&&this._deleteMessageElement(i)}this._messagesContainer?.childNodes.length===0&&(this._messagesContainer.remove(),this._messagesContainer=null)}ngOnDestroy(){let e=this._document.querySelectorAll(`[${_u}="${this._id}"]`);for(let r=0;ro.indexOf(Jw)!=0);e.setAttribute("aria-describedby",r.join(" "))}_addMessageReference(e,r){let o=this._messageRegistry.get(r);sk(e,"aria-describedby",o.messageElement.id),e.setAttribute(_u,this._id),o.referenceCount++}_removeMessageReference(e,r){let o=this._messageRegistry.get(r);o.referenceCount--,ak(e,"aria-describedby",o.messageElement.id),e.removeAttribute(_u)}_isElementDescribedByMessage(e,r){let o=Du(e,"aria-describedby"),i=this._messageRegistry.get(r),s=i&&i.messageElement.id;return!!s&&o.indexOf(s)!=-1}_canBeDescribed(e,r){if(!this._isElementNode(e))return!1;if(r&&typeof r=="object")return!0;let o=r==null?"":`${r}`.trim(),i=e.getAttribute("aria-label");return o?!i||i.trim()!==o:!1}_isElementNode(e){return e.nodeType===this._document.ELEMENT_NODE}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function rg(t,n){return typeof t=="string"?`${n||""}/${t}`:t}function Qw(t,n){t.id||(t.id=`${Jw}-${n}-${og++}`)}var en=(function(t){return t[t.NORMAL=0]="NORMAL",t[t.NEGATED=1]="NEGATED",t[t.INVERTED=2]="INVERTED",t})(en||{}),Eu,lo;function wu(){if(lo==null){if(typeof document!="object"||!document||typeof Element!="function"||!Element)return lo=!1,lo;if(document.documentElement?.style&&"scrollBehavior"in document.documentElement.style)lo=!0;else{let t=Element.prototype.scrollTo;t?lo=!/\{\s*\[native code\]\s*\}/.test(t.toString()):lo=!1}}return lo}function ui(){if(typeof document!="object"||!document)return en.NORMAL;if(Eu==null){let t=document.createElement("div"),n=t.style;t.dir="rtl",n.width="1px",n.overflow="auto",n.visibility="hidden",n.pointerEvents="none",n.position="absolute";let e=document.createElement("div"),r=e.style;r.width="2px",r.height="1px",t.appendChild(e),document.body.appendChild(t),Eu=en.NORMAL,t.scrollLeft===0&&(t.scrollLeft=1,Eu=t.scrollLeft===0?en.NEGATED:en.INVERTED),t.remove()}return Eu}function ig(){return typeof __karma__<"u"&&!!__karma__||typeof jasmine<"u"&&!!jasmine||typeof jest<"u"&&!!jest||typeof Mocha<"u"&&!!Mocha}var di,eC=["color","button","checkbox","date","datetime-local","email","file","hidden","image","month","number","password","radio","range","reset","search","submit","tel","text","time","url","week"];function QZ(){if(di)return di;if(typeof document!="object"||!document)return di=new Set(eC),di;let t=document.createElement("input");return di=new Set(eC.filter(n=>(t.setAttribute("type",n),t.type===n))),di}var n7={XSmall:"(max-width: 599.98px)",Small:"(min-width: 600px) and (max-width: 959.98px)",Medium:"(min-width: 960px) and (max-width: 1279.98px)",Large:"(min-width: 1280px) and (max-width: 1919.98px)",XLarge:"(min-width: 1920px)",Handset:"(max-width: 599.98px) and (orientation: portrait), (max-width: 959.98px) and (orientation: landscape)",Tablet:"(min-width: 600px) and (max-width: 839.98px) and (orientation: portrait), (min-width: 960px) and (max-width: 1279.98px) and (orientation: landscape)",Web:"(min-width: 840px) and (orientation: portrait), (min-width: 1280px) and (orientation: landscape)",HandsetPortrait:"(max-width: 599.98px) and (orientation: portrait)",TabletPortrait:"(min-width: 600px) and (max-width: 839.98px) and (orientation: portrait)",WebPortrait:"(min-width: 840px) and (orientation: portrait)",HandsetLandscape:"(max-width: 959.98px) and (orientation: landscape)",TabletLandscape:"(min-width: 960px) and (max-width: 1279.98px) and (orientation: landscape)",WebLandscape:"(min-width: 1280px) and (orientation: landscape)"};var ck=new y("MATERIAL_ANIMATIONS"),tC=null;function lk(){return f(ck,{optional:!0})?.animationsDisabled||f(Is,{optional:!0})==="NoopAnimations"?"di-disabled":(tC??=f(yu).matchMedia("(prefers-reduced-motion)").matches,tC?"reduced-motion":"enabled")}function mr(){return lk()!=="enabled"}function De(t){return t==null?"":typeof t=="string"?t:`${t}px`}function l7(t){return t!=null&&`${t}`!="false"}var At=(function(t){return t[t.FADING_IN=0]="FADING_IN",t[t.VISIBLE=1]="VISIBLE",t[t.FADING_OUT=2]="FADING_OUT",t[t.HIDDEN=3]="HIDDEN",t})(At||{}),sg=class{_renderer;element;config;_animationForciblyDisabledThroughCss;state=At.HIDDEN;constructor(n,e,r,o=!1){this._renderer=n,this.element=e,this.config=r,this._animationForciblyDisabledThroughCss=o}fadeOut(){this._renderer.fadeOutRipple(this)}},nC=ai({passive:!0,capture:!0}),ag=class{_events=new Map;addHandler(n,e,r,o){let i=this._events.get(e);if(i){let s=i.get(r);s?s.add(o):i.set(r,new Set([o]))}else this._events.set(e,new Map([[r,new Set([o])]])),n.runOutsideAngular(()=>{document.addEventListener(e,this._delegateEventHandler,nC)})}removeHandler(n,e,r){let o=this._events.get(n);if(!o)return;let i=o.get(e);i&&(i.delete(r),i.size===0&&o.delete(e),o.size===0&&(this._events.delete(n),document.removeEventListener(n,this._delegateEventHandler,nC)))}_delegateEventHandler=n=>{let e=Je(n);e&&this._events.get(n.type)?.forEach((r,o)=>{(o===e||o.contains(e))&&r.forEach(i=>i.handleEvent(n))})}},na={enterDuration:225,exitDuration:150},uk=800,rC=ai({passive:!0,capture:!0}),oC=["mousedown","touchstart"],iC=["mouseup","mouseleave","touchend","touchcancel"],dk=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=ke({type:t,selectors:[["ng-component"]],hostAttrs:["mat-ripple-style-loader",""],decls:0,vars:0,template:function(r,o){},styles:[`.mat-ripple { - overflow: hidden; - position: relative; -} -.mat-ripple:not(:empty) { - transform: translateZ(0); -} - -.mat-ripple.mat-ripple-unbounded { - overflow: visible; -} - -.mat-ripple-element { - position: absolute; - border-radius: 50%; - pointer-events: none; - transition: opacity, transform 0ms cubic-bezier(0, 0, 0.2, 1); - transform: scale3d(0, 0, 0); - background-color: var(--mat-ripple-color, color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent)); -} -@media (forced-colors: active) { - .mat-ripple-element { - display: none; - } -} -.cdk-drag-preview .mat-ripple-element, .cdk-drag-placeholder .mat-ripple-element { - display: none; -} -`],encapsulation:2,changeDetection:0})}return t})(),ra=class t{_target;_ngZone;_platform;_containerElement;_triggerElement=null;_isPointerDown=!1;_activeRipples=new Map;_mostRecentTransientRipple=null;_lastTouchStartEvent;_pointerUpEventsRegistered=!1;_containerRect=null;static _eventManager=new ag;constructor(n,e,r,o,i){this._target=n,this._ngZone=e,this._platform=o,o.isBrowser&&(this._containerElement=Tt(r)),i&&i.get(xt).load(dk)}fadeInRipple(n,e,r={}){let o=this._containerRect=this._containerRect||this._containerElement.getBoundingClientRect(),i=g(g({},na),r.animation);r.centered&&(n=o.left+o.width/2,e=o.top+o.height/2);let s=r.radius||fk(n,e,o),a=n-o.left,c=e-o.top,l=i.enterDuration,u=document.createElement("div");u.classList.add("mat-ripple-element"),u.style.left=`${a-s}px`,u.style.top=`${c-s}px`,u.style.height=`${s*2}px`,u.style.width=`${s*2}px`,r.color!=null&&(u.style.backgroundColor=r.color),u.style.transitionDuration=`${l}ms`,this._containerElement.appendChild(u);let d=window.getComputedStyle(u),h=d.transitionProperty,p=d.transitionDuration,m=h==="none"||p==="0s"||p==="0s, 0s"||o.width===0&&o.height===0,_=new sg(this,u,r,m);u.style.transform="scale3d(1, 1, 1)",_.state=At.FADING_IN,r.persistent||(this._mostRecentTransientRipple=_);let E=null;return!m&&(l||i.exitDuration)&&this._ngZone.runOutsideAngular(()=>{let I=()=>{E&&(E.fallbackTimer=null),clearTimeout(Pe),this._finishRippleTransition(_)},ee=()=>this._destroyRipple(_),Pe=setTimeout(ee,l+100);u.addEventListener("transitionend",I),u.addEventListener("transitioncancel",ee),E={onTransitionEnd:I,onTransitionCancel:ee,fallbackTimer:Pe}}),this._activeRipples.set(_,E),(m||!l)&&this._finishRippleTransition(_),_}fadeOutRipple(n){if(n.state===At.FADING_OUT||n.state===At.HIDDEN)return;let e=n.element,r=g(g({},na),n.config.animation);e.style.transitionDuration=`${r.exitDuration}ms`,e.style.opacity="0",n.state=At.FADING_OUT,(n._animationForciblyDisabledThroughCss||!r.exitDuration)&&this._finishRippleTransition(n)}fadeOutAll(){this._getActiveRipples().forEach(n=>n.fadeOut())}fadeOutAllNonPersistent(){this._getActiveRipples().forEach(n=>{n.config.persistent||n.fadeOut()})}setupTriggerEvents(n){let e=Tt(n);!this._platform.isBrowser||!e||e===this._triggerElement||(this._removeTriggerEvents(),this._triggerElement=e,oC.forEach(r=>{t._eventManager.addHandler(this._ngZone,r,e,this)}))}handleEvent(n){n.type==="mousedown"?this._onMousedown(n):n.type==="touchstart"?this._onTouchStart(n):this._onPointerUp(),this._pointerUpEventsRegistered||(this._ngZone.runOutsideAngular(()=>{iC.forEach(e=>{this._triggerElement.addEventListener(e,this,rC)})}),this._pointerUpEventsRegistered=!0)}_finishRippleTransition(n){n.state===At.FADING_IN?this._startFadeOutTransition(n):n.state===At.FADING_OUT&&this._destroyRipple(n)}_startFadeOutTransition(n){let e=n===this._mostRecentTransientRipple,{persistent:r}=n.config;n.state=At.VISIBLE,!r&&(!e||!this._isPointerDown)&&n.fadeOut()}_destroyRipple(n){let e=this._activeRipples.get(n)??null;this._activeRipples.delete(n),this._activeRipples.size||(this._containerRect=null),n===this._mostRecentTransientRipple&&(this._mostRecentTransientRipple=null),n.state=At.HIDDEN,e!==null&&(n.element.removeEventListener("transitionend",e.onTransitionEnd),n.element.removeEventListener("transitioncancel",e.onTransitionCancel),e.fallbackTimer!==null&&clearTimeout(e.fallbackTimer)),n.element.remove()}_onMousedown(n){let e=Qs(n),r=this._lastTouchStartEvent&&Date.now(){let e=n.state===At.VISIBLE||n.config.terminateOnPointerUp&&n.state===At.FADING_IN;!n.config.persistent&&e&&n.fadeOut()}))}_getActiveRipples(){return Array.from(this._activeRipples.keys())}_removeTriggerEvents(){let n=this._triggerElement;n&&(oC.forEach(e=>t._eventManager.removeHandler(e,n,this)),this._pointerUpEventsRegistered&&(iC.forEach(e=>n.removeEventListener(e,this,rC)),this._pointerUpEventsRegistered=!1))}};function fk(t,n,e){let r=Math.max(Math.abs(t-e.left),Math.abs(t-e.right)),o=Math.max(Math.abs(n-e.top),Math.abs(n-e.bottom));return Math.sqrt(r*r+o*o)}var cg=new y("mat-ripple-global-options"),C7=(()=>{class t{_elementRef=f(z);_animationsDisabled=mr();color;unbounded=!1;centered=!1;radius=0;animation;get disabled(){return this._disabled}set disabled(e){e&&this.fadeOutAllNonPersistent(),this._disabled=e,this._setupTriggerEventsIfEnabled()}_disabled=!1;get trigger(){return this._trigger||this._elementRef.nativeElement}set trigger(e){this._trigger=e,this._setupTriggerEventsIfEnabled()}_trigger;_rippleRenderer;_globalOptions;_isInitialized=!1;constructor(){let e=f(j),r=f(he),o=f(cg,{optional:!0}),i=f($);this._globalOptions=o||{},this._rippleRenderer=new ra(this,e,this._elementRef,r,i)}ngOnInit(){this._isInitialized=!0,this._setupTriggerEventsIfEnabled()}ngOnDestroy(){this._rippleRenderer._removeTriggerEvents()}fadeOutAll(){this._rippleRenderer.fadeOutAll()}fadeOutAllNonPersistent(){this._rippleRenderer.fadeOutAllNonPersistent()}get rippleConfig(){return{centered:this.centered,radius:this.radius,color:this.color,animation:g(g(g({},this._globalOptions.animation),this._animationsDisabled?{enterDuration:0,exitDuration:0}:{}),this.animation),terminateOnPointerUp:this._globalOptions.terminateOnPointerUp}}get rippleDisabled(){return this.disabled||!!this._globalOptions.disabled}_setupTriggerEventsIfEnabled(){!this.disabled&&this._isInitialized&&this._rippleRenderer.setupTriggerEvents(this.trigger)}launch(e,r=0,o){return typeof e=="number"?this._rippleRenderer.fadeInRipple(e,r,g(g({},this.rippleConfig),o)):this._rippleRenderer.fadeInRipple(0,0,g(g({},this.rippleConfig),e))}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["","mat-ripple",""],["","matRipple",""]],hostAttrs:[1,"mat-ripple"],hostVars:2,hostBindings:function(r,o){r&2&&Xe("mat-ripple-unbounded",o.unbounded)},inputs:{color:[0,"matRippleColor","color"],unbounded:[0,"matRippleUnbounded","unbounded"],centered:[0,"matRippleCentered","centered"],radius:[0,"matRippleRadius","radius"],animation:[0,"matRippleAnimation","animation"],disabled:[0,"matRippleDisabled","disabled"],trigger:[0,"matRippleTrigger","trigger"]},exportAs:["matRipple"]})}return t})();var hk={capture:!0},pk=["focus","mousedown","mouseenter","touchstart"],lg="mat-ripple-loader-uninitialized",ug="mat-ripple-loader-class-name",sC="mat-ripple-loader-centered",Cu="mat-ripple-loader-disabled",aC=(()=>{class t{_document=f(L);_animationsDisabled=mr();_globalRippleOptions=f(cg,{optional:!0});_platform=f(he);_ngZone=f(j);_injector=f($);_eventCleanups;_hosts=new Map;constructor(){let e=f(je).createRenderer(null,null);this._eventCleanups=this._ngZone.runOutsideAngular(()=>pk.map(r=>e.listen(this._document,r,this._onInteraction,hk)))}ngOnDestroy(){let e=this._hosts.keys();for(let r of e)this.destroyRipple(r);this._eventCleanups.forEach(r=>r())}configureRipple(e,r){e.setAttribute(lg,this._globalRippleOptions?.namespace??""),(r.className||!e.hasAttribute(ug))&&e.setAttribute(ug,r.className||""),r.centered&&e.setAttribute(sC,""),r.disabled&&e.setAttribute(Cu,"")}setDisabled(e,r){let o=this._hosts.get(e);o?(o.target.rippleDisabled=r,!r&&!o.hasSetUpEvents&&(o.hasSetUpEvents=!0,o.renderer.setupTriggerEvents(e))):r?e.setAttribute(Cu,""):e.removeAttribute(Cu)}_onInteraction=e=>{let r=Je(e);if(r instanceof HTMLElement){let o=r.closest(`[${lg}="${this._globalRippleOptions?.namespace??""}"]`);o&&this._createRipple(o)}};_createRipple(e){if(!this._document||this._hosts.has(e))return;e.querySelector(".mat-ripple")?.remove();let r=this._document.createElement("span");r.classList.add("mat-ripple",e.getAttribute(ug)),e.append(r);let o=this._globalRippleOptions,i=this._animationsDisabled?0:o?.animation?.enterDuration??na.enterDuration,s=this._animationsDisabled?0:o?.animation?.exitDuration??na.exitDuration,a={rippleDisabled:this._animationsDisabled||o?.disabled||e.hasAttribute(Cu),rippleConfig:{centered:e.hasAttribute(sC),terminateOnPointerUp:o?.terminateOnPointerUp,animation:{enterDuration:i,exitDuration:s}}},c=new ra(a,this._ngZone,r,this._platform,this._injector),l=!a.rippleDisabled;l&&c.setupTriggerEvents(e),this._hosts.set(e,{target:a,renderer:c,hasSetUpEvents:l}),e.removeAttribute(lg)}destroyRipple(e){let r=this._hosts.get(e);r&&(r.renderer._removeTriggerEvents(),this._hosts.delete(e))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var cC=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=ke({type:t,selectors:[["structural-styles"]],decls:0,vars:0,template:function(r,o){},styles:[`.mat-focus-indicator { - position: relative; -} -.mat-focus-indicator::before { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - box-sizing: border-box; - pointer-events: none; - display: var(--mat-focus-indicator-display, none); - border-width: var(--mat-focus-indicator-border-width, 3px); - border-style: var(--mat-focus-indicator-border-style, solid); - border-color: var(--mat-focus-indicator-border-color, transparent); - border-radius: var(--mat-focus-indicator-border-radius, 4px); -} -.mat-focus-indicator:focus-visible::before { - content: ""; -} - -@media (forced-colors: active) { - html { - --mat-focus-indicator-display: block; - } -} -`],encapsulation:2,changeDetection:0})}return t})();var mk=["mat-icon-button",""],gk=["*"],vk=new y("MAT_BUTTON_CONFIG");function lC(t){return t==null?void 0:hm(t)}var dg=(()=>{class t{_elementRef=f(z);_ngZone=f(j);_animationsDisabled=mr();_config=f(vk,{optional:!0});_focusMonitor=f(pu);_cleanupClick;_renderer=f(Oe);_rippleLoader=f(aC);_isAnchor;_isFab=!1;color;get disableRipple(){return this._disableRipple}set disableRipple(e){this._disableRipple=e,this._updateRippleDisabled()}_disableRipple=!1;get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._updateRippleDisabled()}_disabled=!1;ariaDisabled;disabledInteractive;tabIndex;set _tabindex(e){this.tabIndex=e}constructor(){f(xt).load(cC);let e=this._elementRef.nativeElement;this._isAnchor=e.tagName==="A",this.disabledInteractive=this._config?.disabledInteractive??!1,this.color=this._config?.color??null,this._rippleLoader?.configureRipple(e,{className:"mat-mdc-button-ripple"})}ngAfterViewInit(){this._focusMonitor.monitor(this._elementRef,!0),this._isAnchor&&this._setupAsAnchor()}ngOnDestroy(){this._cleanupClick?.(),this._focusMonitor.stopMonitoring(this._elementRef),this._rippleLoader?.destroyRipple(this._elementRef.nativeElement)}focus(e="program",r){e?this._focusMonitor.focusVia(this._elementRef.nativeElement,e,r):this._elementRef.nativeElement.focus(r)}_getAriaDisabled(){return this.ariaDisabled!=null?this.ariaDisabled:this._isAnchor?this.disabled||null:this.disabled&&this.disabledInteractive?!0:null}_getDisabledAttribute(){return this.disabledInteractive||!this.disabled?null:!0}_updateRippleDisabled(){this._rippleLoader?.setDisabled(this._elementRef.nativeElement,this.disableRipple||this.disabled)}_getTabIndex(){return this._isAnchor?this.disabled&&!this.disabledInteractive?-1:this.tabIndex:this.tabIndex}_setupAsAnchor(){this._cleanupClick=this._ngZone.runOutsideAngular(()=>this._renderer.listen(this._elementRef.nativeElement,"click",e=>{this.disabled&&(e.preventDefault(),e.stopImmediatePropagation())}))}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,hostAttrs:[1,"mat-mdc-button-base"],hostVars:13,hostBindings:function(r,o){r&2&&(Yt("disabled",o._getDisabledAttribute())("aria-disabled",o._getAriaDisabled())("tabindex",o._getTabIndex()),Yp(o.color?"mat-"+o.color:""),Xe("mat-mdc-button-disabled",o.disabled)("mat-mdc-button-disabled-interactive",o.disabledInteractive)("mat-unthemed",!o.color)("_mat-animation-noopable",o._animationsDisabled))},inputs:{color:"color",disableRipple:[2,"disableRipple","disableRipple",ue],disabled:[2,"disabled","disabled",ue],ariaDisabled:[2,"aria-disabled","ariaDisabled",ue],disabledInteractive:[2,"disabledInteractive","disabledInteractive",ue],tabIndex:[2,"tabIndex","tabIndex",lC],_tabindex:[2,"tabindex","_tabindex",lC]}})}return t})(),yk=(()=>{class t extends dg{constructor(){super(),this._rippleLoader.configureRipple(this._elementRef.nativeElement,{centered:!0})}static \u0275fac=function(r){return new(r||t)};static \u0275cmp=ke({type:t,selectors:[["button","mat-icon-button",""],["a","mat-icon-button",""],["button","matIconButton",""],["a","matIconButton",""]],hostAttrs:[1,"mdc-icon-button","mat-mdc-icon-button"],exportAs:["matButton","matAnchor"],features:[J],attrs:mk,ngContentSelectors:gk,decls:4,vars:0,consts:[[1,"mat-mdc-button-persistent-ripple","mdc-icon-button__ripple"],[1,"mat-focus-indicator"],[1,"mat-mdc-button-touch-target"]],template:function(r,o){r&1&&(ro(),yn(0,"span",0),Vn(1),yn(2,"span",1)(3,"span",2))},styles:[`.mat-mdc-icon-button { - -webkit-user-select: none; - user-select: none; - display: inline-block; - position: relative; - box-sizing: border-box; - border: none; - outline: none; - background-color: transparent; - fill: currentColor; - text-decoration: none; - cursor: pointer; - z-index: 0; - overflow: visible; - border-radius: var(--mat-icon-button-container-shape, var(--mat-sys-corner-full, 50%)); - flex-shrink: 0; - text-align: center; - width: var(--mat-icon-button-state-layer-size, 40px); - height: var(--mat-icon-button-state-layer-size, 40px); - padding: calc(calc(var(--mat-icon-button-state-layer-size, 40px) - var(--mat-icon-button-icon-size, 24px)) / 2); - font-size: var(--mat-icon-button-icon-size, 24px); - color: var(--mat-icon-button-icon-color, var(--mat-sys-on-surface-variant)); - -webkit-tap-highlight-color: transparent; -} -.mat-mdc-icon-button .mat-mdc-button-ripple, -.mat-mdc-icon-button .mat-mdc-button-persistent-ripple, -.mat-mdc-icon-button .mat-mdc-button-persistent-ripple::before { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - pointer-events: none; - border-radius: inherit; -} -.mat-mdc-icon-button .mat-mdc-button-ripple { - overflow: hidden; -} -.mat-mdc-icon-button .mat-mdc-button-persistent-ripple::before { - content: ""; - opacity: 0; -} -.mat-mdc-icon-button .mdc-button__label, -.mat-mdc-icon-button .mat-icon { - z-index: 1; - position: relative; -} -.mat-mdc-icon-button .mat-focus-indicator { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - border-radius: inherit; -} -.mat-mdc-icon-button:focus-visible > .mat-focus-indicator::before { - content: ""; - border-radius: inherit; -} -.mat-mdc-icon-button .mat-ripple-element { - background-color: var(--mat-icon-button-ripple-color, color-mix(in srgb, var(--mat-sys-on-surface-variant) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); -} -.mat-mdc-icon-button .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-icon-button-state-layer-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-icon-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-icon-button-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-icon-button:hover > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-icon-button-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); -} -.mat-mdc-icon-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-icon-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-icon-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-icon-button-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); -} -.mat-mdc-icon-button:active > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-icon-button-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); -} -.mat-mdc-icon-button .mat-mdc-button-touch-target { - position: absolute; - top: 50%; - height: var(--mat-icon-button-touch-target-size, 48px); - display: var(--mat-icon-button-touch-target-display, block); - left: 50%; - width: var(--mat-icon-button-touch-target-size, 48px); - transform: translate(-50%, -50%); -} -.mat-mdc-icon-button._mat-animation-noopable { - transition: none !important; - animation: none !important; -} -.mat-mdc-icon-button[disabled], .mat-mdc-icon-button.mat-mdc-button-disabled { - cursor: default; - pointer-events: none; - color: var(--mat-icon-button-disabled-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-mdc-icon-button.mat-mdc-button-disabled-interactive { - pointer-events: auto; -} -.mat-mdc-icon-button img, -.mat-mdc-icon-button svg { - width: var(--mat-icon-button-icon-size, 24px); - height: var(--mat-icon-button-icon-size, 24px); - vertical-align: baseline; -} -.mat-mdc-icon-button .mat-mdc-button-persistent-ripple { - border-radius: var(--mat-icon-button-container-shape, var(--mat-sys-corner-full, 50%)); -} -.mat-mdc-icon-button[hidden] { - display: none; -} -.mat-mdc-icon-button.mat-unthemed:not(.mdc-ripple-upgraded):focus::before, .mat-mdc-icon-button.mat-primary:not(.mdc-ripple-upgraded):focus::before, .mat-mdc-icon-button.mat-accent:not(.mdc-ripple-upgraded):focus::before, .mat-mdc-icon-button.mat-warn:not(.mdc-ripple-upgraded):focus::before { - background: transparent; - opacity: 1; -} -`,`@media (forced-colors: active) { - .mat-mdc-button:not(.mdc-button--outlined), - .mat-mdc-unelevated-button:not(.mdc-button--outlined), - .mat-mdc-raised-button:not(.mdc-button--outlined), - .mat-mdc-outlined-button:not(.mdc-button--outlined), - .mat-mdc-button-base.mat-tonal-button, - .mat-mdc-icon-button.mat-mdc-icon-button, - .mat-mdc-outlined-button .mdc-button__ripple { - outline: solid 1px; - } -} -`],encapsulation:2,changeDetection:0})}return t})();var bk=new y("cdk-dir-doc",{providedIn:"root",factory:()=>f(L)}),_k=/^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_](Adlm|Arab|Hebr|Nkoo|Rohg|Thaa))(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)/i;function uC(t){let n=t?.toLowerCase()||"";return n==="auto"&&typeof navigator<"u"&&navigator?.language?_k.test(navigator.language)?"rtl":"ltr":n==="rtl"?"rtl":"ltr"}var fi=(()=>{class t{get value(){return this.valueSignal()}valueSignal=W("ltr");change=new U;constructor(){let e=f(bk,{optional:!0});if(e){let r=e.body?e.body.dir:null,o=e.documentElement?e.documentElement.dir:null;this.valueSignal.set(uC(r||o||"ltr"))}}ngOnDestroy(){this.change.complete()}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var zn=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({})}return t})();var dC=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({imports:[zn]})}return t})();var Dk=["matButton",""],Ek=[[["",8,"material-icons",3,"iconPositionEnd",""],["mat-icon",3,"iconPositionEnd",""],["","matButtonIcon","",3,"iconPositionEnd",""]],"*",[["","iconPositionEnd","",8,"material-icons"],["mat-icon","iconPositionEnd",""],["","matButtonIcon","","iconPositionEnd",""]]],wk=[".material-icons:not([iconPositionEnd]), mat-icon:not([iconPositionEnd]), [matButtonIcon]:not([iconPositionEnd])","*",".material-icons[iconPositionEnd], mat-icon[iconPositionEnd], [matButtonIcon][iconPositionEnd]"];var fC=new Map([["text",["mat-mdc-button"]],["filled",["mdc-button--unelevated","mat-mdc-unelevated-button"]],["elevated",["mdc-button--raised","mat-mdc-raised-button"]],["outlined",["mdc-button--outlined","mat-mdc-outlined-button"]],["tonal",["mat-tonal-button"]]]),nK=(()=>{class t extends dg{get appearance(){return this._appearance}set appearance(e){this.setAppearance(e||this._config?.defaultAppearance||"text")}_appearance=null;constructor(){super();let e=Ck(this._elementRef.nativeElement);e&&this.setAppearance(e)}setAppearance(e){if(e===this._appearance)return;let r=this._elementRef.nativeElement.classList,o=this._appearance?fC.get(this._appearance):null,i=fC.get(e);o&&r.remove(...o),r.add(...i),this._appearance=e}static \u0275fac=function(r){return new(r||t)};static \u0275cmp=ke({type:t,selectors:[["button","matButton",""],["a","matButton",""],["button","mat-button",""],["button","mat-raised-button",""],["button","mat-flat-button",""],["button","mat-stroked-button",""],["a","mat-button",""],["a","mat-raised-button",""],["a","mat-flat-button",""],["a","mat-stroked-button",""]],hostAttrs:[1,"mdc-button"],inputs:{appearance:[0,"matButton","appearance"]},exportAs:["matButton","matAnchor"],features:[J],attrs:Dk,ngContentSelectors:wk,decls:7,vars:4,consts:[[1,"mat-mdc-button-persistent-ripple"],[1,"mdc-button__label"],[1,"mat-focus-indicator"],[1,"mat-mdc-button-touch-target"]],template:function(r,o){r&1&&(ro(Ek),yn(0,"span",0),Vn(1),to(2,"span",1),Vn(3,1),no(),Vn(4,2),yn(5,"span",2)(6,"span",3)),r&2&&Xe("mdc-button__ripple",!o._isFab)("mdc-fab__ripple",o._isFab)},styles:[`.mat-mdc-button-base { - text-decoration: none; -} -.mat-mdc-button-base .mat-icon { - min-height: fit-content; - flex-shrink: 0; -} -@media (hover: none) { - .mat-mdc-button-base:hover > span.mat-mdc-button-persistent-ripple::before { - opacity: 0; - } -} - -.mdc-button { - -webkit-user-select: none; - user-select: none; - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - min-width: 64px; - border: none; - outline: none; - line-height: inherit; - -webkit-appearance: none; - overflow: visible; - vertical-align: middle; - background: transparent; - padding: 0 8px; -} -.mdc-button::-moz-focus-inner { - padding: 0; - border: 0; -} -.mdc-button:active { - outline: none; -} -.mdc-button:hover { - cursor: pointer; -} -.mdc-button:disabled { - cursor: default; - pointer-events: none; -} -.mdc-button[hidden] { - display: none; -} -.mdc-button .mdc-button__label { - position: relative; -} - -.mat-mdc-button { - padding: 0 var(--mat-button-text-horizontal-padding, 12px); - height: var(--mat-button-text-container-height, 40px); - font-family: var(--mat-button-text-label-text-font, var(--mat-sys-label-large-font)); - font-size: var(--mat-button-text-label-text-size, var(--mat-sys-label-large-size)); - letter-spacing: var(--mat-button-text-label-text-tracking, var(--mat-sys-label-large-tracking)); - text-transform: var(--mat-button-text-label-text-transform); - font-weight: var(--mat-button-text-label-text-weight, var(--mat-sys-label-large-weight)); -} -.mat-mdc-button, .mat-mdc-button .mdc-button__ripple { - border-radius: var(--mat-button-text-container-shape, var(--mat-sys-corner-full)); -} -.mat-mdc-button:not(:disabled) { - color: var(--mat-button-text-label-text-color, var(--mat-sys-primary)); -} -.mat-mdc-button[disabled], .mat-mdc-button.mat-mdc-button-disabled { - cursor: default; - pointer-events: none; - color: var(--mat-button-text-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-mdc-button.mat-mdc-button-disabled-interactive { - pointer-events: auto; -} -.mat-mdc-button:has(.material-icons, mat-icon, [matButtonIcon]) { - padding: 0 var(--mat-button-text-with-icon-horizontal-padding, 16px); -} -.mat-mdc-button > .mat-icon { - margin-right: var(--mat-button-text-icon-spacing, 8px); - margin-left: var(--mat-button-text-icon-offset, -4px); -} -[dir=rtl] .mat-mdc-button > .mat-icon { - margin-right: var(--mat-button-text-icon-offset, -4px); - margin-left: var(--mat-button-text-icon-spacing, 8px); -} -.mat-mdc-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-text-icon-offset, -4px); - margin-left: var(--mat-button-text-icon-spacing, 8px); -} -[dir=rtl] .mat-mdc-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-text-icon-spacing, 8px); - margin-left: var(--mat-button-text-icon-offset, -4px); -} -.mat-mdc-button .mat-ripple-element { - background-color: var(--mat-button-text-ripple-color, color-mix(in srgb, var(--mat-sys-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); -} -.mat-mdc-button .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-text-state-layer-color, var(--mat-sys-primary)); -} -.mat-mdc-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-text-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-button:hover > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-text-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); -} -.mat-mdc-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-text-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); -} -.mat-mdc-button:active > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-text-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); -} -.mat-mdc-button .mat-mdc-button-touch-target { - position: absolute; - top: 50%; - height: var(--mat-button-text-touch-target-size, 48px); - display: var(--mat-button-text-touch-target-display, block); - left: 0; - right: 0; - transform: translateY(-50%); -} - -.mat-mdc-unelevated-button { - transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); - height: var(--mat-button-filled-container-height, 40px); - font-family: var(--mat-button-filled-label-text-font, var(--mat-sys-label-large-font)); - font-size: var(--mat-button-filled-label-text-size, var(--mat-sys-label-large-size)); - letter-spacing: var(--mat-button-filled-label-text-tracking, var(--mat-sys-label-large-tracking)); - text-transform: var(--mat-button-filled-label-text-transform); - font-weight: var(--mat-button-filled-label-text-weight, var(--mat-sys-label-large-weight)); - padding: 0 var(--mat-button-filled-horizontal-padding, 24px); -} -.mat-mdc-unelevated-button > .mat-icon { - margin-right: var(--mat-button-filled-icon-spacing, 8px); - margin-left: var(--mat-button-filled-icon-offset, -8px); -} -[dir=rtl] .mat-mdc-unelevated-button > .mat-icon { - margin-right: var(--mat-button-filled-icon-offset, -8px); - margin-left: var(--mat-button-filled-icon-spacing, 8px); -} -.mat-mdc-unelevated-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-filled-icon-offset, -8px); - margin-left: var(--mat-button-filled-icon-spacing, 8px); -} -[dir=rtl] .mat-mdc-unelevated-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-filled-icon-spacing, 8px); - margin-left: var(--mat-button-filled-icon-offset, -8px); -} -.mat-mdc-unelevated-button .mat-ripple-element { - background-color: var(--mat-button-filled-ripple-color, color-mix(in srgb, var(--mat-sys-on-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); -} -.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-filled-state-layer-color, var(--mat-sys-on-primary)); -} -.mat-mdc-unelevated-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-filled-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-unelevated-button:hover > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-filled-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); -} -.mat-mdc-unelevated-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-unelevated-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-unelevated-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-filled-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); -} -.mat-mdc-unelevated-button:active > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-filled-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); -} -.mat-mdc-unelevated-button .mat-mdc-button-touch-target { - position: absolute; - top: 50%; - height: var(--mat-button-filled-touch-target-size, 48px); - display: var(--mat-button-filled-touch-target-display, block); - left: 0; - right: 0; - transform: translateY(-50%); -} -.mat-mdc-unelevated-button:not(:disabled) { - color: var(--mat-button-filled-label-text-color, var(--mat-sys-on-primary)); - background-color: var(--mat-button-filled-container-color, var(--mat-sys-primary)); -} -.mat-mdc-unelevated-button, .mat-mdc-unelevated-button .mdc-button__ripple { - border-radius: var(--mat-button-filled-container-shape, var(--mat-sys-corner-full)); -} -.mat-mdc-unelevated-button[disabled], .mat-mdc-unelevated-button.mat-mdc-button-disabled { - cursor: default; - pointer-events: none; - color: var(--mat-button-filled-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); - background-color: var(--mat-button-filled-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); -} -.mat-mdc-unelevated-button.mat-mdc-button-disabled-interactive { - pointer-events: auto; -} - -.mat-mdc-raised-button { - transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: var(--mat-button-protected-container-elevation-shadow, var(--mat-sys-level1)); - height: var(--mat-button-protected-container-height, 40px); - font-family: var(--mat-button-protected-label-text-font, var(--mat-sys-label-large-font)); - font-size: var(--mat-button-protected-label-text-size, var(--mat-sys-label-large-size)); - letter-spacing: var(--mat-button-protected-label-text-tracking, var(--mat-sys-label-large-tracking)); - text-transform: var(--mat-button-protected-label-text-transform); - font-weight: var(--mat-button-protected-label-text-weight, var(--mat-sys-label-large-weight)); - padding: 0 var(--mat-button-protected-horizontal-padding, 24px); -} -.mat-mdc-raised-button > .mat-icon { - margin-right: var(--mat-button-protected-icon-spacing, 8px); - margin-left: var(--mat-button-protected-icon-offset, -8px); -} -[dir=rtl] .mat-mdc-raised-button > .mat-icon { - margin-right: var(--mat-button-protected-icon-offset, -8px); - margin-left: var(--mat-button-protected-icon-spacing, 8px); -} -.mat-mdc-raised-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-protected-icon-offset, -8px); - margin-left: var(--mat-button-protected-icon-spacing, 8px); -} -[dir=rtl] .mat-mdc-raised-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-protected-icon-spacing, 8px); - margin-left: var(--mat-button-protected-icon-offset, -8px); -} -.mat-mdc-raised-button .mat-ripple-element { - background-color: var(--mat-button-protected-ripple-color, color-mix(in srgb, var(--mat-sys-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); -} -.mat-mdc-raised-button .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-protected-state-layer-color, var(--mat-sys-primary)); -} -.mat-mdc-raised-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-protected-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-raised-button:hover > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-protected-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); -} -.mat-mdc-raised-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-raised-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-raised-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-protected-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); -} -.mat-mdc-raised-button:active > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-protected-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); -} -.mat-mdc-raised-button .mat-mdc-button-touch-target { - position: absolute; - top: 50%; - height: var(--mat-button-protected-touch-target-size, 48px); - display: var(--mat-button-protected-touch-target-display, block); - left: 0; - right: 0; - transform: translateY(-50%); -} -.mat-mdc-raised-button:not(:disabled) { - color: var(--mat-button-protected-label-text-color, var(--mat-sys-primary)); - background-color: var(--mat-button-protected-container-color, var(--mat-sys-surface)); -} -.mat-mdc-raised-button, .mat-mdc-raised-button .mdc-button__ripple { - border-radius: var(--mat-button-protected-container-shape, var(--mat-sys-corner-full)); -} -@media (hover: hover) { - .mat-mdc-raised-button:hover { - box-shadow: var(--mat-button-protected-hover-container-elevation-shadow, var(--mat-sys-level2)); - } -} -.mat-mdc-raised-button:focus { - box-shadow: var(--mat-button-protected-focus-container-elevation-shadow, var(--mat-sys-level1)); -} -.mat-mdc-raised-button:active, .mat-mdc-raised-button:focus:active { - box-shadow: var(--mat-button-protected-pressed-container-elevation-shadow, var(--mat-sys-level1)); -} -.mat-mdc-raised-button[disabled], .mat-mdc-raised-button.mat-mdc-button-disabled { - cursor: default; - pointer-events: none; - color: var(--mat-button-protected-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); - background-color: var(--mat-button-protected-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); -} -.mat-mdc-raised-button[disabled].mat-mdc-button-disabled, .mat-mdc-raised-button.mat-mdc-button-disabled.mat-mdc-button-disabled { - box-shadow: var(--mat-button-protected-disabled-container-elevation-shadow, var(--mat-sys-level0)); -} -.mat-mdc-raised-button.mat-mdc-button-disabled-interactive { - pointer-events: auto; -} - -.mat-mdc-outlined-button { - border-style: solid; - transition: border 280ms cubic-bezier(0.4, 0, 0.2, 1); - height: var(--mat-button-outlined-container-height, 40px); - font-family: var(--mat-button-outlined-label-text-font, var(--mat-sys-label-large-font)); - font-size: var(--mat-button-outlined-label-text-size, var(--mat-sys-label-large-size)); - letter-spacing: var(--mat-button-outlined-label-text-tracking, var(--mat-sys-label-large-tracking)); - text-transform: var(--mat-button-outlined-label-text-transform); - font-weight: var(--mat-button-outlined-label-text-weight, var(--mat-sys-label-large-weight)); - border-radius: var(--mat-button-outlined-container-shape, var(--mat-sys-corner-full)); - border-width: var(--mat-button-outlined-outline-width, 1px); - padding: 0 var(--mat-button-outlined-horizontal-padding, 24px); -} -.mat-mdc-outlined-button > .mat-icon { - margin-right: var(--mat-button-outlined-icon-spacing, 8px); - margin-left: var(--mat-button-outlined-icon-offset, -8px); -} -[dir=rtl] .mat-mdc-outlined-button > .mat-icon { - margin-right: var(--mat-button-outlined-icon-offset, -8px); - margin-left: var(--mat-button-outlined-icon-spacing, 8px); -} -.mat-mdc-outlined-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-outlined-icon-offset, -8px); - margin-left: var(--mat-button-outlined-icon-spacing, 8px); -} -[dir=rtl] .mat-mdc-outlined-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-outlined-icon-spacing, 8px); - margin-left: var(--mat-button-outlined-icon-offset, -8px); -} -.mat-mdc-outlined-button .mat-ripple-element { - background-color: var(--mat-button-outlined-ripple-color, color-mix(in srgb, var(--mat-sys-primary) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); -} -.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-outlined-state-layer-color, var(--mat-sys-primary)); -} -.mat-mdc-outlined-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-outlined-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); -} -.mat-mdc-outlined-button:hover > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-outlined-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); -} -.mat-mdc-outlined-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-outlined-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-mdc-outlined-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-outlined-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); -} -.mat-mdc-outlined-button:active > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-outlined-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); -} -.mat-mdc-outlined-button .mat-mdc-button-touch-target { - position: absolute; - top: 50%; - height: var(--mat-button-outlined-touch-target-size, 48px); - display: var(--mat-button-outlined-touch-target-display, block); - left: 0; - right: 0; - transform: translateY(-50%); -} -.mat-mdc-outlined-button:not(:disabled) { - color: var(--mat-button-outlined-label-text-color, var(--mat-sys-primary)); - border-color: var(--mat-button-outlined-outline-color, var(--mat-sys-outline)); -} -.mat-mdc-outlined-button[disabled], .mat-mdc-outlined-button.mat-mdc-button-disabled { - cursor: default; - pointer-events: none; - color: var(--mat-button-outlined-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); - border-color: var(--mat-button-outlined-disabled-outline-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); -} -.mat-mdc-outlined-button.mat-mdc-button-disabled-interactive { - pointer-events: auto; -} - -.mat-tonal-button { - transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); - height: var(--mat-button-tonal-container-height, 40px); - font-family: var(--mat-button-tonal-label-text-font, var(--mat-sys-label-large-font)); - font-size: var(--mat-button-tonal-label-text-size, var(--mat-sys-label-large-size)); - letter-spacing: var(--mat-button-tonal-label-text-tracking, var(--mat-sys-label-large-tracking)); - text-transform: var(--mat-button-tonal-label-text-transform); - font-weight: var(--mat-button-tonal-label-text-weight, var(--mat-sys-label-large-weight)); - padding: 0 var(--mat-button-tonal-horizontal-padding, 24px); -} -.mat-tonal-button:not(:disabled) { - color: var(--mat-button-tonal-label-text-color, var(--mat-sys-on-secondary-container)); - background-color: var(--mat-button-tonal-container-color, var(--mat-sys-secondary-container)); -} -.mat-tonal-button, .mat-tonal-button .mdc-button__ripple { - border-radius: var(--mat-button-tonal-container-shape, var(--mat-sys-corner-full)); -} -.mat-tonal-button[disabled], .mat-tonal-button.mat-mdc-button-disabled { - cursor: default; - pointer-events: none; - color: var(--mat-button-tonal-disabled-label-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); - background-color: var(--mat-button-tonal-disabled-container-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); -} -.mat-tonal-button.mat-mdc-button-disabled-interactive { - pointer-events: auto; -} -.mat-tonal-button > .mat-icon { - margin-right: var(--mat-button-tonal-icon-spacing, 8px); - margin-left: var(--mat-button-tonal-icon-offset, -8px); -} -[dir=rtl] .mat-tonal-button > .mat-icon { - margin-right: var(--mat-button-tonal-icon-offset, -8px); - margin-left: var(--mat-button-tonal-icon-spacing, 8px); -} -.mat-tonal-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-tonal-icon-offset, -8px); - margin-left: var(--mat-button-tonal-icon-spacing, 8px); -} -[dir=rtl] .mat-tonal-button .mdc-button__label + .mat-icon { - margin-right: var(--mat-button-tonal-icon-spacing, 8px); - margin-left: var(--mat-button-tonal-icon-offset, -8px); -} -.mat-tonal-button .mat-ripple-element { - background-color: var(--mat-button-tonal-ripple-color, color-mix(in srgb, var(--mat-sys-on-secondary-container) calc(var(--mat-sys-pressed-state-layer-opacity) * 100%), transparent)); -} -.mat-tonal-button .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-tonal-state-layer-color, var(--mat-sys-on-secondary-container)); -} -.mat-tonal-button.mat-mdc-button-disabled .mat-mdc-button-persistent-ripple::before { - background-color: var(--mat-button-tonal-disabled-state-layer-color, var(--mat-sys-on-surface-variant)); -} -.mat-tonal-button:hover > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-tonal-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); -} -.mat-tonal-button.cdk-program-focused > .mat-mdc-button-persistent-ripple::before, .mat-tonal-button.cdk-keyboard-focused > .mat-mdc-button-persistent-ripple::before, .mat-tonal-button.mat-mdc-button-disabled-interactive:focus > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-tonal-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); -} -.mat-tonal-button:active > .mat-mdc-button-persistent-ripple::before { - opacity: var(--mat-button-tonal-pressed-state-layer-opacity, var(--mat-sys-pressed-state-layer-opacity)); -} -.mat-tonal-button .mat-mdc-button-touch-target { - position: absolute; - top: 50%; - height: var(--mat-button-tonal-touch-target-size, 48px); - display: var(--mat-button-tonal-touch-target-display, block); - left: 0; - right: 0; - transform: translateY(-50%); -} - -.mat-mdc-button, -.mat-mdc-unelevated-button, -.mat-mdc-raised-button, -.mat-mdc-outlined-button, -.mat-tonal-button { - -webkit-tap-highlight-color: transparent; -} -.mat-mdc-button .mat-mdc-button-ripple, -.mat-mdc-button .mat-mdc-button-persistent-ripple, -.mat-mdc-button .mat-mdc-button-persistent-ripple::before, -.mat-mdc-unelevated-button .mat-mdc-button-ripple, -.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple, -.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple::before, -.mat-mdc-raised-button .mat-mdc-button-ripple, -.mat-mdc-raised-button .mat-mdc-button-persistent-ripple, -.mat-mdc-raised-button .mat-mdc-button-persistent-ripple::before, -.mat-mdc-outlined-button .mat-mdc-button-ripple, -.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple, -.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before, -.mat-tonal-button .mat-mdc-button-ripple, -.mat-tonal-button .mat-mdc-button-persistent-ripple, -.mat-tonal-button .mat-mdc-button-persistent-ripple::before { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - pointer-events: none; - border-radius: inherit; -} -.mat-mdc-button .mat-mdc-button-ripple, -.mat-mdc-unelevated-button .mat-mdc-button-ripple, -.mat-mdc-raised-button .mat-mdc-button-ripple, -.mat-mdc-outlined-button .mat-mdc-button-ripple, -.mat-tonal-button .mat-mdc-button-ripple { - overflow: hidden; -} -.mat-mdc-button .mat-mdc-button-persistent-ripple::before, -.mat-mdc-unelevated-button .mat-mdc-button-persistent-ripple::before, -.mat-mdc-raised-button .mat-mdc-button-persistent-ripple::before, -.mat-mdc-outlined-button .mat-mdc-button-persistent-ripple::before, -.mat-tonal-button .mat-mdc-button-persistent-ripple::before { - content: ""; - opacity: 0; -} -.mat-mdc-button .mdc-button__label, -.mat-mdc-button .mat-icon, -.mat-mdc-unelevated-button .mdc-button__label, -.mat-mdc-unelevated-button .mat-icon, -.mat-mdc-raised-button .mdc-button__label, -.mat-mdc-raised-button .mat-icon, -.mat-mdc-outlined-button .mdc-button__label, -.mat-mdc-outlined-button .mat-icon, -.mat-tonal-button .mdc-button__label, -.mat-tonal-button .mat-icon { - z-index: 1; - position: relative; -} -.mat-mdc-button .mat-focus-indicator, -.mat-mdc-unelevated-button .mat-focus-indicator, -.mat-mdc-raised-button .mat-focus-indicator, -.mat-mdc-outlined-button .mat-focus-indicator, -.mat-tonal-button .mat-focus-indicator { - top: 0; - left: 0; - right: 0; - bottom: 0; - position: absolute; - border-radius: inherit; -} -.mat-mdc-button:focus-visible > .mat-focus-indicator::before, -.mat-mdc-unelevated-button:focus-visible > .mat-focus-indicator::before, -.mat-mdc-raised-button:focus-visible > .mat-focus-indicator::before, -.mat-mdc-outlined-button:focus-visible > .mat-focus-indicator::before, -.mat-tonal-button:focus-visible > .mat-focus-indicator::before { - content: ""; - border-radius: inherit; -} -.mat-mdc-button._mat-animation-noopable, -.mat-mdc-unelevated-button._mat-animation-noopable, -.mat-mdc-raised-button._mat-animation-noopable, -.mat-mdc-outlined-button._mat-animation-noopable, -.mat-tonal-button._mat-animation-noopable { - transition: none !important; - animation: none !important; -} -.mat-mdc-button > .mat-icon, -.mat-mdc-unelevated-button > .mat-icon, -.mat-mdc-raised-button > .mat-icon, -.mat-mdc-outlined-button > .mat-icon, -.mat-tonal-button > .mat-icon { - display: inline-block; - position: relative; - vertical-align: top; - font-size: 1.125rem; - height: 1.125rem; - width: 1.125rem; -} - -.mat-mdc-outlined-button .mat-mdc-button-ripple, -.mat-mdc-outlined-button .mdc-button__ripple { - top: -1px; - left: -1px; - bottom: -1px; - right: -1px; -} - -.mat-mdc-unelevated-button .mat-focus-indicator::before, -.mat-tonal-button .mat-focus-indicator::before, -.mat-mdc-raised-button .mat-focus-indicator::before { - margin: calc(calc(var(--mat-focus-indicator-border-width, 3px) + 2px) * -1); -} - -.mat-mdc-outlined-button .mat-focus-indicator::before { - margin: calc(calc(var(--mat-focus-indicator-border-width, 3px) + 3px) * -1); -} -`,`@media (forced-colors: active) { - .mat-mdc-button:not(.mdc-button--outlined), - .mat-mdc-unelevated-button:not(.mdc-button--outlined), - .mat-mdc-raised-button:not(.mdc-button--outlined), - .mat-mdc-outlined-button:not(.mdc-button--outlined), - .mat-mdc-button-base.mat-tonal-button, - .mat-mdc-icon-button.mat-mdc-icon-button, - .mat-mdc-outlined-button .mdc-button__ripple { - outline: solid 1px; - } -} -`],encapsulation:2,changeDetection:0})}return t})();function Ck(t){return t.hasAttribute("mat-raised-button")?"elevated":t.hasAttribute("mat-stroked-button")?"outlined":t.hasAttribute("mat-flat-button")?"filled":t.hasAttribute("mat-button")?"text":null}var rK=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({imports:[dC,zn]})}return t})();var Iu={production:!0,electron:!1,githubio:!1,solarputty_download_url:"",current_version:"v3",compute_id:"local"};var oa=class{};function Ik(t){return t&&typeof t.connect=="function"&&!(t instanceof ki)}var fg=class extends oa{_data;constructor(n){super(),this._data=n}connect(){return Ft(this._data)?this._data:T(this._data)}disconnect(){}},Dn=(function(t){return t[t.REPLACED=0]="REPLACED",t[t.INSERTED=1]="INSERTED",t[t.MOVED=2]="MOVED",t[t.REMOVED=3]="REMOVED",t})(Dn||{}),hg=class{viewCacheSize=20;_viewCache=[];applyChanges(n,e,r,o,i){n.forEachOperation((s,a,c)=>{let l,u;if(s.previousIndex==null){let d=()=>r(s,a,c);l=this._insertView(d,c,e,o(s)),u=l?Dn.INSERTED:Dn.REPLACED}else c==null?(this._detachAndCacheView(a,e),u=Dn.REMOVED):(l=this._moveView(a,c,e,o(s)),u=Dn.MOVED);i&&i({context:l?.context,operation:u,record:s})})}detach(){for(let n of this._viewCache)n.destroy();this._viewCache=[]}_insertView(n,e,r,o){let i=this._insertViewFromCache(e,r);if(i){i.context.$implicit=o;return}let s=n();return r.createEmbeddedView(s.templateRef,s.context,s.index)}_detachAndCacheView(n,e){let r=e.detach(n);this._maybeCacheView(r,e)}_moveView(n,e,r,o){let i=r.get(n);return r.move(i,e),i.context.$implicit=o,i}_maybeCacheView(n,e){if(this._viewCache.length0?i/this._itemSize:0;if(e.end>o){let c=Math.ceil(r/this._itemSize),l=Math.max(0,Math.min(s,o-c));s!=l&&(s=l,i=l*this._itemSize,e.start=Math.floor(s)),e.end=Math.max(0,Math.min(o,e.start+c))}let a=i-e.start*this._itemSize;if(a0&&(e.end=Math.min(o,e.end+l),e.start=Math.max(0,Math.floor(s-this._minBufferPx/this._itemSize)))}}this._viewport.setRenderedRange(e),this._viewport.setRenderedContentOffset(Math.round(this._itemSize*e.start)),this._scrolledIndexChange.next(Math.floor(s))}};function Tk(t){return t._scrollStrategy}var xk=(()=>{class t{get itemSize(){return this._itemSize}set itemSize(e){this._itemSize=so(e)}_itemSize=20;get minBufferPx(){return this._minBufferPx}set minBufferPx(e){this._minBufferPx=so(e)}_minBufferPx=100;get maxBufferPx(){return this._maxBufferPx}set maxBufferPx(e){this._maxBufferPx=so(e)}_maxBufferPx=200;_scrollStrategy=new pg(this.itemSize,this.minBufferPx,this.maxBufferPx);ngOnChanges(){this._scrollStrategy.updateItemAndBufferSize(this.itemSize,this.minBufferPx,this.maxBufferPx)}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["cdk-virtual-scroll-viewport","itemSize",""]],inputs:{itemSize:"itemSize",minBufferPx:"minBufferPx",maxBufferPx:"maxBufferPx"},features:[we([{provide:pC,useFactory:Tk,deps:[be(()=>t)]}]),Re]})}return t})(),Ak=20,ia=(()=>{class t{_ngZone=f(j);_platform=f(he);_renderer=f(je).createRenderer(null,null);_cleanupGlobalListener;constructor(){}_scrolled=new S;_scrolledCount=0;scrollContainers=new Map;register(e){this.scrollContainers.has(e)||this.scrollContainers.set(e,e.elementScrolled().subscribe(()=>this._scrolled.next(e)))}deregister(e){let r=this.scrollContainers.get(e);r&&(r.unsubscribe(),this.scrollContainers.delete(e))}scrolled(e=Ak){return this._platform.isBrowser?new O(r=>{this._cleanupGlobalListener||(this._cleanupGlobalListener=this._ngZone.runOutsideAngular(()=>this._renderer.listen("document","scroll",()=>this._scrolled.next())));let o=e>0?this._scrolled.pipe(Bi(e)).subscribe(r):this._scrolled.subscribe(r);return this._scrolledCount++,()=>{o.unsubscribe(),this._scrolledCount--,this._scrolledCount||(this._cleanupGlobalListener?.(),this._cleanupGlobalListener=void 0)}}):T()}ngOnDestroy(){this._cleanupGlobalListener?.(),this._cleanupGlobalListener=void 0,this.scrollContainers.forEach((e,r)=>this.deregister(r)),this._scrolled.complete()}ancestorScrolled(e,r){let o=this.getAncestorScrollContainers(e);return this.scrolled(r).pipe(fe(i=>!i||o.indexOf(i)>-1))}getAncestorScrollContainers(e){let r=[];return this.scrollContainers.forEach((o,i)=>{this._scrollableContainsElement(i,e)&&r.push(i)}),r}_scrollableContainsElement(e,r){let o=Tt(r),i=e.getElementRef().nativeElement;do if(o==i)return!0;while(o=o.parentElement);return!1}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),gg=(()=>{class t{elementRef=f(z);scrollDispatcher=f(ia);ngZone=f(j);dir=f(fi,{optional:!0});_scrollElement=this.elementRef.nativeElement;_destroyed=new S;_renderer=f(Oe);_cleanupScroll;_elementScrolled=new S;constructor(){}ngOnInit(){this._cleanupScroll=this.ngZone.runOutsideAngular(()=>this._renderer.listen(this._scrollElement,"scroll",e=>this._elementScrolled.next(e))),this.scrollDispatcher.register(this)}ngOnDestroy(){this._cleanupScroll?.(),this._elementScrolled.complete(),this.scrollDispatcher.deregister(this),this._destroyed.next(),this._destroyed.complete()}elementScrolled(){return this._elementScrolled}getElementRef(){return this.elementRef}scrollTo(e){let r=this.elementRef.nativeElement,o=this.dir&&this.dir.value=="rtl";e.left==null&&(e.left=o?e.end:e.start),e.right==null&&(e.right=o?e.start:e.end),e.bottom!=null&&(e.top=r.scrollHeight-r.clientHeight-e.bottom),o&&ui()!=en.NORMAL?(e.left!=null&&(e.right=r.scrollWidth-r.clientWidth-e.left),ui()==en.INVERTED?e.left=e.right:ui()==en.NEGATED&&(e.left=e.right?-e.right:e.right)):e.right!=null&&(e.left=r.scrollWidth-r.clientWidth-e.right),this._applyScrollToOptions(e)}_applyScrollToOptions(e){let r=this.elementRef.nativeElement;wu()?r.scrollTo(e):(e.top!=null&&(r.scrollTop=e.top),e.left!=null&&(r.scrollLeft=e.left))}measureScrollOffset(e){let r="left",o="right",i=this.elementRef.nativeElement;if(e=="top")return i.scrollTop;if(e=="bottom")return i.scrollHeight-i.clientHeight-i.scrollTop;let s=this.dir&&this.dir.value=="rtl";return e=="start"?e=s?o:r:e=="end"&&(e=s?r:o),s&&ui()==en.INVERTED?e==r?i.scrollWidth-i.clientWidth-i.scrollLeft:i.scrollLeft:s&&ui()==en.NEGATED?e==r?i.scrollLeft+i.scrollWidth-i.clientWidth:-i.scrollLeft:e==r?i.scrollLeft:i.scrollWidth-i.clientWidth-i.scrollLeft}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["","cdk-scrollable",""],["","cdkScrollable",""]]})}return t})(),Rk=20,uo=(()=>{class t{_platform=f(he);_listeners;_viewportSize=null;_change=new S;_document=f(L);constructor(){let e=f(j),r=f(je).createRenderer(null,null);e.runOutsideAngular(()=>{if(this._platform.isBrowser){let o=i=>this._change.next(i);this._listeners=[r.listen("window","resize",o),r.listen("window","orientationchange",o)]}this.change().subscribe(()=>this._viewportSize=null)})}ngOnDestroy(){this._listeners?.forEach(e=>e()),this._change.complete()}getViewportSize(){this._viewportSize||this._updateViewportSize();let e={width:this._viewportSize.width,height:this._viewportSize.height};return this._platform.isBrowser||(this._viewportSize=null),e}getViewportRect(){let e=this.getViewportScrollPosition(),{width:r,height:o}=this.getViewportSize();return{top:e.top,left:e.left,bottom:e.top+o,right:e.left+r,height:o,width:r}}getViewportScrollPosition(){if(!this._platform.isBrowser)return{top:0,left:0};let e=this._document,r=this._getWindow(),o=e.documentElement,i=o.getBoundingClientRect(),s=-i.top||e.body?.scrollTop||r.scrollY||o.scrollTop||0,a=-i.left||e.body?.scrollLeft||r.scrollX||o.scrollLeft||0;return{top:s,left:a}}change(e=Rk){return e>0?this._change.pipe(Bi(e)):this._change}_getWindow(){return this._document.defaultView||window}_updateViewportSize(){let e=this._getWindow();this._viewportSize=this._platform.isBrowser?{width:e.innerWidth,height:e.innerHeight}:{width:0,height:0}}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),hC=new y("VIRTUAL_SCROLLABLE"),Nk=(()=>{class t extends gg{constructor(){super()}measureViewportSize(e){let r=this.elementRef.nativeElement;return e==="horizontal"?r.clientWidth:r.clientHeight}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,features:[J]})}return t})();function Ok(t,n){return t.start==n.start&&t.end==n.end}var kk=typeof requestAnimationFrame<"u"?Vd:jd,Fk=new y("CDK_VIRTUAL_SCROLL_VIEWPORT"),Pk=(()=>{class t extends Nk{elementRef=f(z);_changeDetectorRef=f(St);_scrollStrategy=f(pC,{optional:!0});scrollable=f(hC,{optional:!0});_platform=f(he);_detachedSubject=new S;_renderedRangeSubject=new S;_renderedContentOffsetSubject=new S;get orientation(){return this._orientation}set orientation(e){this._orientation!==e&&(this._orientation=e,this._calculateSpacerSize())}_orientation="vertical";appendOnly=!1;scrolledIndexChange=new O(e=>this._scrollStrategy.scrolledIndexChange.subscribe(r=>Promise.resolve().then(()=>this.ngZone.run(()=>e.next(r)))));_contentWrapper;renderedRangeStream=this._renderedRangeSubject;renderedContentOffset=this._renderedContentOffsetSubject.pipe(fe(e=>e!==null),xo());_totalContentSize=0;_totalContentWidth=W("");_totalContentHeight=W("");_renderedContentTransform;_renderedRange={start:0,end:0};_dataLength=0;_viewportSize=0;_forOf=null;_renderedContentOffset=0;_renderedContentOffsetNeedsRewrite=!1;_changeDetectionNeeded=W(!1);_runAfterChangeDetection=[];_viewportChanges=G.EMPTY;_injector=f($);_isDestroyed=!1;constructor(){super();let e=f(uo);this._scrollStrategy,this._viewportChanges=e.change().subscribe(()=>{this.checkViewportSize()}),this.scrollable||(this.elementRef.nativeElement.classList.add("cdk-virtual-scrollable"),this.scrollable=this);let r=Go(()=>{this._changeDetectionNeeded()&&this._doChangeDetection()},{injector:f(Be).injector});f(Ae).onDestroy(()=>{r.destroy()})}ngOnInit(){this._platform.isBrowser&&(this.scrollable===this&&super.ngOnInit(),this.ngZone.runOutsideAngular(()=>Promise.resolve().then(()=>{this._measureViewportSize(),this._scrollStrategy.attach(this),this.scrollable.elementScrolled().pipe(Nr(null),Bi(0,kk),at(this._destroyed)).subscribe(()=>this._scrollStrategy.onContentScrolled()),this._markChangeDetectionNeeded()})))}ngOnDestroy(){this.detach(),this._scrollStrategy.detach(),this._renderedRangeSubject.complete(),this._detachedSubject.complete(),this._viewportChanges.unsubscribe(),this._isDestroyed=!0,super.ngOnDestroy()}attach(e){this._forOf,this.ngZone.runOutsideAngular(()=>{this._forOf=e,this._forOf.dataStream.pipe(at(this._detachedSubject)).subscribe(r=>{let o=r.length;o!==this._dataLength&&(this._dataLength=o,this._scrollStrategy.onDataLengthChanged()),this._doChangeDetection()})})}detach(){this._forOf=null,this._detachedSubject.next()}getDataLength(){return this._dataLength}getViewportSize(){return this._viewportSize}getRenderedRange(){return this._renderedRange}measureBoundingClientRectWithScrollOffset(e){return this.getElementRef().nativeElement.getBoundingClientRect()[e]}setTotalContentSize(e){this._totalContentSize!==e&&(this._totalContentSize=e,this._calculateSpacerSize(),this._markChangeDetectionNeeded())}setRenderedRange(e){Ok(this._renderedRange,e)||(this.appendOnly&&(e={start:0,end:Math.max(this._renderedRange.end,e.end)}),this._renderedRangeSubject.next(this._renderedRange=e),this._markChangeDetectionNeeded(()=>this._scrollStrategy.onContentRendered()))}getOffsetToRenderedContentStart(){return this._renderedContentOffsetNeedsRewrite?null:this._renderedContentOffset}setRenderedContentOffset(e,r="to-start"){e=this.appendOnly&&r==="to-start"?0:e;let o=this.dir&&this.dir.value=="rtl",i=this.orientation=="horizontal",s=i?"X":"Y",c=`translate${s}(${Number((i&&o?-1:1)*e)}px)`;this._renderedContentOffset=e,r==="to-end"&&(c+=` translate${s}(-100%)`,this._renderedContentOffsetNeedsRewrite=!0),this._renderedContentTransform!=c&&(this._renderedContentTransform=c,this._markChangeDetectionNeeded(()=>{this._renderedContentOffsetNeedsRewrite?(this._renderedContentOffset-=this.measureRenderedContentSize(),this._renderedContentOffsetNeedsRewrite=!1,this.setRenderedContentOffset(this._renderedContentOffset)):this._scrollStrategy.onRenderedOffsetChanged()}))}scrollToOffset(e,r="auto"){let o={behavior:r};this.orientation==="horizontal"?o.start=e:o.top=e,this.scrollable.scrollTo(o)}scrollToIndex(e,r="auto"){this._scrollStrategy.scrollToIndex(e,r)}measureScrollOffset(e){let r;return this.scrollable==this?r=o=>super.measureScrollOffset(o):r=o=>this.scrollable.measureScrollOffset(o),Math.max(0,r(e??(this.orientation==="horizontal"?"start":"top"))-this.measureViewportOffset())}measureViewportOffset(e){let r,o="left",i="right",s=this.dir?.value=="rtl";e=="start"?r=s?i:o:e=="end"?r=s?o:i:e?r=e:r=this.orientation==="horizontal"?"left":"top";let a=this.scrollable.measureBoundingClientRectWithScrollOffset(r);return this.elementRef.nativeElement.getBoundingClientRect()[r]-a}measureRenderedContentSize(){let e=this._contentWrapper.nativeElement;return this.orientation==="horizontal"?e.offsetWidth:e.offsetHeight}measureRangeSize(e){return this._forOf?this._forOf.measureRangeSize(e,this.orientation):0}checkViewportSize(){this._measureViewportSize(),this._scrollStrategy.onDataLengthChanged()}_measureViewportSize(){this._viewportSize=this.scrollable.measureViewportSize(this.orientation)}_markChangeDetectionNeeded(e){e&&this._runAfterChangeDetection.push(e),!q(this._changeDetectionNeeded)&&this.ngZone.runOutsideAngular(()=>{Promise.resolve().then(()=>{this.ngZone.run(()=>{this._changeDetectionNeeded.set(!0)})})})}_doChangeDetection(){this._isDestroyed||this.ngZone.run(()=>{this._changeDetectorRef.markForCheck(),this._contentWrapper.nativeElement.style.transform=this._renderedContentTransform,this._renderedContentOffsetSubject.next(this.getOffsetToRenderedContentStart()),ht(()=>{this._changeDetectionNeeded.set(!1);let e=this._runAfterChangeDetection;this._runAfterChangeDetection=[];for(let r of e)r()},{injector:this._injector})})}_calculateSpacerSize(){this._totalContentHeight.set(this.orientation==="horizontal"?"":`${this._totalContentSize}px`),this._totalContentWidth.set(this.orientation==="horizontal"?`${this._totalContentSize}px`:"")}static \u0275fac=function(r){return new(r||t)};static \u0275cmp=ke({type:t,selectors:[["cdk-virtual-scroll-viewport"]],viewQuery:function(r,o){if(r&1&&Fl(Sk,7),r&2){let i;Fs(i=Ps())&&(o._contentWrapper=i.first)}},hostAttrs:[1,"cdk-virtual-scroll-viewport"],hostVars:4,hostBindings:function(r,o){r&2&&Xe("cdk-virtual-scroll-orientation-horizontal",o.orientation==="horizontal")("cdk-virtual-scroll-orientation-vertical",o.orientation!=="horizontal")},inputs:{orientation:"orientation",appendOnly:[2,"appendOnly","appendOnly",ue]},outputs:{scrolledIndexChange:"scrolledIndexChange"},features:[we([{provide:gg,useFactory:()=>f(hC,{optional:!0})||f(t)},{provide:Fk,useExisting:t}]),J],ngContentSelectors:Mk,decls:4,vars:4,consts:[["contentWrapper",""],[1,"cdk-virtual-scroll-content-wrapper"],[1,"cdk-virtual-scroll-spacer"]],template:function(r,o){r&1&&(ro(),to(0,"div",1,0),Vn(2),no(),yn(3,"div",2)),r&2&&(bp(3),Pl("width",o._totalContentWidth())("height",o._totalContentHeight()))},styles:[`cdk-virtual-scroll-viewport { - display: block; - position: relative; - transform: translateZ(0); -} - -.cdk-virtual-scrollable { - overflow: auto; - will-change: scroll-position; - contain: strict; -} - -.cdk-virtual-scroll-content-wrapper { - position: absolute; - top: 0; - left: 0; - contain: content; -} -[dir=rtl] .cdk-virtual-scroll-content-wrapper { - right: 0; - left: auto; -} - -.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper { - min-height: 100%; -} -.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper > dl:not([cdkVirtualFor]), .cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper > ol:not([cdkVirtualFor]), .cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper > table:not([cdkVirtualFor]), .cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper > ul:not([cdkVirtualFor]) { - padding-left: 0; - padding-right: 0; - margin-left: 0; - margin-right: 0; - border-left-width: 0; - border-right-width: 0; - outline: none; -} - -.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper { - min-width: 100%; -} -.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper > dl:not([cdkVirtualFor]), .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper > ol:not([cdkVirtualFor]), .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper > table:not([cdkVirtualFor]), .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper > ul:not([cdkVirtualFor]) { - padding-top: 0; - padding-bottom: 0; - margin-top: 0; - margin-bottom: 0; - border-top-width: 0; - border-bottom-width: 0; - outline: none; -} - -.cdk-virtual-scroll-spacer { - height: 1px; - transform-origin: 0 0; - flex: 0 0 auto; -} -[dir=rtl] .cdk-virtual-scroll-spacer { - transform-origin: 100% 0; -} -`],encapsulation:2,changeDetection:0})}return t})();var mg=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({})}return t})(),vg=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({imports:[zn,mg,zn,mg]})}return t})();var sa=class{_attachedHost=null;attach(n){return this._attachedHost=n,n.attach(this)}detach(){let n=this._attachedHost;n!=null&&(this._attachedHost=null,n.detach())}get isAttached(){return this._attachedHost!=null}setAttachedHost(n){this._attachedHost=n}},yg=class extends sa{component;viewContainerRef;injector;projectableNodes;bindings;constructor(n,e,r,o,i){super(),this.component=n,this.viewContainerRef=e,this.injector=r,this.projectableNodes=o,this.bindings=i||null}},hi=class extends sa{templateRef;viewContainerRef;context;injector;constructor(n,e,r,o){super(),this.templateRef=n,this.viewContainerRef=e,this.context=r,this.injector=o}get origin(){return this.templateRef.elementRef}attach(n,e=this.context){return this.context=e,super.attach(n)}detach(){return this.context=void 0,super.detach()}},bg=class extends sa{element;constructor(n){super(),this.element=n instanceof z?n.nativeElement:n}},Su=class{_attachedPortal=null;_disposeFn=null;_isDisposed=!1;hasAttached(){return!!this._attachedPortal}attach(n){if(n instanceof yg)return this._attachedPortal=n,this.attachComponentPortal(n);if(n instanceof hi)return this._attachedPortal=n,this.attachTemplatePortal(n);if(this.attachDomPortal&&n instanceof bg)return this._attachedPortal=n,this.attachDomPortal(n)}attachDomPortal=null;detach(){this._attachedPortal&&(this._attachedPortal.setAttachedHost(null),this._attachedPortal=null),this._invokeDisposeFn()}dispose(){this.hasAttached()&&this.detach(),this._invokeDisposeFn(),this._isDisposed=!0}setDisposeFn(n){this._disposeFn=n}_invokeDisposeFn(){this._disposeFn&&(this._disposeFn(),this._disposeFn=null)}},Mu=class extends Su{outletElement;_appRef;_defaultInjector;constructor(n,e,r){super(),this.outletElement=n,this._appRef=e,this._defaultInjector=r}attachComponentPortal(n){let e;if(n.viewContainerRef){let r=n.injector||n.viewContainerRef.injector,o=r.get(gn,null,{optional:!0})||void 0;e=n.viewContainerRef.createComponent(n.component,{index:n.viewContainerRef.length,injector:r,ngModuleRef:o,projectableNodes:n.projectableNodes||void 0,bindings:n.bindings||void 0}),this.setDisposeFn(()=>e.destroy())}else{let r=this._appRef,o=n.injector||this._defaultInjector||$.NULL,i=o.get(re,r.injector);e=$l(n.component,{elementInjector:o,environmentInjector:i,projectableNodes:n.projectableNodes||void 0,bindings:n.bindings||void 0}),r.attachView(e.hostView),this.setDisposeFn(()=>{r.viewCount>0&&r.detachView(e.hostView),e.destroy()})}return this.outletElement.appendChild(this._getComponentRootNode(e)),this._attachedPortal=n,e}attachTemplatePortal(n){let e=n.viewContainerRef,r=e.createEmbeddedView(n.templateRef,n.context,{injector:n.injector});return r.rootNodes.forEach(o=>this.outletElement.appendChild(o)),r.detectChanges(),this.setDisposeFn(()=>{let o=e.indexOf(r);o!==-1&&e.remove(o)}),this._attachedPortal=n,r}attachDomPortal=n=>{let e=n.element;e.parentNode;let r=this.outletElement.ownerDocument.createComment("dom-portal");e.parentNode.insertBefore(r,e),this.outletElement.appendChild(e),this._attachedPortal=n,super.setDisposeFn(()=>{r.parentNode&&r.parentNode.replaceChild(e,r)})};dispose(){super.dispose(),this.outletElement.remove()}_getComponentRootNode(n){return n.hostView.rootNodes[0]}},kK=(()=>{class t extends hi{constructor(){let e=f(dt),r=f(qe);super(e,r)}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["","cdkPortal",""]],exportAs:["cdkPortal"],features:[J]})}return t})(),FK=(()=>{class t extends Su{_moduleRef=f(gn,{optional:!0});_document=f(L);_viewContainerRef=f(qe);_isInitialized=!1;_attachedRef=null;constructor(){super()}get portal(){return this._attachedPortal}set portal(e){this.hasAttached()&&!e&&!this._isInitialized||(this.hasAttached()&&super.detach(),e&&super.attach(e),this._attachedPortal=e||null)}attached=new U;get attachedRef(){return this._attachedRef}ngOnInit(){this._isInitialized=!0}ngOnDestroy(){super.dispose(),this._attachedRef=this._attachedPortal=null}attachComponentPortal(e){e.setAttachedHost(this);let r=e.viewContainerRef!=null?e.viewContainerRef:this._viewContainerRef,o=r.createComponent(e.component,{index:r.length,injector:e.injector||r.injector,projectableNodes:e.projectableNodes||void 0,ngModuleRef:this._moduleRef||void 0,bindings:e.bindings||void 0});return r!==this._viewContainerRef&&this._getRootNode().appendChild(o.hostView.rootNodes[0]),super.setDisposeFn(()=>o.destroy()),this._attachedPortal=e,this._attachedRef=o,this.attached.emit(o),o}attachTemplatePortal(e){e.setAttachedHost(this);let r=this._viewContainerRef.createEmbeddedView(e.templateRef,e.context,{injector:e.injector});return super.setDisposeFn(()=>this._viewContainerRef.clear()),this._attachedPortal=e,this._attachedRef=r,this.attached.emit(r),r}attachDomPortal=e=>{let r=e.element;r.parentNode;let o=this._document.createComment("dom-portal");e.setAttachedHost(this),r.parentNode.insertBefore(o,r),this._getRootNode().appendChild(r),this._attachedPortal=e,super.setDisposeFn(()=>{o.parentNode&&o.parentNode.replaceChild(r,o)})};_getRootNode(){let e=this._viewContainerRef.element.nativeElement;return e.nodeType===e.ELEMENT_NODE?e:e.parentNode}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["","cdkPortalOutlet",""]],inputs:{portal:[0,"cdkPortalOutlet","portal"]},outputs:{attached:"attached"},exportAs:["cdkPortalOutlet"],features:[J]})}return t})(),mC=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({})}return t})();var gC=wu();function wC(t){return new Tu(t.get(uo),t.get(L))}var Tu=class{_viewportRuler;_previousHTMLStyles={top:"",left:""};_previousScrollPosition;_isEnabled=!1;_document;constructor(n,e){this._viewportRuler=n,this._document=e}attach(){}enable(){if(this._canBeEnabled()){let n=this._document.documentElement;this._previousScrollPosition=this._viewportRuler.getViewportScrollPosition(),this._previousHTMLStyles.left=n.style.left||"",this._previousHTMLStyles.top=n.style.top||"",n.style.left=De(-this._previousScrollPosition.left),n.style.top=De(-this._previousScrollPosition.top),n.classList.add("cdk-global-scrollblock"),this._isEnabled=!0}}disable(){if(this._isEnabled){let n=this._document.documentElement,e=this._document.body,r=n.style,o=e.style,i=r.scrollBehavior||"",s=o.scrollBehavior||"";this._isEnabled=!1,r.left=this._previousHTMLStyles.left,r.top=this._previousHTMLStyles.top,n.classList.remove("cdk-global-scrollblock"),gC&&(r.scrollBehavior=o.scrollBehavior="auto"),window.scroll(this._previousScrollPosition.left,this._previousScrollPosition.top),gC&&(r.scrollBehavior=i,o.scrollBehavior=s)}}_canBeEnabled(){if(this._document.documentElement.classList.contains("cdk-global-scrollblock")||this._isEnabled)return!1;let e=this._document.documentElement,r=this._viewportRuler.getViewportSize();return e.scrollHeight>r.height||e.scrollWidth>r.width}};function CC(t,n){return new xu(t.get(ia),t.get(j),t.get(uo),n)}var xu=class{_scrollDispatcher;_ngZone;_viewportRuler;_config;_scrollSubscription=null;_overlayRef;_initialScrollPosition;constructor(n,e,r,o){this._scrollDispatcher=n,this._ngZone=e,this._viewportRuler=r,this._config=o}attach(n){this._overlayRef,this._overlayRef=n}enable(){if(this._scrollSubscription)return;let n=this._scrollDispatcher.scrolled(0).pipe(fe(e=>!e||!this._overlayRef.overlayElement.contains(e.getElementRef().nativeElement)));this._config&&this._config.threshold&&this._config.threshold>1?(this._initialScrollPosition=this._viewportRuler.getViewportScrollPosition().top,this._scrollSubscription=n.subscribe(()=>{let e=this._viewportRuler.getViewportScrollPosition().top;Math.abs(e-this._initialScrollPosition)>this._config.threshold?this._detach():this._overlayRef.updatePosition()})):this._scrollSubscription=n.subscribe(this._detach)}disable(){this._scrollSubscription&&(this._scrollSubscription.unsubscribe(),this._scrollSubscription=null)}detach(){this.disable(),this._overlayRef=null}_detach=()=>{this.disable(),this._overlayRef.hasAttached()&&this._ngZone.run(()=>this._overlayRef.detach())}};var aa=class{enable(){}disable(){}attach(){}};function _g(t,n){return n.some(e=>{let r=t.bottome.bottom,i=t.righte.right;return r||o||i||s})}function vC(t,n){return n.some(e=>{let r=t.tope.bottom,i=t.lefte.right;return r||o||i||s})}function wg(t,n){return new Au(t.get(ia),t.get(uo),t.get(j),n)}var Au=class{_scrollDispatcher;_viewportRuler;_ngZone;_config;_scrollSubscription=null;_overlayRef;constructor(n,e,r,o){this._scrollDispatcher=n,this._viewportRuler=e,this._ngZone=r,this._config=o}attach(n){this._overlayRef,this._overlayRef=n}enable(){if(!this._scrollSubscription){let n=this._config?this._config.scrollThrottle:0;this._scrollSubscription=this._scrollDispatcher.scrolled(n).subscribe(()=>{if(this._overlayRef.updatePosition(),this._config&&this._config.autoClose){let e=this._overlayRef.overlayElement.getBoundingClientRect(),{width:r,height:o}=this._viewportRuler.getViewportSize();_g(e,[{width:r,height:o,bottom:o,right:r,top:0,left:0}])&&(this.disable(),this._ngZone.run(()=>this._overlayRef.detach()))}})}}disable(){this._scrollSubscription&&(this._scrollSubscription.unsubscribe(),this._scrollSubscription=null)}detach(){this.disable(),this._overlayRef=null}},IC=(()=>{class t{_injector=f($);constructor(){}noop=()=>new aa;close=e=>CC(this._injector,e);block=()=>wC(this._injector);reposition=e=>wg(this._injector,e);static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),ca=class{positionStrategy;scrollStrategy=new aa;panelClass="";hasBackdrop=!1;backdropClass="cdk-overlay-dark-backdrop";disableAnimations;width;height;minWidth;minHeight;maxWidth;maxHeight;direction;disposeOnNavigation=!1;usePopover;eventPredicate;constructor(n){if(n){let e=Object.keys(n);for(let r of e)n[r]!==void 0&&(this[r]=n[r])}}};var Ru=class{connectionPair;scrollableViewProperties;constructor(n,e){this.connectionPair=n,this.scrollableViewProperties=e}};var SC=(()=>{class t{_attachedOverlays=[];_document=f(L);_isAttached=!1;constructor(){}ngOnDestroy(){this.detach()}add(e){this.remove(e),this._attachedOverlays.push(e)}remove(e){let r=this._attachedOverlays.indexOf(e);r>-1&&this._attachedOverlays.splice(r,1),this._attachedOverlays.length===0&&this.detach()}canReceiveEvent(e,r,o){return o.observers.length<1?!1:e.eventPredicate?e.eventPredicate(r):!0}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),MC=(()=>{class t extends SC{_ngZone=f(j);_renderer=f(je).createRenderer(null,null);_cleanupKeydown;add(e){super.add(e),this._isAttached||(this._ngZone.runOutsideAngular(()=>{this._cleanupKeydown=this._renderer.listen("body","keydown",this._keydownListener)}),this._isAttached=!0)}detach(){this._isAttached&&(this._cleanupKeydown?.(),this._isAttached=!1)}_keydownListener=e=>{let r=this._attachedOverlays;for(let o=r.length-1;o>-1;o--){let i=r[o];if(this.canReceiveEvent(i,e,i._keydownEvents)){this._ngZone.run(()=>i._keydownEvents.next(e));break}}};static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),TC=(()=>{class t extends SC{_platform=f(he);_ngZone=f(j);_renderer=f(je).createRenderer(null,null);_cursorOriginalValue;_cursorStyleIsSet=!1;_pointerDownEventTarget=null;_cleanups;add(e){if(super.add(e),!this._isAttached){let r=this._document.body,o={capture:!0},i=this._renderer;this._cleanups=this._ngZone.runOutsideAngular(()=>[i.listen(r,"pointerdown",this._pointerDownListener,o),i.listen(r,"click",this._clickListener,o),i.listen(r,"auxclick",this._clickListener,o),i.listen(r,"contextmenu",this._clickListener,o)]),this._platform.IOS&&!this._cursorStyleIsSet&&(this._cursorOriginalValue=r.style.cursor,r.style.cursor="pointer",this._cursorStyleIsSet=!0),this._isAttached=!0}}detach(){this._isAttached&&(this._cleanups?.forEach(e=>e()),this._cleanups=void 0,this._platform.IOS&&this._cursorStyleIsSet&&(this._document.body.style.cursor=this._cursorOriginalValue,this._cursorStyleIsSet=!1),this._isAttached=!1)}_pointerDownListener=e=>{this._pointerDownEventTarget=Je(e)};_clickListener=e=>{let r=Je(e),o=e.type==="click"&&this._pointerDownEventTarget?this._pointerDownEventTarget:r;this._pointerDownEventTarget=null;let i=this._attachedOverlays.slice();for(let s=i.length-1;s>-1;s--){let a=i[s],c=a._outsidePointerEvents;if(!(!a.hasAttached()||!this.canReceiveEvent(a,e,c))){if(yC(a.overlayElement,r)||yC(a.overlayElement,o))break;this._ngZone?this._ngZone.run(()=>c.next(e)):c.next(e)}}};static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function yC(t,n){let e=typeof ShadowRoot<"u"&&ShadowRoot,r=n;for(;r;){if(r===t)return!0;r=e&&r instanceof ShadowRoot?r.host:r.parentNode}return!1}var xC=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=ke({type:t,selectors:[["ng-component"]],hostAttrs:["cdk-overlay-style-loader",""],decls:0,vars:0,template:function(r,o){},styles:[`.cdk-overlay-container, .cdk-global-overlay-wrapper { - pointer-events: none; - top: 0; - left: 0; - height: 100%; - width: 100%; -} - -.cdk-overlay-container { - position: fixed; -} -@layer cdk-overlay { - .cdk-overlay-container { - z-index: 1000; - } -} -.cdk-overlay-container:empty { - display: none; -} - -.cdk-global-overlay-wrapper { - display: flex; - position: absolute; -} -@layer cdk-overlay { - .cdk-global-overlay-wrapper { - z-index: 1000; - } -} - -.cdk-overlay-pane { - position: absolute; - pointer-events: auto; - box-sizing: border-box; - display: flex; - max-width: 100%; - max-height: 100%; -} -@layer cdk-overlay { - .cdk-overlay-pane { - z-index: 1000; - } -} - -.cdk-overlay-backdrop { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - pointer-events: auto; - -webkit-tap-highlight-color: transparent; - opacity: 0; - touch-action: manipulation; -} -@layer cdk-overlay { - .cdk-overlay-backdrop { - z-index: 1000; - transition: opacity 400ms cubic-bezier(0.25, 0.8, 0.25, 1); - } -} -@media (prefers-reduced-motion) { - .cdk-overlay-backdrop { - transition-duration: 1ms; - } -} - -.cdk-overlay-backdrop-showing { - opacity: 1; -} -@media (forced-colors: active) { - .cdk-overlay-backdrop-showing { - opacity: 0.6; - } -} - -@layer cdk-overlay { - .cdk-overlay-dark-backdrop { - background: rgba(0, 0, 0, 0.32); - } -} - -.cdk-overlay-transparent-backdrop { - transition: visibility 1ms linear, opacity 1ms linear; - visibility: hidden; - opacity: 1; -} -.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing, .cdk-high-contrast-active .cdk-overlay-transparent-backdrop { - opacity: 0; - visibility: visible; -} - -.cdk-overlay-backdrop-noop-animation { - transition: none; -} - -.cdk-overlay-connected-position-bounding-box { - position: absolute; - display: flex; - flex-direction: column; - min-width: 1px; - min-height: 1px; -} -@layer cdk-overlay { - .cdk-overlay-connected-position-bounding-box { - z-index: 1000; - } -} - -.cdk-global-scrollblock { - position: fixed; - width: 100%; - overflow-y: scroll; -} - -.cdk-overlay-popover { - background: none; - border: none; - padding: 0; - outline: 0; - overflow: visible; - position: fixed; - pointer-events: none; - white-space: normal; - color: inherit; - text-decoration: none; - width: 100%; - height: 100%; - inset: auto; - top: 0; - left: 0; -} -.cdk-overlay-popover::backdrop { - display: none; -} -.cdk-overlay-popover .cdk-overlay-backdrop { - position: fixed; - z-index: auto; -} -`],encapsulation:2,changeDetection:0})}return t})(),Cg=(()=>{class t{_platform=f(he);_containerElement;_document=f(L);_styleLoader=f(xt);constructor(){}ngOnDestroy(){this._containerElement?.remove()}getContainerElement(){return this._loadStyles(),this._containerElement||this._createContainer(),this._containerElement}_createContainer(){let e="cdk-overlay-container";if(this._platform.isBrowser||ig()){let o=this._document.querySelectorAll(`.${e}[platform="server"], .${e}[platform="test"]`);for(let i=0;i{let n=this.element;clearTimeout(this._fallbackTimeout),this._cleanupTransitionEnd?.(),this._cleanupTransitionEnd=this._renderer.listen(n,"transitionend",this.dispose),this._fallbackTimeout=setTimeout(this.dispose,500),n.style.pointerEvents="none",n.classList.remove("cdk-overlay-backdrop-showing")})}dispose=()=>{clearTimeout(this._fallbackTimeout),this._cleanupClick?.(),this._cleanupTransitionEnd?.(),this._cleanupClick=this._cleanupTransitionEnd=this._fallbackTimeout=void 0,this.element.remove()}};function Ig(t){return t&&t.nodeType===1}var Nu=class{_portalOutlet;_host;_pane;_config;_ngZone;_keyboardDispatcher;_document;_location;_outsideClickDispatcher;_animationsDisabled;_injector;_renderer;_backdropClick=new S;_attachments=new S;_detachments=new S;_positionStrategy;_scrollStrategy;_locationChanges=G.EMPTY;_backdropRef=null;_detachContentMutationObserver;_detachContentAfterRenderRef;_disposed=!1;_previousHostParent;_keydownEvents=new S;_outsidePointerEvents=new S;_afterNextRenderRef;constructor(n,e,r,o,i,s,a,c,l,u=!1,d,h){this._portalOutlet=n,this._host=e,this._pane=r,this._config=o,this._ngZone=i,this._keyboardDispatcher=s,this._document=a,this._location=c,this._outsideClickDispatcher=l,this._animationsDisabled=u,this._injector=d,this._renderer=h,o.scrollStrategy&&(this._scrollStrategy=o.scrollStrategy,this._scrollStrategy.attach(this)),this._positionStrategy=o.positionStrategy}get overlayElement(){return this._pane}get backdropElement(){return this._backdropRef?.element||null}get hostElement(){return this._host}get eventPredicate(){return this._config?.eventPredicate||null}attach(n){if(this._disposed)return null;this._attachHost();let e=this._portalOutlet.attach(n);return this._positionStrategy?.attach(this),this._updateStackingOrder(),this._updateElementSize(),this._updateElementDirection(),this._scrollStrategy&&this._scrollStrategy.enable(),this._afterNextRenderRef?.destroy(),this._afterNextRenderRef=ht(()=>{this.hasAttached()&&this.updatePosition()},{injector:this._injector}),this._togglePointerEvents(!0),this._config.hasBackdrop&&this._attachBackdrop(),this._config.panelClass&&this._toggleClasses(this._pane,this._config.panelClass,!0),this._attachments.next(),this._completeDetachContent(),this._keyboardDispatcher.add(this),this._config.disposeOnNavigation&&(this._locationChanges=this._location.subscribe(()=>this.dispose())),this._outsideClickDispatcher.add(this),typeof e?.onDestroy=="function"&&e.onDestroy(()=>{this.hasAttached()&&this._ngZone.runOutsideAngular(()=>Promise.resolve().then(()=>this.detach()))}),e}detach(){if(!this.hasAttached())return;this.detachBackdrop(),this._togglePointerEvents(!1),this._positionStrategy&&this._positionStrategy.detach&&this._positionStrategy.detach(),this._scrollStrategy&&this._scrollStrategy.disable();let n=this._portalOutlet.detach();return this._detachments.next(),this._completeDetachContent(),this._keyboardDispatcher.remove(this),this._detachContentWhenEmpty(),this._locationChanges.unsubscribe(),this._outsideClickDispatcher.remove(this),n}dispose(){if(this._disposed)return;let n=this.hasAttached();this._positionStrategy&&this._positionStrategy.dispose(),this._disposeScrollStrategy(),this._backdropRef?.dispose(),this._locationChanges.unsubscribe(),this._keyboardDispatcher.remove(this),this._portalOutlet.dispose(),this._attachments.complete(),this._backdropClick.complete(),this._keydownEvents.complete(),this._outsidePointerEvents.complete(),this._outsideClickDispatcher.remove(this),this._host?.remove(),this._afterNextRenderRef?.destroy(),this._previousHostParent=this._pane=this._host=this._backdropRef=null,n&&this._detachments.next(),this._detachments.complete(),this._completeDetachContent(),this._disposed=!0}hasAttached(){return this._portalOutlet.hasAttached()}backdropClick(){return this._backdropClick}attachments(){return this._attachments}detachments(){return this._detachments}keydownEvents(){return this._keydownEvents}outsidePointerEvents(){return this._outsidePointerEvents}getConfig(){return this._config}updatePosition(){this._positionStrategy&&this._positionStrategy.apply()}updatePositionStrategy(n){n!==this._positionStrategy&&(this._positionStrategy&&this._positionStrategy.dispose(),this._positionStrategy=n,this.hasAttached()&&(n.attach(this),this.updatePosition()))}updateSize(n){this._config=g(g({},this._config),n),this._updateElementSize()}setDirection(n){this._config=F(g({},this._config),{direction:n}),this._updateElementDirection()}addPanelClass(n){this._pane&&this._toggleClasses(this._pane,n,!0)}removePanelClass(n){this._pane&&this._toggleClasses(this._pane,n,!1)}getDirection(){let n=this._config.direction;return n?typeof n=="string"?n:n.value:"ltr"}updateScrollStrategy(n){n!==this._scrollStrategy&&(this._disposeScrollStrategy(),this._scrollStrategy=n,this.hasAttached()&&(n.attach(this),n.enable()))}_updateElementDirection(){this._host.setAttribute("dir",this.getDirection())}_updateElementSize(){if(!this._pane)return;let n=this._pane.style;n.width=De(this._config.width),n.height=De(this._config.height),n.minWidth=De(this._config.minWidth),n.minHeight=De(this._config.minHeight),n.maxWidth=De(this._config.maxWidth),n.maxHeight=De(this._config.maxHeight)}_togglePointerEvents(n){this._pane.style.pointerEvents=n?"":"none"}_attachHost(){if(!this._host.parentElement){let n=this._config.usePopover?this._positionStrategy?.getPopoverInsertionPoint?.():null;Ig(n)?n.after(this._host):n?.type==="parent"?n.element.appendChild(this._host):this._previousHostParent?.appendChild(this._host)}if(this._config.usePopover)try{this._host.showPopover()}catch{}}_attachBackdrop(){let n="cdk-overlay-backdrop-showing";this._backdropRef?.dispose(),this._backdropRef=new Dg(this._document,this._renderer,this._ngZone,e=>{this._backdropClick.next(e)}),this._animationsDisabled&&this._backdropRef.element.classList.add("cdk-overlay-backdrop-noop-animation"),this._config.backdropClass&&this._toggleClasses(this._backdropRef.element,this._config.backdropClass,!0),this._config.usePopover?this._host.prepend(this._backdropRef.element):this._host.parentElement.insertBefore(this._backdropRef.element,this._host),!this._animationsDisabled&&typeof requestAnimationFrame<"u"?this._ngZone.runOutsideAngular(()=>{requestAnimationFrame(()=>this._backdropRef?.element.classList.add(n))}):this._backdropRef.element.classList.add(n)}_updateStackingOrder(){!this._config.usePopover&&this._host.nextSibling&&this._host.parentNode.appendChild(this._host)}detachBackdrop(){this._animationsDisabled?(this._backdropRef?.dispose(),this._backdropRef=null):this._backdropRef?.detach()}_toggleClasses(n,e,r){let o=ao(e||[]).filter(i=>!!i);o.length&&(r?n.classList.add(...o):n.classList.remove(...o))}_detachContentWhenEmpty(){let n=!1;try{this._detachContentAfterRenderRef=ht(()=>{n=!0,this._detachContent()},{injector:this._injector})}catch(e){if(n)throw e;this._detachContent()}globalThis.MutationObserver&&this._pane&&(this._detachContentMutationObserver||=new globalThis.MutationObserver(()=>{this._detachContent()}),this._detachContentMutationObserver.observe(this._pane,{childList:!0}))}_detachContent(){(!this._pane||!this._host||this._pane.children.length===0)&&(this._pane&&this._config.panelClass&&this._toggleClasses(this._pane,this._config.panelClass,!1),this._host&&this._host.parentElement&&(this._previousHostParent=this._host.parentElement,this._host.remove()),this._completeDetachContent())}_completeDetachContent(){this._detachContentAfterRenderRef?.destroy(),this._detachContentAfterRenderRef=void 0,this._detachContentMutationObserver?.disconnect()}_disposeScrollStrategy(){let n=this._scrollStrategy;n?.disable(),n?.detach?.()}},bC="cdk-overlay-connected-position-bounding-box",Lk=/([A-Za-z%]+)$/;function Sg(t,n){return new Ou(n,t.get(uo),t.get(L),t.get(he),t.get(Cg))}var Ou=class{_viewportRuler;_document;_platform;_overlayContainer;_overlayRef;_isInitialRender=!1;_lastBoundingBoxSize={width:0,height:0};_isPushed=!1;_canPush=!0;_growAfterOpen=!1;_hasFlexibleDimensions=!0;_positionLocked=!1;_originRect;_overlayRect;_viewportRect;_containerRect;_viewportMargin=0;_scrollables=[];_preferredPositions=[];_origin;_pane;_isDisposed=!1;_boundingBox=null;_lastPosition=null;_lastScrollVisibility=null;_positionChanges=new S;_resizeSubscription=G.EMPTY;_offsetX=0;_offsetY=0;_transformOriginSelector;_appliedPanelClasses=[];_previousPushAmount=null;_popoverLocation="global";positionChanges=this._positionChanges;get positions(){return this._preferredPositions}constructor(n,e,r,o,i){this._viewportRuler=e,this._document=r,this._platform=o,this._overlayContainer=i,this.setOrigin(n)}attach(n){this._overlayRef&&this._overlayRef,this._validatePositions(),n.hostElement.classList.add(bC),this._overlayRef=n,this._boundingBox=n.hostElement,this._pane=n.overlayElement,this._isDisposed=!1,this._isInitialRender=!0,this._lastPosition=null,this._resizeSubscription.unsubscribe(),this._resizeSubscription=this._viewportRuler.change().subscribe(()=>{this._isInitialRender=!0,this.apply()})}apply(){if(this._isDisposed||!this._platform.isBrowser)return;if(!this._isInitialRender&&this._positionLocked&&this._lastPosition){this.reapplyLastPosition();return}this._clearPanelClasses(),this._resetOverlayElementStyles(),this._resetBoundingBoxStyles(),this._viewportRect=this._getNarrowedViewportRect(),this._originRect=this._getOriginRect(),this._overlayRect=this._pane.getBoundingClientRect(),this._containerRect=this._getContainerRect();let n=this._originRect,e=this._overlayRect,r=this._viewportRect,o=this._containerRect,i=[],s;for(let a of this._preferredPositions){let c=this._getOriginPoint(n,o,a),l=this._getOverlayPoint(c,e,a),u=this._getOverlayFit(l,e,r,a);if(u.isCompletelyWithinViewport){this._isPushed=!1,this._applyPosition(a,c);return}if(this._canFitWithFlexibleDimensions(u,l,r)){i.push({position:a,origin:c,overlayRect:e,boundingBoxRect:this._calculateBoundingBoxRect(c,a)});continue}(!s||s.overlayFit.visibleAreac&&(c=u,a=l)}this._isPushed=!1,this._applyPosition(a.position,a.origin);return}if(this._canPush){this._isPushed=!0,this._applyPosition(s.position,s.originPoint);return}this._applyPosition(s.position,s.originPoint)}detach(){this._clearPanelClasses(),this._lastPosition=null,this._previousPushAmount=null,this._resizeSubscription.unsubscribe()}dispose(){this._isDisposed||(this._boundingBox&&fo(this._boundingBox.style,{top:"",left:"",right:"",bottom:"",height:"",width:"",alignItems:"",justifyContent:""}),this._pane&&this._resetOverlayElementStyles(),this._overlayRef&&this._overlayRef.hostElement.classList.remove(bC),this.detach(),this._positionChanges.complete(),this._overlayRef=this._boundingBox=null,this._isDisposed=!0)}reapplyLastPosition(){if(this._isDisposed||!this._platform.isBrowser)return;let n=this._lastPosition;n?(this._originRect=this._getOriginRect(),this._overlayRect=this._pane.getBoundingClientRect(),this._viewportRect=this._getNarrowedViewportRect(),this._containerRect=this._getContainerRect(),this._applyPosition(n,this._getOriginPoint(this._originRect,this._containerRect,n))):this.apply()}withScrollableContainers(n){return this._scrollables=n,this}withPositions(n){return this._preferredPositions=n,n.indexOf(this._lastPosition)===-1&&(this._lastPosition=null),this._validatePositions(),this}withViewportMargin(n){return this._viewportMargin=n,this}withFlexibleDimensions(n=!0){return this._hasFlexibleDimensions=n,this}withGrowAfterOpen(n=!0){return this._growAfterOpen=n,this}withPush(n=!0){return this._canPush=n,this}withLockedPosition(n=!0){return this._positionLocked=n,this}setOrigin(n){return this._origin=n,this}withDefaultOffsetX(n){return this._offsetX=n,this}withDefaultOffsetY(n){return this._offsetY=n,this}withTransformOriginOn(n){return this._transformOriginSelector=n,this}withPopoverLocation(n){return this._popoverLocation=n,this}getPopoverInsertionPoint(){return this._popoverLocation==="global"?null:this._popoverLocation!=="inline"?this._popoverLocation:this._origin instanceof z?this._origin.nativeElement:Ig(this._origin)?this._origin:null}_getOriginPoint(n,e,r){let o;if(r.originX=="center")o=n.left+n.width/2;else{let s=this._isRtl()?n.right:n.left,a=this._isRtl()?n.left:n.right;o=r.originX=="start"?s:a}e.left<0&&(o-=e.left);let i;return r.originY=="center"?i=n.top+n.height/2:i=r.originY=="top"?n.top:n.bottom,e.top<0&&(i-=e.top),{x:o,y:i}}_getOverlayPoint(n,e,r){let o;r.overlayX=="center"?o=-e.width/2:r.overlayX==="start"?o=this._isRtl()?-e.width:0:o=this._isRtl()?0:-e.width;let i;return r.overlayY=="center"?i=-e.height/2:i=r.overlayY=="top"?0:-e.height,{x:n.x+o,y:n.y+i}}_getOverlayFit(n,e,r,o){let i=DC(e),{x:s,y:a}=n,c=this._getOffset(o,"x"),l=this._getOffset(o,"y");c&&(s+=c),l&&(a+=l);let u=0-s,d=s+i.width-r.width,h=0-a,p=a+i.height-r.height,m=this._subtractOverflows(i.width,u,d),_=this._subtractOverflows(i.height,h,p),E=m*_;return{visibleArea:E,isCompletelyWithinViewport:i.width*i.height===E,fitsInViewportVertically:_===i.height,fitsInViewportHorizontally:m==i.width}}_canFitWithFlexibleDimensions(n,e,r){if(this._hasFlexibleDimensions){let o=r.bottom-e.y,i=r.right-e.x,s=_C(this._overlayRef.getConfig().minHeight),a=_C(this._overlayRef.getConfig().minWidth),c=n.fitsInViewportVertically||s!=null&&s<=o,l=n.fitsInViewportHorizontally||a!=null&&a<=i;return c&&l}return!1}_pushOverlayOnScreen(n,e,r){if(this._previousPushAmount&&this._positionLocked)return{x:n.x+this._previousPushAmount.x,y:n.y+this._previousPushAmount.y};let o=DC(e),i=this._viewportRect,s=Math.max(n.x+o.width-i.width,0),a=Math.max(n.y+o.height-i.height,0),c=Math.max(i.top-r.top-n.y,0),l=Math.max(i.left-r.left-n.x,0),u=0,d=0;return o.width<=i.width?u=l||-s:u=n.xm&&!this._isInitialRender&&!this._growAfterOpen&&(s=n.y-m/2)}let c=e.overlayX==="start"&&!o||e.overlayX==="end"&&o,l=e.overlayX==="end"&&!o||e.overlayX==="start"&&o,u,d,h;if(l)h=r.width-n.x+this._getViewportMarginStart()+this._getViewportMarginEnd(),u=n.x-this._getViewportMarginStart();else if(c)d=n.x,u=r.right-n.x-this._getViewportMarginEnd();else{let p=Math.min(r.right-n.x+r.left,n.x),m=this._lastBoundingBoxSize.width;u=p*2,d=n.x-p,u>m&&!this._isInitialRender&&!this._growAfterOpen&&(d=n.x-m/2)}return{top:s,left:d,bottom:a,right:h,width:u,height:i}}_setBoundingBoxStyles(n,e){let r=this._calculateBoundingBoxRect(n,e);!this._isInitialRender&&!this._growAfterOpen&&(r.height=Math.min(r.height,this._lastBoundingBoxSize.height),r.width=Math.min(r.width,this._lastBoundingBoxSize.width));let o={};if(this._hasExactPosition())o.top=o.left="0",o.bottom=o.right="auto",o.maxHeight=o.maxWidth="",o.width=o.height="100%";else{let i=this._overlayRef.getConfig().maxHeight,s=this._overlayRef.getConfig().maxWidth;o.width=De(r.width),o.height=De(r.height),o.top=De(r.top)||"auto",o.bottom=De(r.bottom)||"auto",o.left=De(r.left)||"auto",o.right=De(r.right)||"auto",e.overlayX==="center"?o.alignItems="center":o.alignItems=e.overlayX==="end"?"flex-end":"flex-start",e.overlayY==="center"?o.justifyContent="center":o.justifyContent=e.overlayY==="bottom"?"flex-end":"flex-start",i&&(o.maxHeight=De(i)),s&&(o.maxWidth=De(s))}this._lastBoundingBoxSize=r,fo(this._boundingBox.style,o)}_resetBoundingBoxStyles(){fo(this._boundingBox.style,{top:"0",left:"0",right:"0",bottom:"0",height:"",width:"",alignItems:"",justifyContent:""})}_resetOverlayElementStyles(){fo(this._pane.style,{top:"",left:"",bottom:"",right:"",position:"",transform:""})}_setOverlayElementStyles(n,e){let r={},o=this._hasExactPosition(),i=this._hasFlexibleDimensions,s=this._overlayRef.getConfig();if(o){let u=this._viewportRuler.getViewportScrollPosition();fo(r,this._getExactOverlayY(e,n,u)),fo(r,this._getExactOverlayX(e,n,u))}else r.position="static";let a="",c=this._getOffset(e,"x"),l=this._getOffset(e,"y");c&&(a+=`translateX(${c}px) `),l&&(a+=`translateY(${l}px)`),r.transform=a.trim(),s.maxHeight&&(o?r.maxHeight=De(s.maxHeight):i&&(r.maxHeight="")),s.maxWidth&&(o?r.maxWidth=De(s.maxWidth):i&&(r.maxWidth="")),fo(this._pane.style,r)}_getExactOverlayY(n,e,r){let o={top:"",bottom:""},i=this._getOverlayPoint(e,this._overlayRect,n);if(this._isPushed&&(i=this._pushOverlayOnScreen(i,this._overlayRect,r)),n.overlayY==="bottom"){let s=this._document.documentElement.clientHeight;o.bottom=`${s-(i.y+this._overlayRect.height)}px`}else o.top=De(i.y);return o}_getExactOverlayX(n,e,r){let o={left:"",right:""},i=this._getOverlayPoint(e,this._overlayRect,n);this._isPushed&&(i=this._pushOverlayOnScreen(i,this._overlayRect,r));let s;if(this._isRtl()?s=n.overlayX==="end"?"left":"right":s=n.overlayX==="end"?"right":"left",s==="right"){let a=this._document.documentElement.clientWidth;o.right=`${a-(i.x+this._overlayRect.width)}px`}else o.left=De(i.x);return o}_getScrollVisibility(){let n=this._getOriginRect(),e=this._pane.getBoundingClientRect(),r=this._scrollables.map(o=>o.getElementRef().nativeElement.getBoundingClientRect());return{isOriginClipped:vC(n,r),isOriginOutsideView:_g(n,r),isOverlayClipped:vC(e,r),isOverlayOutsideView:_g(e,r)}}_subtractOverflows(n,...e){return e.reduce((r,o)=>r-Math.max(o,0),n)}_getNarrowedViewportRect(){let n=this._document.documentElement.clientWidth,e=this._document.documentElement.clientHeight,r=this._viewportRuler.getViewportScrollPosition();return{top:r.top+this._getViewportMarginTop(),left:r.left+this._getViewportMarginStart(),right:r.left+n-this._getViewportMarginEnd(),bottom:r.top+e-this._getViewportMarginBottom(),width:n-this._getViewportMarginStart()-this._getViewportMarginEnd(),height:e-this._getViewportMarginTop()-this._getViewportMarginBottom()}}_isRtl(){return this._overlayRef.getDirection()==="rtl"}_hasExactPosition(){return!this._hasFlexibleDimensions||this._isPushed}_getOffset(n,e){return e==="x"?n.offsetX==null?this._offsetX:n.offsetX:n.offsetY==null?this._offsetY:n.offsetY}_validatePositions(){}_addPanelClasses(n){this._pane&&ao(n).forEach(e=>{e!==""&&this._appliedPanelClasses.indexOf(e)===-1&&(this._appliedPanelClasses.push(e),this._pane.classList.add(e))})}_clearPanelClasses(){this._pane&&(this._appliedPanelClasses.forEach(n=>{this._pane.classList.remove(n)}),this._appliedPanelClasses=[])}_getViewportMarginStart(){return typeof this._viewportMargin=="number"?this._viewportMargin:this._viewportMargin?.start??0}_getViewportMarginEnd(){return typeof this._viewportMargin=="number"?this._viewportMargin:this._viewportMargin?.end??0}_getViewportMarginTop(){return typeof this._viewportMargin=="number"?this._viewportMargin:this._viewportMargin?.top??0}_getViewportMarginBottom(){return typeof this._viewportMargin=="number"?this._viewportMargin:this._viewportMargin?.bottom??0}_getOriginRect(){let n=this._origin;if(n instanceof z)return n.nativeElement.getBoundingClientRect();if(n instanceof Element)return n.getBoundingClientRect();let e=n.width||0,r=n.height||0;return{top:n.y,bottom:n.y+r,left:n.x,right:n.x+e,height:r,width:e}}_getContainerRect(){let n=this._overlayRef.getConfig().usePopover&&this._popoverLocation!=="global",e=this._overlayContainer.getContainerElement();n&&(e.style.display="block");let r=e.getBoundingClientRect();return n&&(e.style.display=""),r}};function fo(t,n){for(let e in n)n.hasOwnProperty(e)&&(t[e]=n[e]);return t}function _C(t){if(typeof t!="number"&&t!=null){let[n,e]=t.split(Lk);return!e||e==="px"?parseFloat(n):null}return t||null}function DC(t){return{top:Math.floor(t.top),right:Math.floor(t.right),bottom:Math.floor(t.bottom),left:Math.floor(t.left),width:Math.floor(t.width),height:Math.floor(t.height)}}function jk(t,n){return t===n?!0:t.isOriginClipped===n.isOriginClipped&&t.isOriginOutsideView===n.isOriginOutsideView&&t.isOverlayClipped===n.isOverlayClipped&&t.isOverlayOutsideView===n.isOverlayOutsideView}var EC="cdk-global-overlay-wrapper";function AC(t){return new ku}var ku=class{_overlayRef;_cssPosition="static";_topOffset="";_bottomOffset="";_alignItems="";_xPosition="";_xOffset="";_width="";_height="";_isDisposed=!1;attach(n){let e=n.getConfig();this._overlayRef=n,this._width&&!e.width&&n.updateSize({width:this._width}),this._height&&!e.height&&n.updateSize({height:this._height}),n.hostElement.classList.add(EC),this._isDisposed=!1}top(n=""){return this._bottomOffset="",this._topOffset=n,this._alignItems="flex-start",this}left(n=""){return this._xOffset=n,this._xPosition="left",this}bottom(n=""){return this._topOffset="",this._bottomOffset=n,this._alignItems="flex-end",this}right(n=""){return this._xOffset=n,this._xPosition="right",this}start(n=""){return this._xOffset=n,this._xPosition="start",this}end(n=""){return this._xOffset=n,this._xPosition="end",this}width(n=""){return this._overlayRef?this._overlayRef.updateSize({width:n}):this._width=n,this}height(n=""){return this._overlayRef?this._overlayRef.updateSize({height:n}):this._height=n,this}centerHorizontally(n=""){return this.left(n),this._xPosition="center",this}centerVertically(n=""){return this.top(n),this._alignItems="center",this}apply(){if(!this._overlayRef||!this._overlayRef.hasAttached())return;let n=this._overlayRef.overlayElement.style,e=this._overlayRef.hostElement.style,r=this._overlayRef.getConfig(),{width:o,height:i,maxWidth:s,maxHeight:a}=r,c=(o==="100%"||o==="100vw")&&(!s||s==="100%"||s==="100vw"),l=(i==="100%"||i==="100vh")&&(!a||a==="100%"||a==="100vh"),u=this._xPosition,d=this._xOffset,h=this._overlayRef.getConfig().direction==="rtl",p="",m="",_="";c?_="flex-start":u==="center"?(_="center",h?m=d:p=d):h?u==="left"||u==="end"?(_="flex-end",p=d):(u==="right"||u==="start")&&(_="flex-start",m=d):u==="left"||u==="start"?(_="flex-start",p=d):(u==="right"||u==="end")&&(_="flex-end",m=d),n.position=this._cssPosition,n.marginLeft=c?"0":p,n.marginTop=l?"0":this._topOffset,n.marginBottom=this._bottomOffset,n.marginRight=c?"0":m,e.justifyContent=_,e.alignItems=l?"flex-start":this._alignItems}dispose(){if(this._isDisposed||!this._overlayRef)return;let n=this._overlayRef.overlayElement.style,e=this._overlayRef.hostElement,r=e.style;e.classList.remove(EC),r.justifyContent=r.alignItems=n.marginTop=n.marginBottom=n.marginLeft=n.marginRight=n.position="",this._overlayRef=null,this._isDisposed=!0}},RC=(()=>{class t{_injector=f($);constructor(){}global(){return AC()}flexibleConnectedTo(e){return Sg(this._injector,e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Mg=new y("OVERLAY_DEFAULT_CONFIG");function Tg(t,n){t.get(xt).load(xC);let e=t.get(Cg),r=t.get(L),o=t.get(ta),i=t.get(Be),s=t.get(fi),a=t.get(Oe,null,{optional:!0})||t.get(je).createRenderer(null,null),c=new ca(n),l=t.get(Mg,null,{optional:!0})?.usePopover??!0;c.direction=c.direction||s.value,"showPopover"in r.body?c.usePopover=n?.usePopover??l:c.usePopover=!1;let u=r.createElement("div"),d=r.createElement("div");u.id=o.getId("cdk-overlay-"),u.classList.add("cdk-overlay-pane"),d.appendChild(u),c.usePopover&&(d.setAttribute("popover","manual"),d.classList.add("cdk-overlay-popover"));let h=c.usePopover?c.positionStrategy?.getPopoverInsertionPoint?.():null;return Ig(h)?h.after(d):h?.type==="parent"?h.element.appendChild(d):e.getContainerElement().appendChild(d),new Nu(new Mu(u,i,t),d,u,c,t.get(j),t.get(MC),r,t.get(bn),t.get(TC),n?.disableAnimations??t.get(Is,null,{optional:!0})==="NoopAnimations",t.get(re),a)}var NC=(()=>{class t{scrollStrategies=f(IC);_positionBuilder=f(RC);_injector=f($);constructor(){}create(e){return Tg(this._injector,e)}position(){return this._positionBuilder}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Vk=[{originX:"start",originY:"bottom",overlayX:"start",overlayY:"top"},{originX:"start",originY:"top",overlayX:"start",overlayY:"bottom"},{originX:"end",originY:"top",overlayX:"end",overlayY:"bottom"},{originX:"end",originY:"bottom",overlayX:"end",overlayY:"top"}],Bk=new y("cdk-connected-overlay-scroll-strategy",{providedIn:"root",factory:()=>{let t=f($);return()=>wg(t)}}),Eg=(()=>{class t{elementRef=f(z);constructor(){}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["","cdk-overlay-origin",""],["","overlay-origin",""],["","cdkOverlayOrigin",""]],exportAs:["cdkOverlayOrigin"]})}return t})(),OC=new y("cdk-connected-overlay-default-config"),Uk=(()=>{class t{_dir=f(fi,{optional:!0});_injector=f($);_overlayRef;_templatePortal;_backdropSubscription=G.EMPTY;_attachSubscription=G.EMPTY;_detachSubscription=G.EMPTY;_positionSubscription=G.EMPTY;_offsetX;_offsetY;_position;_scrollStrategyFactory=f(Bk);_ngZone=f(j);origin;positions;positionStrategy;get offsetX(){return this._offsetX}set offsetX(e){this._offsetX=e,this._position&&this._updatePositionStrategy(this._position)}get offsetY(){return this._offsetY}set offsetY(e){this._offsetY=e,this._position&&this._updatePositionStrategy(this._position)}width;height;minWidth;minHeight;backdropClass;panelClass;viewportMargin=0;scrollStrategy;open=!1;disableClose=!1;transformOriginSelector;hasBackdrop=!1;lockPosition=!1;flexibleDimensions=!1;growAfterOpen=!1;push=!1;disposeOnNavigation=!1;usePopover;matchWidth=!1;set _config(e){typeof e!="string"&&this._assignConfig(e)}backdropClick=new U;positionChange=new U;attach=new U;detach=new U;overlayKeydown=new U;overlayOutsideClick=new U;constructor(){let e=f(dt),r=f(qe),o=f(OC,{optional:!0}),i=f(Mg,{optional:!0});this.usePopover=i?.usePopover===!1?null:"global",this._templatePortal=new hi(e,r),this.scrollStrategy=this._scrollStrategyFactory(),o&&this._assignConfig(o)}get overlayRef(){return this._overlayRef}get dir(){return this._dir?this._dir.value:"ltr"}ngOnDestroy(){this._attachSubscription.unsubscribe(),this._detachSubscription.unsubscribe(),this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this._overlayRef?.dispose()}ngOnChanges(e){this._position&&(this._updatePositionStrategy(this._position),this._overlayRef?.updateSize({width:this._getWidth(),minWidth:this.minWidth,height:this.height,minHeight:this.minHeight}),e.origin&&this.open&&this._position.apply()),e.open&&(this.open?this.attachOverlay():this.detachOverlay())}_createOverlay(){(!this.positions||!this.positions.length)&&(this.positions=Vk);let e=this._overlayRef=Tg(this._injector,this._buildConfig());this._attachSubscription=e.attachments().subscribe(()=>this.attach.emit()),this._detachSubscription=e.detachments().subscribe(()=>this.detach.emit()),e.keydownEvents().subscribe(r=>{this.overlayKeydown.next(r),r.keyCode===27&&!this.disableClose&&!bu(r)&&(r.preventDefault(),this.detachOverlay())}),this._overlayRef.outsidePointerEvents().subscribe(r=>{let o=this._getOriginElement(),i=Je(r);(!o||o!==i&&!o.contains(i))&&this.overlayOutsideClick.next(r)})}_buildConfig(){let e=this._position=this.positionStrategy||this._createPositionStrategy(),r=new ca({direction:this._dir||"ltr",positionStrategy:e,scrollStrategy:this.scrollStrategy,hasBackdrop:this.hasBackdrop,disposeOnNavigation:this.disposeOnNavigation,usePopover:!!this.usePopover});return(this.height||this.height===0)&&(r.height=this.height),(this.minWidth||this.minWidth===0)&&(r.minWidth=this.minWidth),(this.minHeight||this.minHeight===0)&&(r.minHeight=this.minHeight),this.backdropClass&&(r.backdropClass=this.backdropClass),this.panelClass&&(r.panelClass=this.panelClass),r}_updatePositionStrategy(e){let r=this.positions.map(o=>({originX:o.originX,originY:o.originY,overlayX:o.overlayX,overlayY:o.overlayY,offsetX:o.offsetX||this.offsetX,offsetY:o.offsetY||this.offsetY,panelClass:o.panelClass||void 0}));return e.setOrigin(this._getOrigin()).withPositions(r).withFlexibleDimensions(this.flexibleDimensions).withPush(this.push).withGrowAfterOpen(this.growAfterOpen).withViewportMargin(this.viewportMargin).withLockedPosition(this.lockPosition).withTransformOriginOn(this.transformOriginSelector).withPopoverLocation(this.usePopover===null?"global":this.usePopover)}_createPositionStrategy(){let e=Sg(this._injector,this._getOrigin());return this._updatePositionStrategy(e),e}_getOrigin(){return this.origin instanceof Eg?this.origin.elementRef:this.origin}_getOriginElement(){return this.origin instanceof Eg?this.origin.elementRef.nativeElement:this.origin instanceof z?this.origin.nativeElement:typeof Element<"u"&&this.origin instanceof Element?this.origin:null}_getWidth(){return this.width?this.width:this.matchWidth?this._getOriginElement()?.getBoundingClientRect?.().width:void 0}attachOverlay(){this._overlayRef||this._createOverlay();let e=this._overlayRef;e.getConfig().hasBackdrop=this.hasBackdrop,e.updateSize({width:this._getWidth()}),e.hasAttached()||e.attach(this._templatePortal),this.hasBackdrop?this._backdropSubscription=e.backdropClick().subscribe(r=>this.backdropClick.emit(r)):this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this.positionChange.observers.length>0&&(this._positionSubscription=this._position.positionChanges.pipe(Yd(()=>this.positionChange.observers.length>0)).subscribe(r=>{this._ngZone.run(()=>this.positionChange.emit(r)),this.positionChange.observers.length===0&&this._positionSubscription.unsubscribe()})),this.open=!0}detachOverlay(){this._overlayRef?.detach(),this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this.open=!1}_assignConfig(e){this.origin=e.origin??this.origin,this.positions=e.positions??this.positions,this.positionStrategy=e.positionStrategy??this.positionStrategy,this.offsetX=e.offsetX??this.offsetX,this.offsetY=e.offsetY??this.offsetY,this.width=e.width??this.width,this.height=e.height??this.height,this.minWidth=e.minWidth??this.minWidth,this.minHeight=e.minHeight??this.minHeight,this.backdropClass=e.backdropClass??this.backdropClass,this.panelClass=e.panelClass??this.panelClass,this.viewportMargin=e.viewportMargin??this.viewportMargin,this.scrollStrategy=e.scrollStrategy??this.scrollStrategy,this.disableClose=e.disableClose??this.disableClose,this.transformOriginSelector=e.transformOriginSelector??this.transformOriginSelector,this.hasBackdrop=e.hasBackdrop??this.hasBackdrop,this.lockPosition=e.lockPosition??this.lockPosition,this.flexibleDimensions=e.flexibleDimensions??this.flexibleDimensions,this.growAfterOpen=e.growAfterOpen??this.growAfterOpen,this.push=e.push??this.push,this.disposeOnNavigation=e.disposeOnNavigation??this.disposeOnNavigation,this.usePopover=e.usePopover??this.usePopover,this.matchWidth=e.matchWidth??this.matchWidth}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["","cdk-connected-overlay",""],["","connected-overlay",""],["","cdkConnectedOverlay",""]],inputs:{origin:[0,"cdkConnectedOverlayOrigin","origin"],positions:[0,"cdkConnectedOverlayPositions","positions"],positionStrategy:[0,"cdkConnectedOverlayPositionStrategy","positionStrategy"],offsetX:[0,"cdkConnectedOverlayOffsetX","offsetX"],offsetY:[0,"cdkConnectedOverlayOffsetY","offsetY"],width:[0,"cdkConnectedOverlayWidth","width"],height:[0,"cdkConnectedOverlayHeight","height"],minWidth:[0,"cdkConnectedOverlayMinWidth","minWidth"],minHeight:[0,"cdkConnectedOverlayMinHeight","minHeight"],backdropClass:[0,"cdkConnectedOverlayBackdropClass","backdropClass"],panelClass:[0,"cdkConnectedOverlayPanelClass","panelClass"],viewportMargin:[0,"cdkConnectedOverlayViewportMargin","viewportMargin"],scrollStrategy:[0,"cdkConnectedOverlayScrollStrategy","scrollStrategy"],open:[0,"cdkConnectedOverlayOpen","open"],disableClose:[0,"cdkConnectedOverlayDisableClose","disableClose"],transformOriginSelector:[0,"cdkConnectedOverlayTransformOriginOn","transformOriginSelector"],hasBackdrop:[2,"cdkConnectedOverlayHasBackdrop","hasBackdrop",ue],lockPosition:[2,"cdkConnectedOverlayLockPosition","lockPosition",ue],flexibleDimensions:[2,"cdkConnectedOverlayFlexibleDimensions","flexibleDimensions",ue],growAfterOpen:[2,"cdkConnectedOverlayGrowAfterOpen","growAfterOpen",ue],push:[2,"cdkConnectedOverlayPush","push",ue],disposeOnNavigation:[2,"cdkConnectedOverlayDisposeOnNavigation","disposeOnNavigation",ue],usePopover:[0,"cdkConnectedOverlayUsePopover","usePopover"],matchWidth:[2,"cdkConnectedOverlayMatchWidth","matchWidth",ue],_config:[0,"cdkConnectedOverlay","_config"]},outputs:{backdropClick:"backdropClick",positionChange:"positionChange",attach:"attach",detach:"detach",overlayKeydown:"overlayKeydown",overlayOutsideClick:"overlayOutsideClick"},exportAs:["cdkConnectedOverlay"],features:[Re]})}return t})(),Hk=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({providers:[NC],imports:[zn,mC,vg,vg]})}return t})();var $C=(()=>{class t{_renderer;_elementRef;onChange=e=>{};onTouched=()=>{};constructor(e,r){this._renderer=e,this._elementRef=r}setProperty(e,r){this._renderer.setProperty(this._elementRef.nativeElement,e,r)}registerOnTouched(e){this.onTouched=e}registerOnChange(e){this.onChange=e}setDisabledState(e){this.setProperty("disabled",e)}static \u0275fac=function(r){return new(r||t)(D(Oe),D(z))};static \u0275dir=M({type:t})}return t})(),Gu=(()=>{class t extends $C{static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275dir=M({type:t,features:[J]})}return t})(),po=new y("");var $k={provide:po,useExisting:be(()=>zC),multi:!0};function zk(){let t=mt()?mt().getUserAgent():"";return/android (\d+)/.test(t.toLowerCase())}var Gk=new y(""),zC=(()=>{class t extends $C{_compositionMode;_composing=!1;constructor(e,r,o){super(e,r),this._compositionMode=o,this._compositionMode==null&&(this._compositionMode=!zk())}writeValue(e){let r=e??"";this.setProperty("value",r)}_handleInput(e){(!this._compositionMode||this._compositionMode&&!this._composing)&&this.onChange(e)}_compositionStart(){this._composing=!0}_compositionEnd(e){this._composing=!1,this._compositionMode&&this.onChange(e)}static \u0275fac=function(r){return new(r||t)(D(Oe),D(z),D(Gk,8))};static \u0275dir=M({type:t,selectors:[["input","formControlName","",3,"type","checkbox"],["textarea","formControlName",""],["input","formControl","",3,"type","checkbox"],["textarea","formControl",""],["input","ngModel","",3,"type","checkbox"],["textarea","ngModel",""],["","ngDefaultControl",""]],hostBindings:function(r,o){r&1&&Zt("input",function(s){return o._handleInput(s.target.value)})("blur",function(){return o.onTouched()})("compositionstart",function(){return o._compositionStart()})("compositionend",function(s){return o._compositionEnd(s.target.value)})},standalone:!1,features:[we([$k]),J]})}return t})();function Ng(t){return t==null||Og(t)===0}function Og(t){return t==null?null:Array.isArray(t)||typeof t=="string"?t.length:t instanceof Set?t.size:null}var En=new y(""),mo=new y(""),Wk=/^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,kC=class{static min(n){return GC(n)}static max(n){return WC(n)}static required(n){return qC(n)}static requiredTrue(n){return qk(n)}static email(n){return Yk(n)}static minLength(n){return Zk(n)}static maxLength(n){return Kk(n)}static pattern(n){return Qk(n)}static nullValidator(n){return Pu()}static compose(n){return JC(n)}static composeAsync(n){return eI(n)}};function GC(t){return n=>{if(n.value==null||t==null)return null;let e=parseFloat(n.value);return!isNaN(e)&&e{if(n.value==null||t==null)return null;let e=parseFloat(n.value);return!isNaN(e)&&e>t?{max:{max:t,actual:n.value}}:null}}function qC(t){return Ng(t.value)?{required:!0}:null}function qk(t){return t.value===!0?null:{required:!0}}function Yk(t){return Ng(t.value)||Wk.test(t.value)?null:{email:!0}}function Zk(t){return n=>{let e=n.value?.length??Og(n.value);return e===null||e===0?null:e{let e=n.value?.length??Og(n.value);return e!==null&&e>t?{maxlength:{requiredLength:t,actualLength:e}}:null}}function Qk(t){if(!t)return Pu;let n,e;return typeof t=="string"?(e="",t.charAt(0)!=="^"&&(e+="^"),e+=t,t.charAt(t.length-1)!=="$"&&(e+="$"),n=new RegExp(e)):(e=t.toString(),n=t),r=>{if(Ng(r.value))return null;let o=r.value;return n.test(o)?null:{pattern:{requiredPattern:e,actualValue:o}}}}function Pu(t){return null}function YC(t){return t!=null}function ZC(t){return jn(t)?se(t):t}function KC(t){let n={};return t.forEach(e=>{n=e!=null?g(g({},n),e):n}),Object.keys(n).length===0?null:n}function QC(t,n){return n.map(e=>e(t))}function Xk(t){return!t.validate}function XC(t){return t.map(n=>Xk(n)?n:e=>n.validate(e))}function JC(t){if(!t)return null;let n=t.filter(YC);return n.length==0?null:function(e){return KC(QC(e,n))}}function kg(t){return t!=null?JC(XC(t)):null}function eI(t){if(!t)return null;let n=t.filter(YC);return n.length==0?null:function(e){let r=QC(e,n).map(ZC);return Ud(r).pipe(H(KC))}}function Fg(t){return t!=null?eI(XC(t)):null}function FC(t,n){return t===null?[n]:Array.isArray(t)?[...t,n]:[t,n]}function tI(t){return t._rawValidators}function nI(t){return t._rawAsyncValidators}function xg(t){return t?Array.isArray(t)?t:[t]:[]}function Lu(t,n){return Array.isArray(t)?t.includes(n):t===n}function PC(t,n){let e=xg(n);return xg(t).forEach(o=>{Lu(e,o)||e.push(o)}),e}function LC(t,n){return xg(n).filter(e=>!Lu(t,e))}var ju=class{get value(){return this.control?this.control.value:null}get valid(){return this.control?this.control.valid:null}get invalid(){return this.control?this.control.invalid:null}get pending(){return this.control?this.control.pending:null}get disabled(){return this.control?this.control.disabled:null}get enabled(){return this.control?this.control.enabled:null}get errors(){return this.control?this.control.errors:null}get pristine(){return this.control?this.control.pristine:null}get dirty(){return this.control?this.control.dirty:null}get touched(){return this.control?this.control.touched:null}get status(){return this.control?this.control.status:null}get untouched(){return this.control?this.control.untouched:null}get statusChanges(){return this.control?this.control.statusChanges:null}get valueChanges(){return this.control?this.control.valueChanges:null}get path(){return null}_composedValidatorFn;_composedAsyncValidatorFn;_rawValidators=[];_rawAsyncValidators=[];_setValidators(n){this._rawValidators=n||[],this._composedValidatorFn=kg(this._rawValidators)}_setAsyncValidators(n){this._rawAsyncValidators=n||[],this._composedAsyncValidatorFn=Fg(this._rawAsyncValidators)}get validator(){return this._composedValidatorFn||null}get asyncValidator(){return this._composedAsyncValidatorFn||null}_onDestroyCallbacks=[];_registerOnDestroy(n){this._onDestroyCallbacks.push(n)}_invokeOnDestroyCallbacks(){this._onDestroyCallbacks.forEach(n=>n()),this._onDestroyCallbacks=[]}reset(n=void 0){this.control?.reset(n)}hasError(n,e){return this.control?this.control.hasError(n,e):!1}getError(n,e){return this.control?this.control.getError(n,e):null}},et=class extends ju{name;get formDirective(){return null}get path(){return null}},Gn=class extends ju{_parent=null;name=null;valueAccessor=null},Vu=class{_cd;constructor(n){this._cd=n}get isTouched(){return this._cd?.control?._touched?.(),!!this._cd?.control?.touched}get isUntouched(){return!!this._cd?.control?.untouched}get isPristine(){return this._cd?.control?._pristine?.(),!!this._cd?.control?.pristine}get isDirty(){return!!this._cd?.control?.dirty}get isValid(){return this._cd?.control?._status?.(),!!this._cd?.control?.valid}get isInvalid(){return!!this._cd?.control?.invalid}get isPending(){return!!this._cd?.control?.pending}get isSubmitted(){return this._cd?._submitted?.(),!!this._cd?.submitted}};var BQ=(()=>{class t extends Vu{constructor(e){super(e)}static \u0275fac=function(r){return new(r||t)(D(Gn,2))};static \u0275dir=M({type:t,selectors:[["","formControlName",""],["","ngModel",""],["","formControl",""]],hostVars:14,hostBindings:function(r,o){r&2&&Xe("ng-untouched",o.isUntouched)("ng-touched",o.isTouched)("ng-pristine",o.isPristine)("ng-dirty",o.isDirty)("ng-valid",o.isValid)("ng-invalid",o.isInvalid)("ng-pending",o.isPending)},standalone:!1,features:[J]})}return t})(),UQ=(()=>{class t extends Vu{constructor(e){super(e)}static \u0275fac=function(r){return new(r||t)(D(et,10))};static \u0275dir=M({type:t,selectors:[["","formGroupName",""],["","formArrayName",""],["","ngModelGroup",""],["","formGroup",""],["","formArray",""],["form",3,"ngNoForm",""],["","ngForm",""]],hostVars:16,hostBindings:function(r,o){r&2&&Xe("ng-untouched",o.isUntouched)("ng-touched",o.isTouched)("ng-pristine",o.isPristine)("ng-dirty",o.isDirty)("ng-valid",o.isValid)("ng-invalid",o.isInvalid)("ng-pending",o.isPending)("ng-submitted",o.isSubmitted)},standalone:!1,features:[J]})}return t})();var la="VALID",Fu="INVALID",pi="PENDING",ua="DISABLED",gr=class{},Bu=class extends gr{value;source;constructor(n,e){super(),this.value=n,this.source=e}},fa=class extends gr{pristine;source;constructor(n,e){super(),this.pristine=n,this.source=e}},ha=class extends gr{touched;source;constructor(n,e){super(),this.touched=n,this.source=e}},mi=class extends gr{status;source;constructor(n,e){super(),this.status=n,this.source=e}},Uu=class extends gr{source;constructor(n){super(),this.source=n}},pa=class extends gr{source;constructor(n){super(),this.source=n}};function Pg(t){return(Wu(t)?t.validators:t)||null}function Jk(t){return Array.isArray(t)?kg(t):t||null}function Lg(t,n){return(Wu(n)?n.asyncValidators:t)||null}function eF(t){return Array.isArray(t)?Fg(t):t||null}function Wu(t){return t!=null&&!Array.isArray(t)&&typeof t=="object"}function rI(t,n,e){let r=t.controls;if(!(n?Object.keys(r):r).length)throw new b(1e3,"");if(!r[e])throw new b(1001,"")}function oI(t,n,e){t._forEachChild((r,o)=>{if(e[o]===void 0)throw new b(-1002,"")})}var vi=class{_pendingDirty=!1;_hasOwnPendingAsyncValidator=null;_pendingTouched=!1;_onCollectionChange=()=>{};_updateOn;_parent=null;_asyncValidationSubscription;_composedValidatorFn;_composedAsyncValidatorFn;_rawValidators;_rawAsyncValidators;value;constructor(n,e){this._assignValidators(n),this._assignAsyncValidators(e)}get validator(){return this._composedValidatorFn}set validator(n){this._rawValidators=this._composedValidatorFn=n}get asyncValidator(){return this._composedAsyncValidatorFn}set asyncValidator(n){this._rawAsyncValidators=this._composedAsyncValidatorFn=n}get parent(){return this._parent}get status(){return q(this.statusReactive)}set status(n){q(()=>this.statusReactive.set(n))}_status=Kt(()=>this.statusReactive());statusReactive=W(void 0);get valid(){return this.status===la}get invalid(){return this.status===Fu}get pending(){return this.status===pi}get disabled(){return this.status===ua}get enabled(){return this.status!==ua}errors;get pristine(){return q(this.pristineReactive)}set pristine(n){q(()=>this.pristineReactive.set(n))}_pristine=Kt(()=>this.pristineReactive());pristineReactive=W(!0);get dirty(){return!this.pristine}get touched(){return q(this.touchedReactive)}set touched(n){q(()=>this.touchedReactive.set(n))}_touched=Kt(()=>this.touchedReactive());touchedReactive=W(!1);get untouched(){return!this.touched}_events=new S;events=this._events.asObservable();valueChanges;statusChanges;get updateOn(){return this._updateOn?this._updateOn:this.parent?this.parent.updateOn:"change"}setValidators(n){this._assignValidators(n)}setAsyncValidators(n){this._assignAsyncValidators(n)}addValidators(n){this.setValidators(PC(n,this._rawValidators))}addAsyncValidators(n){this.setAsyncValidators(PC(n,this._rawAsyncValidators))}removeValidators(n){this.setValidators(LC(n,this._rawValidators))}removeAsyncValidators(n){this.setAsyncValidators(LC(n,this._rawAsyncValidators))}hasValidator(n){return Lu(this._rawValidators,n)}hasAsyncValidator(n){return Lu(this._rawAsyncValidators,n)}clearValidators(){this.validator=null}clearAsyncValidators(){this.asyncValidator=null}markAsTouched(n={}){let e=this.touched===!1;this.touched=!0;let r=n.sourceControl??this;n.onlySelf||this._parent?.markAsTouched(F(g({},n),{sourceControl:r})),e&&n.emitEvent!==!1&&this._events.next(new ha(!0,r))}markAllAsDirty(n={}){this.markAsDirty({onlySelf:!0,emitEvent:n.emitEvent,sourceControl:this}),this._forEachChild(e=>e.markAllAsDirty(n))}markAllAsTouched(n={}){this.markAsTouched({onlySelf:!0,emitEvent:n.emitEvent,sourceControl:this}),this._forEachChild(e=>e.markAllAsTouched(n))}markAsUntouched(n={}){let e=this.touched===!0;this.touched=!1,this._pendingTouched=!1;let r=n.sourceControl??this;this._forEachChild(o=>{o.markAsUntouched({onlySelf:!0,emitEvent:n.emitEvent,sourceControl:r})}),n.onlySelf||this._parent?._updateTouched(n,r),e&&n.emitEvent!==!1&&this._events.next(new ha(!1,r))}markAsDirty(n={}){let e=this.pristine===!0;this.pristine=!1;let r=n.sourceControl??this;n.onlySelf||this._parent?.markAsDirty(F(g({},n),{sourceControl:r})),e&&n.emitEvent!==!1&&this._events.next(new fa(!1,r))}markAsPristine(n={}){let e=this.pristine===!1;this.pristine=!0,this._pendingDirty=!1;let r=n.sourceControl??this;this._forEachChild(o=>{o.markAsPristine({onlySelf:!0,emitEvent:n.emitEvent})}),n.onlySelf||this._parent?._updatePristine(n,r),e&&n.emitEvent!==!1&&this._events.next(new fa(!0,r))}markAsPending(n={}){this.status=pi;let e=n.sourceControl??this;n.emitEvent!==!1&&(this._events.next(new mi(this.status,e)),this.statusChanges.emit(this.status)),n.onlySelf||this._parent?.markAsPending(F(g({},n),{sourceControl:e}))}disable(n={}){let e=this._parentMarkedDirty(n.onlySelf);this.status=ua,this.errors=null,this._forEachChild(o=>{o.disable(F(g({},n),{onlySelf:!0}))}),this._updateValue();let r=n.sourceControl??this;n.emitEvent!==!1&&(this._events.next(new Bu(this.value,r)),this._events.next(new mi(this.status,r)),this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._updateAncestors(F(g({},n),{skipPristineCheck:e}),this),this._onDisabledChange.forEach(o=>o(!0))}enable(n={}){let e=this._parentMarkedDirty(n.onlySelf);this.status=la,this._forEachChild(r=>{r.enable(F(g({},n),{onlySelf:!0}))}),this.updateValueAndValidity({onlySelf:!0,emitEvent:n.emitEvent}),this._updateAncestors(F(g({},n),{skipPristineCheck:e}),this),this._onDisabledChange.forEach(r=>r(!1))}_updateAncestors(n,e){n.onlySelf||(this._parent?.updateValueAndValidity(n),n.skipPristineCheck||this._parent?._updatePristine({},e),this._parent?._updateTouched({},e))}setParent(n){this._parent=n}getRawValue(){return this.value}updateValueAndValidity(n={}){if(this._setInitialStatus(),this._updateValue(),this.enabled){let r=this._cancelExistingSubscription();this.errors=this._runValidator(),this.status=this._calculateStatus(),(this.status===la||this.status===pi)&&this._runAsyncValidator(r,n.emitEvent)}let e=n.sourceControl??this;n.emitEvent!==!1&&(this._events.next(new Bu(this.value,e)),this._events.next(new mi(this.status,e)),this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),n.onlySelf||this._parent?.updateValueAndValidity(F(g({},n),{sourceControl:e}))}_updateTreeValidity(n={emitEvent:!0}){this._forEachChild(e=>e._updateTreeValidity(n)),this.updateValueAndValidity({onlySelf:!0,emitEvent:n.emitEvent})}_setInitialStatus(){this.status=this._allControlsDisabled()?ua:la}_runValidator(){return this.validator?this.validator(this):null}_runAsyncValidator(n,e){if(this.asyncValidator){this.status=pi,this._hasOwnPendingAsyncValidator={emitEvent:e!==!1,shouldHaveEmitted:n!==!1};let r=ZC(this.asyncValidator(this));this._asyncValidationSubscription=r.subscribe(o=>{this._hasOwnPendingAsyncValidator=null,this.setErrors(o,{emitEvent:e,shouldHaveEmitted:n})})}}_cancelExistingSubscription(){if(this._asyncValidationSubscription){this._asyncValidationSubscription.unsubscribe();let n=(this._hasOwnPendingAsyncValidator?.emitEvent||this._hasOwnPendingAsyncValidator?.shouldHaveEmitted)??!1;return this._hasOwnPendingAsyncValidator=null,n}return!1}setErrors(n,e={}){this.errors=n,this._updateControlsErrors(e.emitEvent!==!1,this,e.shouldHaveEmitted)}get(n){let e=n;return e==null||(Array.isArray(e)||(e=e.split(".")),e.length===0)?null:e.reduce((r,o)=>r&&r._find(o),this)}getError(n,e){let r=e?this.get(e):this;return r?.errors?r.errors[n]:null}hasError(n,e){return!!this.getError(n,e)}get root(){let n=this;for(;n._parent;)n=n._parent;return n}_updateControlsErrors(n,e,r){this.status=this._calculateStatus(),n&&this.statusChanges.emit(this.status),(n||r)&&this._events.next(new mi(this.status,e)),this._parent&&this._parent._updateControlsErrors(n,e,r)}_initObservables(){this.valueChanges=new U,this.statusChanges=new U}_calculateStatus(){return this._allControlsDisabled()?ua:this.errors?Fu:this._hasOwnPendingAsyncValidator||this._anyControlsHaveStatus(pi)?pi:this._anyControlsHaveStatus(Fu)?Fu:la}_anyControlsHaveStatus(n){return this._anyControls(e=>e.status===n)}_anyControlsDirty(){return this._anyControls(n=>n.dirty)}_anyControlsTouched(){return this._anyControls(n=>n.touched)}_updatePristine(n,e){let r=!this._anyControlsDirty(),o=this.pristine!==r;this.pristine=r,n.onlySelf||this._parent?._updatePristine(n,e),o&&this._events.next(new fa(this.pristine,e))}_updateTouched(n={},e){this.touched=this._anyControlsTouched(),this._events.next(new ha(this.touched,e)),n.onlySelf||this._parent?._updateTouched(n,e)}_onDisabledChange=[];_registerOnCollectionChange(n){this._onCollectionChange=n}_setUpdateStrategy(n){Wu(n)&&n.updateOn!=null&&(this._updateOn=n.updateOn)}_parentMarkedDirty(n){return!n&&!!this._parent?.dirty&&!this._parent._anyControlsDirty()}_find(n){return null}_assignValidators(n){this._rawValidators=Array.isArray(n)?n.slice():n,this._composedValidatorFn=Jk(this._rawValidators)}_assignAsyncValidators(n){this._rawAsyncValidators=Array.isArray(n)?n.slice():n,this._composedAsyncValidatorFn=eF(this._rawAsyncValidators)}},ho=class extends vi{constructor(n,e,r){super(Pg(e),Lg(r,e)),this.controls=n,this._initObservables(),this._setUpdateStrategy(e),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator})}controls;registerControl(n,e){return this.controls[n]?this.controls[n]:(this.controls[n]=e,e.setParent(this),e._registerOnCollectionChange(this._onCollectionChange),e)}addControl(n,e,r={}){this.registerControl(n,e),this.updateValueAndValidity({emitEvent:r.emitEvent}),this._onCollectionChange()}removeControl(n,e={}){this.controls[n]&&this.controls[n]._registerOnCollectionChange(()=>{}),delete this.controls[n],this.updateValueAndValidity({emitEvent:e.emitEvent}),this._onCollectionChange()}setControl(n,e,r={}){this.controls[n]&&this.controls[n]._registerOnCollectionChange(()=>{}),delete this.controls[n],e&&this.registerControl(n,e),this.updateValueAndValidity({emitEvent:r.emitEvent}),this._onCollectionChange()}contains(n){return this.controls.hasOwnProperty(n)&&this.controls[n].enabled}setValue(n,e={}){oI(this,!0,n),Object.keys(n).forEach(r=>{rI(this,!0,r),this.controls[r].setValue(n[r],{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e)}patchValue(n,e={}){n!=null&&(Object.keys(n).forEach(r=>{let o=this.controls[r];o&&o.patchValue(n[r],{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e))}reset(n={},e={}){this._forEachChild((r,o)=>{r.reset(n?n[o]:null,F(g({},e),{onlySelf:!0}))}),this._updatePristine(e,this),this._updateTouched(e,this),this.updateValueAndValidity(e),e?.emitEvent!==!1&&this._events.next(new pa(this))}getRawValue(){return this._reduceChildren({},(n,e,r)=>(n[r]=e.getRawValue(),n))}_syncPendingControls(){let n=this._reduceChildren(!1,(e,r)=>r._syncPendingControls()?!0:e);return n&&this.updateValueAndValidity({onlySelf:!0}),n}_forEachChild(n){Object.keys(this.controls).forEach(e=>{let r=this.controls[e];r&&n(r,e)})}_setUpControls(){this._forEachChild(n=>{n.setParent(this),n._registerOnCollectionChange(this._onCollectionChange)})}_updateValue(){this.value=this._reduceValue()}_anyControls(n){for(let[e,r]of Object.entries(this.controls))if(this.contains(e)&&n(r))return!0;return!1}_reduceValue(){let n={};return this._reduceChildren(n,(e,r,o)=>((r.enabled||this.disabled)&&(e[o]=r.value),e))}_reduceChildren(n,e){let r=n;return this._forEachChild((o,i)=>{r=e(r,o,i)}),r}_allControlsDisabled(){for(let n of Object.keys(this.controls))if(this.controls[n].enabled)return!1;return Object.keys(this.controls).length>0||this.disabled}_find(n){return this.controls.hasOwnProperty(n)?this.controls[n]:null}};var HQ=ho;var Ag=class extends ho{};var yi=new y("",{factory:()=>qu}),qu="always";function Yu(t,n){return[...n.path,t]}function ma(t,n,e=qu){jg(t,n),n.valueAccessor.writeValue(t.value),(t.disabled||e==="always")&&n.valueAccessor.setDisabledState?.(t.disabled),nF(t,n),oF(t,n),rF(t,n),tF(t,n)}function Hu(t,n,e=!0){let r=()=>{};n?.valueAccessor?.registerOnChange(r),n?.valueAccessor?.registerOnTouched(r),zu(t,n),t&&(n._invokeOnDestroyCallbacks(),t._registerOnCollectionChange(()=>{}))}function $u(t,n){t.forEach(e=>{e.registerOnValidatorChange&&e.registerOnValidatorChange(n)})}function tF(t,n){if(n.valueAccessor.setDisabledState){let e=r=>{n.valueAccessor.setDisabledState(r)};t.registerOnDisabledChange(e),n._registerOnDestroy(()=>{t._unregisterOnDisabledChange(e)})}}function jg(t,n){let e=tI(t);n.validator!==null?t.setValidators(FC(e,n.validator)):typeof e=="function"&&t.setValidators([e]);let r=nI(t);n.asyncValidator!==null?t.setAsyncValidators(FC(r,n.asyncValidator)):typeof r=="function"&&t.setAsyncValidators([r]);let o=()=>t.updateValueAndValidity();$u(n._rawValidators,o),$u(n._rawAsyncValidators,o)}function zu(t,n){let e=!1;if(t!==null){if(n.validator!==null){let o=tI(t);if(Array.isArray(o)&&o.length>0){let i=o.filter(s=>s!==n.validator);i.length!==o.length&&(e=!0,t.setValidators(i))}}if(n.asyncValidator!==null){let o=nI(t);if(Array.isArray(o)&&o.length>0){let i=o.filter(s=>s!==n.asyncValidator);i.length!==o.length&&(e=!0,t.setAsyncValidators(i))}}}let r=()=>{};return $u(n._rawValidators,r),$u(n._rawAsyncValidators,r),e}function nF(t,n){n.valueAccessor.registerOnChange(e=>{t._pendingValue=e,t._pendingChange=!0,t._pendingDirty=!0,t.updateOn==="change"&&iI(t,n)})}function rF(t,n){n.valueAccessor.registerOnTouched(()=>{t._pendingTouched=!0,t.updateOn==="blur"&&t._pendingChange&&iI(t,n),t.updateOn!=="submit"&&t.markAsTouched()})}function iI(t,n){t._pendingDirty&&t.markAsDirty(),t.setValue(t._pendingValue,{emitModelToViewChange:!1}),n.viewToModelUpdate(t._pendingValue),t._pendingChange=!1}function oF(t,n){let e=(r,o)=>{n.valueAccessor.writeValue(r),o&&n.viewToModelUpdate(r)};t.registerOnChange(e),n._registerOnDestroy(()=>{t._unregisterOnChange(e)})}function sI(t,n){t==null,jg(t,n)}function iF(t,n){return zu(t,n)}function Vg(t,n){if(!t.hasOwnProperty("model"))return!1;let e=t.model;return e.isFirstChange()?!0:!Object.is(n,e.currentValue)}function sF(t){return Object.getPrototypeOf(t.constructor)===Gu}function aI(t,n){t._syncPendingControls(),n.forEach(e=>{let r=e.control;r.updateOn==="submit"&&r._pendingChange&&(e.viewToModelUpdate(r._pendingValue),r._pendingChange=!1)})}function Bg(t,n){if(!n)return null;Array.isArray(n);let e,r,o;return n.forEach(i=>{i.constructor===zC?e=i:sF(i)?r=i:o=i}),o||r||e||null}function aF(t,n){let e=t.indexOf(n);e>-1&&t.splice(e,1)}var cF={provide:et,useExisting:be(()=>lF)},da=Promise.resolve(),lF=(()=>{class t extends et{callSetDisabledState;get submitted(){return q(this.submittedReactive)}_submitted=Kt(()=>this.submittedReactive());submittedReactive=W(!1);_directives=new Set;form;ngSubmit=new U;options;constructor(e,r,o){super(),this.callSetDisabledState=o,this.form=new ho({},kg(e),Fg(r))}ngAfterViewInit(){this._setUpdateStrategy()}get formDirective(){return this}get control(){return this.form}get path(){return[]}get controls(){return this.form.controls}addControl(e){da.then(()=>{let r=this._findContainer(e.path);e.control=r.registerControl(e.name,e.control),ma(e.control,e,this.callSetDisabledState),e.control.updateValueAndValidity({emitEvent:!1}),this._directives.add(e)})}getControl(e){return this.form.get(e.path)}removeControl(e){da.then(()=>{this._findContainer(e.path)?.removeControl(e.name),this._directives.delete(e)})}addFormGroup(e){da.then(()=>{let r=this._findContainer(e.path),o=new ho({});sI(o,e),r.registerControl(e.name,o),o.updateValueAndValidity({emitEvent:!1})})}removeFormGroup(e){da.then(()=>{this._findContainer(e.path)?.removeControl?.(e.name)})}getFormGroup(e){return this.form.get(e.path)}updateModel(e,r){da.then(()=>{this.form.get(e.path).setValue(r)})}setValue(e){this.control.setValue(e)}onSubmit(e){return this.submittedReactive.set(!0),aI(this.form,this._directives),this.ngSubmit.emit(e),this.form._events.next(new Uu(this.control)),e?.target?.method==="dialog"}onReset(){this.resetForm()}resetForm(e=void 0){this.form.reset(e),this.submittedReactive.set(!1)}_setUpdateStrategy(){this.options&&this.options.updateOn!=null&&(this.form._updateOn=this.options.updateOn)}_findContainer(e){return e.pop(),e.length?this.form.get(e):this.form}static \u0275fac=function(r){return new(r||t)(D(En,10),D(mo,10),D(yi,8))};static \u0275dir=M({type:t,selectors:[["form",3,"ngNoForm","",3,"formGroup","",3,"formArray",""],["ng-form"],["","ngForm",""]],hostBindings:function(r,o){r&1&&Zt("submit",function(s){return o.onSubmit(s)})("reset",function(){return o.onReset()})},inputs:{options:[0,"ngFormOptions","options"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],standalone:!1,features:[we([cF]),J]})}return t})();function jC(t,n){let e=t.indexOf(n);e>-1&&t.splice(e,1)}function VC(t){return typeof t=="object"&&t!==null&&Object.keys(t).length===2&&"value"in t&&"disabled"in t}var gi=class extends vi{defaultValue=null;_onChange=[];_pendingValue;_pendingChange=!1;constructor(n=null,e,r){super(Pg(e),Lg(r,e)),this._applyFormState(n),this._setUpdateStrategy(e),this._initObservables(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator}),Wu(e)&&(e.nonNullable||e.initialValueIsDefault)&&(VC(n)?this.defaultValue=n.value:this.defaultValue=n)}setValue(n,e={}){this.value=this._pendingValue=n,this._onChange.length&&e.emitModelToViewChange!==!1&&this._onChange.forEach(r=>r(this.value,e.emitViewToModelChange!==!1)),this.updateValueAndValidity(e)}patchValue(n,e={}){this.setValue(n,e)}reset(n=this.defaultValue,e={}){this._applyFormState(n),this.markAsPristine(e),this.markAsUntouched(e),this.setValue(this.value,e),e.overwriteDefaultValue&&(this.defaultValue=this.value),this._pendingChange=!1,e?.emitEvent!==!1&&this._events.next(new pa(this))}_updateValue(){}_anyControls(n){return!1}_allControlsDisabled(){return this.disabled}registerOnChange(n){this._onChange.push(n)}_unregisterOnChange(n){jC(this._onChange,n)}registerOnDisabledChange(n){this._onDisabledChange.push(n)}_unregisterOnDisabledChange(n){jC(this._onDisabledChange,n)}_forEachChild(n){}_syncPendingControls(){return this.updateOn==="submit"&&(this._pendingDirty&&this.markAsDirty(),this._pendingTouched&&this.markAsTouched(),this._pendingChange)?(this.setValue(this._pendingValue,{onlySelf:!0,emitModelToViewChange:!1}),!0):!1}_applyFormState(n){VC(n)?(this.value=this._pendingValue=n.value,n.disabled?this.disable({onlySelf:!0,emitEvent:!1}):this.enable({onlySelf:!0,emitEvent:!1})):this.value=this._pendingValue=n}},zQ=gi,uF=t=>t instanceof gi,dF=(()=>{class t extends et{_parent;ngOnInit(){this._checkParentType(),this.formDirective.addFormGroup(this)}ngOnDestroy(){this.formDirective?.removeFormGroup(this)}get control(){return this.formDirective.getFormGroup(this)}get path(){return Yu(this.name==null?this.name:this.name.toString(),this._parent)}get formDirective(){return this._parent?this._parent.formDirective:null}_checkParentType(){}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275dir=M({type:t,standalone:!1,features:[J]})}return t})();var fF={provide:Gn,useExisting:be(()=>hF)},BC=Promise.resolve(),hF=(()=>{class t extends Gn{_changeDetectorRef;callSetDisabledState;control=new gi;static ngAcceptInputType_isDisabled;_registered=!1;viewModel;name="";isDisabled;model;options;update=new U;constructor(e,r,o,i,s,a){super(),this._changeDetectorRef=s,this.callSetDisabledState=a,this._parent=e,this._setValidators(r),this._setAsyncValidators(o),this.valueAccessor=Bg(this,i)}ngOnChanges(e){if(this._checkForErrors(),!this._registered||"name"in e){if(this._registered&&(this._checkName(),this.formDirective)){let r=e.name.previousValue;this.formDirective.removeControl({name:r,path:this._getPath(r)})}this._setUpControl()}"isDisabled"in e&&this._updateDisabled(e),Vg(e,this.viewModel)&&(this._updateValue(this.model),this.viewModel=this.model)}ngOnDestroy(){this.formDirective?.removeControl(this)}get path(){return this._getPath(this.name)}get formDirective(){return this._parent?this._parent.formDirective:null}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}_setUpControl(){this._setUpdateStrategy(),this._isStandalone()?this._setUpStandalone():this.formDirective.addControl(this),this._registered=!0}_setUpdateStrategy(){this.options&&this.options.updateOn!=null&&(this.control._updateOn=this.options.updateOn)}_isStandalone(){return!this._parent||!!(this.options&&this.options.standalone)}_setUpStandalone(){ma(this.control,this,this.callSetDisabledState),this.control.updateValueAndValidity({emitEvent:!1})}_checkForErrors(){this._checkName()}_checkName(){this.options&&this.options.name&&(this.name=this.options.name),!this._isStandalone()&&this.name}_updateValue(e){BC.then(()=>{this.control.setValue(e,{emitViewToModelChange:!1}),this._changeDetectorRef?.markForCheck()})}_updateDisabled(e){let r=e.isDisabled.currentValue,o=r!==0&&ue(r);BC.then(()=>{o&&!this.control.disabled?this.control.disable():!o&&this.control.disabled&&this.control.enable(),this._changeDetectorRef?.markForCheck()})}_getPath(e){return this._parent?Yu(e,this._parent):[e]}static \u0275fac=function(r){return new(r||t)(D(et,9),D(En,10),D(mo,10),D(po,10),D(St,8),D(yi,8))};static \u0275dir=M({type:t,selectors:[["","ngModel","",3,"formControlName","",3,"formControl",""]],inputs:{name:"name",isDisabled:[0,"disabled","isDisabled"],model:[0,"ngModel","model"],options:[0,"ngModelOptions","options"]},outputs:{update:"ngModelChange"},exportAs:["ngModel"],standalone:!1,features:[we([fF]),J,Re]})}return t})();var GQ=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["form",3,"ngNoForm","",3,"ngNativeValidate",""]],hostAttrs:["novalidate",""],standalone:!1})}return t})(),pF={provide:po,useExisting:be(()=>mF),multi:!0},mF=(()=>{class t extends Gu{writeValue(e){let r=e??"";this.setProperty("value",r)}registerOnChange(e){this.onChange=r=>{e(r==""?null:parseFloat(r))}}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275dir=M({type:t,selectors:[["input","type","number","formControlName",""],["input","type","number","formControl",""],["input","type","number","ngModel",""]],hostBindings:function(r,o){r&1&&Zt("input",function(s){return o.onChange(s.target.value)})("blur",function(){return o.onTouched()})},standalone:!1,features:[we([pF]),J]})}return t})();var Rg=class extends vi{constructor(n,e,r){super(Pg(e),Lg(r,e)),this.controls=n,this._initObservables(),this._setUpdateStrategy(e),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!this.asyncValidator})}controls;at(n){return this.controls[this._adjustIndex(n)]}push(n,e={}){Array.isArray(n)?n.forEach(r=>{this.controls.push(r),this._registerControl(r)}):(this.controls.push(n),this._registerControl(n)),this.updateValueAndValidity({emitEvent:e.emitEvent}),this._onCollectionChange()}insert(n,e,r={}){this.controls.splice(n,0,e),this._registerControl(e),this.updateValueAndValidity({emitEvent:r.emitEvent})}removeAt(n,e={}){let r=this._adjustIndex(n);r<0&&(r=0),this.controls[r]&&this.controls[r]._registerOnCollectionChange(()=>{}),this.controls.splice(r,1),this.updateValueAndValidity({emitEvent:e.emitEvent})}setControl(n,e,r={}){let o=this._adjustIndex(n);o<0&&(o=0),this.controls[o]&&this.controls[o]._registerOnCollectionChange(()=>{}),this.controls.splice(o,1),e&&(this.controls.splice(o,0,e),this._registerControl(e)),this.updateValueAndValidity({emitEvent:r.emitEvent}),this._onCollectionChange()}get length(){return this.controls.length}setValue(n,e={}){oI(this,!1,n),n.forEach((r,o)=>{rI(this,!1,o),this.at(o).setValue(r,{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e)}patchValue(n,e={}){n!=null&&(n.forEach((r,o)=>{this.at(o)&&this.at(o).patchValue(r,{onlySelf:!0,emitEvent:e.emitEvent})}),this.updateValueAndValidity(e))}reset(n=[],e={}){this._forEachChild((r,o)=>{r.reset(n[o],F(g({},e),{onlySelf:!0}))}),this._updatePristine(e,this),this._updateTouched(e,this),this.updateValueAndValidity(e),e?.emitEvent!==!1&&this._events.next(new pa(this))}getRawValue(){return this.controls.map(n=>n.getRawValue())}clear(n={}){this.controls.length<1||(this._forEachChild(e=>e._registerOnCollectionChange(()=>{})),this.controls.splice(0),this.updateValueAndValidity({emitEvent:n.emitEvent}))}_adjustIndex(n){return n<0?n+this.length:n}_syncPendingControls(){let n=this.controls.reduce((e,r)=>r._syncPendingControls()?!0:e,!1);return n&&this.updateValueAndValidity({onlySelf:!0}),n}_forEachChild(n){this.controls.forEach((e,r)=>{n(e,r)})}_updateValue(){this.value=this.controls.filter(n=>n.enabled||this.disabled).map(n=>n.value)}_anyControls(n){return this.controls.some(e=>e.enabled&&n(e))}_setUpControls(){this._forEachChild(n=>this._registerControl(n))}_allControlsDisabled(){for(let n of this.controls)if(n.enabled)return!1;return this.controls.length>0||this.disabled}_registerControl(n){n.setParent(this),n._registerOnCollectionChange(this._onCollectionChange)}_find(n){return this.at(n)??null}};var cI=(()=>{class t extends et{callSetDisabledState;get submitted(){return q(this._submittedReactive)}set submitted(e){this._submittedReactive.set(e)}_submitted=Kt(()=>this._submittedReactive());_submittedReactive=W(!1);_oldForm;_onCollectionChange=()=>this._updateDomValue();directives=[];constructor(e,r,o){super(),this.callSetDisabledState=o,this._setValidators(e),this._setAsyncValidators(r)}ngOnChanges(e){this.onChanges(e)}ngOnDestroy(){this.onDestroy()}onChanges(e){this._checkFormPresent(),e.hasOwnProperty("form")&&(this._updateValidators(),this._updateDomValue(),this._updateRegistrations(),this._oldForm=this.form)}onDestroy(){this.form&&(zu(this.form,this),this.form._onCollectionChange===this._onCollectionChange&&this.form._registerOnCollectionChange(()=>{}))}get formDirective(){return this}get path(){return[]}addControl(e){let r=this.form.get(e.path);return ma(r,e,this.callSetDisabledState),r.updateValueAndValidity({emitEvent:!1}),this.directives.push(e),r}getControl(e){return this.form.get(e.path)}removeControl(e){Hu(e.control||null,e,!1),aF(this.directives,e)}addFormGroup(e){this._setUpFormContainer(e)}removeFormGroup(e){this._cleanUpFormContainer(e)}getFormGroup(e){return this.form.get(e.path)}getFormArray(e){return this.form.get(e.path)}addFormArray(e){this._setUpFormContainer(e)}removeFormArray(e){this._cleanUpFormContainer(e)}updateModel(e,r){this.form.get(e.path).setValue(r)}onReset(){this.resetForm()}resetForm(e=void 0,r={}){this.form.reset(e,r),this._submittedReactive.set(!1)}onSubmit(e){return this.submitted=!0,aI(this.form,this.directives),this.ngSubmit.emit(e),this.form._events.next(new Uu(this.control)),e?.target?.method==="dialog"}_updateDomValue(){this.directives.forEach(e=>{let r=e.control,o=this.form.get(e.path);r!==o&&(Hu(r||null,e),uF(o)&&(ma(o,e,this.callSetDisabledState),e.control=o))}),this.form._updateTreeValidity({emitEvent:!1})}_setUpFormContainer(e){let r=this.form.get(e.path);sI(r,e),r.updateValueAndValidity({emitEvent:!1})}_cleanUpFormContainer(e){let r=this.form?.get(e.path);r&&iF(r,e)&&r.updateValueAndValidity({emitEvent:!1})}_updateRegistrations(){this.form._registerOnCollectionChange(this._onCollectionChange),this._oldForm?._registerOnCollectionChange(()=>{})}_updateValidators(){jg(this.form,this),this._oldForm&&zu(this._oldForm,this)}_checkFormPresent(){this.form}static \u0275fac=function(r){return new(r||t)(D(En,10),D(mo,10),D(yi,8))};static \u0275dir=M({type:t,features:[J,Re]})}return t})();var Ug=new y(""),gF={provide:Gn,useExisting:be(()=>vF)},vF=(()=>{class t extends Gn{_ngModelWarningConfig;callSetDisabledState;viewModel;form;set isDisabled(e){}model;update=new U;static _ngModelWarningSentOnce=!1;_ngModelWarningSent=!1;constructor(e,r,o,i,s){super(),this._ngModelWarningConfig=i,this.callSetDisabledState=s,this._setValidators(e),this._setAsyncValidators(r),this.valueAccessor=Bg(this,o)}ngOnChanges(e){if(this._isControlChanged(e)){let r=e.form.previousValue;r&&Hu(r,this,!1),ma(this.form,this,this.callSetDisabledState),this.form.updateValueAndValidity({emitEvent:!1})}Vg(e,this.viewModel)&&(this.form.setValue(this.model),this.viewModel=this.model)}ngOnDestroy(){this.form&&Hu(this.form,this,!1)}get path(){return[]}get control(){return this.form}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}_isControlChanged(e){return e.hasOwnProperty("form")}static \u0275fac=function(r){return new(r||t)(D(En,10),D(mo,10),D(po,10),D(Ug,8),D(yi,8))};static \u0275dir=M({type:t,selectors:[["","formControl",""]],inputs:{form:[0,"formControl","form"],isDisabled:[0,"disabled","isDisabled"],model:[0,"ngModel","model"]},outputs:{update:"ngModelChange"},exportAs:["ngForm"],standalone:!1,features:[we([gF]),J,Re]})}return t})(),yF={provide:et,useExisting:be(()=>lI)},lI=(()=>{class t extends dF{name=null;constructor(e,r,o){super(),this._parent=e,this._setValidators(r),this._setAsyncValidators(o)}_checkParentType(){dI(this._parent)}static \u0275fac=function(r){return new(r||t)(D(et,13),D(En,10),D(mo,10))};static \u0275dir=M({type:t,selectors:[["","formGroupName",""]],inputs:{name:[0,"formGroupName","name"]},standalone:!1,features:[we([yF]),J]})}return t})(),bF={provide:et,useExisting:be(()=>uI)},uI=(()=>{class t extends et{_parent;name=null;constructor(e,r,o){super(),this._parent=e,this._setValidators(r),this._setAsyncValidators(o)}ngOnInit(){dI(this._parent),this.formDirective.addFormArray(this)}ngOnDestroy(){this.formDirective?.removeFormArray(this)}get control(){return this.formDirective.getFormArray(this)}get formDirective(){return this._parent?this._parent.formDirective:null}get path(){return Yu(this.name==null?this.name:this.name.toString(),this._parent)}static \u0275fac=function(r){return new(r||t)(D(et,13),D(En,10),D(mo,10))};static \u0275dir=M({type:t,selectors:[["","formArrayName",""]],inputs:{name:[0,"formArrayName","name"]},standalone:!1,features:[we([bF]),J]})}return t})();function dI(t){return!(t instanceof lI)&&!(t instanceof cI)&&!(t instanceof uI)}var _F={provide:Gn,useExisting:be(()=>DF)},DF=(()=>{class t extends Gn{_ngModelWarningConfig;_added=!1;viewModel;control;name=null;set isDisabled(e){}model;update=new U;static _ngModelWarningSentOnce=!1;_ngModelWarningSent=!1;constructor(e,r,o,i,s){super(),this._ngModelWarningConfig=s,this._parent=e,this._setValidators(r),this._setAsyncValidators(o),this.valueAccessor=Bg(this,i)}ngOnChanges(e){this._added||this._setUpControl(),Vg(e,this.viewModel)&&(this.viewModel=this.model,this.formDirective.updateModel(this,this.model))}ngOnDestroy(){this.formDirective?.removeControl(this)}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}get path(){return Yu(this.name==null?this.name:this.name.toString(),this._parent)}get formDirective(){return this._parent?this._parent.formDirective:null}_setUpControl(){this.control=this.formDirective.addControl(this),this._added=!0}static \u0275fac=function(r){return new(r||t)(D(et,13),D(En,10),D(mo,10),D(po,10),D(Ug,8))};static \u0275dir=M({type:t,selectors:[["","formControlName",""]],inputs:{name:[0,"formControlName","name"],isDisabled:[0,"disabled","isDisabled"],model:[0,"ngModel","model"]},outputs:{update:"ngModelChange"},standalone:!1,features:[we([_F]),J,Re]})}return t})();var EF={provide:et,useExisting:be(()=>wF)},wF=(()=>{class t extends cI{form=null;ngSubmit=new U;get control(){return this.form}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275dir=M({type:t,selectors:[["","formGroup",""]],hostBindings:function(r,o){r&1&&Zt("submit",function(s){return o.onSubmit(s)})("reset",function(){return o.onReset()})},inputs:{form:[0,"formGroup","form"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],standalone:!1,features:[we([EF]),J]})}return t})(),CF={provide:po,useExisting:be(()=>hI),multi:!0};function fI(t,n){return t==null?`${n}`:(n&&typeof n=="object"&&(n="Object"),`${t}: ${n}`.slice(0,50))}function IF(t){return t.split(":")[0]}var hI=(()=>{class t extends Gu{value;_optionMap=new Map;_idCounter=0;set compareWith(e){this._compareWith=e}_compareWith=Object.is;appRefInjector=f(Be).injector;destroyRef=f(Ae);cdr=f(St);_queuedWrite=!1;_writeValueAfterRender(){this._queuedWrite||this.appRefInjector.destroyed||(this._queuedWrite=!0,ht({write:()=>{this.destroyRef.destroyed||(this._queuedWrite=!1,this.writeValue(this.value))}},{injector:this.appRefInjector}))}writeValue(e){this.cdr.markForCheck(),this.value=e;let r=this._getOptionId(e),o=fI(r,e);this.setProperty("value",o)}registerOnChange(e){this.onChange=r=>{this.value=this._getOptionValue(r),e(this.value)}}_registerOption(){return(this._idCounter++).toString()}_getOptionId(e){for(let r of this._optionMap.keys())if(this._compareWith(this._optionMap.get(r),e))return r;return null}_getOptionValue(e){let r=IF(e);return this._optionMap.has(r)?this._optionMap.get(r):e}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275dir=M({type:t,selectors:[["select","formControlName","",3,"multiple",""],["select","formControl","",3,"multiple",""],["select","ngModel","",3,"multiple",""]],hostBindings:function(r,o){r&1&&Zt("change",function(s){return o.onChange(s.target.value)})("blur",function(){return o.onTouched()})},inputs:{compareWith:"compareWith"},standalone:!1,features:[we([CF]),J]})}return t})(),WQ=(()=>{class t{_element;_renderer;_select;id;constructor(e,r,o){this._element=e,this._renderer=r,this._select=o,this._select&&(this.id=this._select._registerOption())}set ngValue(e){this._select!=null&&(this._select._optionMap.set(this.id,e),this._setElementValue(fI(this.id,e)),this._select._writeValueAfterRender())}set value(e){this._setElementValue(e),this._select?._writeValueAfterRender()}_setElementValue(e){this._renderer.setProperty(this._element.nativeElement,"value",e)}ngOnDestroy(){this._select?._optionMap.delete(this.id),this._select?._writeValueAfterRender()}static \u0275fac=function(r){return new(r||t)(D(z),D(Oe),D(hI,9))};static \u0275dir=M({type:t,selectors:[["option"]],inputs:{ngValue:"ngValue",value:"value"},standalone:!1})}return t})(),SF={provide:po,useExisting:be(()=>pI),multi:!0};function UC(t,n){return t==null?`${n}`:(typeof n=="string"&&(n=`'${n}'`),n&&typeof n=="object"&&(n="Object"),`${t}: ${n}`.slice(0,50))}function MF(t){return t.split(":")[0]}var pI=(()=>{class t extends Gu{value;_optionMap=new Map;_idCounter=0;set compareWith(e){this._compareWith=e}_compareWith=Object.is;writeValue(e){this.value=e;let r;if(Array.isArray(e)){let o=e.map(i=>this._getOptionId(i));r=(i,s)=>{i._setSelected(o.indexOf(s)>-1)}}else r=o=>{o._setSelected(!1)};this._optionMap.forEach(r)}registerOnChange(e){this.onChange=r=>{let o=[],i=r.selectedOptions;if(i!==void 0){let s=i;for(let a=0;a{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275dir=M({type:t,selectors:[["select","multiple","","formControlName",""],["select","multiple","","formControl",""],["select","multiple","","ngModel",""]],hostBindings:function(r,o){r&1&&Zt("change",function(s){return o.onChange(s.target)})("blur",function(){return o.onTouched()})},inputs:{compareWith:"compareWith"},standalone:!1,features:[we([SF]),J]})}return t})(),qQ=(()=>{class t{_element;_renderer;_select;id;_value;constructor(e,r,o){this._element=e,this._renderer=r,this._select=o,this._select&&(this.id=this._select._registerOption(this))}set ngValue(e){this._select!=null&&(this._value=e,this._setElementValue(UC(this.id,e)),this._select.writeValue(this._select.value))}set value(e){this._select?(this._value=e,this._setElementValue(UC(this.id,e)),this._select.writeValue(this._select.value)):this._setElementValue(e)}_setElementValue(e){this._renderer.setProperty(this._element.nativeElement,"value",e)}_setSelected(e){this._renderer.setProperty(this._element.nativeElement,"selected",e)}ngOnDestroy(){this._select&&(this._select._optionMap.delete(this.id),this._select.writeValue(this._select.value))}static \u0275fac=function(r){return new(r||t)(D(z),D(Oe),D(pI,9))};static \u0275dir=M({type:t,selectors:[["option"]],inputs:{ngValue:"ngValue",value:"value"},standalone:!1})}return t})();function mI(t){return typeof t=="number"?t:parseFloat(t)}var Hg=(()=>{class t{_validator=Pu;_onChange;_enabled;ngOnChanges(e){if(this.inputName in e){let r=this.normalizeInput(e[this.inputName].currentValue);this._enabled=this.enabled(r),this._validator=this._enabled?this.createValidator(r):Pu,this._onChange?.()}}validate(e){return this._validator(e)}registerOnValidatorChange(e){this._onChange=e}enabled(e){return e!=null}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,features:[Re]})}return t})(),TF={provide:En,useExisting:be(()=>xF),multi:!0},xF=(()=>{class t extends Hg{max;inputName="max";normalizeInput=e=>mI(e);createValidator=e=>WC(e);static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275dir=M({type:t,selectors:[["input","type","number","max","","formControlName",""],["input","type","number","max","","formControl",""],["input","type","number","max","","ngModel",""]],hostVars:1,hostBindings:function(r,o){r&2&&Yt("max",o._enabled?o.max:null)},inputs:{max:"max"},standalone:!1,features:[we([TF]),J]})}return t})(),AF={provide:En,useExisting:be(()=>RF),multi:!0},RF=(()=>{class t extends Hg{min;inputName="min";normalizeInput=e=>mI(e);createValidator=e=>GC(e);static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275dir=M({type:t,selectors:[["input","type","number","min","","formControlName",""],["input","type","number","min","","formControl",""],["input","type","number","min","","ngModel",""]],hostVars:1,hostBindings:function(r,o){r&2&&Yt("min",o._enabled?o.min:null)},inputs:{min:"min"},standalone:!1,features:[we([AF]),J]})}return t})(),NF={provide:En,useExisting:be(()=>OF),multi:!0};var OF=(()=>{class t extends Hg{required;inputName="required";normalizeInput=ue;createValidator=e=>qC;enabled(e){return e}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275dir=M({type:t,selectors:[["","required","","formControlName","",3,"type","checkbox"],["","required","","formControl","",3,"type","checkbox"],["","required","","ngModel","",3,"type","checkbox"]],hostVars:1,hostBindings:function(r,o){r&2&&Yt("required",o._enabled?"":null)},inputs:{required:"required"},standalone:!1,features:[we([NF]),J]})}return t})();var gI=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({})}return t})();function HC(t){return!!t&&(t.asyncValidators!==void 0||t.validators!==void 0||t.updateOn!==void 0)}var kF=(()=>{class t{useNonNullable=!1;get nonNullable(){let e=new t;return e.useNonNullable=!0,e}group(e,r=null){let o=this._reduceControls(e),i={};return HC(r)?i=r:r!==null&&(i.validators=r.validator,i.asyncValidators=r.asyncValidator),new ho(o,i)}record(e,r=null){let o=this._reduceControls(e);return new Ag(o,r)}control(e,r,o){let i={};return this.useNonNullable?(HC(r)?i=r:(i.validators=r,i.asyncValidators=o),new gi(e,F(g({},i),{nonNullable:!0}))):new gi(e,r,o)}array(e,r,o){let i=e.map(s=>this._createControl(s));return new Rg(i,r,o)}_reduceControls(e){let r={};return Object.keys(e).forEach(o=>{r[o]=this._createControl(e[o])}),r}_createControl(e){if(e instanceof gi)return e;if(e instanceof vi)return e;if(Array.isArray(e)){let r=e[0],o=e.length>1?e[1]:null,i=e.length>2?e[2]:null;return this.control(r,o,i)}else return this.control(e)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var YQ=(()=>{class t extends kF{group(e,r=null){return super.group(e,r)}control(e,r,o){return super.control(e,r,o)}array(e,r,o){return super.array(e,r,o)}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),ZQ=(()=>{class t{static withConfig(e){return{ngModule:t,providers:[{provide:yi,useValue:e.callSetDisabledState??qu}]}}static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({imports:[gI]})}return t})(),KQ=(()=>{class t{static withConfig(e){return{ngModule:t,providers:[{provide:Ug,useValue:e.warnOnNgModelWithFormControl??"always"},{provide:yi,useValue:e.callSetDisabledState??qu}]}}static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({imports:[gI]})}return t})();var V="primary",Ta=Symbol("RouteTitle"),qg=class{params;constructor(n){this.params=n||{}}has(n){return Object.prototype.hasOwnProperty.call(this.params,n)}get(n){if(this.has(n)){let e=this.params[n];return Array.isArray(e)?e[0]:e}return null}getAll(n){if(this.has(n)){let e=this.params[n];return Array.isArray(e)?e:[e]}return[]}get keys(){return Object.keys(this.params)}};function vo(t){return new qg(t)}function $g(t,n,e){for(let r=0;rt.length||e.pathMatch==="full"&&(n.hasChildren()||r.lengtht.length||e.pathMatch==="full"&&n.hasChildren()&&e.path!=="**")return null;let a={};return!$g(i,t.slice(0,i.length),a)||!$g(s,t.slice(t.length-s.length),a)?null:{consumed:t,posParams:a}}function ed(t){return new Promise((n,e)=>{t.pipe(Sn()).subscribe({next:r=>n(r),error:r=>e(r)})})}function FF(t,n){if(t.length!==n.length)return!1;for(let e=0;er[i]===o)}else return t===n}function PF(t){return t.length>0?t[t.length-1]:null}function bo(t){return Ft(t)?t:jn(t)?se(Promise.resolve(t)):T(t)}function II(t){return Ft(t)?ed(t):Promise.resolve(t)}var LF={exact:MI,subset:TI},SI={exact:jF,subset:VF,ignored:()=>!0},cv={paths:"exact",fragment:"ignored",matrixParams:"ignored",queryParams:"exact"},_a={paths:"subset",fragment:"ignored",matrixParams:"ignored",queryParams:"subset"};function lv(t,n,e){let r=t instanceof tt?t:n.parseUrl(t);return Kt(()=>Zg(n.lastSuccessfulNavigation()?.finalUrl??new tt,r,g(g({},_a),e)))}function Zg(t,n,e){return LF[e.paths](t.root,n.root,e.matrixParams)&&SI[e.queryParams](t.queryParams,n.queryParams)&&!(e.fragment==="exact"&&t.fragment!==n.fragment)}function jF(t,n){return wn(t,n)}function MI(t,n,e){if(!go(t.segments,n.segments)||!Qu(t.segments,n.segments,e)||t.numberOfChildren!==n.numberOfChildren)return!1;for(let r in n.children)if(!t.children[r]||!MI(t.children[r],n.children[r],e))return!1;return!0}function VF(t,n){return Object.keys(n).length<=Object.keys(t).length&&Object.keys(n).every(e=>CI(t[e],n[e]))}function TI(t,n,e){return xI(t,n,n.segments,e)}function xI(t,n,e,r){if(t.segments.length>e.length){let o=t.segments.slice(0,e.length);return!(!go(o,e)||n.hasChildren()||!Qu(o,e,r))}else if(t.segments.length===e.length){if(!go(t.segments,e)||!Qu(t.segments,e,r))return!1;for(let o in n.children)if(!t.children[o]||!TI(t.children[o],n.children[o],r))return!1;return!0}else{let o=e.slice(0,t.segments.length),i=e.slice(t.segments.length);return!go(t.segments,o)||!Qu(t.segments,o,r)||!t.children[V]?!1:xI(t.children[V],n,i,r)}}function Qu(t,n,e){return n.every((r,o)=>SI[e](t[o].parameters,r.parameters))}var tt=class{root;queryParams;fragment;_queryParamMap;constructor(n=new te([],{}),e={},r=null){this.root=n,this.queryParams=e,this.fragment=r}get queryParamMap(){return this._queryParamMap??=vo(this.queryParams),this._queryParamMap}toString(){return HF.serialize(this)}},te=class{segments;children;parent=null;constructor(n,e){this.segments=n,this.children=e,Object.values(e).forEach(r=>r.parent=this)}hasChildren(){return this.numberOfChildren>0}get numberOfChildren(){return Object.keys(this.children).length}toString(){return Xu(this)}},vr=class{path;parameters;_parameterMap;constructor(n,e){this.path=n,this.parameters=e}get parameterMap(){return this._parameterMap??=vo(this.parameters),this._parameterMap}toString(){return RI(this)}};function BF(t,n){return go(t,n)&&t.every((e,r)=>wn(e.parameters,n[r].parameters))}function go(t,n){return t.length!==n.length?!1:t.every((e,r)=>e.path===n[r].path)}function UF(t,n){let e=[];return Object.entries(t.children).forEach(([r,o])=>{r===V&&(e=e.concat(n(o,r)))}),Object.entries(t.children).forEach(([r,o])=>{r!==V&&(e=e.concat(n(o,r)))}),e}var _r=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>new qn,providedIn:"root"})}return t})(),qn=class{parse(n){let e=new Qg(n);return new tt(e.parseRootSegment(),e.parseQueryParams(),e.parseFragment())}serialize(n){let e=`/${ga(n.root,!0)}`,r=GF(n.queryParams),o=typeof n.fragment=="string"?`#${$F(n.fragment)}`:"";return`${e}${r}${o}`}},HF=new qn;function Xu(t){return t.segments.map(n=>RI(n)).join("/")}function ga(t,n){if(!t.hasChildren())return Xu(t);if(n){let e=t.children[V]?ga(t.children[V],!1):"",r=[];return Object.entries(t.children).forEach(([o,i])=>{o!==V&&r.push(`${o}:${ga(i,!1)}`)}),r.length>0?`${e}(${r.join("//")})`:e}else{let e=UF(t,(r,o)=>o===V?[ga(t.children[V],!1)]:[`${o}:${ga(r,!1)}`]);return Object.keys(t.children).length===1&&t.children[V]!=null?`${Xu(t)}/${e[0]}`:`${Xu(t)}/(${e.join("//")})`}}function AI(t){return encodeURIComponent(t).replace(/%40/g,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",")}function Zu(t){return AI(t).replace(/%3B/gi,";")}function $F(t){return encodeURI(t)}function Kg(t){return AI(t).replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/%26/gi,"&")}function Ju(t){return decodeURIComponent(t)}function vI(t){return Ju(t.replace(/\+/g,"%20"))}function RI(t){return`${Kg(t.path)}${zF(t.parameters)}`}function zF(t){return Object.entries(t).map(([n,e])=>`;${Kg(n)}=${Kg(e)}`).join("")}function GF(t){let n=Object.entries(t).map(([e,r])=>Array.isArray(r)?r.map(o=>`${Zu(e)}=${Zu(o)}`).join("&"):`${Zu(e)}=${Zu(r)}`).filter(e=>e);return n.length?`?${n.join("&")}`:""}var WF=/^[^\/()?;#]+/;function zg(t){let n=t.match(WF);return n?n[0]:""}var qF=/^[^\/()?;=#]+/;function YF(t){let n=t.match(qF);return n?n[0]:""}var ZF=/^[^=?&#]+/;function KF(t){let n=t.match(ZF);return n?n[0]:""}var QF=/^[^&#]+/;function XF(t){let n=t.match(QF);return n?n[0]:""}var Qg=class{url;remaining;constructor(n){this.url=n,this.remaining=n}parseRootSegment(){for(;this.consumeOptional("/"););return this.remaining===""||this.peekStartsWith("?")||this.peekStartsWith("#")?new te([],{}):new te([],this.parseChildren())}parseQueryParams(){let n={};if(this.consumeOptional("?"))do this.parseQueryParam(n);while(this.consumeOptional("&"));return n}parseFragment(){return this.consumeOptional("#")?decodeURIComponent(this.remaining):null}parseChildren(n=0){if(n>50)throw new b(4010,!1);if(this.remaining==="")return{};this.consumeOptional("/");let e=[];for(this.peekStartsWith("(")||e.push(this.parseSegment());this.peekStartsWith("/")&&!this.peekStartsWith("//")&&!this.peekStartsWith("/(");)this.capture("/"),e.push(this.parseSegment());let r={};this.peekStartsWith("/(")&&(this.capture("/"),r=this.parseParens(!0,n));let o={};return this.peekStartsWith("(")&&(o=this.parseParens(!1,n)),(e.length>0||Object.keys(r).length>0)&&(o[V]=new te(e,r)),o}parseSegment(){let n=zg(this.remaining);if(n===""&&this.peekStartsWith(";"))throw new b(4009,!1);return this.capture(n),new vr(Ju(n),this.parseMatrixParams())}parseMatrixParams(){let n={};for(;this.consumeOptional(";");)this.parseParam(n);return n}parseParam(n){let e=YF(this.remaining);if(!e)return;this.capture(e);let r="";if(this.consumeOptional("=")){let o=zg(this.remaining);o&&(r=o,this.capture(r))}n[Ju(e)]=Ju(r)}parseQueryParam(n){let e=KF(this.remaining);if(!e)return;this.capture(e);let r="";if(this.consumeOptional("=")){let s=XF(this.remaining);s&&(r=s,this.capture(r))}let o=vI(e),i=vI(r);if(n.hasOwnProperty(o)){let s=n[o];Array.isArray(s)||(s=[s],n[o]=s),s.push(i)}else n[o]=i}parseParens(n,e){let r={};for(this.capture("(");!this.consumeOptional(")")&&this.remaining.length>0;){let o=zg(this.remaining),i=this.remaining[o.length];if(i!=="/"&&i!==")"&&i!==";")throw new b(4010,!1);let s;o.indexOf(":")>-1?(s=o.slice(0,o.indexOf(":")),this.capture(s),this.capture(":")):n&&(s=V);let a=this.parseChildren(e+1);r[s??V]=Object.keys(a).length===1&&a[V]?a[V]:new te([],a),this.consumeOptional("//")}return r}peekStartsWith(n){return this.remaining.startsWith(n)}consumeOptional(n){return this.peekStartsWith(n)?(this.remaining=this.remaining.substring(n.length),!0):!1}capture(n){if(!this.consumeOptional(n))throw new b(4011,!1)}};function NI(t){return t.segments.length>0?new te([],{[V]:t}):t}function OI(t){let n={};for(let[r,o]of Object.entries(t.children)){let i=OI(o);if(r===V&&i.segments.length===0&&i.hasChildren())for(let[s,a]of Object.entries(i.children))n[s]=a;else(i.segments.length>0||i.hasChildren())&&(n[r]=i)}let e=new te(t.segments,n);return JF(e)}function JF(t){if(t.numberOfChildren===1&&t.children[V]){let n=t.children[V];return new te(t.segments.concat(n.segments),n.children)}return t}function yr(t){return t instanceof tt}function kI(t,n,e=null,r=null,o=new qn){let i=FI(t);return PI(i,n,e,r,o)}function FI(t){let n;function e(i){let s={};for(let c of i.children){let l=e(c);s[c.outlet]=l}let a=new te(i.url,s);return i===t&&(n=a),a}let r=e(t.root),o=NI(r);return n??o}function PI(t,n,e,r,o){let i=t;for(;i.parent;)i=i.parent;if(n.length===0)return Gg(i,i,i,e,r,o);let s=eP(n);if(s.toRoot())return Gg(i,i,new te([],{}),e,r,o);let a=tP(s,i,t),c=a.processChildren?ya(a.segmentGroup,a.index,s.commands):jI(a.segmentGroup,a.index,s.commands);return Gg(i,a.segmentGroup,c,e,r,o)}function td(t){return typeof t=="object"&&t!=null&&!t.outlets&&!t.segmentPath}function Da(t){return typeof t=="object"&&t!=null&&t.outlets}function yI(t,n,e){t||="\u0275";let r=new tt;return r.queryParams={[t]:n},e.parse(e.serialize(r)).queryParams[t]}function Gg(t,n,e,r,o,i){let s={};for(let[l,u]of Object.entries(r??{}))s[l]=Array.isArray(u)?u.map(d=>yI(l,d,i)):yI(l,u,i);let a;t===n?a=e:a=LI(t,n,e);let c=NI(OI(a));return new tt(c,s,o)}function LI(t,n,e){let r={};return Object.entries(t.children).forEach(([o,i])=>{i===n?r[o]=e:r[o]=LI(i,n,e)}),new te(t.segments,r)}var nd=class{isAbsolute;numberOfDoubleDots;commands;constructor(n,e,r){if(this.isAbsolute=n,this.numberOfDoubleDots=e,this.commands=r,n&&r.length>0&&td(r[0]))throw new b(4003,!1);let o=r.find(Da);if(o&&o!==PF(r))throw new b(4004,!1)}toRoot(){return this.isAbsolute&&this.commands.length===1&&this.commands[0]=="/"}};function eP(t){if(typeof t[0]=="string"&&t.length===1&&t[0]==="/")return new nd(!0,0,t);let n=0,e=!1,r=t.reduce((o,i,s)=>{if(typeof i=="object"&&i!=null){if(i.outlets){let a={};return Object.entries(i.outlets).forEach(([c,l])=>{a[c]=typeof l=="string"?l.split("/"):l}),[...o,{outlets:a}]}if(i.segmentPath)return[...o,i.segmentPath]}return typeof i!="string"?[...o,i]:s===0?(i.split("/").forEach((a,c)=>{c==0&&a==="."||(c==0&&a===""?e=!0:a===".."?n++:a!=""&&o.push(a))}),o):[...o,i]},[]);return new nd(e,n,r)}var _i=class{segmentGroup;processChildren;index;constructor(n,e,r){this.segmentGroup=n,this.processChildren=e,this.index=r}};function tP(t,n,e){if(t.isAbsolute)return new _i(n,!0,0);if(!e)return new _i(n,!1,NaN);if(e.parent===null)return new _i(e,!0,0);let r=td(t.commands[0])?0:1,o=e.segments.length-1+r;return nP(e,o,t.numberOfDoubleDots)}function nP(t,n,e){let r=t,o=n,i=e;for(;i>o;){if(i-=o,r=r.parent,!r)throw new b(4005,!1);o=r.segments.length}return new _i(r,!1,o-i)}function rP(t){return Da(t[0])?t[0].outlets:{[V]:t}}function jI(t,n,e){if(t??=new te([],{}),t.segments.length===0&&t.hasChildren())return ya(t,n,e);let r=oP(t,n,e),o=e.slice(r.commandIndex);if(r.match&&r.pathIndexi!==V)&&t.children[V]&&t.numberOfChildren===1&&t.children[V].segments.length===0){let i=ya(t.children[V],n,e);return new te(t.segments,i.children)}return Object.entries(r).forEach(([i,s])=>{typeof s=="string"&&(s=[s]),s!==null&&(o[i]=jI(t.children[i],n,s))}),Object.entries(t.children).forEach(([i,s])=>{r[i]===void 0&&(o[i]=s)}),new te(t.segments,o)}}function oP(t,n,e){let r=0,o=n,i={match:!1,pathIndex:0,commandIndex:0};for(;o=e.length)return i;let s=t.segments[o],a=e[r];if(Da(a))break;let c=`${a}`,l=r0&&c===void 0)break;if(c&&l&&typeof l=="object"&&l.outlets===void 0){if(!_I(c,l,s))return i;r+=2}else{if(!_I(c,{},s))return i;r++}o++}return{match:!0,pathIndex:o,commandIndex:r}}function Xg(t,n,e){let r=t.segments.slice(0,n),o=0;for(;o{typeof r=="string"&&(r=[r]),r!==null&&(n[e]=Xg(new te([],{}),0,r))}),n}function bI(t){let n={};return Object.entries(t).forEach(([e,r])=>n[e]=`${r}`),n}function _I(t,n,e){return t==e.path&&wn(n,e.parameters)}var Di="imperative",Fe=(function(t){return t[t.NavigationStart=0]="NavigationStart",t[t.NavigationEnd=1]="NavigationEnd",t[t.NavigationCancel=2]="NavigationCancel",t[t.NavigationError=3]="NavigationError",t[t.RoutesRecognized=4]="RoutesRecognized",t[t.ResolveStart=5]="ResolveStart",t[t.ResolveEnd=6]="ResolveEnd",t[t.GuardsCheckStart=7]="GuardsCheckStart",t[t.GuardsCheckEnd=8]="GuardsCheckEnd",t[t.RouteConfigLoadStart=9]="RouteConfigLoadStart",t[t.RouteConfigLoadEnd=10]="RouteConfigLoadEnd",t[t.ChildActivationStart=11]="ChildActivationStart",t[t.ChildActivationEnd=12]="ChildActivationEnd",t[t.ActivationStart=13]="ActivationStart",t[t.ActivationEnd=14]="ActivationEnd",t[t.Scroll=15]="Scroll",t[t.NavigationSkipped=16]="NavigationSkipped",t})(Fe||{}),vt=class{id;url;constructor(n,e){this.id=n,this.url=e}},br=class extends vt{type=Fe.NavigationStart;navigationTrigger;restoredState;constructor(n,e,r="imperative",o=null){super(n,e),this.navigationTrigger=r,this.restoredState=o}toString(){return`NavigationStart(id: ${this.id}, url: '${this.url}')`}},yt=class extends vt{urlAfterRedirects;type=Fe.NavigationEnd;constructor(n,e,r){super(n,e),this.urlAfterRedirects=r}toString(){return`NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`}},Ze=(function(t){return t[t.Redirect=0]="Redirect",t[t.SupersededByNewNavigation=1]="SupersededByNewNavigation",t[t.NoDataFromResolver=2]="NoDataFromResolver",t[t.GuardRejected=3]="GuardRejected",t[t.Aborted=4]="Aborted",t})(Ze||{}),wi=(function(t){return t[t.IgnoredSameUrlNavigation=0]="IgnoredSameUrlNavigation",t[t.IgnoredByUrlHandlingStrategy=1]="IgnoredByUrlHandlingStrategy",t})(wi||{}),Rt=class extends vt{reason;code;type=Fe.NavigationCancel;constructor(n,e,r,o){super(n,e),this.reason=r,this.code=o}toString(){return`NavigationCancel(id: ${this.id}, url: '${this.url}')`}};function VI(t){return t instanceof Rt&&(t.code===Ze.Redirect||t.code===Ze.SupersededByNewNavigation)}var Cn=class extends vt{reason;code;type=Fe.NavigationSkipped;constructor(n,e,r,o){super(n,e),this.reason=r,this.code=o}},yo=class extends vt{error;target;type=Fe.NavigationError;constructor(n,e,r,o){super(n,e),this.error=r,this.target=o}toString(){return`NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`}},Ea=class extends vt{urlAfterRedirects;state;type=Fe.RoutesRecognized;constructor(n,e,r,o){super(n,e),this.urlAfterRedirects=r,this.state=o}toString(){return`RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},rd=class extends vt{urlAfterRedirects;state;type=Fe.GuardsCheckStart;constructor(n,e,r,o){super(n,e),this.urlAfterRedirects=r,this.state=o}toString(){return`GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},od=class extends vt{urlAfterRedirects;state;shouldActivate;type=Fe.GuardsCheckEnd;constructor(n,e,r,o,i){super(n,e),this.urlAfterRedirects=r,this.state=o,this.shouldActivate=i}toString(){return`GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`}},id=class extends vt{urlAfterRedirects;state;type=Fe.ResolveStart;constructor(n,e,r,o){super(n,e),this.urlAfterRedirects=r,this.state=o}toString(){return`ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},sd=class extends vt{urlAfterRedirects;state;type=Fe.ResolveEnd;constructor(n,e,r,o){super(n,e),this.urlAfterRedirects=r,this.state=o}toString(){return`ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}},ad=class{route;type=Fe.RouteConfigLoadStart;constructor(n){this.route=n}toString(){return`RouteConfigLoadStart(path: ${this.route.path})`}},cd=class{route;type=Fe.RouteConfigLoadEnd;constructor(n){this.route=n}toString(){return`RouteConfigLoadEnd(path: ${this.route.path})`}},ld=class{snapshot;type=Fe.ChildActivationStart;constructor(n){this.snapshot=n}toString(){return`ChildActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},ud=class{snapshot;type=Fe.ChildActivationEnd;constructor(n){this.snapshot=n}toString(){return`ChildActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},dd=class{snapshot;type=Fe.ActivationStart;constructor(n){this.snapshot=n}toString(){return`ActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},fd=class{snapshot;type=Fe.ActivationEnd;constructor(n){this.snapshot=n}toString(){return`ActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}},Ci=class{routerEvent;position;anchor;scrollBehavior;type=Fe.Scroll;constructor(n,e,r,o){this.routerEvent=n,this.position=e,this.anchor=r,this.scrollBehavior=o}toString(){let n=this.position?`${this.position[0]}, ${this.position[1]}`:null;return`Scroll(anchor: '${this.anchor}', position: '${n}')`}},Ii=class{},wa=class{},Si=class{url;navigationBehaviorOptions;constructor(n,e){this.url=n,this.navigationBehaviorOptions=e}};function sP(t){return!(t instanceof Ii)&&!(t instanceof Si)&&!(t instanceof wa)}var hd=class{rootInjector;outlet=null;route=null;children;attachRef=null;get injector(){return this.route?.snapshot._environmentInjector??this.rootInjector}constructor(n){this.rootInjector=n,this.children=new _o(this.rootInjector)}},_o=(()=>{class t{rootInjector;contexts=new Map;constructor(e){this.rootInjector=e}onChildOutletCreated(e,r){let o=this.getOrCreateContext(e);o.outlet=r,this.contexts.set(e,o)}onChildOutletDestroyed(e){let r=this.getContext(e);r&&(r.outlet=null,r.attachRef=null)}onOutletDeactivated(){let e=this.contexts;return this.contexts=new Map,e}onOutletReAttached(e){this.contexts=e}getOrCreateContext(e){let r=this.getContext(e);return r||(r=new hd(this.rootInjector),this.contexts.set(e,r)),r}getContext(e){return this.contexts.get(e)||null}static \u0275fac=function(r){return new(r||t)(w(re))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),pd=class{_root;constructor(n){this._root=n}get root(){return this._root.value}parent(n){let e=this.pathFromRoot(n);return e.length>1?e[e.length-2]:null}children(n){let e=Jg(n,this._root);return e?e.children.map(r=>r.value):[]}firstChild(n){let e=Jg(n,this._root);return e&&e.children.length>0?e.children[0].value:null}siblings(n){let e=ev(n,this._root);return e.length<2?[]:e[e.length-2].children.map(o=>o.value).filter(o=>o!==n)}pathFromRoot(n){return ev(n,this._root).map(e=>e.value)}};function Jg(t,n){if(t===n.value)return n;for(let e of n.children){let r=Jg(t,e);if(r)return r}return null}function ev(t,n){if(t===n.value)return[n];for(let e of n.children){let r=ev(t,e);if(r.length)return r.unshift(n),r}return[]}var gt=class{value;children;constructor(n,e){this.value=n,this.children=e}toString(){return`TreeNode(${this.value})`}};function bi(t){let n={};return t&&t.children.forEach(e=>n[e.value.outlet]=e),n}var Ca=class extends pd{snapshot;constructor(n,e){super(n),this.snapshot=e,dv(this,n)}toString(){return this.snapshot.toString()}};function BI(t,n){let e=aP(t,n),r=new Ie([new vr("",{})]),o=new Ie({}),i=new Ie({}),s=new Ie({}),a=new Ie(""),c=new Yn(r,o,s,a,i,V,t,e.root);return c.snapshot=e.root,new Ca(new gt(c,[]),e)}function aP(t,n){let e={},r={},o={},s=new Mi([],e,o,"",r,V,t,null,{},n);return new Ia("",new gt(s,[]))}var Yn=class{urlSubject;paramsSubject;queryParamsSubject;fragmentSubject;dataSubject;outlet;component;snapshot;_futureSnapshot;_routerState;_paramMap;_queryParamMap;title;url;params;queryParams;fragment;data;constructor(n,e,r,o,i,s,a,c){this.urlSubject=n,this.paramsSubject=e,this.queryParamsSubject=r,this.fragmentSubject=o,this.dataSubject=i,this.outlet=s,this.component=a,this._futureSnapshot=c,this.title=this.dataSubject?.pipe(H(l=>l[Ta]))??T(void 0),this.url=n,this.params=e,this.queryParams=r,this.fragment=o,this.data=i}get routeConfig(){return this._futureSnapshot.routeConfig}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=this.params.pipe(H(n=>vo(n))),this._paramMap}get queryParamMap(){return this._queryParamMap??=this.queryParams.pipe(H(n=>vo(n))),this._queryParamMap}toString(){return this.snapshot?this.snapshot.toString():`Future(${this._futureSnapshot})`}};function uv(t,n,e="emptyOnly"){let r,{routeConfig:o}=t;return n!==null&&(e==="always"||o?.path===""||!n.component&&!n.routeConfig?.loadComponent)?r={params:g(g({},n.params),t.params),data:g(g({},n.data),t.data),resolve:g(g(g(g({},t.data),n.data),o?.data),t._resolvedData)}:r={params:g({},t.params),data:g({},t.data),resolve:g(g({},t.data),t._resolvedData??{})},o&&HI(o)&&(r.resolve[Ta]=o.title),r}var Mi=class{url;params;queryParams;fragment;data;outlet;component;routeConfig;_resolve;_resolvedData;_routerState;_paramMap;_queryParamMap;_environmentInjector;get title(){return this.data?.[Ta]}constructor(n,e,r,o,i,s,a,c,l,u){this.url=n,this.params=e,this.queryParams=r,this.fragment=o,this.data=i,this.outlet=s,this.component=a,this.routeConfig=c,this._resolve=l,this._environmentInjector=u}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap??=vo(this.params),this._paramMap}get queryParamMap(){return this._queryParamMap??=vo(this.queryParams),this._queryParamMap}toString(){let n=this.url.map(r=>r.toString()).join("/"),e=this.routeConfig?this.routeConfig.path:"";return`Route(url:'${n}', path:'${e}')`}},Ia=class extends pd{url;constructor(n,e){super(e),this.url=n,dv(this,e)}toString(){return UI(this._root)}};function dv(t,n){n.value._routerState=t,n.children.forEach(e=>dv(t,e))}function UI(t){let n=t.children.length>0?` { ${t.children.map(UI).join(", ")} } `:"";return`${t.value}${n}`}function Wg(t){if(t.snapshot){let n=t.snapshot,e=t._futureSnapshot;t.snapshot=e,wn(n.queryParams,e.queryParams)||t.queryParamsSubject.next(e.queryParams),n.fragment!==e.fragment&&t.fragmentSubject.next(e.fragment),wn(n.params,e.params)||t.paramsSubject.next(e.params),FF(n.url,e.url)||t.urlSubject.next(e.url),wn(n.data,e.data)||t.dataSubject.next(e.data)}else t.snapshot=t._futureSnapshot,t.dataSubject.next(t._futureSnapshot.data)}function tv(t,n){let e=wn(t.params,n.params)&&BF(t.url,n.url),r=!t.parent!=!n.parent;return e&&!r&&(!t.parent||tv(t.parent,n.parent))}function HI(t){return typeof t.title=="string"||t.title===null}var $I=new y(""),fv=(()=>{class t{activated=null;get activatedComponentRef(){return this.activated}_activatedRoute=null;name=V;activateEvents=new U;deactivateEvents=new U;attachEvents=new U;detachEvents=new U;routerOutletData=RE();parentContexts=f(_o);location=f(qe);changeDetector=f(St);inputBinder=f(xa,{optional:!0});supportsBindingToComponentInputs=!0;ngOnChanges(e){if(e.name){let{firstChange:r,previousValue:o}=e.name;if(r)return;this.isTrackedInParentContexts(o)&&(this.deactivate(),this.parentContexts.onChildOutletDestroyed(o)),this.initializeOutletWithName()}}ngOnDestroy(){this.isTrackedInParentContexts(this.name)&&this.parentContexts.onChildOutletDestroyed(this.name),this.inputBinder?.unsubscribeFromRouteData(this)}isTrackedInParentContexts(e){return this.parentContexts.getContext(e)?.outlet===this}ngOnInit(){this.initializeOutletWithName()}initializeOutletWithName(){if(this.parentContexts.onChildOutletCreated(this.name,this),this.activated)return;let e=this.parentContexts.getContext(this.name);e?.route&&(e.attachRef?this.attach(e.attachRef,e.route):this.activateWith(e.route,e.injector))}get isActivated(){return!!this.activated}get component(){if(!this.activated)throw new b(4012,!1);return this.activated.instance}get activatedRoute(){if(!this.activated)throw new b(4012,!1);return this._activatedRoute}get activatedRouteData(){return this._activatedRoute?this._activatedRoute.snapshot.data:{}}detach(){if(!this.activated)throw new b(4012,!1);this.location.detach();let e=this.activated;return this.activated=null,this._activatedRoute=null,this.detachEvents.emit(e.instance),e}attach(e,r){this.activated=e,this._activatedRoute=r,this.location.insert(e.hostView),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.attachEvents.emit(e.instance)}deactivate(){if(this.activated){let e=this.component;this.activated.destroy(),this.activated=null,this._activatedRoute=null,this.deactivateEvents.emit(e)}}activateWith(e,r){if(this.isActivated)throw new b(4013,!1);this._activatedRoute=e;let o=this.location,s=e.snapshot.component,a=this.parentContexts.getOrCreateContext(this.name).children,c=new nv(e,a,o.injector,this.routerOutletData);this.activated=o.createComponent(s,{index:o.length,injector:c,environmentInjector:r}),this.changeDetector.markForCheck(),this.inputBinder?.bindActivatedRouteToOutletComponent(this),this.activateEvents.emit(this.activated.instance)}static \u0275fac=function(r){return new(r||t)};static \u0275dir=M({type:t,selectors:[["router-outlet"]],inputs:{name:"name",routerOutletData:[1,"routerOutletData"]},outputs:{activateEvents:"activate",deactivateEvents:"deactivate",attachEvents:"attach",detachEvents:"detach"},exportAs:["outlet"],features:[Re]})}return t})(),nv=class{route;childContexts;parent;outletData;constructor(n,e,r,o){this.route=n,this.childContexts=e,this.parent=r,this.outletData=o}get(n,e){return n===Yn?this.route:n===_o?this.childContexts:n===$I?this.outletData:this.parent.get(n,e)}},xa=new y(""),hv=(()=>{class t{outletDataSubscriptions=new Map;bindActivatedRouteToOutletComponent(e){this.unsubscribeFromRouteData(e),this.subscribeToRouteData(e)}unsubscribeFromRouteData(e){this.outletDataSubscriptions.get(e)?.unsubscribe(),this.outletDataSubscriptions.delete(e)}subscribeToRouteData(e){let{activatedRoute:r}=e,o=To([r.queryParams,r.params,r.data]).pipe(He(([i,s,a],c)=>(a=g(g(g({},i),s),a),c===0?T(a):Promise.resolve(a)))).subscribe(i=>{if(!e.isActivated||!e.activatedComponentRef||e.activatedRoute!==r||r.component===null){this.unsubscribeFromRouteData(e);return}let s=PE(r.component);if(!s){this.unsubscribeFromRouteData(e);return}for(let{templateName:a}of s.inputs)e.activatedComponentRef.setInput(a,i[a])});this.outletDataSubscriptions.set(e,o)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),pv=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275cmp=ke({type:t,selectors:[["ng-component"]],exportAs:["emptyRouterOutlet"],decls:1,vars:0,template:function(r,o){r&1&&Ol(0,"router-outlet")},dependencies:[fv],encapsulation:2})}return t})();function mv(t){let n=t.children&&t.children.map(mv),e=n?F(g({},t),{children:n}):g({},t);return!e.component&&!e.loadComponent&&(n||e.loadChildren)&&e.outlet&&e.outlet!==V&&(e.component=pv),e}function cP(t,n,e){let r=Sa(t,n._root,e?e._root:void 0);return new Ca(r,n)}function Sa(t,n,e){if(e&&t.shouldReuseRoute(n.value,e.value.snapshot)){let r=e.value;r._futureSnapshot=n.value;let o=lP(t,n,e);return new gt(r,o)}else{if(t.shouldAttach(n.value)){let i=t.retrieve(n.value);if(i!==null){let s=i.route;return s.value._futureSnapshot=n.value,s.children=n.children.map(a=>Sa(t,a)),s}}let r=uP(n.value),o=n.children.map(i=>Sa(t,i));return new gt(r,o)}}function lP(t,n,e){return n.children.map(r=>{for(let o of e.children)if(t.shouldReuseRoute(r.value,o.value.snapshot))return Sa(t,r,o);return Sa(t,r)})}function uP(t){return new Yn(new Ie(t.url),new Ie(t.params),new Ie(t.queryParams),new Ie(t.fragment),new Ie(t.data),t.outlet,t.component,t)}var Ti=class{redirectTo;navigationBehaviorOptions;constructor(n,e){this.redirectTo=n,this.navigationBehaviorOptions=e}},zI="ngNavigationCancelingError";function md(t,n){let{redirectTo:e,navigationBehaviorOptions:r}=yr(n)?{redirectTo:n,navigationBehaviorOptions:void 0}:n,o=GI(!1,Ze.Redirect);return o.url=e,o.navigationBehaviorOptions=r,o}function GI(t,n){let e=new Error(`NavigationCancelingError: ${t||""}`);return e[zI]=!0,e.cancellationCode=n,e}function dP(t){return WI(t)&&yr(t.url)}function WI(t){return!!t&&t[zI]}var rv=class{routeReuseStrategy;futureState;currState;forwardEvent;inputBindingEnabled;constructor(n,e,r,o,i){this.routeReuseStrategy=n,this.futureState=e,this.currState=r,this.forwardEvent=o,this.inputBindingEnabled=i}activate(n){let e=this.futureState._root,r=this.currState?this.currState._root:null;this.deactivateChildRoutes(e,r,n),Wg(this.futureState.root),this.activateChildRoutes(e,r,n)}deactivateChildRoutes(n,e,r){let o=bi(e);n.children.forEach(i=>{let s=i.value.outlet;this.deactivateRoutes(i,o[s],r),delete o[s]}),Object.values(o).forEach(i=>{this.deactivateRouteAndItsChildren(i,r)})}deactivateRoutes(n,e,r){let o=n.value,i=e?e.value:null;if(o===i)if(o.component){let s=r.getContext(o.outlet);s&&this.deactivateChildRoutes(n,e,s.children)}else this.deactivateChildRoutes(n,e,r);else i&&this.deactivateRouteAndItsChildren(e,r)}deactivateRouteAndItsChildren(n,e){n.value.component&&this.routeReuseStrategy.shouldDetach(n.value.snapshot)?this.detachAndStoreRouteSubtree(n,e):this.deactivateRouteAndOutlet(n,e)}detachAndStoreRouteSubtree(n,e){let r=e.getContext(n.value.outlet),o=r&&n.value.component?r.children:e,i=bi(n);for(let s of Object.values(i))this.deactivateRouteAndItsChildren(s,o);if(r&&r.outlet){let s=r.outlet.detach(),a=r.children.onOutletDeactivated();this.routeReuseStrategy.store(n.value.snapshot,{componentRef:s,route:n,contexts:a})}}deactivateRouteAndOutlet(n,e){let r=e.getContext(n.value.outlet),o=r&&n.value.component?r.children:e,i=bi(n);for(let s of Object.values(i))this.deactivateRouteAndItsChildren(s,o);r&&(r.outlet&&(r.outlet.deactivate(),r.children.onOutletDeactivated()),r.attachRef=null,r.route=null)}activateChildRoutes(n,e,r){let o=bi(e);n.children.forEach(i=>{this.activateRoutes(i,o[i.value.outlet],r),this.forwardEvent(new fd(i.value.snapshot))}),n.children.length&&this.forwardEvent(new ud(n.value.snapshot))}activateRoutes(n,e,r){let o=n.value,i=e?e.value:null;if(Wg(o),o===i)if(o.component){let s=r.getOrCreateContext(o.outlet);this.activateChildRoutes(n,e,s.children)}else this.activateChildRoutes(n,e,r);else if(o.component){let s=r.getOrCreateContext(o.outlet);if(this.routeReuseStrategy.shouldAttach(o.snapshot)){let a=this.routeReuseStrategy.retrieve(o.snapshot);this.routeReuseStrategy.store(o.snapshot,null),s.children.onOutletReAttached(a.contexts),s.attachRef=a.componentRef,s.route=a.route.value,s.outlet&&s.outlet.attach(a.componentRef,a.route.value),Wg(a.route.value),this.activateChildRoutes(n,null,s.children)}else s.attachRef=null,s.route=o,s.outlet&&s.outlet.activateWith(o,s.injector),this.activateChildRoutes(n,null,s.children)}else this.activateChildRoutes(n,null,r)}},gd=class{path;route;constructor(n){this.path=n,this.route=this.path[this.path.length-1]}},Ei=class{component;route;constructor(n,e){this.component=n,this.route=e}};function fP(t,n,e){let r=t._root,o=n?n._root:null;return va(r,o,e,[r.value])}function hP(t){let n=t.routeConfig?t.routeConfig.canActivateChild:null;return!n||n.length===0?null:{node:t,guards:n}}function Ai(t,n){let e=Symbol(),r=n.get(t,e);return r===e?typeof t=="function"&&!bf(t)?t:n.get(t):r}function va(t,n,e,r,o={canDeactivateChecks:[],canActivateChecks:[]}){let i=bi(n);return t.children.forEach(s=>{pP(s,i[s.value.outlet],e,r.concat([s.value]),o),delete i[s.value.outlet]}),Object.entries(i).forEach(([s,a])=>ba(a,e.getContext(s),o)),o}function pP(t,n,e,r,o={canDeactivateChecks:[],canActivateChecks:[]}){let i=t.value,s=n?n.value:null,a=e?e.getContext(t.value.outlet):null;if(s&&i.routeConfig===s.routeConfig){let c=mP(s,i,i.routeConfig.runGuardsAndResolvers);c?o.canActivateChecks.push(new gd(r)):(i.data=s.data,i._resolvedData=s._resolvedData),i.component?va(t,n,a?a.children:null,r,o):va(t,n,e,r,o),c&&a&&a.outlet&&a.outlet.isActivated&&o.canDeactivateChecks.push(new Ei(a.outlet.component,s))}else s&&ba(n,a,o),o.canActivateChecks.push(new gd(r)),i.component?va(t,null,a?a.children:null,r,o):va(t,null,e,r,o);return o}function mP(t,n,e){if(typeof e=="function")return xe(n._environmentInjector,()=>e(t,n));switch(e){case"pathParamsChange":return!go(t.url,n.url);case"pathParamsOrQueryParamsChange":return!go(t.url,n.url)||!wn(t.queryParams,n.queryParams);case"always":return!0;case"paramsOrQueryParamsChange":return!tv(t,n)||!wn(t.queryParams,n.queryParams);default:return!tv(t,n)}}function ba(t,n,e){let r=bi(t),o=t.value;Object.entries(r).forEach(([i,s])=>{o.component?n?ba(s,n.children.getContext(i),e):ba(s,null,e):ba(s,n,e)}),o.component?n&&n.outlet&&n.outlet.isActivated?e.canDeactivateChecks.push(new Ei(n.outlet.component,o)):e.canDeactivateChecks.push(new Ei(null,o)):e.canDeactivateChecks.push(new Ei(null,o))}function Aa(t){return typeof t=="function"}function gP(t){return typeof t=="boolean"}function vP(t){return t&&Aa(t.canLoad)}function yP(t){return t&&Aa(t.canActivate)}function bP(t){return t&&Aa(t.canActivateChild)}function _P(t){return t&&Aa(t.canDeactivate)}function DP(t){return t&&Aa(t.canMatch)}function qI(t){return t instanceof In||t?.name==="EmptyError"}var Ku=Symbol("INITIAL_VALUE");function xi(){return He(t=>To(t.map(n=>n.pipe(Ue(1),Nr(Ku)))).pipe(H(n=>{for(let e of n)if(e!==!0){if(e===Ku)return Ku;if(e===!1||EP(e))return e}return!0}),fe(n=>n!==Ku),Ue(1)))}function EP(t){return yr(t)||t instanceof Ti}function YI(t){return t.aborted?T(void 0).pipe(Ue(1)):new O(n=>{let e=()=>{n.next(),n.complete()};return t.addEventListener("abort",e),()=>t.removeEventListener("abort",e)})}function ZI(t){return at(YI(t))}function wP(t){return ve(n=>{let{targetSnapshot:e,currentSnapshot:r,guards:{canActivateChecks:o,canDeactivateChecks:i}}=n;return i.length===0&&o.length===0?T(F(g({},n),{guardsResult:!0})):CP(i,e,r).pipe(ve(s=>s&&gP(s)?IP(e,o,t):T(s)),H(s=>F(g({},n),{guardsResult:s})))})}function CP(t,n,e){return se(t).pipe(ve(r=>AP(r.component,r.route,e,n)),Sn(r=>r!==!0,!0))}function IP(t,n,e){return se(n).pipe(Xn(r=>on(MP(r.route.parent,e),SP(r.route,e),xP(t,r.path),TP(t,r.route))),Sn(r=>r!==!0,!0))}function SP(t,n){return t!==null&&n&&n(new dd(t)),T(!0)}function MP(t,n){return t!==null&&n&&n(new ld(t)),T(!0)}function TP(t,n){let e=n.routeConfig?n.routeConfig.canActivate:null;if(!e||e.length===0)return T(!0);let r=e.map(o=>Vi(()=>{let i=n._environmentInjector,s=Ai(o,i),a=yP(s)?s.canActivate(n,t):xe(i,()=>s(n,t));return bo(a).pipe(Sn())}));return T(r).pipe(xi())}function xP(t,n){let e=n[n.length-1],o=n.slice(0,n.length-1).reverse().map(i=>hP(i)).filter(i=>i!==null).map(i=>Vi(()=>{let s=i.guards.map(a=>{let c=i.node._environmentInjector,l=Ai(a,c),u=bP(l)?l.canActivateChild(e,t):xe(c,()=>l(e,t));return bo(u).pipe(Sn())});return T(s).pipe(xi())}));return T(o).pipe(xi())}function AP(t,n,e,r){let o=n&&n.routeConfig?n.routeConfig.canDeactivate:null;if(!o||o.length===0)return T(!0);let i=o.map(s=>{let a=n._environmentInjector,c=Ai(s,a),l=_P(c)?c.canDeactivate(t,n,e,r):xe(a,()=>c(t,n,e,r));return bo(l).pipe(Sn())});return T(i).pipe(xi())}function RP(t,n,e,r,o){let i=n.canLoad;if(i===void 0||i.length===0)return T(!0);let s=i.map(a=>{let c=Ai(a,t),l=vP(c)?c.canLoad(n,e):xe(t,()=>c(n,e)),u=bo(l);return o?u.pipe(ZI(o)):u});return T(s).pipe(xi(),KI(r))}function KI(t){return Nd(nt(n=>{if(typeof n!="boolean")throw md(t,n)}),H(n=>n===!0))}function NP(t,n,e,r,o,i){let s=n.canMatch;if(!s||s.length===0)return T(!0);let a=s.map(c=>{let l=Ai(c,t),u=DP(l)?l.canMatch(n,e,o):xe(t,()=>l(n,e,o));return bo(u).pipe(ZI(i))});return T(a).pipe(xi(),KI(r))}var Wn=class t extends Error{segmentGroup;constructor(n){super(),this.segmentGroup=n||null,Object.setPrototypeOf(this,t.prototype)}},Ma=class t extends Error{urlTree;constructor(n){super(),this.urlTree=n,Object.setPrototypeOf(this,t.prototype)}};function OP(t){throw new b(4e3,!1)}function kP(t){throw GI(!1,Ze.GuardRejected)}var ov=class{urlSerializer;urlTree;constructor(n,e){this.urlSerializer=n,this.urlTree=e}async lineralizeSegments(n,e){let r=[],o=e.root;for(;;){if(r=r.concat(o.segments),o.numberOfChildren===0)return r;if(o.numberOfChildren>1||!o.children[V])throw OP(`${n.redirectTo}`);o=o.children[V]}}async applyRedirectCommands(n,e,r,o,i){let s=await FP(e,o,i);if(s instanceof tt)throw new Ma(s);let a=this.applyRedirectCreateUrlTree(s,this.urlSerializer.parse(s),n,r);if(s[0]==="/")throw new Ma(a);return a}applyRedirectCreateUrlTree(n,e,r,o){let i=this.createSegmentGroup(n,e.root,r,o);return new tt(i,this.createQueryParams(e.queryParams,this.urlTree.queryParams),e.fragment)}createQueryParams(n,e){let r={};return Object.entries(n).forEach(([o,i])=>{if(typeof i=="string"&&i[0]===":"){let a=i.substring(1);r[o]=e[a]}else r[o]=i}),r}createSegmentGroup(n,e,r,o){let i=this.createSegments(n,e.segments,r,o),s={};return Object.entries(e.children).forEach(([a,c])=>{s[a]=this.createSegmentGroup(n,c,r,o)}),new te(i,s)}createSegments(n,e,r,o){return e.map(i=>i.path[0]===":"?this.findPosParam(n,i,o):this.findOrReturn(i,r))}findPosParam(n,e,r){let o=r[e.path.substring(1)];if(!o)throw new b(4001,!1);return o}findOrReturn(n,e){let r=0;for(let o of e){if(o.path===n.path)return e.splice(r),o;r++}return n}};function FP(t,n,e){if(typeof t=="string")return Promise.resolve(t);let r=t;return ed(bo(xe(e,()=>r(n))))}function PP(t,n){return t.providers&&!t._injector&&(t._injector=ni(t.providers,n,`Route: ${t.path}`)),t._injector??n}function tn(t){return t.outlet||V}function LP(t,n){let e=t.filter(r=>tn(r)===n);return e.push(...t.filter(r=>tn(r)!==n)),e}var iv={matched:!1,consumedSegments:[],remainingSegments:[],parameters:{},positionalParamSegments:{}};function QI(t){return{routeConfig:t.routeConfig,url:t.url,params:t.params,queryParams:t.queryParams,fragment:t.fragment,data:t.data,outlet:t.outlet,title:t.title,paramMap:t.paramMap,queryParamMap:t.queryParamMap}}function jP(t,n,e,r,o,i,s){let a=XI(t,n,e);if(!a.matched)return T(a);let c=QI(i(a));return r=PP(n,r),NP(r,n,e,o,c,s).pipe(H(l=>l===!0?a:g({},iv)))}function XI(t,n,e){if(n.path==="")return n.pathMatch==="full"&&(t.hasChildren()||e.length>0)?g({},iv):{matched:!0,consumedSegments:[],remainingSegments:e,parameters:{},positionalParamSegments:{}};let o=(n.matcher||wI)(e,t,n);if(!o)return g({},iv);let i={};Object.entries(o.posParams??{}).forEach(([a,c])=>{i[a]=c.path});let s=o.consumed.length>0?g(g({},i),o.consumed[o.consumed.length-1].parameters):i;return{matched:!0,consumedSegments:o.consumed,remainingSegments:e.slice(o.consumed.length),parameters:s,positionalParamSegments:o.posParams??{}}}function DI(t,n,e,r,o){return e.length>0&&UP(t,e,r,o)?{segmentGroup:new te(n,BP(r,new te(e,t.children))),slicedSegments:[]}:e.length===0&&HP(t,e,r)?{segmentGroup:new te(t.segments,VP(t,e,r,t.children)),slicedSegments:e}:{segmentGroup:new te(t.segments,t.children),slicedSegments:e}}function VP(t,n,e,r){let o={};for(let i of e)if(yd(t,n,i)&&!r[tn(i)]){let s=new te([],{});o[tn(i)]=s}return g(g({},r),o)}function BP(t,n){let e={};e[V]=n;for(let r of t)if(r.path===""&&tn(r)!==V){let o=new te([],{});e[tn(r)]=o}return e}function UP(t,n,e,r){return e.some(o=>!yd(t,n,o)||!(tn(o)!==V)?!1:!(r!==void 0&&tn(o)===r))}function HP(t,n,e){return e.some(r=>yd(t,n,r))}function yd(t,n,e){return(t.hasChildren()||n.length>0)&&e.pathMatch==="full"?!1:e.path===""}function $P(t,n,e){return n.length===0&&!t.children[e]}var sv=class{};async function zP(t,n,e,r,o,i,s="emptyOnly",a){return new av(t,n,e,r,o,s,i,a).recognize()}var GP=31,av=class{injector;configLoader;rootComponentType;config;urlTree;paramsInheritanceStrategy;urlSerializer;abortSignal;applyRedirects;absoluteRedirectCount=0;allowRedirects=!0;constructor(n,e,r,o,i,s,a,c){this.injector=n,this.configLoader=e,this.rootComponentType=r,this.config=o,this.urlTree=i,this.paramsInheritanceStrategy=s,this.urlSerializer=a,this.abortSignal=c,this.applyRedirects=new ov(this.urlSerializer,this.urlTree)}noMatchError(n){return new b(4002,`'${n.segmentGroup}'`)}async recognize(){let n=DI(this.urlTree.root,[],[],this.config).segmentGroup,{children:e,rootSnapshot:r}=await this.match(n),o=new gt(r,e),i=new Ia("",o),s=kI(r,[],this.urlTree.queryParams,this.urlTree.fragment);return s.queryParams=this.urlTree.queryParams,i.url=this.urlSerializer.serialize(s),{state:i,tree:s}}async match(n){let e=new Mi([],Object.freeze({}),Object.freeze(g({},this.urlTree.queryParams)),this.urlTree.fragment,Object.freeze({}),V,this.rootComponentType,null,{},this.injector);try{return{children:await this.processSegmentGroup(this.injector,this.config,n,V,e),rootSnapshot:e}}catch(r){if(r instanceof Ma)return this.urlTree=r.urlTree,this.match(r.urlTree.root);throw r instanceof Wn?this.noMatchError(r):r}}async processSegmentGroup(n,e,r,o,i){if(r.segments.length===0&&r.hasChildren())return this.processChildren(n,e,r,i);let s=await this.processSegment(n,e,r,r.segments,o,!0,i);return s instanceof gt?[s]:[]}async processChildren(n,e,r,o){let i=[];for(let c of Object.keys(r.children))c==="primary"?i.unshift(c):i.push(c);let s=[];for(let c of i){let l=r.children[c],u=LP(e,c),d=await this.processSegmentGroup(n,u,l,c,o);s.push(...d)}let a=JI(s);return WP(a),a}async processSegment(n,e,r,o,i,s,a){for(let c of e)try{return await this.processSegmentAgainstRoute(c._injector??n,e,c,r,o,i,s,a)}catch(l){if(l instanceof Wn||qI(l))continue;throw l}if($P(r,o,i))return new sv;throw new Wn(r)}async processSegmentAgainstRoute(n,e,r,o,i,s,a,c){if(tn(r)!==s&&(s===V||!yd(o,i,r)))throw new Wn(o);if(r.redirectTo===void 0)return this.matchSegmentAgainstRoute(n,o,r,i,s,c);if(this.allowRedirects&&a)return this.expandSegmentAgainstRouteUsingRedirect(n,o,e,r,i,s,c);throw new Wn(o)}async expandSegmentAgainstRouteUsingRedirect(n,e,r,o,i,s,a){let{matched:c,parameters:l,consumedSegments:u,positionalParamSegments:d,remainingSegments:h}=XI(e,o,i);if(!c)throw new Wn(e);typeof o.redirectTo=="string"&&o.redirectTo[0]==="/"&&(this.absoluteRedirectCount++,this.absoluteRedirectCount>GP&&(this.allowRedirects=!1));let p=this.createSnapshot(n,o,i,l,a);if(this.abortSignal.aborted)throw new Error(this.abortSignal.reason);let m=await this.applyRedirects.applyRedirectCommands(u,o.redirectTo,d,QI(p),n),_=await this.applyRedirects.lineralizeSegments(o,m);return this.processSegment(n,r,e,_.concat(h),s,!1,a)}createSnapshot(n,e,r,o,i){let s=new Mi(r,o,Object.freeze(g({},this.urlTree.queryParams)),this.urlTree.fragment,YP(e),tn(e),e.component??e._loadedComponent??null,e,ZP(e),n),a=uv(s,i,this.paramsInheritanceStrategy);return s.params=Object.freeze(a.params),s.data=Object.freeze(a.data),s}async matchSegmentAgainstRoute(n,e,r,o,i,s){if(this.abortSignal.aborted)throw new Error(this.abortSignal.reason);let a=Pe=>this.createSnapshot(n,r,Pe.consumedSegments,Pe.parameters,s),c=await ed(jP(e,r,o,n,this.urlSerializer,a,this.abortSignal));if(r.path==="**"&&(e.children={}),!c?.matched)throw new Wn(e);n=r._injector??n;let{routes:l}=await this.getChildConfig(n,r,o),u=r._loadedInjector??n,{parameters:d,consumedSegments:h,remainingSegments:p}=c,m=this.createSnapshot(n,r,h,d,s),{segmentGroup:_,slicedSegments:E}=DI(e,h,p,l,i);if(E.length===0&&_.hasChildren()){let Pe=await this.processChildren(u,l,_,m);return new gt(m,Pe)}if(l.length===0&&E.length===0)return new gt(m,[]);let I=tn(r)===i,ee=await this.processSegment(u,l,_,E,I?V:i,!0,m);return new gt(m,ee instanceof gt?[ee]:[])}async getChildConfig(n,e,r){if(e.children)return{routes:e.children,injector:n};if(e.loadChildren){if(e._loadedRoutes!==void 0){let i=e._loadedNgModuleFactory;return i&&!e._loadedInjector&&(e._loadedInjector=i.create(n).injector),{routes:e._loadedRoutes,injector:e._loadedInjector}}if(this.abortSignal.aborted)throw new Error(this.abortSignal.reason);if(await ed(RP(n,e,r,this.urlSerializer,this.abortSignal))){let i=await this.configLoader.loadChildren(n,e);return e._loadedRoutes=i.routes,e._loadedInjector=i.injector,e._loadedNgModuleFactory=i.factory,i}throw kP(e)}return{routes:[],injector:n}}};function WP(t){t.sort((n,e)=>n.value.outlet===V?-1:e.value.outlet===V?1:n.value.outlet.localeCompare(e.value.outlet))}function qP(t){let n=t.value.routeConfig;return n&&n.path===""}function JI(t){let n=[],e=new Set;for(let r of t){if(!qP(r)){n.push(r);continue}let o=n.find(i=>r.value.routeConfig===i.value.routeConfig);o!==void 0?(o.children.push(...r.children),e.add(o)):n.push(r)}for(let r of e){let o=JI(r.children);n.push(new gt(r.value,o))}return n.filter(r=>!e.has(r))}function YP(t){return t.data||{}}function ZP(t){return t.resolve||{}}function KP(t,n,e,r,o,i,s){return ve(async a=>{let{state:c,tree:l}=await zP(t,n,e,r,a.extractedUrl,o,i,s);return F(g({},a),{targetSnapshot:c,urlAfterRedirects:l})})}function QP(t){return ve(n=>{let{targetSnapshot:e,guards:{canActivateChecks:r}}=n;if(!r.length)return T(n);let o=new Set(r.map(a=>a.route)),i=new Set;for(let a of o)if(!i.has(a))for(let c of eS(a))i.add(c);let s=0;return se(i).pipe(Xn(a=>o.has(a)?XP(a,e,t):(a.data=uv(a,a.parent,t).resolve,T(void 0))),nt(()=>s++),ac(1),ve(a=>s===i.size?T(n):Se))})}function eS(t){let n=t.children.map(e=>eS(e)).flat();return[t,...n]}function XP(t,n,e){let r=t.routeConfig,o=t._resolve;return r?.title!==void 0&&!HI(r)&&(o[Ta]=r.title),Vi(()=>(t.data=uv(t,t.parent,e).resolve,JP(o,t,n).pipe(H(i=>(t._resolvedData=i,t.data=g(g({},t.data),i),null)))))}function JP(t,n,e){let r=Yg(t);if(r.length===0)return T({});let o={};return se(r).pipe(ve(i=>eL(t[i],n,e).pipe(Sn(),nt(s=>{if(s instanceof Ti)throw md(new qn,s);o[i]=s}))),ac(1),H(()=>o),sn(i=>qI(i)?Se:Tr(i)))}function eL(t,n,e){let r=n._environmentInjector,o=Ai(t,r),i=o.resolve?o.resolve(n,e):xe(r,()=>o(n,e));return bo(i)}function EI(t){return He(n=>{let e=t(n);return e?se(e).pipe(H(()=>n)):T(n)})}var gv=(()=>{class t{buildTitle(e){let r,o=e.root;for(;o!==void 0;)r=this.getResolvedTitleForRoute(o)??r,o=o.children.find(i=>i.outlet===V);return r}getResolvedTitleForRoute(e){return e.data[Ta]}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(tS),providedIn:"root"})}return t})(),tS=(()=>{class t extends gv{title;constructor(e){super(),this.title=e}updateTitle(e){let r=this.buildTitle(e);r!==void 0&&this.title.setTitle(r)}static \u0275fac=function(r){return new(r||t)(w(xw))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Dr=new y("",{factory:()=>({})}),Ri=new y(""),bd=(()=>{class t{componentLoaders=new WeakMap;childrenLoaders=new WeakMap;onLoadStartListener;onLoadEndListener;compiler=f(Xp);async loadComponent(e,r){if(this.componentLoaders.get(r))return this.componentLoaders.get(r);if(r._loadedComponent)return Promise.resolve(r._loadedComponent);this.onLoadStartListener&&this.onLoadStartListener(r);let o=(async()=>{try{let i=await II(xe(e,()=>r.loadComponent())),s=await oS(rS(i));return this.onLoadEndListener&&this.onLoadEndListener(r),r._loadedComponent=s,s}finally{this.componentLoaders.delete(r)}})();return this.componentLoaders.set(r,o),o}loadChildren(e,r){if(this.childrenLoaders.get(r))return this.childrenLoaders.get(r);if(r._loadedRoutes)return Promise.resolve({routes:r._loadedRoutes,injector:r._loadedInjector});this.onLoadStartListener&&this.onLoadStartListener(r);let o=(async()=>{try{let i=await nS(r,this.compiler,e,this.onLoadEndListener);return r._loadedRoutes=i.routes,r._loadedInjector=i.injector,r._loadedNgModuleFactory=i.factory,i}finally{this.childrenLoaders.delete(r)}})();return this.childrenLoaders.set(r,o),o}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();async function nS(t,n,e,r){let o=await II(xe(e,()=>t.loadChildren())),i=await oS(rS(o)),s;i instanceof Sl||Array.isArray(i)?s=i:s=await n.compileModuleAsync(i),r&&r(t);let a,c,l=!1,u;return Array.isArray(s)?(c=s,l=!0):(a=s.create(e).injector,u=s,c=a.get(Ri,[],{optional:!0,self:!0}).flat()),{routes:c.map(mv),injector:a,factory:u}}function tL(t){return t&&typeof t=="object"&&"default"in t}function rS(t){return tL(t)?t.default:t}async function oS(t){return t}var _d=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(nL),providedIn:"root"})}return t})(),nL=(()=>{class t{shouldProcessUrl(e){return!0}extract(e){return e}merge(e,r){return e}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),vv=new y(""),yv=new y("");function iS(t,n,e){let r=t.get(yv),o=t.get(L);if(!o.startViewTransition||r.skipNextTransition)return r.skipNextTransition=!1,new Promise(l=>setTimeout(l));let i,s=new Promise(l=>{i=l}),a=o.startViewTransition(()=>(i(),rL(t)));a.updateCallbackDone.catch(l=>{}),a.ready.catch(l=>{}),a.finished.catch(l=>{});let{onViewTransitionCreated:c}=r;return c&&xe(t,()=>c({transition:a,from:n,to:e})),s}function rL(t){return new Promise(n=>{ht({read:()=>setTimeout(n)},{injector:t})})}var oL=()=>{},bv=new y(""),Dd=(()=>{class t{currentNavigation=W(null,{equal:()=>!1});currentTransition=null;lastSuccessfulNavigation=W(null);events=new S;transitionAbortWithErrorSubject=new S;configLoader=f(bd);environmentInjector=f(re);destroyRef=f(Ae);urlSerializer=f(_r);rootContexts=f(_o);location=f(bn);inputBindingEnabled=f(xa,{optional:!0})!==null;titleStrategy=f(gv);options=f(Dr,{optional:!0})||{};paramsInheritanceStrategy=this.options.paramsInheritanceStrategy||"emptyOnly";urlHandlingStrategy=f(_d);createViewTransition=f(vv,{optional:!0});navigationErrorHandler=f(bv,{optional:!0});navigationId=0;get hasRequestedNavigation(){return this.navigationId!==0}transitions;afterPreactivation=()=>T(void 0);rootComponentType=null;destroyed=!1;constructor(){let e=o=>this.events.next(new ad(o)),r=o=>this.events.next(new cd(o));this.configLoader.onLoadEndListener=r,this.configLoader.onLoadStartListener=e,this.destroyRef.onDestroy(()=>{this.destroyed=!0})}complete(){this.transitions?.complete()}handleNavigationRequest(e){let r=++this.navigationId;q(()=>{this.transitions?.next(F(g({},e),{extractedUrl:this.urlHandlingStrategy.extract(e.rawUrl),targetSnapshot:null,targetRouterState:null,guards:{canActivateChecks:[],canDeactivateChecks:[]},guardsResult:null,id:r,routesRecognizeHandler:{},beforeActivateHandler:{}}))})}setupNavigations(e){return this.transitions=new Ie(null),this.transitions.pipe(fe(r=>r!==null),He(r=>{let o=!1,i=new AbortController,s=()=>!o&&this.currentTransition?.id===r.id;return T(r).pipe(He(a=>{if(this.navigationId>r.id)return this.cancelNavigationTransition(r,"",Ze.SupersededByNewNavigation),Se;this.currentTransition=r;let c=this.lastSuccessfulNavigation();this.currentNavigation.set({id:a.id,initialUrl:a.rawUrl,extractedUrl:a.extractedUrl,targetBrowserUrl:typeof a.extras.browserUrl=="string"?this.urlSerializer.parse(a.extras.browserUrl):a.extras.browserUrl,trigger:a.source,extras:a.extras,previousNavigation:c?F(g({},c),{previousNavigation:null}):null,abort:()=>i.abort(),routesRecognizeHandler:a.routesRecognizeHandler,beforeActivateHandler:a.beforeActivateHandler});let l=!e.navigated||this.isUpdatingInternalState()||this.isUpdatedBrowserUrl(),u=a.extras.onSameUrlNavigation??e.onSameUrlNavigation;if(!l&&u!=="reload")return this.events.next(new Cn(a.id,this.urlSerializer.serialize(a.rawUrl),"",wi.IgnoredSameUrlNavigation)),a.resolve(!1),Se;if(this.urlHandlingStrategy.shouldProcessUrl(a.rawUrl))return T(a).pipe(He(d=>(this.events.next(new br(d.id,this.urlSerializer.serialize(d.extractedUrl),d.source,d.restoredState)),d.id!==this.navigationId?Se:Promise.resolve(d))),KP(this.environmentInjector,this.configLoader,this.rootComponentType,e.config,this.urlSerializer,this.paramsInheritanceStrategy,i.signal),nt(d=>{r.targetSnapshot=d.targetSnapshot,r.urlAfterRedirects=d.urlAfterRedirects,this.currentNavigation.update(h=>(h.finalUrl=d.urlAfterRedirects,h)),this.events.next(new wa)}),He(d=>se(r.routesRecognizeHandler.deferredHandle??T(void 0)).pipe(H(()=>d))),nt(()=>{let d=new Ea(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);this.events.next(d)}));if(l&&this.urlHandlingStrategy.shouldProcessUrl(a.currentRawUrl)){let{id:d,extractedUrl:h,source:p,restoredState:m,extras:_}=a,E=new br(d,this.urlSerializer.serialize(h),p,m);this.events.next(E);let I=BI(this.rootComponentType,this.environmentInjector).snapshot;return this.currentTransition=r=F(g({},a),{targetSnapshot:I,urlAfterRedirects:h,extras:F(g({},_),{skipLocationChange:!1,replaceUrl:!1})}),this.currentNavigation.update(ee=>(ee.finalUrl=h,ee)),T(r)}else return this.events.next(new Cn(a.id,this.urlSerializer.serialize(a.extractedUrl),"",wi.IgnoredByUrlHandlingStrategy)),a.resolve(!1),Se}),H(a=>{let c=new rd(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);return this.events.next(c),this.currentTransition=r=F(g({},a),{guards:fP(a.targetSnapshot,a.currentSnapshot,this.rootContexts)}),r}),wP(a=>this.events.next(a)),He(a=>{if(r.guardsResult=a.guardsResult,a.guardsResult&&typeof a.guardsResult!="boolean")throw md(this.urlSerializer,a.guardsResult);let c=new od(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot,!!a.guardsResult);if(this.events.next(c),!s())return Se;if(!a.guardsResult)return this.cancelNavigationTransition(a,"",Ze.GuardRejected),Se;if(a.guards.canActivateChecks.length===0)return T(a);let l=new id(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);if(this.events.next(l),!s())return Se;let u=!1;return T(a).pipe(QP(this.paramsInheritanceStrategy),nt({next:()=>{u=!0;let d=new sd(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects),a.targetSnapshot);this.events.next(d)},complete:()=>{u||this.cancelNavigationTransition(a,"",Ze.NoDataFromResolver)}}))}),EI(a=>{let c=u=>{let d=[];if(u.routeConfig?._loadedComponent)u.component=u.routeConfig?._loadedComponent;else if(u.routeConfig?.loadComponent){let h=u._environmentInjector;d.push(this.configLoader.loadComponent(h,u.routeConfig).then(p=>{u.component=p}))}for(let h of u.children)d.push(...c(h));return d},l=c(a.targetSnapshot.root);return l.length===0?T(a):se(Promise.all(l).then(()=>a))}),EI(()=>this.afterPreactivation()),He(()=>{let{currentSnapshot:a,targetSnapshot:c}=r,l=this.createViewTransition?.(this.environmentInjector,a.root,c.root);return l?se(l).pipe(H(()=>r)):T(r)}),Ue(1),He(a=>{let c=cP(e.routeReuseStrategy,a.targetSnapshot,a.currentRouterState);this.currentTransition=r=a=F(g({},a),{targetRouterState:c}),this.currentNavigation.update(u=>(u.targetRouterState=c,u)),this.events.next(new Ii);let l=r.beforeActivateHandler.deferredHandle;return l?se(l.then(()=>a)):T(a)}),nt(a=>{new rv(e.routeReuseStrategy,r.targetRouterState,r.currentRouterState,c=>this.events.next(c),this.inputBindingEnabled).activate(this.rootContexts),s()&&(o=!0,this.currentNavigation.update(c=>(c.abort=oL,c)),this.lastSuccessfulNavigation.set(q(this.currentNavigation)),this.events.next(new yt(a.id,this.urlSerializer.serialize(a.extractedUrl),this.urlSerializer.serialize(a.urlAfterRedirects))),this.titleStrategy?.updateTitle(a.targetRouterState.snapshot),a.resolve(!0))}),at(YI(i.signal).pipe(fe(()=>!o&&!r.targetRouterState),nt(()=>{this.cancelNavigationTransition(r,i.signal.reason+"",Ze.Aborted)}))),nt({complete:()=>{o=!0}}),at(this.transitionAbortWithErrorSubject.pipe(nt(a=>{throw a}))),Rr(()=>{i.abort(),o||this.cancelNavigationTransition(r,"",Ze.SupersededByNewNavigation),this.currentTransition?.id===r.id&&(this.currentNavigation.set(null),this.currentTransition=null)}),sn(a=>{if(o=!0,this.destroyed)return r.resolve(!1),Se;if(WI(a))this.events.next(new Rt(r.id,this.urlSerializer.serialize(r.extractedUrl),a.message,a.cancellationCode)),dP(a)?this.events.next(new Si(a.url,a.navigationBehaviorOptions)):r.resolve(!1);else{let c=new yo(r.id,this.urlSerializer.serialize(r.extractedUrl),a,r.targetSnapshot??void 0);try{let l=xe(this.environmentInjector,()=>this.navigationErrorHandler?.(c));if(l instanceof Ti){let{message:u,cancellationCode:d}=md(this.urlSerializer,l);this.events.next(new Rt(r.id,this.urlSerializer.serialize(r.extractedUrl),u,d)),this.events.next(new Si(l.redirectTo,l.navigationBehaviorOptions))}else throw this.events.next(c),a}catch(l){this.options.resolveNavigationPromiseOnError?r.resolve(!1):r.reject(l)}}return Se}))}))}cancelNavigationTransition(e,r,o){let i=new Rt(e.id,this.urlSerializer.serialize(e.extractedUrl),r,o);this.events.next(i),e.resolve(!1)}isUpdatingInternalState(){return this.currentTransition?.extractedUrl.toString()!==this.currentTransition?.currentUrlTree.toString()}isUpdatedBrowserUrl(){let e=this.urlHandlingStrategy.extract(this.urlSerializer.parse(this.location.path(!0))),r=q(this.currentNavigation),o=r?.targetBrowserUrl??r?.extractedUrl;return e.toString()!==o?.toString()&&!r?.extras.skipLocationChange}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function iL(t){return t!==Di}var sS=new y("");var aS=(()=>{class t{static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(sL),providedIn:"root"})}return t})(),vd=class{shouldDetach(n){return!1}store(n,e){}shouldAttach(n){return!1}retrieve(n){return null}shouldReuseRoute(n,e){return n.routeConfig===e.routeConfig}shouldDestroyInjector(n){return!0}},sL=(()=>{class t extends vd{static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Ed=(()=>{class t{urlSerializer=f(_r);options=f(Dr,{optional:!0})||{};canceledNavigationResolution=this.options.canceledNavigationResolution||"replace";location=f(bn);urlHandlingStrategy=f(_d);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";currentUrlTree=new tt;getCurrentUrlTree(){return this.currentUrlTree}rawUrlTree=this.currentUrlTree;getRawUrlTree(){return this.rawUrlTree}createBrowserPath({finalUrl:e,initialUrl:r,targetBrowserUrl:o}){let i=e!==void 0?this.urlHandlingStrategy.merge(e,r):r,s=o??i;return s instanceof tt?this.urlSerializer.serialize(s):s}routerUrlState(e){return e?.targetBrowserUrl===void 0||e?.finalUrl===void 0?{}:{\u0275routerUrl:this.urlSerializer.serialize(e.finalUrl)}}commitTransition({targetRouterState:e,finalUrl:r,initialUrl:o}){r&&e?(this.currentUrlTree=r,this.rawUrlTree=this.urlHandlingStrategy.merge(r,o),this.routerState=e):this.rawUrlTree=o}routerState=BI(null,f(re));getRouterState(){return this.routerState}_stateMemento=this.createStateMemento();get stateMemento(){return this._stateMemento}updateStateMemento(){this._stateMemento=this.createStateMemento()}createStateMemento(){return{rawUrlTree:this.rawUrlTree,currentUrlTree:this.currentUrlTree,routerState:this.routerState}}restoredState(){return this.location.getState()}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:()=>f(aL),providedIn:"root"})}return t})(),aL=(()=>{class t extends Ed{currentPageId=0;lastSuccessfulId=-1;get browserPageId(){return this.canceledNavigationResolution!=="computed"?this.currentPageId:this.restoredState()?.\u0275routerPageId??this.currentPageId}registerNonRouterCurrentEntryChangeListener(e){return this.location.subscribe(r=>{r.type==="popstate"&&setTimeout(()=>{e(r.url,r.state,"popstate",{replaceUrl:!0})})})}handleRouterEvent(e,r){e instanceof br?this.updateStateMemento():e instanceof Cn?this.commitTransition(r):e instanceof Ea?this.urlUpdateStrategy==="eager"&&(r.extras.skipLocationChange||this.setBrowserUrl(this.createBrowserPath(r),r)):e instanceof Ii?(this.commitTransition(r),this.urlUpdateStrategy==="deferred"&&!r.extras.skipLocationChange&&this.setBrowserUrl(this.createBrowserPath(r),r)):e instanceof Rt&&!VI(e)?this.restoreHistory(r):e instanceof yo?this.restoreHistory(r,!0):e instanceof yt&&(this.lastSuccessfulId=e.id,this.currentPageId=this.browserPageId)}setBrowserUrl(e,r){let{extras:o,id:i}=r,{replaceUrl:s,state:a}=o;if(this.location.isCurrentPathEqualTo(e)||s){let c=this.browserPageId,l=g(g({},a),this.generateNgRouterState(i,c,r));this.location.replaceState(e,"",l)}else{let c=g(g({},a),this.generateNgRouterState(i,this.browserPageId+1,r));this.location.go(e,"",c)}}restoreHistory(e,r=!1){if(this.canceledNavigationResolution==="computed"){let o=this.browserPageId,i=this.currentPageId-o;i!==0?this.location.historyGo(i):this.getCurrentUrlTree()===e.finalUrl&&i===0&&(this.resetInternalState(e),this.resetUrlToCurrentUrlTree())}else this.canceledNavigationResolution==="replace"&&(r&&this.resetInternalState(e),this.resetUrlToCurrentUrlTree())}resetInternalState({finalUrl:e}){this.routerState=this.stateMemento.routerState,this.currentUrlTree=this.stateMemento.currentUrlTree,this.rawUrlTree=this.urlHandlingStrategy.merge(this.currentUrlTree,e??this.rawUrlTree)}resetUrlToCurrentUrlTree(){this.location.replaceState(this.urlSerializer.serialize(this.getRawUrlTree()),"",this.generateNgRouterState(this.lastSuccessfulId,this.currentPageId))}generateNgRouterState(e,r,o){return this.canceledNavigationResolution==="computed"?g({navigationId:e,\u0275routerPageId:r},this.routerUrlState(o)):g({navigationId:e},this.routerUrlState(o))}static \u0275fac=(()=>{let e;return function(o){return(e||(e=Ne(t)))(o||t)}})();static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function wd(t,n){t.events.pipe(fe(e=>e instanceof yt||e instanceof Rt||e instanceof yo||e instanceof Cn),H(e=>e instanceof yt||e instanceof Cn?0:(e instanceof Rt?e.code===Ze.Redirect||e.code===Ze.SupersededByNewNavigation:!1)?2:1),fe(e=>e!==2),Ue(1)).subscribe(()=>{n()})}var bt=(()=>{class t{get currentUrlTree(){return this.stateManager.getCurrentUrlTree()}get rawUrlTree(){return this.stateManager.getRawUrlTree()}disposed=!1;nonRouterCurrentEntryChangeSubscription;console=f(Ml);stateManager=f(Ed);options=f(Dr,{optional:!0})||{};pendingTasks=f(kn);urlUpdateStrategy=this.options.urlUpdateStrategy||"deferred";navigationTransitions=f(Dd);urlSerializer=f(_r);location=f(bn);urlHandlingStrategy=f(_d);injector=f(re);_events=new S;get events(){return this._events}get routerState(){return this.stateManager.getRouterState()}navigated=!1;routeReuseStrategy=f(aS);injectorCleanup=f(sS,{optional:!0});onSameUrlNavigation=this.options.onSameUrlNavigation||"ignore";config=f(Ri,{optional:!0})?.flat()??[];componentInputBindingEnabled=!!f(xa,{optional:!0});currentNavigation=this.navigationTransitions.currentNavigation.asReadonly();constructor(){this.resetConfig(this.config),this.navigationTransitions.setupNavigations(this).subscribe({error:e=>{}}),this.subscribeToNavigationEvents()}eventsSubscription=new G;subscribeToNavigationEvents(){let e=this.navigationTransitions.events.subscribe(r=>{try{let o=this.navigationTransitions.currentTransition,i=q(this.navigationTransitions.currentNavigation);if(o!==null&&i!==null){if(this.stateManager.handleRouterEvent(r,i),r instanceof Rt&&r.code!==Ze.Redirect&&r.code!==Ze.SupersededByNewNavigation)this.navigated=!0;else if(r instanceof yt)this.navigated=!0,this.injectorCleanup?.(this.routeReuseStrategy,this.routerState,this.config);else if(r instanceof Si){let s=r.navigationBehaviorOptions,a=this.urlHandlingStrategy.merge(r.url,o.currentRawUrl),c=g({scroll:o.extras.scroll,browserUrl:o.extras.browserUrl,info:o.extras.info,skipLocationChange:o.extras.skipLocationChange,replaceUrl:o.extras.replaceUrl||this.urlUpdateStrategy==="eager"||iL(o.source)},s);this.scheduleNavigation(a,Di,null,c,{resolve:o.resolve,reject:o.reject,promise:o.promise})}}sP(r)&&this._events.next(r)}catch(o){this.navigationTransitions.transitionAbortWithErrorSubject.next(o)}});this.eventsSubscription.add(e)}resetRootComponentType(e){this.routerState.root.component=e,this.navigationTransitions.rootComponentType=e}initialNavigation(){this.setUpLocationChangeListener(),this.navigationTransitions.hasRequestedNavigation||this.navigateToSyncWithBrowser(this.location.path(!0),Di,this.stateManager.restoredState(),{replaceUrl:!0})}setUpLocationChangeListener(){this.nonRouterCurrentEntryChangeSubscription??=this.stateManager.registerNonRouterCurrentEntryChangeListener((e,r,o,i)=>{this.navigateToSyncWithBrowser(e,o,r,i)})}navigateToSyncWithBrowser(e,r,o,i){let s=o?.navigationId?o:null,a=o?.\u0275routerUrl??e;if(o?.\u0275routerUrl&&(i=F(g({},i),{browserUrl:e})),o){let l=g({},o);delete l.navigationId,delete l.\u0275routerPageId,delete l.\u0275routerUrl,Object.keys(l).length!==0&&(i.state=l)}let c=this.parseUrl(a);this.scheduleNavigation(c,r,s,i).catch(l=>{this.disposed||this.injector.get(ut)(l)})}get url(){return this.serializeUrl(this.currentUrlTree)}getCurrentNavigation(){return q(this.navigationTransitions.currentNavigation)}get lastSuccessfulNavigation(){return this.navigationTransitions.lastSuccessfulNavigation}resetConfig(e){this.config=e.map(mv),this.navigated=!1}ngOnDestroy(){this.dispose()}dispose(){this._events.unsubscribe(),this.navigationTransitions.complete(),this.nonRouterCurrentEntryChangeSubscription?.unsubscribe(),this.nonRouterCurrentEntryChangeSubscription=void 0,this.disposed=!0,this.eventsSubscription.unsubscribe()}createUrlTree(e,r={}){let{relativeTo:o,queryParams:i,fragment:s,queryParamsHandling:a,preserveFragment:c}=r,l=c?this.currentUrlTree.fragment:s,u=null;switch(a??this.options.defaultQueryParamsHandling){case"merge":u=g(g({},this.currentUrlTree.queryParams),i);break;case"preserve":u=this.currentUrlTree.queryParams;break;default:u=i||null}u!==null&&(u=this.removeEmptyProps(u));let d;try{let h=o?o.snapshot:this.routerState.snapshot.root;d=FI(h)}catch{(typeof e[0]!="string"||e[0][0]!=="/")&&(e=[]),d=this.currentUrlTree.root}return PI(d,e,u,l??null,this.urlSerializer)}navigateByUrl(e,r={skipLocationChange:!1}){let o=yr(e)?e:this.parseUrl(e),i=this.urlHandlingStrategy.merge(o,this.rawUrlTree);return this.scheduleNavigation(i,Di,null,r)}navigate(e,r={skipLocationChange:!1}){return cL(e),this.navigateByUrl(this.createUrlTree(e,r),r)}serializeUrl(e){return this.urlSerializer.serialize(e)}parseUrl(e){try{return this.urlSerializer.parse(e)}catch{return this.console.warn(Dt(4018,!1)),this.urlSerializer.parse("/")}}isActive(e,r){let o;if(r===!0?o=g({},cv):r===!1?o=g({},_a):o=g(g({},_a),r),yr(e))return Zg(this.currentUrlTree,e,o);let i=this.parseUrl(e);return Zg(this.currentUrlTree,i,o)}removeEmptyProps(e){return Object.entries(e).reduce((r,[o,i])=>(i!=null&&(r[o]=i),r),{})}scheduleNavigation(e,r,o,i,s){if(this.disposed)return Promise.resolve(!1);let a,c,l;s?(a=s.resolve,c=s.reject,l=s.promise):l=new Promise((d,h)=>{a=d,c=h});let u=this.pendingTasks.add();return wd(this,()=>{queueMicrotask(()=>this.pendingTasks.remove(u))}),this.navigationTransitions.handleNavigationRequest({source:r,restoredState:o,currentUrlTree:this.currentUrlTree,currentRawUrl:this.currentUrlTree,rawUrl:e,extras:i,resolve:a,reject:c,promise:l,currentSnapshot:this.routerState.snapshot,currentRouterState:this.routerState}),l.catch(Promise.reject.bind(Promise))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();function cL(t){for(let n=0;n{class t{router=f(bt);stateManager=f(Ed);fragment=W("");queryParams=W({});path=W("");serializer=f(_r);constructor(){this.updateState(),this.router.events?.subscribe(e=>{e instanceof yt&&this.updateState()})}updateState(){let{fragment:e,root:r,queryParams:o}=this.stateManager.getCurrentUrlTree();this.fragment.set(e),this.queryParams.set(o),this.path.set(this.serializer.serialize(new tt(r)))}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),Cd=(()=>{class t{router;route;tabIndexAttribute;renderer;el;locationStrategy;hrefAttributeValue=f(new Vl("href"),{optional:!0});reactiveHref=em(()=>this.isAnchorElement?this.computeHref(this._urlTree()):this.hrefAttributeValue);get href(){return q(this.reactiveHref)}set href(e){this.reactiveHref.set(e)}set target(e){this._target.set(e)}get target(){return q(this._target)}_target=W(void 0);set queryParams(e){this._queryParams.set(e)}get queryParams(){return q(this._queryParams)}_queryParams=W(void 0,{equal:()=>!1});set fragment(e){this._fragment.set(e)}get fragment(){return q(this._fragment)}_fragment=W(void 0);set queryParamsHandling(e){this._queryParamsHandling.set(e)}get queryParamsHandling(){return q(this._queryParamsHandling)}_queryParamsHandling=W(void 0);set state(e){this._state.set(e)}get state(){return q(this._state)}_state=W(void 0,{equal:()=>!1});set info(e){this._info.set(e)}get info(){return q(this._info)}_info=W(void 0,{equal:()=>!1});set relativeTo(e){this._relativeTo.set(e)}get relativeTo(){return q(this._relativeTo)}_relativeTo=W(void 0);set preserveFragment(e){this._preserveFragment.set(e)}get preserveFragment(){return q(this._preserveFragment)}_preserveFragment=W(!1);set skipLocationChange(e){this._skipLocationChange.set(e)}get skipLocationChange(){return q(this._skipLocationChange)}_skipLocationChange=W(!1);set replaceUrl(e){this._replaceUrl.set(e)}get replaceUrl(){return q(this._replaceUrl)}_replaceUrl=W(!1);isAnchorElement;onChanges=new S;applicationErrorHandler=f(ut);options=f(Dr,{optional:!0});reactiveRouterState=f(lL);constructor(e,r,o,i,s,a){this.router=e,this.route=r,this.tabIndexAttribute=o,this.renderer=i,this.el=s,this.locationStrategy=a;let c=s.nativeElement.tagName?.toLowerCase();this.isAnchorElement=c==="a"||c==="area"||!!(typeof customElements=="object"&&customElements.get(c)?.observedAttributes?.includes?.("href"))}setTabIndexIfNotOnNativeEl(e){this.tabIndexAttribute!=null||this.isAnchorElement||this.applyAttributeValue("tabindex",e)}ngOnChanges(e){this.onChanges.next(this)}routerLinkInput=W(null);set routerLink(e){e==null?(this.routerLinkInput.set(null),this.setTabIndexIfNotOnNativeEl(null)):(yr(e)?this.routerLinkInput.set(e):this.routerLinkInput.set(Array.isArray(e)?e:[e]),this.setTabIndexIfNotOnNativeEl("0"))}onClick(e,r,o,i,s){let a=this._urlTree();if(a===null||this.isAnchorElement&&(e!==0||r||o||i||s||typeof this.target=="string"&&this.target!="_self"))return!0;let c={skipLocationChange:this.skipLocationChange,replaceUrl:this.replaceUrl,state:this.state,info:this.info};return this.router.navigateByUrl(a,c)?.catch(l=>{this.applicationErrorHandler(l)}),!this.isAnchorElement}ngOnDestroy(){}applyAttributeValue(e,r){let o=this.renderer,i=this.el.nativeElement;r!==null?o.setAttribute(i,e,r):o.removeAttribute(i,e)}_urlTree=Kt(()=>{this.reactiveRouterState.path(),this._preserveFragment()&&this.reactiveRouterState.fragment();let e=o=>o==="preserve"||o==="merge";(e(this._queryParamsHandling())||e(this.options?.defaultQueryParamsHandling))&&this.reactiveRouterState.queryParams();let r=this.routerLinkInput();return r===null||!this.router.createUrlTree?null:yr(r)?r:this.router.createUrlTree(r,{relativeTo:this._relativeTo()!==void 0?this._relativeTo():this.route,queryParams:this._queryParams(),fragment:this._fragment(),queryParamsHandling:this._queryParamsHandling(),preserveFragment:this._preserveFragment()})},{equal:(e,r)=>this.computeHref(e)===this.computeHref(r)});get urlTree(){return q(this._urlTree)}computeHref(e){return e!==null&&this.locationStrategy?this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(e))??"":null}static \u0275fac=function(r){return new(r||t)(D(bt),D(Yn),Cs("tabindex"),D(Oe),D(z),D(Xt))};static \u0275dir=M({type:t,selectors:[["","routerLink",""]],hostVars:2,hostBindings:function(r,o){r&1&&Zt("click",function(s){return o.onClick(s.button,s.ctrlKey,s.shiftKey,s.altKey,s.metaKey)}),r&2&&Yt("href",o.reactiveHref(),hp)("target",o._target())},inputs:{target:"target",queryParams:"queryParams",fragment:"fragment",queryParamsHandling:"queryParamsHandling",state:"state",info:"info",relativeTo:"relativeTo",preserveFragment:[2,"preserveFragment","preserveFragment",ue],skipLocationChange:[2,"skipLocationChange","skipLocationChange",ue],replaceUrl:[2,"replaceUrl","replaceUrl",ue],routerLink:"routerLink"},features:[Re]})}return t})(),uL=(()=>{class t{router;element;renderer;cdr;links;classes=[];routerEventsSubscription;linkInputChangesSubscription;_isActive=!1;get isActive(){return this._isActive}routerLinkActiveOptions={exact:!1};ariaCurrentWhenActive;isActiveChange=new U;link=f(Cd,{optional:!0});constructor(e,r,o,i){this.router=e,this.element=r,this.renderer=o,this.cdr=i,this.routerEventsSubscription=e.events.subscribe(s=>{s instanceof yt&&this.update()})}ngAfterContentInit(){T(this.links.changes,T(null)).pipe(rn()).subscribe(e=>{this.update(),this.subscribeToEachLinkOnChanges()})}subscribeToEachLinkOnChanges(){this.linkInputChangesSubscription?.unsubscribe();let e=[...this.links.toArray(),this.link].filter(r=>!!r).map(r=>r.onChanges);this.linkInputChangesSubscription=se(e).pipe(rn()).subscribe(r=>{this._isActive!==this.isLinkActive(this.router)(r)&&this.update()})}set routerLinkActive(e){let r=Array.isArray(e)?e:e.split(" ");this.classes=r.filter(o=>!!o)}ngOnChanges(e){this.update()}ngOnDestroy(){this.routerEventsSubscription.unsubscribe(),this.linkInputChangesSubscription?.unsubscribe()}update(){!this.links||!this.router.navigated||queueMicrotask(()=>{let e=this.hasActiveLinks();this.classes.forEach(r=>{e?this.renderer.addClass(this.element.nativeElement,r):this.renderer.removeClass(this.element.nativeElement,r)}),e&&this.ariaCurrentWhenActive!==void 0?this.renderer.setAttribute(this.element.nativeElement,"aria-current",this.ariaCurrentWhenActive.toString()):this.renderer.removeAttribute(this.element.nativeElement,"aria-current"),this._isActive!==e&&(this._isActive=e,this.cdr.markForCheck(),this.isActiveChange.emit(e))})}isLinkActive(e){let r=dL(this.routerLinkActiveOptions)?this.routerLinkActiveOptions:this.routerLinkActiveOptions.exact??!1?g({},cv):g({},_a);return o=>{let i=o.urlTree;return i?q(lv(i,e,r)):!1}}hasActiveLinks(){let e=this.isLinkActive(this.router);return this.link&&e(this.link)||this.links.some(e)}static \u0275fac=function(r){return new(r||t)(D(bt),D(z),D(Oe),D(St))};static \u0275dir=M({type:t,selectors:[["","routerLinkActive",""]],contentQueries:function(r,o,i){if(r&1&&kl(i,Cd,5),r&2){let s;Fs(s=Ps())&&(o.links=s)}},inputs:{routerLinkActiveOptions:"routerLinkActiveOptions",ariaCurrentWhenActive:"ariaCurrentWhenActive",routerLinkActive:"routerLinkActive"},outputs:{isActiveChange:"isActiveChange"},exportAs:["routerLinkActive"],features:[Re]})}return t})();function dL(t){let n=t;return!!(n.paths||n.matrixParams||n.queryParams||n.fragment)}var Ra=class{};var cS=(()=>{class t{router;injector;preloadingStrategy;loader;subscription;constructor(e,r,o,i){this.router=e,this.injector=r,this.preloadingStrategy=o,this.loader=i}setUpPreloading(){this.subscription=this.router.events.pipe(fe(e=>e instanceof yt),Xn(()=>this.preload())).subscribe(()=>{})}preload(){return this.processRoutes(this.injector,this.router.config)}ngOnDestroy(){this.subscription?.unsubscribe()}processRoutes(e,r){let o=[];for(let i of r){i.providers&&!i._injector&&(i._injector=ni(i.providers,e,""));let s=i._injector??e;i._loadedNgModuleFactory&&!i._loadedInjector&&(i._loadedInjector=i._loadedNgModuleFactory.create(s).injector);let a=i._loadedInjector??s;(i.loadChildren&&!i._loadedRoutes&&i.canLoad===void 0||i.loadComponent&&!i._loadedComponent)&&o.push(this.preloadConfig(s,i)),(i.children||i._loadedRoutes)&&o.push(this.processRoutes(a,i.children??i._loadedRoutes))}return se(o).pipe(rn())}preloadConfig(e,r){return this.preloadingStrategy.preload(r,()=>{if(e.destroyed)return T(null);let o;r.loadChildren&&r.canLoad===void 0?o=se(this.loader.loadChildren(e,r)):o=T(null);let i=o.pipe(ve(s=>s===null?T(void 0):(r._loadedRoutes=s.routes,r._loadedInjector=s.injector,r._loadedNgModuleFactory=s.factory,this.processRoutes(s.injector??e,s.routes))));if(r.loadComponent&&!r._loadedComponent){let s=this.loader.loadComponent(e,r);return se([i,s]).pipe(rn())}else return i})}static \u0275fac=function(r){return new(r||t)(w(bt),w(re),w(Ra),w(bd))};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})(),lS=new y(""),fL=(()=>{class t{options;routerEventsSubscription;scrollEventsSubscription;lastId=0;lastSource=Di;restoredId=0;store={};isHydrating=f(sp,{optional:!0})??!1;urlSerializer=f(_r);zone=f(j);viewportScroller=f(Mm);transitions=f(Dd);constructor(e){this.options=e,this.options.scrollPositionRestoration||="disabled",this.options.anchorScrolling||="disabled",this.isHydrating&&f(Be).whenStable().then(()=>{this.isHydrating=!1})}init(){this.options.scrollPositionRestoration!=="disabled"&&this.viewportScroller.setHistoryScrollRestoration("manual"),this.routerEventsSubscription=this.createScrollEvents(),this.scrollEventsSubscription=this.consumeScrollEvents()}createScrollEvents(){return this.transitions.events.subscribe(e=>{e instanceof br?(this.store[this.lastId]=this.viewportScroller.getScrollPosition(),this.lastSource=e.navigationTrigger,this.restoredId=e.restoredState?e.restoredState.navigationId:0):e instanceof yt?(this.lastId=e.id,this.scheduleScrollEvent(e,this.urlSerializer.parse(e.urlAfterRedirects).fragment)):e instanceof Cn&&e.code===wi.IgnoredSameUrlNavigation&&(this.lastSource=void 0,this.restoredId=0,this.scheduleScrollEvent(e,this.urlSerializer.parse(e.url).fragment))})}consumeScrollEvents(){return this.transitions.events.subscribe(e=>{if(!(e instanceof Ci)||e.scrollBehavior==="manual")return;let r={behavior:"instant"};e.position?this.options.scrollPositionRestoration==="top"?this.viewportScroller.scrollToPosition([0,0],r):this.options.scrollPositionRestoration==="enabled"&&this.viewportScroller.scrollToPosition(e.position,r):e.anchor&&this.options.anchorScrolling==="enabled"?this.viewportScroller.scrollToAnchor(e.anchor):this.options.scrollPositionRestoration!=="disabled"&&this.viewportScroller.scrollToPosition([0,0])})}scheduleScrollEvent(e,r){if(this.isHydrating)return;let o=q(this.transitions.currentNavigation)?.extras.scroll;this.zone.runOutsideAngular(async()=>{await new Promise(i=>{setTimeout(i),typeof requestAnimationFrame<"u"&&requestAnimationFrame(i)}),this.zone.run(()=>{this.transitions.events.next(new Ci(e,this.lastSource==="popstate"?this.store[this.restoredId]:null,r,o))})})}ngOnDestroy(){this.routerEventsSubscription?.unsubscribe(),this.scrollEventsSubscription?.unsubscribe()}static \u0275fac=function(r){Rp()};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})();function hL(){return f(bt).routerState.root}function Na(t,n){return{\u0275kind:t,\u0275providers:n}}function pL(){let t=f($);return n=>{let e=t.get(Be);if(n!==e.components[0])return;let r=t.get(bt),o=t.get(uS);t.get(Dv)===1&&r.initialNavigation(),t.get(hS,null,{optional:!0})?.setUpPreloading(),t.get(lS,null,{optional:!0})?.init(),r.resetRootComponentType(e.componentTypes[0]),o.closed||(o.next(),o.complete(),o.unsubscribe())}}var uS=new y("",{factory:()=>new S}),Dv=new y("",{factory:()=>1});function dS(){let t=[{provide:fl,useValue:!0},{provide:Dv,useValue:0},Rl(()=>{let n=f($);return n.get(mm,Promise.resolve()).then(()=>new Promise(r=>{let o=n.get(bt),i=n.get(uS);wd(o,()=>{r(!0)}),n.get(Dd).afterPreactivation=()=>(r(!0),i.closed?T(void 0):i),o.initialNavigation()}))})];return Na(2,t)}function fS(){let t=[Rl(()=>{f(bt).setUpLocationChangeListener()}),{provide:Dv,useValue:2}];return Na(3,t)}var hS=new y("");function pS(t){return Na(0,[{provide:hS,useExisting:cS},{provide:Ra,useExisting:t}])}function mS(){return Na(8,[hv,{provide:xa,useExisting:hv}])}function gS(t){qt("NgRouterViewTransitions");let n=[{provide:vv,useValue:iS},{provide:yv,useValue:g({skipNextTransition:!!t?.skipInitialTransition},t)}];return Na(9,n)}var vS=[bn,{provide:_r,useClass:qn},bt,_o,{provide:Yn,useFactory:hL},bd,[]],mL=(()=>{class t{constructor(){}static forRoot(e,r){return{ngModule:t,providers:[vS,[],{provide:Ri,multi:!0,useValue:e},[],r?.errorHandler?{provide:bv,useValue:r.errorHandler}:[],{provide:Dr,useValue:r||{}},r?.useHash?vL():yL(),gL(),r?.preloadingStrategy?pS(r.preloadingStrategy).\u0275providers:[],r?.initialNavigation?bL(r):[],r?.bindToComponentInputs?mS().\u0275providers:[],r?.enableViewTransitions?gS().\u0275providers:[],_L()]}}static forChild(e){return{ngModule:t,providers:[{provide:Ri,multi:!0,useValue:e}]}}static \u0275fac=function(r){return new(r||t)};static \u0275mod=X({type:t});static \u0275inj=Z({})}return t})();function gL(){return{provide:lS,useFactory:()=>{let t=f(Mm),n=f(Dr);return n.scrollOffset&&t.setOffset(n.scrollOffset),new fL(n)}}}function vL(){return{provide:Xt,useClass:Em}}function yL(){return{provide:Xt,useClass:Wl}}function bL(t){return[t.initialNavigation==="disabled"?fS().\u0275providers:[],t.initialNavigation==="enabledBlocking"?dS().\u0275providers:[]]}var _v=new y("");function _L(){return[{provide:_v,useFactory:pL},{provide:Nl,multi:!0,useExisting:_v}]}var Id=class t extends Error{originalError;constructor(n){super(n)}static fromError(n,e){let r=new t(n);return r.originalError=e,r}},EL=(()=>{class t{handleError(e){let r=e;return e.name==="HttpErrorResponse"&&e.status===0?r=Id.fromError("Controller is unreachable",e):e.error?.message&&(r=Id.fromError(e.error.message,e)),Tr(()=>r)}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})(),tJ=(()=>{class t{http;errorHandler;router;requestsNotificationEmitter=new U;isRefreshing=!1;failedQueue=[];constructor(e,r,o){this.http=e,this.errorHandler=r,this.router=o}get(e,r,o){o=this.getJsonOptions(o);let i=this.getOptionsForController(e,r,o);return this.requestsNotificationEmitter.emit(`GET ${i.url}`),this.handleResponse(e,this.http.get(i.url,i.options),"GET",r,null,o)}getText(e,r,o){o=this.getTextOptions(o);let i=this.getOptionsForController(e,r,o);return this.requestsNotificationEmitter.emit(`GET ${i.url}`),this.handleResponse(e,this.http.get(i.url,i.options),"GET",r,null,o)}getBlob(e,r,o){o=this.getBlobOptions(o);let i=this.getOptionsForController(e,r,o);return this.requestsNotificationEmitter.emit(`GET ${i.url}`),this.handleResponse(e,this.http.get(i.url,i.options),"GET",r,null,o)}post(e,r,o,i){i=this.getJsonOptions(i);let s=this.getOptionsForController(e,r,i);return this.requestsNotificationEmitter.emit(`POST ${s.url}`),this.handleResponse(e,this.http.post(s.url,o,s.options),"POST",r,o,i)}postBlob(e,r,o){let i={responseType:"blob",headers:{}},s=this.getOptionsForController(e,r,i);return this.requestsNotificationEmitter.emit(`POST ${s.url}`),this.handleResponse(e,this.http.post(s.url,o,s.options),"POST",r,o,i)}put(e,r,o,i){i=this.getJsonOptions(i);let s=this.getOptionsForController(e,r,i);return this.requestsNotificationEmitter.emit(`PUT ${s.url}`),this.handleResponse(e,this.http.put(s.url,o,s.options),"PUT",r,o,i)}delete(e,r,o){o=this.getJsonOptions(o);let i=this.getOptionsForController(e,r,o);return this.requestsNotificationEmitter.emit(`DELETE ${i.url}`),this.handleResponse(e,this.http.delete(i.url,i.options),"DELETE",r,null,o)}patch(e,r,o,i){i=this.getJsonOptions(i);let s=this.getOptionsForController(e,r,i);return this.handleResponse(e,this.http.patch(s.url,o,s.options),"PATCH",r,o,i)}head(e,r,o){o=this.getJsonOptions(o);let i=this.getOptionsForController(e,r,o);return this.handleResponse(e,this.http.head(i.url,i.options),"HEAD",r,null,o)}options(e,r,o){o=this.getJsonOptions(o);let i=this.getOptionsForController(e,r,o);return this.handleResponse(e,this.http.options(i.url,i.options),"OPTIONS",r,null,o)}getJsonOptions(e){return e||{responseType:"json"}}getTextOptions(e){return e||{responseType:"text"}}getBlobOptions(e){return e||{responseType:"blob"}}getOptionsForController(e,r,o){return e&&e.host&&e.port?(e.protocol||(e.protocol=location.protocol),r=`${e.protocol}//${e.host}:${e.port}/${Iu.current_version}${r}`):r=`/${Iu.current_version}${r}`,o.headers||(o.headers={}),e&&e.authToken&&!e.tokenExpired&&(o.headers.Authorization=`Bearer ${e.authToken}`),{url:r,options:o}}handleResponse(e,r,o,i,s,a){return r.pipe(sn(c=>{if(c.status!==401)return this.errorHandler.handleError(c);if(i.endsWith("/access/users/login")||i.endsWith("/access/users/authenticate")||i.endsWith("/access/users/refresh"))return this.errorHandler.handleError(c);let l=localStorage.getItem(`refresh_token_${e.id}`);return l?this.retryAfterRefresh(e,o,i,s,a,l):(this.redirectToLogin(e),Tr(()=>c))}))}retryAfterRefresh(e,r,o,i,s,a){return this.isRefreshing?new O(c=>{this.failedQueue.push({resolve:()=>{this.executeRequest(e,r,o,i,s).subscribe({next:l=>{c.next(l),c.complete()},error:l=>c.error(l)})},reject:l=>c.error(l)})}):(this.isRefreshing=!0,this.doRefreshToken(e,a).pipe(He(c=>(e.authToken=c.access_token,localStorage.setItem(`controller-${e.id}`,JSON.stringify(e)),c.refresh_token&&localStorage.setItem(`refresh_token_${e.id}`,c.refresh_token),this.isRefreshing=!1,this.processQueue(null),this.executeRequest(e,r,o,i,s))),sn(c=>(this.isRefreshing=!1,this.processQueue(c),c.status===401&&this.redirectToLogin(e),Tr(()=>c)))))}doRefreshToken(e,r){let o=`${e.protocol}//${e.host}:${e.port}/${Iu.current_version}/access/users/refresh`;return this.http.post(o,{refresh_token:r},{headers:new _n({"Content-Type":"application/json"})})}executeRequest(e,r,o,i,s){let a=this.getOptionsForController(e,o,s);return this.requestsNotificationEmitter.emit(`${r} ${a.url}`),this.http.request(r,a.url,{body:i,headers:a.options.headers,params:a.options.params,responseType:a.options.responseType||"json"})}processQueue(e){e?this.failedQueue.forEach(r=>r.reject(e)):this.failedQueue.forEach(r=>r.resolve()),this.failedQueue=[]}clearTokens(e){localStorage.removeItem(`refresh_token_${e.id}`),e.authToken=null,localStorage.setItem(`controller-${e.id}`,JSON.stringify(e))}redirectToLogin(e){this.clearTokens(e),e.tokenExpired=!0,localStorage.setItem(`controller-${e.id}`,JSON.stringify(e)),this.isRefreshing=!1,this.processQueue(new Error("Session expired, redirecting to login")),this.router.navigate(["/controller",e.id,"login"])}static \u0275fac=function(r){return new(r||t)(w(fu),w(EL),w(bt))};static \u0275prov=v({token:t,factory:t.\u0275fac})}return t})();var Ev=class{_multiple;_emitChanges;compareWith;_selection=new Set;_deselectedToEmit=[];_selectedToEmit=[];_selected=null;get selected(){return this._selected||(this._selected=Array.from(this._selection.values())),this._selected}changed=new S;constructor(n=!1,e,r=!0,o){this._multiple=n,this._emitChanges=r,this.compareWith=o,e&&e.length&&(n?e.forEach(i=>this._markSelected(i)):this._markSelected(e[0]),this._selectedToEmit.length=0)}select(...n){this._verifyValueAssignment(n),n.forEach(r=>this._markSelected(r));let e=this._hasQueuedChanges();return this._emitChangeEvent(),e}deselect(...n){this._verifyValueAssignment(n),n.forEach(r=>this._unmarkSelected(r));let e=this._hasQueuedChanges();return this._emitChangeEvent(),e}setSelection(...n){this._verifyValueAssignment(n);let e=this.selected,r=new Set(n.map(i=>this._getConcreteValue(i)));n.forEach(i=>this._markSelected(i)),e.filter(i=>!r.has(this._getConcreteValue(i,r))).forEach(i=>this._unmarkSelected(i));let o=this._hasQueuedChanges();return this._emitChangeEvent(),o}toggle(n){return this.isSelected(n)?this.deselect(n):this.select(n)}clear(n=!0){this._unmarkAll();let e=this._hasQueuedChanges();return n&&this._emitChangeEvent(),e}isSelected(n){return this._selection.has(this._getConcreteValue(n))}isEmpty(){return this._selection.size===0}hasValue(){return!this.isEmpty()}sort(n){this._multiple&&this.selected&&this._selected.sort(n)}isMultipleSelection(){return this._multiple}_emitChangeEvent(){this._selected=null,(this._selectedToEmit.length||this._deselectedToEmit.length)&&(this.changed.next({source:this,added:this._selectedToEmit,removed:this._deselectedToEmit}),this._deselectedToEmit=[],this._selectedToEmit=[])}_markSelected(n){n=this._getConcreteValue(n),this.isSelected(n)||(this._multiple||this._unmarkAll(),this.isSelected(n)||this._selection.add(n),this._emitChanges&&this._selectedToEmit.push(n))}_unmarkSelected(n){n=this._getConcreteValue(n),this.isSelected(n)&&(this._selection.delete(n),this._emitChanges&&this._deselectedToEmit.push(n))}_unmarkAll(){this.isEmpty()||this._selection.forEach(n=>this._unmarkSelected(n))}_verifyValueAssignment(n){n.length>1&&this._multiple}_hasQueuedChanges(){return!!(this._deselectedToEmit.length||this._selectedToEmit.length)}_getConcreteValue(n,e){if(this.compareWith){e=e??this._selection;for(let r of e)if(this.compareWith(n,r))return r;return n}else return n}};var wL=(()=>{class t{_listeners=[];notify(e,r){for(let o of this._listeners)o(e,r)}listen(e){return this._listeners.push(e),()=>{this._listeners=this._listeners.filter(r=>e!==r)}}ngOnDestroy(){this._listeners=[]}static \u0275fac=function(r){return new(r||t)};static \u0275prov=v({token:t,factory:t.\u0275fac,providedIn:"root"})}return t})();var yS=class{applyChanges(n,e,r,o,i){n.forEachOperation((s,a,c)=>{let l,u;if(s.previousIndex==null){let d=r(s,a,c);l=e.createEmbeddedView(d.templateRef,d.context,d.index),u=Dn.INSERTED}else c==null?(e.remove(a),u=Dn.REMOVED):(l=e.get(a),e.move(l,c),u=Dn.MOVED);i&&i({context:l?.context,operation:u,record:s})})}detach(){}};var mJ=(()=>{class t{_animationsDisabled=mr();state="unchecked";disabled=!1;appearance="full";constructor(){}static \u0275fac=function(r){return new(r||t)};static \u0275cmp=ke({type:t,selectors:[["mat-pseudo-checkbox"]],hostAttrs:[1,"mat-pseudo-checkbox"],hostVars:12,hostBindings:function(r,o){r&2&&Xe("mat-pseudo-checkbox-indeterminate",o.state==="indeterminate")("mat-pseudo-checkbox-checked",o.state==="checked")("mat-pseudo-checkbox-disabled",o.disabled)("mat-pseudo-checkbox-minimal",o.appearance==="minimal")("mat-pseudo-checkbox-full",o.appearance==="full")("_mat-animation-noopable",o._animationsDisabled)},inputs:{state:"state",disabled:"disabled",appearance:"appearance"},decls:0,vars:0,template:function(r,o){},styles:[`.mat-pseudo-checkbox { - border-radius: 2px; - cursor: pointer; - display: inline-block; - vertical-align: middle; - box-sizing: border-box; - position: relative; - flex-shrink: 0; - transition: border-color 90ms cubic-bezier(0, 0, 0.2, 0.1), background-color 90ms cubic-bezier(0, 0, 0.2, 0.1); -} -.mat-pseudo-checkbox::after { - position: absolute; - opacity: 0; - content: ""; - border-bottom: 2px solid currentColor; - transition: opacity 90ms cubic-bezier(0, 0, 0.2, 0.1); -} -.mat-pseudo-checkbox._mat-animation-noopable { - transition: none !important; - animation: none !important; -} -.mat-pseudo-checkbox._mat-animation-noopable::after { - transition: none; -} - -.mat-pseudo-checkbox-disabled { - cursor: default; -} - -.mat-pseudo-checkbox-indeterminate::after { - left: 1px; - opacity: 1; - border-radius: 2px; -} - -.mat-pseudo-checkbox-checked::after { - left: 1px; - border-left: 2px solid currentColor; - transform: rotate(-45deg); - opacity: 1; - box-sizing: content-box; -} - -.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-checked::after, .mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-indeterminate::after { - color: var(--mat-pseudo-checkbox-minimal-selected-checkmark-color, var(--mat-sys-primary)); -} -.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled::after, .mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled::after { - color: var(--mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} - -.mat-pseudo-checkbox-full { - border-color: var(--mat-pseudo-checkbox-full-unselected-icon-color, var(--mat-sys-on-surface-variant)); - border-width: 2px; - border-style: solid; -} -.mat-pseudo-checkbox-full.mat-pseudo-checkbox-disabled { - border-color: var(--mat-pseudo-checkbox-full-disabled-unselected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked, .mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate { - background-color: var(--mat-pseudo-checkbox-full-selected-icon-color, var(--mat-sys-primary)); - border-color: transparent; -} -.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked::after, .mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate::after { - color: var(--mat-pseudo-checkbox-full-selected-checkmark-color, var(--mat-sys-on-primary)); -} -.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled, .mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled { - background-color: var(--mat-pseudo-checkbox-full-disabled-selected-icon-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); -} -.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled::after, .mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled::after { - color: var(--mat-pseudo-checkbox-full-disabled-selected-checkmark-color, var(--mat-sys-surface)); -} - -.mat-pseudo-checkbox { - width: 18px; - height: 18px; -} - -.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-checked::after { - width: 14px; - height: 6px; - transform-origin: center; - top: -4.2426406871px; - left: 0; - bottom: 0; - right: 0; - margin: auto; -} -.mat-pseudo-checkbox-minimal.mat-pseudo-checkbox-indeterminate::after { - top: 8px; - width: 16px; -} - -.mat-pseudo-checkbox-full.mat-pseudo-checkbox-checked::after { - width: 10px; - height: 4px; - transform-origin: center; - top: -2.8284271247px; - left: 0; - bottom: 0; - right: 0; - margin: auto; -} -.mat-pseudo-checkbox-full.mat-pseudo-checkbox-indeterminate::after { - top: 6px; - width: 12px; -} -`],encapsulation:2,changeDetection:0})}return t})();export{g as a,F as b,IS as c,CL as d,IL as e,SL as f,G as g,O as h,S as i,Ie as j,Pi as k,jd as l,Vd as m,Se as n,se as o,T as p,Tr as q,Ft as r,HS as s,zS as t,H as u,To as v,ve as w,on as x,Vi as y,Ud as z,Hd as A,xr as B,sM as C,aM as D,fe as E,Bi as F,sn as G,$d as H,Xn as I,cM as J,Ar as K,Ue as L,zd as M,lM as N,xo as O,Rr as P,ac as Q,ry as R,qd as S,oy as T,Ui as U,Nr as V,He as W,at as X,nt as Y,b as Z,be as _,v as $,Z as aa,y as ba,w as ca,f as da,xy as ea,re as fa,Hy as ga,$y as ha,tb as ia,nb as ja,$ as ka,L as la,Ae as ma,U as na,j as oa,_t as pa,W as qa,Go as ra,Re as sa,Ne as ta,z as ua,Fn as va,Jr as wa,Is as xa,ot as ya,c0 as za,k_ as Aa,F_ as Ba,d0 as Ca,f0 as Da,bp as Ea,ht as Fa,dt as Ga,je as Ha,Oe as Ia,D as Ja,Rp as Ka,qe as La,ke as Ma,X as Na,M as Oa,Ns as Pa,pA as Qa,J as Ra,jD as Sa,VD as Ta,ri as Ua,Be as Va,zD as Wa,Yt as Xa,MA as Ya,xA as Za,zp as _a,AA as $a,RA as ab,NA as bb,OA as cb,kA as db,GD as eb,cl as fb,Gp as gb,Ol as hb,to as ib,no as jb,yn as kb,Wp as lb,qp as mb,qD as nb,BA as ob,YD as pb,Zt as qb,KD as rb,GA as sb,ro as tb,Vn as ub,kl as vb,Fl as wb,Fs as xb,Ps as yb,XD as zb,JD as Ab,YA as Bb,ZA as Cb,Pl as Db,Xe as Eb,Yp as Fb,gR as Gb,cE as Hb,Zp as Ib,lE as Jb,uE as Kb,dE as Lb,_R as Mb,fE as Nb,DR as Ob,ER as Pb,we as Qb,SR as Rb,MR as Sb,TR as Tb,xR as Ub,AR as Vb,NR as Wb,kR as Xb,FR as Yb,PR as Zb,LR as _b,jR as $b,q as ac,Kt as bc,Vl as cc,Zq as dc,Kq as ec,RE as fc,Qq as gc,Xq as hc,Jq as ic,e5 as jc,St as kc,Hl as lc,ue as mc,hm as nc,n5 as oc,bn as pc,AN as qc,nw as rc,RN as sc,NN as tc,ON as uc,PN as vc,jN as wc,UN as xc,Cm as yc,iw as zc,km as Ac,nO as Bc,lO as Cc,_n as Dc,$n as Ec,oi as Fc,io as Gc,ii as Hc,Cw as Ic,fu as Jc,RO as Kc,xw as Lc,k9 as Mc,zm as Nc,xt as Oc,Wm as Pc,LO as Qc,Je as Rc,Qs as Sc,Xs as Tc,so as Uc,Nw as Vc,Tt as Wc,he as Xc,fi as Yc,oa as Zc,Ik as _c,fg as $c,Dn as ad,hg as bd,zn as cd,xk as dd,ia as ed,gg as fd,uo as gd,Fk as hd,Pk as id,mg as jd,vg as kd,ta as ld,ao as md,yg as nd,hi as od,Su as pd,Mu as qd,kK as rd,FK as sd,mC as td,bu as ud,wC as vd,wg as wd,ca as xd,Cg as yd,Nu as zd,Sg as Ad,AC as Bd,Mg as Cd,Tg as Dd,Eg as Ed,Uk as Fd,Hk as Gd,yS as Hd,po as Id,zC as Jd,En as Kd,kC as Ld,et as Md,Gn as Nd,BQ as Od,UQ as Pd,ho as Qd,HQ as Rd,lF as Sd,gi as Td,zQ as Ud,hF as Vd,GQ as Wd,mF as Xd,vF as Yd,lI as Zd,uI as _d,DF as $d,wF as ae,WQ as be,qQ as ce,xF as de,RF as ee,OF as fe,kF as ge,YQ as he,ZQ as ie,KQ as je,pu as ke,jO as le,vu as me,BO as ne,yu as oe,Zm as pe,FY as qe,$w as re,zO as se,tk as te,rk as ue,ok as ve,Xm as we,Jm as xe,eg as ye,TZ as ze,sk as Ae,ak as Be,UZ as Ce,l7 as De,QZ as Ee,n7 as Fe,lk as Ge,mr as He,br as Ie,yt as Je,Rt as Ke,yo as Le,Yn as Me,fv as Ne,bt as Oe,Cd as Pe,uL as Qe,mL as Re,ra as Se,cg as Te,C7 as Ue,aC as Ve,cC as We,yk as Xe,dC as Ye,nK as Ze,rK as _e,Iu as $e,EL as af,tJ as bf,Ev as cf,wL as df,mJ as ef}; diff --git a/gns3server/static/web-ui/chunk-UX44Z5PA.js b/gns3server/static/web-ui/chunk-UX44Z5PA.js new file mode 100644 index 000000000..8fdfefec4 --- /dev/null +++ b/gns3server/static/web-ui/chunk-UX44Z5PA.js @@ -0,0 +1 @@ +import{b as V,e as $,f as R,i as L,j as G,k as W,l as Y,m as X,n as U,o as q,q as H}from"./chunk-VB5QSUFX.js";import{$a as d,Bc as F,Ea as s,Eb as v,Gb as a,Hb as h,Ma as M,Nb as k,Ob as T,Pb as D,Te as A,Ue as z,X as w,Ya as j,Za as m,a as b,af as B,bf as N,cb as E,da as x,db as I,eb as C,fb as e,gb as n,ha as p,hb as P,i as S,ia as u,lc as O,ob as _,qb as f,ra as g,sb as l}from"./chunk-G7NALUVX.js";function J(o,c){if(o&1){let t=_();e(0,"div",3)(1,"mat-icon",6),a(2,"warning"),n(),e(3,"p",7),a(4,"Ready to inject a network fault?"),n(),e(5,"div",8)(6,"p",9),a(7,"Number of faults to inject"),n(),e(8,"mat-button-toggle-group",10),D("valueChange",function(i){p(t);let y=l();return T(y.faultType,i)||(y.faultType=i),u(i)}),e(9,"mat-button-toggle",11),a(10,"1"),n(),e(11,"mat-button-toggle",11),a(12,"2"),n(),e(13,"mat-button-toggle",11),a(14,"3"),n(),e(15,"mat-button-toggle",11),a(16,"Random"),n()()(),e(17,"p",12),a(18," This will inject a simulated network fault into your topology for troubleshooting practice. Make sure you have saved your current work. "),n(),e(19,"p",12),a(20," The fault injection process will analyze your topology, select an appropriate fault, and apply it automatically. You'll be able to see the details in AI Chat. "),n()()}if(o&2){let t=l();s(8),k("value",t.faultType),s(),C("value",1),s(2),C("value",2),s(2),C("value",3),s(2),C("value","random")}}function K(o,c){if(o&1&&(e(0,"p",17),a(1),n(),e(2,"p",18),a(3,"Agent is working"),n()),o&2){let t=l(2);s(),h(t.currentStep()||"Injecting fault...")}}function Q(o,c){if(o&1&&(e(0,"div",19)(1,"mat-icon",20),a(2),n(),e(3,"div",21)(4,"p",22),a(5),n(),e(6,"p",23),a(7,"Check the AI Chat panel for detailed execution history and tool results."),n()()()),o&2){let t=l(2);v("success",t.completionStatus()==="success")("error",t.completionStatus()==="error")("aborted",t.completionStatus()==="aborted"),s(2),h(t.completionStatus()==="success"?"check_circle":t.completionStatus()==="aborted"?"cancel":"error"),s(3),h(t.completionTitle())}}function Z(o,c){if(o&1&&(e(0,"div",26)(1,"mat-icon"),a(2),n(),e(3,"div",27)(4,"p",28),a(5),n()()()),o&2){let t=c.$implicit,r=l(3);v("success",t.type==="success")("error",t.type==="error"),s(2),h(r.getEventIcon(t.type)),s(3),h(t.message)}}function tt(o,c){if(o&1&&(e(0,"div",16)(1,"h3"),a(2,"Progress"),n(),e(3,"div",24),E(4,Z,6,6,"div",25,j().trackByEventId,!0),n()()),o&2){let t=l(2);s(4),I(t.displayedEvents())}}function et(o,c){if(o&1&&(e(0,"div",13),P(1,"img",14),n(),m(2,K,4,1),m(3,Q,8,8,"div",15),m(4,tt,6,0,"div",16)),o&2){let t=l();v("injecting",t.isInjecting()),s(2),d(t.isInjecting()?2:-1),s(),d(t.completed()&&!t.isInjecting()?3:-1),s(),d(t.isInjecting()&&t.displayedEvents().length>0?4:-1)}}function nt(o,c){if(o&1){let t=_();e(0,"button",29),f("click",function(){p(t);let i=l();return u(i.onCancelConfirm())}),a(1,"Cancel"),n(),e(2,"button",30),f("click",function(){p(t);let i=l();return u(i.onConfirmInject())}),e(3,"mat-icon"),a(4,"bug_report"),n(),e(5,"span"),a(6,"Confirm & Inject"),n()()}}function it(o,c){if(o&1){let t=_();e(0,"button",29),f("click",function(){p(t);let i=l();return u(i.onAbort())}),a(1,"Abort"),n()}}function ot(o,c){if(o&1){let t=_();e(0,"button",29),f("click",function(){p(t);let i=l();return u(i.onCancel())}),a(1,"Cancel"),n(),e(2,"button",30),f("click",function(){p(t);let i=l();return u(i.onInject())}),e(3,"mat-icon"),a(4,"bug_report"),n(),e(5,"span"),a(6,"Inject Fault"),n()()}}function at(o,c){if(o&1){let t=_();e(0,"button",29),f("click",function(){p(t);let i=l();return u(i.onViewDetails())}),e(1,"mat-icon"),a(2,"chat"),n(),e(3,"span"),a(4,"View in AI Chat"),n()(),e(5,"button",31),f("click",function(){p(t);let i=l();return u(i.onClose())}),e(6,"mat-icon"),a(7,"check"),n(),e(8,"span"),a(9,"Done"),n()()}}var bt=(()=>{class o{dialogRef=x($);data=x(R);aiChatService=x(H);controller=this.data.controller;project=this.data.project;isInjecting=g(!1);completed=g(!1);showConfirm=g(!1);faultType=O(1);faceState=g("idle");completionStatus=g("success");completionTitle=g("");currentStep=g("");eventsBuffer=[];displayedEvents=g([]);firstToolCallProcessed=!1;destroy$=new S;static MAX_DISPLAYED_EVENTS=3;ngOnInit(){this.faceState.set("idle")}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}onInject(){this.showConfirm.set(!0)}onCancelConfirm(){this.showConfirm.set(!1)}onConfirmInject(){if(this.isInjecting())return;this.showConfirm.set(!1),this.isInjecting.set(!0),this.completed.set(!1),this.faceState.set("injecting"),this.eventsBuffer=[],this.displayedEvents.set([]),this.firstToolCallProcessed=!1;let r=`Inject ${this.faultType()==="random"?"random":String(this.faultType())} network fault(s) for troubleshooting practice`;this.aiChatService.injectFault(this.controller,this.project.project_id,r).pipe(w(this.destroy$)).subscribe({next:i=>{this.handleFaultEvent(i)},error:i=>{this.handleError(i)},complete:()=>{}})}handleFaultEvent(t){if(console.log("Fault injection event:",t),!(t.type==="heartbeat"||t.type==="content"))switch(t.type){case"tool_call":if(t.tool_call&&!this.firstToolCallProcessed){let r=t.tool_call.function.name;this.currentStep.set(`Preparing: ${r}`),this.addEvent({type:"tool_call",message:`Preparing: ${r}`}),this.firstToolCallProcessed=!0}break;case"tool_start":t.tool_name&&(this.currentStep.set(`Executing: ${t.tool_name}`),this.addEvent({type:"info",message:`Executing: ${t.tool_name}`}));break;case"tool_end":t.tool_name&&this.addEvent({type:"success",message:`Completed: ${t.tool_name}`});break;case"error":this.handleError(t);break;case"done":this.isInjecting.set(!1),this.completed.set(!0),this.faceState.set("success"),this.completionStatus.set("success"),this.completionTitle.set("Fault injected successfully!"),this.currentStep.set("");break}}handleError(t){console.error("Fault injection error:",t);let r=t?.error?.message||t?.message||t?.error||"Failed to inject fault";this.addEvent({type:"error",message:"Error injecting fault",details:r}),this.isInjecting.set(!1),this.completed.set(!0),this.faceState.set("error"),this.completionStatus.set("error"),this.completionTitle.set("Failed to inject fault"),this.currentStep.set("")}addEvent(t){let r=b({id:`event_${Date.now()}_${Math.random().toString(36).substring(2,11)}`,timestamp:new Date().toISOString()},t);this.eventsBuffer.push(r),this.eventsBuffer.length>o.MAX_DISPLAYED_EVENTS&&this.eventsBuffer.shift(),this.displayedEvents.set([...this.eventsBuffer])}getEventIcon(t){switch(t){case"info":return"info";case"tool_call":return"build";case"success":return"check_circle";case"error":return"error";default:return"info"}}onAbort(){this.isInjecting.set(!1),this.completed.set(!0),this.faceState.set("aborted"),this.completionStatus.set("aborted"),this.completionTitle.set("Fault injection aborted"),this.currentStep.set(""),this.destroy$.next(),this.addEvent({type:"info",message:"Fault injection aborted by user"})}onViewDetails(){this.dialogRef.close({success:this.completionStatus()==="success",openAIChat:!0})}onCancel(){this.isInjecting()||this.dialogRef.close(null)}onClose(){this.dialogRef.close({success:this.completionStatus()==="success",events:this.displayedEvents()})}trackByEventId(t,r){return r.id}static \u0275fac=function(r){return new(r||o)};static \u0275cmp=M({type:o,selectors:[["app-fault-injection-dialog"]],inputs:{faultType:[1,"faultType"]},outputs:{faultType:"faultTypeChange"},decls:13,vars:6,consts:[["mat-dialog-title","",1,"fault-injection-title"],[1,"fault-injection-icon"],["mat-dialog-content","",1,"fault-injection-content"],[1,"confirm-panel"],["mat-dialog-actions","","align","end"],["mat-button",""],[1,"confirm-panel__icon"],[1,"confirm-panel__title"],[1,"fault-type-selector"],[1,"fault-type-selector__label"],["hideSingleSelectionIndicator","true",1,"fault-type-selector__group",3,"valueChange","value"],[1,"fault-type-btn",3,"value"],[1,"confirm-panel__desc"],[1,"animation-area"],["src","assets/gns3_icon.svg","alt","GNS3",1,"gns3-logo"],[1,"completion-message",3,"success","error","aborted"],[1,"events-section"],[1,"status-main"],[1,"status-sub"],[1,"completion-message"],[1,"completion-icon"],[1,"completion-content"],[1,"completion-title"],[1,"completion-text"],[1,"events-list"],[1,"event",3,"success","error"],[1,"event"],[1,"event-content"],[1,"event-msg"],["mat-button","",3,"click"],["mat-raised-button","","color","warn",3,"click"],["mat-raised-button","","color","primary",3,"click"]],template:function(r,i){r&1&&(e(0,"h2",0)(1,"mat-icon",1),a(2,"bug_report"),n(),e(3,"span"),a(4,"Fault Injection"),n()(),e(5,"div",2),m(6,J,21,5,"div",3),m(7,et,5,5),n(),e(8,"div",4),m(9,nt,7,0),m(10,it,2,0,"button",5),m(11,ot,7,0),m(12,at,10,0),n()),r&2&&(s(6),d(i.showConfirm()?6:-1),s(),d(!i.showConfirm()||i.isInjecting()||i.completed()?7:-1),s(2),d(i.showConfirm()?9:-1),s(),d(i.isInjecting()?10:-1),s(),d(!i.showConfirm()&&!i.isInjecting()&&!i.completed()?11:-1),s(),d(i.completed()&&!i.isInjecting()?12:-1))},dependencies:[F,Y,L,W,G,N,B,z,A,V,q,X,U],styles:[".fault-injection-title[_ngcontent-%COMP%]{display:flex;align-items:center;gap:12px}.fault-injection-icon[_ngcontent-%COMP%]{font-size:24px;width:24px;height:24px;color:var(--mat-sys-error)}.fault-injection-content[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:var(--gns3-density-space-5)}.confirm-panel[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;gap:var(--gns3-density-card-gap);padding:32px var(--gns3-density-card-padding);text-align:center}.confirm-panel__icon[_ngcontent-%COMP%]{font-size:48px;width:48px;height:48px;color:var(--mat-sys-error)}.confirm-panel__title[_ngcontent-%COMP%]{font-size:20px;font-weight:500;margin:0;color:var(--mat-sys-on-surface)}.confirm-panel__desc[_ngcontent-%COMP%]{font-size:14px;margin:0;color:var(--mat-sys-on-surface-variant);line-height:1.6;text-align:left;width:100%}.fault-type-selector[_ngcontent-%COMP%]{width:100%;max-width:360px}.fault-type-selector__label[_ngcontent-%COMP%]{font-size:12px;font-weight:500;margin:0 0 8px;text-align:left;color:var(--mat-sys-on-surface-variant)}.fault-type-selector__group[_ngcontent-%COMP%]{display:flex;gap:var(--gns3-density-space-2);width:100%;border:none;border-radius:0}.fault-type-btn[_ngcontent-%COMP%]{flex:1;min-width:0;height:36px;font-size:13px;font-weight:500;border:1px solid var(--mat-sys-outline-variant);border-radius:8px;color:var(--mat-sys-on-surface);display:flex;align-items:center;justify-content:center}.fault-type-btn.mat-button-toggle-checked[_ngcontent-%COMP%]{border-color:var(--mat-sys-primary);background:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);font-weight:600}.animation-area[_ngcontent-%COMP%]{position:relative;display:flex;align-items:center;justify-content:center;padding:32px;background:var(--mat-sys-surface-container-low);border-radius:12px;min-height:200px}.animation-area.injecting[_ngcontent-%COMP%]{background:color-mix(in srgb,var(--mat-sys-error-container) 30%,var(--mat-sys-surface-container-low))}.gns3-logo[_ngcontent-%COMP%]{width:120px;height:120px}.status-main[_ngcontent-%COMP%]{font-size:18px;font-weight:500;margin:0;text-align:center;color:var(--mat-sys-on-surface)}.status-sub[_ngcontent-%COMP%]{font-size:14px;margin:0;text-align:center;color:var(--mat-sys-on-surface-variant)}.completion-message[_ngcontent-%COMP%]{display:flex;gap:var(--gns3-density-card-gap);padding:var(--gns3-density-card-padding);background:var(--mat-sys-surface-container-low);border-radius:12px;border-left:4px solid var(--mat-sys-primary)}.completion-message.success[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-primary);background:var(--mat-sys-primary-container-low)}.completion-message.error[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-error);background:var(--mat-sys-error-container-low)}.completion-message.aborted[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-outline);background:var(--mat-sys-surface-container-high)}.completion-icon[_ngcontent-%COMP%]{font-size:32px;width:32px;height:32px;flex-shrink:0;color:var(--mat-sys-primary)}.completion-message.error[_ngcontent-%COMP%] .completion-icon[_ngcontent-%COMP%]{color:var(--mat-sys-error)}.completion-message.aborted[_ngcontent-%COMP%] .completion-icon[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface-variant)}.completion-content[_ngcontent-%COMP%]{flex:1}.completion-title[_ngcontent-%COMP%]{font-size:18px;font-weight:500;margin:0 0 8px;color:var(--mat-sys-on-surface)}.completion-text[_ngcontent-%COMP%]{font-size:14px;margin:0;color:var(--mat-sys-on-surface-variant);line-height:1.5}.events-section[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:var(--gns3-density-space-3)}.events-section[_ngcontent-%COMP%] h3[_ngcontent-%COMP%]{font-size:16px;font-weight:500;margin:0;color:var(--mat-sys-on-surface)}.events-list[_ngcontent-%COMP%]{display:flex;flex-direction:column;gap:var(--gns3-density-space-2)}.event[_ngcontent-%COMP%]{display:flex;gap:var(--gns3-density-space-3);padding:12px 16px;background:var(--mat-sys-surface-container-low);border-radius:8px;border-left:3px solid var(--mat-sys-outline)}.event.success[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-primary);background:var(--mat-sys-primary-container-low)}.event.error[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-error);background:var(--mat-sys-error-container-low)}.event[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:20px;width:20px;height:20px;flex-shrink:0;color:var(--mat-sys-on-surface-variant)}.event-content[_ngcontent-%COMP%]{flex:1;min-width:0}.event-msg[_ngcontent-%COMP%]{font-size:14px;font-weight:500;margin:0 0 4px;color:var(--mat-sys-on-surface);word-wrap:break-word}.event-details[_ngcontent-%COMP%]{font-size:12px;margin:0;color:var(--mat-sys-on-surface-variant);word-wrap:break-word}"],changeDetection:0})}return o})();export{bt as FaultInjectionDialogComponent}; diff --git a/gns3server/static/web-ui/chunk-VB5QSUFX.js b/gns3server/static/web-ui/chunk-VB5QSUFX.js new file mode 100644 index 000000000..bcb096310 --- /dev/null +++ b/gns3server/static/web-ui/chunk-VB5QSUFX.js @@ -0,0 +1,649 @@ +import{$ as w,$a as Xt,$c as et,$e as Te,A as lt,Ad as be,Cb as pt,Cd as vt,Db as ht,Ea as y,Eb as F,Ed as _e,F as zt,Fa as qt,Fb as ie,G as X,Ga as Jt,Gc as le,Ge as De,Hd as yt,He as ot,I as b,Ia as Kt,Jd as ve,M as z,Ma as O,Mc as ce,Na as E,Oa as M,Qa as Zt,Ra as W,Rc as de,Sa as $,Sb as oe,Tc as bt,V as ct,Xa as m,Xc as Mt,Xe as Se,Za as Yt,Ze as xe,_ as Ht,_c as ue,a as S,aa as I,ac as ae,b as jt,ba as _,ca as H,da as l,eb as R,ec as re,ef as kt,fb as v,fd as J,ff as we,gb as C,h as It,hb as U,hd as ge,hf as Ie,i as D,j as Gt,ja as Ot,k as At,ka as Wt,la as x,le as Ct,ma as $t,mc as tt,md as P,nb as gt,oa as A,oc as k,od as Rt,pa as Ut,pb as Et,pc as ft,pd as me,qb as Q,qd as pe,ra as dt,s as p,sb as mt,se as ye,ta as Qt,tb as te,td as nt,te as Ce,ua as ut,ub as ee,ud as it,v as Vt,va as T,vb as ne,vd as K,ve as ke,wb as q,wc as se,wd as _t,xb as G,yb as V,yd as he,zd as fe}from"./chunk-G7NALUVX.js";var We=["determinateSpinner"];function $e(i,a){if(i&1&&(Ot(),v(0,"svg",11),U(1,"circle",12),C()),i&2){let t=mt();m("viewBox",t._viewBox()),y(),ht("stroke-dasharray",t._strokeCircumference(),"px")("stroke-dashoffset",t._strokeCircumference()/2,"px")("stroke-width",t._circleStrokeWidth(),"%"),m("r",t._circleRadius())}}var Ue=new _("mat-progress-spinner-default-options",{providedIn:"root",factory:()=>({diameter:Ae})}),Ae=100,Qe=10,En=(()=>{class i{_elementRef=l(T);_noopAnimations;get color(){return this._color||this._defaultColor}set color(t){this._color=t}_color;_defaultColor="primary";_determinateCircle;constructor(){let t=l(Ue),e=De(),n=this._elementRef.nativeElement;this._noopAnimations=e==="di-disabled"&&!!t&&!t._forceAnimations,this.mode=n.nodeName.toLowerCase()==="mat-spinner"?"indeterminate":"determinate",!this._noopAnimations&&e==="reduced-motion"&&n.classList.add("mat-progress-spinner-reduced-motion"),t&&(t.color&&(this.color=this._defaultColor=t.color),t.diameter&&(this.diameter=t.diameter),t.strokeWidth&&(this.strokeWidth=t.strokeWidth))}mode;get value(){return this.mode==="determinate"?this._value:0}set value(t){this._value=Math.max(0,Math.min(100,t||0))}_value=0;get diameter(){return this._diameter}set diameter(t){this._diameter=t||0}_diameter=Ae;get strokeWidth(){return this._strokeWidth??this.diameter/10}set strokeWidth(t){this._strokeWidth=t||0}_strokeWidth;_circleRadius(){return(this.diameter-Qe)/2}_viewBox(){let t=this._circleRadius()*2+this.strokeWidth;return`0 0 ${t} ${t}`}_strokeCircumference(){return 2*Math.PI*this._circleRadius()}_strokeDashOffset(){return this.mode==="determinate"?this._strokeCircumference()*(100-this._value)/100:null}_circleStrokeWidth(){return this.strokeWidth/this.diameter*100}static \u0275fac=function(e){return new(e||i)};static \u0275cmp=O({type:i,selectors:[["mat-progress-spinner"],["mat-spinner"]],viewQuery:function(e,n){if(e&1&&q(We,5),e&2){let o;G(o=V())&&(n._determinateCircle=o.first)}},hostAttrs:["role","progressbar","tabindex","-1",1,"mat-mdc-progress-spinner","mdc-circular-progress"],hostVars:18,hostBindings:function(e,n){e&2&&(m("aria-valuemin",0)("aria-valuemax",100)("aria-valuenow",n.mode==="determinate"?n.value:null)("mode",n.mode),ie("mat-"+n.color),ht("width",n.diameter,"px")("height",n.diameter,"px")("--mat-progress-spinner-size",n.diameter+"px")("--mat-progress-spinner-active-indicator-width",n.diameter+"px"),F("_mat-animation-noopable",n._noopAnimations)("mdc-circular-progress--indeterminate",n.mode==="indeterminate"))},inputs:{color:"color",mode:"mode",value:[2,"value","value",ft],diameter:[2,"diameter","diameter",ft],strokeWidth:[2,"strokeWidth","strokeWidth",ft]},exportAs:["matProgressSpinner"],decls:14,vars:11,consts:[["circle",""],["determinateSpinner",""],["aria-hidden","true",1,"mdc-circular-progress__determinate-container"],["xmlns","http://www.w3.org/2000/svg","focusable","false",1,"mdc-circular-progress__determinate-circle-graphic"],["cx","50%","cy","50%",1,"mdc-circular-progress__determinate-circle"],["aria-hidden","true",1,"mdc-circular-progress__indeterminate-container"],[1,"mdc-circular-progress__spinner-layer"],[1,"mdc-circular-progress__circle-clipper","mdc-circular-progress__circle-left"],[3,"ngTemplateOutlet"],[1,"mdc-circular-progress__gap-patch"],[1,"mdc-circular-progress__circle-clipper","mdc-circular-progress__circle-right"],["xmlns","http://www.w3.org/2000/svg","focusable","false",1,"mdc-circular-progress__indeterminate-circle-graphic"],["cx","50%","cy","50%"]],template:function(e,n){if(e&1&&($(0,$e,2,8,"ng-template",null,0,ae),v(2,"div",2,1),Ot(),v(4,"svg",3),U(5,"circle",4),C()(),Wt(),v(6,"div",5)(7,"div",6)(8,"div",7),gt(9,8),C(),v(10,"div",9),gt(11,8),C(),v(12,"div",10),gt(13,8),C()()()),e&2){let o=pt(1);y(4),m("viewBox",n._viewBox()),y(),ht("stroke-dasharray",n._strokeCircumference(),"px")("stroke-dashoffset",n._strokeDashOffset(),"px")("stroke-width",n._circleStrokeWidth(),"%"),m("r",n._circleRadius()),y(4),R("ngTemplateOutlet",o),y(2),R("ngTemplateOutlet",o),y(2),R("ngTemplateOutlet",o)}},dependencies:[se],styles:[`.mat-mdc-progress-spinner { + --mat-progress-spinner-animation-multiplier: 1; + display: block; + overflow: hidden; + line-height: 0; + position: relative; + direction: ltr; + transition: opacity 250ms cubic-bezier(0.4, 0, 0.6, 1); +} +.mat-mdc-progress-spinner circle { + stroke-width: var(--mat-progress-spinner-active-indicator-width, 4px); +} +.mat-mdc-progress-spinner._mat-animation-noopable, .mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__determinate-circle { + transition: none !important; +} +.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__indeterminate-circle-graphic, +.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__spinner-layer, +.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__indeterminate-container { + animation: none !important; +} +.mat-mdc-progress-spinner._mat-animation-noopable .mdc-circular-progress__indeterminate-container circle { + stroke-dasharray: 0 !important; +} +@media (forced-colors: active) { + .mat-mdc-progress-spinner .mdc-circular-progress__indeterminate-circle-graphic, + .mat-mdc-progress-spinner .mdc-circular-progress__determinate-circle { + stroke: currentColor; + stroke: CanvasText; + } +} + +.mat-progress-spinner-reduced-motion { + --mat-progress-spinner-animation-multiplier: 1.25; +} + +.mdc-circular-progress__determinate-container, +.mdc-circular-progress__indeterminate-circle-graphic, +.mdc-circular-progress__indeterminate-container, +.mdc-circular-progress__spinner-layer { + position: absolute; + width: 100%; + height: 100%; +} + +.mdc-circular-progress__determinate-container { + transform: rotate(-90deg); +} +.mdc-circular-progress--indeterminate .mdc-circular-progress__determinate-container { + opacity: 0; +} + +.mdc-circular-progress__indeterminate-container { + font-size: 0; + letter-spacing: 0; + white-space: nowrap; + opacity: 0; +} +.mdc-circular-progress--indeterminate .mdc-circular-progress__indeterminate-container { + opacity: 1; + animation: mdc-circular-progress-container-rotate calc(1568.2352941176ms * var(--mat-progress-spinner-animation-multiplier)) linear infinite; +} + +.mdc-circular-progress__determinate-circle-graphic, +.mdc-circular-progress__indeterminate-circle-graphic { + fill: transparent; +} + +.mat-mdc-progress-spinner .mdc-circular-progress__determinate-circle, +.mat-mdc-progress-spinner .mdc-circular-progress__indeterminate-circle-graphic { + stroke: var(--mat-progress-spinner-active-indicator-color, var(--mat-sys-primary)); +} +@media (forced-colors: active) { + .mat-mdc-progress-spinner .mdc-circular-progress__determinate-circle, + .mat-mdc-progress-spinner .mdc-circular-progress__indeterminate-circle-graphic { + stroke: CanvasText; + } +} + +.mdc-circular-progress__determinate-circle { + transition: stroke-dashoffset 500ms cubic-bezier(0, 0, 0.2, 1); +} + +.mdc-circular-progress__gap-patch { + position: absolute; + top: 0; + left: 47.5%; + box-sizing: border-box; + width: 5%; + height: 100%; + overflow: hidden; +} + +.mdc-circular-progress__gap-patch .mdc-circular-progress__indeterminate-circle-graphic { + left: -900%; + width: 2000%; + transform: rotate(180deg); +} +.mdc-circular-progress__circle-clipper .mdc-circular-progress__indeterminate-circle-graphic { + width: 200%; +} +.mdc-circular-progress__circle-right .mdc-circular-progress__indeterminate-circle-graphic { + left: -100%; +} +.mdc-circular-progress--indeterminate .mdc-circular-progress__circle-left .mdc-circular-progress__indeterminate-circle-graphic { + animation: mdc-circular-progress-left-spin calc(1333ms * var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} +.mdc-circular-progress--indeterminate .mdc-circular-progress__circle-right .mdc-circular-progress__indeterminate-circle-graphic { + animation: mdc-circular-progress-right-spin calc(1333ms * var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} + +.mdc-circular-progress__circle-clipper { + display: inline-flex; + position: relative; + width: 50%; + height: 100%; + overflow: hidden; +} + +.mdc-circular-progress--indeterminate .mdc-circular-progress__spinner-layer { + animation: mdc-circular-progress-spinner-layer-rotate calc(5332ms * var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both; +} + +@keyframes mdc-circular-progress-container-rotate { + to { + transform: rotate(360deg); + } +} +@keyframes mdc-circular-progress-spinner-layer-rotate { + 12.5% { + transform: rotate(135deg); + } + 25% { + transform: rotate(270deg); + } + 37.5% { + transform: rotate(405deg); + } + 50% { + transform: rotate(540deg); + } + 62.5% { + transform: rotate(675deg); + } + 75% { + transform: rotate(810deg); + } + 87.5% { + transform: rotate(945deg); + } + 100% { + transform: rotate(1080deg); + } +} +@keyframes mdc-circular-progress-left-spin { + from { + transform: rotate(265deg); + } + 50% { + transform: rotate(130deg); + } + to { + transform: rotate(265deg); + } +} +@keyframes mdc-circular-progress-right-spin { + from { + transform: rotate(-265deg); + } + 50% { + transform: rotate(-130deg); + } + to { + transform: rotate(-265deg); + } +} +`],encapsulation:2,changeDetection:0})}return i})();var Mn=(()=>{class i{static \u0275fac=function(e){return new(e||i)};static \u0275mod=E({type:i});static \u0275inj=I({imports:[J]})}return i})();function qe(i,a){}var B=class{viewContainerRef;injector;id;role="dialog";panelClass="";hasBackdrop=!0;backdropClass="";disableClose=!1;closePredicate;width="";height="";minWidth;minHeight;maxWidth;maxHeight;positionStrategy;data=null;direction;ariaDescribedBy=null;ariaLabelledBy=null;ariaLabel=null;ariaModal=!1;autoFocus="first-tabbable";restoreFocus=!0;scrollStrategy;closeOnNavigation=!0;closeOnDestroy=!0;closeOnOverlayDetachments=!0;disableAnimations=!1;providers;container;templateContext};var Pt=(()=>{class i extends pe{_elementRef=l(T);_focusTrapFactory=l(Ce);_config;_interactivityChecker=l(ye);_ngZone=l(Ut);_focusMonitor=l(Ct);_renderer=l(Kt);_changeDetectorRef=l(tt);_injector=l(x);_platform=l(ue);_document=l($t);_portalOutlet;_focusTrapped=new D;_focusTrap=null;_elementFocusedBeforeDialogWasOpened=null;_closeInteractionType=null;_ariaLabelledByQueue=[];_isDestroyed=!1;constructor(){super(),this._config=l(B,{optional:!0})||new B,this._config.ariaLabelledBy&&this._ariaLabelledByQueue.push(this._config.ariaLabelledBy)}_addAriaLabelledBy(t){this._ariaLabelledByQueue.push(t),this._changeDetectorRef.markForCheck()}_removeAriaLabelledBy(t){let e=this._ariaLabelledByQueue.indexOf(t);e>-1&&(this._ariaLabelledByQueue.splice(e,1),this._changeDetectorRef.markForCheck())}_contentAttached(){this._initializeFocusTrap(),this._captureInitialFocus()}_captureInitialFocus(){this._trapFocus()}ngOnDestroy(){this._focusTrapped.complete(),this._isDestroyed=!0,this._restoreFocus()}attachComponentPortal(t){this._portalOutlet.hasAttached();let e=this._portalOutlet.attachComponentPortal(t);return this._contentAttached(),e}attachTemplatePortal(t){this._portalOutlet.hasAttached();let e=this._portalOutlet.attachTemplatePortal(t);return this._contentAttached(),e}attachDomPortal=t=>{this._portalOutlet.hasAttached();let e=this._portalOutlet.attachDomPortal(t);return this._contentAttached(),e};_recaptureFocus(){this._containsFocus()||this._trapFocus()}_forceFocus(t,e){this._interactivityChecker.isFocusable(t)||(t.tabIndex=-1,this._ngZone.runOutsideAngular(()=>{let n=()=>{o(),r(),t.removeAttribute("tabindex")},o=this._renderer.listen(t,"blur",n),r=this._renderer.listen(t,"mousedown",n)})),t.focus(e)}_focusByCssSelector(t,e){let n=this._elementRef.nativeElement.querySelector(t);n&&this._forceFocus(n,e)}_trapFocus(t){this._isDestroyed||qt(()=>{let e=this._elementRef.nativeElement;switch(this._config.autoFocus){case!1:case"dialog":this._containsFocus()||e.focus(t);break;case!0:case"first-tabbable":this._focusTrap?.focusInitialElement(t)||this._focusDialogContainer(t);break;case"first-heading":this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]',t);break;default:this._focusByCssSelector(this._config.autoFocus,t);break}this._focusTrapped.next()},{injector:this._injector})}_restoreFocus(){let t=this._config.restoreFocus,e=null;if(typeof t=="string"?e=this._document.querySelector(t):typeof t=="boolean"?e=t?this._elementFocusedBeforeDialogWasOpened:null:t&&(e=t),this._config.restoreFocus&&e&&typeof e.focus=="function"){let n=bt(),o=this._elementRef.nativeElement;(!n||n===this._document.body||n===o||o.contains(n))&&(this._focusMonitor?(this._focusMonitor.focusVia(e,this._closeInteractionType),this._closeInteractionType=null):e.focus())}this._focusTrap&&this._focusTrap.destroy()}_focusDialogContainer(t){this._elementRef.nativeElement.focus?.(t)}_containsFocus(){let t=this._elementRef.nativeElement,e=bt();return t===e||t.contains(e)}_initializeFocusTrap(){this._platform.isBrowser&&(this._focusTrap=this._focusTrapFactory.create(this._elementRef.nativeElement),this._document&&(this._elementFocusedBeforeDialogWasOpened=bt()))}static \u0275fac=function(e){return new(e||i)};static \u0275cmp=O({type:i,selectors:[["cdk-dialog-container"]],viewQuery:function(e,n){if(e&1&&q(nt,7),e&2){let o;G(o=V())&&(n._portalOutlet=o.first)}},hostAttrs:["tabindex","-1",1,"cdk-dialog-container"],hostVars:6,hostBindings:function(e,n){e&2&&m("id",n._config.id||null)("role",n._config.role)("aria-modal",n._config.ariaModal)("aria-labelledby",n._config.ariaLabel?null:n._ariaLabelledByQueue[0])("aria-label",n._config.ariaLabel)("aria-describedby",n._config.ariaDescribedBy||null)},features:[W],decls:1,vars:0,consts:[["cdkPortalOutlet",""]],template:function(e,n){e&1&&$(0,qe,0,0,"ng-template",0)},dependencies:[nt],styles:[`.cdk-dialog-container { + display: block; + width: 100%; + height: 100%; + min-height: inherit; + max-height: inherit; +} +`],encapsulation:2})}return i})(),rt=class{overlayRef;config;componentInstance=null;componentRef=null;containerInstance;disableClose;closed=new D;backdropClick;keydownEvents;outsidePointerEvents;id;_detachSubscription;constructor(a,t){this.overlayRef=a,this.config=t,this.disableClose=t.disableClose,this.backdropClick=a.backdropClick(),this.keydownEvents=a.keydownEvents(),this.outsidePointerEvents=a.outsidePointerEvents(),this.id=t.id,this.keydownEvents.subscribe(e=>{e.keyCode===27&&!this.disableClose&&!K(e)&&(e.preventDefault(),this.close(void 0,{focusOrigin:"keyboard"}))}),this.backdropClick.subscribe(()=>{!this.disableClose&&this._canClose()?this.close(void 0,{focusOrigin:"mouse"}):this.containerInstance._recaptureFocus?.()}),this._detachSubscription=a.detachments().subscribe(()=>{t.closeOnOverlayDetachments!==!1&&this.close()})}close(a,t){if(this._canClose(a)){let e=this.closed;this.containerInstance._closeInteractionType=t?.focusOrigin||"program",this._detachSubscription.unsubscribe(),this.overlayRef.dispose(),e.next(a),e.complete(),this.componentInstance=this.containerInstance=null}}updatePosition(){return this.overlayRef.updatePosition(),this}updateSize(a="",t=""){return this.overlayRef.updateSize({width:a,height:t}),this}addPanelClass(a){return this.overlayRef.addPanelClass(a),this}removePanelClass(a){return this.overlayRef.removePanelClass(a),this}_canClose(a){let t=this.config;return!!this.containerInstance&&(!t.closePredicate||t.closePredicate(a,t,this.componentInstance))}},Je=new _("DialogScrollStrategy",{providedIn:"root",factory:()=>{let i=l(x);return()=>_t(i)}}),Ke=new _("DialogData"),Ze=new _("DefaultDialogConfig");function Ye(i){let a=dt(i),t=new A;return{valueSignal:a,get value(){return a()},change:t,ngOnDestroy(){t.complete()}}}var Bt=(()=>{class i{_injector=l(x);_defaultOptions=l(Ze,{optional:!0});_parentDialog=l(i,{optional:!0,skipSelf:!0});_overlayContainer=l(fe);_idGenerator=l(P);_openDialogsAtThisLevel=[];_afterAllClosedAtThisLevel=new D;_afterOpenedAtThisLevel=new D;_ariaHiddenElements=new Map;_scrollStrategy=l(Je);get openDialogs(){return this._parentDialog?this._parentDialog.openDialogs:this._openDialogsAtThisLevel}get afterOpened(){return this._parentDialog?this._parentDialog.afterOpened:this._afterOpenedAtThisLevel}afterAllClosed=lt(()=>this.openDialogs.length?this._getAfterAllClosed():this._getAfterAllClosed().pipe(ct(void 0)));constructor(){}open(t,e){let n=this._defaultOptions||new B;e=S(S({},n),e),e.id=e.id||this._idGenerator.getId("cdk-dialog-"),e.id&&this.getDialogById(e.id);let o=this._getOverlayConfig(e),r=_e(this._injector,o),s=new rt(r,e),d=this._attachContainer(r,s,e);if(s.containerInstance=d,!this.openDialogs.length){let c=this._overlayContainer.getContainerElement();d._focusTrapped?d._focusTrapped.pipe(z(1)).subscribe(()=>{this._hideNonDialogContentFromAssistiveTechnology(c)}):this._hideNonDialogContentFromAssistiveTechnology(c)}return this._attachDialogContent(t,s,d,e),this.openDialogs.push(s),s.closed.subscribe(()=>this._removeOpenDialog(s,!0)),this.afterOpened.next(s),s}closeAll(){Ft(this.openDialogs,t=>t.close())}getDialogById(t){return this.openDialogs.find(e=>e.id===t)}ngOnDestroy(){Ft(this._openDialogsAtThisLevel,t=>{t.config.closeOnDestroy===!1&&this._removeOpenDialog(t,!1)}),Ft(this._openDialogsAtThisLevel,t=>t.close()),this._afterAllClosedAtThisLevel.complete(),this._afterOpenedAtThisLevel.complete(),this._openDialogsAtThisLevel=[]}_getOverlayConfig(t){let e=new he({positionStrategy:t.positionStrategy||vt().centerHorizontally().centerVertically(),scrollStrategy:t.scrollStrategy||this._scrollStrategy(),panelClass:t.panelClass,hasBackdrop:t.hasBackdrop,direction:t.direction,minWidth:t.minWidth,minHeight:t.minHeight,maxWidth:t.maxWidth,maxHeight:t.maxHeight,width:t.width,height:t.height,disposeOnNavigation:t.closeOnNavigation,disableAnimations:t.disableAnimations});return t.backdropClass&&(e.backdropClass=t.backdropClass),e}_attachContainer(t,e,n){let o=n.injector||n.viewContainerRef?.injector,r=[{provide:B,useValue:n},{provide:rt,useValue:e},{provide:be,useValue:t}],s;n.container?typeof n.container=="function"?s=n.container:(s=n.container.type,r.push(...n.container.providers(n))):s=Pt;let d=new Rt(s,n.viewContainerRef,x.create({parent:o||this._injector,providers:r}));return t.attach(d).instance}_attachDialogContent(t,e,n,o){if(t instanceof Jt){let r=this._createInjector(o,e,n,void 0),s={$implicit:o.data,dialogRef:e};o.templateContext&&(s=S(S({},s),typeof o.templateContext=="function"?o.templateContext():o.templateContext)),n.attachTemplatePortal(new me(t,null,s,r))}else{let r=this._createInjector(o,e,n,this._injector),s=n.attachComponentPortal(new Rt(t,o.viewContainerRef,r));e.componentRef=s,e.componentInstance=s.instance}}_createInjector(t,e,n,o){let r=t.injector||t.viewContainerRef?.injector,s=[{provide:Ke,useValue:t.data},{provide:rt,useValue:e}];return t.providers&&(typeof t.providers=="function"?s.push(...t.providers(e,t,n)):s.push(...t.providers)),t.direction&&(!r||!r.get(et,null,{optional:!0}))&&s.push({provide:et,useValue:Ye(t.direction)}),x.create({parent:r||o,providers:s})}_removeOpenDialog(t,e){let n=this.openDialogs.indexOf(t);n>-1&&(this.openDialogs.splice(n,1),this.openDialogs.length||(this._ariaHiddenElements.forEach((o,r)=>{o?r.setAttribute("aria-hidden",o):r.removeAttribute("aria-hidden")}),this._ariaHiddenElements.clear(),e&&this._getAfterAllClosed().next()))}_hideNonDialogContentFromAssistiveTechnology(t){if(t.parentElement){let e=t.parentElement.children;for(let n=e.length-1;n>-1;n--){let o=e[n];o!==t&&o.nodeName!=="SCRIPT"&&o.nodeName!=="STYLE"&&!o.hasAttribute("aria-live")&&!o.hasAttribute("popover")&&(this._ariaHiddenElements.set(o,o.getAttribute("aria-hidden")),o.setAttribute("aria-hidden","true"))}}}_getAfterAllClosed(){let t=this._parentDialog;return t?t._getAfterAllClosed():this._afterAllClosedAtThisLevel}static \u0275fac=function(e){return new(e||i)};static \u0275prov=w({token:i,factory:i.\u0275fac,providedIn:"root"})}return i})();function Ft(i,a){let t=i.length;for(;t--;)a(i[t])}var Ee=(()=>{class i{static \u0275fac=function(e){return new(e||i)};static \u0275mod=E({type:i});static \u0275inj=I({providers:[Bt],imports:[yt,it,ke,it]})}return i})();function Xe(i,a){}var St=class{viewContainerRef;injector;id;role="dialog";panelClass="";hasBackdrop=!0;backdropClass="";disableClose=!1;closePredicate;width="";height="";minWidth;minHeight;maxWidth;maxHeight;position;data=null;direction;ariaDescribedBy=null;ariaLabelledBy=null;ariaLabel=null;ariaModal=!1;autoFocus="first-tabbable";restoreFocus=!0;delayFocusTrap=!0;scrollStrategy;closeOnNavigation=!0;enterAnimationDuration;exitAnimationDuration},Nt="mdc-dialog--open",Me="mdc-dialog--opening",Re="mdc-dialog--closing",tn=150,en=75,nn=(()=>{class i extends Pt{_animationStateChanged=new A;_animationsEnabled=!ot();_actionSectionCount=0;_hostElement=this._elementRef.nativeElement;_enterAnimationDuration=this._animationsEnabled?Pe(this._config.enterAnimationDuration)??tn:0;_exitAnimationDuration=this._animationsEnabled?Pe(this._config.exitAnimationDuration)??en:0;_animationTimer=null;_contentAttached(){super._contentAttached(),this._startOpenAnimation()}_startOpenAnimation(){this._animationStateChanged.emit({state:"opening",totalTime:this._enterAnimationDuration}),this._animationsEnabled?(this._hostElement.style.setProperty(Fe,`${this._enterAnimationDuration}ms`),this._requestAnimationFrame(()=>this._hostElement.classList.add(Me,Nt)),this._waitForAnimationToComplete(this._enterAnimationDuration,this._finishDialogOpen)):(this._hostElement.classList.add(Nt),Promise.resolve().then(()=>this._finishDialogOpen()))}_startExitAnimation(){this._animationStateChanged.emit({state:"closing",totalTime:this._exitAnimationDuration}),this._hostElement.classList.remove(Nt),this._animationsEnabled?(this._hostElement.style.setProperty(Fe,`${this._exitAnimationDuration}ms`),this._requestAnimationFrame(()=>this._hostElement.classList.add(Re)),this._waitForAnimationToComplete(this._exitAnimationDuration,this._finishDialogClose)):Promise.resolve().then(()=>this._finishDialogClose())}_updateActionSectionCount(t){this._actionSectionCount+=t,this._changeDetectorRef.markForCheck()}_finishDialogOpen=()=>{this._clearAnimationClasses(),this._openAnimationDone(this._enterAnimationDuration)};_finishDialogClose=()=>{this._clearAnimationClasses(),this._animationStateChanged.emit({state:"closed",totalTime:this._exitAnimationDuration})};_clearAnimationClasses(){this._hostElement.classList.remove(Me,Re)}_waitForAnimationToComplete(t,e){this._animationTimer!==null&&clearTimeout(this._animationTimer),this._animationTimer=setTimeout(e,t)}_requestAnimationFrame(t){this._ngZone.runOutsideAngular(()=>{typeof requestAnimationFrame=="function"?requestAnimationFrame(t):t()})}_captureInitialFocus(){this._config.delayFocusTrap||this._trapFocus()}_openAnimationDone(t){this._config.delayFocusTrap&&this._trapFocus(),this._animationStateChanged.next({state:"opened",totalTime:t})}ngOnDestroy(){super.ngOnDestroy(),this._animationTimer!==null&&clearTimeout(this._animationTimer)}attachComponentPortal(t){let e=super.attachComponentPortal(t);return e.location.nativeElement.classList.add("mat-mdc-dialog-component-host"),e}static \u0275fac=(()=>{let t;return function(n){return(t||(t=ut(i)))(n||i)}})();static \u0275cmp=O({type:i,selectors:[["mat-dialog-container"]],hostAttrs:["tabindex","-1",1,"mat-mdc-dialog-container","mdc-dialog"],hostVars:10,hostBindings:function(e,n){e&2&&(Et("id",n._config.id),m("aria-modal",n._config.ariaModal)("role",n._config.role)("aria-labelledby",n._config.ariaLabel?null:n._ariaLabelledByQueue[0])("aria-label",n._config.ariaLabel)("aria-describedby",n._config.ariaDescribedBy||null),F("_mat-animation-noopable",!n._animationsEnabled)("mat-mdc-dialog-container-with-actions",n._actionSectionCount>0))},features:[W],decls:3,vars:0,consts:[[1,"mat-mdc-dialog-inner-container","mdc-dialog__container"],[1,"mat-mdc-dialog-surface","mdc-dialog__surface"],["cdkPortalOutlet",""]],template:function(e,n){e&1&&(v(0,"div",0)(1,"div",1),$(2,Xe,0,0,"ng-template",2),C()())},dependencies:[nt],styles:[`.mat-mdc-dialog-container { + width: 100%; + height: 100%; + display: block; + box-sizing: border-box; + max-height: inherit; + min-height: inherit; + min-width: inherit; + max-width: inherit; + outline: 0; +} + +.cdk-overlay-pane.mat-mdc-dialog-panel { + max-width: var(--mat-dialog-container-max-width, 560px); + min-width: var(--mat-dialog-container-min-width, 280px); +} +@media (max-width: 599px) { + .cdk-overlay-pane.mat-mdc-dialog-panel { + max-width: var(--mat-dialog-container-small-max-width, calc(100vw - 32px)); + } +} + +.mat-mdc-dialog-inner-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-around; + box-sizing: border-box; + height: 100%; + opacity: 0; + transition: opacity linear var(--mat-dialog-transition-duration, 0ms); + max-height: inherit; + min-height: inherit; + min-width: inherit; + max-width: inherit; +} +.mdc-dialog--closing .mat-mdc-dialog-inner-container { + transition: opacity 75ms linear; + transform: none; +} +.mdc-dialog--open .mat-mdc-dialog-inner-container { + opacity: 1; +} +._mat-animation-noopable .mat-mdc-dialog-inner-container { + transition: none; +} + +.mat-mdc-dialog-surface { + display: flex; + flex-direction: column; + flex-grow: 0; + flex-shrink: 0; + box-sizing: border-box; + width: 100%; + height: 100%; + position: relative; + overflow-y: auto; + outline: 0; + transform: scale(0.8); + transition: transform var(--mat-dialog-transition-duration, 0ms) cubic-bezier(0, 0, 0.2, 1); + max-height: inherit; + min-height: inherit; + min-width: inherit; + max-width: inherit; + box-shadow: var(--mat-dialog-container-elevation-shadow, none); + border-radius: var(--mat-dialog-container-shape, var(--mat-sys-corner-extra-large, 4px)); + background-color: var(--mat-dialog-container-color, var(--mat-sys-surface, white)); +} +[dir=rtl] .mat-mdc-dialog-surface { + text-align: right; +} +.mdc-dialog--open .mat-mdc-dialog-surface, .mdc-dialog--closing .mat-mdc-dialog-surface { + transform: none; +} +._mat-animation-noopable .mat-mdc-dialog-surface { + transition: none; +} +.mat-mdc-dialog-surface::before { + position: absolute; + box-sizing: border-box; + width: 100%; + height: 100%; + top: 0; + left: 0; + border: 2px solid transparent; + border-radius: inherit; + content: ""; + pointer-events: none; +} + +.mat-mdc-dialog-title { + display: block; + position: relative; + flex-shrink: 0; + box-sizing: border-box; + margin: 0 0 1px; + padding: var(--mat-dialog-headline-padding, 6px 24px 13px); +} +.mat-mdc-dialog-title::before { + display: inline-block; + width: 0; + height: 40px; + content: ""; + vertical-align: 0; +} +[dir=rtl] .mat-mdc-dialog-title { + text-align: right; +} +.mat-mdc-dialog-container .mat-mdc-dialog-title { + color: var(--mat-dialog-subhead-color, var(--mat-sys-on-surface, rgba(0, 0, 0, 0.87))); + font-family: var(--mat-dialog-subhead-font, var(--mat-sys-headline-small-font, inherit)); + line-height: var(--mat-dialog-subhead-line-height, var(--mat-sys-headline-small-line-height, 1.5rem)); + font-size: var(--mat-dialog-subhead-size, var(--mat-sys-headline-small-size, 1rem)); + font-weight: var(--mat-dialog-subhead-weight, var(--mat-sys-headline-small-weight, 400)); + letter-spacing: var(--mat-dialog-subhead-tracking, var(--mat-sys-headline-small-tracking, 0.03125em)); +} + +.mat-mdc-dialog-content { + display: block; + flex-grow: 1; + box-sizing: border-box; + margin: 0; + overflow: auto; + max-height: 65vh; +} +.mat-mdc-dialog-content > :first-child { + margin-top: 0; +} +.mat-mdc-dialog-content > :last-child { + margin-bottom: 0; +} +.mat-mdc-dialog-container .mat-mdc-dialog-content { + color: var(--mat-dialog-supporting-text-color, var(--mat-sys-on-surface-variant, rgba(0, 0, 0, 0.6))); + font-family: var(--mat-dialog-supporting-text-font, var(--mat-sys-body-medium-font, inherit)); + line-height: var(--mat-dialog-supporting-text-line-height, var(--mat-sys-body-medium-line-height, 1.5rem)); + font-size: var(--mat-dialog-supporting-text-size, var(--mat-sys-body-medium-size, 1rem)); + font-weight: var(--mat-dialog-supporting-text-weight, var(--mat-sys-body-medium-weight, 400)); + letter-spacing: var(--mat-dialog-supporting-text-tracking, var(--mat-sys-body-medium-tracking, 0.03125em)); +} +.mat-mdc-dialog-container .mat-mdc-dialog-content { + padding: var(--mat-dialog-content-padding, 20px 24px); +} +.mat-mdc-dialog-container-with-actions .mat-mdc-dialog-content { + padding: var(--mat-dialog-with-actions-content-padding, 20px 24px 0); +} +.mat-mdc-dialog-container .mat-mdc-dialog-title + .mat-mdc-dialog-content { + padding-top: 0; +} + +.mat-mdc-dialog-actions { + display: flex; + position: relative; + flex-shrink: 0; + flex-wrap: wrap; + align-items: center; + box-sizing: border-box; + min-height: 52px; + margin: 0; + border-top: 1px solid transparent; + padding: var(--mat-dialog-actions-padding, 16px 24px); + justify-content: var(--mat-dialog-actions-alignment, flex-end); +} +@media (forced-colors: active) { + .mat-mdc-dialog-actions { + border-top-color: CanvasText; + } +} +.mat-mdc-dialog-actions.mat-mdc-dialog-actions-align-start, .mat-mdc-dialog-actions[align=start] { + justify-content: start; +} +.mat-mdc-dialog-actions.mat-mdc-dialog-actions-align-center, .mat-mdc-dialog-actions[align=center] { + justify-content: center; +} +.mat-mdc-dialog-actions.mat-mdc-dialog-actions-align-end, .mat-mdc-dialog-actions[align=end] { + justify-content: flex-end; +} +.mat-mdc-dialog-actions .mat-button-base + .mat-button-base, +.mat-mdc-dialog-actions .mat-mdc-button-base + .mat-mdc-button-base { + margin-left: 8px; +} +[dir=rtl] .mat-mdc-dialog-actions .mat-button-base + .mat-button-base, +[dir=rtl] .mat-mdc-dialog-actions .mat-mdc-button-base + .mat-mdc-button-base { + margin-left: 0; + margin-right: 8px; +} + +.mat-mdc-dialog-component-host { + display: contents; +} +`],encapsulation:2})}return i})(),Fe="--mat-dialog-transition-duration";function Pe(i){return i==null?null:typeof i=="number"?i:i.endsWith("ms")?Mt(i.substring(0,i.length-2)):i.endsWith("s")?Mt(i.substring(0,i.length-1))*1e3:i==="0"?0:null}var Dt=(function(i){return i[i.OPEN=0]="OPEN",i[i.CLOSING=1]="CLOSING",i[i.CLOSED=2]="CLOSED",i})(Dt||{}),st=class{_ref;_config;_containerInstance;componentInstance;componentRef=null;disableClose;id;_afterOpened=new At(1);_beforeClosed=new At(1);_result;_closeFallbackTimeout;_state=Dt.OPEN;_closeInteractionType;constructor(a,t,e){this._ref=a,this._config=t,this._containerInstance=e,this.disableClose=t.disableClose,this.id=a.id,a.addPanelClass("mat-mdc-dialog-panel"),e._animationStateChanged.pipe(X(n=>n.state==="opened"),z(1)).subscribe(()=>{this._afterOpened.next(),this._afterOpened.complete()}),e._animationStateChanged.pipe(X(n=>n.state==="closed"),z(1)).subscribe(()=>{clearTimeout(this._closeFallbackTimeout),this._finishDialogClose()}),a.overlayRef.detachments().subscribe(()=>{this._beforeClosed.next(this._result),this._beforeClosed.complete(),this._finishDialogClose()}),zt(this.backdropClick(),this.keydownEvents().pipe(X(n=>n.keyCode===27&&!this.disableClose&&!K(n)))).subscribe(n=>{this.disableClose||(n.preventDefault(),Be(this,n.type==="keydown"?"keyboard":"mouse"))})}close(a){let t=this._config.closePredicate;t&&!t(a,this._config,this.componentInstance)||(this._result=a,this._containerInstance._animationStateChanged.pipe(X(e=>e.state==="closing"),z(1)).subscribe(e=>{this._beforeClosed.next(a),this._beforeClosed.complete(),this._ref.overlayRef.detachBackdrop(),this._closeFallbackTimeout=setTimeout(()=>this._finishDialogClose(),e.totalTime+100)}),this._state=Dt.CLOSING,this._containerInstance._startExitAnimation())}afterOpened(){return this._afterOpened}afterClosed(){return this._ref.closed}beforeClosed(){return this._beforeClosed}backdropClick(){return this._ref.backdropClick}keydownEvents(){return this._ref.keydownEvents}updatePosition(a){let t=this._ref.config.positionStrategy;return a&&(a.left||a.right)?a.left?t.left(a.left):t.right(a.right):t.centerHorizontally(),a&&(a.top||a.bottom)?a.top?t.top(a.top):t.bottom(a.bottom):t.centerVertically(),this._ref.updatePosition(),this}updateSize(a="",t=""){return this._ref.updateSize(a,t),this}addPanelClass(a){return this._ref.addPanelClass(a),this}removePanelClass(a){return this._ref.removePanelClass(a),this}getState(){return this._state}_finishDialogClose(){this._state=Dt.CLOSED,this._ref.close(this._result,{focusOrigin:this._closeInteractionType}),this.componentInstance=null}};function Be(i,a,t){return i._closeInteractionType=a,i.close(t)}var on=new _("MatMdcDialogData"),an=new _("mat-mdc-dialog-default-options"),rn=new _("mat-mdc-dialog-scroll-strategy",{providedIn:"root",factory:()=>{let i=l(x);return()=>_t(i)}}),Lt=(()=>{class i{_defaultOptions=l(an,{optional:!0});_scrollStrategy=l(rn);_parentDialog=l(i,{optional:!0,skipSelf:!0});_idGenerator=l(P);_injector=l(x);_dialog=l(Bt);_animationsDisabled=ot();_openDialogsAtThisLevel=[];_afterAllClosedAtThisLevel=new D;_afterOpenedAtThisLevel=new D;dialogConfigClass=St;_dialogRefConstructor;_dialogContainerType;_dialogDataToken;get openDialogs(){return this._parentDialog?this._parentDialog.openDialogs:this._openDialogsAtThisLevel}get afterOpened(){return this._parentDialog?this._parentDialog.afterOpened:this._afterOpenedAtThisLevel}_getAfterAllClosed(){let t=this._parentDialog;return t?t._getAfterAllClosed():this._afterAllClosedAtThisLevel}afterAllClosed=lt(()=>this.openDialogs.length?this._getAfterAllClosed():this._getAfterAllClosed().pipe(ct(void 0)));constructor(){this._dialogRefConstructor=st,this._dialogContainerType=nn,this._dialogDataToken=on}open(t,e){let n;e=S(S({},this._defaultOptions||new St),e),e.id=e.id||this._idGenerator.getId("mat-mdc-dialog-"),e.scrollStrategy=e.scrollStrategy||this._scrollStrategy();let o=this._dialog.open(t,jt(S({},e),{positionStrategy:vt(this._injector).centerHorizontally().centerVertically(),disableClose:!0,closePredicate:void 0,closeOnDestroy:!1,closeOnOverlayDetachments:!1,disableAnimations:this._animationsDisabled||e.enterAnimationDuration?.toLocaleString()==="0"||e.exitAnimationDuration?.toString()==="0",container:{type:this._dialogContainerType,providers:()=>[{provide:this.dialogConfigClass,useValue:e},{provide:B,useValue:e}]},templateContext:()=>({dialogRef:n}),providers:(r,s,d)=>(n=new this._dialogRefConstructor(r,e,d),n.updatePosition(e?.position),[{provide:this._dialogContainerType,useValue:d},{provide:this._dialogDataToken,useValue:s.data},{provide:this._dialogRefConstructor,useValue:n}])}));return n.componentRef=o.componentRef,n.componentInstance=o.componentInstance,this.openDialogs.push(n),this.afterOpened.next(n),n.afterClosed().subscribe(()=>{let r=this.openDialogs.indexOf(n);r>-1&&(this.openDialogs.splice(r,1),this.openDialogs.length||this._getAfterAllClosed().next())}),n}closeAll(){this._closeDialogs(this.openDialogs)}getDialogById(t){return this.openDialogs.find(e=>e.id===t)}ngOnDestroy(){this._closeDialogs(this._openDialogsAtThisLevel),this._afterAllClosedAtThisLevel.complete(),this._afterOpenedAtThisLevel.complete()}_closeDialogs(t){let e=t.length;for(;e--;)t[e].close()}static \u0275fac=function(e){return new(e||i)};static \u0275prov=w({token:i,factory:i.\u0275fac,providedIn:"root"})}return i})(),vi=(()=>{class i{dialogRef=l(st,{optional:!0});_elementRef=l(T);_dialog=l(Lt);ariaLabel;type="button";dialogResult;_matDialogClose;constructor(){}ngOnInit(){this.dialogRef||(this.dialogRef=Le(this._elementRef,this._dialog.openDialogs))}ngOnChanges(t){let e=t._matDialogClose||t._matDialogCloseResult;e&&(this.dialogResult=e.currentValue)}_onButtonClick(t){Be(this.dialogRef,t.screenX===0&&t.screenY===0?"keyboard":"mouse",this.dialogResult)}static \u0275fac=function(e){return new(e||i)};static \u0275dir=M({type:i,selectors:[["","mat-dialog-close",""],["","matDialogClose",""]],hostVars:2,hostBindings:function(e,n){e&1&&Q("click",function(r){return n._onButtonClick(r)}),e&2&&m("aria-label",n.ariaLabel||null)("type",n.type)},inputs:{ariaLabel:[0,"aria-label","ariaLabel"],type:"type",dialogResult:[0,"mat-dialog-close","dialogResult"],_matDialogClose:[0,"matDialogClose","_matDialogClose"]},exportAs:["matDialogClose"],features:[Qt]})}return i})(),Ne=(()=>{class i{_dialogRef=l(st,{optional:!0});_elementRef=l(T);_dialog=l(Lt);constructor(){}ngOnInit(){this._dialogRef||(this._dialogRef=Le(this._elementRef,this._dialog.openDialogs)),this._dialogRef&&Promise.resolve().then(()=>{this._onAdd()})}ngOnDestroy(){this._dialogRef?._containerInstance&&Promise.resolve().then(()=>{this._onRemove()})}static \u0275fac=function(e){return new(e||i)};static \u0275dir=M({type:i})}return i})(),yi=(()=>{class i extends Ne{id=l(P).getId("mat-mdc-dialog-title-");_onAdd(){this._dialogRef._containerInstance?._addAriaLabelledBy?.(this.id)}_onRemove(){this._dialogRef?._containerInstance?._removeAriaLabelledBy?.(this.id)}static \u0275fac=(()=>{let t;return function(n){return(t||(t=ut(i)))(n||i)}})();static \u0275dir=M({type:i,selectors:[["","mat-dialog-title",""],["","matDialogTitle",""]],hostAttrs:[1,"mat-mdc-dialog-title","mdc-dialog__title"],hostVars:1,hostBindings:function(e,n){e&2&&Et("id",n.id)},inputs:{id:"id"},exportAs:["matDialogTitle"],features:[W]})}return i})(),Ci=(()=>{class i{static \u0275fac=function(e){return new(e||i)};static \u0275dir=M({type:i,selectors:[["","mat-dialog-content",""],["mat-dialog-content"],["","matDialogContent",""]],hostAttrs:[1,"mat-mdc-dialog-content","mdc-dialog__content"],features:[Zt([ge])]})}return i})(),ki=(()=>{class i extends Ne{align;_onAdd(){this._dialogRef._containerInstance?._updateActionSectionCount?.(1)}_onRemove(){this._dialogRef._containerInstance?._updateActionSectionCount?.(-1)}static \u0275fac=(()=>{let t;return function(n){return(t||(t=ut(i)))(n||i)}})();static \u0275dir=M({type:i,selectors:[["","mat-dialog-actions",""],["mat-dialog-actions"],["","matDialogActions",""]],hostAttrs:[1,"mat-mdc-dialog-actions","mdc-dialog__actions"],hostVars:6,hostBindings:function(e,n){e&2&&F("mat-mdc-dialog-actions-align-start",n.align==="start")("mat-mdc-dialog-actions-align-center",n.align==="center")("mat-mdc-dialog-actions-align-end",n.align==="end")},inputs:{align:"align"},features:[W]})}return i})();function Le(i,a){let t=i.nativeElement.parentElement;for(;t&&!t.classList.contains("mat-mdc-dialog-container");)t=t.parentElement;return t?a.find(e=>e.id===t.id):null}var Di=(()=>{class i{static \u0275fac=function(e){return new(e||i)};static \u0275mod=E({type:i});static \u0275inj=I({providers:[Lt],imports:[Ee,yt,it,J]})}return i})();var mn=["button"],pn=["*"];function hn(i,a){if(i&1&&(v(0,"div",2),U(1,"mat-pseudo-checkbox",6),C()),i&2){let t=mt();y(),R("disabled",t.disabled)}}var je=new _("MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS",{providedIn:"root",factory:()=>({hideSingleSelectionIndicator:!1,hideMultipleSelectionIndicator:!1,disabledInteractive:!1})}),Ge=new _("MatButtonToggleGroup"),fn={provide:ve,useExisting:Ht(()=>bn),multi:!0},xt=class{source;value;constructor(a,t){this.source=a,this.value=t}},bn=(()=>{class i{_changeDetector=l(tt);_dir=l(et,{optional:!0});_multiple=!1;_disabled=!1;_disabledInteractive=!1;_selectionModel;_rawValue;_controlValueAccessorChangeFn=()=>{};_onTouched=()=>{};_buttonToggles;appearance;get name(){return this._name}set name(t){this._name=t,this._markButtonsForCheck()}_name=l(P).getId("mat-button-toggle-group-");vertical=!1;get value(){let t=this._selectionModel?this._selectionModel.selected:[];return this.multiple?t.map(e=>e.value):t[0]?t[0].value:void 0}set value(t){this._setSelectionByValue(t),this.valueChange.emit(this.value)}valueChange=new A;get selected(){let t=this._selectionModel?this._selectionModel.selected:[];return this.multiple?t:t[0]||null}get multiple(){return this._multiple}set multiple(t){this._multiple=t,this._markButtonsForCheck()}get disabled(){return this._disabled}set disabled(t){this._disabled=t,this._markButtonsForCheck()}get disabledInteractive(){return this._disabledInteractive}set disabledInteractive(t){this._disabledInteractive=t,this._markButtonsForCheck()}get dir(){return this._dir&&this._dir.value==="rtl"?"rtl":"ltr"}change=new A;get hideSingleSelectionIndicator(){return this._hideSingleSelectionIndicator}set hideSingleSelectionIndicator(t){this._hideSingleSelectionIndicator=t,this._markButtonsForCheck()}_hideSingleSelectionIndicator;get hideMultipleSelectionIndicator(){return this._hideMultipleSelectionIndicator}set hideMultipleSelectionIndicator(t){this._hideMultipleSelectionIndicator=t,this._markButtonsForCheck()}_hideMultipleSelectionIndicator;constructor(){let t=l(je,{optional:!0});this.appearance=t&&t.appearance?t.appearance:"standard",this._hideSingleSelectionIndicator=t?.hideSingleSelectionIndicator??!1,this._hideMultipleSelectionIndicator=t?.hideMultipleSelectionIndicator??!1}ngOnInit(){this._selectionModel=new we(this.multiple,void 0,!1)}ngAfterContentInit(){this._selectionModel.select(...this._buttonToggles.filter(t=>t.checked)),this.multiple||this._initializeTabIndex()}writeValue(t){this.value=t,this._changeDetector.markForCheck()}registerOnChange(t){this._controlValueAccessorChangeFn=t}registerOnTouched(t){this._onTouched=t}setDisabledState(t){this.disabled=t}_keydown(t){if(this.multiple||this.disabled||K(t))return;let n=t.target.id,o=this._buttonToggles.toArray().findIndex(s=>s.buttonId===n),r=null;switch(t.keyCode){case 32:case 13:r=this._buttonToggles.get(o)||null;break;case 38:r=this._getNextButton(o,-1);break;case 37:r=this._getNextButton(o,this.dir==="ltr"?-1:1);break;case 40:r=this._getNextButton(o,1);break;case 39:r=this._getNextButton(o,this.dir==="ltr"?1:-1);break;default:return}r&&(t.preventDefault(),r._onButtonClick(),r.focus())}_emitChangeEvent(t){let e=new xt(t,this.value);this._rawValue=e.value,this._controlValueAccessorChangeFn(e.value),this.change.emit(e)}_syncButtonToggle(t,e,n=!1,o=!1){!this.multiple&&this.selected&&!t.checked&&(this.selected.checked=!1),this._selectionModel?e?this._selectionModel.select(t):this._selectionModel.deselect(t):o=!0,o?Promise.resolve().then(()=>this._updateModelValue(t,n)):this._updateModelValue(t,n)}_isSelected(t){return this._selectionModel&&this._selectionModel.isSelected(t)}_isPrechecked(t){return typeof this._rawValue>"u"?!1:this.multiple&&Array.isArray(this._rawValue)?this._rawValue.some(e=>t.value!=null&&e===t.value):t.value===this._rawValue}_initializeTabIndex(){if(this._buttonToggles.forEach(t=>{t.tabIndex=-1}),this.selected)this.selected.tabIndex=0;else for(let t=0;tthis._selectValue(n,e))):(this._clearSelection(),this._selectValue(t,e)),!this.multiple&&e.every(n=>n.tabIndex===-1)){for(let n of e)if(!n.disabled){n.tabIndex=0;break}}}_clearSelection(){this._selectionModel.clear(),this._buttonToggles.forEach(t=>{t.checked=!1,this.multiple||(t.tabIndex=-1)})}_selectValue(t,e){for(let n of e)if(n.value===t){n.checked=!0,this._selectionModel.select(n),this.multiple||(n.tabIndex=0);break}}_updateModelValue(t,e){e&&this._emitChangeEvent(t),this.valueChange.emit(this.value)}_markButtonsForCheck(){this._buttonToggles?.forEach(t=>t._markForCheck())}static \u0275fac=function(e){return new(e||i)};static \u0275dir=M({type:i,selectors:[["mat-button-toggle-group"]],contentQueries:function(e,n,o){if(e&1&&ne(o,Ve,5),e&2){let r;G(r=V())&&(n._buttonToggles=r)}},hostAttrs:[1,"mat-button-toggle-group"],hostVars:6,hostBindings:function(e,n){e&1&&Q("keydown",function(r){return n._keydown(r)}),e&2&&(m("role",n.multiple?"group":"radiogroup")("aria-disabled",n.disabled),F("mat-button-toggle-vertical",n.vertical)("mat-button-toggle-group-appearance-standard",n.appearance==="standard"))},inputs:{appearance:"appearance",name:"name",vertical:[2,"vertical","vertical",k],value:"value",multiple:[2,"multiple","multiple",k],disabled:[2,"disabled","disabled",k],disabledInteractive:[2,"disabledInteractive","disabledInteractive",k],hideSingleSelectionIndicator:[2,"hideSingleSelectionIndicator","hideSingleSelectionIndicator",k],hideMultipleSelectionIndicator:[2,"hideMultipleSelectionIndicator","hideMultipleSelectionIndicator",k]},outputs:{valueChange:"valueChange",change:"change"},exportAs:["matButtonToggleGroup"],features:[oe([fn,{provide:Ge,useExisting:i}])]})}return i})(),Ve=(()=>{class i{_changeDetectorRef=l(tt);_elementRef=l(T);_focusMonitor=l(Ct);_idGenerator=l(P);_animationDisabled=ot();_checked=!1;ariaLabel;ariaLabelledby=null;_buttonElement;buttonToggleGroup;get buttonId(){return`${this.id}-button`}id;name;value;get tabIndex(){return this._tabIndex()}set tabIndex(t){this._tabIndex.set(t)}_tabIndex;disableRipple=!1;get appearance(){return this.buttonToggleGroup?this.buttonToggleGroup.appearance:this._appearance}set appearance(t){this._appearance=t}_appearance;get checked(){return this.buttonToggleGroup?this.buttonToggleGroup._isSelected(this):this._checked}set checked(t){t!==this._checked&&(this._checked=t,this.buttonToggleGroup&&this.buttonToggleGroup._syncButtonToggle(this,this._checked),this._changeDetectorRef.markForCheck())}get disabled(){return this._disabled||this.buttonToggleGroup&&this.buttonToggleGroup.disabled}set disabled(t){this._disabled=t}_disabled=!1;get disabledInteractive(){return this._disabledInteractive||this.buttonToggleGroup!==null&&this.buttonToggleGroup.disabledInteractive}set disabledInteractive(t){this._disabledInteractive=t}_disabledInteractive;change=new A;constructor(){l(de).load(xe);let t=l(Ge,{optional:!0}),e=l(new re("tabindex"),{optional:!0})||"",n=l(je,{optional:!0});this._tabIndex=dt(parseInt(e)||0),this.buttonToggleGroup=t,this._appearance=n&&n.appearance?n.appearance:"standard",this._disabledInteractive=n?.disabledInteractive??!1}ngOnInit(){let t=this.buttonToggleGroup;this.id=this.id||this._idGenerator.getId("mat-button-toggle-"),t&&(t._isPrechecked(this)?this.checked=!0:t._isSelected(this)!==this._checked&&t._syncButtonToggle(this,this._checked))}ngAfterViewInit(){this._animationDisabled||this._elementRef.nativeElement.classList.add("mat-button-toggle-animations-enabled"),this._focusMonitor.monitor(this._elementRef,!0)}ngOnDestroy(){let t=this.buttonToggleGroup;this._focusMonitor.stopMonitoring(this._elementRef),t&&t._isSelected(this)&&t._syncButtonToggle(this,!1,!1,!0)}focus(t){this._buttonElement.nativeElement.focus(t)}_onButtonClick(){if(this.disabled)return;let t=this.isSingleSelector()?!0:!this._checked;if(t!==this._checked&&(this._checked=t,this.buttonToggleGroup&&(this.buttonToggleGroup._syncButtonToggle(this,this._checked,!0),this.buttonToggleGroup._onTouched())),this.isSingleSelector()){let e=this.buttonToggleGroup._buttonToggles.find(n=>n.tabIndex===0);e&&(e.tabIndex=-1),this.tabIndex=0}this.change.emit(new xt(this,this.value))}_markForCheck(){this._changeDetectorRef.markForCheck()}_getButtonName(){return this.isSingleSelector()?this.buttonToggleGroup.name:this.name||null}isSingleSelector(){return this.buttonToggleGroup&&!this.buttonToggleGroup.multiple}static \u0275fac=function(e){return new(e||i)};static \u0275cmp=O({type:i,selectors:[["mat-button-toggle"]],viewQuery:function(e,n){if(e&1&&q(mn,5),e&2){let o;G(o=V())&&(n._buttonElement=o.first)}},hostAttrs:["role","presentation",1,"mat-button-toggle"],hostVars:14,hostBindings:function(e,n){e&1&&Q("focus",function(){return n.focus()}),e&2&&(m("aria-label",null)("aria-labelledby",null)("id",n.id)("name",null),F("mat-button-toggle-standalone",!n.buttonToggleGroup)("mat-button-toggle-checked",n.checked)("mat-button-toggle-disabled",n.disabled)("mat-button-toggle-disabled-interactive",n.disabledInteractive)("mat-button-toggle-appearance-standard",n.appearance==="standard"))},inputs:{ariaLabel:[0,"aria-label","ariaLabel"],ariaLabelledby:[0,"aria-labelledby","ariaLabelledby"],id:"id",name:"name",value:"value",tabIndex:"tabIndex",disableRipple:[2,"disableRipple","disableRipple",k],appearance:"appearance",checked:[2,"checked","checked",k],disabled:[2,"disabled","disabled",k],disabledInteractive:[2,"disabledInteractive","disabledInteractive",k]},outputs:{change:"change"},exportAs:["matButtonToggle"],ngContentSelectors:pn,decls:7,vars:13,consts:[["button",""],["type","button",1,"mat-button-toggle-button","mat-focus-indicator",3,"click","id","disabled"],[1,"mat-button-toggle-checkbox-wrapper"],[1,"mat-button-toggle-label-content"],[1,"mat-button-toggle-focus-overlay"],["matRipple","",1,"mat-button-toggle-ripple",3,"matRippleTrigger","matRippleDisabled"],["state","checked","aria-hidden","true","appearance","minimal",3,"disabled"]],template:function(e,n){if(e&1&&(te(),v(0,"button",1,0),Q("click",function(){return n._onButtonClick()}),Yt(2,hn,2,1,"div",2),v(3,"span",3),ee(4),C()(),U(5,"span",4)(6,"span",5)),e&2){let o=pt(1);R("id",n.buttonId)("disabled",n.disabled&&!n.disabledInteractive||null),m("role",n.isSingleSelector()?"radio":"button")("tabindex",n.disabled&&!n.disabledInteractive?-1:n.tabIndex)("aria-pressed",n.isSingleSelector()?null:n.checked)("aria-checked",n.isSingleSelector()?n.checked:null)("name",n._getButtonName())("aria-label",n.ariaLabel)("aria-labelledby",n.ariaLabelledby)("aria-disabled",n.disabled&&n.disabledInteractive?"true":null),y(2),Xt(n.buttonToggleGroup&&(!n.buttonToggleGroup.multiple&&!n.buttonToggleGroup.hideSingleSelectionIndicator||n.buttonToggleGroup.multiple&&!n.buttonToggleGroup.hideMultipleSelectionIndicator)?2:-1),y(4),R("matRippleTrigger",o)("matRippleDisabled",n.disableRipple||n.disabled)}},dependencies:[Se,Ie],styles:[`.mat-button-toggle-standalone, +.mat-button-toggle-group { + position: relative; + display: inline-flex; + flex-direction: row; + white-space: nowrap; + overflow: hidden; + -webkit-tap-highlight-color: transparent; + border-radius: var(--mat-button-toggle-legacy-shape); + transform: translateZ(0); +} +.mat-button-toggle-standalone:not([class*=mat-elevation-z]), +.mat-button-toggle-group:not([class*=mat-elevation-z]) { + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); +} +@media (forced-colors: active) { + .mat-button-toggle-standalone, + .mat-button-toggle-group { + outline: solid 1px; + } +} + +.mat-button-toggle-standalone.mat-button-toggle-appearance-standard, +.mat-button-toggle-group-appearance-standard { + border-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); + border: solid 1px var(--mat-button-toggle-divider-color, var(--mat-sys-outline)); +} +.mat-button-toggle-standalone.mat-button-toggle-appearance-standard .mat-pseudo-checkbox, +.mat-button-toggle-group-appearance-standard .mat-pseudo-checkbox { + --mat-pseudo-checkbox-minimal-selected-checkmark-color: var(--mat-button-toggle-selected-state-text-color, var(--mat-sys-on-secondary-container)); +} +.mat-button-toggle-standalone.mat-button-toggle-appearance-standard:not([class*=mat-elevation-z]), +.mat-button-toggle-group-appearance-standard:not([class*=mat-elevation-z]) { + box-shadow: none; +} +@media (forced-colors: active) { + .mat-button-toggle-standalone.mat-button-toggle-appearance-standard, + .mat-button-toggle-group-appearance-standard { + outline: 0; + } +} + +.mat-button-toggle-vertical { + flex-direction: column; +} +.mat-button-toggle-vertical .mat-button-toggle-label-content { + display: block; +} + +.mat-button-toggle { + white-space: nowrap; + position: relative; + color: var(--mat-button-toggle-legacy-text-color); + font-family: var(--mat-button-toggle-legacy-label-text-font); + font-size: var(--mat-button-toggle-legacy-label-text-size); + line-height: var(--mat-button-toggle-legacy-label-text-line-height); + font-weight: var(--mat-button-toggle-legacy-label-text-weight); + letter-spacing: var(--mat-button-toggle-legacy-label-text-tracking); + --mat-pseudo-checkbox-minimal-selected-checkmark-color: var(--mat-button-toggle-legacy-selected-state-text-color); +} +.mat-button-toggle.cdk-keyboard-focused .mat-button-toggle-focus-overlay { + opacity: var(--mat-button-toggle-legacy-focus-state-layer-opacity); +} +.mat-button-toggle .mat-icon svg { + vertical-align: top; +} + +.mat-button-toggle-checkbox-wrapper { + display: inline-block; + justify-content: flex-start; + align-items: center; + width: 0; + height: 18px; + line-height: 18px; + overflow: hidden; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 16px; + transform: translate3d(0, -50%, 0); +} +[dir=rtl] .mat-button-toggle-checkbox-wrapper { + left: auto; + right: 16px; +} +.mat-button-toggle-appearance-standard .mat-button-toggle-checkbox-wrapper { + left: 12px; +} +[dir=rtl] .mat-button-toggle-appearance-standard .mat-button-toggle-checkbox-wrapper { + left: auto; + right: 12px; +} +.mat-button-toggle-checked .mat-button-toggle-checkbox-wrapper { + width: 18px; +} +.mat-button-toggle-animations-enabled .mat-button-toggle-checkbox-wrapper { + transition: width 150ms 45ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-button-toggle-vertical .mat-button-toggle-checkbox-wrapper { + transition: none; +} + +.mat-button-toggle-checked { + color: var(--mat-button-toggle-legacy-selected-state-text-color); + background-color: var(--mat-button-toggle-legacy-selected-state-background-color); +} + +.mat-button-toggle-disabled { + pointer-events: none; + color: var(--mat-button-toggle-legacy-disabled-state-text-color); + background-color: var(--mat-button-toggle-legacy-disabled-state-background-color); + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: var(--mat-button-toggle-legacy-disabled-state-text-color); +} +.mat-button-toggle-disabled.mat-button-toggle-checked { + background-color: var(--mat-button-toggle-legacy-disabled-selected-state-background-color); +} + +.mat-button-toggle-disabled-interactive { + pointer-events: auto; +} + +.mat-button-toggle-appearance-standard { + color: var(--mat-button-toggle-text-color, var(--mat-sys-on-surface)); + background-color: var(--mat-button-toggle-background-color, transparent); + font-family: var(--mat-button-toggle-label-text-font, var(--mat-sys-label-large-font)); + font-size: var(--mat-button-toggle-label-text-size, var(--mat-sys-label-large-size)); + line-height: var(--mat-button-toggle-label-text-line-height, var(--mat-sys-label-large-line-height)); + font-weight: var(--mat-button-toggle-label-text-weight, var(--mat-sys-label-large-weight)); + letter-spacing: var(--mat-button-toggle-label-text-tracking, var(--mat-sys-label-large-tracking)); +} +.mat-button-toggle-group-appearance-standard .mat-button-toggle-appearance-standard + .mat-button-toggle-appearance-standard { + border-left: solid 1px var(--mat-button-toggle-divider-color, var(--mat-sys-outline)); +} +[dir=rtl] .mat-button-toggle-group-appearance-standard .mat-button-toggle-appearance-standard + .mat-button-toggle-appearance-standard { + border-left: none; + border-right: solid 1px var(--mat-button-toggle-divider-color, var(--mat-sys-outline)); +} +.mat-button-toggle-group-appearance-standard.mat-button-toggle-vertical .mat-button-toggle-appearance-standard + .mat-button-toggle-appearance-standard { + border-left: none; + border-right: none; + border-top: solid 1px var(--mat-button-toggle-divider-color, var(--mat-sys-outline)); +} +.mat-button-toggle-appearance-standard.mat-button-toggle-checked { + color: var(--mat-button-toggle-selected-state-text-color, var(--mat-sys-on-secondary-container)); + background-color: var(--mat-button-toggle-selected-state-background-color, var(--mat-sys-secondary-container)); +} +.mat-button-toggle-appearance-standard.mat-button-toggle-disabled { + color: var(--mat-button-toggle-disabled-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); + background-color: var(--mat-button-toggle-disabled-state-background-color, transparent); +} +.mat-button-toggle-appearance-standard.mat-button-toggle-disabled .mat-pseudo-checkbox { + --mat-pseudo-checkbox-minimal-disabled-selected-checkmark-color: var(--mat-button-toggle-disabled-selected-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-button-toggle-appearance-standard.mat-button-toggle-disabled.mat-button-toggle-checked { + color: var(--mat-button-toggle-disabled-selected-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); + background-color: var(--mat-button-toggle-disabled-selected-state-background-color, color-mix(in srgb, var(--mat-sys-on-surface) 12%, transparent)); +} +.mat-button-toggle-appearance-standard .mat-button-toggle-focus-overlay { + background-color: var(--mat-button-toggle-state-layer-color, var(--mat-sys-on-surface)); +} +.mat-button-toggle-appearance-standard:hover .mat-button-toggle-focus-overlay { + opacity: var(--mat-button-toggle-hover-state-layer-opacity, var(--mat-sys-hover-state-layer-opacity)); +} +.mat-button-toggle-appearance-standard.cdk-keyboard-focused .mat-button-toggle-focus-overlay { + opacity: var(--mat-button-toggle-focus-state-layer-opacity, var(--mat-sys-focus-state-layer-opacity)); +} +@media (hover: none) { + .mat-button-toggle-appearance-standard:hover .mat-button-toggle-focus-overlay { + display: none; + } +} + +.mat-button-toggle-label-content { + -webkit-user-select: none; + user-select: none; + display: inline-block; + padding: 0 16px; + line-height: var(--mat-button-toggle-legacy-height); + position: relative; +} +.mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + padding: 0 12px; + line-height: var(--mat-button-toggle-height, 40px); +} + +.mat-button-toggle-label-content > * { + vertical-align: middle; +} + +.mat-button-toggle-focus-overlay { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + border-radius: inherit; + pointer-events: none; + opacity: 0; + background-color: var(--mat-button-toggle-legacy-state-layer-color); +} + +@media (forced-colors: active) { + .mat-button-toggle-checked .mat-button-toggle-focus-overlay { + border-bottom: solid 500px; + opacity: 0.5; + height: 0; + } + .mat-button-toggle-checked:hover .mat-button-toggle-focus-overlay { + opacity: 0.6; + } + .mat-button-toggle-checked.mat-button-toggle-appearance-standard .mat-button-toggle-focus-overlay { + border-bottom: solid 500px; + } +} +.mat-button-toggle .mat-button-toggle-ripple { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + pointer-events: none; +} + +.mat-button-toggle-button { + border: 0; + background: none; + color: inherit; + padding: 0; + margin: 0; + font: inherit; + outline: none; + width: 100%; + cursor: pointer; +} +.mat-button-toggle-animations-enabled .mat-button-toggle-button { + transition: padding 150ms 45ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-button-toggle-vertical .mat-button-toggle-button { + transition: none; +} +.mat-button-toggle-disabled .mat-button-toggle-button { + cursor: default; +} +.mat-button-toggle-button::-moz-focus-inner { + border: 0; +} +.mat-button-toggle-checked .mat-button-toggle-button:has(.mat-button-toggle-checkbox-wrapper) { + padding-left: 30px; +} +[dir=rtl] .mat-button-toggle-checked .mat-button-toggle-button:has(.mat-button-toggle-checkbox-wrapper) { + padding-left: 0; + padding-right: 30px; +} + +.mat-button-toggle-standalone.mat-button-toggle-appearance-standard { + --mat-focus-indicator-border-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); +} + +.mat-button-toggle-group-appearance-standard:not(.mat-button-toggle-vertical) .mat-button-toggle:last-of-type .mat-button-toggle-button::before { + border-top-right-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); + border-bottom-right-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); +} +.mat-button-toggle-group-appearance-standard:not(.mat-button-toggle-vertical) .mat-button-toggle:first-of-type .mat-button-toggle-button::before { + border-top-left-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); + border-bottom-left-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); +} + +.mat-button-toggle-group-appearance-standard.mat-button-toggle-vertical .mat-button-toggle:last-of-type .mat-button-toggle-button::before { + border-bottom-right-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); + border-bottom-left-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); +} +.mat-button-toggle-group-appearance-standard.mat-button-toggle-vertical .mat-button-toggle:first-of-type .mat-button-toggle-button::before { + border-top-right-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); + border-top-left-radius: var(--mat-button-toggle-shape, var(--mat-sys-corner-extra-large)); +} +`],encapsulation:2,changeDetection:0})}return i})(),Qi=(()=>{class i{static \u0275fac=function(e){return new(e||i)};static \u0275mod=E({type:i});static \u0275inj=I({imports:[Te,Ve,J]})}return i})();function _n(i){return i.metadata&&(typeof i.metadata=="string"?JSON.parse(i.metadata):i.metadata).copilot_mode||null}function Ji(i){return _n(i)==="troubleshooting_injection"}var Z=(function(i){return i.NETWORK_ERROR="NETWORK_ERROR",i.PROJECT_NOT_OPENED="PROJECT_NOT_OPENED",i.LLM_NOT_CONFIGURED="LLM_NOT_CONFIGURED",i.SESSION_NOT_FOUND="SESSION_NOT_FOUND",i.UNAUTHORIZED="UNAUTHORIZED",i.UNKNOWN_ERROR="UNKNOWN_ERROR",i})(Z||{});var Tt=class{authToken;id;name;location;host;port;path;ubridge_path;status="stopped";protocol;username;password;tokenExpired=!1};var ze=(()=>{class i{httpController;controllerIds=[];serviceInitialized=new D;isServiceInitialized;constructor(t){this.httpController=t,this.controllerIds=this.getcontrollerIds(),this.isServiceInitialized=!0,this.serviceInitialized.next(this.isServiceInitialized)}getcontrollerIds(){let t=localStorage.getItem("controllerIds");if(t?.length>0){let e=t.split(",");return[...new Set(e)].filter(n=>n&&n.trim().length>0)}return[]}updatecontrollerIds(){localStorage.removeItem("controllerIds"),localStorage.setItem("controllerIds",this.controllerIds.toString())}get(t){let e=JSON.parse(localStorage.getItem(`controller-${t}`));return new Promise(o=>{o(e)})}create(t){if(this.findAllSync().some(s=>s.name===t.name))return Promise.reject(new Error(`Controller with name "${t.name}" already exists`));let n=this.controllerIds.map(s=>parseInt(s.replace("controller-",""),10)).filter(s=>!isNaN(s)),o=n.length>0?Math.max(...n):0;return t.id=o+1,localStorage.setItem(`controller-${t.id}`,JSON.stringify(t)),this.controllerIds.push(`controller-${t.id}`),this.updatecontrollerIds(),new Promise(s=>{s(t)})}findAllSync(){let t=[];return this.controllerIds.forEach(e=>{let n=localStorage.getItem(e);n&&t.push(JSON.parse(n))}),t}isControllerNameTaken(t){return this.findAllSync().some(n=>n.name===t)}update(t){return localStorage.removeItem(`controller-${t.id}`),localStorage.setItem(`controller-${t.id}`,JSON.stringify(t)),new Promise(n=>{n(t)})}findAll(){return new Promise(e=>{let n=[];this.controllerIds.forEach(o=>{let r=localStorage.getItem(o);if(r){let s=JSON.parse(r);n.push(s)}}),e(n)})}delete(t){return localStorage.removeItem(`controller-${t.id}`),this.controllerIds=this.controllerIds.filter(n=>n!==`controller-${t.id}`),this.updatecontrollerIds(),new Promise(n=>{n(t.id)})}getControllerUrl(t){return`${t.protocol}//${t.host}:${t.port}/`}checkControllerVersion(t){return this.httpController.get(t,"/version").pipe(Vt(5e3),b(e=>{if(e.name==="TimeoutError"){let n=new Error("Connection timeout");return p(()=>n)}return p(()=>e)}))}getLocalController(t,e){return new Promise((o,r)=>{this.findAll().then(s=>{let d=s.find(c=>c.location==="bundled");if(d)d.host=t,d.port=e,d.protocol=location.protocol,this.update(d).then(c=>{o(c)},r);else{let c=new Tt;c.name="local",c.host=t,c.port=e,c.location="bundled",c.protocol=location.protocol,this.create(c).then(N=>{o(N)},r)}},r)})}static \u0275fac=function(e){return new(e||i)(H(kt))};static \u0275prov=w({token:i,factory:i.\u0275fac})}return i})();var ro=(()=>{class i{http;httpController;controllerService;currentProjectId=null;currentSessionId=null;isStreaming=new Gt(!1);constructor(t,e,n){this.http=t,this.httpController=e,this.controllerService=n}injectFault(t,e,n){let o=`${this.getControllerUrl(t)}/v3/copilot/projects/${e}/chat/inject`,r=this.getAuthHeaders(t),s={"Content-Type":"application/json"};return r.keys().forEach(d=>{let c=r.get(d);c&&(s[d]=c)}),new It(d=>(fetch(o,{method:"POST",headers:s,body:JSON.stringify({message:n})}).then(async c=>{if(!c.ok){let u=`HTTP error! status: ${c.status}`;try{let f=await c.json();f.message&&(u=f.message)}catch{c.statusText&&(u=c.statusText)}let h=new Error(u);throw h.status=c.status,h.statusText=c.statusText,h.error={message:u},h}if(!c.body)throw new Error("Response body is null");let N=c.body.getReader(),wt=new TextDecoder,L="";(async()=>{try{for(;;){let{done:u,value:h}=await N.read();if(u){d.complete();break}L+=wt.decode(h,{stream:!0});let f=L.split(` +`);L=f.pop()||"";for(let Y of f)if(Y.startsWith("data: ")){let j=Y.slice(6).trim();if(j)try{let g=JSON.parse(j);if(g.type==="heartbeat")continue;if(d.next(g),g.type==="done"||g.type==="error"){d.complete();break}}catch(g){console.error("Failed to parse SSE data:",j,g)}}if(d.closed)break}}catch(u){console.error("Stream processing error:",u),d.error(u)}finally{N.cancel()}})()}).catch(c=>{console.error("Fetch error:",c),d.error(c)}),()=>{})).pipe(b(d=>p(()=>d)))}streamChat(t,e,n){this.currentProjectId=e,this.isStreaming.next(!0);let o=`${this.getControllerUrl(t)}/v3/copilot/projects/${e}/chat/stream`,r=this.getAuthHeaders(t),s={"Content-Type":"application/json"};return r.keys().forEach(d=>{let c=r.get(d);c&&(s[d]=c)}),new It(d=>(fetch(o,{method:"POST",headers:s,body:JSON.stringify(n)}).then(async c=>{if(!c.ok){let u=`HTTP error! status: ${c.status}`;try{let f=await c.json();f.message&&(u=f.message)}catch{c.statusText&&(u=c.statusText)}let h=new Error(u);throw h.status=c.status,h.statusText=c.statusText,h.error={message:u},h}if(!c.body)throw new Error("Response body is null");let N=c.body.getReader(),wt=new TextDecoder,L="";(async()=>{try{for(;;){let{done:u,value:h}=await N.read();if(u){d.complete();break}L+=wt.decode(h,{stream:!0});let f=L.split(` +`);L=f.pop()||"";for(let Y of f)if(Y.startsWith("data: ")){let j=Y.slice(6).trim();if(j)try{let g=JSON.parse(j);if(g.type==="heartbeat")continue;if(g.session_id&&(this.currentSessionId=g.session_id),d.next(g),g.type==="done"||g.type==="error"){d.complete();break}}catch(g){console.error("Failed to parse SSE data:",j,g)}}if(d.closed)break}}catch(u){console.error("Stream processing error:",u),d.error(u)}finally{N.cancel()}})()}).catch(c=>{console.error("Fetch error:",c),d.error(c)}).finally(()=>{this.isStreaming.next(!1)}),()=>{this.isStreaming.next(!1)})).pipe(b(d=>(this.isStreaming.next(!1),p(()=>d))))}getSessions(t,e){return this.httpController.get(t,`/copilot/projects/${e}/chat/sessions`).pipe(b(n=>(console.error("Failed to get sessions:",n),p(()=>n))))}getSessionHistory(t,e,n,o=100){let r=o?{limit:o}:void 0;return this.httpController.get(t,`/copilot/projects/${e}/chat/sessions/${n}/history`).pipe(b(s=>(console.error("Failed to get session history:",s),p(()=>s))))}renameSession(t,e,n,o){let r={title:o};return this.httpController.patch(t,`/copilot/projects/${e}/chat/sessions/${n}`,r).pipe(b(s=>(console.error("Failed to rename session:",s),p(()=>s))))}deleteSession(t,e,n){return this.httpController.delete(t,`/copilot/projects/${e}/chat/sessions/${n}`).pipe(b(o=>(console.error("Failed to delete session:",o),p(()=>o))))}pinSession(t,e,n){return this.httpController.put(t,`/copilot/projects/${e}/chat/sessions/${n}/pin`,null).pipe(b(o=>(console.error("Failed to pin session:",o),p(()=>o))))}unpinSession(t,e,n){return this.httpController.delete(t,`/copilot/projects/${e}/chat/sessions/${n}/pin`).pipe(b(o=>(console.error("Failed to unpin session:",o),p(()=>o))))}getStreamingState(){return this.isStreaming.asObservable()}abortChat(t,e,n){return this.httpController.post(t,`/copilot/projects/${e}/chat/sessions/${n}/abort`,null).pipe(b(o=>(console.error("Failed to abort chat:",o),p(()=>o))))}getCurrentSessionId(){return this.currentSessionId}resetCurrentSession(){this.currentSessionId=null,this.currentProjectId=null}reloadSkills(t){return this.httpController.post(t,"/copilot/reload/skills",null).pipe(b(e=>(console.error("Failed to reload skills:",e),p(()=>e))))}getControllerUrl(t){return`${t.protocol==="https:"?"https":"http"}://${t.host}:${t.port}`}getAuthHeaders(t){let e=new le;if(t.authToken)return e.set("Authorization",`Bearer ${t.authToken}`);if(t.username&&t.password){let n=btoa(`${t.username}:${t.password}`);return e.set("Authorization",`Basic ${n}`)}return e}createChatError(t){return t.status===401?{type:Z.UNAUTHORIZED,message:"Unauthorized access",details:t}:t.status===404?{type:Z.SESSION_NOT_FOUND,message:"Session not found",details:t}:t.message&&t.message.includes("fetch")?{type:Z.NETWORK_ERROR,message:"Network connection failed",details:t}:{type:Z.UNKNOWN_ERROR,message:t.message||"Unknown error",details:t}}static \u0275fac=function(e){return new(e||i)(H(ce),H(kt),H(ze))};static \u0275prov=w({token:i,factory:i.\u0275fac,providedIn:"root"})}return i})();export{En as a,Mn as b,Tt as c,ze as d,st as e,on as f,Lt as g,vi as h,yi as i,Ci as j,ki as k,Di as l,bn as m,Ve as n,Qi as o,Ji as p,ro as q}; diff --git a/gns3server/static/web-ui/index.html b/gns3server/static/web-ui/index.html index ad6e9c4ad..65d330b40 100644 --- a/gns3server/static/web-ui/index.html +++ b/gns3server/static/web-ui/index.html @@ -33,7 +33,7 @@ } })(); - + @@ -115,5 +115,5 @@ } })(); - + diff --git a/gns3server/static/web-ui/main-D46BBTLD.js b/gns3server/static/web-ui/main-D46BBTLD.js new file mode 100644 index 000000000..f2fa59cc1 --- /dev/null +++ b/gns3server/static/web-ui/main-D46BBTLD.js @@ -0,0 +1,4592 @@ +import{a as dt,b as ot,c as KA,d as _t,e as He,f as Wt,g as Xe,h as Xv,i as Ge,j as xt,k as qe,l as Se,m as u5,n as g5,o as tx,p as h5,q as hg}from"./chunk-VB5QSUFX.js";import{$ as pi,A as Vo,B as mt,C as o5,D as st,E as dl,F as us,G as r5,H as a5,I as pl,J as Jv,K as s5,L as l5,M as gg,N as re,O as Nk,P as ex,Q as Ni,R as Yt,S as zt,T as c5,U as Ta,V as qo,W as Qo,X as d5,Y as p5,Z as m5,_ as Co,a as pt,aa as lp,b as at,ba as vo,c as ut,ca as qn,d as YA,da as cp,e as dr,ea as pr,f as ro,fa as gs,g as rt,ga as Zr,h as ze,ha as Jr,i as ni,ia as ea,j as kc,k as Go,l as di,m as Yv,n as mg,o as ke,p as Me,q as Sd,r as Zv,s as ZA,t as JA,u as ug,v as e5,w as nt,x as t5,y as n5,z as i5}from"./chunk-3DUTSYEG.js";import{$ as J,$a as O,$b as eA,$c as Ua,$d as IA,$e as Sc,A as U4,Aa as Lr,Ab as Vn,Ac as aA,Ae as Ik,B as fi,Ba as Ev,Bb as Bn,Bc as fe,Bd as kA,Be as qv,C as wr,Ca as wo,Cb as Te,Cc as Jl,Ce as jA,D as pa,Da as ip,Db as Ot,Dc as sA,De as Pa,E as kv,Ea as p,Eb as se,Ec as lA,Ed as PA,Ee as Ak,F as io,Fa as Kr,Fb as Xr,Fc as Nv,Fe as lf,G as oo,Ga as lr,Gb as c,Gc as Fv,Ge as $A,H as rl,Ha as Dm,Hb as N,Hc as cA,Hd as jv,He as Br,I as Yn,Ia as Ui,Ib as Z,Ic as dA,Id as TA,Ie as HA,J as Pk,Ja as wt,Jb as Ti,Jc as ng,Jd as Md,Je as Fm,K as Tk,Kb as Y4,Kc as pA,Kd as jt,Ke as UA,L as Ju,La as $o,Lb as Z4,Lc as mA,Ld as EA,Le as WA,M as $a,Ma as B,Mb as J4,Mc as cs,Md as We,Me as Lt,N as of,Na as nn,Nb as ln,Nc as uA,Nd as OA,Ne as dg,O as W4,Oa as yt,Ob as Xt,Oc as ig,Od as $v,Oe as It,P as eg,Pa as Ha,Pb as cn,Pc as gA,Pd as $t,Pe as fo,Q as Pv,Qb as Mr,Qc as ka,Qd as Ct,Qe as Qv,Ra as li,Rb as xd,Rc as kr,Rd as Hv,Re as fn,Sa as ce,Sb as un,Sc as Rv,Sd as Uo,Se as GA,T as vd,Ta as Ov,Tb as it,Tc as sf,Td as Yi,Te as pe,Ua as K4,Ub as rn,Uc as Im,Ud as ag,Ue as ne,V as Oi,Vb as Yr,Vc as hA,Vd as Qe,Ve as qA,W as $n,Wa as js,Wb as Iv,Wc as _A,We as Kv,X as $e,Xa as Ve,Xb as Av,Xc as ap,Xd as vt,Xe as sp,Y as ei,Ya as X4,Yb as Dt,Yc as fA,Yd as Wo,Ye as QA,Z as Di,Za as E,Zb as hn,Zc as sl,Zd as ll,Ze as Wa,_ as wc,_a as Dv,_b as zo,_c as Vr,_d as DA,_e as Be,a as q,aa as tn,ab as Ii,ac as $s,ad as Am,ae as Kt,af as _e,b as Le,ba as mn,bb as Ee,bc as tA,bd as og,be as Ht,bf as X,c as Mv,ca as he,cb as Q,cc as af,cd as CA,ce as AA,cf as Li,d as Vs,da as f,db as K,dc as Fe,dd as vA,de as NA,df as XA,e as Un,ea as G4,eb as C,ec as op,ed as xA,ee as sg,ef as on,f as Bs,fa as q4,fb as a,fc as nA,fd as zi,fe as ps,ff as Ga,g as To,ga as Ek,gb as l,gc as zr,gd as Dk,ge as cl,gf as pg,h as ls,ha as w,hb as z,hc as te,hd as Lv,he as lg,i as Ye,ia as M,ib as ti,ic as an,id as wd,ie as Ai,j as sn,ja as gi,jb as wi,jc as iA,jd as yA,je as kt,k as Xu,ka as Qr,kb as Sa,kd as Nm,ke as Ft,l as j4,la as Cr,lb as Ae,lc as $,ld as bA,le as ms,m as $4,ma as Eo,mb as Ne,mc as ie,md as cr,n as ol,na as Rr,nb as Ci,nc as yd,nd as wA,ne as Uv,o as Yu,oa as ve,ob as L,oc as At,p as H4,pa as Ki,pb as ma,pc as Ho,pd as rg,pe as Wv,q as Zu,qa as Q4,qb as _,qe as FA,r as Ut,ra as R,rb as Mc,rc as Mi,re as RA,s as jo,sa as zn,sb as g,sc as Oo,se as LA,t as nf,ta as In,tb as ci,tc as oA,td as zv,te as zA,u as Sv,ua as yi,ub as Zt,uc as rA,ud as Vv,va as Qt,vb as Xi,vc as al,vd as ds,w as Pt,wa as Zl,wb as Gn,wc as bd,we as VA,x as da,xa as Tv,xb as gt,xc as Sr,xd as MA,xe as cg,y as Om,ya as Ok,yb as ht,yc as tg,yd as SA,ye as Gv,z as kk,za as rf,zc as rp,zd as Bv,ze as BA}from"./chunk-G7NALUVX.js";var PL=Vs((SL,kL)=>{(function(n){if(typeof SL=="object")kL.exports=n();else if(typeof define=="function"&&define.amd)define(n);else{var o;try{o=window}catch{o=self}o.SparkMD5=n()}})(function(n){"use strict";var o=function(S,T){return S+T&4294967295},e=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];function t(S,T,A,D,F,I){return T=o(o(T,S),o(D,I)),o(T<>>32-F,A)}function i(S,T){var A=S[0],D=S[1],F=S[2],I=S[3];A+=(D&F|~D&I)+T[0]-680876936|0,A=(A<<7|A>>>25)+D|0,I+=(A&D|~A&F)+T[1]-389564586|0,I=(I<<12|I>>>20)+A|0,F+=(I&A|~I&D)+T[2]+606105819|0,F=(F<<17|F>>>15)+I|0,D+=(F&I|~F&A)+T[3]-1044525330|0,D=(D<<22|D>>>10)+F|0,A+=(D&F|~D&I)+T[4]-176418897|0,A=(A<<7|A>>>25)+D|0,I+=(A&D|~A&F)+T[5]+1200080426|0,I=(I<<12|I>>>20)+A|0,F+=(I&A|~I&D)+T[6]-1473231341|0,F=(F<<17|F>>>15)+I|0,D+=(F&I|~F&A)+T[7]-45705983|0,D=(D<<22|D>>>10)+F|0,A+=(D&F|~D&I)+T[8]+1770035416|0,A=(A<<7|A>>>25)+D|0,I+=(A&D|~A&F)+T[9]-1958414417|0,I=(I<<12|I>>>20)+A|0,F+=(I&A|~I&D)+T[10]-42063|0,F=(F<<17|F>>>15)+I|0,D+=(F&I|~F&A)+T[11]-1990404162|0,D=(D<<22|D>>>10)+F|0,A+=(D&F|~D&I)+T[12]+1804603682|0,A=(A<<7|A>>>25)+D|0,I+=(A&D|~A&F)+T[13]-40341101|0,I=(I<<12|I>>>20)+A|0,F+=(I&A|~I&D)+T[14]-1502002290|0,F=(F<<17|F>>>15)+I|0,D+=(F&I|~F&A)+T[15]+1236535329|0,D=(D<<22|D>>>10)+F|0,A+=(D&I|F&~I)+T[1]-165796510|0,A=(A<<5|A>>>27)+D|0,I+=(A&F|D&~F)+T[6]-1069501632|0,I=(I<<9|I>>>23)+A|0,F+=(I&D|A&~D)+T[11]+643717713|0,F=(F<<14|F>>>18)+I|0,D+=(F&A|I&~A)+T[0]-373897302|0,D=(D<<20|D>>>12)+F|0,A+=(D&I|F&~I)+T[5]-701558691|0,A=(A<<5|A>>>27)+D|0,I+=(A&F|D&~F)+T[10]+38016083|0,I=(I<<9|I>>>23)+A|0,F+=(I&D|A&~D)+T[15]-660478335|0,F=(F<<14|F>>>18)+I|0,D+=(F&A|I&~A)+T[4]-405537848|0,D=(D<<20|D>>>12)+F|0,A+=(D&I|F&~I)+T[9]+568446438|0,A=(A<<5|A>>>27)+D|0,I+=(A&F|D&~F)+T[14]-1019803690|0,I=(I<<9|I>>>23)+A|0,F+=(I&D|A&~D)+T[3]-187363961|0,F=(F<<14|F>>>18)+I|0,D+=(F&A|I&~A)+T[8]+1163531501|0,D=(D<<20|D>>>12)+F|0,A+=(D&I|F&~I)+T[13]-1444681467|0,A=(A<<5|A>>>27)+D|0,I+=(A&F|D&~F)+T[2]-51403784|0,I=(I<<9|I>>>23)+A|0,F+=(I&D|A&~D)+T[7]+1735328473|0,F=(F<<14|F>>>18)+I|0,D+=(F&A|I&~A)+T[12]-1926607734|0,D=(D<<20|D>>>12)+F|0,A+=(D^F^I)+T[5]-378558|0,A=(A<<4|A>>>28)+D|0,I+=(A^D^F)+T[8]-2022574463|0,I=(I<<11|I>>>21)+A|0,F+=(I^A^D)+T[11]+1839030562|0,F=(F<<16|F>>>16)+I|0,D+=(F^I^A)+T[14]-35309556|0,D=(D<<23|D>>>9)+F|0,A+=(D^F^I)+T[1]-1530992060|0,A=(A<<4|A>>>28)+D|0,I+=(A^D^F)+T[4]+1272893353|0,I=(I<<11|I>>>21)+A|0,F+=(I^A^D)+T[7]-155497632|0,F=(F<<16|F>>>16)+I|0,D+=(F^I^A)+T[10]-1094730640|0,D=(D<<23|D>>>9)+F|0,A+=(D^F^I)+T[13]+681279174|0,A=(A<<4|A>>>28)+D|0,I+=(A^D^F)+T[0]-358537222|0,I=(I<<11|I>>>21)+A|0,F+=(I^A^D)+T[3]-722521979|0,F=(F<<16|F>>>16)+I|0,D+=(F^I^A)+T[6]+76029189|0,D=(D<<23|D>>>9)+F|0,A+=(D^F^I)+T[9]-640364487|0,A=(A<<4|A>>>28)+D|0,I+=(A^D^F)+T[12]-421815835|0,I=(I<<11|I>>>21)+A|0,F+=(I^A^D)+T[15]+530742520|0,F=(F<<16|F>>>16)+I|0,D+=(F^I^A)+T[2]-995338651|0,D=(D<<23|D>>>9)+F|0,A+=(F^(D|~I))+T[0]-198630844|0,A=(A<<6|A>>>26)+D|0,I+=(D^(A|~F))+T[7]+1126891415|0,I=(I<<10|I>>>22)+A|0,F+=(A^(I|~D))+T[14]-1416354905|0,F=(F<<15|F>>>17)+I|0,D+=(I^(F|~A))+T[5]-57434055|0,D=(D<<21|D>>>11)+F|0,A+=(F^(D|~I))+T[12]+1700485571|0,A=(A<<6|A>>>26)+D|0,I+=(D^(A|~F))+T[3]-1894986606|0,I=(I<<10|I>>>22)+A|0,F+=(A^(I|~D))+T[10]-1051523|0,F=(F<<15|F>>>17)+I|0,D+=(I^(F|~A))+T[1]-2054922799|0,D=(D<<21|D>>>11)+F|0,A+=(F^(D|~I))+T[8]+1873313359|0,A=(A<<6|A>>>26)+D|0,I+=(D^(A|~F))+T[15]-30611744|0,I=(I<<10|I>>>22)+A|0,F+=(A^(I|~D))+T[6]-1560198380|0,F=(F<<15|F>>>17)+I|0,D+=(I^(F|~A))+T[13]+1309151649|0,D=(D<<21|D>>>11)+F|0,A+=(F^(D|~I))+T[4]-145523070|0,A=(A<<6|A>>>26)+D|0,I+=(D^(A|~F))+T[11]-1120210379|0,I=(I<<10|I>>>22)+A|0,F+=(A^(I|~D))+T[2]+718787259|0,F=(F<<15|F>>>17)+I|0,D+=(I^(F|~A))+T[9]-343485551|0,D=(D<<21|D>>>11)+F|0,S[0]=A+S[0]|0,S[1]=D+S[1]|0,S[2]=F+S[2]|0,S[3]=I+S[3]|0}function r(S){var T=[],A;for(A=0;A<64;A+=4)T[A>>2]=S.charCodeAt(A)+(S.charCodeAt(A+1)<<8)+(S.charCodeAt(A+2)<<16)+(S.charCodeAt(A+3)<<24);return T}function s(S){var T=[],A;for(A=0;A<64;A+=4)T[A>>2]=S[A]+(S[A+1]<<8)+(S[A+2]<<16)+(S[A+3]<<24);return T}function d(S){var T=S.length,A=[1732584193,-271733879,-1732584194,271733878],D,F,I,V,ae,oe;for(D=64;D<=T;D+=64)i(A,r(S.substring(D-64,D)));for(S=S.substring(D-64),F=S.length,I=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],D=0;D>2]|=S.charCodeAt(D)<<(D%4<<3);if(I[D>>2]|=128<<(D%4<<3),D>55)for(i(A,I),D=0;D<16;D+=1)I[D]=0;return V=T*8,V=V.toString(16).match(/(.*?)(.{0,8})$/),ae=parseInt(V[2],16),oe=parseInt(V[1],16)||0,I[14]=ae,I[15]=oe,i(A,I),A}function m(S){var T=S.length,A=[1732584193,-271733879,-1732584194,271733878],D,F,I,V,ae,oe;for(D=64;D<=T;D+=64)i(A,s(S.subarray(D-64,D)));for(S=D-64>2]|=S[D]<<(D%4<<3);if(I[D>>2]|=128<<(D%4<<3),D>55)for(i(A,I),D=0;D<16;D+=1)I[D]=0;return V=T*8,V=V.toString(16).match(/(.*?)(.{0,8})$/),ae=parseInt(V[2],16),oe=parseInt(V[1],16)||0,I[14]=ae,I[15]=oe,i(A,I),A}function u(S){var T="",A;for(A=0;A<4;A+=1)T+=e[S>>A*8+4&15]+e[S>>A*8&15];return T}function h(S){var T;for(T=0;T>16)+(T>>16)+(A>>16);return D<<16|A&65535}),typeof ArrayBuffer<"u"&&!ArrayBuffer.prototype.slice&&(function(){function S(T,A){return T=T|0||0,T<0?Math.max(T+A,0):Math.min(T,A)}ArrayBuffer.prototype.slice=function(T,A){var D=this.byteLength,F=S(T,D),I=D,V,ae,oe,W;return A!==n&&(I=S(A,D)),F>I?new ArrayBuffer(0):(V=I-F,ae=new ArrayBuffer(V),oe=new Uint8Array(ae),W=new Uint8Array(this,F,V),oe.set(W),ae)}})();function v(S){return/[\u0080-\uFFFF]/.test(S)&&(S=unescape(encodeURIComponent(S))),S}function b(S,T){var A=S.length,D=new ArrayBuffer(A),F=new Uint8Array(D),I;for(I=0;I>2]|=T.charCodeAt(D)<<(D%4<<3);return this._finish(F,A),I=h(this._hash),S&&(I=P(I)),this.reset(),I},k.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},k.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash.slice()}},k.prototype.setState=function(S){return this._buff=S.buff,this._length=S.length,this._hash=S.hash,this},k.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},k.prototype._finish=function(S,T){var A=T,D,F,I;if(S[A>>2]|=128<<(A%4<<3),A>55)for(i(this._hash,S),A=0;A<16;A+=1)S[A]=0;D=this._length*8,D=D.toString(16).match(/(.*?)(.{0,8})$/),F=parseInt(D[2],16),I=parseInt(D[1],16)||0,S[14]=F,S[15]=I,i(this._hash,S)},k.hash=function(S,T){return k.hashBinary(v(S),T)},k.hashBinary=function(S,T){var A=d(S),D=h(A);return T?P(D):D},k.ArrayBuffer=function(){this.reset()},k.ArrayBuffer.prototype.append=function(S){var T=x(this._buff.buffer,S,!0),A=T.length,D;for(this._length+=S.byteLength,D=64;D<=A;D+=64)i(this._hash,s(T.subarray(D-64,D)));return this._buff=D-64>2]|=T[F]<<(F%4<<3);return this._finish(D,A),I=h(this._hash),S&&(I=P(I)),this.reset(),I},k.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},k.ArrayBuffer.prototype.getState=function(){var S=k.prototype.getState.call(this);return S.buff=y(S.buff),S},k.ArrayBuffer.prototype.setState=function(S){return S.buff=b(S.buff,!0),k.prototype.setState.call(this,S)},k.ArrayBuffer.prototype.destroy=k.prototype.destroy,k.ArrayBuffer.prototype._finish=k.prototype._finish,k.ArrayBuffer.hash=function(S,T){var A=m(new Uint8Array(S)),D=h(A);return T?P(D):D},k})});var RL=Vs((Lpt,db)=>{(function(n,o,e){if(!n)return;for(var t={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},i={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},r={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},s={option:"alt",command:"meta",return:"enter",escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},d,m=1;m<20;++m)t[111+m]="f"+m;for(m=0;m<=9;++m)t[m+96]=m.toString();function u(I,V,ae){if(I.addEventListener){I.addEventListener(V,ae,!1);return}I.attachEvent("on"+V,ae)}function h(I){if(I.type=="keypress"){var V=String.fromCharCode(I.which);return I.shiftKey||(V=V.toLowerCase()),V}return t[I.which]?t[I.which]:i[I.which]?i[I.which]:String.fromCharCode(I.which).toLowerCase()}function v(I,V){return I.sort().join(",")===V.sort().join(",")}function b(I){var V=[];return I.shiftKey&&V.push("shift"),I.altKey&&V.push("alt"),I.ctrlKey&&V.push("ctrl"),I.metaKey&&V.push("meta"),V}function y(I){if(I.preventDefault){I.preventDefault();return}I.returnValue=!1}function x(I){if(I.stopPropagation){I.stopPropagation();return}I.cancelBubble=!0}function P(I){return I=="shift"||I=="ctrl"||I=="alt"||I=="meta"}function k(){if(!d){d={};for(var I in t)I>95&&I<112||t.hasOwnProperty(I)&&(d[t[I]]=I)}return d}function S(I,V,ae){return ae||(ae=k()[I]?"keydown":"keypress"),ae=="keypress"&&V.length&&(ae="keydown"),ae}function T(I){return I==="+"?["+"]:(I=I.replace(/\+{2}/g,"+plus"),I.split("+"))}function A(I,V){var ae,oe,W,G=[];for(ae=T(I),W=0;W1){ee(de,ye,De,je);return}Y=A(de,je),V._callbacks[Y.key]=V._callbacks[Y.key]||[],me(Y.key,Y.modifiers,{type:Y.action},tt,de,et),V._callbacks[Y.key][tt?"unshift":"push"]({callback:De,modifiers:Y.modifiers,action:Y.action,seq:tt,level:et,combo:de})}V._bindMultiple=function(de,De,je){for(var tt=0;tt-1||D(V,ae.target))return!1;if("composedPath"in I&&typeof I.composedPath=="function"){var oe=I.composedPath()[0];oe!==I.target&&(V=oe)}return V.tagName=="INPUT"||V.tagName=="SELECT"||V.tagName=="TEXTAREA"||V.isContentEditable},F.prototype.handleKey=function(){var I=this;return I._handleKey.apply(I,arguments)},F.addKeycodes=function(I){for(var V in I)I.hasOwnProperty(V)&&(t[V]=I[V]);d=null},F.init=function(){var I=F(o);for(var V in I)V.charAt(0)!=="_"&&(F[V]=(function(ae){return function(){return I[ae].apply(I,arguments)}})(V))},F.init(),n.Mousetrap=F,typeof db<"u"&&db.exports&&(db.exports=F),typeof define=="function"&&define.amd&&define(function(){return F})})(typeof window<"u"?window:null,typeof window<"u"?document:null)});var Uz=Vs(X3=>{var Hz="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");X3.encode=function(n){if(0<=n&&n{var Wz=Uz(),Y3=5,Gz=1<>1;return o?-e:e}Z3.encode=function(o){var e="",t,i=the(o);do t=i&qz,i>>>=Y3,i>0&&(t|=Qz),e+=Wz.encode(t);while(i>0);return e};Z3.decode=function(o,e,t){var i=o.length,r=0,s=0,d,m;do{if(e>=i)throw new Error("Expected more digits in base 64 VLQ value.");if(m=Wz.decode(o.charCodeAt(e++)),m===-1)throw new Error("Invalid base64 digit: "+o.charAt(e-1));d=!!(m&Qz),m&=qz,r=r+(m<{function ihe(n,o,e){if(o in n)return n[o];if(arguments.length===3)return e;throw new Error('"'+o+'" is a required argument.')}la.getArg=ihe;var Xz=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/,ohe=/^data:.+\,.+$/;function MC(n){var o=n.match(Xz);return o?{scheme:o[1],auth:o[2],host:o[3],port:o[4],path:o[5]}:null}la.urlParse=MC;function Fh(n){var o="";return n.scheme&&(o+=n.scheme+":"),o+="//",n.auth&&(o+=n.auth+"@"),n.host&&(o+=n.host),n.port&&(o+=":"+n.port),n.path&&(o+=n.path),o}la.urlGenerate=Fh;var rhe=32;function ahe(n){var o=[];return function(e){for(var t=0;trhe&&o.pop(),r}}var J3=ahe(function(o){var e=o,t=MC(o);if(t){if(!t.path)return o;e=t.path}for(var i=la.isAbsolute(e),r=[],s=0,d=0;;)if(s=d,d=e.indexOf("/",s),d===-1){r.push(e.slice(s));break}else for(r.push(e.slice(s,d));d=0;d--)m=r[d],m==="."?r.splice(d,1):m===".."?u++:u>0&&(m===""?(r.splice(d+1,u),u=0):(r.splice(d,2),u--));return e=r.join("/"),e===""&&(e=i?"/":"."),t?(t.path=e,Fh(t)):e});la.normalize=J3;function Yz(n,o){n===""&&(n="."),o===""&&(o=".");var e=MC(o),t=MC(n);if(t&&(n=t.path||"/"),e&&!e.scheme)return t&&(e.scheme=t.scheme),Fh(e);if(e||o.match(ohe))return o;if(t&&!t.host&&!t.path)return t.host=o,Fh(t);var i=o.charAt(0)==="/"?o:J3(n.replace(/\/+$/,"")+"/"+o);return t?(t.path=i,Fh(t)):i}la.join=Yz;la.isAbsolute=function(n){return n.charAt(0)==="/"||Xz.test(n)};function she(n,o){n===""&&(n="."),n=n.replace(/\/$/,"");for(var e=0;o.indexOf(n+"/")!==0;){var t=n.lastIndexOf("/");if(t<0||(n=n.slice(0,t),n.match(/^([^\/]+:\/)?\/*$/)))return o;++e}return Array(e+1).join("../")+o.substr(n.length+1)}la.relative=she;var Zz=(function(){var n=Object.create(null);return!("__proto__"in n)})();function Jz(n){return n}function lhe(n){return e7(n)?"$"+n:n}la.toSetString=Zz?Jz:lhe;function che(n){return e7(n)?n.slice(1):n}la.fromSetString=Zz?Jz:che;function e7(n){if(!n)return!1;var o=n.length;if(o<9||n.charCodeAt(o-1)!==95||n.charCodeAt(o-2)!==95||n.charCodeAt(o-3)!==111||n.charCodeAt(o-4)!==116||n.charCodeAt(o-5)!==111||n.charCodeAt(o-6)!==114||n.charCodeAt(o-7)!==112||n.charCodeAt(o-8)!==95||n.charCodeAt(o-9)!==95)return!1;for(var e=o-10;e>=0;e--)if(n.charCodeAt(e)!==36)return!1;return!0}function dhe(n,o,e){var t=Gd(n.source,o.source);return t!==0||(t=n.originalLine-o.originalLine,t!==0)||(t=n.originalColumn-o.originalColumn,t!==0||e)||(t=n.generatedColumn-o.generatedColumn,t!==0)||(t=n.generatedLine-o.generatedLine,t!==0)?t:Gd(n.name,o.name)}la.compareByOriginalPositions=dhe;function phe(n,o,e){var t;return t=n.originalLine-o.originalLine,t!==0||(t=n.originalColumn-o.originalColumn,t!==0||e)||(t=n.generatedColumn-o.generatedColumn,t!==0)||(t=n.generatedLine-o.generatedLine,t!==0)?t:Gd(n.name,o.name)}la.compareByOriginalPositionsNoSource=phe;function mhe(n,o,e){var t=n.generatedLine-o.generatedLine;return t!==0||(t=n.generatedColumn-o.generatedColumn,t!==0||e)||(t=Gd(n.source,o.source),t!==0)||(t=n.originalLine-o.originalLine,t!==0)||(t=n.originalColumn-o.originalColumn,t!==0)?t:Gd(n.name,o.name)}la.compareByGeneratedPositionsDeflated=mhe;function uhe(n,o,e){var t=n.generatedColumn-o.generatedColumn;return t!==0||e||(t=Gd(n.source,o.source),t!==0)||(t=n.originalLine-o.originalLine,t!==0)||(t=n.originalColumn-o.originalColumn,t!==0)?t:Gd(n.name,o.name)}la.compareByGeneratedPositionsDeflatedNoLine=uhe;function Gd(n,o){return n===o?0:n===null?1:o===null?-1:n>o?1:-1}function ghe(n,o){var e=n.generatedLine-o.generatedLine;return e!==0||(e=n.generatedColumn-o.generatedColumn,e!==0)||(e=Gd(n.source,o.source),e!==0)||(e=n.originalLine-o.originalLine,e!==0)||(e=n.originalColumn-o.originalColumn,e!==0)?e:Gd(n.name,o.name)}la.compareByGeneratedPositionsInflated=ghe;function hhe(n){return JSON.parse(n.replace(/^\)]}'[^\n]*\n/,""))}la.parseSourceMapInput=hhe;function _he(n,o,e){if(o=o||"",n&&(n[n.length-1]!=="/"&&o[0]!=="/"&&(n+="/"),o=n+o),e){var t=MC(e);if(!t)throw new Error("sourceMapURL could not be parsed");if(t.path){var i=t.path.lastIndexOf("/");i>=0&&(t.path=t.path.substring(0,i+1))}o=Yz(Fh(t),o)}return J3(o)}la.computeSourceURL=_he});var n7=Vs(t7=>{var eO=Ob(),tO=Object.prototype.hasOwnProperty,Ou=typeof Map<"u";function qd(){this._array=[],this._set=Ou?new Map:Object.create(null)}qd.fromArray=function(o,e){for(var t=new qd,i=0,r=o.length;i=0)return e}else{var t=eO.toSetString(o);if(tO.call(this._set,t))return this._set[t]}throw new Error('"'+o+'" is not in the set.')};qd.prototype.at=function(o){if(o>=0&&o{var i7=Ob();function fhe(n,o){var e=n.generatedLine,t=o.generatedLine,i=n.generatedColumn,r=o.generatedColumn;return t>e||t==e&&r>=i||i7.compareByGeneratedPositionsInflated(n,o)<=0}function Db(){this._array=[],this._sorted=!0,this._last={generatedLine:-1,generatedColumn:0}}Db.prototype.unsortedForEach=function(o,e){this._array.forEach(o,e)};Db.prototype.add=function(o){fhe(this._last,o)?(this._last=o,this._array.push(o)):(this._sorted=!1,this._array.push(o))};Db.prototype.toArray=function(){return this._sorted||(this._array.sort(i7.compareByGeneratedPositionsInflated),this._sorted=!0),this._array};o7.MappingList=Db});var s7=Vs(a7=>{var SC=Kz(),br=Ob(),Ib=n7().ArraySet,Che=r7().MappingList;function Bl(n){n||(n={}),this._file=br.getArg(n,"file",null),this._sourceRoot=br.getArg(n,"sourceRoot",null),this._skipValidation=br.getArg(n,"skipValidation",!1),this._ignoreInvalidMapping=br.getArg(n,"ignoreInvalidMapping",!1),this._sources=new Ib,this._names=new Ib,this._mappings=new Che,this._sourcesContents=null}Bl.prototype._version=3;Bl.fromSourceMap=function(o,e){var t=o.sourceRoot,i=new Bl(Object.assign(e||{},{file:o.file,sourceRoot:t}));return o.eachMapping(function(r){var s={generated:{line:r.generatedLine,column:r.generatedColumn}};r.source!=null&&(s.source=r.source,t!=null&&(s.source=br.relative(t,s.source)),s.original={line:r.originalLine,column:r.originalColumn},r.name!=null&&(s.name=r.name)),i.addMapping(s)}),o.sources.forEach(function(r){var s=r;t!==null&&(s=br.relative(t,r)),i._sources.has(s)||i._sources.add(s);var d=o.sourceContentFor(r);d!=null&&i.setSourceContent(r,d)}),i};Bl.prototype.addMapping=function(o){var e=br.getArg(o,"generated"),t=br.getArg(o,"original",null),i=br.getArg(o,"source",null),r=br.getArg(o,"name",null);!this._skipValidation&&this._validateMapping(e,t,i,r)===!1||(i!=null&&(i=String(i),this._sources.has(i)||this._sources.add(i)),r!=null&&(r=String(r),this._names.has(r)||this._names.add(r)),this._mappings.add({generatedLine:e.line,generatedColumn:e.column,originalLine:t!=null&&t.line,originalColumn:t!=null&&t.column,source:i,name:r}))};Bl.prototype.setSourceContent=function(o,e){var t=o;this._sourceRoot!=null&&(t=br.relative(this._sourceRoot,t)),e!=null?(this._sourcesContents||(this._sourcesContents=Object.create(null)),this._sourcesContents[br.toSetString(t)]=e):this._sourcesContents&&(delete this._sourcesContents[br.toSetString(t)],Object.keys(this._sourcesContents).length===0&&(this._sourcesContents=null))};Bl.prototype.applySourceMap=function(o,e,t){var i=e;if(e==null){if(o.file==null)throw new Error(`SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, or the source map's "file" property. Both were omitted.`);i=o.file}var r=this._sourceRoot;r!=null&&(i=br.relative(r,i));var s=new Ib,d=new Ib;this._mappings.unsortedForEach(function(m){if(m.source===i&&m.originalLine!=null){var u=o.originalPositionFor({line:m.originalLine,column:m.originalColumn});u.source!=null&&(m.source=u.source,t!=null&&(m.source=br.join(t,m.source)),r!=null&&(m.source=br.relative(r,m.source)),m.originalLine=u.line,m.originalColumn=u.column,u.name!=null&&(m.name=u.name))}var h=m.source;h!=null&&!s.has(h)&&s.add(h);var v=m.name;v!=null&&!d.has(v)&&d.add(v)},this),this._sources=s,this._names=d,o.sources.forEach(function(m){var u=o.sourceContentFor(m);u!=null&&(t!=null&&(m=br.join(t,m)),r!=null&&(m=br.relative(r,m)),this.setSourceContent(m,u))},this)};Bl.prototype._validateMapping=function(o,e,t,i){if(e&&typeof e.line!="number"&&typeof e.column!="number"){var r="original.line and original.column are not numbers -- you probably meant to omit the original mapping entirely and only map the generated position. If so, pass null for the original mapping instead of an object with empty or null values.";if(this._ignoreInvalidMapping)return typeof console<"u"&&console.warn&&console.warn(r),!1;throw new Error(r)}if(!(o&&"line"in o&&"column"in o&&o.line>0&&o.column>=0&&!e&&!t&&!i)){if(o&&"line"in o&&"column"in o&&e&&"line"in e&&"column"in e&&o.line>0&&o.column>=0&&e.line>0&&e.column>=0&&t)return;var r="Invalid mapping: "+JSON.stringify({generated:o,source:t,original:e,name:i});if(this._ignoreInvalidMapping)return typeof console<"u"&&console.warn&&console.warn(r),!1;throw new Error(r)}};Bl.prototype._serializeMappings=function(){for(var o=0,e=1,t=0,i=0,r=0,s=0,d="",m,u,h,v,b=this._mappings.toArray(),y=0,x=b.length;y0){if(!br.compareByGeneratedPositionsInflated(u,b[y-1]))continue;m+=","}m+=SC.encode(u.generatedColumn-o),o=u.generatedColumn,u.source!=null&&(v=this._sources.indexOf(u.source),m+=SC.encode(v-s),s=v,m+=SC.encode(u.originalLine-1-i),i=u.originalLine-1,m+=SC.encode(u.originalColumn-t),t=u.originalColumn,u.name!=null&&(h=this._names.indexOf(u.name),m+=SC.encode(h-r),r=h)),d+=m}return d};Bl.prototype._generateSourcesContent=function(o,e){return o.map(function(t){if(!this._sourcesContents)return null;e!=null&&(t=br.relative(e,t));var i=br.toSetString(t);return Object.prototype.hasOwnProperty.call(this._sourcesContents,i)?this._sourcesContents[i]:null},this)};Bl.prototype.toJSON=function(){var o={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};return this._file!=null&&(o.file=this._file),this._sourceRoot!=null&&(o.sourceRoot=this._sourceRoot),this._sourcesContents&&(o.sourcesContent=this._generateSourcesContent(o.sources,o.sourceRoot)),o};Bl.prototype.toString=function(){return JSON.stringify(this.toJSON())};a7.SourceMapGenerator=Bl});var AM=Vs((zB,IM)=>{(function(n){"use strict";let o="(0?\\d+|0x[a-f0-9]+)",e={fourOctet:new RegExp(`^${o}\\.${o}\\.${o}\\.${o}$`,"i"),threeOctet:new RegExp(`^${o}\\.${o}\\.${o}$`,"i"),twoOctet:new RegExp(`^${o}\\.${o}$`,"i"),longValue:new RegExp(`^${o}$`,"i")},t=new RegExp("^0[0-7]+$","i"),i=new RegExp("^0x[a-f0-9]+$","i"),r="%[0-9a-z]{1,}",s="(?:[0-9a-f]+::?)+",d={zoneIndex:new RegExp(r,"i"),native:new RegExp(`^(::)?(${s})?([0-9a-f]+)?(::)?(${r})?$`,"i"),deprecatedTransitional:new RegExp(`^(?:::)(${o}\\.${o}\\.${o}\\.${o}(${r})?)$`,"i"),transitional:new RegExp(`^((?:${s})|(?:::)(?:${s})?)${o}\\.${o}\\.${o}\\.${o}(${r})?$`,"i")};function m(y,x){if(y.indexOf("::")!==y.lastIndexOf("::"))return null;let P=0,k=-1,S=(y.match(d.zoneIndex)||[])[0],T,A;for(S&&(S=S.substring(1),y=y.replace(/%.+$/,""));(k=y.indexOf(":",k+1))>=0;)P++;if(y.substr(0,2)==="::"&&P--,y.substr(-2,2)==="::"&&P--,P>x)return null;for(A=x-P,T=":";A--;)T+="0:";return y=y.replace("::",T),y[0]===":"&&(y=y.slice(1)),y[y.length-1]===":"&&(y=y.slice(0,-1)),x=(function(){let D=y.split(":"),F=[];for(let I=0;I0;){if(T=P-k,T<0&&(T=0),y[S]>>T!==x[S]>>T)return!1;k-=P,S+=1}return!0}function h(y){if(i.test(y))return parseInt(y,16);if(y[0]==="0"&&!isNaN(parseInt(y[1],10))){if(t.test(y))return parseInt(y,8);throw new Error(`ipaddr: cannot parse ${y} as octal`)}return parseInt(y,10)}function v(y,x){for(;y.length=0;S-=1)if(T=this.octets[S],T in k){if(A=k[T],P&&A!==0)return null;A!==8&&(P=!0),x+=A}else return null;return 32-x},y.prototype.range=function(){return b.subnetMatch(this,this.SpecialRanges)},y.prototype.toByteArray=function(){return this.octets.slice(0)},y.prototype.toIPv4MappedAddress=function(){return b.IPv6.parse(`::ffff:${this.toString()}`)},y.prototype.toNormalizedString=function(){return this.toString()},y.prototype.toString=function(){return this.octets.join(".")},y})(),b.IPv4.broadcastAddressFromCIDR=function(y){try{let x=this.parseCIDR(y),P=x[0].toByteArray(),k=this.subnetMaskFromPrefixLength(x[1]).toByteArray(),S=[],T=0;for(;T<4;)S.push(parseInt(P[T],10)|parseInt(k[T],10)^255),T++;return new this(S)}catch{throw new Error("ipaddr: the address does not have IPv4 CIDR format")}},b.IPv4.isIPv4=function(y){return this.parser(y)!==null},b.IPv4.isValid=function(y){try{return new this(this.parser(y)),!0}catch{return!1}},b.IPv4.isValidCIDR=function(y){try{return this.parseCIDR(y),!0}catch{return!1}},b.IPv4.isValidFourPartDecimal=function(y){return!!(b.IPv4.isValid(y)&&y.match(/^(0|[1-9]\d*)(\.(0|[1-9]\d*)){3}$/))},b.IPv4.isValidCIDRFourPartDecimal=function(y){let x=y.match(/^(.+)\/(\d+)$/);return!b.IPv4.isValidCIDR(y)||!x?!1:b.IPv4.isValidFourPartDecimal(x[1])},b.IPv4.networkAddressFromCIDR=function(y){let x,P,k,S,T;try{for(x=this.parseCIDR(y),k=x[0].toByteArray(),T=this.subnetMaskFromPrefixLength(x[1]).toByteArray(),S=[],P=0;P<4;)S.push(parseInt(k[P],10)&parseInt(T[P],10)),P++;return new this(S)}catch{throw new Error("ipaddr: the address does not have IPv4 CIDR format")}},b.IPv4.parse=function(y){let x=this.parser(y);if(x===null)throw new Error("ipaddr: string is not formatted like an IPv4 Address");return new this(x)},b.IPv4.parseCIDR=function(y){let x;if(x=y.match(/^(.+)\/(\d+)$/)){let P=parseInt(x[2]);if(P>=0&&P<=32){let k=[this.parse(x[1]),P];return Object.defineProperty(k,"toString",{value:function(){return this.join("/")}}),k}}throw new Error("ipaddr: string is not formatted like an IPv4 CIDR range")},b.IPv4.parser=function(y){let x,P,k;if(x=y.match(e.fourOctet))return(function(){let S=x.slice(1,6),T=[];for(let A=0;A4294967295||k<0)throw new Error("ipaddr: address outside defined range");return(function(){let S=[],T;for(T=0;T<=24;T+=8)S.push(k>>T&255);return S})().reverse()}else return(x=y.match(e.twoOctet))?(function(){let S=x.slice(1,4),T=[];if(k=h(S[1]),k>16777215||k<0)throw new Error("ipaddr: address outside defined range");return T.push(h(S[0])),T.push(k>>16&255),T.push(k>>8&255),T.push(k&255),T})():(x=y.match(e.threeOctet))?(function(){let S=x.slice(1,5),T=[];if(k=h(S[2]),k>65535||k<0)throw new Error("ipaddr: address outside defined range");return T.push(h(S[0])),T.push(h(S[1])),T.push(k>>8&255),T.push(k&255),T})():null},b.IPv4.subnetMaskFromPrefixLength=function(y){if(y=parseInt(y),y<0||y>32)throw new Error("ipaddr: invalid IPv4 prefix length");let x=[0,0,0,0],P=0,k=Math.floor(y/8);for(;P=0;A-=1)if(S=this.parts[A],S in k){if(T=k[S],P&&T!==0)return null;T!==16&&(P=!0),x+=T}else return null;return 128-x},y.prototype.range=function(){return b.subnetMatch(this,this.SpecialRanges)},y.prototype.toByteArray=function(){let x,P=[],k=this.parts;for(let S=0;S>8),P.push(x&255);return P},y.prototype.toFixedLengthString=function(){let x=function(){let k=[];for(let S=0;S>8,P&255,k>>8,k&255])},y.prototype.toNormalizedString=function(){let x=function(){let k=[];for(let S=0;SS&&(k=T.index,S=T[0].length);return S<0?P:`${P.substring(0,k)}::${P.substring(k+S)}`},y.prototype.toString=function(){return this.toRFC5952String()},y})(),b.IPv6.broadcastAddressFromCIDR=function(y){try{let x=this.parseCIDR(y),P=x[0].toByteArray(),k=this.subnetMaskFromPrefixLength(x[1]).toByteArray(),S=[],T=0;for(;T<16;)S.push(parseInt(P[T],10)|parseInt(k[T],10)^255),T++;return new this(S)}catch(x){throw new Error(`ipaddr: the address does not have IPv6 CIDR format (${x})`)}},b.IPv6.isIPv6=function(y){return this.parser(y)!==null},b.IPv6.isValid=function(y){if(typeof y=="string"&&y.indexOf(":")===-1)return!1;try{let x=this.parser(y);return new this(x.parts,x.zoneId),!0}catch{return!1}},b.IPv6.isValidCIDR=function(y){if(typeof y=="string"&&y.indexOf(":")===-1)return!1;try{return this.parseCIDR(y),!0}catch{return!1}},b.IPv6.networkAddressFromCIDR=function(y){let x,P,k,S,T;try{for(x=this.parseCIDR(y),k=x[0].toByteArray(),T=this.subnetMaskFromPrefixLength(x[1]).toByteArray(),S=[],P=0;P<16;)S.push(parseInt(k[P],10)&parseInt(T[P],10)),P++;return new this(S)}catch(A){throw new Error(`ipaddr: the address does not have IPv6 CIDR format (${A})`)}},b.IPv6.parse=function(y){let x=this.parser(y);if(x.parts===null)throw new Error("ipaddr: string is not formatted like an IPv6 Address");return new this(x.parts,x.zoneId)},b.IPv6.parseCIDR=function(y){let x,P,k;if((P=y.match(/^(.+)\/(\d+)$/))&&(x=parseInt(P[2]),x>=0&&x<=128))return k=[this.parse(P[1]),x],Object.defineProperty(k,"toString",{value:function(){return this.join("/")}}),k;throw new Error("ipaddr: string is not formatted like an IPv6 CIDR range")},b.IPv6.parser=function(y){let x,P,k,S,T,A;if(k=y.match(d.deprecatedTransitional))return this.parser(`::ffff:${k[1]}`);if(d.native.test(y))return m(y,8);if((k=y.match(d.transitional))&&(A=k[6]||"",x=k[1],k[1].endsWith("::")||(x=x.slice(0,-1)),x=m(x+A,6),x.parts)){for(T=[parseInt(k[2]),parseInt(k[3]),parseInt(k[4]),parseInt(k[5])],P=0;P128)throw new Error("ipaddr: invalid IPv6 prefix length");let x=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],P=0,k=Math.floor(y/8);for(;P{"use strict";(function(){var n=typeof p4<"u"&&p4||typeof define<"u"&&{}||this||window;typeof define<"u"&&define("save-svg-as-png",[],function(){return n}),n.default=n;var o="http://www.w3.org/2000/xmlns/",e="http://www.w3.org/1999/xhtml",t="http://www.w3.org/2000/svg",i=']>',r=/url\(["']?(.+?)["']?\)/,s={woff2:"font/woff2",woff:"font/woff",otf:"application/x-font-opentype",ttf:"application/x-font-ttf",eot:"application/vnd.ms-fontobject",sfnt:"application/font-sfnt",svg:"image/svg+xml"},d=function(G){return G instanceof HTMLElement||G instanceof SVGElement},m=function(G){if(!d(G))throw new Error("an HTMLElement or SVGElement is required; got "+G)},u=function(G){return new Promise(function(Ce,we){d(G)?Ce(G):we(new Error("an HTMLElement or SVGElement is required; got "+G))})},h=function(G){return G&&G.lastIndexOf("http",0)===0&&G.lastIndexOf(window.location.host)===-1},v=function(G){var Ce=Object.keys(s).filter(function(we){return G.indexOf("."+we)>0}).map(function(we){return s[we]});return Ce?Ce[0]:(console.error("Unknown font format for "+G+". Fonts may not be working correctly."),"application/octet-stream")},b=function(G){for(var Ce="",we=new Uint8Array(G),me=0;me"u"||me===null||isNaN(parseFloat(me))?0:me},x=function(G,Ce,we,me){if(G.tagName==="svg")return{width:we||y(G,Ce,"width"),height:me||y(G,Ce,"height")};if(G.getBBox){var Ie=G.getBBox(),ue=Ie.x,H=Ie.y,ee=Ie.width,le=Ie.height;return{width:ue+ee,height:H+le}}},P=function(G){return decodeURIComponent(encodeURIComponent(G).replace(/%([0-9A-F]{2})/g,function(Ce,we){var me=String.fromCharCode("0x"+we);return me==="%"?"%25":me}))},k=function(G){for(var Ce=window.atob(G.split(",")[1]),we=G.split(",")[0].split(":")[1].split(";")[0],me=new ArrayBuffer(Ce.length),Ie=new Uint8Array(me),ue=0;ue"u",je=H||[];return V().forEach(function(tt){var et=tt.rules,ye=tt.href;et&&Array.from(et).forEach(function(Y){if(typeof Y.style<"u")if(S(G,Y.selectorText))de.push(le(Y.selectorText,Y.style.cssText));else if(De&&Y.cssText.match(/^@font-face/)){var Re=T(Y,ye);Re&&je.push(Re)}else ee||de.push(Y.cssText)})}),F(je).then(function(tt){return de.join(` +`)+tt})},oe=function(){if(!navigator.msSaveOrOpenBlob&&!("download"in document.createElement("a")))return{popup:window.open()}};n.prepareSvg=function(W,G,Ce){m(W);var we=G||{},me=we.left,Ie=me===void 0?0:me,ue=we.top,H=ue===void 0?0:ue,ee=we.width,le=we.height,de=we.scale,De=de===void 0?1:de,je=we.responsive,tt=je===void 0?!1:je,et=we.excludeCss,ye=et===void 0?!1:et;return A(W).then(function(){var Y=W.cloneNode(!0);Y.style.backgroundColor=(G||{}).backgroundColor||W.style.backgroundColor;var Re=x(W,Y,ee,le),Ze=Re.width,ct=Re.height;if(W.tagName!=="svg")if(W.getBBox){Y.getAttribute("transform")!=null&&Y.setAttribute("transform",Y.getAttribute("transform").replace(/translate\(.*?\)/,""));var Tt=document.createElementNS("http://www.w3.org/2000/svg","svg");Tt.appendChild(Y),Y=Tt}else{console.error("Attempted to render non-SVG element",W);return}if(Y.setAttribute("version","1.1"),Y.setAttribute("viewBox",[Ie,H,Ze,ct].join(" ")),Y.getAttribute("xmlns")||Y.setAttributeNS(o,"xmlns",t),Y.getAttribute("xmlns:xlink")||Y.setAttributeNS(o,"xmlns:xlink","http://www.w3.org/1999/xlink"),tt?(Y.removeAttribute("width"),Y.removeAttribute("height"),Y.setAttribute("preserveAspectRatio","xMinYMin meet")):(Y.setAttribute("width",Ze*De),Y.setAttribute("height",ct*De)),Array.from(Y.querySelectorAll("foreignObject > *")).forEach(function(bo){bo.setAttributeNS(o,"xmlns",bo.tagName==="svg"?t:e)}),ye){var ri=document.createElement("div");ri.appendChild(Y);var Lo=ri.innerHTML;if(typeof Ce=="function")Ce(Lo,Ze,ct);else return{src:Lo,width:Ze,height:ct}}else return ae(W,G).then(function(bo){var qr=document.createElement("style");qr.setAttribute("type","text/css"),qr.innerHTML=``;var Hn=document.createElement("defs");Hn.appendChild(qr),Y.insertBefore(Hn,Y.firstChild);var ep=document.createElement("div");ep.appendChild(Y);var Ku=ep.innerHTML.replace(/NS\d+:href/gi,'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href');if(typeof Ce=="function")Ce(Ku,Ze,ct);else return{src:Ku,width:Ze,height:ct}})})},n.svgAsDataUri=function(W,G,Ce){return m(W),n.prepareSvg(W,G).then(function(we){var me=we.src,Ie=we.width,ue=we.height,H="data:image/svg+xml;base64,"+window.btoa(P(i+me));return typeof Ce=="function"&&Ce(H,Ie,ue),H})},n.svgAsPngUri=function(W,G,Ce){m(W);var we=G||{},me=we.encoderType,Ie=me===void 0?"image/png":me,ue=we.encoderOptions,H=ue===void 0?.8:ue,ee=we.canvg,le=function(De){var je=De.src,tt=De.width,et=De.height,ye=document.createElement("canvas"),Y=ye.getContext("2d"),Re=window.devicePixelRatio||1;ye.width=tt*Re,ye.height=et*Re,ye.style.width=ye.width+"px",ye.style.height=ye.height+"px",Y.setTransform(Re,0,0,Re,0,0),ee?ee(ye,je):Y.drawImage(je,0,0);var Ze=void 0;try{Ze=ye.toDataURL(Ie,H)}catch(ct){if(typeof SecurityError<"u"&&ct instanceof SecurityError||ct.name==="SecurityError"){console.error("Rendered SVG images cannot be downloaded in this browser.");return}else throw ct}return typeof Ce=="function"&&Ce(Ze,ye.width,ye.height),Promise.resolve(Ze)};return ee?n.prepareSvg(W,G).then(le):n.svgAsDataUri(W,G).then(function(de){return new Promise(function(De,je){var tt=new Image;tt.onload=function(){return De(le({src:tt,width:tt.width,height:tt.height}))},tt.onerror=function(){je(`There was an error loading the data URI as an image on the following SVG +`+window.atob(de.slice(26))+`Open the following link to see browser's diagnosis +`+de)},tt.src=de})})},n.download=function(W,G,Ce){if(navigator.msSaveOrOpenBlob)navigator.msSaveOrOpenBlob(k(G),W);else{var we=document.createElement("a");if("download"in we){we.download=W,we.style.display="none",document.body.appendChild(we);try{var me=k(G),Ie=URL.createObjectURL(me);we.href=Ie,we.onclick=function(){return requestAnimationFrame(function(){return URL.revokeObjectURL(Ie)})}}catch(ue){console.error(ue),console.warn("Error while getting object URL. Falling back to string URL."),we.href=G}we.click(),document.body.removeChild(we)}else Ce&&Ce.popup&&(Ce.popup.document.title=W,Ce.popup.location.replace(G))}},n.saveSvg=function(W,G,Ce){var we=oe();return u(W).then(function(me){return n.svgAsDataUri(me,Ce||{})}).then(function(me){return n.download(G,me,we)})},n.saveSvgAsPng=function(W,G,Ce){var we=oe();return u(W).then(function(me){return n.svgAsPngUri(me,Ce||{})}).then(function(me){return n.download(G,me,we)})}})()});var _4=Vs((e2,h4)=>{(function(n,o){if(typeof e2=="object"&&typeof h4=="object")h4.exports=o();else if(typeof define=="function"&&define.amd)define([],o);else{var e=o();for(var t in e)(typeof e2=="object"?e2:n)[t]=e[t]}})(globalThis,()=>(()=>{"use strict";var n={4567:function(i,r,s){var d=this&&this.__decorate||function(k,S,T,A){var D,F=arguments.length,I=F<3?S:A===null?A=Object.getOwnPropertyDescriptor(S,T):A;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")I=Reflect.decorate(k,S,T,A);else for(var V=k.length-1;V>=0;V--)(D=k[V])&&(I=(F<3?D(I):F>3?D(S,T,I):D(S,T))||I);return F>3&&I&&Object.defineProperty(S,T,I),I},m=this&&this.__param||function(k,S){return function(T,A){S(T,A,k)}};Object.defineProperty(r,"__esModule",{value:!0}),r.AccessibilityManager=void 0;let u=s(9042),h=s(9924),v=s(844),b=s(4725),y=s(2585),x=s(3656),P=r.AccessibilityManager=class extends v.Disposable{constructor(k,S,T,A){super(),this._terminal=k,this._coreBrowserService=T,this._renderService=A,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let D=0;Dthis._handleBoundaryFocus(D,0),this._bottomBoundaryFocusListener=D=>this._handleBoundaryFocus(D,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new h.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize(D=>this._handleResize(D.rows))),this.register(this._terminal.onRender(D=>this._refreshRows(D.start,D.end))),this.register(this._terminal.onScroll(()=>this._refreshRows())),this.register(this._terminal.onA11yChar(D=>this._handleChar(D))),this.register(this._terminal.onLineFeed(()=>this._handleChar(` +`))),this.register(this._terminal.onA11yTab(D=>this._handleTab(D))),this.register(this._terminal.onKey(D=>this._handleKey(D.key))),this.register(this._terminal.onBlur(()=>this._clearLiveRegion())),this.register(this._renderService.onDimensionsChange(()=>this._refreshRowsDimensions())),this.register((0,x.addDisposableDomListener)(document,"selectionchange",()=>this._handleSelectionChange())),this.register(this._coreBrowserService.onDprChange(()=>this._refreshRowsDimensions())),this._refreshRows(),this.register((0,v.toDisposable)(()=>{this._accessibilityContainer.remove(),this._rowElements.length=0}))}_handleTab(k){for(let S=0;S0?this._charsToConsume.shift()!==k&&(this._charsToAnnounce+=k):this._charsToAnnounce+=k,k===` +`&&(this._liveRegionLineCount++,this._liveRegionLineCount===21&&(this._liveRegion.textContent+=u.tooMuchOutput)))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0}_handleKey(k){this._clearLiveRegion(),new RegExp("\\p{Control}","u").test(k)||this._charsToConsume.push(k)}_refreshRows(k,S){this._liveRegionDebouncer.refresh(k,S,this._terminal.rows)}_renderRows(k,S){let T=this._terminal.buffer,A=T.lines.length.toString();for(let D=k;D<=S;D++){let F=T.lines.get(T.ydisp+D),I=[],V=F?.translateToString(!0,void 0,void 0,I)||"",ae=(T.ydisp+D+1).toString(),oe=this._rowElements[D];oe&&(V.length===0?(oe.innerText="\xA0",this._rowColumns.set(oe,[0,1])):(oe.textContent=V,this._rowColumns.set(oe,I)),oe.setAttribute("aria-posinset",ae),oe.setAttribute("aria-setsize",A))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(k,S){let T=k.target,A=this._rowElements[S===0?1:this._rowElements.length-2];if(T.getAttribute("aria-posinset")===(S===0?"1":`${this._terminal.buffer.lines.length}`)||k.relatedTarget!==A)return;let D,F;if(S===0?(D=T,F=this._rowElements.pop(),this._rowContainer.removeChild(F)):(D=this._rowElements.shift(),F=T,this._rowContainer.removeChild(D)),D.removeEventListener("focus",this._topBoundaryFocusListener),F.removeEventListener("focus",this._bottomBoundaryFocusListener),S===0){let I=this._createAccessibilityTreeNode();this._rowElements.unshift(I),this._rowContainer.insertAdjacentElement("afterbegin",I)}else{let I=this._createAccessibilityTreeNode();this._rowElements.push(I),this._rowContainer.appendChild(I)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(S===0?-1:1),this._rowElements[S===0?1:this._rowElements.length-2].focus(),k.preventDefault(),k.stopImmediatePropagation()}_handleSelectionChange(){if(this._rowElements.length===0)return;let k=document.getSelection();if(!k)return;if(k.isCollapsed)return void(this._rowContainer.contains(k.anchorNode)&&this._terminal.clearSelection());if(!k.anchorNode||!k.focusNode)return void console.error("anchorNode and/or focusNode are null");let S={node:k.anchorNode,offset:k.anchorOffset},T={node:k.focusNode,offset:k.focusOffset};if((S.node.compareDocumentPosition(T.node)&Node.DOCUMENT_POSITION_PRECEDING||S.node===T.node&&S.offset>T.offset)&&([S,T]=[T,S]),S.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(S={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(S.node))return;let A=this._rowElements.slice(-1)[0];if(T.node.compareDocumentPosition(A)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(T={node:A,offset:A.textContent?.length??0}),!this._rowContainer.contains(T.node))return;let D=({node:V,offset:ae})=>{let oe=V instanceof Text?V.parentNode:V,W=parseInt(oe?.getAttribute("aria-posinset"),10)-1;if(isNaN(W))return console.warn("row is invalid. Race condition?"),null;let G=this._rowColumns.get(oe);if(!G)return console.warn("columns is null. Race condition?"),null;let Ce=ae=this._terminal.cols&&(++W,Ce=0),{row:W,column:Ce}},F=D(S),I=D(T);if(F&&I){if(F.row>I.row||F.row===I.row&&F.column>=I.column)throw new Error("invalid range");this._terminal.select(F.column,F.row,(I.row-F.row)*this._terminal.cols-F.column+I.column)}}_handleResize(k){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let S=this._rowContainer.children.length;Sk;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){let k=this._coreBrowserService.mainDocument.createElement("div");return k.setAttribute("role","listitem"),k.tabIndex=-1,this._refreshRowDimensions(k),k}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=`${this._renderService.dimensions.css.canvas.width}px`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let k=0;k{function s(h){return h.replace(/\r?\n/g,"\r")}function d(h,v){return v?"\x1B[200~"+h+"\x1B[201~":h}function m(h,v,b,y){h=d(h=s(h),b.decPrivateModes.bracketedPasteMode&&y.rawOptions.ignoreBracketedPasteMode!==!0),b.triggerDataEvent(h,!0),v.value=""}function u(h,v,b){let y=b.getBoundingClientRect(),x=h.clientX-y.left-10,P=h.clientY-y.top-10;v.style.width="20px",v.style.height="20px",v.style.left=`${x}px`,v.style.top=`${P}px`,v.style.zIndex="1000",v.focus()}Object.defineProperty(r,"__esModule",{value:!0}),r.rightClickHandler=r.moveTextAreaUnderMouseCursor=r.paste=r.handlePasteEvent=r.copyHandler=r.bracketTextForPaste=r.prepareTextForTerminal=void 0,r.prepareTextForTerminal=s,r.bracketTextForPaste=d,r.copyHandler=function(h,v){h.clipboardData&&h.clipboardData.setData("text/plain",v.selectionText),h.preventDefault()},r.handlePasteEvent=function(h,v,b,y){h.stopPropagation(),h.clipboardData&&m(h.clipboardData.getData("text/plain"),v,b,y)},r.paste=m,r.moveTextAreaUnderMouseCursor=u,r.rightClickHandler=function(h,v,b,y,x){u(h,v,b),x&&y.rightClickSelect(h),v.value=y.selectionText,v.select()}},7239:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.ColorContrastCache=void 0;let d=s(1505);r.ColorContrastCache=class{constructor(){this._color=new d.TwoKeyMap,this._css=new d.TwoKeyMap}setCss(m,u,h){this._css.set(m,u,h)}getCss(m,u){return this._css.get(m,u)}setColor(m,u,h){this._color.set(m,u,h)}getColor(m,u){return this._color.get(m,u)}clear(){this._color.clear(),this._css.clear()}}},3656:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.addDisposableDomListener=void 0,r.addDisposableDomListener=function(s,d,m,u){s.addEventListener(d,m,u);let h=!1;return{dispose:()=>{h||(h=!0,s.removeEventListener(d,m,u))}}}},3551:function(i,r,s){var d=this&&this.__decorate||function(P,k,S,T){var A,D=arguments.length,F=D<3?k:T===null?T=Object.getOwnPropertyDescriptor(k,S):T;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")F=Reflect.decorate(P,k,S,T);else for(var I=P.length-1;I>=0;I--)(A=P[I])&&(F=(D<3?A(F):D>3?A(k,S,F):A(k,S))||F);return D>3&&F&&Object.defineProperty(k,S,F),F},m=this&&this.__param||function(P,k){return function(S,T){k(S,T,P)}};Object.defineProperty(r,"__esModule",{value:!0}),r.Linkifier=void 0;let u=s(3656),h=s(8460),v=s(844),b=s(2585),y=s(4725),x=r.Linkifier=class extends v.Disposable{get currentLink(){return this._currentLink}constructor(P,k,S,T,A){super(),this._element=P,this._mouseService=k,this._renderService=S,this._bufferService=T,this._linkProviderService=A,this._linkCacheDisposables=[],this._isMouseOut=!0,this._wasResized=!1,this._activeLine=-1,this._onShowLinkUnderline=this.register(new h.EventEmitter),this.onShowLinkUnderline=this._onShowLinkUnderline.event,this._onHideLinkUnderline=this.register(new h.EventEmitter),this.onHideLinkUnderline=this._onHideLinkUnderline.event,this.register((0,v.getDisposeArrayDisposable)(this._linkCacheDisposables)),this.register((0,v.toDisposable)(()=>{this._lastMouseEvent=void 0,this._activeProviderReplies?.clear()})),this.register(this._bufferService.onResize(()=>{this._clearCurrentLink(),this._wasResized=!0})),this.register((0,u.addDisposableDomListener)(this._element,"mouseleave",()=>{this._isMouseOut=!0,this._clearCurrentLink()})),this.register((0,u.addDisposableDomListener)(this._element,"mousemove",this._handleMouseMove.bind(this))),this.register((0,u.addDisposableDomListener)(this._element,"mousedown",this._handleMouseDown.bind(this))),this.register((0,u.addDisposableDomListener)(this._element,"mouseup",this._handleMouseUp.bind(this)))}_handleMouseMove(P){this._lastMouseEvent=P;let k=this._positionFromMouseEvent(P,this._element,this._mouseService);if(!k)return;this._isMouseOut=!1;let S=P.composedPath();for(let T=0;T{T?.forEach(A=>{A.link.dispose&&A.link.dispose()})}),this._activeProviderReplies=new Map,this._activeLine=P.y);let S=!1;for(let[T,A]of this._linkProviderService.linkProviders.entries())k?this._activeProviderReplies?.get(T)&&(S=this._checkLinkProviderResult(T,P,S)):A.provideLinks(P.y,D=>{if(this._isMouseOut)return;let F=D?.map(I=>({link:I}));this._activeProviderReplies?.set(T,F),S=this._checkLinkProviderResult(T,P,S),this._activeProviderReplies?.size===this._linkProviderService.linkProviders.length&&this._removeIntersectingLinks(P.y,this._activeProviderReplies)})}_removeIntersectingLinks(P,k){let S=new Set;for(let T=0;TP?this._bufferService.cols:F.link.range.end.x;for(let ae=I;ae<=V;ae++){if(S.has(ae)){A.splice(D--,1);break}S.add(ae)}}}}_checkLinkProviderResult(P,k,S){if(!this._activeProviderReplies)return S;let T=this._activeProviderReplies.get(P),A=!1;for(let D=0;Dthis._linkAtPosition(F.link,k));D&&(S=!0,this._handleNewLink(D))}if(this._activeProviderReplies.size===this._linkProviderService.linkProviders.length&&!S)for(let D=0;Dthis._linkAtPosition(I.link,k));if(F){S=!0,this._handleNewLink(F);break}}return S}_handleMouseDown(){this._mouseDownLink=this._currentLink}_handleMouseUp(P){if(!this._currentLink)return;let k=this._positionFromMouseEvent(P,this._element,this._mouseService);k&&this._mouseDownLink===this._currentLink&&this._linkAtPosition(this._currentLink.link,k)&&this._currentLink.link.activate(P,this._currentLink.link.text)}_clearCurrentLink(P,k){this._currentLink&&this._lastMouseEvent&&(!P||!k||this._currentLink.link.range.start.y>=P&&this._currentLink.link.range.end.y<=k)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,(0,v.disposeArray)(this._linkCacheDisposables))}_handleNewLink(P){if(!this._lastMouseEvent)return;let k=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);k&&this._linkAtPosition(P.link,k)&&(this._currentLink=P,this._currentLink.state={decorations:{underline:P.link.decorations===void 0||P.link.decorations.underline,pointerCursor:P.link.decorations===void 0||P.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,P.link,this._lastMouseEvent),P.link.decorations={},Object.defineProperties(P.link.decorations,{pointerCursor:{get:()=>this._currentLink?.state?.decorations.pointerCursor,set:S=>{this._currentLink?.state&&this._currentLink.state.decorations.pointerCursor!==S&&(this._currentLink.state.decorations.pointerCursor=S,this._currentLink.state.isHovered&&this._element.classList.toggle("xterm-cursor-pointer",S))}},underline:{get:()=>this._currentLink?.state?.decorations.underline,set:S=>{this._currentLink?.state&&this._currentLink?.state?.decorations.underline!==S&&(this._currentLink.state.decorations.underline=S,this._currentLink.state.isHovered&&this._fireUnderlineEvent(P.link,S))}}}),this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(S=>{if(!this._currentLink)return;let T=S.start===0?0:S.start+1+this._bufferService.buffer.ydisp,A=this._bufferService.buffer.ydisp+1+S.end;if(this._currentLink.link.range.start.y>=T&&this._currentLink.link.range.end.y<=A&&(this._clearCurrentLink(T,A),this._lastMouseEvent)){let D=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);D&&this._askForLink(D,!1)}})))}_linkHover(P,k,S){this._currentLink?.state&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(k,!0),this._currentLink.state.decorations.pointerCursor&&P.classList.add("xterm-cursor-pointer")),k.hover&&k.hover(S,k.text)}_fireUnderlineEvent(P,k){let S=P.range,T=this._bufferService.buffer.ydisp,A=this._createLinkUnderlineEvent(S.start.x-1,S.start.y-T-1,S.end.x,S.end.y-T-1,void 0);(k?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(A)}_linkLeave(P,k,S){this._currentLink?.state&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(k,!1),this._currentLink.state.decorations.pointerCursor&&P.classList.remove("xterm-cursor-pointer")),k.leave&&k.leave(S,k.text)}_linkAtPosition(P,k){let S=P.range.start.y*this._bufferService.cols+P.range.start.x,T=P.range.end.y*this._bufferService.cols+P.range.end.x,A=k.y*this._bufferService.cols+k.x;return S<=A&&A<=T}_positionFromMouseEvent(P,k,S){let T=S.getCoords(P,k,this._bufferService.cols,this._bufferService.rows);if(T)return{x:T[0],y:T[1]+this._bufferService.buffer.ydisp}}_createLinkUnderlineEvent(P,k,S,T,A){return{x1:P,y1:k,x2:S,y2:T,cols:this._bufferService.cols,fg:A}}};r.Linkifier=x=d([m(1,y.IMouseService),m(2,y.IRenderService),m(3,b.IBufferService),m(4,y.ILinkProviderService)],x)},9042:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.tooMuchOutput=r.promptLabel=void 0,r.promptLabel="Terminal input",r.tooMuchOutput="Too much output to announce, navigate to rows manually to read"},3730:function(i,r,s){var d=this&&this.__decorate||function(y,x,P,k){var S,T=arguments.length,A=T<3?x:k===null?k=Object.getOwnPropertyDescriptor(x,P):k;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")A=Reflect.decorate(y,x,P,k);else for(var D=y.length-1;D>=0;D--)(S=y[D])&&(A=(T<3?S(A):T>3?S(x,P,A):S(x,P))||A);return T>3&&A&&Object.defineProperty(x,P,A),A},m=this&&this.__param||function(y,x){return function(P,k){x(P,k,y)}};Object.defineProperty(r,"__esModule",{value:!0}),r.OscLinkProvider=void 0;let u=s(511),h=s(2585),v=r.OscLinkProvider=class{constructor(y,x,P){this._bufferService=y,this._optionsService=x,this._oscLinkService=P}provideLinks(y,x){let P=this._bufferService.buffer.lines.get(y-1);if(!P)return void x(void 0);let k=[],S=this._optionsService.rawOptions.linkHandler,T=new u.CellData,A=P.getTrimmedLength(),D=-1,F=-1,I=!1;for(let V=0;VS?S.activate(G,Ce,oe):b(0,Ce),hover:(G,Ce)=>S?.hover?.(G,Ce,oe),leave:(G,Ce)=>S?.leave?.(G,Ce,oe)})}I=!1,T.hasExtendedAttrs()&&T.extended.urlId?(F=V,D=T.extended.urlId):(F=-1,D=-1)}}x(k)}};function b(y,x){if(confirm(`Do you want to navigate to ${x}? + +WARNING: This link could potentially be dangerous`)){let P=window.open();if(P){try{P.opener=null}catch{}P.location.href=x}else console.warn("Opening link blocked as opener could not be cleared")}}r.OscLinkProvider=v=d([m(0,h.IBufferService),m(1,h.IOptionsService),m(2,h.IOscLinkService)],v)},6193:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.RenderDebouncer=void 0,r.RenderDebouncer=class{constructor(s,d){this._renderCallback=s,this._coreBrowserService=d,this._refreshCallbacks=[]}dispose(){this._animationFrame&&(this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)}addRefreshCallback(s){return this._refreshCallbacks.push(s),this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame(()=>this._innerRefresh())),this._animationFrame}refresh(s,d,m){this._rowCount=m,s=s!==void 0?s:0,d=d!==void 0?d:this._rowCount-1,this._rowStart=this._rowStart!==void 0?Math.min(this._rowStart,s):s,this._rowEnd=this._rowEnd!==void 0?Math.max(this._rowEnd,d):d,this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame(()=>this._innerRefresh()))}_innerRefresh(){if(this._animationFrame=void 0,this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0)return void this._runRefreshCallbacks();let s=Math.max(this._rowStart,0),d=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(s,d),this._runRefreshCallbacks()}_runRefreshCallbacks(){for(let s of this._refreshCallbacks)s(0);this._refreshCallbacks=[]}}},3236:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.Terminal=void 0;let d=s(3614),m=s(3656),u=s(3551),h=s(9042),v=s(3730),b=s(1680),y=s(3107),x=s(5744),P=s(2950),k=s(1296),S=s(428),T=s(4269),A=s(5114),D=s(8934),F=s(3230),I=s(9312),V=s(4725),ae=s(6731),oe=s(8055),W=s(8969),G=s(8460),Ce=s(844),we=s(6114),me=s(8437),Ie=s(2584),ue=s(7399),H=s(5941),ee=s(9074),le=s(2585),de=s(5435),De=s(4567),je=s(779);class tt extends W.CoreTerminal{get onFocus(){return this._onFocus.event}get onBlur(){return this._onBlur.event}get onA11yChar(){return this._onA11yCharEmitter.event}get onA11yTab(){return this._onA11yTabEmitter.event}get onWillOpen(){return this._onWillOpen.event}constructor(ye={}){super(ye),this.browser=we,this._keyDownHandled=!1,this._keyDownSeen=!1,this._keyPressHandled=!1,this._unprocessedDeadKey=!1,this._accessibilityManager=this.register(new Ce.MutableDisposable),this._onCursorMove=this.register(new G.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onKey=this.register(new G.EventEmitter),this.onKey=this._onKey.event,this._onRender=this.register(new G.EventEmitter),this.onRender=this._onRender.event,this._onSelectionChange=this.register(new G.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onTitleChange=this.register(new G.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onBell=this.register(new G.EventEmitter),this.onBell=this._onBell.event,this._onFocus=this.register(new G.EventEmitter),this._onBlur=this.register(new G.EventEmitter),this._onA11yCharEmitter=this.register(new G.EventEmitter),this._onA11yTabEmitter=this.register(new G.EventEmitter),this._onWillOpen=this.register(new G.EventEmitter),this._setup(),this._decorationService=this._instantiationService.createInstance(ee.DecorationService),this._instantiationService.setService(le.IDecorationService,this._decorationService),this._linkProviderService=this._instantiationService.createInstance(je.LinkProviderService),this._instantiationService.setService(V.ILinkProviderService,this._linkProviderService),this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(v.OscLinkProvider)),this.register(this._inputHandler.onRequestBell(()=>this._onBell.fire())),this.register(this._inputHandler.onRequestRefreshRows((Y,Re)=>this.refresh(Y,Re))),this.register(this._inputHandler.onRequestSendFocus(()=>this._reportFocus())),this.register(this._inputHandler.onRequestReset(()=>this.reset())),this.register(this._inputHandler.onRequestWindowsOptionsReport(Y=>this._reportWindowsOptions(Y))),this.register(this._inputHandler.onColor(Y=>this._handleColorEvent(Y))),this.register((0,G.forwardEvent)(this._inputHandler.onCursorMove,this._onCursorMove)),this.register((0,G.forwardEvent)(this._inputHandler.onTitleChange,this._onTitleChange)),this.register((0,G.forwardEvent)(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this.register((0,G.forwardEvent)(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this.register(this._bufferService.onResize(Y=>this._afterResize(Y.cols,Y.rows))),this.register((0,Ce.toDisposable)(()=>{this._customKeyEventHandler=void 0,this.element?.parentNode?.removeChild(this.element)}))}_handleColorEvent(ye){if(this._themeService)for(let Y of ye){let Re,Ze="";switch(Y.index){case 256:Re="foreground",Ze="10";break;case 257:Re="background",Ze="11";break;case 258:Re="cursor",Ze="12";break;default:Re="ansi",Ze="4;"+Y.index}switch(Y.type){case 0:let ct=oe.color.toColorRGB(Re==="ansi"?this._themeService.colors.ansi[Y.index]:this._themeService.colors[Re]);this.coreService.triggerDataEvent(`${Ie.C0.ESC}]${Ze};${(0,H.toRgbString)(ct)}${Ie.C1_ESCAPED.ST}`);break;case 1:if(Re==="ansi")this._themeService.modifyColors(Tt=>Tt.ansi[Y.index]=oe.channels.toColor(...Y.color));else{let Tt=Re;this._themeService.modifyColors(ri=>ri[Tt]=oe.channels.toColor(...Y.color))}break;case 2:this._themeService.restoreColor(Y.index)}}}_setup(){super._setup(),this._customKeyEventHandler=void 0}get buffer(){return this.buffers.active}focus(){this.textarea&&this.textarea.focus({preventScroll:!0})}_handleScreenReaderModeOptionChange(ye){ye?!this._accessibilityManager.value&&this._renderService&&(this._accessibilityManager.value=this._instantiationService.createInstance(De.AccessibilityManager,this)):this._accessibilityManager.clear()}_handleTextAreaFocus(ye){this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(Ie.C0.ESC+"[I"),this.element.classList.add("focus"),this._showCursor(),this._onFocus.fire()}blur(){return this.textarea?.blur()}_handleTextAreaBlur(){this.textarea.value="",this.refresh(this.buffer.y,this.buffer.y),this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(Ie.C0.ESC+"[O"),this.element.classList.remove("focus"),this._onBlur.fire()}_syncTextArea(){if(!this.textarea||!this.buffer.isCursorInViewport||this._compositionHelper.isComposing||!this._renderService)return;let ye=this.buffer.ybase+this.buffer.y,Y=this.buffer.lines.get(ye);if(!Y)return;let Re=Math.min(this.buffer.x,this.cols-1),Ze=this._renderService.dimensions.css.cell.height,ct=Y.getWidth(Re),Tt=this._renderService.dimensions.css.cell.width*ct,ri=this.buffer.y*this._renderService.dimensions.css.cell.height,Lo=Re*this._renderService.dimensions.css.cell.width;this.textarea.style.left=Lo+"px",this.textarea.style.top=ri+"px",this.textarea.style.width=Tt+"px",this.textarea.style.height=Ze+"px",this.textarea.style.lineHeight=Ze+"px",this.textarea.style.zIndex="-5"}_initGlobal(){this._bindKeys(),this.register((0,m.addDisposableDomListener)(this.element,"copy",Y=>{this.hasSelection()&&(0,d.copyHandler)(Y,this._selectionService)}));let ye=Y=>(0,d.handlePasteEvent)(Y,this.textarea,this.coreService,this.optionsService);this.register((0,m.addDisposableDomListener)(this.textarea,"paste",ye)),this.register((0,m.addDisposableDomListener)(this.element,"paste",ye)),we.isFirefox?this.register((0,m.addDisposableDomListener)(this.element,"mousedown",Y=>{Y.button===2&&(0,d.rightClickHandler)(Y,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)})):this.register((0,m.addDisposableDomListener)(this.element,"contextmenu",Y=>{(0,d.rightClickHandler)(Y,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)})),we.isLinux&&this.register((0,m.addDisposableDomListener)(this.element,"auxclick",Y=>{Y.button===1&&(0,d.moveTextAreaUnderMouseCursor)(Y,this.textarea,this.screenElement)}))}_bindKeys(){this.register((0,m.addDisposableDomListener)(this.textarea,"keyup",ye=>this._keyUp(ye),!0)),this.register((0,m.addDisposableDomListener)(this.textarea,"keydown",ye=>this._keyDown(ye),!0)),this.register((0,m.addDisposableDomListener)(this.textarea,"keypress",ye=>this._keyPress(ye),!0)),this.register((0,m.addDisposableDomListener)(this.textarea,"compositionstart",()=>this._compositionHelper.compositionstart())),this.register((0,m.addDisposableDomListener)(this.textarea,"compositionupdate",ye=>this._compositionHelper.compositionupdate(ye))),this.register((0,m.addDisposableDomListener)(this.textarea,"compositionend",()=>this._compositionHelper.compositionend())),this.register((0,m.addDisposableDomListener)(this.textarea,"input",ye=>this._inputEvent(ye),!0)),this.register(this.onRender(()=>this._compositionHelper.updateCompositionElements()))}open(ye){if(!ye)throw new Error("Terminal requires a parent element.");if(ye.isConnected||this._logService.debug("Terminal.open was called on an element that was not attached to the DOM"),this.element?.ownerDocument.defaultView&&this._coreBrowserService)return void(this.element.ownerDocument.defaultView!==this._coreBrowserService.window&&(this._coreBrowserService.window=this.element.ownerDocument.defaultView));this._document=ye.ownerDocument,this.options.documentOverride&&this.options.documentOverride instanceof Document&&(this._document=this.optionsService.rawOptions.documentOverride),this.element=this._document.createElement("div"),this.element.dir="ltr",this.element.classList.add("terminal"),this.element.classList.add("xterm"),ye.appendChild(this.element);let Y=this._document.createDocumentFragment();this._viewportElement=this._document.createElement("div"),this._viewportElement.classList.add("xterm-viewport"),Y.appendChild(this._viewportElement),this._viewportScrollArea=this._document.createElement("div"),this._viewportScrollArea.classList.add("xterm-scroll-area"),this._viewportElement.appendChild(this._viewportScrollArea),this.screenElement=this._document.createElement("div"),this.screenElement.classList.add("xterm-screen"),this.register((0,m.addDisposableDomListener)(this.screenElement,"mousemove",Re=>this.updateCursorStyle(Re))),this._helperContainer=this._document.createElement("div"),this._helperContainer.classList.add("xterm-helpers"),this.screenElement.appendChild(this._helperContainer),Y.appendChild(this.screenElement),this.textarea=this._document.createElement("textarea"),this.textarea.classList.add("xterm-helper-textarea"),this.textarea.setAttribute("aria-label",h.promptLabel),we.isChromeOS||this.textarea.setAttribute("aria-multiline","false"),this.textarea.setAttribute("autocorrect","off"),this.textarea.setAttribute("autocapitalize","off"),this.textarea.setAttribute("spellcheck","false"),this.textarea.tabIndex=0,this._coreBrowserService=this.register(this._instantiationService.createInstance(A.CoreBrowserService,this.textarea,ye.ownerDocument.defaultView??window,this._document??typeof window<"u"?window.document:null)),this._instantiationService.setService(V.ICoreBrowserService,this._coreBrowserService),this.register((0,m.addDisposableDomListener)(this.textarea,"focus",Re=>this._handleTextAreaFocus(Re))),this.register((0,m.addDisposableDomListener)(this.textarea,"blur",()=>this._handleTextAreaBlur())),this._helperContainer.appendChild(this.textarea),this._charSizeService=this._instantiationService.createInstance(S.CharSizeService,this._document,this._helperContainer),this._instantiationService.setService(V.ICharSizeService,this._charSizeService),this._themeService=this._instantiationService.createInstance(ae.ThemeService),this._instantiationService.setService(V.IThemeService,this._themeService),this._characterJoinerService=this._instantiationService.createInstance(T.CharacterJoinerService),this._instantiationService.setService(V.ICharacterJoinerService,this._characterJoinerService),this._renderService=this.register(this._instantiationService.createInstance(F.RenderService,this.rows,this.screenElement)),this._instantiationService.setService(V.IRenderService,this._renderService),this.register(this._renderService.onRenderedViewportChange(Re=>this._onRender.fire(Re))),this.onResize(Re=>this._renderService.resize(Re.cols,Re.rows)),this._compositionView=this._document.createElement("div"),this._compositionView.classList.add("composition-view"),this._compositionHelper=this._instantiationService.createInstance(P.CompositionHelper,this.textarea,this._compositionView),this._helperContainer.appendChild(this._compositionView),this._mouseService=this._instantiationService.createInstance(D.MouseService),this._instantiationService.setService(V.IMouseService,this._mouseService),this.linkifier=this.register(this._instantiationService.createInstance(u.Linkifier,this.screenElement)),this.element.appendChild(Y);try{this._onWillOpen.fire(this.element)}catch{}this._renderService.hasRenderer()||this._renderService.setRenderer(this._createRenderer()),this.viewport=this._instantiationService.createInstance(b.Viewport,this._viewportElement,this._viewportScrollArea),this.viewport.onRequestScrollLines(Re=>this.scrollLines(Re.amount,Re.suppressScrollEvent,1)),this.register(this._inputHandler.onRequestSyncScrollBar(()=>this.viewport.syncScrollArea())),this.register(this.viewport),this.register(this.onCursorMove(()=>{this._renderService.handleCursorMove(),this._syncTextArea()})),this.register(this.onResize(()=>this._renderService.handleResize(this.cols,this.rows))),this.register(this.onBlur(()=>this._renderService.handleBlur())),this.register(this.onFocus(()=>this._renderService.handleFocus())),this.register(this._renderService.onDimensionsChange(()=>this.viewport.syncScrollArea())),this._selectionService=this.register(this._instantiationService.createInstance(I.SelectionService,this.element,this.screenElement,this.linkifier)),this._instantiationService.setService(V.ISelectionService,this._selectionService),this.register(this._selectionService.onRequestScrollLines(Re=>this.scrollLines(Re.amount,Re.suppressScrollEvent))),this.register(this._selectionService.onSelectionChange(()=>this._onSelectionChange.fire())),this.register(this._selectionService.onRequestRedraw(Re=>this._renderService.handleSelectionChanged(Re.start,Re.end,Re.columnSelectMode))),this.register(this._selectionService.onLinuxMouseSelection(Re=>{this.textarea.value=Re,this.textarea.focus(),this.textarea.select()})),this.register(this._onScroll.event(Re=>{this.viewport.syncScrollArea(),this._selectionService.refresh()})),this.register((0,m.addDisposableDomListener)(this._viewportElement,"scroll",()=>this._selectionService.refresh())),this.register(this._instantiationService.createInstance(y.BufferDecorationRenderer,this.screenElement)),this.register((0,m.addDisposableDomListener)(this.element,"mousedown",Re=>this._selectionService.handleMouseDown(Re))),this.coreMouseService.areMouseEventsActive?(this._selectionService.disable(),this.element.classList.add("enable-mouse-events")):this._selectionService.enable(),this.options.screenReaderMode&&(this._accessibilityManager.value=this._instantiationService.createInstance(De.AccessibilityManager,this)),this.register(this.optionsService.onSpecificOptionChange("screenReaderMode",Re=>this._handleScreenReaderModeOptionChange(Re))),this.options.overviewRulerWidth&&(this._overviewRulerRenderer=this.register(this._instantiationService.createInstance(x.OverviewRulerRenderer,this._viewportElement,this.screenElement))),this.optionsService.onSpecificOptionChange("overviewRulerWidth",Re=>{!this._overviewRulerRenderer&&Re&&this._viewportElement&&this.screenElement&&(this._overviewRulerRenderer=this.register(this._instantiationService.createInstance(x.OverviewRulerRenderer,this._viewportElement,this.screenElement)))}),this._charSizeService.measure(),this.refresh(0,this.rows-1),this._initGlobal(),this.bindMouse()}_createRenderer(){return this._instantiationService.createInstance(k.DomRenderer,this,this._document,this.element,this.screenElement,this._viewportElement,this._helperContainer,this.linkifier)}bindMouse(){let ye=this,Y=this.element;function Re(Tt){let ri=ye._mouseService.getMouseReportCoords(Tt,ye.screenElement);if(!ri)return!1;let Lo,bo;switch(Tt.overrideType||Tt.type){case"mousemove":bo=32,Tt.buttons===void 0?(Lo=3,Tt.button!==void 0&&(Lo=Tt.button<3?Tt.button:3)):Lo=1&Tt.buttons?0:4&Tt.buttons?1:2&Tt.buttons?2:3;break;case"mouseup":bo=0,Lo=Tt.button<3?Tt.button:3;break;case"mousedown":bo=1,Lo=Tt.button<3?Tt.button:3;break;case"wheel":if(ye._customWheelEventHandler&&ye._customWheelEventHandler(Tt)===!1||ye.viewport.getLinesScrolled(Tt)===0)return!1;bo=Tt.deltaY<0?0:1,Lo=4;break;default:return!1}return!(bo===void 0||Lo===void 0||Lo>4)&&ye.coreMouseService.triggerMouseEvent({col:ri.col,row:ri.row,x:ri.x,y:ri.y,button:Lo,action:bo,ctrl:Tt.ctrlKey,alt:Tt.altKey,shift:Tt.shiftKey})}let Ze={mouseup:null,wheel:null,mousedrag:null,mousemove:null},ct={mouseup:Tt=>(Re(Tt),Tt.buttons||(this._document.removeEventListener("mouseup",Ze.mouseup),Ze.mousedrag&&this._document.removeEventListener("mousemove",Ze.mousedrag)),this.cancel(Tt)),wheel:Tt=>(Re(Tt),this.cancel(Tt,!0)),mousedrag:Tt=>{Tt.buttons&&Re(Tt)},mousemove:Tt=>{Tt.buttons||Re(Tt)}};this.register(this.coreMouseService.onProtocolChange(Tt=>{Tt?(this.optionsService.rawOptions.logLevel==="debug"&&this._logService.debug("Binding to mouse events:",this.coreMouseService.explainEvents(Tt)),this.element.classList.add("enable-mouse-events"),this._selectionService.disable()):(this._logService.debug("Unbinding from mouse events."),this.element.classList.remove("enable-mouse-events"),this._selectionService.enable()),8&Tt?Ze.mousemove||(Y.addEventListener("mousemove",ct.mousemove),Ze.mousemove=ct.mousemove):(Y.removeEventListener("mousemove",Ze.mousemove),Ze.mousemove=null),16&Tt?Ze.wheel||(Y.addEventListener("wheel",ct.wheel,{passive:!1}),Ze.wheel=ct.wheel):(Y.removeEventListener("wheel",Ze.wheel),Ze.wheel=null),2&Tt?Ze.mouseup||(Ze.mouseup=ct.mouseup):(this._document.removeEventListener("mouseup",Ze.mouseup),Ze.mouseup=null),4&Tt?Ze.mousedrag||(Ze.mousedrag=ct.mousedrag):(this._document.removeEventListener("mousemove",Ze.mousedrag),Ze.mousedrag=null)})),this.coreMouseService.activeProtocol=this.coreMouseService.activeProtocol,this.register((0,m.addDisposableDomListener)(Y,"mousedown",Tt=>{if(Tt.preventDefault(),this.focus(),this.coreMouseService.areMouseEventsActive&&!this._selectionService.shouldForceSelection(Tt))return Re(Tt),Ze.mouseup&&this._document.addEventListener("mouseup",Ze.mouseup),Ze.mousedrag&&this._document.addEventListener("mousemove",Ze.mousedrag),this.cancel(Tt)})),this.register((0,m.addDisposableDomListener)(Y,"wheel",Tt=>{if(!Ze.wheel){if(this._customWheelEventHandler&&this._customWheelEventHandler(Tt)===!1)return!1;if(!this.buffer.hasScrollback){let ri=this.viewport.getLinesScrolled(Tt);if(ri===0)return;let Lo=Ie.C0.ESC+(this.coreService.decPrivateModes.applicationCursorKeys?"O":"[")+(Tt.deltaY<0?"A":"B"),bo="";for(let qr=0;qr{if(!this.coreMouseService.areMouseEventsActive)return this.viewport.handleTouchStart(Tt),this.cancel(Tt)},{passive:!0})),this.register((0,m.addDisposableDomListener)(Y,"touchmove",Tt=>{if(!this.coreMouseService.areMouseEventsActive)return this.viewport.handleTouchMove(Tt)?void 0:this.cancel(Tt)},{passive:!1}))}refresh(ye,Y){this._renderService?.refreshRows(ye,Y)}updateCursorStyle(ye){this._selectionService?.shouldColumnSelect(ye)?this.element.classList.add("column-select"):this.element.classList.remove("column-select")}_showCursor(){this.coreService.isCursorInitialized||(this.coreService.isCursorInitialized=!0,this.refresh(this.buffer.y,this.buffer.y))}scrollLines(ye,Y,Re=0){Re===1?(super.scrollLines(ye,Y,Re),this.refresh(0,this.rows-1)):this.viewport?.scrollLines(ye)}paste(ye){(0,d.paste)(ye,this.textarea,this.coreService,this.optionsService)}attachCustomKeyEventHandler(ye){this._customKeyEventHandler=ye}attachCustomWheelEventHandler(ye){this._customWheelEventHandler=ye}registerLinkProvider(ye){return this._linkProviderService.registerLinkProvider(ye)}registerCharacterJoiner(ye){if(!this._characterJoinerService)throw new Error("Terminal must be opened first");let Y=this._characterJoinerService.register(ye);return this.refresh(0,this.rows-1),Y}deregisterCharacterJoiner(ye){if(!this._characterJoinerService)throw new Error("Terminal must be opened first");this._characterJoinerService.deregister(ye)&&this.refresh(0,this.rows-1)}get markers(){return this.buffer.markers}registerMarker(ye){return this.buffer.addMarker(this.buffer.ybase+this.buffer.y+ye)}registerDecoration(ye){return this._decorationService.registerDecoration(ye)}hasSelection(){return!!this._selectionService&&this._selectionService.hasSelection}select(ye,Y,Re){this._selectionService.setSelection(ye,Y,Re)}getSelection(){return this._selectionService?this._selectionService.selectionText:""}getSelectionPosition(){if(this._selectionService&&this._selectionService.hasSelection)return{start:{x:this._selectionService.selectionStart[0],y:this._selectionService.selectionStart[1]},end:{x:this._selectionService.selectionEnd[0],y:this._selectionService.selectionEnd[1]}}}clearSelection(){this._selectionService?.clearSelection()}selectAll(){this._selectionService?.selectAll()}selectLines(ye,Y){this._selectionService?.selectLines(ye,Y)}_keyDown(ye){if(this._keyDownHandled=!1,this._keyDownSeen=!0,this._customKeyEventHandler&&this._customKeyEventHandler(ye)===!1)return!1;let Y=this.browser.isMac&&this.options.macOptionIsMeta&&ye.altKey;if(!Y&&!this._compositionHelper.keydown(ye))return this.options.scrollOnUserInput&&this.buffer.ybase!==this.buffer.ydisp&&this.scrollToBottom(),!1;Y||ye.key!=="Dead"&&ye.key!=="AltGraph"||(this._unprocessedDeadKey=!0);let Re=(0,ue.evaluateKeyboardEvent)(ye,this.coreService.decPrivateModes.applicationCursorKeys,this.browser.isMac,this.options.macOptionIsMeta);if(this.updateCursorStyle(ye),Re.type===3||Re.type===2){let Ze=this.rows-1;return this.scrollLines(Re.type===2?-Ze:Ze),this.cancel(ye,!0)}return Re.type===1&&this.selectAll(),!!this._isThirdLevelShift(this.browser,ye)||(Re.cancel&&this.cancel(ye,!0),!Re.key||!!(ye.key&&!ye.ctrlKey&&!ye.altKey&&!ye.metaKey&&ye.key.length===1&&ye.key.charCodeAt(0)>=65&&ye.key.charCodeAt(0)<=90)||(this._unprocessedDeadKey?(this._unprocessedDeadKey=!1,!0):(Re.key!==Ie.C0.ETX&&Re.key!==Ie.C0.CR||(this.textarea.value=""),this._onKey.fire({key:Re.key,domEvent:ye}),this._showCursor(),this.coreService.triggerDataEvent(Re.key,!0),!this.optionsService.rawOptions.screenReaderMode||ye.altKey||ye.ctrlKey?this.cancel(ye,!0):void(this._keyDownHandled=!0))))}_isThirdLevelShift(ye,Y){let Re=ye.isMac&&!this.options.macOptionIsMeta&&Y.altKey&&!Y.ctrlKey&&!Y.metaKey||ye.isWindows&&Y.altKey&&Y.ctrlKey&&!Y.metaKey||ye.isWindows&&Y.getModifierState("AltGraph");return Y.type==="keypress"?Re:Re&&(!Y.keyCode||Y.keyCode>47)}_keyUp(ye){this._keyDownSeen=!1,this._customKeyEventHandler&&this._customKeyEventHandler(ye)===!1||((function(Y){return Y.keyCode===16||Y.keyCode===17||Y.keyCode===18})(ye)||this.focus(),this.updateCursorStyle(ye),this._keyPressHandled=!1)}_keyPress(ye){let Y;if(this._keyPressHandled=!1,this._keyDownHandled||this._customKeyEventHandler&&this._customKeyEventHandler(ye)===!1)return!1;if(this.cancel(ye),ye.charCode)Y=ye.charCode;else if(ye.which===null||ye.which===void 0)Y=ye.keyCode;else{if(ye.which===0||ye.charCode===0)return!1;Y=ye.which}return!(!Y||(ye.altKey||ye.ctrlKey||ye.metaKey)&&!this._isThirdLevelShift(this.browser,ye)||(Y=String.fromCharCode(Y),this._onKey.fire({key:Y,domEvent:ye}),this._showCursor(),this.coreService.triggerDataEvent(Y,!0),this._keyPressHandled=!0,this._unprocessedDeadKey=!1,0))}_inputEvent(ye){if(ye.data&&ye.inputType==="insertText"&&(!ye.composed||!this._keyDownSeen)&&!this.optionsService.rawOptions.screenReaderMode){if(this._keyPressHandled)return!1;this._unprocessedDeadKey=!1;let Y=ye.data;return this.coreService.triggerDataEvent(Y,!0),this.cancel(ye),!0}return!1}resize(ye,Y){ye!==this.cols||Y!==this.rows?super.resize(ye,Y):this._charSizeService&&!this._charSizeService.hasValidSize&&this._charSizeService.measure()}_afterResize(ye,Y){this._charSizeService?.measure(),this.viewport?.syncScrollArea(!0)}clear(){if(this.buffer.ybase!==0||this.buffer.y!==0){this.buffer.clearAllMarkers(),this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(let ye=1;ye{Object.defineProperty(r,"__esModule",{value:!0}),r.TimeBasedDebouncer=void 0,r.TimeBasedDebouncer=class{constructor(s,d=1e3){this._renderCallback=s,this._debounceThresholdMS=d,this._lastRefreshMs=0,this._additionalRefreshRequested=!1}dispose(){this._refreshTimeoutID&&clearTimeout(this._refreshTimeoutID)}refresh(s,d,m){this._rowCount=m,s=s!==void 0?s:0,d=d!==void 0?d:this._rowCount-1,this._rowStart=this._rowStart!==void 0?Math.min(this._rowStart,s):s,this._rowEnd=this._rowEnd!==void 0?Math.max(this._rowEnd,d):d;let u=Date.now();if(u-this._lastRefreshMs>=this._debounceThresholdMS)this._lastRefreshMs=u,this._innerRefresh();else if(!this._additionalRefreshRequested){let h=u-this._lastRefreshMs,v=this._debounceThresholdMS-h;this._additionalRefreshRequested=!0,this._refreshTimeoutID=window.setTimeout(()=>{this._lastRefreshMs=Date.now(),this._innerRefresh(),this._additionalRefreshRequested=!1,this._refreshTimeoutID=void 0},v)}}_innerRefresh(){if(this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0)return;let s=Math.max(this._rowStart,0),d=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(s,d)}}},1680:function(i,r,s){var d=this&&this.__decorate||function(P,k,S,T){var A,D=arguments.length,F=D<3?k:T===null?T=Object.getOwnPropertyDescriptor(k,S):T;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")F=Reflect.decorate(P,k,S,T);else for(var I=P.length-1;I>=0;I--)(A=P[I])&&(F=(D<3?A(F):D>3?A(k,S,F):A(k,S))||F);return D>3&&F&&Object.defineProperty(k,S,F),F},m=this&&this.__param||function(P,k){return function(S,T){k(S,T,P)}};Object.defineProperty(r,"__esModule",{value:!0}),r.Viewport=void 0;let u=s(3656),h=s(4725),v=s(8460),b=s(844),y=s(2585),x=r.Viewport=class extends b.Disposable{constructor(P,k,S,T,A,D,F,I){super(),this._viewportElement=P,this._scrollArea=k,this._bufferService=S,this._optionsService=T,this._charSizeService=A,this._renderService=D,this._coreBrowserService=F,this.scrollBarWidth=0,this._currentRowHeight=0,this._currentDeviceCellHeight=0,this._lastRecordedBufferLength=0,this._lastRecordedViewportHeight=0,this._lastRecordedBufferHeight=0,this._lastTouchY=0,this._lastScrollTop=0,this._wheelPartialScroll=0,this._refreshAnimationFrame=null,this._ignoreNextScrollEvent=!1,this._smoothScrollState={startTime:0,origin:-1,target:-1},this._onRequestScrollLines=this.register(new v.EventEmitter),this.onRequestScrollLines=this._onRequestScrollLines.event,this.scrollBarWidth=this._viewportElement.offsetWidth-this._scrollArea.offsetWidth||15,this.register((0,u.addDisposableDomListener)(this._viewportElement,"scroll",this._handleScroll.bind(this))),this._activeBuffer=this._bufferService.buffer,this.register(this._bufferService.buffers.onBufferActivate(V=>this._activeBuffer=V.activeBuffer)),this._renderDimensions=this._renderService.dimensions,this.register(this._renderService.onDimensionsChange(V=>this._renderDimensions=V)),this._handleThemeChange(I.colors),this.register(I.onChangeColors(V=>this._handleThemeChange(V))),this.register(this._optionsService.onSpecificOptionChange("scrollback",()=>this.syncScrollArea())),setTimeout(()=>this.syncScrollArea())}_handleThemeChange(P){this._viewportElement.style.backgroundColor=P.background.css}reset(){this._currentRowHeight=0,this._currentDeviceCellHeight=0,this._lastRecordedBufferLength=0,this._lastRecordedViewportHeight=0,this._lastRecordedBufferHeight=0,this._lastTouchY=0,this._lastScrollTop=0,this._coreBrowserService.window.requestAnimationFrame(()=>this.syncScrollArea())}_refresh(P){if(P)return this._innerRefresh(),void(this._refreshAnimationFrame!==null&&this._coreBrowserService.window.cancelAnimationFrame(this._refreshAnimationFrame));this._refreshAnimationFrame===null&&(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame(()=>this._innerRefresh()))}_innerRefresh(){if(this._charSizeService.height>0){this._currentRowHeight=this._renderDimensions.device.cell.height/this._coreBrowserService.dpr,this._currentDeviceCellHeight=this._renderDimensions.device.cell.height,this._lastRecordedViewportHeight=this._viewportElement.offsetHeight;let k=Math.round(this._currentRowHeight*this._lastRecordedBufferLength)+(this._lastRecordedViewportHeight-this._renderDimensions.css.canvas.height);this._lastRecordedBufferHeight!==k&&(this._lastRecordedBufferHeight=k,this._scrollArea.style.height=this._lastRecordedBufferHeight+"px")}let P=this._bufferService.buffer.ydisp*this._currentRowHeight;this._viewportElement.scrollTop!==P&&(this._ignoreNextScrollEvent=!0,this._viewportElement.scrollTop=P),this._refreshAnimationFrame=null}syncScrollArea(P=!1){if(this._lastRecordedBufferLength!==this._bufferService.buffer.lines.length)return this._lastRecordedBufferLength=this._bufferService.buffer.lines.length,void this._refresh(P);this._lastRecordedViewportHeight===this._renderService.dimensions.css.canvas.height&&this._lastScrollTop===this._activeBuffer.ydisp*this._currentRowHeight&&this._renderDimensions.device.cell.height===this._currentDeviceCellHeight||this._refresh(P)}_handleScroll(P){if(this._lastScrollTop=this._viewportElement.scrollTop,!this._viewportElement.offsetParent)return;if(this._ignoreNextScrollEvent)return this._ignoreNextScrollEvent=!1,void this._onRequestScrollLines.fire({amount:0,suppressScrollEvent:!0});let k=Math.round(this._lastScrollTop/this._currentRowHeight)-this._bufferService.buffer.ydisp;this._onRequestScrollLines.fire({amount:k,suppressScrollEvent:!0})}_smoothScroll(){if(this._isDisposed||this._smoothScrollState.origin===-1||this._smoothScrollState.target===-1)return;let P=this._smoothScrollPercent();this._viewportElement.scrollTop=this._smoothScrollState.origin+Math.round(P*(this._smoothScrollState.target-this._smoothScrollState.origin)),P<1?this._coreBrowserService.window.requestAnimationFrame(()=>this._smoothScroll()):this._clearSmoothScrollState()}_smoothScrollPercent(){return this._optionsService.rawOptions.smoothScrollDuration&&this._smoothScrollState.startTime?Math.max(Math.min((Date.now()-this._smoothScrollState.startTime)/this._optionsService.rawOptions.smoothScrollDuration,1),0):1}_clearSmoothScrollState(){this._smoothScrollState.startTime=0,this._smoothScrollState.origin=-1,this._smoothScrollState.target=-1}_bubbleScroll(P,k){let S=this._viewportElement.scrollTop+this._lastRecordedViewportHeight;return!(k<0&&this._viewportElement.scrollTop!==0||k>0&&S0&&(S=oe),T=""}}return{bufferElements:A,cursorElement:S}}getLinesScrolled(P){if(P.deltaY===0||P.shiftKey)return 0;let k=this._applyScrollModifier(P.deltaY,P);return P.deltaMode===WheelEvent.DOM_DELTA_PIXEL?(k/=this._currentRowHeight+0,this._wheelPartialScroll+=k,k=Math.floor(Math.abs(this._wheelPartialScroll))*(this._wheelPartialScroll>0?1:-1),this._wheelPartialScroll%=1):P.deltaMode===WheelEvent.DOM_DELTA_PAGE&&(k*=this._bufferService.rows),k}_applyScrollModifier(P,k){let S=this._optionsService.rawOptions.fastScrollModifier;return S==="alt"&&k.altKey||S==="ctrl"&&k.ctrlKey||S==="shift"&&k.shiftKey?P*this._optionsService.rawOptions.fastScrollSensitivity*this._optionsService.rawOptions.scrollSensitivity:P*this._optionsService.rawOptions.scrollSensitivity}handleTouchStart(P){this._lastTouchY=P.touches[0].pageY}handleTouchMove(P){let k=this._lastTouchY-P.touches[0].pageY;return this._lastTouchY=P.touches[0].pageY,k!==0&&(this._viewportElement.scrollTop+=k,this._bubbleScroll(P,k))}};r.Viewport=x=d([m(2,y.IBufferService),m(3,y.IOptionsService),m(4,h.ICharSizeService),m(5,h.IRenderService),m(6,h.ICoreBrowserService),m(7,h.IThemeService)],x)},3107:function(i,r,s){var d=this&&this.__decorate||function(y,x,P,k){var S,T=arguments.length,A=T<3?x:k===null?k=Object.getOwnPropertyDescriptor(x,P):k;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")A=Reflect.decorate(y,x,P,k);else for(var D=y.length-1;D>=0;D--)(S=y[D])&&(A=(T<3?S(A):T>3?S(x,P,A):S(x,P))||A);return T>3&&A&&Object.defineProperty(x,P,A),A},m=this&&this.__param||function(y,x){return function(P,k){x(P,k,y)}};Object.defineProperty(r,"__esModule",{value:!0}),r.BufferDecorationRenderer=void 0;let u=s(4725),h=s(844),v=s(2585),b=r.BufferDecorationRenderer=class extends h.Disposable{constructor(y,x,P,k,S){super(),this._screenElement=y,this._bufferService=x,this._coreBrowserService=P,this._decorationService=k,this._renderService=S,this._decorationElements=new Map,this._altBufferIsActive=!1,this._dimensionsChanged=!1,this._container=document.createElement("div"),this._container.classList.add("xterm-decoration-container"),this._screenElement.appendChild(this._container),this.register(this._renderService.onRenderedViewportChange(()=>this._doRefreshDecorations())),this.register(this._renderService.onDimensionsChange(()=>{this._dimensionsChanged=!0,this._queueRefresh()})),this.register(this._coreBrowserService.onDprChange(()=>this._queueRefresh())),this.register(this._bufferService.buffers.onBufferActivate(()=>{this._altBufferIsActive=this._bufferService.buffer===this._bufferService.buffers.alt})),this.register(this._decorationService.onDecorationRegistered(()=>this._queueRefresh())),this.register(this._decorationService.onDecorationRemoved(T=>this._removeDecoration(T))),this.register((0,h.toDisposable)(()=>{this._container.remove(),this._decorationElements.clear()}))}_queueRefresh(){this._animationFrame===void 0&&(this._animationFrame=this._renderService.addRefreshCallback(()=>{this._doRefreshDecorations(),this._animationFrame=void 0}))}_doRefreshDecorations(){for(let y of this._decorationService.decorations)this._renderDecoration(y);this._dimensionsChanged=!1}_renderDecoration(y){this._refreshStyle(y),this._dimensionsChanged&&this._refreshXPosition(y)}_createElement(y){let x=this._coreBrowserService.mainDocument.createElement("div");x.classList.add("xterm-decoration"),x.classList.toggle("xterm-decoration-top-layer",y?.options?.layer==="top"),x.style.width=`${Math.round((y.options.width||1)*this._renderService.dimensions.css.cell.width)}px`,x.style.height=(y.options.height||1)*this._renderService.dimensions.css.cell.height+"px",x.style.top=(y.marker.line-this._bufferService.buffers.active.ydisp)*this._renderService.dimensions.css.cell.height+"px",x.style.lineHeight=`${this._renderService.dimensions.css.cell.height}px`;let P=y.options.x??0;return P&&P>this._bufferService.cols&&(x.style.display="none"),this._refreshXPosition(y,x),x}_refreshStyle(y){let x=y.marker.line-this._bufferService.buffers.active.ydisp;if(x<0||x>=this._bufferService.rows)y.element&&(y.element.style.display="none",y.onRenderEmitter.fire(y.element));else{let P=this._decorationElements.get(y);P||(P=this._createElement(y),y.element=P,this._decorationElements.set(y,P),this._container.appendChild(P),y.onDispose(()=>{this._decorationElements.delete(y),P.remove()})),P.style.top=x*this._renderService.dimensions.css.cell.height+"px",P.style.display=this._altBufferIsActive?"none":"block",y.onRenderEmitter.fire(P)}}_refreshXPosition(y,x=y.element){if(!x)return;let P=y.options.x??0;(y.options.anchor||"left")==="right"?x.style.right=P?P*this._renderService.dimensions.css.cell.width+"px":"":x.style.left=P?P*this._renderService.dimensions.css.cell.width+"px":""}_removeDecoration(y){this._decorationElements.get(y)?.remove(),this._decorationElements.delete(y),y.dispose()}};r.BufferDecorationRenderer=b=d([m(1,v.IBufferService),m(2,u.ICoreBrowserService),m(3,v.IDecorationService),m(4,u.IRenderService)],b)},5871:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.ColorZoneStore=void 0,r.ColorZoneStore=class{constructor(){this._zones=[],this._zonePool=[],this._zonePoolIndex=0,this._linePadding={full:0,left:0,center:0,right:0}}get zones(){return this._zonePool.length=Math.min(this._zonePool.length,this._zones.length),this._zones}clear(){this._zones.length=0,this._zonePoolIndex=0}addDecoration(s){if(s.options.overviewRulerOptions){for(let d of this._zones)if(d.color===s.options.overviewRulerOptions.color&&d.position===s.options.overviewRulerOptions.position){if(this._lineIntersectsZone(d,s.marker.line))return;if(this._lineAdjacentToZone(d,s.marker.line,s.options.overviewRulerOptions.position))return void this._addLineToZone(d,s.marker.line)}if(this._zonePoolIndex=s.startBufferLine&&d<=s.endBufferLine}_lineAdjacentToZone(s,d,m){return d>=s.startBufferLine-this._linePadding[m||"full"]&&d<=s.endBufferLine+this._linePadding[m||"full"]}_addLineToZone(s,d){s.startBufferLine=Math.min(s.startBufferLine,d),s.endBufferLine=Math.max(s.endBufferLine,d)}}},5744:function(i,r,s){var d=this&&this.__decorate||function(S,T,A,D){var F,I=arguments.length,V=I<3?T:D===null?D=Object.getOwnPropertyDescriptor(T,A):D;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")V=Reflect.decorate(S,T,A,D);else for(var ae=S.length-1;ae>=0;ae--)(F=S[ae])&&(V=(I<3?F(V):I>3?F(T,A,V):F(T,A))||V);return I>3&&V&&Object.defineProperty(T,A,V),V},m=this&&this.__param||function(S,T){return function(A,D){T(A,D,S)}};Object.defineProperty(r,"__esModule",{value:!0}),r.OverviewRulerRenderer=void 0;let u=s(5871),h=s(4725),v=s(844),b=s(2585),y={full:0,left:0,center:0,right:0},x={full:0,left:0,center:0,right:0},P={full:0,left:0,center:0,right:0},k=r.OverviewRulerRenderer=class extends v.Disposable{get _width(){return this._optionsService.options.overviewRulerWidth||0}constructor(S,T,A,D,F,I,V){super(),this._viewportElement=S,this._screenElement=T,this._bufferService=A,this._decorationService=D,this._renderService=F,this._optionsService=I,this._coreBrowserService=V,this._colorZoneStore=new u.ColorZoneStore,this._shouldUpdateDimensions=!0,this._shouldUpdateAnchor=!0,this._lastKnownBufferLength=0,this._canvas=this._coreBrowserService.mainDocument.createElement("canvas"),this._canvas.classList.add("xterm-decoration-overview-ruler"),this._refreshCanvasDimensions(),this._viewportElement.parentElement?.insertBefore(this._canvas,this._viewportElement);let ae=this._canvas.getContext("2d");if(!ae)throw new Error("Ctx cannot be null");this._ctx=ae,this._registerDecorationListeners(),this._registerBufferChangeListeners(),this._registerDimensionChangeListeners(),this.register((0,v.toDisposable)(()=>{this._canvas?.remove()}))}_registerDecorationListeners(){this.register(this._decorationService.onDecorationRegistered(()=>this._queueRefresh(void 0,!0))),this.register(this._decorationService.onDecorationRemoved(()=>this._queueRefresh(void 0,!0)))}_registerBufferChangeListeners(){this.register(this._renderService.onRenderedViewportChange(()=>this._queueRefresh())),this.register(this._bufferService.buffers.onBufferActivate(()=>{this._canvas.style.display=this._bufferService.buffer===this._bufferService.buffers.alt?"none":"block"})),this.register(this._bufferService.onScroll(()=>{this._lastKnownBufferLength!==this._bufferService.buffers.normal.lines.length&&(this._refreshDrawHeightConstants(),this._refreshColorZonePadding())}))}_registerDimensionChangeListeners(){this.register(this._renderService.onRender(()=>{this._containerHeight&&this._containerHeight===this._screenElement.clientHeight||(this._queueRefresh(!0),this._containerHeight=this._screenElement.clientHeight)})),this.register(this._optionsService.onSpecificOptionChange("overviewRulerWidth",()=>this._queueRefresh(!0))),this.register(this._coreBrowserService.onDprChange(()=>this._queueRefresh(!0))),this._queueRefresh(!0)}_refreshDrawConstants(){let S=Math.floor(this._canvas.width/3),T=Math.ceil(this._canvas.width/3);x.full=this._canvas.width,x.left=S,x.center=T,x.right=S,this._refreshDrawHeightConstants(),P.full=0,P.left=0,P.center=x.left,P.right=x.left+x.center}_refreshDrawHeightConstants(){y.full=Math.round(2*this._coreBrowserService.dpr);let S=this._canvas.height/this._bufferService.buffer.lines.length,T=Math.round(Math.max(Math.min(S,12),6)*this._coreBrowserService.dpr);y.left=T,y.center=T,y.right=T}_refreshColorZonePadding(){this._colorZoneStore.setPadding({full:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*y.full),left:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*y.left),center:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*y.center),right:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*y.right)}),this._lastKnownBufferLength=this._bufferService.buffers.normal.lines.length}_refreshCanvasDimensions(){this._canvas.style.width=`${this._width}px`,this._canvas.width=Math.round(this._width*this._coreBrowserService.dpr),this._canvas.style.height=`${this._screenElement.clientHeight}px`,this._canvas.height=Math.round(this._screenElement.clientHeight*this._coreBrowserService.dpr),this._refreshDrawConstants(),this._refreshColorZonePadding()}_refreshDecorations(){this._shouldUpdateDimensions&&this._refreshCanvasDimensions(),this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height),this._colorZoneStore.clear();for(let T of this._decorationService.decorations)this._colorZoneStore.addDecoration(T);this._ctx.lineWidth=1;let S=this._colorZoneStore.zones;for(let T of S)T.position!=="full"&&this._renderColorZone(T);for(let T of S)T.position==="full"&&this._renderColorZone(T);this._shouldUpdateDimensions=!1,this._shouldUpdateAnchor=!1}_renderColorZone(S){this._ctx.fillStyle=S.color,this._ctx.fillRect(P[S.position||"full"],Math.round((this._canvas.height-1)*(S.startBufferLine/this._bufferService.buffers.active.lines.length)-y[S.position||"full"]/2),x[S.position||"full"],Math.round((this._canvas.height-1)*((S.endBufferLine-S.startBufferLine)/this._bufferService.buffers.active.lines.length)+y[S.position||"full"]))}_queueRefresh(S,T){this._shouldUpdateDimensions=S||this._shouldUpdateDimensions,this._shouldUpdateAnchor=T||this._shouldUpdateAnchor,this._animationFrame===void 0&&(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame(()=>{this._refreshDecorations(),this._animationFrame=void 0}))}};r.OverviewRulerRenderer=k=d([m(2,b.IBufferService),m(3,b.IDecorationService),m(4,h.IRenderService),m(5,b.IOptionsService),m(6,h.ICoreBrowserService)],k)},2950:function(i,r,s){var d=this&&this.__decorate||function(y,x,P,k){var S,T=arguments.length,A=T<3?x:k===null?k=Object.getOwnPropertyDescriptor(x,P):k;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")A=Reflect.decorate(y,x,P,k);else for(var D=y.length-1;D>=0;D--)(S=y[D])&&(A=(T<3?S(A):T>3?S(x,P,A):S(x,P))||A);return T>3&&A&&Object.defineProperty(x,P,A),A},m=this&&this.__param||function(y,x){return function(P,k){x(P,k,y)}};Object.defineProperty(r,"__esModule",{value:!0}),r.CompositionHelper=void 0;let u=s(4725),h=s(2585),v=s(2584),b=r.CompositionHelper=class{get isComposing(){return this._isComposing}constructor(y,x,P,k,S,T){this._textarea=y,this._compositionView=x,this._bufferService=P,this._optionsService=k,this._coreService=S,this._renderService=T,this._isComposing=!1,this._isSendingComposition=!1,this._compositionPosition={start:0,end:0},this._dataAlreadySent=""}compositionstart(){this._isComposing=!0,this._compositionPosition.start=this._textarea.value.length,this._compositionView.textContent="",this._dataAlreadySent="",this._compositionView.classList.add("active")}compositionupdate(y){this._compositionView.textContent=y.data,this.updateCompositionElements(),setTimeout(()=>{this._compositionPosition.end=this._textarea.value.length},0)}compositionend(){this._finalizeComposition(!0)}keydown(y){if(this._isComposing||this._isSendingComposition){if(y.keyCode===229||y.keyCode===16||y.keyCode===17||y.keyCode===18)return!1;this._finalizeComposition(!1)}return y.keyCode!==229||(this._handleAnyTextareaChanges(),!1)}_finalizeComposition(y){if(this._compositionView.classList.remove("active"),this._isComposing=!1,y){let x={start:this._compositionPosition.start,end:this._compositionPosition.end};this._isSendingComposition=!0,setTimeout(()=>{if(this._isSendingComposition){let P;this._isSendingComposition=!1,x.start+=this._dataAlreadySent.length,P=this._isComposing?this._textarea.value.substring(x.start,x.end):this._textarea.value.substring(x.start),P.length>0&&this._coreService.triggerDataEvent(P,!0)}},0)}else{this._isSendingComposition=!1;let x=this._textarea.value.substring(this._compositionPosition.start,this._compositionPosition.end);this._coreService.triggerDataEvent(x,!0)}}_handleAnyTextareaChanges(){let y=this._textarea.value;setTimeout(()=>{if(!this._isComposing){let x=this._textarea.value,P=x.replace(y,"");this._dataAlreadySent=P,x.length>y.length?this._coreService.triggerDataEvent(P,!0):x.lengththis.updateCompositionElements(!0),0)}}};r.CompositionHelper=b=d([m(2,h.IBufferService),m(3,h.IOptionsService),m(4,h.ICoreService),m(5,u.IRenderService)],b)},9806:(i,r)=>{function s(d,m,u){let h=u.getBoundingClientRect(),v=d.getComputedStyle(u),b=parseInt(v.getPropertyValue("padding-left")),y=parseInt(v.getPropertyValue("padding-top"));return[m.clientX-h.left-b,m.clientY-h.top-y]}Object.defineProperty(r,"__esModule",{value:!0}),r.getCoords=r.getCoordsRelativeToElement=void 0,r.getCoordsRelativeToElement=s,r.getCoords=function(d,m,u,h,v,b,y,x,P){if(!b)return;let k=s(d,m,u);return k?(k[0]=Math.ceil((k[0]+(P?y/2:0))/y),k[1]=Math.ceil(k[1]/x),k[0]=Math.min(Math.max(k[0],1),h+(P?1:0)),k[1]=Math.min(Math.max(k[1],1),v),k):void 0}},9504:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.moveToCellSequence=void 0;let d=s(2584);function m(x,P,k,S){let T=x-u(x,k),A=P-u(P,k),D=Math.abs(T-A)-(function(F,I,V){let ae=0,oe=F-u(F,V),W=I-u(I,V);for(let G=0;G=0&&xP?"A":"B"}function v(x,P,k,S,T,A){let D=x,F=P,I="";for(;D!==k||F!==S;)D+=T?1:-1,T&&D>A.cols-1?(I+=A.buffer.translateBufferLineToString(F,!1,x,D),D=0,x=0,F++):!T&&D<0&&(I+=A.buffer.translateBufferLineToString(F,!1,0,x+1),D=A.cols-1,x=D,F--);return I+A.buffer.translateBufferLineToString(F,!1,x,D)}function b(x,P){let k=P?"O":"[";return d.C0.ESC+k+x}function y(x,P){x=Math.floor(x);let k="";for(let S=0;S0?oe-u(oe,W):V;let we=oe,me=(function(Ie,ue,H,ee,le,de){let De;return De=m(H,ee,le,de).length>0?ee-u(ee,le):ue,Ie=H&&Dex?"D":"C",y(Math.abs(T-x),b(D,S));D=A>P?"D":"C";let F=Math.abs(A-P);return y((function(I,V){return V.cols-I})(A>P?x:T,k)+(F-1)*k.cols+1+((A>P?T:x)-1),b(D,S))}},1296:function(i,r,s){var d=this&&this.__decorate||function(G,Ce,we,me){var Ie,ue=arguments.length,H=ue<3?Ce:me===null?me=Object.getOwnPropertyDescriptor(Ce,we):me;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")H=Reflect.decorate(G,Ce,we,me);else for(var ee=G.length-1;ee>=0;ee--)(Ie=G[ee])&&(H=(ue<3?Ie(H):ue>3?Ie(Ce,we,H):Ie(Ce,we))||H);return ue>3&&H&&Object.defineProperty(Ce,we,H),H},m=this&&this.__param||function(G,Ce){return function(we,me){Ce(we,me,G)}};Object.defineProperty(r,"__esModule",{value:!0}),r.DomRenderer=void 0;let u=s(3787),h=s(2550),v=s(2223),b=s(6171),y=s(6052),x=s(4725),P=s(8055),k=s(8460),S=s(844),T=s(2585),A="xterm-dom-renderer-owner-",D="xterm-rows",F="xterm-fg-",I="xterm-bg-",V="xterm-focus",ae="xterm-selection",oe=1,W=r.DomRenderer=class extends S.Disposable{constructor(G,Ce,we,me,Ie,ue,H,ee,le,de,De,je,tt){super(),this._terminal=G,this._document=Ce,this._element=we,this._screenElement=me,this._viewportElement=Ie,this._helperContainer=ue,this._linkifier2=H,this._charSizeService=le,this._optionsService=de,this._bufferService=De,this._coreBrowserService=je,this._themeService=tt,this._terminalClass=oe++,this._rowElements=[],this._selectionRenderModel=(0,y.createSelectionRenderModel)(),this.onRequestRedraw=this.register(new k.EventEmitter).event,this._rowContainer=this._document.createElement("div"),this._rowContainer.classList.add(D),this._rowContainer.style.lineHeight="normal",this._rowContainer.setAttribute("aria-hidden","true"),this._refreshRowElements(this._bufferService.cols,this._bufferService.rows),this._selectionContainer=this._document.createElement("div"),this._selectionContainer.classList.add(ae),this._selectionContainer.setAttribute("aria-hidden","true"),this.dimensions=(0,b.createRenderDimensions)(),this._updateDimensions(),this.register(this._optionsService.onOptionChange(()=>this._handleOptionsChanged())),this.register(this._themeService.onChangeColors(et=>this._injectCss(et))),this._injectCss(this._themeService.colors),this._rowFactory=ee.createInstance(u.DomRendererRowFactory,document),this._element.classList.add(A+this._terminalClass),this._screenElement.appendChild(this._rowContainer),this._screenElement.appendChild(this._selectionContainer),this.register(this._linkifier2.onShowLinkUnderline(et=>this._handleLinkHover(et))),this.register(this._linkifier2.onHideLinkUnderline(et=>this._handleLinkLeave(et))),this.register((0,S.toDisposable)(()=>{this._element.classList.remove(A+this._terminalClass),this._rowContainer.remove(),this._selectionContainer.remove(),this._widthCache.dispose(),this._themeStyleElement.remove(),this._dimensionsStyleElement.remove()})),this._widthCache=new h.WidthCache(this._document,this._helperContainer),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}_updateDimensions(){let G=this._coreBrowserService.dpr;this.dimensions.device.char.width=this._charSizeService.width*G,this.dimensions.device.char.height=Math.ceil(this._charSizeService.height*G),this.dimensions.device.cell.width=this.dimensions.device.char.width+Math.round(this._optionsService.rawOptions.letterSpacing),this.dimensions.device.cell.height=Math.floor(this.dimensions.device.char.height*this._optionsService.rawOptions.lineHeight),this.dimensions.device.char.left=0,this.dimensions.device.char.top=0,this.dimensions.device.canvas.width=this.dimensions.device.cell.width*this._bufferService.cols,this.dimensions.device.canvas.height=this.dimensions.device.cell.height*this._bufferService.rows,this.dimensions.css.canvas.width=Math.round(this.dimensions.device.canvas.width/G),this.dimensions.css.canvas.height=Math.round(this.dimensions.device.canvas.height/G),this.dimensions.css.cell.width=this.dimensions.css.canvas.width/this._bufferService.cols,this.dimensions.css.cell.height=this.dimensions.css.canvas.height/this._bufferService.rows;for(let we of this._rowElements)we.style.width=`${this.dimensions.css.canvas.width}px`,we.style.height=`${this.dimensions.css.cell.height}px`,we.style.lineHeight=`${this.dimensions.css.cell.height}px`,we.style.overflow="hidden";this._dimensionsStyleElement||(this._dimensionsStyleElement=this._document.createElement("style"),this._screenElement.appendChild(this._dimensionsStyleElement));let Ce=`${this._terminalSelector} .${D} span { display: inline-block; height: 100%; vertical-align: top;}`;this._dimensionsStyleElement.textContent=Ce,this._selectionContainer.style.height=this._viewportElement.style.height,this._screenElement.style.width=`${this.dimensions.css.canvas.width}px`,this._screenElement.style.height=`${this.dimensions.css.canvas.height}px`}_injectCss(G){this._themeStyleElement||(this._themeStyleElement=this._document.createElement("style"),this._screenElement.appendChild(this._themeStyleElement));let Ce=`${this._terminalSelector} .${D} { color: ${G.foreground.css}; font-family: ${this._optionsService.rawOptions.fontFamily}; font-size: ${this._optionsService.rawOptions.fontSize}px; font-kerning: none; white-space: pre}`;Ce+=`${this._terminalSelector} .${D} .xterm-dim { color: ${P.color.multiplyOpacity(G.foreground,.5).css};}`,Ce+=`${this._terminalSelector} span:not(.xterm-bold) { font-weight: ${this._optionsService.rawOptions.fontWeight};}${this._terminalSelector} span.xterm-bold { font-weight: ${this._optionsService.rawOptions.fontWeightBold};}${this._terminalSelector} span.xterm-italic { font-style: italic;}`;let we=`blink_underline_${this._terminalClass}`,me=`blink_bar_${this._terminalClass}`,Ie=`blink_block_${this._terminalClass}`;Ce+=`@keyframes ${we} { 50% { border-bottom-style: hidden; }}`,Ce+=`@keyframes ${me} { 50% { box-shadow: none; }}`,Ce+=`@keyframes ${Ie} { 0% { background-color: ${G.cursor.css}; color: ${G.cursorAccent.css}; } 50% { background-color: inherit; color: ${G.cursor.css}; }}`,Ce+=`${this._terminalSelector} .${D}.${V} .xterm-cursor.xterm-cursor-blink.xterm-cursor-underline { animation: ${we} 1s step-end infinite;}${this._terminalSelector} .${D}.${V} .xterm-cursor.xterm-cursor-blink.xterm-cursor-bar { animation: ${me} 1s step-end infinite;}${this._terminalSelector} .${D}.${V} .xterm-cursor.xterm-cursor-blink.xterm-cursor-block { animation: ${Ie} 1s step-end infinite;}${this._terminalSelector} .${D} .xterm-cursor.xterm-cursor-block { background-color: ${G.cursor.css}; color: ${G.cursorAccent.css};}${this._terminalSelector} .${D} .xterm-cursor.xterm-cursor-block:not(.xterm-cursor-blink) { background-color: ${G.cursor.css} !important; color: ${G.cursorAccent.css} !important;}${this._terminalSelector} .${D} .xterm-cursor.xterm-cursor-outline { outline: 1px solid ${G.cursor.css}; outline-offset: -1px;}${this._terminalSelector} .${D} .xterm-cursor.xterm-cursor-bar { box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${G.cursor.css} inset;}${this._terminalSelector} .${D} .xterm-cursor.xterm-cursor-underline { border-bottom: 1px ${G.cursor.css}; border-bottom-style: solid; height: calc(100% - 1px);}`,Ce+=`${this._terminalSelector} .${ae} { position: absolute; top: 0; left: 0; z-index: 1; pointer-events: none;}${this._terminalSelector}.focus .${ae} div { position: absolute; background-color: ${G.selectionBackgroundOpaque.css};}${this._terminalSelector} .${ae} div { position: absolute; background-color: ${G.selectionInactiveBackgroundOpaque.css};}`;for(let[ue,H]of G.ansi.entries())Ce+=`${this._terminalSelector} .${F}${ue} { color: ${H.css}; }${this._terminalSelector} .${F}${ue}.xterm-dim { color: ${P.color.multiplyOpacity(H,.5).css}; }${this._terminalSelector} .${I}${ue} { background-color: ${H.css}; }`;Ce+=`${this._terminalSelector} .${F}${v.INVERTED_DEFAULT_COLOR} { color: ${P.color.opaque(G.background).css}; }${this._terminalSelector} .${F}${v.INVERTED_DEFAULT_COLOR}.xterm-dim { color: ${P.color.multiplyOpacity(P.color.opaque(G.background),.5).css}; }${this._terminalSelector} .${I}${v.INVERTED_DEFAULT_COLOR} { background-color: ${G.foreground.css}; }`,this._themeStyleElement.textContent=Ce}_setDefaultSpacing(){let G=this.dimensions.css.cell.width-this._widthCache.get("W",!1,!1);this._rowContainer.style.letterSpacing=`${G}px`,this._rowFactory.defaultSpacing=G}handleDevicePixelRatioChange(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}_refreshRowElements(G,Ce){for(let we=this._rowElements.length;we<=Ce;we++){let me=this._document.createElement("div");this._rowContainer.appendChild(me),this._rowElements.push(me)}for(;this._rowElements.length>Ce;)this._rowContainer.removeChild(this._rowElements.pop())}handleResize(G,Ce){this._refreshRowElements(G,Ce),this._updateDimensions(),this.handleSelectionChanged(this._selectionRenderModel.selectionStart,this._selectionRenderModel.selectionEnd,this._selectionRenderModel.columnSelectMode)}handleCharSizeChanged(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}handleBlur(){this._rowContainer.classList.remove(V),this.renderRows(0,this._bufferService.rows-1)}handleFocus(){this._rowContainer.classList.add(V),this.renderRows(this._bufferService.buffer.y,this._bufferService.buffer.y)}handleSelectionChanged(G,Ce,we){if(this._selectionContainer.replaceChildren(),this._rowFactory.handleSelectionChanged(G,Ce,we),this.renderRows(0,this._bufferService.rows-1),!G||!Ce)return;this._selectionRenderModel.update(this._terminal,G,Ce,we);let me=this._selectionRenderModel.viewportStartRow,Ie=this._selectionRenderModel.viewportEndRow,ue=this._selectionRenderModel.viewportCappedStartRow,H=this._selectionRenderModel.viewportCappedEndRow;if(ue>=this._bufferService.rows||H<0)return;let ee=this._document.createDocumentFragment();if(we){let le=G[0]>Ce[0];ee.appendChild(this._createSelectionElement(ue,le?Ce[0]:G[0],le?G[0]:Ce[0],H-ue+1))}else{let le=me===ue?G[0]:0,de=ue===Ie?Ce[0]:this._bufferService.cols;ee.appendChild(this._createSelectionElement(ue,le,de));let De=H-ue-1;if(ee.appendChild(this._createSelectionElement(ue+1,0,this._bufferService.cols,De)),ue!==H){let je=Ie===H?Ce[0]:this._bufferService.cols;ee.appendChild(this._createSelectionElement(H,0,je))}}this._selectionContainer.appendChild(ee)}_createSelectionElement(G,Ce,we,me=1){let Ie=this._document.createElement("div"),ue=Ce*this.dimensions.css.cell.width,H=this.dimensions.css.cell.width*(we-Ce);return ue+H>this.dimensions.css.canvas.width&&(H=this.dimensions.css.canvas.width-ue),Ie.style.height=me*this.dimensions.css.cell.height+"px",Ie.style.top=G*this.dimensions.css.cell.height+"px",Ie.style.left=`${ue}px`,Ie.style.width=`${H}px`,Ie}handleCursorMove(){}_handleOptionsChanged(){this._updateDimensions(),this._injectCss(this._themeService.colors),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}clear(){for(let G of this._rowElements)G.replaceChildren()}renderRows(G,Ce){let we=this._bufferService.buffer,me=we.ybase+we.y,Ie=Math.min(we.x,this._bufferService.cols-1),ue=this._optionsService.rawOptions.cursorBlink,H=this._optionsService.rawOptions.cursorStyle,ee=this._optionsService.rawOptions.cursorInactiveStyle;for(let le=G;le<=Ce;le++){let de=le+we.ydisp,De=this._rowElements[le],je=we.lines.get(de);if(!De||!je)break;De.replaceChildren(...this._rowFactory.createRow(je,de,de===me,H,ee,Ie,ue,this.dimensions.css.cell.width,this._widthCache,-1,-1))}}get _terminalSelector(){return`.${A}${this._terminalClass}`}_handleLinkHover(G){this._setCellUnderline(G.x1,G.x2,G.y1,G.y2,G.cols,!0)}_handleLinkLeave(G){this._setCellUnderline(G.x1,G.x2,G.y1,G.y2,G.cols,!1)}_setCellUnderline(G,Ce,we,me,Ie,ue){we<0&&(G=0),me<0&&(Ce=0);let H=this._bufferService.rows-1;we=Math.max(Math.min(we,H),0),me=Math.max(Math.min(me,H),0),Ie=Math.min(Ie,this._bufferService.cols);let ee=this._bufferService.buffer,le=ee.ybase+ee.y,de=Math.min(ee.x,Ie-1),De=this._optionsService.rawOptions.cursorBlink,je=this._optionsService.rawOptions.cursorStyle,tt=this._optionsService.rawOptions.cursorInactiveStyle;for(let et=we;et<=me;++et){let ye=et+ee.ydisp,Y=this._rowElements[et],Re=ee.lines.get(ye);if(!Y||!Re)break;Y.replaceChildren(...this._rowFactory.createRow(Re,ye,ye===le,je,tt,de,De,this.dimensions.css.cell.width,this._widthCache,ue?et===we?G:0:-1,ue?(et===me?Ce:Ie)-1:-1))}}};r.DomRenderer=W=d([m(7,T.IInstantiationService),m(8,x.ICharSizeService),m(9,T.IOptionsService),m(10,T.IBufferService),m(11,x.ICoreBrowserService),m(12,x.IThemeService)],W)},3787:function(i,r,s){var d=this&&this.__decorate||function(D,F,I,V){var ae,oe=arguments.length,W=oe<3?F:V===null?V=Object.getOwnPropertyDescriptor(F,I):V;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")W=Reflect.decorate(D,F,I,V);else for(var G=D.length-1;G>=0;G--)(ae=D[G])&&(W=(oe<3?ae(W):oe>3?ae(F,I,W):ae(F,I))||W);return oe>3&&W&&Object.defineProperty(F,I,W),W},m=this&&this.__param||function(D,F){return function(I,V){F(I,V,D)}};Object.defineProperty(r,"__esModule",{value:!0}),r.DomRendererRowFactory=void 0;let u=s(2223),h=s(643),v=s(511),b=s(2585),y=s(8055),x=s(4725),P=s(4269),k=s(6171),S=s(3734),T=r.DomRendererRowFactory=class{constructor(D,F,I,V,ae,oe,W){this._document=D,this._characterJoinerService=F,this._optionsService=I,this._coreBrowserService=V,this._coreService=ae,this._decorationService=oe,this._themeService=W,this._workCell=new v.CellData,this._columnSelectMode=!1,this.defaultSpacing=0}handleSelectionChanged(D,F,I){this._selectionStart=D,this._selectionEnd=F,this._columnSelectMode=I}createRow(D,F,I,V,ae,oe,W,G,Ce,we,me){let Ie=[],ue=this._characterJoinerService.getJoinedCharacters(F),H=this._themeService.colors,ee,le=D.getNoBgTrimmedLength();I&&le0&&ri===ue[0][0]){bo=!0;let tr=ue.shift();Hn=new P.JoinedCellData(this._workCell,D.translateToString(!0,tr[0],tr[1]),tr[1]-tr[0]),qr=tr[1]-1,Lo=Hn.getWidth()}let ep=this._isCellInSelection(ri,F),Ku=I&&ri===oe,wk=Tt&&ri>=we&&ri<=me,Mk=!1;this._decorationService.forEachDecorationAtCell(ri,F,void 0,tr=>{Mk=!0});let bv=Hn.getChars()||h.WHITESPACE_CELL_CHAR;if(bv===" "&&(Hn.isUnderline()||Hn.isOverline())&&(bv="\xA0"),Ze=Lo*G-Ce.get(bv,Hn.isBold(),Hn.isItalic()),ee){if(de&&(ep&&Re||!ep&&!Re&&Hn.bg===je)&&(ep&&Re&&H.selectionForeground||Hn.fg===tt)&&Hn.extended.ext===et&&wk===ye&&Ze===Y&&!Ku&&!bo&&!Mk){Hn.isInvisible()?De+=h.WHITESPACE_CELL_CHAR:De+=bv,de++;continue}de&&(ee.textContent=De),ee=this._document.createElement("span"),de=0,De=""}else ee=this._document.createElement("span");if(je=Hn.bg,tt=Hn.fg,et=Hn.extended.ext,ye=wk,Y=Ze,Re=ep,bo&&oe>=ri&&oe<=qr&&(oe=ri),!this._coreService.isCursorHidden&&Ku&&this._coreService.isCursorInitialized){if(ct.push("xterm-cursor"),this._coreBrowserService.isFocused)W&&ct.push("xterm-cursor-blink"),ct.push(V==="bar"?"xterm-cursor-bar":V==="underline"?"xterm-cursor-underline":"xterm-cursor-block");else if(ae)switch(ae){case"outline":ct.push("xterm-cursor-outline");break;case"block":ct.push("xterm-cursor-block");break;case"bar":ct.push("xterm-cursor-bar");break;case"underline":ct.push("xterm-cursor-underline")}}if(Hn.isBold()&&ct.push("xterm-bold"),Hn.isItalic()&&ct.push("xterm-italic"),Hn.isDim()&&ct.push("xterm-dim"),De=Hn.isInvisible()?h.WHITESPACE_CELL_CHAR:Hn.getChars()||h.WHITESPACE_CELL_CHAR,Hn.isUnderline()&&(ct.push(`xterm-underline-${Hn.extended.underlineStyle}`),De===" "&&(De="\xA0"),!Hn.isUnderlineColorDefault()))if(Hn.isUnderlineColorRGB())ee.style.textDecorationColor=`rgb(${S.AttributeData.toColorRGB(Hn.getUnderlineColor()).join(",")})`;else{let tr=Hn.getUnderlineColor();this._optionsService.rawOptions.drawBoldTextInBrightColors&&Hn.isBold()&&tr<8&&(tr+=8),ee.style.textDecorationColor=H.ansi[tr].css}Hn.isOverline()&&(ct.push("xterm-overline"),De===" "&&(De="\xA0")),Hn.isStrikethrough()&&ct.push("xterm-strikethrough"),wk&&(ee.style.textDecoration="underline");let il=Hn.getFgColor(),J_=Hn.getFgColorMode(),bc=Hn.getBgColor(),ef=Hn.getBgColorMode(),Sk=!!Hn.isInverse();if(Sk){let tr=il;il=bc,bc=tr;let _H=J_;J_=ef,ef=_H}let tp,wv,np,tf=!1;switch(this._decorationService.forEachDecorationAtCell(ri,F,void 0,tr=>{tr.options.layer!=="top"&&tf||(tr.backgroundColorRGB&&(ef=50331648,bc=tr.backgroundColorRGB.rgba>>8&16777215,tp=tr.backgroundColorRGB),tr.foregroundColorRGB&&(J_=50331648,il=tr.foregroundColorRGB.rgba>>8&16777215,wv=tr.foregroundColorRGB),tf=tr.options.layer==="top")}),!tf&&ep&&(tp=this._coreBrowserService.isFocused?H.selectionBackgroundOpaque:H.selectionInactiveBackgroundOpaque,bc=tp.rgba>>8&16777215,ef=50331648,tf=!0,H.selectionForeground&&(J_=50331648,il=H.selectionForeground.rgba>>8&16777215,wv=H.selectionForeground)),tf&&ct.push("xterm-decoration-top"),ef){case 16777216:case 33554432:np=H.ansi[bc],ct.push(`xterm-bg-${bc}`);break;case 50331648:np=y.channels.toColor(bc>>16,bc>>8&255,255&bc),this._addStyle(ee,`background-color:#${A((bc>>>0).toString(16),"0",6)}`);break;default:Sk?(np=H.foreground,ct.push(`xterm-bg-${u.INVERTED_DEFAULT_COLOR}`)):np=H.background}switch(tp||Hn.isDim()&&(tp=y.color.multiplyOpacity(np,.5)),J_){case 16777216:case 33554432:Hn.isBold()&&il<8&&this._optionsService.rawOptions.drawBoldTextInBrightColors&&(il+=8),this._applyMinimumContrast(ee,np,H.ansi[il],Hn,tp,void 0)||ct.push(`xterm-fg-${il}`);break;case 50331648:let tr=y.channels.toColor(il>>16&255,il>>8&255,255&il);this._applyMinimumContrast(ee,np,tr,Hn,tp,wv)||this._addStyle(ee,`color:#${A(il.toString(16),"0",6)}`);break;default:this._applyMinimumContrast(ee,np,H.foreground,Hn,tp,wv)||Sk&&ct.push(`xterm-fg-${u.INVERTED_DEFAULT_COLOR}`)}ct.length&&(ee.className=ct.join(" "),ct.length=0),Ku||bo||Mk?ee.textContent=De:de++,Ze!==this.defaultSpacing&&(ee.style.letterSpacing=`${Ze}px`),Ie.push(ee),ri=qr}return ee&&de&&(ee.textContent=De),Ie}_applyMinimumContrast(D,F,I,V,ae,oe){if(this._optionsService.rawOptions.minimumContrastRatio===1||(0,k.treatGlyphAsBackgroundColor)(V.getCode()))return!1;let W=this._getContrastCache(V),G;if(ae||oe||(G=W.getColor(F.rgba,I.rgba)),G===void 0){let Ce=this._optionsService.rawOptions.minimumContrastRatio/(V.isDim()?2:1);G=y.color.ensureContrastRatio(ae||F,oe||I,Ce),W.setColor((ae||F).rgba,(oe||I).rgba,G??null)}return!!G&&(this._addStyle(D,`color:${G.css}`),!0)}_getContrastCache(D){return D.isDim()?this._themeService.colors.halfContrastCache:this._themeService.colors.contrastCache}_addStyle(D,F){D.setAttribute("style",`${D.getAttribute("style")||""}${F};`)}_isCellInSelection(D,F){let I=this._selectionStart,V=this._selectionEnd;return!(!I||!V)&&(this._columnSelectMode?I[0]<=V[0]?D>=I[0]&&F>=I[1]&&D=I[1]&&D>=V[0]&&F<=V[1]:F>I[1]&&F=I[0]&&D=I[0])}};function A(D,F,I){for(;D.length{Object.defineProperty(r,"__esModule",{value:!0}),r.WidthCache=void 0,r.WidthCache=class{constructor(s,d){this._flat=new Float32Array(256),this._font="",this._fontSize=0,this._weight="normal",this._weightBold="bold",this._measureElements=[],this._container=s.createElement("div"),this._container.classList.add("xterm-width-cache-measure-container"),this._container.setAttribute("aria-hidden","true"),this._container.style.whiteSpace="pre",this._container.style.fontKerning="none";let m=s.createElement("span");m.classList.add("xterm-char-measure-element");let u=s.createElement("span");u.classList.add("xterm-char-measure-element"),u.style.fontWeight="bold";let h=s.createElement("span");h.classList.add("xterm-char-measure-element"),h.style.fontStyle="italic";let v=s.createElement("span");v.classList.add("xterm-char-measure-element"),v.style.fontWeight="bold",v.style.fontStyle="italic",this._measureElements=[m,u,h,v],this._container.appendChild(m),this._container.appendChild(u),this._container.appendChild(h),this._container.appendChild(v),d.appendChild(this._container),this.clear()}dispose(){this._container.remove(),this._measureElements.length=0,this._holey=void 0}clear(){this._flat.fill(-9999),this._holey=new Map}setFont(s,d,m,u){s===this._font&&d===this._fontSize&&m===this._weight&&u===this._weightBold||(this._font=s,this._fontSize=d,this._weight=m,this._weightBold=u,this._container.style.fontFamily=this._font,this._container.style.fontSize=`${this._fontSize}px`,this._measureElements[0].style.fontWeight=`${m}`,this._measureElements[1].style.fontWeight=`${u}`,this._measureElements[2].style.fontWeight=`${m}`,this._measureElements[3].style.fontWeight=`${u}`,this.clear())}get(s,d,m){let u=0;if(!d&&!m&&s.length===1&&(u=s.charCodeAt(0))<256){if(this._flat[u]!==-9999)return this._flat[u];let b=this._measure(s,0);return b>0&&(this._flat[u]=b),b}let h=s;d&&(h+="B"),m&&(h+="I");let v=this._holey.get(h);if(v===void 0){let b=0;d&&(b|=1),m&&(b|=2),v=this._measure(s,b),v>0&&this._holey.set(h,v)}return v}_measure(s,d){let m=this._measureElements[d];return m.textContent=s.repeat(32),m.offsetWidth/32}}},2223:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.TEXT_BASELINE=r.DIM_OPACITY=r.INVERTED_DEFAULT_COLOR=void 0;let d=s(6114);r.INVERTED_DEFAULT_COLOR=257,r.DIM_OPACITY=.5,r.TEXT_BASELINE=d.isFirefox||d.isLegacyEdge?"bottom":"ideographic"},6171:(i,r)=>{function s(m){return 57508<=m&&m<=57558}function d(m){return m>=128512&&m<=128591||m>=127744&&m<=128511||m>=128640&&m<=128767||m>=9728&&m<=9983||m>=9984&&m<=10175||m>=65024&&m<=65039||m>=129280&&m<=129535||m>=127462&&m<=127487}Object.defineProperty(r,"__esModule",{value:!0}),r.computeNextVariantOffset=r.createRenderDimensions=r.treatGlyphAsBackgroundColor=r.allowRescaling=r.isEmoji=r.isRestrictedPowerlineGlyph=r.isPowerlineGlyph=r.throwIfFalsy=void 0,r.throwIfFalsy=function(m){if(!m)throw new Error("value must not be falsy");return m},r.isPowerlineGlyph=s,r.isRestrictedPowerlineGlyph=function(m){return 57520<=m&&m<=57527},r.isEmoji=d,r.allowRescaling=function(m,u,h,v){return u===1&&h>Math.ceil(1.5*v)&&m!==void 0&&m>255&&!d(m)&&!s(m)&&!(function(b){return 57344<=b&&b<=63743})(m)},r.treatGlyphAsBackgroundColor=function(m){return s(m)||(function(u){return 9472<=u&&u<=9631})(m)},r.createRenderDimensions=function(){return{css:{canvas:{width:0,height:0},cell:{width:0,height:0}},device:{canvas:{width:0,height:0},cell:{width:0,height:0},char:{width:0,height:0,left:0,top:0}}}},r.computeNextVariantOffset=function(m,u,h=0){return(m-(2*Math.round(u)-h))%(2*Math.round(u))}},6052:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createSelectionRenderModel=void 0;class s{constructor(){this.clear()}clear(){this.hasSelection=!1,this.columnSelectMode=!1,this.viewportStartRow=0,this.viewportEndRow=0,this.viewportCappedStartRow=0,this.viewportCappedEndRow=0,this.startCol=0,this.endCol=0,this.selectionStart=void 0,this.selectionEnd=void 0}update(m,u,h,v=!1){if(this.selectionStart=u,this.selectionEnd=h,!u||!h||u[0]===h[0]&&u[1]===h[1])return void this.clear();let b=m.buffers.active.ydisp,y=u[1]-b,x=h[1]-b,P=Math.max(y,0),k=Math.min(x,m.rows-1);P>=m.rows||k<0?this.clear():(this.hasSelection=!0,this.columnSelectMode=v,this.viewportStartRow=y,this.viewportEndRow=x,this.viewportCappedStartRow=P,this.viewportCappedEndRow=k,this.startCol=u[0],this.endCol=h[0])}isCellSelected(m,u,h){return!!this.hasSelection&&(h-=m.buffer.active.viewportY,this.columnSelectMode?this.startCol<=this.endCol?u>=this.startCol&&h>=this.viewportCappedStartRow&&u=this.viewportCappedStartRow&&u>=this.endCol&&h<=this.viewportCappedEndRow:h>this.viewportStartRow&&h=this.startCol&&u=this.startCol)}}r.createSelectionRenderModel=function(){return new s}},456:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.SelectionModel=void 0,r.SelectionModel=class{constructor(s){this._bufferService=s,this.isSelectAllActive=!1,this.selectionStartLength=0}clearSelection(){this.selectionStart=void 0,this.selectionEnd=void 0,this.isSelectAllActive=!1,this.selectionStartLength=0}get finalSelectionStart(){return this.isSelectAllActive?[0,0]:this.selectionEnd&&this.selectionStart&&this.areSelectionValuesReversed()?this.selectionEnd:this.selectionStart}get finalSelectionEnd(){if(this.isSelectAllActive)return[this._bufferService.cols,this._bufferService.buffer.ybase+this._bufferService.rows-1];if(this.selectionStart){if(!this.selectionEnd||this.areSelectionValuesReversed()){let s=this.selectionStart[0]+this.selectionStartLength;return s>this._bufferService.cols?s%this._bufferService.cols==0?[this._bufferService.cols,this.selectionStart[1]+Math.floor(s/this._bufferService.cols)-1]:[s%this._bufferService.cols,this.selectionStart[1]+Math.floor(s/this._bufferService.cols)]:[s,this.selectionStart[1]]}if(this.selectionStartLength&&this.selectionEnd[1]===this.selectionStart[1]){let s=this.selectionStart[0]+this.selectionStartLength;return s>this._bufferService.cols?[s%this._bufferService.cols,this.selectionStart[1]+Math.floor(s/this._bufferService.cols)]:[Math.max(s,this.selectionEnd[0]),this.selectionEnd[1]]}return this.selectionEnd}}areSelectionValuesReversed(){let s=this.selectionStart,d=this.selectionEnd;return!(!s||!d)&&(s[1]>d[1]||s[1]===d[1]&&s[0]>d[0])}handleTrim(s){return this.selectionStart&&(this.selectionStart[1]-=s),this.selectionEnd&&(this.selectionEnd[1]-=s),this.selectionEnd&&this.selectionEnd[1]<0?(this.clearSelection(),!0):(this.selectionStart&&this.selectionStart[1]<0&&(this.selectionStart[1]=0),!1)}}},428:function(i,r,s){var d=this&&this.__decorate||function(k,S,T,A){var D,F=arguments.length,I=F<3?S:A===null?A=Object.getOwnPropertyDescriptor(S,T):A;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")I=Reflect.decorate(k,S,T,A);else for(var V=k.length-1;V>=0;V--)(D=k[V])&&(I=(F<3?D(I):F>3?D(S,T,I):D(S,T))||I);return F>3&&I&&Object.defineProperty(S,T,I),I},m=this&&this.__param||function(k,S){return function(T,A){S(T,A,k)}};Object.defineProperty(r,"__esModule",{value:!0}),r.CharSizeService=void 0;let u=s(2585),h=s(8460),v=s(844),b=r.CharSizeService=class extends v.Disposable{get hasValidSize(){return this.width>0&&this.height>0}constructor(k,S,T){super(),this._optionsService=T,this.width=0,this.height=0,this._onCharSizeChange=this.register(new h.EventEmitter),this.onCharSizeChange=this._onCharSizeChange.event;try{this._measureStrategy=this.register(new P(this._optionsService))}catch{this._measureStrategy=this.register(new x(k,S,this._optionsService))}this.register(this._optionsService.onMultipleOptionChange(["fontFamily","fontSize"],()=>this.measure()))}measure(){let k=this._measureStrategy.measure();k.width===this.width&&k.height===this.height||(this.width=k.width,this.height=k.height,this._onCharSizeChange.fire())}};r.CharSizeService=b=d([m(2,u.IOptionsService)],b);class y extends v.Disposable{constructor(){super(...arguments),this._result={width:0,height:0}}_validateAndSet(S,T){S!==void 0&&S>0&&T!==void 0&&T>0&&(this._result.width=S,this._result.height=T)}}class x extends y{constructor(S,T,A){super(),this._document=S,this._parentElement=T,this._optionsService=A,this._measureElement=this._document.createElement("span"),this._measureElement.classList.add("xterm-char-measure-element"),this._measureElement.textContent="W".repeat(32),this._measureElement.setAttribute("aria-hidden","true"),this._measureElement.style.whiteSpace="pre",this._measureElement.style.fontKerning="none",this._parentElement.appendChild(this._measureElement)}measure(){return this._measureElement.style.fontFamily=this._optionsService.rawOptions.fontFamily,this._measureElement.style.fontSize=`${this._optionsService.rawOptions.fontSize}px`,this._validateAndSet(Number(this._measureElement.offsetWidth)/32,Number(this._measureElement.offsetHeight)),this._result}}class P extends y{constructor(S){super(),this._optionsService=S,this._canvas=new OffscreenCanvas(100,100),this._ctx=this._canvas.getContext("2d");let T=this._ctx.measureText("W");if(!("width"in T&&"fontBoundingBoxAscent"in T&&"fontBoundingBoxDescent"in T))throw new Error("Required font metrics not supported")}measure(){this._ctx.font=`${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`;let S=this._ctx.measureText("W");return this._validateAndSet(S.width,S.fontBoundingBoxAscent+S.fontBoundingBoxDescent),this._result}}},4269:function(i,r,s){var d=this&&this.__decorate||function(P,k,S,T){var A,D=arguments.length,F=D<3?k:T===null?T=Object.getOwnPropertyDescriptor(k,S):T;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")F=Reflect.decorate(P,k,S,T);else for(var I=P.length-1;I>=0;I--)(A=P[I])&&(F=(D<3?A(F):D>3?A(k,S,F):A(k,S))||F);return D>3&&F&&Object.defineProperty(k,S,F),F},m=this&&this.__param||function(P,k){return function(S,T){k(S,T,P)}};Object.defineProperty(r,"__esModule",{value:!0}),r.CharacterJoinerService=r.JoinedCellData=void 0;let u=s(3734),h=s(643),v=s(511),b=s(2585);class y extends u.AttributeData{constructor(k,S,T){super(),this.content=0,this.combinedData="",this.fg=k.fg,this.bg=k.bg,this.combinedData=S,this._width=T}isCombined(){return 2097152}getWidth(){return this._width}getChars(){return this.combinedData}getCode(){return 2097151}setFromCharData(k){throw new Error("not implemented")}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}}r.JoinedCellData=y;let x=r.CharacterJoinerService=class mj{constructor(k){this._bufferService=k,this._characterJoiners=[],this._nextCharacterJoinerId=0,this._workCell=new v.CellData}register(k){let S={id:this._nextCharacterJoinerId++,handler:k};return this._characterJoiners.push(S),S.id}deregister(k){for(let S=0;S1){let W=this._getJoinedRanges(A,I,F,S,D);for(let G=0;G1){let oe=this._getJoinedRanges(A,I,F,S,D);for(let W=0;W{Object.defineProperty(r,"__esModule",{value:!0}),r.CoreBrowserService=void 0;let d=s(844),m=s(8460),u=s(3656);class h extends d.Disposable{constructor(y,x,P){super(),this._textarea=y,this._window=x,this.mainDocument=P,this._isFocused=!1,this._cachedIsFocused=void 0,this._screenDprMonitor=new v(this._window),this._onDprChange=this.register(new m.EventEmitter),this.onDprChange=this._onDprChange.event,this._onWindowChange=this.register(new m.EventEmitter),this.onWindowChange=this._onWindowChange.event,this.register(this.onWindowChange(k=>this._screenDprMonitor.setWindow(k))),this.register((0,m.forwardEvent)(this._screenDprMonitor.onDprChange,this._onDprChange)),this._textarea.addEventListener("focus",()=>this._isFocused=!0),this._textarea.addEventListener("blur",()=>this._isFocused=!1)}get window(){return this._window}set window(y){this._window!==y&&(this._window=y,this._onWindowChange.fire(this._window))}get dpr(){return this.window.devicePixelRatio}get isFocused(){return this._cachedIsFocused===void 0&&(this._cachedIsFocused=this._isFocused&&this._textarea.ownerDocument.hasFocus(),queueMicrotask(()=>this._cachedIsFocused=void 0)),this._cachedIsFocused}}r.CoreBrowserService=h;class v extends d.Disposable{constructor(y){super(),this._parentWindow=y,this._windowResizeListener=this.register(new d.MutableDisposable),this._onDprChange=this.register(new m.EventEmitter),this.onDprChange=this._onDprChange.event,this._outerListener=()=>this._setDprAndFireIfDiffers(),this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this._updateDpr(),this._setWindowResizeListener(),this.register((0,d.toDisposable)(()=>this.clearListener()))}setWindow(y){this._parentWindow=y,this._setWindowResizeListener(),this._setDprAndFireIfDiffers()}_setWindowResizeListener(){this._windowResizeListener.value=(0,u.addDisposableDomListener)(this._parentWindow,"resize",()=>this._setDprAndFireIfDiffers())}_setDprAndFireIfDiffers(){this._parentWindow.devicePixelRatio!==this._currentDevicePixelRatio&&this._onDprChange.fire(this._parentWindow.devicePixelRatio),this._updateDpr()}_updateDpr(){this._outerListener&&(this._resolutionMediaMatchList?.removeListener(this._outerListener),this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this._resolutionMediaMatchList=this._parentWindow.matchMedia(`screen and (resolution: ${this._parentWindow.devicePixelRatio}dppx)`),this._resolutionMediaMatchList.addListener(this._outerListener))}clearListener(){this._resolutionMediaMatchList&&this._outerListener&&(this._resolutionMediaMatchList.removeListener(this._outerListener),this._resolutionMediaMatchList=void 0,this._outerListener=void 0)}}},779:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.LinkProviderService=void 0;let d=s(844);class m extends d.Disposable{constructor(){super(),this.linkProviders=[],this.register((0,d.toDisposable)(()=>this.linkProviders.length=0))}registerLinkProvider(h){return this.linkProviders.push(h),{dispose:()=>{let v=this.linkProviders.indexOf(h);v!==-1&&this.linkProviders.splice(v,1)}}}}r.LinkProviderService=m},8934:function(i,r,s){var d=this&&this.__decorate||function(b,y,x,P){var k,S=arguments.length,T=S<3?y:P===null?P=Object.getOwnPropertyDescriptor(y,x):P;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")T=Reflect.decorate(b,y,x,P);else for(var A=b.length-1;A>=0;A--)(k=b[A])&&(T=(S<3?k(T):S>3?k(y,x,T):k(y,x))||T);return S>3&&T&&Object.defineProperty(y,x,T),T},m=this&&this.__param||function(b,y){return function(x,P){y(x,P,b)}};Object.defineProperty(r,"__esModule",{value:!0}),r.MouseService=void 0;let u=s(4725),h=s(9806),v=r.MouseService=class{constructor(b,y){this._renderService=b,this._charSizeService=y}getCoords(b,y,x,P,k){return(0,h.getCoords)(window,b,y,x,P,this._charSizeService.hasValidSize,this._renderService.dimensions.css.cell.width,this._renderService.dimensions.css.cell.height,k)}getMouseReportCoords(b,y){let x=(0,h.getCoordsRelativeToElement)(window,b,y);if(this._charSizeService.hasValidSize)return x[0]=Math.min(Math.max(x[0],0),this._renderService.dimensions.css.canvas.width-1),x[1]=Math.min(Math.max(x[1],0),this._renderService.dimensions.css.canvas.height-1),{col:Math.floor(x[0]/this._renderService.dimensions.css.cell.width),row:Math.floor(x[1]/this._renderService.dimensions.css.cell.height),x:Math.floor(x[0]),y:Math.floor(x[1])}}};r.MouseService=v=d([m(0,u.IRenderService),m(1,u.ICharSizeService)],v)},3230:function(i,r,s){var d=this&&this.__decorate||function(k,S,T,A){var D,F=arguments.length,I=F<3?S:A===null?A=Object.getOwnPropertyDescriptor(S,T):A;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")I=Reflect.decorate(k,S,T,A);else for(var V=k.length-1;V>=0;V--)(D=k[V])&&(I=(F<3?D(I):F>3?D(S,T,I):D(S,T))||I);return F>3&&I&&Object.defineProperty(S,T,I),I},m=this&&this.__param||function(k,S){return function(T,A){S(T,A,k)}};Object.defineProperty(r,"__esModule",{value:!0}),r.RenderService=void 0;let u=s(6193),h=s(4725),v=s(8460),b=s(844),y=s(7226),x=s(2585),P=r.RenderService=class extends b.Disposable{get dimensions(){return this._renderer.value.dimensions}constructor(k,S,T,A,D,F,I,V){super(),this._rowCount=k,this._charSizeService=A,this._renderer=this.register(new b.MutableDisposable),this._pausedResizeTask=new y.DebouncedIdleTask,this._observerDisposable=this.register(new b.MutableDisposable),this._isPaused=!1,this._needsFullRefresh=!1,this._isNextRenderRedrawOnly=!0,this._needsSelectionRefresh=!1,this._canvasWidth=0,this._canvasHeight=0,this._selectionState={start:void 0,end:void 0,columnSelectMode:!1},this._onDimensionsChange=this.register(new v.EventEmitter),this.onDimensionsChange=this._onDimensionsChange.event,this._onRenderedViewportChange=this.register(new v.EventEmitter),this.onRenderedViewportChange=this._onRenderedViewportChange.event,this._onRender=this.register(new v.EventEmitter),this.onRender=this._onRender.event,this._onRefreshRequest=this.register(new v.EventEmitter),this.onRefreshRequest=this._onRefreshRequest.event,this._renderDebouncer=new u.RenderDebouncer((ae,oe)=>this._renderRows(ae,oe),I),this.register(this._renderDebouncer),this.register(I.onDprChange(()=>this.handleDevicePixelRatioChange())),this.register(F.onResize(()=>this._fullRefresh())),this.register(F.buffers.onBufferActivate(()=>this._renderer.value?.clear())),this.register(T.onOptionChange(()=>this._handleOptionsChanged())),this.register(this._charSizeService.onCharSizeChange(()=>this.handleCharSizeChanged())),this.register(D.onDecorationRegistered(()=>this._fullRefresh())),this.register(D.onDecorationRemoved(()=>this._fullRefresh())),this.register(T.onMultipleOptionChange(["customGlyphs","drawBoldTextInBrightColors","letterSpacing","lineHeight","fontFamily","fontSize","fontWeight","fontWeightBold","minimumContrastRatio","rescaleOverlappingGlyphs"],()=>{this.clear(),this.handleResize(F.cols,F.rows),this._fullRefresh()})),this.register(T.onMultipleOptionChange(["cursorBlink","cursorStyle"],()=>this.refreshRows(F.buffer.y,F.buffer.y,!0))),this.register(V.onChangeColors(()=>this._fullRefresh())),this._registerIntersectionObserver(I.window,S),this.register(I.onWindowChange(ae=>this._registerIntersectionObserver(ae,S)))}_registerIntersectionObserver(k,S){if("IntersectionObserver"in k){let T=new k.IntersectionObserver(A=>this._handleIntersectionChange(A[A.length-1]),{threshold:0});T.observe(S),this._observerDisposable.value=(0,b.toDisposable)(()=>T.disconnect())}}_handleIntersectionChange(k){this._isPaused=k.isIntersecting===void 0?k.intersectionRatio===0:!k.isIntersecting,this._isPaused||this._charSizeService.hasValidSize||this._charSizeService.measure(),!this._isPaused&&this._needsFullRefresh&&(this._pausedResizeTask.flush(),this.refreshRows(0,this._rowCount-1),this._needsFullRefresh=!1)}refreshRows(k,S,T=!1){this._isPaused?this._needsFullRefresh=!0:(T||(this._isNextRenderRedrawOnly=!1),this._renderDebouncer.refresh(k,S,this._rowCount))}_renderRows(k,S){this._renderer.value&&(k=Math.min(k,this._rowCount-1),S=Math.min(S,this._rowCount-1),this._renderer.value.renderRows(k,S),this._needsSelectionRefresh&&(this._renderer.value.handleSelectionChanged(this._selectionState.start,this._selectionState.end,this._selectionState.columnSelectMode),this._needsSelectionRefresh=!1),this._isNextRenderRedrawOnly||this._onRenderedViewportChange.fire({start:k,end:S}),this._onRender.fire({start:k,end:S}),this._isNextRenderRedrawOnly=!0)}resize(k,S){this._rowCount=S,this._fireOnCanvasResize()}_handleOptionsChanged(){this._renderer.value&&(this.refreshRows(0,this._rowCount-1),this._fireOnCanvasResize())}_fireOnCanvasResize(){this._renderer.value&&(this._renderer.value.dimensions.css.canvas.width===this._canvasWidth&&this._renderer.value.dimensions.css.canvas.height===this._canvasHeight||this._onDimensionsChange.fire(this._renderer.value.dimensions))}hasRenderer(){return!!this._renderer.value}setRenderer(k){this._renderer.value=k,this._renderer.value&&(this._renderer.value.onRequestRedraw(S=>this.refreshRows(S.start,S.end,!0)),this._needsSelectionRefresh=!0,this._fullRefresh())}addRefreshCallback(k){return this._renderDebouncer.addRefreshCallback(k)}_fullRefresh(){this._isPaused?this._needsFullRefresh=!0:this.refreshRows(0,this._rowCount-1)}clearTextureAtlas(){this._renderer.value&&(this._renderer.value.clearTextureAtlas?.(),this._fullRefresh())}handleDevicePixelRatioChange(){this._charSizeService.measure(),this._renderer.value&&(this._renderer.value.handleDevicePixelRatioChange(),this.refreshRows(0,this._rowCount-1))}handleResize(k,S){this._renderer.value&&(this._isPaused?this._pausedResizeTask.set(()=>this._renderer.value?.handleResize(k,S)):this._renderer.value.handleResize(k,S),this._fullRefresh())}handleCharSizeChanged(){this._renderer.value?.handleCharSizeChanged()}handleBlur(){this._renderer.value?.handleBlur()}handleFocus(){this._renderer.value?.handleFocus()}handleSelectionChanged(k,S,T){this._selectionState.start=k,this._selectionState.end=S,this._selectionState.columnSelectMode=T,this._renderer.value?.handleSelectionChanged(k,S,T)}handleCursorMove(){this._renderer.value?.handleCursorMove()}clear(){this._renderer.value?.clear()}};r.RenderService=P=d([m(2,x.IOptionsService),m(3,h.ICharSizeService),m(4,x.IDecorationService),m(5,x.IBufferService),m(6,h.ICoreBrowserService),m(7,h.IThemeService)],P)},9312:function(i,r,s){var d=this&&this.__decorate||function(I,V,ae,oe){var W,G=arguments.length,Ce=G<3?V:oe===null?oe=Object.getOwnPropertyDescriptor(V,ae):oe;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")Ce=Reflect.decorate(I,V,ae,oe);else for(var we=I.length-1;we>=0;we--)(W=I[we])&&(Ce=(G<3?W(Ce):G>3?W(V,ae,Ce):W(V,ae))||Ce);return G>3&&Ce&&Object.defineProperty(V,ae,Ce),Ce},m=this&&this.__param||function(I,V){return function(ae,oe){V(ae,oe,I)}};Object.defineProperty(r,"__esModule",{value:!0}),r.SelectionService=void 0;let u=s(9806),h=s(9504),v=s(456),b=s(4725),y=s(8460),x=s(844),P=s(6114),k=s(4841),S=s(511),T=s(2585),A="\xA0",D=new RegExp(A,"g"),F=r.SelectionService=class extends x.Disposable{constructor(I,V,ae,oe,W,G,Ce,we,me){super(),this._element=I,this._screenElement=V,this._linkifier=ae,this._bufferService=oe,this._coreService=W,this._mouseService=G,this._optionsService=Ce,this._renderService=we,this._coreBrowserService=me,this._dragScrollAmount=0,this._enabled=!0,this._workCell=new S.CellData,this._mouseDownTimeStamp=0,this._oldHasSelection=!1,this._oldSelectionStart=void 0,this._oldSelectionEnd=void 0,this._onLinuxMouseSelection=this.register(new y.EventEmitter),this.onLinuxMouseSelection=this._onLinuxMouseSelection.event,this._onRedrawRequest=this.register(new y.EventEmitter),this.onRequestRedraw=this._onRedrawRequest.event,this._onSelectionChange=this.register(new y.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onRequestScrollLines=this.register(new y.EventEmitter),this.onRequestScrollLines=this._onRequestScrollLines.event,this._mouseMoveListener=Ie=>this._handleMouseMove(Ie),this._mouseUpListener=Ie=>this._handleMouseUp(Ie),this._coreService.onUserInput(()=>{this.hasSelection&&this.clearSelection()}),this._trimListener=this._bufferService.buffer.lines.onTrim(Ie=>this._handleTrim(Ie)),this.register(this._bufferService.buffers.onBufferActivate(Ie=>this._handleBufferActivate(Ie))),this.enable(),this._model=new v.SelectionModel(this._bufferService),this._activeSelectionMode=0,this.register((0,x.toDisposable)(()=>{this._removeMouseDownListeners()}))}reset(){this.clearSelection()}disable(){this.clearSelection(),this._enabled=!1}enable(){this._enabled=!0}get selectionStart(){return this._model.finalSelectionStart}get selectionEnd(){return this._model.finalSelectionEnd}get hasSelection(){let I=this._model.finalSelectionStart,V=this._model.finalSelectionEnd;return!(!I||!V||I[0]===V[0]&&I[1]===V[1])}get selectionText(){let I=this._model.finalSelectionStart,V=this._model.finalSelectionEnd;if(!I||!V)return"";let ae=this._bufferService.buffer,oe=[];if(this._activeSelectionMode===3){if(I[0]===V[0])return"";let W=I[0]W.replace(D," ")).join(P.isWindows?`\r +`:` +`)}clearSelection(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()}refresh(I){this._refreshAnimationFrame||(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame(()=>this._refresh())),P.isLinux&&I&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)}_refresh(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:this._activeSelectionMode===3})}_isClickInSelection(I){let V=this._getMouseBufferCoords(I),ae=this._model.finalSelectionStart,oe=this._model.finalSelectionEnd;return!!(ae&&oe&&V)&&this._areCoordsInSelection(V,ae,oe)}isCellInSelection(I,V){let ae=this._model.finalSelectionStart,oe=this._model.finalSelectionEnd;return!(!ae||!oe)&&this._areCoordsInSelection([I,V],ae,oe)}_areCoordsInSelection(I,V,ae){return I[1]>V[1]&&I[1]=V[0]&&I[0]=V[0]}_selectWordAtCursor(I,V){let ae=this._linkifier.currentLink?.link?.range;if(ae)return this._model.selectionStart=[ae.start.x-1,ae.start.y-1],this._model.selectionStartLength=(0,k.getRangeLength)(ae,this._bufferService.cols),this._model.selectionEnd=void 0,!0;let oe=this._getMouseBufferCoords(I);return!!oe&&(this._selectWordAt(oe,V),this._model.selectionEnd=void 0,!0)}selectAll(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()}selectLines(I,V){this._model.clearSelection(),I=Math.max(I,0),V=Math.min(V,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,I],this._model.selectionEnd=[this._bufferService.cols,V],this.refresh(),this._onSelectionChange.fire()}_handleTrim(I){this._model.handleTrim(I)&&this.refresh()}_getMouseBufferCoords(I){let V=this._mouseService.getCoords(I,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(V)return V[0]--,V[1]--,V[1]+=this._bufferService.buffer.ydisp,V}_getMouseEventScrollAmount(I){let V=(0,u.getCoordsRelativeToElement)(this._coreBrowserService.window,I,this._screenElement)[1],ae=this._renderService.dimensions.css.canvas.height;return V>=0&&V<=ae?0:(V>ae&&(V-=ae),V=Math.min(Math.max(V,-50),50),V/=50,V/Math.abs(V)+Math.round(14*V))}shouldForceSelection(I){return P.isMac?I.altKey&&this._optionsService.rawOptions.macOptionClickForcesSelection:I.shiftKey}handleMouseDown(I){if(this._mouseDownTimeStamp=I.timeStamp,(I.button!==2||!this.hasSelection)&&I.button===0){if(!this._enabled){if(!this.shouldForceSelection(I))return;I.stopPropagation()}I.preventDefault(),this._dragScrollAmount=0,this._enabled&&I.shiftKey?this._handleIncrementalClick(I):I.detail===1?this._handleSingleClick(I):I.detail===2?this._handleDoubleClick(I):I.detail===3&&this._handleTripleClick(I),this._addMouseDownListeners(),this.refresh(!0)}}_addMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener("mouseup",this._mouseUpListener)),this._dragScrollIntervalTimer=this._coreBrowserService.window.setInterval(()=>this._dragScroll(),50)}_removeMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener("mouseup",this._mouseUpListener)),this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0}_handleIncrementalClick(I){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(I))}_handleSingleClick(I){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(I)?3:0,this._model.selectionStart=this._getMouseBufferCoords(I),!this._model.selectionStart)return;this._model.selectionEnd=void 0;let V=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);V&&V.length!==this._model.selectionStart[0]&&V.hasWidth(this._model.selectionStart[0])===0&&this._model.selectionStart[0]++}_handleDoubleClick(I){this._selectWordAtCursor(I,!0)&&(this._activeSelectionMode=1)}_handleTripleClick(I){let V=this._getMouseBufferCoords(I);V&&(this._activeSelectionMode=2,this._selectLineAt(V[1]))}shouldColumnSelect(I){return I.altKey&&!(P.isMac&&this._optionsService.rawOptions.macOptionClickForcesSelection)}_handleMouseMove(I){if(I.stopImmediatePropagation(),!this._model.selectionStart)return;let V=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(I),!this._model.selectionEnd)return void this.refresh(!0);this._activeSelectionMode===2?this._model.selectionEnd[1]0?this._model.selectionEnd[0]=this._bufferService.cols:this._dragScrollAmount<0&&(this._model.selectionEnd[0]=0));let ae=this._bufferService.buffer;if(this._model.selectionEnd[1]0?(this._activeSelectionMode!==3&&(this._model.selectionEnd[0]=this._bufferService.cols),this._model.selectionEnd[1]=Math.min(I.ydisp+this._bufferService.rows,I.lines.length-1)):(this._activeSelectionMode!==3&&(this._model.selectionEnd[0]=0),this._model.selectionEnd[1]=I.ydisp),this.refresh()}}_handleMouseUp(I){let V=I.timeStamp-this._mouseDownTimeStamp;if(this._removeMouseDownListeners(),this.selectionText.length<=1&&V<500&&I.altKey&&this._optionsService.rawOptions.altClickMovesCursor){if(this._bufferService.buffer.ybase===this._bufferService.buffer.ydisp){let ae=this._mouseService.getCoords(I,this._element,this._bufferService.cols,this._bufferService.rows,!1);if(ae&&ae[0]!==void 0&&ae[1]!==void 0){let oe=(0,h.moveToCellSequence)(ae[0]-1,ae[1]-1,this._bufferService,this._coreService.decPrivateModes.applicationCursorKeys);this._coreService.triggerDataEvent(oe,!0)}}}else this._fireEventIfSelectionChanged()}_fireEventIfSelectionChanged(){let I=this._model.finalSelectionStart,V=this._model.finalSelectionEnd,ae=!(!I||!V||I[0]===V[0]&&I[1]===V[1]);ae?I&&V&&(this._oldSelectionStart&&this._oldSelectionEnd&&I[0]===this._oldSelectionStart[0]&&I[1]===this._oldSelectionStart[1]&&V[0]===this._oldSelectionEnd[0]&&V[1]===this._oldSelectionEnd[1]||this._fireOnSelectionChange(I,V,ae)):this._oldHasSelection&&this._fireOnSelectionChange(I,V,ae)}_fireOnSelectionChange(I,V,ae){this._oldSelectionStart=I,this._oldSelectionEnd=V,this._oldHasSelection=ae,this._onSelectionChange.fire()}_handleBufferActivate(I){this.clearSelection(),this._trimListener.dispose(),this._trimListener=I.activeBuffer.lines.onTrim(V=>this._handleTrim(V))}_convertViewportColToCharacterIndex(I,V){let ae=V;for(let oe=0;V>=oe;oe++){let W=I.loadCell(oe,this._workCell).getChars().length;this._workCell.getWidth()===0?ae--:W>1&&V!==oe&&(ae+=W-1)}return ae}setSelection(I,V,ae){this._model.clearSelection(),this._removeMouseDownListeners(),this._model.selectionStart=[I,V],this._model.selectionStartLength=ae,this.refresh(),this._fireEventIfSelectionChanged()}rightClickSelect(I){this._isClickInSelection(I)||(this._selectWordAtCursor(I,!1)&&this.refresh(!0),this._fireEventIfSelectionChanged())}_getWordAt(I,V,ae=!0,oe=!0){if(I[0]>=this._bufferService.cols)return;let W=this._bufferService.buffer,G=W.lines.get(I[1]);if(!G)return;let Ce=W.translateBufferLineToString(I[1],!1),we=this._convertViewportColToCharacterIndex(G,I[0]),me=we,Ie=I[0]-we,ue=0,H=0,ee=0,le=0;if(Ce.charAt(we)===" "){for(;we>0&&Ce.charAt(we-1)===" ";)we--;for(;me1&&(le+=et-1,me+=et-1);je>0&&we>0&&!this._isCharWordSeparator(G.loadCell(je-1,this._workCell));){G.loadCell(je-1,this._workCell);let ye=this._workCell.getChars().length;this._workCell.getWidth()===0?(ue++,je--):ye>1&&(ee+=ye-1,we-=ye-1),we--,je--}for(;tt1&&(le+=ye-1,me+=ye-1),me++,tt++}}me++;let de=we+Ie-ue+ee,De=Math.min(this._bufferService.cols,me-we+ue+H-ee-le);if(V||Ce.slice(we,me).trim()!==""){if(ae&&de===0&&G.getCodePoint(0)!==32){let je=W.lines.get(I[1]-1);if(je&&G.isWrapped&&je.getCodePoint(this._bufferService.cols-1)!==32){let tt=this._getWordAt([this._bufferService.cols-1,I[1]-1],!1,!0,!1);if(tt){let et=this._bufferService.cols-tt.start;de-=et,De+=et}}}if(oe&&de+De===this._bufferService.cols&&G.getCodePoint(this._bufferService.cols-1)!==32){let je=W.lines.get(I[1]+1);if(je?.isWrapped&&je.getCodePoint(0)!==32){let tt=this._getWordAt([0,I[1]+1],!1,!1,!0);tt&&(De+=tt.length)}}return{start:de,length:De}}}_selectWordAt(I,V){let ae=this._getWordAt(I,V);if(ae){for(;ae.start<0;)ae.start+=this._bufferService.cols,I[1]--;this._model.selectionStart=[ae.start,I[1]],this._model.selectionStartLength=ae.length}}_selectToWordAt(I){let V=this._getWordAt(I,!0);if(V){let ae=I[1];for(;V.start<0;)V.start+=this._bufferService.cols,ae--;if(!this._model.areSelectionValuesReversed())for(;V.start+V.length>this._bufferService.cols;)V.length-=this._bufferService.cols,ae++;this._model.selectionEnd=[this._model.areSelectionValuesReversed()?V.start:V.start+V.length,ae]}}_isCharWordSeparator(I){return I.getWidth()!==0&&this._optionsService.rawOptions.wordSeparator.indexOf(I.getChars())>=0}_selectLineAt(I){let V=this._bufferService.buffer.getWrappedRangeForLine(I),ae={start:{x:0,y:V.first},end:{x:this._bufferService.cols-1,y:V.last}};this._model.selectionStart=[0,V.first],this._model.selectionEnd=void 0,this._model.selectionStartLength=(0,k.getRangeLength)(ae,this._bufferService.cols)}};r.SelectionService=F=d([m(3,T.IBufferService),m(4,T.ICoreService),m(5,b.IMouseService),m(6,T.IOptionsService),m(7,b.IRenderService),m(8,b.ICoreBrowserService)],F)},4725:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.ILinkProviderService=r.IThemeService=r.ICharacterJoinerService=r.ISelectionService=r.IRenderService=r.IMouseService=r.ICoreBrowserService=r.ICharSizeService=void 0;let d=s(8343);r.ICharSizeService=(0,d.createDecorator)("CharSizeService"),r.ICoreBrowserService=(0,d.createDecorator)("CoreBrowserService"),r.IMouseService=(0,d.createDecorator)("MouseService"),r.IRenderService=(0,d.createDecorator)("RenderService"),r.ISelectionService=(0,d.createDecorator)("SelectionService"),r.ICharacterJoinerService=(0,d.createDecorator)("CharacterJoinerService"),r.IThemeService=(0,d.createDecorator)("ThemeService"),r.ILinkProviderService=(0,d.createDecorator)("LinkProviderService")},6731:function(i,r,s){var d=this&&this.__decorate||function(F,I,V,ae){var oe,W=arguments.length,G=W<3?I:ae===null?ae=Object.getOwnPropertyDescriptor(I,V):ae;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")G=Reflect.decorate(F,I,V,ae);else for(var Ce=F.length-1;Ce>=0;Ce--)(oe=F[Ce])&&(G=(W<3?oe(G):W>3?oe(I,V,G):oe(I,V))||G);return W>3&&G&&Object.defineProperty(I,V,G),G},m=this&&this.__param||function(F,I){return function(V,ae){I(V,ae,F)}};Object.defineProperty(r,"__esModule",{value:!0}),r.ThemeService=r.DEFAULT_ANSI_COLORS=void 0;let u=s(7239),h=s(8055),v=s(8460),b=s(844),y=s(2585),x=h.css.toColor("#ffffff"),P=h.css.toColor("#000000"),k=h.css.toColor("#ffffff"),S=h.css.toColor("#000000"),T={css:"rgba(255, 255, 255, 0.3)",rgba:4294967117};r.DEFAULT_ANSI_COLORS=Object.freeze((()=>{let F=[h.css.toColor("#2e3436"),h.css.toColor("#cc0000"),h.css.toColor("#4e9a06"),h.css.toColor("#c4a000"),h.css.toColor("#3465a4"),h.css.toColor("#75507b"),h.css.toColor("#06989a"),h.css.toColor("#d3d7cf"),h.css.toColor("#555753"),h.css.toColor("#ef2929"),h.css.toColor("#8ae234"),h.css.toColor("#fce94f"),h.css.toColor("#729fcf"),h.css.toColor("#ad7fa8"),h.css.toColor("#34e2e2"),h.css.toColor("#eeeeec")],I=[0,95,135,175,215,255];for(let V=0;V<216;V++){let ae=I[V/36%6|0],oe=I[V/6%6|0],W=I[V%6];F.push({css:h.channels.toCss(ae,oe,W),rgba:h.channels.toRgba(ae,oe,W)})}for(let V=0;V<24;V++){let ae=8+10*V;F.push({css:h.channels.toCss(ae,ae,ae),rgba:h.channels.toRgba(ae,ae,ae)})}return F})());let A=r.ThemeService=class extends b.Disposable{get colors(){return this._colors}constructor(F){super(),this._optionsService=F,this._contrastCache=new u.ColorContrastCache,this._halfContrastCache=new u.ColorContrastCache,this._onChangeColors=this.register(new v.EventEmitter),this.onChangeColors=this._onChangeColors.event,this._colors={foreground:x,background:P,cursor:k,cursorAccent:S,selectionForeground:void 0,selectionBackgroundTransparent:T,selectionBackgroundOpaque:h.color.blend(P,T),selectionInactiveBackgroundTransparent:T,selectionInactiveBackgroundOpaque:h.color.blend(P,T),ansi:r.DEFAULT_ANSI_COLORS.slice(),contrastCache:this._contrastCache,halfContrastCache:this._halfContrastCache},this._updateRestoreColors(),this._setTheme(this._optionsService.rawOptions.theme),this.register(this._optionsService.onSpecificOptionChange("minimumContrastRatio",()=>this._contrastCache.clear())),this.register(this._optionsService.onSpecificOptionChange("theme",()=>this._setTheme(this._optionsService.rawOptions.theme)))}_setTheme(F={}){let I=this._colors;if(I.foreground=D(F.foreground,x),I.background=D(F.background,P),I.cursor=D(F.cursor,k),I.cursorAccent=D(F.cursorAccent,S),I.selectionBackgroundTransparent=D(F.selectionBackground,T),I.selectionBackgroundOpaque=h.color.blend(I.background,I.selectionBackgroundTransparent),I.selectionInactiveBackgroundTransparent=D(F.selectionInactiveBackground,I.selectionBackgroundTransparent),I.selectionInactiveBackgroundOpaque=h.color.blend(I.background,I.selectionInactiveBackgroundTransparent),I.selectionForeground=F.selectionForeground?D(F.selectionForeground,h.NULL_COLOR):void 0,I.selectionForeground===h.NULL_COLOR&&(I.selectionForeground=void 0),h.color.isOpaque(I.selectionBackgroundTransparent)&&(I.selectionBackgroundTransparent=h.color.opacity(I.selectionBackgroundTransparent,.3)),h.color.isOpaque(I.selectionInactiveBackgroundTransparent)&&(I.selectionInactiveBackgroundTransparent=h.color.opacity(I.selectionInactiveBackgroundTransparent,.3)),I.ansi=r.DEFAULT_ANSI_COLORS.slice(),I.ansi[0]=D(F.black,r.DEFAULT_ANSI_COLORS[0]),I.ansi[1]=D(F.red,r.DEFAULT_ANSI_COLORS[1]),I.ansi[2]=D(F.green,r.DEFAULT_ANSI_COLORS[2]),I.ansi[3]=D(F.yellow,r.DEFAULT_ANSI_COLORS[3]),I.ansi[4]=D(F.blue,r.DEFAULT_ANSI_COLORS[4]),I.ansi[5]=D(F.magenta,r.DEFAULT_ANSI_COLORS[5]),I.ansi[6]=D(F.cyan,r.DEFAULT_ANSI_COLORS[6]),I.ansi[7]=D(F.white,r.DEFAULT_ANSI_COLORS[7]),I.ansi[8]=D(F.brightBlack,r.DEFAULT_ANSI_COLORS[8]),I.ansi[9]=D(F.brightRed,r.DEFAULT_ANSI_COLORS[9]),I.ansi[10]=D(F.brightGreen,r.DEFAULT_ANSI_COLORS[10]),I.ansi[11]=D(F.brightYellow,r.DEFAULT_ANSI_COLORS[11]),I.ansi[12]=D(F.brightBlue,r.DEFAULT_ANSI_COLORS[12]),I.ansi[13]=D(F.brightMagenta,r.DEFAULT_ANSI_COLORS[13]),I.ansi[14]=D(F.brightCyan,r.DEFAULT_ANSI_COLORS[14]),I.ansi[15]=D(F.brightWhite,r.DEFAULT_ANSI_COLORS[15]),F.extendedAnsi){let V=Math.min(I.ansi.length-16,F.extendedAnsi.length);for(let ae=0;ae{Object.defineProperty(r,"__esModule",{value:!0}),r.CircularList=void 0;let d=s(8460),m=s(844);class u extends m.Disposable{constructor(v){super(),this._maxLength=v,this.onDeleteEmitter=this.register(new d.EventEmitter),this.onDelete=this.onDeleteEmitter.event,this.onInsertEmitter=this.register(new d.EventEmitter),this.onInsert=this.onInsertEmitter.event,this.onTrimEmitter=this.register(new d.EventEmitter),this.onTrim=this.onTrimEmitter.event,this._array=new Array(this._maxLength),this._startIndex=0,this._length=0}get maxLength(){return this._maxLength}set maxLength(v){if(this._maxLength===v)return;let b=new Array(v);for(let y=0;ythis._length)for(let b=this._length;b=v;x--)this._array[this._getCyclicIndex(x+y.length)]=this._array[this._getCyclicIndex(x)];for(let x=0;xthis._maxLength){let x=this._length+y.length-this._maxLength;this._startIndex+=x,this._length=this._maxLength,this.onTrimEmitter.fire(x)}else this._length+=y.length}trimStart(v){v>this._length&&(v=this._length),this._startIndex+=v,this._length-=v,this.onTrimEmitter.fire(v)}shiftElements(v,b,y){if(!(b<=0)){if(v<0||v>=this._length)throw new Error("start argument out of range");if(v+y<0)throw new Error("Cannot shift elements in list beyond index 0");if(y>0){for(let P=b-1;P>=0;P--)this.set(v+P+y,this.get(v+P));let x=v+b+y-this._length;if(x>0)for(this._length+=x;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(let x=0;x{Object.defineProperty(r,"__esModule",{value:!0}),r.clone=void 0,r.clone=function s(d,m=5){if(typeof d!="object")return d;let u=Array.isArray(d)?[]:{};for(let h in d)u[h]=m<=1?d[h]:d[h]&&s(d[h],m-1);return u}},8055:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.contrastRatio=r.toPaddedHex=r.rgba=r.rgb=r.css=r.color=r.channels=r.NULL_COLOR=void 0;let s=0,d=0,m=0,u=0;var h,v,b,y,x;function P(S){let T=S.toString(16);return T.length<2?"0"+T:T}function k(S,T){return S>>0},S.toColor=function(T,A,D,F){return{css:S.toCss(T,A,D,F),rgba:S.toRgba(T,A,D,F)}}})(h||(r.channels=h={})),(function(S){function T(A,D){return u=Math.round(255*D),[s,d,m]=x.toChannels(A.rgba),{css:h.toCss(s,d,m,u),rgba:h.toRgba(s,d,m,u)}}S.blend=function(A,D){if(u=(255&D.rgba)/255,u===1)return{css:D.css,rgba:D.rgba};let F=D.rgba>>24&255,I=D.rgba>>16&255,V=D.rgba>>8&255,ae=A.rgba>>24&255,oe=A.rgba>>16&255,W=A.rgba>>8&255;return s=ae+Math.round((F-ae)*u),d=oe+Math.round((I-oe)*u),m=W+Math.round((V-W)*u),{css:h.toCss(s,d,m),rgba:h.toRgba(s,d,m)}},S.isOpaque=function(A){return(255&A.rgba)==255},S.ensureContrastRatio=function(A,D,F){let I=x.ensureContrastRatio(A.rgba,D.rgba,F);if(I)return h.toColor(I>>24&255,I>>16&255,I>>8&255)},S.opaque=function(A){let D=(255|A.rgba)>>>0;return[s,d,m]=x.toChannels(D),{css:h.toCss(s,d,m),rgba:D}},S.opacity=T,S.multiplyOpacity=function(A,D){return u=255&A.rgba,T(A,u*D/255)},S.toColorRGB=function(A){return[A.rgba>>24&255,A.rgba>>16&255,A.rgba>>8&255]}})(v||(r.color=v={})),(function(S){let T,A;try{let D=document.createElement("canvas");D.width=1,D.height=1;let F=D.getContext("2d",{willReadFrequently:!0});F&&(T=F,T.globalCompositeOperation="copy",A=T.createLinearGradient(0,0,1,1))}catch{}S.toColor=function(D){if(D.match(/#[\da-f]{3,8}/i))switch(D.length){case 4:return s=parseInt(D.slice(1,2).repeat(2),16),d=parseInt(D.slice(2,3).repeat(2),16),m=parseInt(D.slice(3,4).repeat(2),16),h.toColor(s,d,m);case 5:return s=parseInt(D.slice(1,2).repeat(2),16),d=parseInt(D.slice(2,3).repeat(2),16),m=parseInt(D.slice(3,4).repeat(2),16),u=parseInt(D.slice(4,5).repeat(2),16),h.toColor(s,d,m,u);case 7:return{css:D,rgba:(parseInt(D.slice(1),16)<<8|255)>>>0};case 9:return{css:D,rgba:parseInt(D.slice(1),16)>>>0}}let F=D.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/);if(F)return s=parseInt(F[1]),d=parseInt(F[2]),m=parseInt(F[3]),u=Math.round(255*(F[5]===void 0?1:parseFloat(F[5]))),h.toColor(s,d,m,u);if(!T||!A)throw new Error("css.toColor: Unsupported css format");if(T.fillStyle=A,T.fillStyle=D,typeof T.fillStyle!="string")throw new Error("css.toColor: Unsupported css format");if(T.fillRect(0,0,1,1),[s,d,m,u]=T.getImageData(0,0,1,1).data,u!==255)throw new Error("css.toColor: Unsupported css format");return{rgba:h.toRgba(s,d,m,u),css:D}}})(b||(r.css=b={})),(function(S){function T(A,D,F){let I=A/255,V=D/255,ae=F/255;return .2126*(I<=.03928?I/12.92:Math.pow((I+.055)/1.055,2.4))+.7152*(V<=.03928?V/12.92:Math.pow((V+.055)/1.055,2.4))+.0722*(ae<=.03928?ae/12.92:Math.pow((ae+.055)/1.055,2.4))}S.relativeLuminance=function(A){return T(A>>16&255,A>>8&255,255&A)},S.relativeLuminance2=T})(y||(r.rgb=y={})),(function(S){function T(D,F,I){let V=D>>24&255,ae=D>>16&255,oe=D>>8&255,W=F>>24&255,G=F>>16&255,Ce=F>>8&255,we=k(y.relativeLuminance2(W,G,Ce),y.relativeLuminance2(V,ae,oe));for(;we0||G>0||Ce>0);)W-=Math.max(0,Math.ceil(.1*W)),G-=Math.max(0,Math.ceil(.1*G)),Ce-=Math.max(0,Math.ceil(.1*Ce)),we=k(y.relativeLuminance2(W,G,Ce),y.relativeLuminance2(V,ae,oe));return(W<<24|G<<16|Ce<<8|255)>>>0}function A(D,F,I){let V=D>>24&255,ae=D>>16&255,oe=D>>8&255,W=F>>24&255,G=F>>16&255,Ce=F>>8&255,we=k(y.relativeLuminance2(W,G,Ce),y.relativeLuminance2(V,ae,oe));for(;we>>0}S.blend=function(D,F){if(u=(255&F)/255,u===1)return F;let I=F>>24&255,V=F>>16&255,ae=F>>8&255,oe=D>>24&255,W=D>>16&255,G=D>>8&255;return s=oe+Math.round((I-oe)*u),d=W+Math.round((V-W)*u),m=G+Math.round((ae-G)*u),h.toRgba(s,d,m)},S.ensureContrastRatio=function(D,F,I){let V=y.relativeLuminance(D>>8),ae=y.relativeLuminance(F>>8);if(k(V,ae)>8));if(Cek(V,y.relativeLuminance(we>>8))?G:we}return G}let oe=A(D,F,I),W=k(V,y.relativeLuminance(oe>>8));if(Wk(V,y.relativeLuminance(G>>8))?oe:G}return oe}},S.reduceLuminance=T,S.increaseLuminance=A,S.toChannels=function(D){return[D>>24&255,D>>16&255,D>>8&255,255&D]}})(x||(r.rgba=x={})),r.toPaddedHex=P,r.contrastRatio=k},8969:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.CoreTerminal=void 0;let d=s(844),m=s(2585),u=s(4348),h=s(7866),v=s(744),b=s(7302),y=s(6975),x=s(8460),P=s(1753),k=s(1480),S=s(7994),T=s(9282),A=s(5435),D=s(5981),F=s(2660),I=!1;class V extends d.Disposable{get onScroll(){return this._onScrollApi||(this._onScrollApi=this.register(new x.EventEmitter),this._onScroll.event(oe=>{this._onScrollApi?.fire(oe.position)})),this._onScrollApi.event}get cols(){return this._bufferService.cols}get rows(){return this._bufferService.rows}get buffers(){return this._bufferService.buffers}get options(){return this.optionsService.options}set options(oe){for(let W in oe)this.optionsService.options[W]=oe[W]}constructor(oe){super(),this._windowsWrappingHeuristics=this.register(new d.MutableDisposable),this._onBinary=this.register(new x.EventEmitter),this.onBinary=this._onBinary.event,this._onData=this.register(new x.EventEmitter),this.onData=this._onData.event,this._onLineFeed=this.register(new x.EventEmitter),this.onLineFeed=this._onLineFeed.event,this._onResize=this.register(new x.EventEmitter),this.onResize=this._onResize.event,this._onWriteParsed=this.register(new x.EventEmitter),this.onWriteParsed=this._onWriteParsed.event,this._onScroll=this.register(new x.EventEmitter),this._instantiationService=new u.InstantiationService,this.optionsService=this.register(new b.OptionsService(oe)),this._instantiationService.setService(m.IOptionsService,this.optionsService),this._bufferService=this.register(this._instantiationService.createInstance(v.BufferService)),this._instantiationService.setService(m.IBufferService,this._bufferService),this._logService=this.register(this._instantiationService.createInstance(h.LogService)),this._instantiationService.setService(m.ILogService,this._logService),this.coreService=this.register(this._instantiationService.createInstance(y.CoreService)),this._instantiationService.setService(m.ICoreService,this.coreService),this.coreMouseService=this.register(this._instantiationService.createInstance(P.CoreMouseService)),this._instantiationService.setService(m.ICoreMouseService,this.coreMouseService),this.unicodeService=this.register(this._instantiationService.createInstance(k.UnicodeService)),this._instantiationService.setService(m.IUnicodeService,this.unicodeService),this._charsetService=this._instantiationService.createInstance(S.CharsetService),this._instantiationService.setService(m.ICharsetService,this._charsetService),this._oscLinkService=this._instantiationService.createInstance(F.OscLinkService),this._instantiationService.setService(m.IOscLinkService,this._oscLinkService),this._inputHandler=this.register(new A.InputHandler(this._bufferService,this._charsetService,this.coreService,this._logService,this.optionsService,this._oscLinkService,this.coreMouseService,this.unicodeService)),this.register((0,x.forwardEvent)(this._inputHandler.onLineFeed,this._onLineFeed)),this.register(this._inputHandler),this.register((0,x.forwardEvent)(this._bufferService.onResize,this._onResize)),this.register((0,x.forwardEvent)(this.coreService.onData,this._onData)),this.register((0,x.forwardEvent)(this.coreService.onBinary,this._onBinary)),this.register(this.coreService.onRequestScrollToBottom(()=>this.scrollToBottom())),this.register(this.coreService.onUserInput(()=>this._writeBuffer.handleUserInput())),this.register(this.optionsService.onMultipleOptionChange(["windowsMode","windowsPty"],()=>this._handleWindowsPtyOptionChange())),this.register(this._bufferService.onScroll(W=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp,source:0}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)})),this.register(this._inputHandler.onScroll(W=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp,source:0}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)})),this._writeBuffer=this.register(new D.WriteBuffer((W,G)=>this._inputHandler.parse(W,G))),this.register((0,x.forwardEvent)(this._writeBuffer.onWriteParsed,this._onWriteParsed))}write(oe,W){this._writeBuffer.write(oe,W)}writeSync(oe,W){this._logService.logLevel<=m.LogLevelEnum.WARN&&!I&&(this._logService.warn("writeSync is unreliable and will be removed soon."),I=!0),this._writeBuffer.writeSync(oe,W)}input(oe,W=!0){this.coreService.triggerDataEvent(oe,W)}resize(oe,W){isNaN(oe)||isNaN(W)||(oe=Math.max(oe,v.MINIMUM_COLS),W=Math.max(W,v.MINIMUM_ROWS),this._bufferService.resize(oe,W))}scroll(oe,W=!1){this._bufferService.scroll(oe,W)}scrollLines(oe,W,G){this._bufferService.scrollLines(oe,W,G)}scrollPages(oe){this.scrollLines(oe*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(oe){let W=oe-this._bufferService.buffer.ydisp;W!==0&&this.scrollLines(W)}registerEscHandler(oe,W){return this._inputHandler.registerEscHandler(oe,W)}registerDcsHandler(oe,W){return this._inputHandler.registerDcsHandler(oe,W)}registerCsiHandler(oe,W){return this._inputHandler.registerCsiHandler(oe,W)}registerOscHandler(oe,W){return this._inputHandler.registerOscHandler(oe,W)}_setup(){this._handleWindowsPtyOptionChange()}reset(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this.coreService.reset(),this.coreMouseService.reset()}_handleWindowsPtyOptionChange(){let oe=!1,W=this.optionsService.rawOptions.windowsPty;W&&W.buildNumber!==void 0&&W.buildNumber!==void 0?oe=W.backend==="conpty"&&W.buildNumber<21376:this.optionsService.rawOptions.windowsMode&&(oe=!0),oe?this._enableWindowsWrappingHeuristics():this._windowsWrappingHeuristics.clear()}_enableWindowsWrappingHeuristics(){if(!this._windowsWrappingHeuristics.value){let oe=[];oe.push(this.onLineFeed(T.updateWindowsModeWrappedState.bind(null,this._bufferService))),oe.push(this.registerCsiHandler({final:"H"},()=>((0,T.updateWindowsModeWrappedState)(this._bufferService),!1))),this._windowsWrappingHeuristics.value=(0,d.toDisposable)(()=>{for(let W of oe)W.dispose()})}}}r.CoreTerminal=V},8460:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.runAndSubscribe=r.forwardEvent=r.EventEmitter=void 0,r.EventEmitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=s=>(this._listeners.push(s),{dispose:()=>{if(!this._disposed){for(let d=0;dd.fire(m))},r.runAndSubscribe=function(s,d){return d(void 0),s(m=>d(m))}},5435:function(i,r,s){var d=this&&this.__decorate||function(ue,H,ee,le){var de,De=arguments.length,je=De<3?H:le===null?le=Object.getOwnPropertyDescriptor(H,ee):le;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")je=Reflect.decorate(ue,H,ee,le);else for(var tt=ue.length-1;tt>=0;tt--)(de=ue[tt])&&(je=(De<3?de(je):De>3?de(H,ee,je):de(H,ee))||je);return De>3&&je&&Object.defineProperty(H,ee,je),je},m=this&&this.__param||function(ue,H){return function(ee,le){H(ee,le,ue)}};Object.defineProperty(r,"__esModule",{value:!0}),r.InputHandler=r.WindowsOptionsReportType=void 0;let u=s(2584),h=s(7116),v=s(2015),b=s(844),y=s(482),x=s(8437),P=s(8460),k=s(643),S=s(511),T=s(3734),A=s(2585),D=s(1480),F=s(6242),I=s(6351),V=s(5941),ae={"(":0,")":1,"*":2,"+":3,"-":1,".":2},oe=131072;function W(ue,H){if(ue>24)return H.setWinLines||!1;switch(ue){case 1:return!!H.restoreWin;case 2:return!!H.minimizeWin;case 3:return!!H.setWinPosition;case 4:return!!H.setWinSizePixels;case 5:return!!H.raiseWin;case 6:return!!H.lowerWin;case 7:return!!H.refreshWin;case 8:return!!H.setWinSizeChars;case 9:return!!H.maximizeWin;case 10:return!!H.fullscreenWin;case 11:return!!H.getWinState;case 13:return!!H.getWinPosition;case 14:return!!H.getWinSizePixels;case 15:return!!H.getScreenSizePixels;case 16:return!!H.getCellSizePixels;case 18:return!!H.getWinSizeChars;case 19:return!!H.getScreenSizeChars;case 20:return!!H.getIconTitle;case 21:return!!H.getWinTitle;case 22:return!!H.pushTitle;case 23:return!!H.popTitle;case 24:return!!H.setWinLines}return!1}var G;(function(ue){ue[ue.GET_WIN_SIZE_PIXELS=0]="GET_WIN_SIZE_PIXELS",ue[ue.GET_CELL_SIZE_PIXELS=1]="GET_CELL_SIZE_PIXELS"})(G||(r.WindowsOptionsReportType=G={}));let Ce=0;class we extends b.Disposable{getAttrData(){return this._curAttrData}constructor(H,ee,le,de,De,je,tt,et,ye=new v.EscapeSequenceParser){super(),this._bufferService=H,this._charsetService=ee,this._coreService=le,this._logService=de,this._optionsService=De,this._oscLinkService=je,this._coreMouseService=tt,this._unicodeService=et,this._parser=ye,this._parseBuffer=new Uint32Array(4096),this._stringDecoder=new y.StringToUtf32,this._utf8Decoder=new y.Utf8ToUtf32,this._workCell=new S.CellData,this._windowTitle="",this._iconName="",this._windowTitleStack=[],this._iconNameStack=[],this._curAttrData=x.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=x.DEFAULT_ATTR_DATA.clone(),this._onRequestBell=this.register(new P.EventEmitter),this.onRequestBell=this._onRequestBell.event,this._onRequestRefreshRows=this.register(new P.EventEmitter),this.onRequestRefreshRows=this._onRequestRefreshRows.event,this._onRequestReset=this.register(new P.EventEmitter),this.onRequestReset=this._onRequestReset.event,this._onRequestSendFocus=this.register(new P.EventEmitter),this.onRequestSendFocus=this._onRequestSendFocus.event,this._onRequestSyncScrollBar=this.register(new P.EventEmitter),this.onRequestSyncScrollBar=this._onRequestSyncScrollBar.event,this._onRequestWindowsOptionsReport=this.register(new P.EventEmitter),this.onRequestWindowsOptionsReport=this._onRequestWindowsOptionsReport.event,this._onA11yChar=this.register(new P.EventEmitter),this.onA11yChar=this._onA11yChar.event,this._onA11yTab=this.register(new P.EventEmitter),this.onA11yTab=this._onA11yTab.event,this._onCursorMove=this.register(new P.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onLineFeed=this.register(new P.EventEmitter),this.onLineFeed=this._onLineFeed.event,this._onScroll=this.register(new P.EventEmitter),this.onScroll=this._onScroll.event,this._onTitleChange=this.register(new P.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onColor=this.register(new P.EventEmitter),this.onColor=this._onColor.event,this._parseStack={paused:!1,cursorStartX:0,cursorStartY:0,decodedLength:0,position:0},this._specialColors=[256,257,258],this.register(this._parser),this._dirtyRowTracker=new me(this._bufferService),this._activeBuffer=this._bufferService.buffer,this.register(this._bufferService.buffers.onBufferActivate(Y=>this._activeBuffer=Y.activeBuffer)),this._parser.setCsiHandlerFallback((Y,Re)=>{this._logService.debug("Unknown CSI code: ",{identifier:this._parser.identToString(Y),params:Re.toArray()})}),this._parser.setEscHandlerFallback(Y=>{this._logService.debug("Unknown ESC code: ",{identifier:this._parser.identToString(Y)})}),this._parser.setExecuteHandlerFallback(Y=>{this._logService.debug("Unknown EXECUTE code: ",{code:Y})}),this._parser.setOscHandlerFallback((Y,Re,Ze)=>{this._logService.debug("Unknown OSC code: ",{identifier:Y,action:Re,data:Ze})}),this._parser.setDcsHandlerFallback((Y,Re,Ze)=>{Re==="HOOK"&&(Ze=Ze.toArray()),this._logService.debug("Unknown DCS code: ",{identifier:this._parser.identToString(Y),action:Re,payload:Ze})}),this._parser.setPrintHandler((Y,Re,Ze)=>this.print(Y,Re,Ze)),this._parser.registerCsiHandler({final:"@"},Y=>this.insertChars(Y)),this._parser.registerCsiHandler({intermediates:" ",final:"@"},Y=>this.scrollLeft(Y)),this._parser.registerCsiHandler({final:"A"},Y=>this.cursorUp(Y)),this._parser.registerCsiHandler({intermediates:" ",final:"A"},Y=>this.scrollRight(Y)),this._parser.registerCsiHandler({final:"B"},Y=>this.cursorDown(Y)),this._parser.registerCsiHandler({final:"C"},Y=>this.cursorForward(Y)),this._parser.registerCsiHandler({final:"D"},Y=>this.cursorBackward(Y)),this._parser.registerCsiHandler({final:"E"},Y=>this.cursorNextLine(Y)),this._parser.registerCsiHandler({final:"F"},Y=>this.cursorPrecedingLine(Y)),this._parser.registerCsiHandler({final:"G"},Y=>this.cursorCharAbsolute(Y)),this._parser.registerCsiHandler({final:"H"},Y=>this.cursorPosition(Y)),this._parser.registerCsiHandler({final:"I"},Y=>this.cursorForwardTab(Y)),this._parser.registerCsiHandler({final:"J"},Y=>this.eraseInDisplay(Y,!1)),this._parser.registerCsiHandler({prefix:"?",final:"J"},Y=>this.eraseInDisplay(Y,!0)),this._parser.registerCsiHandler({final:"K"},Y=>this.eraseInLine(Y,!1)),this._parser.registerCsiHandler({prefix:"?",final:"K"},Y=>this.eraseInLine(Y,!0)),this._parser.registerCsiHandler({final:"L"},Y=>this.insertLines(Y)),this._parser.registerCsiHandler({final:"M"},Y=>this.deleteLines(Y)),this._parser.registerCsiHandler({final:"P"},Y=>this.deleteChars(Y)),this._parser.registerCsiHandler({final:"S"},Y=>this.scrollUp(Y)),this._parser.registerCsiHandler({final:"T"},Y=>this.scrollDown(Y)),this._parser.registerCsiHandler({final:"X"},Y=>this.eraseChars(Y)),this._parser.registerCsiHandler({final:"Z"},Y=>this.cursorBackwardTab(Y)),this._parser.registerCsiHandler({final:"`"},Y=>this.charPosAbsolute(Y)),this._parser.registerCsiHandler({final:"a"},Y=>this.hPositionRelative(Y)),this._parser.registerCsiHandler({final:"b"},Y=>this.repeatPrecedingCharacter(Y)),this._parser.registerCsiHandler({final:"c"},Y=>this.sendDeviceAttributesPrimary(Y)),this._parser.registerCsiHandler({prefix:">",final:"c"},Y=>this.sendDeviceAttributesSecondary(Y)),this._parser.registerCsiHandler({final:"d"},Y=>this.linePosAbsolute(Y)),this._parser.registerCsiHandler({final:"e"},Y=>this.vPositionRelative(Y)),this._parser.registerCsiHandler({final:"f"},Y=>this.hVPosition(Y)),this._parser.registerCsiHandler({final:"g"},Y=>this.tabClear(Y)),this._parser.registerCsiHandler({final:"h"},Y=>this.setMode(Y)),this._parser.registerCsiHandler({prefix:"?",final:"h"},Y=>this.setModePrivate(Y)),this._parser.registerCsiHandler({final:"l"},Y=>this.resetMode(Y)),this._parser.registerCsiHandler({prefix:"?",final:"l"},Y=>this.resetModePrivate(Y)),this._parser.registerCsiHandler({final:"m"},Y=>this.charAttributes(Y)),this._parser.registerCsiHandler({final:"n"},Y=>this.deviceStatus(Y)),this._parser.registerCsiHandler({prefix:"?",final:"n"},Y=>this.deviceStatusPrivate(Y)),this._parser.registerCsiHandler({intermediates:"!",final:"p"},Y=>this.softReset(Y)),this._parser.registerCsiHandler({intermediates:" ",final:"q"},Y=>this.setCursorStyle(Y)),this._parser.registerCsiHandler({final:"r"},Y=>this.setScrollRegion(Y)),this._parser.registerCsiHandler({final:"s"},Y=>this.saveCursor(Y)),this._parser.registerCsiHandler({final:"t"},Y=>this.windowOptions(Y)),this._parser.registerCsiHandler({final:"u"},Y=>this.restoreCursor(Y)),this._parser.registerCsiHandler({intermediates:"'",final:"}"},Y=>this.insertColumns(Y)),this._parser.registerCsiHandler({intermediates:"'",final:"~"},Y=>this.deleteColumns(Y)),this._parser.registerCsiHandler({intermediates:'"',final:"q"},Y=>this.selectProtected(Y)),this._parser.registerCsiHandler({intermediates:"$",final:"p"},Y=>this.requestMode(Y,!0)),this._parser.registerCsiHandler({prefix:"?",intermediates:"$",final:"p"},Y=>this.requestMode(Y,!1)),this._parser.setExecuteHandler(u.C0.BEL,()=>this.bell()),this._parser.setExecuteHandler(u.C0.LF,()=>this.lineFeed()),this._parser.setExecuteHandler(u.C0.VT,()=>this.lineFeed()),this._parser.setExecuteHandler(u.C0.FF,()=>this.lineFeed()),this._parser.setExecuteHandler(u.C0.CR,()=>this.carriageReturn()),this._parser.setExecuteHandler(u.C0.BS,()=>this.backspace()),this._parser.setExecuteHandler(u.C0.HT,()=>this.tab()),this._parser.setExecuteHandler(u.C0.SO,()=>this.shiftOut()),this._parser.setExecuteHandler(u.C0.SI,()=>this.shiftIn()),this._parser.setExecuteHandler(u.C1.IND,()=>this.index()),this._parser.setExecuteHandler(u.C1.NEL,()=>this.nextLine()),this._parser.setExecuteHandler(u.C1.HTS,()=>this.tabSet()),this._parser.registerOscHandler(0,new F.OscHandler(Y=>(this.setTitle(Y),this.setIconName(Y),!0))),this._parser.registerOscHandler(1,new F.OscHandler(Y=>this.setIconName(Y))),this._parser.registerOscHandler(2,new F.OscHandler(Y=>this.setTitle(Y))),this._parser.registerOscHandler(4,new F.OscHandler(Y=>this.setOrReportIndexedColor(Y))),this._parser.registerOscHandler(8,new F.OscHandler(Y=>this.setHyperlink(Y))),this._parser.registerOscHandler(10,new F.OscHandler(Y=>this.setOrReportFgColor(Y))),this._parser.registerOscHandler(11,new F.OscHandler(Y=>this.setOrReportBgColor(Y))),this._parser.registerOscHandler(12,new F.OscHandler(Y=>this.setOrReportCursorColor(Y))),this._parser.registerOscHandler(104,new F.OscHandler(Y=>this.restoreIndexedColor(Y))),this._parser.registerOscHandler(110,new F.OscHandler(Y=>this.restoreFgColor(Y))),this._parser.registerOscHandler(111,new F.OscHandler(Y=>this.restoreBgColor(Y))),this._parser.registerOscHandler(112,new F.OscHandler(Y=>this.restoreCursorColor(Y))),this._parser.registerEscHandler({final:"7"},()=>this.saveCursor()),this._parser.registerEscHandler({final:"8"},()=>this.restoreCursor()),this._parser.registerEscHandler({final:"D"},()=>this.index()),this._parser.registerEscHandler({final:"E"},()=>this.nextLine()),this._parser.registerEscHandler({final:"H"},()=>this.tabSet()),this._parser.registerEscHandler({final:"M"},()=>this.reverseIndex()),this._parser.registerEscHandler({final:"="},()=>this.keypadApplicationMode()),this._parser.registerEscHandler({final:">"},()=>this.keypadNumericMode()),this._parser.registerEscHandler({final:"c"},()=>this.fullReset()),this._parser.registerEscHandler({final:"n"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"o"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"|"},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:"}"},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:"~"},()=>this.setgLevel(1)),this._parser.registerEscHandler({intermediates:"%",final:"@"},()=>this.selectDefaultCharset()),this._parser.registerEscHandler({intermediates:"%",final:"G"},()=>this.selectDefaultCharset());for(let Y in h.CHARSETS)this._parser.registerEscHandler({intermediates:"(",final:Y},()=>this.selectCharset("("+Y)),this._parser.registerEscHandler({intermediates:")",final:Y},()=>this.selectCharset(")"+Y)),this._parser.registerEscHandler({intermediates:"*",final:Y},()=>this.selectCharset("*"+Y)),this._parser.registerEscHandler({intermediates:"+",final:Y},()=>this.selectCharset("+"+Y)),this._parser.registerEscHandler({intermediates:"-",final:Y},()=>this.selectCharset("-"+Y)),this._parser.registerEscHandler({intermediates:".",final:Y},()=>this.selectCharset("."+Y)),this._parser.registerEscHandler({intermediates:"/",final:Y},()=>this.selectCharset("/"+Y));this._parser.registerEscHandler({intermediates:"#",final:"8"},()=>this.screenAlignmentPattern()),this._parser.setErrorHandler(Y=>(this._logService.error("Parsing error: ",Y),Y)),this._parser.registerDcsHandler({intermediates:"$",final:"q"},new I.DcsHandler((Y,Re)=>this.requestStatusString(Y,Re)))}_preserveStack(H,ee,le,de){this._parseStack.paused=!0,this._parseStack.cursorStartX=H,this._parseStack.cursorStartY=ee,this._parseStack.decodedLength=le,this._parseStack.position=de}_logSlowResolvingAsync(H){this._logService.logLevel<=A.LogLevelEnum.WARN&&Promise.race([H,new Promise((ee,le)=>setTimeout(()=>le("#SLOW_TIMEOUT"),5e3))]).catch(ee=>{if(ee!=="#SLOW_TIMEOUT")throw ee;console.warn("async parser handler taking longer than 5000 ms")})}_getCurrentLinkId(){return this._curAttrData.extended.urlId}parse(H,ee){let le,de=this._activeBuffer.x,De=this._activeBuffer.y,je=0,tt=this._parseStack.paused;if(tt){if(le=this._parser.parse(this._parseBuffer,this._parseStack.decodedLength,ee))return this._logSlowResolvingAsync(le),le;de=this._parseStack.cursorStartX,De=this._parseStack.cursorStartY,this._parseStack.paused=!1,H.length>oe&&(je=this._parseStack.position+oe)}if(this._logService.logLevel<=A.LogLevelEnum.DEBUG&&this._logService.debug("parsing data"+(typeof H=="string"?` "${H}"`:` "${Array.prototype.map.call(H,Y=>String.fromCharCode(Y)).join("")}"`),typeof H=="string"?H.split("").map(Y=>Y.charCodeAt(0)):H),this._parseBuffer.lengthoe)for(let Y=je;Y0&&Ze.getWidth(this._activeBuffer.x-1)===2&&Ze.setCellFromCodepoint(this._activeBuffer.x-1,0,1,Re);let ct=this._parser.precedingJoinState;for(let Tt=ee;Ttet){if(ye){let qr=Ze,Hn=this._activeBuffer.x-bo;for(this._activeBuffer.x=bo,this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData(),!0)):(this._activeBuffer.y>=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!0),Ze=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y),bo>0&&Ze instanceof x.BufferLine&&Ze.copyCellsFrom(qr,Hn,0,bo,!1);Hn=0;)Ze.setCellFromCodepoint(this._activeBuffer.x++,0,0,Re)}else if(Y&&(Ze.insertCells(this._activeBuffer.x,De-bo,this._activeBuffer.getNullCell(Re)),Ze.getWidth(et-1)===2&&Ze.setCellFromCodepoint(et-1,k.NULL_CELL_CODE,k.NULL_CELL_WIDTH,Re)),Ze.setCellFromCodepoint(this._activeBuffer.x++,de,De,Re),De>0)for(;--De;)Ze.setCellFromCodepoint(this._activeBuffer.x++,0,0,Re)}this._parser.precedingJoinState=ct,this._activeBuffer.x0&&Ze.getWidth(this._activeBuffer.x)===0&&!Ze.hasContent(this._activeBuffer.x)&&Ze.setCellFromCodepoint(this._activeBuffer.x,0,1,Re),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}registerCsiHandler(H,ee){return H.final!=="t"||H.prefix||H.intermediates?this._parser.registerCsiHandler(H,ee):this._parser.registerCsiHandler(H,le=>!W(le.params[0],this._optionsService.rawOptions.windowOptions)||ee(le))}registerDcsHandler(H,ee){return this._parser.registerDcsHandler(H,new I.DcsHandler(ee))}registerEscHandler(H,ee){return this._parser.registerEscHandler(H,ee)}registerOscHandler(H,ee){return this._parser.registerOscHandler(H,new F.OscHandler(ee))}bell(){return this._onRequestBell.fire(),!0}lineFeed(){return this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._optionsService.rawOptions.convertEol&&(this._activeBuffer.x=0),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows?this._activeBuffer.y=this._bufferService.rows-1:this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.x>=this._bufferService.cols&&this._activeBuffer.x--,this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._onLineFeed.fire(),!0}carriageReturn(){return this._activeBuffer.x=0,!0}backspace(){if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),this._activeBuffer.x>0&&this._activeBuffer.x--,!0;if(this._restrictCursor(this._bufferService.cols),this._activeBuffer.x>0)this._activeBuffer.x--;else if(this._activeBuffer.x===0&&this._activeBuffer.y>this._activeBuffer.scrollTop&&this._activeBuffer.y<=this._activeBuffer.scrollBottom&&this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y)?.isWrapped){this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.y--,this._activeBuffer.x=this._bufferService.cols-1;let H=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);H.hasWidth(this._activeBuffer.x)&&!H.hasContent(this._activeBuffer.x)&&this._activeBuffer.x--}return this._restrictCursor(),!0}tab(){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let H=this._activeBuffer.x;return this._activeBuffer.x=this._activeBuffer.nextStop(),this._optionsService.rawOptions.screenReaderMode&&this._onA11yTab.fire(this._activeBuffer.x-H),!0}shiftOut(){return this._charsetService.setgLevel(1),!0}shiftIn(){return this._charsetService.setgLevel(0),!0}_restrictCursor(H=this._bufferService.cols-1){this._activeBuffer.x=Math.min(H,Math.max(0,this._activeBuffer.x)),this._activeBuffer.y=this._coreService.decPrivateModes.origin?Math.min(this._activeBuffer.scrollBottom,Math.max(this._activeBuffer.scrollTop,this._activeBuffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._activeBuffer.y)),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_setCursor(H,ee){this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._coreService.decPrivateModes.origin?(this._activeBuffer.x=H,this._activeBuffer.y=this._activeBuffer.scrollTop+ee):(this._activeBuffer.x=H,this._activeBuffer.y=ee),this._restrictCursor(),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_moveCursor(H,ee){this._restrictCursor(),this._setCursor(this._activeBuffer.x+H,this._activeBuffer.y+ee)}cursorUp(H){let ee=this._activeBuffer.y-this._activeBuffer.scrollTop;return ee>=0?this._moveCursor(0,-Math.min(ee,H.params[0]||1)):this._moveCursor(0,-(H.params[0]||1)),!0}cursorDown(H){let ee=this._activeBuffer.scrollBottom-this._activeBuffer.y;return ee>=0?this._moveCursor(0,Math.min(ee,H.params[0]||1)):this._moveCursor(0,H.params[0]||1),!0}cursorForward(H){return this._moveCursor(H.params[0]||1,0),!0}cursorBackward(H){return this._moveCursor(-(H.params[0]||1),0),!0}cursorNextLine(H){return this.cursorDown(H),this._activeBuffer.x=0,!0}cursorPrecedingLine(H){return this.cursorUp(H),this._activeBuffer.x=0,!0}cursorCharAbsolute(H){return this._setCursor((H.params[0]||1)-1,this._activeBuffer.y),!0}cursorPosition(H){return this._setCursor(H.length>=2?(H.params[1]||1)-1:0,(H.params[0]||1)-1),!0}charPosAbsolute(H){return this._setCursor((H.params[0]||1)-1,this._activeBuffer.y),!0}hPositionRelative(H){return this._moveCursor(H.params[0]||1,0),!0}linePosAbsolute(H){return this._setCursor(this._activeBuffer.x,(H.params[0]||1)-1),!0}vPositionRelative(H){return this._moveCursor(0,H.params[0]||1),!0}hVPosition(H){return this.cursorPosition(H),!0}tabClear(H){let ee=H.params[0];return ee===0?delete this._activeBuffer.tabs[this._activeBuffer.x]:ee===3&&(this._activeBuffer.tabs={}),!0}cursorForwardTab(H){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let ee=H.params[0]||1;for(;ee--;)this._activeBuffer.x=this._activeBuffer.nextStop();return!0}cursorBackwardTab(H){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let ee=H.params[0]||1;for(;ee--;)this._activeBuffer.x=this._activeBuffer.prevStop();return!0}selectProtected(H){let ee=H.params[0];return ee===1&&(this._curAttrData.bg|=536870912),ee!==2&&ee!==0||(this._curAttrData.bg&=-536870913),!0}_eraseInBufferLine(H,ee,le,de=!1,De=!1){let je=this._activeBuffer.lines.get(this._activeBuffer.ybase+H);je.replaceCells(ee,le,this._activeBuffer.getNullCell(this._eraseAttrData()),De),de&&(je.isWrapped=!1)}_resetBufferLine(H,ee=!1){let le=this._activeBuffer.lines.get(this._activeBuffer.ybase+H);le&&(le.fill(this._activeBuffer.getNullCell(this._eraseAttrData()),ee),this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase+H),le.isWrapped=!1)}eraseInDisplay(H,ee=!1){let le;switch(this._restrictCursor(this._bufferService.cols),H.params[0]){case 0:for(le=this._activeBuffer.y,this._dirtyRowTracker.markDirty(le),this._eraseInBufferLine(le++,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,ee);le=this._bufferService.cols&&(this._activeBuffer.lines.get(le+1).isWrapped=!1);le--;)this._resetBufferLine(le,ee);this._dirtyRowTracker.markDirty(0);break;case 2:for(le=this._bufferService.rows,this._dirtyRowTracker.markDirty(le-1);le--;)this._resetBufferLine(le,ee);this._dirtyRowTracker.markDirty(0);break;case 3:let de=this._activeBuffer.lines.length-this._bufferService.rows;de>0&&(this._activeBuffer.lines.trimStart(de),this._activeBuffer.ybase=Math.max(this._activeBuffer.ybase-de,0),this._activeBuffer.ydisp=Math.max(this._activeBuffer.ydisp-de,0),this._onScroll.fire(0))}return!0}eraseInLine(H,ee=!1){switch(this._restrictCursor(this._bufferService.cols),H.params[0]){case 0:this._eraseInBufferLine(this._activeBuffer.y,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,ee);break;case 1:this._eraseInBufferLine(this._activeBuffer.y,0,this._activeBuffer.x+1,!1,ee);break;case 2:this._eraseInBufferLine(this._activeBuffer.y,0,this._bufferService.cols,!0,ee)}return this._dirtyRowTracker.markDirty(this._activeBuffer.y),!0}insertLines(H){this._restrictCursor();let ee=H.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.y65535?2:1}let ye=et;for(let Y=1;Y0||(this._is("xterm")||this._is("rxvt-unicode")||this._is("screen")?this._coreService.triggerDataEvent(u.C0.ESC+"[?1;2c"):this._is("linux")&&this._coreService.triggerDataEvent(u.C0.ESC+"[?6c")),!0}sendDeviceAttributesSecondary(H){return H.params[0]>0||(this._is("xterm")?this._coreService.triggerDataEvent(u.C0.ESC+"[>0;276;0c"):this._is("rxvt-unicode")?this._coreService.triggerDataEvent(u.C0.ESC+"[>85;95;0c"):this._is("linux")?this._coreService.triggerDataEvent(H.params[0]+"c"):this._is("screen")&&this._coreService.triggerDataEvent(u.C0.ESC+"[>83;40003;0c")),!0}_is(H){return(this._optionsService.rawOptions.termName+"").indexOf(H)===0}setMode(H){for(let ee=0;eeLo?1:2,ct=H.params[0];return Tt=ct,ri=ee?ct===2?4:ct===4?Ze(je.modes.insertMode):ct===12?3:ct===20?Ze(Re.convertEol):0:ct===1?Ze(le.applicationCursorKeys):ct===3?Re.windowOptions.setWinLines?et===80?2:et===132?1:0:0:ct===6?Ze(le.origin):ct===7?Ze(le.wraparound):ct===8?3:ct===9?Ze(de==="X10"):ct===12?Ze(Re.cursorBlink):ct===25?Ze(!je.isCursorHidden):ct===45?Ze(le.reverseWraparound):ct===66?Ze(le.applicationKeypad):ct===67?4:ct===1e3?Ze(de==="VT200"):ct===1002?Ze(de==="DRAG"):ct===1003?Ze(de==="ANY"):ct===1004?Ze(le.sendFocus):ct===1005?4:ct===1006?Ze(De==="SGR"):ct===1015?4:ct===1016?Ze(De==="SGR_PIXELS"):ct===1048?1:ct===47||ct===1047||ct===1049?Ze(ye===Y):ct===2004?Ze(le.bracketedPasteMode):0,je.triggerDataEvent(`${u.C0.ESC}[${ee?"":"?"}${Tt};${ri}$y`),!0;var Tt,ri}_updateAttrColor(H,ee,le,de,De){return ee===2?(H|=50331648,H&=-16777216,H|=T.AttributeData.fromColorRGB([le,de,De])):ee===5&&(H&=-50331904,H|=33554432|255&le),H}_extractColor(H,ee,le){let de=[0,0,-1,0,0,0],De=0,je=0;do{if(de[je+De]=H.params[ee+je],H.hasSubParams(ee+je)){let tt=H.getSubParams(ee+je),et=0;do de[1]===5&&(De=1),de[je+et+1+De]=tt[et];while(++et=2||de[1]===2&&je+De>=5)break;de[1]&&(De=1)}while(++je+ee5)&&(H=1),ee.extended.underlineStyle=H,ee.fg|=268435456,H===0&&(ee.fg&=-268435457),ee.updateExtended()}_processSGR0(H){H.fg=x.DEFAULT_ATTR_DATA.fg,H.bg=x.DEFAULT_ATTR_DATA.bg,H.extended=H.extended.clone(),H.extended.underlineStyle=0,H.extended.underlineColor&=-67108864,H.updateExtended()}charAttributes(H){if(H.length===1&&H.params[0]===0)return this._processSGR0(this._curAttrData),!0;let ee=H.length,le,de=this._curAttrData;for(let De=0;De=30&&le<=37?(de.fg&=-50331904,de.fg|=16777216|le-30):le>=40&&le<=47?(de.bg&=-50331904,de.bg|=16777216|le-40):le>=90&&le<=97?(de.fg&=-50331904,de.fg|=16777224|le-90):le>=100&&le<=107?(de.bg&=-50331904,de.bg|=16777224|le-100):le===0?this._processSGR0(de):le===1?de.fg|=134217728:le===3?de.bg|=67108864:le===4?(de.fg|=268435456,this._processUnderline(H.hasSubParams(De)?H.getSubParams(De)[0]:1,de)):le===5?de.fg|=536870912:le===7?de.fg|=67108864:le===8?de.fg|=1073741824:le===9?de.fg|=2147483648:le===2?de.bg|=134217728:le===21?this._processUnderline(2,de):le===22?(de.fg&=-134217729,de.bg&=-134217729):le===23?de.bg&=-67108865:le===24?(de.fg&=-268435457,this._processUnderline(0,de)):le===25?de.fg&=-536870913:le===27?de.fg&=-67108865:le===28?de.fg&=-1073741825:le===29?de.fg&=2147483647:le===39?(de.fg&=-67108864,de.fg|=16777215&x.DEFAULT_ATTR_DATA.fg):le===49?(de.bg&=-67108864,de.bg|=16777215&x.DEFAULT_ATTR_DATA.bg):le===38||le===48||le===58?De+=this._extractColor(H,De,de):le===53?de.bg|=1073741824:le===55?de.bg&=-1073741825:le===59?(de.extended=de.extended.clone(),de.extended.underlineColor=-1,de.updateExtended()):le===100?(de.fg&=-67108864,de.fg|=16777215&x.DEFAULT_ATTR_DATA.fg,de.bg&=-67108864,de.bg|=16777215&x.DEFAULT_ATTR_DATA.bg):this._logService.debug("Unknown SGR attribute: %d.",le);return!0}deviceStatus(H){switch(H.params[0]){case 5:this._coreService.triggerDataEvent(`${u.C0.ESC}[0n`);break;case 6:let ee=this._activeBuffer.y+1,le=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${u.C0.ESC}[${ee};${le}R`)}return!0}deviceStatusPrivate(H){if(H.params[0]===6){let ee=this._activeBuffer.y+1,le=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${u.C0.ESC}[?${ee};${le}R`)}return!0}softReset(H){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._activeBuffer.scrollTop=0,this._activeBuffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=x.DEFAULT_ATTR_DATA.clone(),this._coreService.reset(),this._charsetService.reset(),this._activeBuffer.savedX=0,this._activeBuffer.savedY=this._activeBuffer.ybase,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0}setCursorStyle(H){let ee=H.params[0]||1;switch(ee){case 1:case 2:this._optionsService.options.cursorStyle="block";break;case 3:case 4:this._optionsService.options.cursorStyle="underline";break;case 5:case 6:this._optionsService.options.cursorStyle="bar"}let le=ee%2==1;return this._optionsService.options.cursorBlink=le,!0}setScrollRegion(H){let ee=H.params[0]||1,le;return(H.length<2||(le=H.params[1])>this._bufferService.rows||le===0)&&(le=this._bufferService.rows),le>ee&&(this._activeBuffer.scrollTop=ee-1,this._activeBuffer.scrollBottom=le-1,this._setCursor(0,0)),!0}windowOptions(H){if(!W(H.params[0],this._optionsService.rawOptions.windowOptions))return!0;let ee=H.length>1?H.params[1]:0;switch(H.params[0]){case 14:ee!==2&&this._onRequestWindowsOptionsReport.fire(G.GET_WIN_SIZE_PIXELS);break;case 16:this._onRequestWindowsOptionsReport.fire(G.GET_CELL_SIZE_PIXELS);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(`${u.C0.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`);break;case 22:ee!==0&&ee!==2||(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),ee!==0&&ee!==1||(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:ee!==0&&ee!==2||this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),ee!==0&&ee!==1||this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop())}return!0}saveCursor(H){return this._activeBuffer.savedX=this._activeBuffer.x,this._activeBuffer.savedY=this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,!0}restoreCursor(H){return this._activeBuffer.x=this._activeBuffer.savedX||0,this._activeBuffer.y=Math.max(this._activeBuffer.savedY-this._activeBuffer.ybase,0),this._curAttrData.fg=this._activeBuffer.savedCurAttrData.fg,this._curAttrData.bg=this._activeBuffer.savedCurAttrData.bg,this._charsetService.charset=this._savedCharset,this._activeBuffer.savedCharset&&(this._charsetService.charset=this._activeBuffer.savedCharset),this._restrictCursor(),!0}setTitle(H){return this._windowTitle=H,this._onTitleChange.fire(H),!0}setIconName(H){return this._iconName=H,!0}setOrReportIndexedColor(H){let ee=[],le=H.split(";");for(;le.length>1;){let de=le.shift(),De=le.shift();if(/^\d+$/.exec(de)){let je=parseInt(de);if(Ie(je))if(De==="?")ee.push({type:0,index:je});else{let tt=(0,V.parseColor)(De);tt&&ee.push({type:1,index:je,color:tt})}}}return ee.length&&this._onColor.fire(ee),!0}setHyperlink(H){let ee=H.split(";");return!(ee.length<2)&&(ee[1]?this._createHyperlink(ee[0],ee[1]):!ee[0]&&this._finishHyperlink())}_createHyperlink(H,ee){this._getCurrentLinkId()&&this._finishHyperlink();let le=H.split(":"),de,De=le.findIndex(je=>je.startsWith("id="));return De!==-1&&(de=le[De].slice(3)||void 0),this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=this._oscLinkService.registerLink({id:de,uri:ee}),this._curAttrData.updateExtended(),!0}_finishHyperlink(){return this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=0,this._curAttrData.updateExtended(),!0}_setOrReportSpecialColor(H,ee){let le=H.split(";");for(let de=0;de=this._specialColors.length);++de,++ee)if(le[de]==="?")this._onColor.fire([{type:0,index:this._specialColors[ee]}]);else{let De=(0,V.parseColor)(le[de]);De&&this._onColor.fire([{type:1,index:this._specialColors[ee],color:De}])}return!0}setOrReportFgColor(H){return this._setOrReportSpecialColor(H,0)}setOrReportBgColor(H){return this._setOrReportSpecialColor(H,1)}setOrReportCursorColor(H){return this._setOrReportSpecialColor(H,2)}restoreIndexedColor(H){if(!H)return this._onColor.fire([{type:2}]),!0;let ee=[],le=H.split(";");for(let de=0;de=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._restrictCursor(),!0}tabSet(){return this._activeBuffer.tabs[this._activeBuffer.x]=!0,!0}reverseIndex(){if(this._restrictCursor(),this._activeBuffer.y===this._activeBuffer.scrollTop){let H=this._activeBuffer.scrollBottom-this._activeBuffer.scrollTop;this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase+this._activeBuffer.y,H,1),this._activeBuffer.lines.set(this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.getBlankLine(this._eraseAttrData())),this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom)}else this._activeBuffer.y--,this._restrictCursor();return!0}fullReset(){return this._parser.reset(),this._onRequestReset.fire(),!0}reset(){this._curAttrData=x.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=x.DEFAULT_ATTR_DATA.clone()}_eraseAttrData(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=67108863&this._curAttrData.bg,this._eraseAttrDataInternal}setgLevel(H){return this._charsetService.setgLevel(H),!0}screenAlignmentPattern(){let H=new S.CellData;H.content=4194373,H.fg=this._curAttrData.fg,H.bg=this._curAttrData.bg,this._setCursor(0,0);for(let ee=0;ee(this._coreService.triggerDataEvent(`${u.C0.ESC}${De}${u.C0.ESC}\\`),!0))(H==='"q'?`P1$r${this._curAttrData.isProtected()?1:0}"q`:H==='"p'?'P1$r61;1"p':H==="r"?`P1$r${le.scrollTop+1};${le.scrollBottom+1}r`:H==="m"?"P1$r0m":H===" q"?`P1$r${{block:2,underline:4,bar:6}[de.cursorStyle]-(de.cursorBlink?1:0)} q`:"P0$r")}markRangeDirty(H,ee){this._dirtyRowTracker.markRangeDirty(H,ee)}}r.InputHandler=we;let me=class{constructor(ue){this._bufferService=ue,this.clearRange()}clearRange(){this.start=this._bufferService.buffer.y,this.end=this._bufferService.buffer.y}markDirty(ue){uethis.end&&(this.end=ue)}markRangeDirty(ue,H){ue>H&&(Ce=ue,ue=H,H=Ce),uethis.end&&(this.end=H)}markAllDirty(){this.markRangeDirty(0,this._bufferService.rows-1)}};function Ie(ue){return 0<=ue&&ue<256}me=d([m(0,A.IBufferService)],me)},844:(i,r)=>{function s(d){for(let m of d)m.dispose();d.length=0}Object.defineProperty(r,"__esModule",{value:!0}),r.getDisposeArrayDisposable=r.disposeArray=r.toDisposable=r.MutableDisposable=r.Disposable=void 0,r.Disposable=class{constructor(){this._disposables=[],this._isDisposed=!1}dispose(){this._isDisposed=!0;for(let d of this._disposables)d.dispose();this._disposables.length=0}register(d){return this._disposables.push(d),d}unregister(d){let m=this._disposables.indexOf(d);m!==-1&&this._disposables.splice(m,1)}},r.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(d){this._isDisposed||d===this._value||(this._value?.dispose(),this._value=d)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}},r.toDisposable=function(d){return{dispose:d}},r.disposeArray=s,r.getDisposeArrayDisposable=function(d){return{dispose:()=>s(d)}}},1505:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.FourKeyMap=r.TwoKeyMap=void 0;class s{constructor(){this._data={}}set(m,u,h){this._data[m]||(this._data[m]={}),this._data[m][u]=h}get(m,u){return this._data[m]?this._data[m][u]:void 0}clear(){this._data={}}}r.TwoKeyMap=s,r.FourKeyMap=class{constructor(){this._data=new s}set(d,m,u,h,v){this._data.get(d,m)||this._data.set(d,m,new s),this._data.get(d,m).set(u,h,v)}get(d,m,u,h){return this._data.get(d,m)?.get(u,h)}clear(){this._data.clear()}}},6114:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.isChromeOS=r.isLinux=r.isWindows=r.isIphone=r.isIpad=r.isMac=r.getSafariVersion=r.isSafari=r.isLegacyEdge=r.isFirefox=r.isNode=void 0,r.isNode=typeof process<"u"&&"title"in process;let s=r.isNode?"node":navigator.userAgent,d=r.isNode?"node":navigator.platform;r.isFirefox=s.includes("Firefox"),r.isLegacyEdge=s.includes("Edge"),r.isSafari=/^((?!chrome|android).)*safari/i.test(s),r.getSafariVersion=function(){if(!r.isSafari)return 0;let m=s.match(/Version\/(\d+)/);return m===null||m.length<2?0:parseInt(m[1])},r.isMac=["Macintosh","MacIntel","MacPPC","Mac68K"].includes(d),r.isIpad=d==="iPad",r.isIphone=d==="iPhone",r.isWindows=["Windows","Win16","Win32","WinCE"].includes(d),r.isLinux=d.indexOf("Linux")>=0,r.isChromeOS=/\bCrOS\b/.test(s)},6106:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.SortedList=void 0;let s=0;r.SortedList=class{constructor(d){this._getKey=d,this._array=[]}clear(){this._array.length=0}insert(d){this._array.length!==0?(s=this._search(this._getKey(d)),this._array.splice(s,0,d)):this._array.push(d)}delete(d){if(this._array.length===0)return!1;let m=this._getKey(d);if(m===void 0||(s=this._search(m),s===-1)||this._getKey(this._array[s])!==m)return!1;do if(this._array[s]===d)return this._array.splice(s,1),!0;while(++s=this._array.length)&&this._getKey(this._array[s])===d))do yield this._array[s];while(++s=this._array.length)&&this._getKey(this._array[s])===d))do m(this._array[s]);while(++s=m;){let h=m+u>>1,v=this._getKey(this._array[h]);if(v>d)u=h-1;else{if(!(v0&&this._getKey(this._array[h-1])===d;)h--;return h}m=h+1}}return m}}},7226:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.DebouncedIdleTask=r.IdleTaskQueue=r.PriorityTaskQueue=void 0;let d=s(6114);class m{constructor(){this._tasks=[],this._i=0}enqueue(v){this._tasks.push(v),this._start()}flush(){for(;this._iP)return x-b<-20&&console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(x-b))}ms`),void this._start();x=P}this.clear()}}class u extends m{_requestCallback(v){return setTimeout(()=>v(this._createDeadline(16)))}_cancelCallback(v){clearTimeout(v)}_createDeadline(v){let b=Date.now()+v;return{timeRemaining:()=>Math.max(0,b-Date.now())}}}r.PriorityTaskQueue=u,r.IdleTaskQueue=!d.isNode&&"requestIdleCallback"in window?class extends m{_requestCallback(h){return requestIdleCallback(h)}_cancelCallback(h){cancelIdleCallback(h)}}:u,r.DebouncedIdleTask=class{constructor(){this._queue=new r.IdleTaskQueue}set(h){this._queue.clear(),this._queue.enqueue(h)}flush(){this._queue.flush()}}},9282:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.updateWindowsModeWrappedState=void 0;let d=s(643);r.updateWindowsModeWrappedState=function(m){let u=m.buffer.lines.get(m.buffer.ybase+m.buffer.y-1),h=u?.get(m.cols-1),v=m.buffer.lines.get(m.buffer.ybase+m.buffer.y);v&&h&&(v.isWrapped=h[d.CHAR_DATA_CODE_INDEX]!==d.NULL_CELL_CODE&&h[d.CHAR_DATA_CODE_INDEX]!==d.WHITESPACE_CELL_CODE)}},3734:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.ExtendedAttrs=r.AttributeData=void 0;class s{constructor(){this.fg=0,this.bg=0,this.extended=new d}static toColorRGB(u){return[u>>>16&255,u>>>8&255,255&u]}static fromColorRGB(u){return(255&u[0])<<16|(255&u[1])<<8|255&u[2]}clone(){let u=new s;return u.fg=this.fg,u.bg=this.bg,u.extended=this.extended.clone(),u}isInverse(){return 67108864&this.fg}isBold(){return 134217728&this.fg}isUnderline(){return this.hasExtendedAttrs()&&this.extended.underlineStyle!==0?1:268435456&this.fg}isBlink(){return 536870912&this.fg}isInvisible(){return 1073741824&this.fg}isItalic(){return 67108864&this.bg}isDim(){return 134217728&this.bg}isStrikethrough(){return 2147483648&this.fg}isProtected(){return 536870912&this.bg}isOverline(){return 1073741824&this.bg}getFgColorMode(){return 50331648&this.fg}getBgColorMode(){return 50331648&this.bg}isFgRGB(){return(50331648&this.fg)==50331648}isBgRGB(){return(50331648&this.bg)==50331648}isFgPalette(){return(50331648&this.fg)==16777216||(50331648&this.fg)==33554432}isBgPalette(){return(50331648&this.bg)==16777216||(50331648&this.bg)==33554432}isFgDefault(){return(50331648&this.fg)==0}isBgDefault(){return(50331648&this.bg)==0}isAttributeDefault(){return this.fg===0&&this.bg===0}getFgColor(){switch(50331648&this.fg){case 16777216:case 33554432:return 255&this.fg;case 50331648:return 16777215&this.fg;default:return-1}}getBgColor(){switch(50331648&this.bg){case 16777216:case 33554432:return 255&this.bg;case 50331648:return 16777215&this.bg;default:return-1}}hasExtendedAttrs(){return 268435456&this.bg}updateExtended(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456}getUnderlineColor(){if(268435456&this.bg&&~this.extended.underlineColor)switch(50331648&this.extended.underlineColor){case 16777216:case 33554432:return 255&this.extended.underlineColor;case 50331648:return 16777215&this.extended.underlineColor;default:return this.getFgColor()}return this.getFgColor()}getUnderlineColorMode(){return 268435456&this.bg&&~this.extended.underlineColor?50331648&this.extended.underlineColor:this.getFgColorMode()}isUnderlineColorRGB(){return 268435456&this.bg&&~this.extended.underlineColor?(50331648&this.extended.underlineColor)==50331648:this.isFgRGB()}isUnderlineColorPalette(){return 268435456&this.bg&&~this.extended.underlineColor?(50331648&this.extended.underlineColor)==16777216||(50331648&this.extended.underlineColor)==33554432:this.isFgPalette()}isUnderlineColorDefault(){return 268435456&this.bg&&~this.extended.underlineColor?(50331648&this.extended.underlineColor)==0:this.isFgDefault()}getUnderlineStyle(){return 268435456&this.fg?268435456&this.bg?this.extended.underlineStyle:1:0}getUnderlineVariantOffset(){return this.extended.underlineVariantOffset}}r.AttributeData=s;class d{get ext(){return this._urlId?-469762049&this._ext|this.underlineStyle<<26:this._ext}set ext(u){this._ext=u}get underlineStyle(){return this._urlId?5:(469762048&this._ext)>>26}set underlineStyle(u){this._ext&=-469762049,this._ext|=u<<26&469762048}get underlineColor(){return 67108863&this._ext}set underlineColor(u){this._ext&=-67108864,this._ext|=67108863&u}get urlId(){return this._urlId}set urlId(u){this._urlId=u}get underlineVariantOffset(){let u=(3758096384&this._ext)>>29;return u<0?4294967288^u:u}set underlineVariantOffset(u){this._ext&=536870911,this._ext|=u<<29&3758096384}constructor(u=0,h=0){this._ext=0,this._urlId=0,this._ext=u,this._urlId=h}clone(){return new d(this._ext,this._urlId)}isEmpty(){return this.underlineStyle===0&&this._urlId===0}}r.ExtendedAttrs=d},9092:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.Buffer=r.MAX_BUFFER_SIZE=void 0;let d=s(6349),m=s(7226),u=s(3734),h=s(8437),v=s(4634),b=s(511),y=s(643),x=s(4863),P=s(7116);r.MAX_BUFFER_SIZE=4294967295,r.Buffer=class{constructor(k,S,T){this._hasScrollback=k,this._optionsService=S,this._bufferService=T,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.tabs={},this.savedY=0,this.savedX=0,this.savedCurAttrData=h.DEFAULT_ATTR_DATA.clone(),this.savedCharset=P.DEFAULT_CHARSET,this.markers=[],this._nullCell=b.CellData.fromCharData([0,y.NULL_CELL_CHAR,y.NULL_CELL_WIDTH,y.NULL_CELL_CODE]),this._whitespaceCell=b.CellData.fromCharData([0,y.WHITESPACE_CELL_CHAR,y.WHITESPACE_CELL_WIDTH,y.WHITESPACE_CELL_CODE]),this._isClearing=!1,this._memoryCleanupQueue=new m.IdleTaskQueue,this._memoryCleanupPosition=0,this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new d.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}getNullCell(k){return k?(this._nullCell.fg=k.fg,this._nullCell.bg=k.bg,this._nullCell.extended=k.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new u.ExtendedAttrs),this._nullCell}getWhitespaceCell(k){return k?(this._whitespaceCell.fg=k.fg,this._whitespaceCell.bg=k.bg,this._whitespaceCell.extended=k.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new u.ExtendedAttrs),this._whitespaceCell}getBlankLine(k,S){return new h.BufferLine(this._bufferService.cols,this.getNullCell(k),S)}get hasScrollback(){return this._hasScrollback&&this.lines.maxLength>this._rows}get isCursorInViewport(){let k=this.ybase+this.y-this.ydisp;return k>=0&&kr.MAX_BUFFER_SIZE?r.MAX_BUFFER_SIZE:S}fillViewportRows(k){if(this.lines.length===0){k===void 0&&(k=h.DEFAULT_ATTR_DATA);let S=this._rows;for(;S--;)this.lines.push(this.getBlankLine(k))}}clear(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new d.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}resize(k,S){let T=this.getNullCell(h.DEFAULT_ATTR_DATA),A=0,D=this._getCorrectBufferLength(S);if(D>this.lines.maxLength&&(this.lines.maxLength=D),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+F+1?(this.ybase--,F++,this.ydisp>0&&this.ydisp--):this.lines.push(new h.BufferLine(k,T)));else for(let I=this._rows;I>S;I--)this.lines.length>S+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(D0&&(this.lines.trimStart(I),this.ybase=Math.max(this.ybase-I,0),this.ydisp=Math.max(this.ydisp-I,0),this.savedY=Math.max(this.savedY-I,0)),this.lines.maxLength=D}this.x=Math.min(this.x,k-1),this.y=Math.min(this.y,S-1),F&&(this.y+=F),this.savedX=Math.min(this.savedX,k-1),this.scrollTop=0}if(this.scrollBottom=S-1,this._isReflowEnabled&&(this._reflow(k,S),this._cols>k))for(let F=0;F.1*this.lines.length&&(this._memoryCleanupPosition=0,this._memoryCleanupQueue.enqueue(()=>this._batchedMemoryCleanup()))}_batchedMemoryCleanup(){let k=!0;this._memoryCleanupPosition>=this.lines.length&&(this._memoryCleanupPosition=0,k=!1);let S=0;for(;this._memoryCleanupPosition100)return!0;return k}get _isReflowEnabled(){let k=this._optionsService.rawOptions.windowsPty;return k&&k.buildNumber?this._hasScrollback&&k.backend==="conpty"&&k.buildNumber>=21376:this._hasScrollback&&!this._optionsService.rawOptions.windowsMode}_reflow(k,S){this._cols!==k&&(k>this._cols?this._reflowLarger(k,S):this._reflowSmaller(k,S))}_reflowLarger(k,S){let T=(0,v.reflowLargerGetLinesToRemove)(this.lines,this._cols,k,this.ybase+this.y,this.getNullCell(h.DEFAULT_ATTR_DATA));if(T.length>0){let A=(0,v.reflowLargerCreateNewLayout)(this.lines,T);(0,v.reflowLargerApplyNewLayout)(this.lines,A.layout),this._reflowLargerAdjustViewport(k,S,A.countRemoved)}}_reflowLargerAdjustViewport(k,S,T){let A=this.getNullCell(h.DEFAULT_ATTR_DATA),D=T;for(;D-- >0;)this.ybase===0?(this.y>0&&this.y--,this.lines.length=0;F--){let I=this.lines.get(F);if(!I||!I.isWrapped&&I.getTrimmedLength()<=k)continue;let V=[I];for(;I.isWrapped&&F>0;)I=this.lines.get(--F),V.unshift(I);let ae=this.ybase+this.y;if(ae>=F&&ae0&&(A.push({start:F+V.length+D,newLines:we}),D+=we.length),V.push(...we);let me=W.length-1,Ie=W[me];Ie===0&&(me--,Ie=W[me]);let ue=V.length-G-1,H=oe;for(;ue>=0;){let le=Math.min(H,Ie);if(V[me]===void 0)break;if(V[me].copyCellsFrom(V[ue],H-le,Ie-le,le,!0),Ie-=le,Ie===0&&(me--,Ie=W[me]),H-=le,H===0){ue--;let de=Math.max(ue,0);H=(0,v.getWrappedLineTrimmedLength)(V,de,this._cols)}}for(let le=0;le0;)this.ybase===0?this.y0){let F=[],I=[];for(let me=0;me=0;me--)if(W&&W.start>ae+G){for(let Ie=W.newLines.length-1;Ie>=0;Ie--)this.lines.set(me--,W.newLines[Ie]);me++,F.push({index:ae+1,amount:W.newLines.length}),G+=W.newLines.length,W=A[++oe]}else this.lines.set(me,I[ae--]);let Ce=0;for(let me=F.length-1;me>=0;me--)F[me].index+=Ce,this.lines.onInsertEmitter.fire(F[me]),Ce+=F[me].amount;let we=Math.max(0,V+D-this.lines.maxLength);we>0&&this.lines.onTrimEmitter.fire(we)}}translateBufferLineToString(k,S,T=0,A){let D=this.lines.get(k);return D?D.translateToString(S,T,A):""}getWrappedRangeForLine(k){let S=k,T=k;for(;S>0&&this.lines.get(S).isWrapped;)S--;for(;T+10;);return k>=this._cols?this._cols-1:k<0?0:k}nextStop(k){for(k==null&&(k=this.x);!this.tabs[++k]&&k=this._cols?this._cols-1:k<0?0:k}clearMarkers(k){this._isClearing=!0;for(let S=0;S{S.line-=T,S.line<0&&S.dispose()})),S.register(this.lines.onInsert(T=>{S.line>=T.index&&(S.line+=T.amount)})),S.register(this.lines.onDelete(T=>{S.line>=T.index&&S.lineT.index&&(S.line-=T.amount)})),S.register(S.onDispose(()=>this._removeMarker(S))),S}_removeMarker(k){this._isClearing||this.markers.splice(this.markers.indexOf(k),1)}}},8437:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.BufferLine=r.DEFAULT_ATTR_DATA=void 0;let d=s(3734),m=s(511),u=s(643),h=s(482);r.DEFAULT_ATTR_DATA=Object.freeze(new d.AttributeData);let v=0;class b{constructor(x,P,k=!1){this.isWrapped=k,this._combined={},this._extendedAttrs={},this._data=new Uint32Array(3*x);let S=P||m.CellData.fromCharData([0,u.NULL_CELL_CHAR,u.NULL_CELL_WIDTH,u.NULL_CELL_CODE]);for(let T=0;T>22,2097152&P?this._combined[x].charCodeAt(this._combined[x].length-1):k]}set(x,P){this._data[3*x+1]=P[u.CHAR_DATA_ATTR_INDEX],P[u.CHAR_DATA_CHAR_INDEX].length>1?(this._combined[x]=P[1],this._data[3*x+0]=2097152|x|P[u.CHAR_DATA_WIDTH_INDEX]<<22):this._data[3*x+0]=P[u.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|P[u.CHAR_DATA_WIDTH_INDEX]<<22}getWidth(x){return this._data[3*x+0]>>22}hasWidth(x){return 12582912&this._data[3*x+0]}getFg(x){return this._data[3*x+1]}getBg(x){return this._data[3*x+2]}hasContent(x){return 4194303&this._data[3*x+0]}getCodePoint(x){let P=this._data[3*x+0];return 2097152&P?this._combined[x].charCodeAt(this._combined[x].length-1):2097151&P}isCombined(x){return 2097152&this._data[3*x+0]}getString(x){let P=this._data[3*x+0];return 2097152&P?this._combined[x]:2097151&P?(0,h.stringFromCodePoint)(2097151&P):""}isProtected(x){return 536870912&this._data[3*x+2]}loadCell(x,P){return v=3*x,P.content=this._data[v+0],P.fg=this._data[v+1],P.bg=this._data[v+2],2097152&P.content&&(P.combinedData=this._combined[x]),268435456&P.bg&&(P.extended=this._extendedAttrs[x]),P}setCell(x,P){2097152&P.content&&(this._combined[x]=P.combinedData),268435456&P.bg&&(this._extendedAttrs[x]=P.extended),this._data[3*x+0]=P.content,this._data[3*x+1]=P.fg,this._data[3*x+2]=P.bg}setCellFromCodepoint(x,P,k,S){268435456&S.bg&&(this._extendedAttrs[x]=S.extended),this._data[3*x+0]=P|k<<22,this._data[3*x+1]=S.fg,this._data[3*x+2]=S.bg}addCodepointToCell(x,P,k){let S=this._data[3*x+0];2097152&S?this._combined[x]+=(0,h.stringFromCodePoint)(P):2097151&S?(this._combined[x]=(0,h.stringFromCodePoint)(2097151&S)+(0,h.stringFromCodePoint)(P),S&=-2097152,S|=2097152):S=P|4194304,k&&(S&=-12582913,S|=k<<22),this._data[3*x+0]=S}insertCells(x,P,k){if((x%=this.length)&&this.getWidth(x-1)===2&&this.setCellFromCodepoint(x-1,0,1,k),P=0;--T)this.setCell(x+P+T,this.loadCell(x+T,S));for(let T=0;Tthis.length){if(this._data.buffer.byteLength>=4*k)this._data=new Uint32Array(this._data.buffer,0,k);else{let S=new Uint32Array(k);S.set(this._data),this._data=S}for(let S=this.length;S=x&&delete this._combined[D]}let T=Object.keys(this._extendedAttrs);for(let A=0;A=x&&delete this._extendedAttrs[D]}}return this.length=x,4*k*2=0;--x)if(4194303&this._data[3*x+0])return x+(this._data[3*x+0]>>22);return 0}getNoBgTrimmedLength(){for(let x=this.length-1;x>=0;--x)if(4194303&this._data[3*x+0]||50331648&this._data[3*x+2])return x+(this._data[3*x+0]>>22);return 0}copyCellsFrom(x,P,k,S,T){let A=x._data;if(T)for(let F=S-1;F>=0;F--){for(let I=0;I<3;I++)this._data[3*(k+F)+I]=A[3*(P+F)+I];268435456&A[3*(P+F)+2]&&(this._extendedAttrs[k+F]=x._extendedAttrs[P+F])}else for(let F=0;F=P&&(this._combined[I-P+k]=x._combined[I])}}translateToString(x,P,k,S){P=P??0,k=k??this.length,x&&(k=Math.min(k,this.getTrimmedLength())),S&&(S.length=0);let T="";for(;P>22||1}return S&&S.push(P),T}}r.BufferLine=b},4841:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.getRangeLength=void 0,r.getRangeLength=function(s,d){if(s.start.y>s.end.y)throw new Error(`Buffer range end (${s.end.x}, ${s.end.y}) cannot be before start (${s.start.x}, ${s.start.y})`);return d*(s.end.y-s.start.y)+(s.end.x-s.start.x+1)}},4634:(i,r)=>{function s(d,m,u){if(m===d.length-1)return d[m].getTrimmedLength();let h=!d[m].hasContent(u-1)&&d[m].getWidth(u-1)===1,v=d[m+1].getWidth(0)===2;return h&&v?u-1:u}Object.defineProperty(r,"__esModule",{value:!0}),r.getWrappedLineTrimmedLength=r.reflowSmallerGetNewLineLengths=r.reflowLargerApplyNewLayout=r.reflowLargerCreateNewLayout=r.reflowLargerGetLinesToRemove=void 0,r.reflowLargerGetLinesToRemove=function(d,m,u,h,v){let b=[];for(let y=0;y=y&&h0&&(I>S||k[I].getTrimmedLength()===0);I--)F++;F>0&&(b.push(y+k.length-F),b.push(F)),y+=k.length-1}return b},r.reflowLargerCreateNewLayout=function(d,m){let u=[],h=0,v=m[h],b=0;for(let y=0;ys(d,k,m)).reduce((P,k)=>P+k),b=0,y=0,x=0;for(;xP&&(b-=P,y++);let k=d[y].getWidth(b-1)===2;k&&b--;let S=k?u-1:u;h.push(S),x+=S}return h},r.getWrappedLineTrimmedLength=s},5295:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.BufferSet=void 0;let d=s(8460),m=s(844),u=s(9092);class h extends m.Disposable{constructor(b,y){super(),this._optionsService=b,this._bufferService=y,this._onBufferActivate=this.register(new d.EventEmitter),this.onBufferActivate=this._onBufferActivate.event,this.reset(),this.register(this._optionsService.onSpecificOptionChange("scrollback",()=>this.resize(this._bufferService.cols,this._bufferService.rows))),this.register(this._optionsService.onSpecificOptionChange("tabStopWidth",()=>this.setupTabStops()))}reset(){this._normal=new u.Buffer(!0,this._optionsService,this._bufferService),this._normal.fillViewportRows(),this._alt=new u.Buffer(!1,this._optionsService,this._bufferService),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}),this.setupTabStops()}get alt(){return this._alt}get active(){return this._activeBuffer}get normal(){return this._normal}activateNormalBuffer(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clearAllMarkers(),this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))}activateAltBuffer(b){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(b),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))}resize(b,y){this._normal.resize(b,y),this._alt.resize(b,y),this.setupTabStops(b)}setupTabStops(b){this._normal.setupTabStops(b),this._alt.setupTabStops(b)}}r.BufferSet=h},511:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.CellData=void 0;let d=s(482),m=s(643),u=s(3734);class h extends u.AttributeData{constructor(){super(...arguments),this.content=0,this.fg=0,this.bg=0,this.extended=new u.ExtendedAttrs,this.combinedData=""}static fromCharData(b){let y=new h;return y.setFromCharData(b),y}isCombined(){return 2097152&this.content}getWidth(){return this.content>>22}getChars(){return 2097152&this.content?this.combinedData:2097151&this.content?(0,d.stringFromCodePoint)(2097151&this.content):""}getCode(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):2097151&this.content}setFromCharData(b){this.fg=b[m.CHAR_DATA_ATTR_INDEX],this.bg=0;let y=!1;if(b[m.CHAR_DATA_CHAR_INDEX].length>2)y=!0;else if(b[m.CHAR_DATA_CHAR_INDEX].length===2){let x=b[m.CHAR_DATA_CHAR_INDEX].charCodeAt(0);if(55296<=x&&x<=56319){let P=b[m.CHAR_DATA_CHAR_INDEX].charCodeAt(1);56320<=P&&P<=57343?this.content=1024*(x-55296)+P-56320+65536|b[m.CHAR_DATA_WIDTH_INDEX]<<22:y=!0}else y=!0}else this.content=b[m.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|b[m.CHAR_DATA_WIDTH_INDEX]<<22;y&&(this.combinedData=b[m.CHAR_DATA_CHAR_INDEX],this.content=2097152|b[m.CHAR_DATA_WIDTH_INDEX]<<22)}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}}r.CellData=h},643:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.WHITESPACE_CELL_CODE=r.WHITESPACE_CELL_WIDTH=r.WHITESPACE_CELL_CHAR=r.NULL_CELL_CODE=r.NULL_CELL_WIDTH=r.NULL_CELL_CHAR=r.CHAR_DATA_CODE_INDEX=r.CHAR_DATA_WIDTH_INDEX=r.CHAR_DATA_CHAR_INDEX=r.CHAR_DATA_ATTR_INDEX=r.DEFAULT_EXT=r.DEFAULT_ATTR=r.DEFAULT_COLOR=void 0,r.DEFAULT_COLOR=0,r.DEFAULT_ATTR=256|r.DEFAULT_COLOR<<9,r.DEFAULT_EXT=0,r.CHAR_DATA_ATTR_INDEX=0,r.CHAR_DATA_CHAR_INDEX=1,r.CHAR_DATA_WIDTH_INDEX=2,r.CHAR_DATA_CODE_INDEX=3,r.NULL_CELL_CHAR="",r.NULL_CELL_WIDTH=1,r.NULL_CELL_CODE=0,r.WHITESPACE_CELL_CHAR=" ",r.WHITESPACE_CELL_WIDTH=1,r.WHITESPACE_CELL_CODE=32},4863:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.Marker=void 0;let d=s(8460),m=s(844);class u{get id(){return this._id}constructor(v){this.line=v,this.isDisposed=!1,this._disposables=[],this._id=u._nextId++,this._onDispose=this.register(new d.EventEmitter),this.onDispose=this._onDispose.event}dispose(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire(),(0,m.disposeArray)(this._disposables),this._disposables.length=0)}register(v){return this._disposables.push(v),v}}r.Marker=u,u._nextId=1},7116:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.DEFAULT_CHARSET=r.CHARSETS=void 0,r.CHARSETS={},r.DEFAULT_CHARSET=r.CHARSETS.B,r.CHARSETS[0]={"`":"\u25C6",a:"\u2592",b:"\u2409",c:"\u240C",d:"\u240D",e:"\u240A",f:"\xB0",g:"\xB1",h:"\u2424",i:"\u240B",j:"\u2518",k:"\u2510",l:"\u250C",m:"\u2514",n:"\u253C",o:"\u23BA",p:"\u23BB",q:"\u2500",r:"\u23BC",s:"\u23BD",t:"\u251C",u:"\u2524",v:"\u2534",w:"\u252C",x:"\u2502",y:"\u2264",z:"\u2265","{":"\u03C0","|":"\u2260","}":"\xA3","~":"\xB7"},r.CHARSETS.A={"#":"\xA3"},r.CHARSETS.B=void 0,r.CHARSETS[4]={"#":"\xA3","@":"\xBE","[":"ij","\\":"\xBD","]":"|","{":"\xA8","|":"f","}":"\xBC","~":"\xB4"},r.CHARSETS.C=r.CHARSETS[5]={"[":"\xC4","\\":"\xD6","]":"\xC5","^":"\xDC","`":"\xE9","{":"\xE4","|":"\xF6","}":"\xE5","~":"\xFC"},r.CHARSETS.R={"#":"\xA3","@":"\xE0","[":"\xB0","\\":"\xE7","]":"\xA7","{":"\xE9","|":"\xF9","}":"\xE8","~":"\xA8"},r.CHARSETS.Q={"@":"\xE0","[":"\xE2","\\":"\xE7","]":"\xEA","^":"\xEE","`":"\xF4","{":"\xE9","|":"\xF9","}":"\xE8","~":"\xFB"},r.CHARSETS.K={"@":"\xA7","[":"\xC4","\\":"\xD6","]":"\xDC","{":"\xE4","|":"\xF6","}":"\xFC","~":"\xDF"},r.CHARSETS.Y={"#":"\xA3","@":"\xA7","[":"\xB0","\\":"\xE7","]":"\xE9","`":"\xF9","{":"\xE0","|":"\xF2","}":"\xE8","~":"\xEC"},r.CHARSETS.E=r.CHARSETS[6]={"@":"\xC4","[":"\xC6","\\":"\xD8","]":"\xC5","^":"\xDC","`":"\xE4","{":"\xE6","|":"\xF8","}":"\xE5","~":"\xFC"},r.CHARSETS.Z={"#":"\xA3","@":"\xA7","[":"\xA1","\\":"\xD1","]":"\xBF","{":"\xB0","|":"\xF1","}":"\xE7"},r.CHARSETS.H=r.CHARSETS[7]={"@":"\xC9","[":"\xC4","\\":"\xD6","]":"\xC5","^":"\xDC","`":"\xE9","{":"\xE4","|":"\xF6","}":"\xE5","~":"\xFC"},r.CHARSETS["="]={"#":"\xF9","@":"\xE0","[":"\xE9","\\":"\xE7","]":"\xEA","^":"\xEE",_:"\xE8","`":"\xF4","{":"\xE4","|":"\xF6","}":"\xFC","~":"\xFB"}},2584:(i,r)=>{var s,d,m;Object.defineProperty(r,"__esModule",{value:!0}),r.C1_ESCAPED=r.C1=r.C0=void 0,(function(u){u.NUL="\0",u.SOH="",u.STX="",u.ETX="",u.EOT="",u.ENQ="",u.ACK="",u.BEL="\x07",u.BS="\b",u.HT=" ",u.LF=` +`,u.VT="\v",u.FF="\f",u.CR="\r",u.SO="",u.SI="",u.DLE="",u.DC1="",u.DC2="",u.DC3="",u.DC4="",u.NAK="",u.SYN="",u.ETB="",u.CAN="",u.EM="",u.SUB="",u.ESC="\x1B",u.FS="",u.GS="",u.RS="",u.US="",u.SP=" ",u.DEL="\x7F"})(s||(r.C0=s={})),(function(u){u.PAD="\x80",u.HOP="\x81",u.BPH="\x82",u.NBH="\x83",u.IND="\x84",u.NEL="\x85",u.SSA="\x86",u.ESA="\x87",u.HTS="\x88",u.HTJ="\x89",u.VTS="\x8A",u.PLD="\x8B",u.PLU="\x8C",u.RI="\x8D",u.SS2="\x8E",u.SS3="\x8F",u.DCS="\x90",u.PU1="\x91",u.PU2="\x92",u.STS="\x93",u.CCH="\x94",u.MW="\x95",u.SPA="\x96",u.EPA="\x97",u.SOS="\x98",u.SGCI="\x99",u.SCI="\x9A",u.CSI="\x9B",u.ST="\x9C",u.OSC="\x9D",u.PM="\x9E",u.APC="\x9F"})(d||(r.C1=d={})),(function(u){u.ST=`${s.ESC}\\`})(m||(r.C1_ESCAPED=m={}))},7399:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.evaluateKeyboardEvent=void 0;let d=s(2584),m={48:["0",")"],49:["1","!"],50:["2","@"],51:["3","#"],52:["4","$"],53:["5","%"],54:["6","^"],55:["7","&"],56:["8","*"],57:["9","("],186:[";",":"],187:["=","+"],188:[",","<"],189:["-","_"],190:[".",">"],191:["/","?"],192:["`","~"],219:["[","{"],220:["\\","|"],221:["]","}"],222:["'",'"']};r.evaluateKeyboardEvent=function(u,h,v,b){let y={type:0,cancel:!1,key:void 0},x=(u.shiftKey?1:0)|(u.altKey?2:0)|(u.ctrlKey?4:0)|(u.metaKey?8:0);switch(u.keyCode){case 0:u.key==="UIKeyInputUpArrow"?y.key=h?d.C0.ESC+"OA":d.C0.ESC+"[A":u.key==="UIKeyInputLeftArrow"?y.key=h?d.C0.ESC+"OD":d.C0.ESC+"[D":u.key==="UIKeyInputRightArrow"?y.key=h?d.C0.ESC+"OC":d.C0.ESC+"[C":u.key==="UIKeyInputDownArrow"&&(y.key=h?d.C0.ESC+"OB":d.C0.ESC+"[B");break;case 8:y.key=u.ctrlKey?"\b":d.C0.DEL,u.altKey&&(y.key=d.C0.ESC+y.key);break;case 9:if(u.shiftKey){y.key=d.C0.ESC+"[Z";break}y.key=d.C0.HT,y.cancel=!0;break;case 13:y.key=u.altKey?d.C0.ESC+d.C0.CR:d.C0.CR,y.cancel=!0;break;case 27:y.key=d.C0.ESC,u.altKey&&(y.key=d.C0.ESC+d.C0.ESC),y.cancel=!0;break;case 37:if(u.metaKey)break;x?(y.key=d.C0.ESC+"[1;"+(x+1)+"D",y.key===d.C0.ESC+"[1;3D"&&(y.key=d.C0.ESC+(v?"b":"[1;5D"))):y.key=h?d.C0.ESC+"OD":d.C0.ESC+"[D";break;case 39:if(u.metaKey)break;x?(y.key=d.C0.ESC+"[1;"+(x+1)+"C",y.key===d.C0.ESC+"[1;3C"&&(y.key=d.C0.ESC+(v?"f":"[1;5C"))):y.key=h?d.C0.ESC+"OC":d.C0.ESC+"[C";break;case 38:if(u.metaKey)break;x?(y.key=d.C0.ESC+"[1;"+(x+1)+"A",v||y.key!==d.C0.ESC+"[1;3A"||(y.key=d.C0.ESC+"[1;5A")):y.key=h?d.C0.ESC+"OA":d.C0.ESC+"[A";break;case 40:if(u.metaKey)break;x?(y.key=d.C0.ESC+"[1;"+(x+1)+"B",v||y.key!==d.C0.ESC+"[1;3B"||(y.key=d.C0.ESC+"[1;5B")):y.key=h?d.C0.ESC+"OB":d.C0.ESC+"[B";break;case 45:u.shiftKey||u.ctrlKey||(y.key=d.C0.ESC+"[2~");break;case 46:y.key=x?d.C0.ESC+"[3;"+(x+1)+"~":d.C0.ESC+"[3~";break;case 36:y.key=x?d.C0.ESC+"[1;"+(x+1)+"H":h?d.C0.ESC+"OH":d.C0.ESC+"[H";break;case 35:y.key=x?d.C0.ESC+"[1;"+(x+1)+"F":h?d.C0.ESC+"OF":d.C0.ESC+"[F";break;case 33:u.shiftKey?y.type=2:u.ctrlKey?y.key=d.C0.ESC+"[5;"+(x+1)+"~":y.key=d.C0.ESC+"[5~";break;case 34:u.shiftKey?y.type=3:u.ctrlKey?y.key=d.C0.ESC+"[6;"+(x+1)+"~":y.key=d.C0.ESC+"[6~";break;case 112:y.key=x?d.C0.ESC+"[1;"+(x+1)+"P":d.C0.ESC+"OP";break;case 113:y.key=x?d.C0.ESC+"[1;"+(x+1)+"Q":d.C0.ESC+"OQ";break;case 114:y.key=x?d.C0.ESC+"[1;"+(x+1)+"R":d.C0.ESC+"OR";break;case 115:y.key=x?d.C0.ESC+"[1;"+(x+1)+"S":d.C0.ESC+"OS";break;case 116:y.key=x?d.C0.ESC+"[15;"+(x+1)+"~":d.C0.ESC+"[15~";break;case 117:y.key=x?d.C0.ESC+"[17;"+(x+1)+"~":d.C0.ESC+"[17~";break;case 118:y.key=x?d.C0.ESC+"[18;"+(x+1)+"~":d.C0.ESC+"[18~";break;case 119:y.key=x?d.C0.ESC+"[19;"+(x+1)+"~":d.C0.ESC+"[19~";break;case 120:y.key=x?d.C0.ESC+"[20;"+(x+1)+"~":d.C0.ESC+"[20~";break;case 121:y.key=x?d.C0.ESC+"[21;"+(x+1)+"~":d.C0.ESC+"[21~";break;case 122:y.key=x?d.C0.ESC+"[23;"+(x+1)+"~":d.C0.ESC+"[23~";break;case 123:y.key=x?d.C0.ESC+"[24;"+(x+1)+"~":d.C0.ESC+"[24~";break;default:if(!u.ctrlKey||u.shiftKey||u.altKey||u.metaKey)if(v&&!b||!u.altKey||u.metaKey)!v||u.altKey||u.ctrlKey||u.shiftKey||!u.metaKey?u.key&&!u.ctrlKey&&!u.altKey&&!u.metaKey&&u.keyCode>=48&&u.key.length===1?y.key=u.key:u.key&&u.ctrlKey&&(u.key==="_"&&(y.key=d.C0.US),u.key==="@"&&(y.key=d.C0.NUL)):u.keyCode===65&&(y.type=1);else{let P=m[u.keyCode],k=P?.[u.shiftKey?1:0];if(k)y.key=d.C0.ESC+k;else if(u.keyCode>=65&&u.keyCode<=90){let S=u.ctrlKey?u.keyCode-64:u.keyCode+32,T=String.fromCharCode(S);u.shiftKey&&(T=T.toUpperCase()),y.key=d.C0.ESC+T}else if(u.keyCode===32)y.key=d.C0.ESC+(u.ctrlKey?d.C0.NUL:" ");else if(u.key==="Dead"&&u.code.startsWith("Key")){let S=u.code.slice(3,4);u.shiftKey||(S=S.toLowerCase()),y.key=d.C0.ESC+S,y.cancel=!0}}else u.keyCode>=65&&u.keyCode<=90?y.key=String.fromCharCode(u.keyCode-64):u.keyCode===32?y.key=d.C0.NUL:u.keyCode>=51&&u.keyCode<=55?y.key=String.fromCharCode(u.keyCode-51+27):u.keyCode===56?y.key=d.C0.DEL:u.keyCode===219?y.key=d.C0.ESC:u.keyCode===220?y.key=d.C0.FS:u.keyCode===221&&(y.key=d.C0.GS)}return y}},482:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.Utf8ToUtf32=r.StringToUtf32=r.utf32ToString=r.stringFromCodePoint=void 0,r.stringFromCodePoint=function(s){return s>65535?(s-=65536,String.fromCharCode(55296+(s>>10))+String.fromCharCode(s%1024+56320)):String.fromCharCode(s)},r.utf32ToString=function(s,d=0,m=s.length){let u="";for(let h=d;h65535?(v-=65536,u+=String.fromCharCode(55296+(v>>10))+String.fromCharCode(v%1024+56320)):u+=String.fromCharCode(v)}return u},r.StringToUtf32=class{constructor(){this._interim=0}clear(){this._interim=0}decode(s,d){let m=s.length;if(!m)return 0;let u=0,h=0;if(this._interim){let v=s.charCodeAt(h++);56320<=v&&v<=57343?d[u++]=1024*(this._interim-55296)+v-56320+65536:(d[u++]=this._interim,d[u++]=v),this._interim=0}for(let v=h;v=m)return this._interim=b,u;let y=s.charCodeAt(v);56320<=y&&y<=57343?d[u++]=1024*(b-55296)+y-56320+65536:(d[u++]=b,d[u++]=y)}else b!==65279&&(d[u++]=b)}return u}},r.Utf8ToUtf32=class{constructor(){this.interim=new Uint8Array(3)}clear(){this.interim.fill(0)}decode(s,d){let m=s.length;if(!m)return 0;let u,h,v,b,y=0,x=0,P=0;if(this.interim[0]){let T=!1,A=this.interim[0];A&=(224&A)==192?31:(240&A)==224?15:7;let D,F=0;for(;(D=63&this.interim[++F])&&F<4;)A<<=6,A|=D;let I=(224&this.interim[0])==192?2:(240&this.interim[0])==224?3:4,V=I-F;for(;P=m)return 0;if(D=s[P++],(192&D)!=128){P--,T=!0;break}this.interim[F++]=D,A<<=6,A|=63&D}T||(I===2?A<128?P--:d[y++]=A:I===3?A<2048||A>=55296&&A<=57343||A===65279||(d[y++]=A):A<65536||A>1114111||(d[y++]=A)),this.interim.fill(0)}let k=m-4,S=P;for(;S=m)return this.interim[0]=u,y;if(h=s[S++],(192&h)!=128){S--;continue}if(x=(31&u)<<6|63&h,x<128){S--;continue}d[y++]=x}else if((240&u)==224){if(S>=m)return this.interim[0]=u,y;if(h=s[S++],(192&h)!=128){S--;continue}if(S>=m)return this.interim[0]=u,this.interim[1]=h,y;if(v=s[S++],(192&v)!=128){S--;continue}if(x=(15&u)<<12|(63&h)<<6|63&v,x<2048||x>=55296&&x<=57343||x===65279)continue;d[y++]=x}else if((248&u)==240){if(S>=m)return this.interim[0]=u,y;if(h=s[S++],(192&h)!=128){S--;continue}if(S>=m)return this.interim[0]=u,this.interim[1]=h,y;if(v=s[S++],(192&v)!=128){S--;continue}if(S>=m)return this.interim[0]=u,this.interim[1]=h,this.interim[2]=v,y;if(b=s[S++],(192&b)!=128){S--;continue}if(x=(7&u)<<18|(63&h)<<12|(63&v)<<6|63&b,x<65536||x>1114111)continue;d[y++]=x}}return y}}},225:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.UnicodeV6=void 0;let d=s(1480),m=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],u=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]],h;r.UnicodeV6=class{constructor(){if(this.version="6",!h){h=new Uint8Array(65536),h.fill(1),h[0]=0,h.fill(0,1,32),h.fill(0,127,160),h.fill(2,4352,4448),h[9001]=2,h[9002]=2,h.fill(2,11904,42192),h[12351]=1,h.fill(2,44032,55204),h.fill(2,63744,64256),h.fill(2,65040,65050),h.fill(2,65072,65136),h.fill(2,65280,65377),h.fill(2,65504,65511);for(let v=0;vy[k][1])return!1;for(;k>=P;)if(x=P+k>>1,b>y[x][1])P=x+1;else{if(!(b=131072&&v<=196605||v>=196608&&v<=262141?2:1}charProperties(v,b){let y=this.wcwidth(v),x=y===0&&b!==0;if(x){let P=d.UnicodeService.extractWidth(b);P===0?x=!1:P>y&&(y=P)}return d.UnicodeService.createPropertyValue(0,y,x)}}},5981:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.WriteBuffer=void 0;let d=s(8460),m=s(844);class u extends m.Disposable{constructor(v){super(),this._action=v,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0,this._isSyncWriting=!1,this._syncCalls=0,this._didUserInput=!1,this._onWriteParsed=this.register(new d.EventEmitter),this.onWriteParsed=this._onWriteParsed.event}handleUserInput(){this._didUserInput=!0}writeSync(v,b){if(b!==void 0&&this._syncCalls>b)return void(this._syncCalls=0);if(this._pendingData+=v.length,this._writeBuffer.push(v),this._callbacks.push(void 0),this._syncCalls++,this._isSyncWriting)return;let y;for(this._isSyncWriting=!0;y=this._writeBuffer.shift();){this._action(y);let x=this._callbacks.shift();x&&x()}this._pendingData=0,this._bufferOffset=2147483647,this._isSyncWriting=!1,this._syncCalls=0}write(v,b){if(this._pendingData>5e7)throw new Error("write data discarded, use flow control to avoid losing data");if(!this._writeBuffer.length){if(this._bufferOffset=0,this._didUserInput)return this._didUserInput=!1,this._pendingData+=v.length,this._writeBuffer.push(v),this._callbacks.push(b),void this._innerWrite();setTimeout(()=>this._innerWrite())}this._pendingData+=v.length,this._writeBuffer.push(v),this._callbacks.push(b)}_innerWrite(v=0,b=!0){let y=v||Date.now();for(;this._writeBuffer.length>this._bufferOffset;){let x=this._writeBuffer[this._bufferOffset],P=this._action(x,b);if(P){let S=T=>Date.now()-y>=12?setTimeout(()=>this._innerWrite(0,T)):this._innerWrite(y,T);return void P.catch(T=>(queueMicrotask(()=>{throw T}),Promise.resolve(!1))).then(S)}let k=this._callbacks[this._bufferOffset];if(k&&k(),this._bufferOffset++,this._pendingData-=x.length,Date.now()-y>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),setTimeout(()=>this._innerWrite())):(this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0),this._onWriteParsed.fire()}}r.WriteBuffer=u},5941:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.toRgbString=r.parseColor=void 0;let s=/^([\da-f])\/([\da-f])\/([\da-f])$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/,d=/^[\da-f]+$/;function m(u,h){let v=u.toString(16),b=v.length<2?"0"+v:v;switch(h){case 4:return v[0];case 8:return b;case 12:return(b+b).slice(0,3);default:return b+b}}r.parseColor=function(u){if(!u)return;let h=u.toLowerCase();if(h.indexOf("rgb:")===0){h=h.slice(4);let v=s.exec(h);if(v){let b=v[1]?15:v[4]?255:v[7]?4095:65535;return[Math.round(parseInt(v[1]||v[4]||v[7]||v[10],16)/b*255),Math.round(parseInt(v[2]||v[5]||v[8]||v[11],16)/b*255),Math.round(parseInt(v[3]||v[6]||v[9]||v[12],16)/b*255)]}}else if(h.indexOf("#")===0&&(h=h.slice(1),d.exec(h)&&[3,6,9,12].includes(h.length))){let v=h.length/3,b=[0,0,0];for(let y=0;y<3;++y){let x=parseInt(h.slice(v*y,v*y+v),16);b[y]=v===1?x<<4:v===2?x:v===3?x>>4:x>>8}return b}},r.toRgbString=function(u,h=16){let[v,b,y]=u;return`rgb:${m(v,h)}/${m(b,h)}/${m(y,h)}`}},5770:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.PAYLOAD_LIMIT=void 0,r.PAYLOAD_LIMIT=1e7},6351:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.DcsHandler=r.DcsParser=void 0;let d=s(482),m=s(8742),u=s(5770),h=[];r.DcsParser=class{constructor(){this._handlers=Object.create(null),this._active=h,this._ident=0,this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=h}registerHandler(b,y){this._handlers[b]===void 0&&(this._handlers[b]=[]);let x=this._handlers[b];return x.push(y),{dispose:()=>{let P=x.indexOf(y);P!==-1&&x.splice(P,1)}}}clearHandler(b){this._handlers[b]&&delete this._handlers[b]}setHandlerFallback(b){this._handlerFb=b}reset(){if(this._active.length)for(let b=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;b>=0;--b)this._active[b].unhook(!1);this._stack.paused=!1,this._active=h,this._ident=0}hook(b,y){if(this.reset(),this._ident=b,this._active=this._handlers[b]||h,this._active.length)for(let x=this._active.length-1;x>=0;x--)this._active[x].hook(y);else this._handlerFb(this._ident,"HOOK",y)}put(b,y,x){if(this._active.length)for(let P=this._active.length-1;P>=0;P--)this._active[P].put(b,y,x);else this._handlerFb(this._ident,"PUT",(0,d.utf32ToString)(b,y,x))}unhook(b,y=!0){if(this._active.length){let x=!1,P=this._active.length-1,k=!1;if(this._stack.paused&&(P=this._stack.loopPosition-1,x=y,k=this._stack.fallThrough,this._stack.paused=!1),!k&&x===!1){for(;P>=0&&(x=this._active[P].unhook(b),x!==!0);P--)if(x instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=P,this._stack.fallThrough=!1,x;P--}for(;P>=0;P--)if(x=this._active[P].unhook(!1),x instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=P,this._stack.fallThrough=!0,x}else this._handlerFb(this._ident,"UNHOOK",b);this._active=h,this._ident=0}};let v=new m.Params;v.addParam(0),r.DcsHandler=class{constructor(b){this._handler=b,this._data="",this._params=v,this._hitLimit=!1}hook(b){this._params=b.length>1||b.params[0]?b.clone():v,this._data="",this._hitLimit=!1}put(b,y,x){this._hitLimit||(this._data+=(0,d.utf32ToString)(b,y,x),this._data.length>u.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))}unhook(b){let y=!1;if(this._hitLimit)y=!1;else if(b&&(y=this._handler(this._data,this._params),y instanceof Promise))return y.then(x=>(this._params=v,this._data="",this._hitLimit=!1,x));return this._params=v,this._data="",this._hitLimit=!1,y}}},2015:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.EscapeSequenceParser=r.VT500_TRANSITION_TABLE=r.TransitionTable=void 0;let d=s(844),m=s(8742),u=s(6242),h=s(6351);class v{constructor(P){this.table=new Uint8Array(P)}setDefault(P,k){this.table.fill(P<<4|k)}add(P,k,S,T){this.table[k<<8|P]=S<<4|T}addMany(P,k,S,T){for(let A=0;AI),k=(F,I)=>P.slice(F,I),S=k(32,127),T=k(0,24);T.push(25),T.push.apply(T,k(28,32));let A=k(0,14),D;for(D in x.setDefault(1,0),x.addMany(S,0,2,0),A)x.addMany([24,26,153,154],D,3,0),x.addMany(k(128,144),D,3,0),x.addMany(k(144,152),D,3,0),x.add(156,D,0,0),x.add(27,D,11,1),x.add(157,D,4,8),x.addMany([152,158,159],D,0,7),x.add(155,D,11,3),x.add(144,D,11,9);return x.addMany(T,0,3,0),x.addMany(T,1,3,1),x.add(127,1,0,1),x.addMany(T,8,0,8),x.addMany(T,3,3,3),x.add(127,3,0,3),x.addMany(T,4,3,4),x.add(127,4,0,4),x.addMany(T,6,3,6),x.addMany(T,5,3,5),x.add(127,5,0,5),x.addMany(T,2,3,2),x.add(127,2,0,2),x.add(93,1,4,8),x.addMany(S,8,5,8),x.add(127,8,5,8),x.addMany([156,27,24,26,7],8,6,0),x.addMany(k(28,32),8,0,8),x.addMany([88,94,95],1,0,7),x.addMany(S,7,0,7),x.addMany(T,7,0,7),x.add(156,7,0,0),x.add(127,7,0,7),x.add(91,1,11,3),x.addMany(k(64,127),3,7,0),x.addMany(k(48,60),3,8,4),x.addMany([60,61,62,63],3,9,4),x.addMany(k(48,60),4,8,4),x.addMany(k(64,127),4,7,0),x.addMany([60,61,62,63],4,0,6),x.addMany(k(32,64),6,0,6),x.add(127,6,0,6),x.addMany(k(64,127),6,0,0),x.addMany(k(32,48),3,9,5),x.addMany(k(32,48),5,9,5),x.addMany(k(48,64),5,0,6),x.addMany(k(64,127),5,7,0),x.addMany(k(32,48),4,9,5),x.addMany(k(32,48),1,9,2),x.addMany(k(32,48),2,9,2),x.addMany(k(48,127),2,10,0),x.addMany(k(48,80),1,10,0),x.addMany(k(81,88),1,10,0),x.addMany([89,90,92],1,10,0),x.addMany(k(96,127),1,10,0),x.add(80,1,11,9),x.addMany(T,9,0,9),x.add(127,9,0,9),x.addMany(k(28,32),9,0,9),x.addMany(k(32,48),9,9,12),x.addMany(k(48,60),9,8,10),x.addMany([60,61,62,63],9,9,10),x.addMany(T,11,0,11),x.addMany(k(32,128),11,0,11),x.addMany(k(28,32),11,0,11),x.addMany(T,10,0,10),x.add(127,10,0,10),x.addMany(k(28,32),10,0,10),x.addMany(k(48,60),10,8,10),x.addMany([60,61,62,63],10,0,11),x.addMany(k(32,48),10,9,12),x.addMany(T,12,0,12),x.add(127,12,0,12),x.addMany(k(28,32),12,0,12),x.addMany(k(32,48),12,9,12),x.addMany(k(48,64),12,0,11),x.addMany(k(64,127),12,12,13),x.addMany(k(64,127),10,12,13),x.addMany(k(64,127),9,12,13),x.addMany(T,13,13,13),x.addMany(S,13,13,13),x.add(127,13,0,13),x.addMany([27,156,24,26],13,14,0),x.add(b,0,2,0),x.add(b,8,5,8),x.add(b,6,0,6),x.add(b,11,0,11),x.add(b,13,13,13),x})();class y extends d.Disposable{constructor(P=r.VT500_TRANSITION_TABLE){super(),this._transitions=P,this._parseStack={state:0,handlers:[],handlerPos:0,transition:0,chunkPos:0},this.initialState=0,this.currentState=this.initialState,this._params=new m.Params,this._params.addParam(0),this._collect=0,this.precedingJoinState=0,this._printHandlerFb=(k,S,T)=>{},this._executeHandlerFb=k=>{},this._csiHandlerFb=(k,S)=>{},this._escHandlerFb=k=>{},this._errorHandlerFb=k=>k,this._printHandler=this._printHandlerFb,this._executeHandlers=Object.create(null),this._csiHandlers=Object.create(null),this._escHandlers=Object.create(null),this.register((0,d.toDisposable)(()=>{this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._escHandlers=Object.create(null)})),this._oscParser=this.register(new u.OscParser),this._dcsParser=this.register(new h.DcsParser),this._errorHandler=this._errorHandlerFb,this.registerEscHandler({final:"\\"},()=>!0)}_identifier(P,k=[64,126]){let S=0;if(P.prefix){if(P.prefix.length>1)throw new Error("only one byte as prefix supported");if(S=P.prefix.charCodeAt(0),S&&60>S||S>63)throw new Error("prefix must be in range 0x3c .. 0x3f")}if(P.intermediates){if(P.intermediates.length>2)throw new Error("only two bytes as intermediates are supported");for(let A=0;AD||D>47)throw new Error("intermediate must be in range 0x20 .. 0x2f");S<<=8,S|=D}}if(P.final.length!==1)throw new Error("final must be a single byte");let T=P.final.charCodeAt(0);if(k[0]>T||T>k[1])throw new Error(`final must be in range ${k[0]} .. ${k[1]}`);return S<<=8,S|=T,S}identToString(P){let k=[];for(;P;)k.push(String.fromCharCode(255&P)),P>>=8;return k.reverse().join("")}setPrintHandler(P){this._printHandler=P}clearPrintHandler(){this._printHandler=this._printHandlerFb}registerEscHandler(P,k){let S=this._identifier(P,[48,126]);this._escHandlers[S]===void 0&&(this._escHandlers[S]=[]);let T=this._escHandlers[S];return T.push(k),{dispose:()=>{let A=T.indexOf(k);A!==-1&&T.splice(A,1)}}}clearEscHandler(P){this._escHandlers[this._identifier(P,[48,126])]&&delete this._escHandlers[this._identifier(P,[48,126])]}setEscHandlerFallback(P){this._escHandlerFb=P}setExecuteHandler(P,k){this._executeHandlers[P.charCodeAt(0)]=k}clearExecuteHandler(P){this._executeHandlers[P.charCodeAt(0)]&&delete this._executeHandlers[P.charCodeAt(0)]}setExecuteHandlerFallback(P){this._executeHandlerFb=P}registerCsiHandler(P,k){let S=this._identifier(P);this._csiHandlers[S]===void 0&&(this._csiHandlers[S]=[]);let T=this._csiHandlers[S];return T.push(k),{dispose:()=>{let A=T.indexOf(k);A!==-1&&T.splice(A,1)}}}clearCsiHandler(P){this._csiHandlers[this._identifier(P)]&&delete this._csiHandlers[this._identifier(P)]}setCsiHandlerFallback(P){this._csiHandlerFb=P}registerDcsHandler(P,k){return this._dcsParser.registerHandler(this._identifier(P),k)}clearDcsHandler(P){this._dcsParser.clearHandler(this._identifier(P))}setDcsHandlerFallback(P){this._dcsParser.setHandlerFallback(P)}registerOscHandler(P,k){return this._oscParser.registerHandler(P,k)}clearOscHandler(P){this._oscParser.clearHandler(P)}setOscHandlerFallback(P){this._oscParser.setHandlerFallback(P)}setErrorHandler(P){this._errorHandler=P}clearErrorHandler(){this._errorHandler=this._errorHandlerFb}reset(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingJoinState=0,this._parseStack.state!==0&&(this._parseStack.state=2,this._parseStack.handlers=[])}_preserveStack(P,k,S,T,A){this._parseStack.state=P,this._parseStack.handlers=k,this._parseStack.handlerPos=S,this._parseStack.transition=T,this._parseStack.chunkPos=A}parse(P,k,S){let T,A=0,D=0,F=0;if(this._parseStack.state)if(this._parseStack.state===2)this._parseStack.state=0,F=this._parseStack.chunkPos+1;else{if(S===void 0||this._parseStack.state===1)throw this._parseStack.state=1,new Error("improper continuation due to previous async handler, giving up parsing");let I=this._parseStack.handlers,V=this._parseStack.handlerPos-1;switch(this._parseStack.state){case 3:if(S===!1&&V>-1){for(;V>=0&&(T=I[V](this._params),T!==!0);V--)if(T instanceof Promise)return this._parseStack.handlerPos=V,T}this._parseStack.handlers=[];break;case 4:if(S===!1&&V>-1){for(;V>=0&&(T=I[V](),T!==!0);V--)if(T instanceof Promise)return this._parseStack.handlerPos=V,T}this._parseStack.handlers=[];break;case 6:if(A=P[this._parseStack.chunkPos],T=this._dcsParser.unhook(A!==24&&A!==26,S),T)return T;A===27&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0;break;case 5:if(A=P[this._parseStack.chunkPos],T=this._oscParser.end(A!==24&&A!==26,S),T)return T;A===27&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0}this._parseStack.state=0,F=this._parseStack.chunkPos+1,this.precedingJoinState=0,this.currentState=15&this._parseStack.transition}for(let I=F;I>4){case 2:for(let G=I+1;;++G){if(G>=k||(A=P[G])<32||A>126&&A=k||(A=P[G])<32||A>126&&A=k||(A=P[G])<32||A>126&&A=k||(A=P[G])<32||A>126&&A=0&&(T=V[ae](this._params),T!==!0);ae--)if(T instanceof Promise)return this._preserveStack(3,V,ae,D,I),T;ae<0&&this._csiHandlerFb(this._collect<<8|A,this._params),this.precedingJoinState=0;break;case 8:do switch(A){case 59:this._params.addParam(0);break;case 58:this._params.addSubParam(-1);break;default:this._params.addDigit(A-48)}while(++I47&&A<60);I--;break;case 9:this._collect<<=8,this._collect|=A;break;case 10:let oe=this._escHandlers[this._collect<<8|A],W=oe?oe.length-1:-1;for(;W>=0&&(T=oe[W](),T!==!0);W--)if(T instanceof Promise)return this._preserveStack(4,oe,W,D,I),T;W<0&&this._escHandlerFb(this._collect<<8|A),this.precedingJoinState=0;break;case 11:this._params.reset(),this._params.addParam(0),this._collect=0;break;case 12:this._dcsParser.hook(this._collect<<8|A,this._params);break;case 13:for(let G=I+1;;++G)if(G>=k||(A=P[G])===24||A===26||A===27||A>127&&A=k||(A=P[G])<32||A>127&&A{Object.defineProperty(r,"__esModule",{value:!0}),r.OscHandler=r.OscParser=void 0;let d=s(5770),m=s(482),u=[];r.OscParser=class{constructor(){this._state=0,this._active=u,this._id=-1,this._handlers=Object.create(null),this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(h,v){this._handlers[h]===void 0&&(this._handlers[h]=[]);let b=this._handlers[h];return b.push(v),{dispose:()=>{let y=b.indexOf(v);y!==-1&&b.splice(y,1)}}}clearHandler(h){this._handlers[h]&&delete this._handlers[h]}setHandlerFallback(h){this._handlerFb=h}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=u}reset(){if(this._state===2)for(let h=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;h>=0;--h)this._active[h].end(!1);this._stack.paused=!1,this._active=u,this._id=-1,this._state=0}_start(){if(this._active=this._handlers[this._id]||u,this._active.length)for(let h=this._active.length-1;h>=0;h--)this._active[h].start();else this._handlerFb(this._id,"START")}_put(h,v,b){if(this._active.length)for(let y=this._active.length-1;y>=0;y--)this._active[y].put(h,v,b);else this._handlerFb(this._id,"PUT",(0,m.utf32ToString)(h,v,b))}start(){this.reset(),this._state=1}put(h,v,b){if(this._state!==3){if(this._state===1)for(;v0&&this._put(h,v,b)}}end(h,v=!0){if(this._state!==0){if(this._state!==3)if(this._state===1&&this._start(),this._active.length){let b=!1,y=this._active.length-1,x=!1;if(this._stack.paused&&(y=this._stack.loopPosition-1,b=v,x=this._stack.fallThrough,this._stack.paused=!1),!x&&b===!1){for(;y>=0&&(b=this._active[y].end(h),b!==!0);y--)if(b instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=y,this._stack.fallThrough=!1,b;y--}for(;y>=0;y--)if(b=this._active[y].end(!1),b instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=y,this._stack.fallThrough=!0,b}else this._handlerFb(this._id,"END",h);this._active=u,this._id=-1,this._state=0}}},r.OscHandler=class{constructor(h){this._handler=h,this._data="",this._hitLimit=!1}start(){this._data="",this._hitLimit=!1}put(h,v,b){this._hitLimit||(this._data+=(0,m.utf32ToString)(h,v,b),this._data.length>d.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))}end(h){let v=!1;if(this._hitLimit)v=!1;else if(h&&(v=this._handler(this._data),v instanceof Promise))return v.then(b=>(this._data="",this._hitLimit=!1,b));return this._data="",this._hitLimit=!1,v}}},8742:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.Params=void 0;let s=2147483647;class d{static fromArray(u){let h=new d;if(!u.length)return h;for(let v=Array.isArray(u[0])?1:0;v256)throw new Error("maxSubParamsLength must not be greater than 256");this.params=new Int32Array(u),this.length=0,this._subParams=new Int32Array(h),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(u),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}clone(){let u=new d(this.maxLength,this.maxSubParamsLength);return u.params.set(this.params),u.length=this.length,u._subParams.set(this._subParams),u._subParamsLength=this._subParamsLength,u._subParamsIdx.set(this._subParamsIdx),u._rejectDigits=this._rejectDigits,u._rejectSubDigits=this._rejectSubDigits,u._digitIsSub=this._digitIsSub,u}toArray(){let u=[];for(let h=0;h>8,b=255&this._subParamsIdx[h];b-v>0&&u.push(Array.prototype.slice.call(this._subParams,v,b))}return u}reset(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}addParam(u){if(this._digitIsSub=!1,this.length>=this.maxLength)this._rejectDigits=!0;else{if(u<-1)throw new Error("values lesser than -1 are not allowed");this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=u>s?s:u}}addSubParam(u){if(this._digitIsSub=!0,this.length)if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength)this._rejectSubDigits=!0;else{if(u<-1)throw new Error("values lesser than -1 are not allowed");this._subParams[this._subParamsLength++]=u>s?s:u,this._subParamsIdx[this.length-1]++}}hasSubParams(u){return(255&this._subParamsIdx[u])-(this._subParamsIdx[u]>>8)>0}getSubParams(u){let h=this._subParamsIdx[u]>>8,v=255&this._subParamsIdx[u];return v-h>0?this._subParams.subarray(h,v):null}getSubParamsAll(){let u={};for(let h=0;h>8,b=255&this._subParamsIdx[h];b-v>0&&(u[h]=this._subParams.slice(v,b))}return u}addDigit(u){let h;if(this._rejectDigits||!(h=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)return;let v=this._digitIsSub?this._subParams:this.params,b=v[h-1];v[h-1]=~b?Math.min(10*b+u,s):u}}r.Params=d},5741:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.AddonManager=void 0,r.AddonManager=class{constructor(){this._addons=[]}dispose(){for(let s=this._addons.length-1;s>=0;s--)this._addons[s].instance.dispose()}loadAddon(s,d){let m={instance:d,dispose:d.dispose,isDisposed:!1};this._addons.push(m),d.dispose=()=>this._wrappedAddonDispose(m),d.activate(s)}_wrappedAddonDispose(s){if(s.isDisposed)return;let d=-1;for(let m=0;m{Object.defineProperty(r,"__esModule",{value:!0}),r.BufferApiView=void 0;let d=s(3785),m=s(511);r.BufferApiView=class{constructor(u,h){this._buffer=u,this.type=h}init(u){return this._buffer=u,this}get cursorY(){return this._buffer.y}get cursorX(){return this._buffer.x}get viewportY(){return this._buffer.ydisp}get baseY(){return this._buffer.ybase}get length(){return this._buffer.lines.length}getLine(u){let h=this._buffer.lines.get(u);if(h)return new d.BufferLineApiView(h)}getNullCell(){return new m.CellData}}},3785:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.BufferLineApiView=void 0;let d=s(511);r.BufferLineApiView=class{constructor(m){this._line=m}get isWrapped(){return this._line.isWrapped}get length(){return this._line.length}getCell(m,u){if(!(m<0||m>=this._line.length))return u?(this._line.loadCell(m,u),u):this._line.loadCell(m,new d.CellData)}translateToString(m,u,h){return this._line.translateToString(m,u,h)}}},8285:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.BufferNamespaceApi=void 0;let d=s(8771),m=s(8460),u=s(844);class h extends u.Disposable{constructor(b){super(),this._core=b,this._onBufferChange=this.register(new m.EventEmitter),this.onBufferChange=this._onBufferChange.event,this._normal=new d.BufferApiView(this._core.buffers.normal,"normal"),this._alternate=new d.BufferApiView(this._core.buffers.alt,"alternate"),this._core.buffers.onBufferActivate(()=>this._onBufferChange.fire(this.active))}get active(){if(this._core.buffers.active===this._core.buffers.normal)return this.normal;if(this._core.buffers.active===this._core.buffers.alt)return this.alternate;throw new Error("Active buffer is neither normal nor alternate")}get normal(){return this._normal.init(this._core.buffers.normal)}get alternate(){return this._alternate.init(this._core.buffers.alt)}}r.BufferNamespaceApi=h},7975:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.ParserApi=void 0,r.ParserApi=class{constructor(s){this._core=s}registerCsiHandler(s,d){return this._core.registerCsiHandler(s,m=>d(m.toArray()))}addCsiHandler(s,d){return this.registerCsiHandler(s,d)}registerDcsHandler(s,d){return this._core.registerDcsHandler(s,(m,u)=>d(m,u.toArray()))}addDcsHandler(s,d){return this.registerDcsHandler(s,d)}registerEscHandler(s,d){return this._core.registerEscHandler(s,d)}addEscHandler(s,d){return this.registerEscHandler(s,d)}registerOscHandler(s,d){return this._core.registerOscHandler(s,d)}addOscHandler(s,d){return this.registerOscHandler(s,d)}}},7090:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.UnicodeApi=void 0,r.UnicodeApi=class{constructor(s){this._core=s}register(s){this._core.unicodeService.register(s)}get versions(){return this._core.unicodeService.versions}get activeVersion(){return this._core.unicodeService.activeVersion}set activeVersion(s){this._core.unicodeService.activeVersion=s}}},744:function(i,r,s){var d=this&&this.__decorate||function(x,P,k,S){var T,A=arguments.length,D=A<3?P:S===null?S=Object.getOwnPropertyDescriptor(P,k):S;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")D=Reflect.decorate(x,P,k,S);else for(var F=x.length-1;F>=0;F--)(T=x[F])&&(D=(A<3?T(D):A>3?T(P,k,D):T(P,k))||D);return A>3&&D&&Object.defineProperty(P,k,D),D},m=this&&this.__param||function(x,P){return function(k,S){P(k,S,x)}};Object.defineProperty(r,"__esModule",{value:!0}),r.BufferService=r.MINIMUM_ROWS=r.MINIMUM_COLS=void 0;let u=s(8460),h=s(844),v=s(5295),b=s(2585);r.MINIMUM_COLS=2,r.MINIMUM_ROWS=1;let y=r.BufferService=class extends h.Disposable{get buffer(){return this.buffers.active}constructor(x){super(),this.isUserScrolling=!1,this._onResize=this.register(new u.EventEmitter),this.onResize=this._onResize.event,this._onScroll=this.register(new u.EventEmitter),this.onScroll=this._onScroll.event,this.cols=Math.max(x.rawOptions.cols||0,r.MINIMUM_COLS),this.rows=Math.max(x.rawOptions.rows||0,r.MINIMUM_ROWS),this.buffers=this.register(new v.BufferSet(x,this))}resize(x,P){this.cols=x,this.rows=P,this.buffers.resize(x,P),this._onResize.fire({cols:x,rows:P})}reset(){this.buffers.reset(),this.isUserScrolling=!1}scroll(x,P=!1){let k=this.buffer,S;S=this._cachedBlankLine,S&&S.length===this.cols&&S.getFg(0)===x.fg&&S.getBg(0)===x.bg||(S=k.getBlankLine(x,P),this._cachedBlankLine=S),S.isWrapped=P;let T=k.ybase+k.scrollTop,A=k.ybase+k.scrollBottom;if(k.scrollTop===0){let D=k.lines.isFull;A===k.lines.length-1?D?k.lines.recycle().copyFrom(S):k.lines.push(S.clone()):k.lines.splice(A+1,0,S.clone()),D?this.isUserScrolling&&(k.ydisp=Math.max(k.ydisp-1,0)):(k.ybase++,this.isUserScrolling||k.ydisp++)}else{let D=A-T+1;k.lines.shiftElements(T+1,D-1,-1),k.lines.set(A,S.clone())}this.isUserScrolling||(k.ydisp=k.ybase),this._onScroll.fire(k.ydisp)}scrollLines(x,P,k){let S=this.buffer;if(x<0){if(S.ydisp===0)return;this.isUserScrolling=!0}else x+S.ydisp>=S.ybase&&(this.isUserScrolling=!1);let T=S.ydisp;S.ydisp=Math.max(Math.min(S.ydisp+x,S.ybase),0),T!==S.ydisp&&(P||this._onScroll.fire(S.ydisp))}};r.BufferService=y=d([m(0,b.IOptionsService)],y)},7994:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.CharsetService=void 0,r.CharsetService=class{constructor(){this.glevel=0,this._charsets=[]}reset(){this.charset=void 0,this._charsets=[],this.glevel=0}setgLevel(s){this.glevel=s,this.charset=this._charsets[s]}setgCharset(s,d){this._charsets[s]=d,this.glevel===s&&(this.charset=d)}}},1753:function(i,r,s){var d=this&&this.__decorate||function(S,T,A,D){var F,I=arguments.length,V=I<3?T:D===null?D=Object.getOwnPropertyDescriptor(T,A):D;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")V=Reflect.decorate(S,T,A,D);else for(var ae=S.length-1;ae>=0;ae--)(F=S[ae])&&(V=(I<3?F(V):I>3?F(T,A,V):F(T,A))||V);return I>3&&V&&Object.defineProperty(T,A,V),V},m=this&&this.__param||function(S,T){return function(A,D){T(A,D,S)}};Object.defineProperty(r,"__esModule",{value:!0}),r.CoreMouseService=void 0;let u=s(2585),h=s(8460),v=s(844),b={NONE:{events:0,restrict:()=>!1},X10:{events:1,restrict:S=>S.button!==4&&S.action===1&&(S.ctrl=!1,S.alt=!1,S.shift=!1,!0)},VT200:{events:19,restrict:S=>S.action!==32},DRAG:{events:23,restrict:S=>S.action!==32||S.button!==3},ANY:{events:31,restrict:S=>!0}};function y(S,T){let A=(S.ctrl?16:0)|(S.shift?4:0)|(S.alt?8:0);return S.button===4?(A|=64,A|=S.action):(A|=3&S.button,4&S.button&&(A|=64),8&S.button&&(A|=128),S.action===32?A|=32:S.action!==0||T||(A|=3)),A}let x=String.fromCharCode,P={DEFAULT:S=>{let T=[y(S,!1)+32,S.col+32,S.row+32];return T[0]>255||T[1]>255||T[2]>255?"":`\x1B[M${x(T[0])}${x(T[1])}${x(T[2])}`},SGR:S=>{let T=S.action===0&&S.button!==4?"m":"M";return`\x1B[<${y(S,!0)};${S.col};${S.row}${T}`},SGR_PIXELS:S=>{let T=S.action===0&&S.button!==4?"m":"M";return`\x1B[<${y(S,!0)};${S.x};${S.y}${T}`}},k=r.CoreMouseService=class extends v.Disposable{constructor(S,T){super(),this._bufferService=S,this._coreService=T,this._protocols={},this._encodings={},this._activeProtocol="",this._activeEncoding="",this._lastEvent=null,this._onProtocolChange=this.register(new h.EventEmitter),this.onProtocolChange=this._onProtocolChange.event;for(let A of Object.keys(b))this.addProtocol(A,b[A]);for(let A of Object.keys(P))this.addEncoding(A,P[A]);this.reset()}addProtocol(S,T){this._protocols[S]=T}addEncoding(S,T){this._encodings[S]=T}get activeProtocol(){return this._activeProtocol}get areMouseEventsActive(){return this._protocols[this._activeProtocol].events!==0}set activeProtocol(S){if(!this._protocols[S])throw new Error(`unknown protocol "${S}"`);this._activeProtocol=S,this._onProtocolChange.fire(this._protocols[S].events)}get activeEncoding(){return this._activeEncoding}set activeEncoding(S){if(!this._encodings[S])throw new Error(`unknown encoding "${S}"`);this._activeEncoding=S}reset(){this.activeProtocol="NONE",this.activeEncoding="DEFAULT",this._lastEvent=null}triggerMouseEvent(S){if(S.col<0||S.col>=this._bufferService.cols||S.row<0||S.row>=this._bufferService.rows||S.button===4&&S.action===32||S.button===3&&S.action!==32||S.button!==4&&(S.action===2||S.action===3)||(S.col++,S.row++,S.action===32&&this._lastEvent&&this._equalEvents(this._lastEvent,S,this._activeEncoding==="SGR_PIXELS"))||!this._protocols[this._activeProtocol].restrict(S))return!1;let T=this._encodings[this._activeEncoding](S);return T&&(this._activeEncoding==="DEFAULT"?this._coreService.triggerBinaryEvent(T):this._coreService.triggerDataEvent(T,!0)),this._lastEvent=S,!0}explainEvents(S){return{down:!!(1&S),up:!!(2&S),drag:!!(4&S),move:!!(8&S),wheel:!!(16&S)}}_equalEvents(S,T,A){if(A){if(S.x!==T.x||S.y!==T.y)return!1}else if(S.col!==T.col||S.row!==T.row)return!1;return S.button===T.button&&S.action===T.action&&S.ctrl===T.ctrl&&S.alt===T.alt&&S.shift===T.shift}};r.CoreMouseService=k=d([m(0,u.IBufferService),m(1,u.ICoreService)],k)},6975:function(i,r,s){var d=this&&this.__decorate||function(k,S,T,A){var D,F=arguments.length,I=F<3?S:A===null?A=Object.getOwnPropertyDescriptor(S,T):A;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")I=Reflect.decorate(k,S,T,A);else for(var V=k.length-1;V>=0;V--)(D=k[V])&&(I=(F<3?D(I):F>3?D(S,T,I):D(S,T))||I);return F>3&&I&&Object.defineProperty(S,T,I),I},m=this&&this.__param||function(k,S){return function(T,A){S(T,A,k)}};Object.defineProperty(r,"__esModule",{value:!0}),r.CoreService=void 0;let u=s(1439),h=s(8460),v=s(844),b=s(2585),y=Object.freeze({insertMode:!1}),x=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,origin:!1,reverseWraparound:!1,sendFocus:!1,wraparound:!0}),P=r.CoreService=class extends v.Disposable{constructor(k,S,T){super(),this._bufferService=k,this._logService=S,this._optionsService=T,this.isCursorInitialized=!1,this.isCursorHidden=!1,this._onData=this.register(new h.EventEmitter),this.onData=this._onData.event,this._onUserInput=this.register(new h.EventEmitter),this.onUserInput=this._onUserInput.event,this._onBinary=this.register(new h.EventEmitter),this.onBinary=this._onBinary.event,this._onRequestScrollToBottom=this.register(new h.EventEmitter),this.onRequestScrollToBottom=this._onRequestScrollToBottom.event,this.modes=(0,u.clone)(y),this.decPrivateModes=(0,u.clone)(x)}reset(){this.modes=(0,u.clone)(y),this.decPrivateModes=(0,u.clone)(x)}triggerDataEvent(k,S=!1){if(this._optionsService.rawOptions.disableStdin)return;let T=this._bufferService.buffer;S&&this._optionsService.rawOptions.scrollOnUserInput&&T.ybase!==T.ydisp&&this._onRequestScrollToBottom.fire(),S&&this._onUserInput.fire(),this._logService.debug(`sending data "${k}"`,()=>k.split("").map(A=>A.charCodeAt(0))),this._onData.fire(k)}triggerBinaryEvent(k){this._optionsService.rawOptions.disableStdin||(this._logService.debug(`sending binary "${k}"`,()=>k.split("").map(S=>S.charCodeAt(0))),this._onBinary.fire(k))}};r.CoreService=P=d([m(0,b.IBufferService),m(1,b.ILogService),m(2,b.IOptionsService)],P)},9074:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.DecorationService=void 0;let d=s(8055),m=s(8460),u=s(844),h=s(6106),v=0,b=0;class y extends u.Disposable{get decorations(){return this._decorations.values()}constructor(){super(),this._decorations=new h.SortedList(k=>k?.marker.line),this._onDecorationRegistered=this.register(new m.EventEmitter),this.onDecorationRegistered=this._onDecorationRegistered.event,this._onDecorationRemoved=this.register(new m.EventEmitter),this.onDecorationRemoved=this._onDecorationRemoved.event,this.register((0,u.toDisposable)(()=>this.reset()))}registerDecoration(k){if(k.marker.isDisposed)return;let S=new x(k);if(S){let T=S.marker.onDispose(()=>S.dispose());S.onDispose(()=>{S&&(this._decorations.delete(S)&&this._onDecorationRemoved.fire(S),T.dispose())}),this._decorations.insert(S),this._onDecorationRegistered.fire(S)}return S}reset(){for(let k of this._decorations.values())k.dispose();this._decorations.clear()}*getDecorationsAtCell(k,S,T){let A=0,D=0;for(let F of this._decorations.getKeyIterator(S))A=F.options.x??0,D=A+(F.options.width??1),k>=A&&k{v=D.options.x??0,b=v+(D.options.width??1),k>=v&&k{Object.defineProperty(r,"__esModule",{value:!0}),r.InstantiationService=r.ServiceCollection=void 0;let d=s(2585),m=s(8343);class u{constructor(...v){this._entries=new Map;for(let[b,y]of v)this.set(b,y)}set(v,b){let y=this._entries.get(v);return this._entries.set(v,b),y}forEach(v){for(let[b,y]of this._entries.entries())v(b,y)}has(v){return this._entries.has(v)}get(v){return this._entries.get(v)}}r.ServiceCollection=u,r.InstantiationService=class{constructor(){this._services=new u,this._services.set(d.IInstantiationService,this)}setService(h,v){this._services.set(h,v)}getService(h){return this._services.get(h)}createInstance(h,...v){let b=(0,m.getServiceDependencies)(h).sort((P,k)=>P.index-k.index),y=[];for(let P of b){let k=this._services.get(P.id);if(!k)throw new Error(`[createInstance] ${h.name} depends on UNKNOWN service ${P.id}.`);y.push(k)}let x=b.length>0?b[0].index:v.length;if(v.length!==x)throw new Error(`[createInstance] First service dependency of ${h.name} at position ${x+1} conflicts with ${v.length} static arguments`);return new h(...v,...y)}}},7866:function(i,r,s){var d=this&&this.__decorate||function(x,P,k,S){var T,A=arguments.length,D=A<3?P:S===null?S=Object.getOwnPropertyDescriptor(P,k):S;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")D=Reflect.decorate(x,P,k,S);else for(var F=x.length-1;F>=0;F--)(T=x[F])&&(D=(A<3?T(D):A>3?T(P,k,D):T(P,k))||D);return A>3&&D&&Object.defineProperty(P,k,D),D},m=this&&this.__param||function(x,P){return function(k,S){P(k,S,x)}};Object.defineProperty(r,"__esModule",{value:!0}),r.traceCall=r.setTraceLogger=r.LogService=void 0;let u=s(844),h=s(2585),v={trace:h.LogLevelEnum.TRACE,debug:h.LogLevelEnum.DEBUG,info:h.LogLevelEnum.INFO,warn:h.LogLevelEnum.WARN,error:h.LogLevelEnum.ERROR,off:h.LogLevelEnum.OFF},b,y=r.LogService=class extends u.Disposable{get logLevel(){return this._logLevel}constructor(x){super(),this._optionsService=x,this._logLevel=h.LogLevelEnum.OFF,this._updateLogLevel(),this.register(this._optionsService.onSpecificOptionChange("logLevel",()=>this._updateLogLevel())),b=this}_updateLogLevel(){this._logLevel=v[this._optionsService.rawOptions.logLevel]}_evalLazyOptionalParams(x){for(let P=0;PJSON.stringify(D)).join(", ")})`);let A=S.apply(this,T);return b.trace(`GlyphRenderer#${S.name} return`,A),A}}},7302:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.OptionsService=r.DEFAULT_OPTIONS=void 0;let d=s(8460),m=s(844),u=s(6114);r.DEFAULT_OPTIONS={cols:80,rows:24,cursorBlink:!1,cursorStyle:"block",cursorWidth:1,cursorInactiveStyle:"outline",customGlyphs:!0,drawBoldTextInBrightColors:!0,documentOverride:null,fastScrollModifier:"alt",fastScrollSensitivity:5,fontFamily:"courier-new, courier, monospace",fontSize:15,fontWeight:"normal",fontWeightBold:"bold",ignoreBracketedPasteMode:!1,lineHeight:1,letterSpacing:0,linkHandler:null,logLevel:"info",logger:null,scrollback:1e3,scrollOnUserInput:!0,scrollSensitivity:1,screenReaderMode:!1,smoothScrollDuration:0,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,disableStdin:!1,allowProposedApi:!1,allowTransparency:!1,tabStopWidth:8,theme:{},rescaleOverlappingGlyphs:!1,rightClickSelectsWord:u.isMac,windowOptions:{},windowsMode:!1,windowsPty:{},wordSeparator:" ()[]{}',\"`",altClickMovesCursor:!0,convertEol:!1,termName:"xterm",cancelEvents:!1,overviewRulerWidth:0};let h=["normal","bold","100","200","300","400","500","600","700","800","900"];class v extends m.Disposable{constructor(y){super(),this._onOptionChange=this.register(new d.EventEmitter),this.onOptionChange=this._onOptionChange.event;let x=q({},r.DEFAULT_OPTIONS);for(let P in y)if(P in x)try{let k=y[P];x[P]=this._sanitizeAndValidateOption(P,k)}catch(k){console.error(k)}this.rawOptions=x,this.options=q({},x),this._setupOptions(),this.register((0,m.toDisposable)(()=>{this.rawOptions.linkHandler=null,this.rawOptions.documentOverride=null}))}onSpecificOptionChange(y,x){return this.onOptionChange(P=>{P===y&&x(this.rawOptions[y])})}onMultipleOptionChange(y,x){return this.onOptionChange(P=>{y.indexOf(P)!==-1&&x()})}_setupOptions(){let y=P=>{if(!(P in r.DEFAULT_OPTIONS))throw new Error(`No option with key "${P}"`);return this.rawOptions[P]},x=(P,k)=>{if(!(P in r.DEFAULT_OPTIONS))throw new Error(`No option with key "${P}"`);k=this._sanitizeAndValidateOption(P,k),this.rawOptions[P]!==k&&(this.rawOptions[P]=k,this._onOptionChange.fire(P))};for(let P in this.rawOptions){let k={get:y.bind(this,P),set:x.bind(this,P)};Object.defineProperty(this.options,P,k)}}_sanitizeAndValidateOption(y,x){switch(y){case"cursorStyle":if(x||(x=r.DEFAULT_OPTIONS[y]),!(function(P){return P==="block"||P==="underline"||P==="bar"})(x))throw new Error(`"${x}" is not a valid value for ${y}`);break;case"wordSeparator":x||(x=r.DEFAULT_OPTIONS[y]);break;case"fontWeight":case"fontWeightBold":if(typeof x=="number"&&1<=x&&x<=1e3)break;x=h.includes(x)?x:r.DEFAULT_OPTIONS[y];break;case"cursorWidth":x=Math.floor(x);case"lineHeight":case"tabStopWidth":if(x<1)throw new Error(`${y} cannot be less than 1, value: ${x}`);break;case"minimumContrastRatio":x=Math.max(1,Math.min(21,Math.round(10*x)/10));break;case"scrollback":if((x=Math.min(x,4294967295))<0)throw new Error(`${y} cannot be less than 0, value: ${x}`);break;case"fastScrollSensitivity":case"scrollSensitivity":if(x<=0)throw new Error(`${y} cannot be less than or equal to 0, value: ${x}`);break;case"rows":case"cols":if(!x&&x!==0)throw new Error(`${y} must be numeric, value: ${x}`);break;case"windowsPty":x=x??{}}return x}}r.OptionsService=v},2660:function(i,r,s){var d=this&&this.__decorate||function(v,b,y,x){var P,k=arguments.length,S=k<3?b:x===null?x=Object.getOwnPropertyDescriptor(b,y):x;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")S=Reflect.decorate(v,b,y,x);else for(var T=v.length-1;T>=0;T--)(P=v[T])&&(S=(k<3?P(S):k>3?P(b,y,S):P(b,y))||S);return k>3&&S&&Object.defineProperty(b,y,S),S},m=this&&this.__param||function(v,b){return function(y,x){b(y,x,v)}};Object.defineProperty(r,"__esModule",{value:!0}),r.OscLinkService=void 0;let u=s(2585),h=r.OscLinkService=class{constructor(v){this._bufferService=v,this._nextId=1,this._entriesWithId=new Map,this._dataByLinkId=new Map}registerLink(v){let b=this._bufferService.buffer;if(v.id===void 0){let T=b.addMarker(b.ybase+b.y),A={data:v,id:this._nextId++,lines:[T]};return T.onDispose(()=>this._removeMarkerFromLink(A,T)),this._dataByLinkId.set(A.id,A),A.id}let y=v,x=this._getEntryIdKey(y),P=this._entriesWithId.get(x);if(P)return this.addLineToLink(P.id,b.ybase+b.y),P.id;let k=b.addMarker(b.ybase+b.y),S={id:this._nextId++,key:this._getEntryIdKey(y),data:y,lines:[k]};return k.onDispose(()=>this._removeMarkerFromLink(S,k)),this._entriesWithId.set(S.key,S),this._dataByLinkId.set(S.id,S),S.id}addLineToLink(v,b){let y=this._dataByLinkId.get(v);if(y&&y.lines.every(x=>x.line!==b)){let x=this._bufferService.buffer.addMarker(b);y.lines.push(x),x.onDispose(()=>this._removeMarkerFromLink(y,x))}}getLinkData(v){return this._dataByLinkId.get(v)?.data}_getEntryIdKey(v){return`${v.id};;${v.uri}`}_removeMarkerFromLink(v,b){let y=v.lines.indexOf(b);y!==-1&&(v.lines.splice(y,1),v.lines.length===0&&(v.data.id!==void 0&&this._entriesWithId.delete(v.key),this._dataByLinkId.delete(v.id)))}};r.OscLinkService=h=d([m(0,u.IBufferService)],h)},8343:(i,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createDecorator=r.getServiceDependencies=r.serviceRegistry=void 0;let s="di$target",d="di$dependencies";r.serviceRegistry=new Map,r.getServiceDependencies=function(m){return m[d]||[]},r.createDecorator=function(m){if(r.serviceRegistry.has(m))return r.serviceRegistry.get(m);let u=function(h,v,b){if(arguments.length!==3)throw new Error("@IServiceName-decorator can only be used to decorate a parameter");(function(y,x,P){x[s]===x?x[d].push({id:y,index:P}):(x[d]=[{id:y,index:P}],x[s]=x)})(u,h,b)};return u.toString=()=>m,r.serviceRegistry.set(m,u),u}},2585:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.IDecorationService=r.IUnicodeService=r.IOscLinkService=r.IOptionsService=r.ILogService=r.LogLevelEnum=r.IInstantiationService=r.ICharsetService=r.ICoreService=r.ICoreMouseService=r.IBufferService=void 0;let d=s(8343);var m;r.IBufferService=(0,d.createDecorator)("BufferService"),r.ICoreMouseService=(0,d.createDecorator)("CoreMouseService"),r.ICoreService=(0,d.createDecorator)("CoreService"),r.ICharsetService=(0,d.createDecorator)("CharsetService"),r.IInstantiationService=(0,d.createDecorator)("InstantiationService"),(function(u){u[u.TRACE=0]="TRACE",u[u.DEBUG=1]="DEBUG",u[u.INFO=2]="INFO",u[u.WARN=3]="WARN",u[u.ERROR=4]="ERROR",u[u.OFF=5]="OFF"})(m||(r.LogLevelEnum=m={})),r.ILogService=(0,d.createDecorator)("LogService"),r.IOptionsService=(0,d.createDecorator)("OptionsService"),r.IOscLinkService=(0,d.createDecorator)("OscLinkService"),r.IUnicodeService=(0,d.createDecorator)("UnicodeService"),r.IDecorationService=(0,d.createDecorator)("DecorationService")},1480:(i,r,s)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.UnicodeService=void 0;let d=s(8460),m=s(225);class u{static extractShouldJoin(v){return(1&v)!=0}static extractWidth(v){return v>>1&3}static extractCharKind(v){return v>>3}static createPropertyValue(v,b,y=!1){return(16777215&v)<<3|(3&b)<<1|(y?1:0)}constructor(){this._providers=Object.create(null),this._active="",this._onChange=new d.EventEmitter,this.onChange=this._onChange.event;let v=new m.UnicodeV6;this.register(v),this._active=v.version,this._activeProvider=v}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(v){if(!this._providers[v])throw new Error(`unknown Unicode version "${v}"`);this._active=v,this._activeProvider=this._providers[v],this._onChange.fire(v)}register(v){this._providers[v.version]=v}wcwidth(v){return this._activeProvider.wcwidth(v)}getStringCellWidth(v){let b=0,y=0,x=v.length;for(let P=0;P=x)return b+this.wcwidth(k);let A=v.charCodeAt(P);56320<=A&&A<=57343?k=1024*(k-55296)+A-56320+65536:b+=this.wcwidth(A)}let S=this.charProperties(k,y),T=u.extractWidth(S);u.extractShouldJoin(S)&&(T-=u.extractWidth(y)),b+=T,y=S}return b}charProperties(v,b){return this._activeProvider.charProperties(v,b)}}r.UnicodeService=u}},o={};function e(i){var r=o[i];if(r!==void 0)return r.exports;var s=o[i]={exports:{}};return n[i].call(s.exports,s,s.exports,e),s.exports}var t={};return(()=>{var i=t;Object.defineProperty(i,"__esModule",{value:!0}),i.Terminal=void 0;let r=e(9042),s=e(3236),d=e(844),m=e(5741),u=e(8285),h=e(7975),v=e(7090),b=["cols","rows"];class y extends d.Disposable{constructor(P){super(),this._core=this.register(new s.Terminal(P)),this._addonManager=this.register(new m.AddonManager),this._publicOptions=q({},this._core.options);let k=T=>this._core.options[T],S=(T,A)=>{this._checkReadonlyOptions(T),this._core.options[T]=A};for(let T in this._core.options){let A={get:k.bind(this,T),set:S.bind(this,T)};Object.defineProperty(this._publicOptions,T,A)}}_checkReadonlyOptions(P){if(b.includes(P))throw new Error(`Option "${P}" can only be set in the constructor`)}_checkProposedApi(){if(!this._core.optionsService.rawOptions.allowProposedApi)throw new Error("You must set the allowProposedApi option to true to use proposed API")}get onBell(){return this._core.onBell}get onBinary(){return this._core.onBinary}get onCursorMove(){return this._core.onCursorMove}get onData(){return this._core.onData}get onKey(){return this._core.onKey}get onLineFeed(){return this._core.onLineFeed}get onRender(){return this._core.onRender}get onResize(){return this._core.onResize}get onScroll(){return this._core.onScroll}get onSelectionChange(){return this._core.onSelectionChange}get onTitleChange(){return this._core.onTitleChange}get onWriteParsed(){return this._core.onWriteParsed}get element(){return this._core.element}get parser(){return this._parser||(this._parser=new h.ParserApi(this._core)),this._parser}get unicode(){return this._checkProposedApi(),new v.UnicodeApi(this._core)}get textarea(){return this._core.textarea}get rows(){return this._core.rows}get cols(){return this._core.cols}get buffer(){return this._buffer||(this._buffer=this.register(new u.BufferNamespaceApi(this._core))),this._buffer}get markers(){return this._checkProposedApi(),this._core.markers}get modes(){let P=this._core.coreService.decPrivateModes,k="none";switch(this._core.coreMouseService.activeProtocol){case"X10":k="x10";break;case"VT200":k="vt200";break;case"DRAG":k="drag";break;case"ANY":k="any"}return{applicationCursorKeysMode:P.applicationCursorKeys,applicationKeypadMode:P.applicationKeypad,bracketedPasteMode:P.bracketedPasteMode,insertMode:this._core.coreService.modes.insertMode,mouseTrackingMode:k,originMode:P.origin,reverseWraparoundMode:P.reverseWraparound,sendFocusMode:P.sendFocus,wraparoundMode:P.wraparound}}get options(){return this._publicOptions}set options(P){for(let k in P)this._publicOptions[k]=P[k]}blur(){this._core.blur()}focus(){this._core.focus()}input(P,k=!0){this._core.input(P,k)}resize(P,k){this._verifyIntegers(P,k),this._core.resize(P,k)}open(P){this._core.open(P)}attachCustomKeyEventHandler(P){this._core.attachCustomKeyEventHandler(P)}attachCustomWheelEventHandler(P){this._core.attachCustomWheelEventHandler(P)}registerLinkProvider(P){return this._core.registerLinkProvider(P)}registerCharacterJoiner(P){return this._checkProposedApi(),this._core.registerCharacterJoiner(P)}deregisterCharacterJoiner(P){this._checkProposedApi(),this._core.deregisterCharacterJoiner(P)}registerMarker(P=0){return this._verifyIntegers(P),this._core.registerMarker(P)}registerDecoration(P){return this._checkProposedApi(),this._verifyPositiveIntegers(P.x??0,P.width??0,P.height??0),this._core.registerDecoration(P)}hasSelection(){return this._core.hasSelection()}select(P,k,S){this._verifyIntegers(P,k,S),this._core.select(P,k,S)}getSelection(){return this._core.getSelection()}getSelectionPosition(){return this._core.getSelectionPosition()}clearSelection(){this._core.clearSelection()}selectAll(){this._core.selectAll()}selectLines(P,k){this._verifyIntegers(P,k),this._core.selectLines(P,k)}dispose(){super.dispose()}scrollLines(P){this._verifyIntegers(P),this._core.scrollLines(P)}scrollPages(P){this._verifyIntegers(P),this._core.scrollPages(P)}scrollToTop(){this._core.scrollToTop()}scrollToBottom(){this._core.scrollToBottom()}scrollToLine(P){this._verifyIntegers(P),this._core.scrollToLine(P)}clear(){this._core.clear()}write(P,k){this._core.write(P,k)}writeln(P,k){this._core.write(P),this._core.write(`\r +`,k)}paste(P){this._core.paste(P)}refresh(P,k){this._verifyIntegers(P,k),this._core.refresh(P,k)}reset(){this._core.reset()}clearTextureAtlas(){this._core.clearTextureAtlas()}loadAddon(P){this._addonManager.loadAddon(this,P)}static get strings(){return r}_verifyIntegers(...P){for(let k of P)if(k===1/0||isNaN(k)||k%1!=0)throw new Error("This API only accepts integers")}_verifyPositiveIntegers(...P){for(let k of P)if(k&&(k===1/0||isNaN(k)||k%1!=0||k<0))throw new Error("This API only accepts positive integers")}}i.Terminal=y})(),t})())});var C4=Vs((t2,f4)=>{(function(n,o){typeof t2=="object"&&typeof f4=="object"?f4.exports=o():typeof define=="function"&&define.amd?define([],o):typeof t2=="object"?t2.AttachAddon=o():n.AttachAddon=o()})(self,()=>(()=>{"use strict";var n={};return(()=>{var o=n;function e(t,i,r){return t.addEventListener(i,r),{dispose:()=>{r&&t.removeEventListener(i,r)}}}Object.defineProperty(o,"__esModule",{value:!0}),o.AttachAddon=void 0,o.AttachAddon=class{constructor(t,i){this._disposables=[],this._socket=t,this._socket.binaryType="arraybuffer",this._bidirectional=!(i&&i.bidirectional===!1)}activate(t){this._disposables.push(e(this._socket,"message",i=>{let r=i.data;t.write(typeof r=="string"?r:new Uint8Array(r))})),this._bidirectional&&(this._disposables.push(t.onData(i=>this._sendData(i))),this._disposables.push(t.onBinary(i=>this._sendBinary(i)))),this._disposables.push(e(this._socket,"close",()=>this.dispose())),this._disposables.push(e(this._socket,"error",()=>this.dispose()))}dispose(){for(let t of this._disposables)t.dispose()}_sendData(t){this._checkOpenSocket()&&this._socket.send(t)}_sendBinary(t){if(!this._checkOpenSocket())return;let i=new Uint8Array(t.length);for(let r=0;r{(function(n,o){typeof n2=="object"&&typeof v4=="object"?v4.exports=o():typeof define=="function"&&define.amd?define([],o):typeof n2=="object"?n2.FitAddon=o():n.FitAddon=o()})(self,()=>(()=>{"use strict";var n={};return(()=>{var o=n;Object.defineProperty(o,"__esModule",{value:!0}),o.FitAddon=void 0,o.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){let e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;let t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal||!this._terminal.element||!this._terminal.element.parentElement)return;let e=this._terminal._core,t=e._renderService.dimensions;if(t.css.cell.width===0||t.css.cell.height===0)return;let i=this._terminal.options.scrollback===0?0:e.viewport.scrollBarWidth,r=window.getComputedStyle(this._terminal.element.parentElement),s=parseInt(r.getPropertyValue("height")),d=Math.max(0,parseInt(r.getPropertyValue("width"))),m=window.getComputedStyle(this._terminal.element),u=s-(parseInt(m.getPropertyValue("padding-top"))+parseInt(m.getPropertyValue("padding-bottom"))),h=d-(parseInt(m.getPropertyValue("padding-right"))+parseInt(m.getPropertyValue("padding-left")))-i;return{cols:Math.max(2,Math.floor(h/t.css.cell.width)),rows:Math.max(1,Math.floor(u/t.css.cell.height))}}}})(),n})())});var _5=new RegExp(`(\\:not\\()|(([\\.\\#]?)[-\\w]+)|(?:\\[([-.\\w*\\\\$]+)(?:=(["']?)([^\\]"']*)\\5)?\\])|(\\))|(\\s*,\\s*)`,"g"),Ag=class n{element=null;classNames=[];attrs=[];notSelectors=[];static parse(o){let e=[],t=(m,u)=>{u.notSelectors.length>0&&!u.element&&u.classNames.length==0&&u.attrs.length==0&&(u.element="*"),m.push(u)},i=new n,r,s=i,d=!1;for(_5.lastIndex=0;r=_5.exec(o);){if(r[1]){if(d)throw new Error("Nesting :not in a selector is not allowed");d=!0,s=new n,i.notSelectors.push(s)}let m=r[2];if(m){let h=r[3];h==="#"?s.addAttribute("id",m.slice(1)):h==="."?s.addClassName(m.slice(1)):s.setElement(m)}let u=r[4];if(u&&s.addAttribute(s.unescapeAttribute(u),r[6]),r[7]&&(d=!1,s=i),r[8]){if(d)throw new Error("Multiple selectors in :not are not supported");t(e,i),i=s=new n}}return t(e,i),e}unescapeAttribute(o){let e="",t=!1;for(let i=0;i0&&o.push("class",this.classNames.join(" ")),o.concat(this.attrs)}addAttribute(o,e=""){this.attrs.push(o,e&&e.toLowerCase()||"")}addClassName(o){this.classNames.push(o.toLowerCase())}toString(){let o=this.element||"";if(this.classNames&&this.classNames.forEach(e=>o+=`.${e}`),this.attrs)for(let e=0;eo+=`:not(${e})`),o}},xx=class n{static createNotMatcher(o){let e=new n;return e.addSelectables(o,null),e}_elementMap=new Map;_elementPartialMap=new Map;_classMap=new Map;_classPartialMap=new Map;_attrValueMap=new Map;_attrValuePartialMap=new Map;_listContexts=[];addSelectables(o,e){let t=null;o.length>1&&(t=new nP(o),this._listContexts.push(t));for(let i=0;i0&&(!this.listContext||!this.listContext.alreadyMatched)&&(t=!xx.createNotMatcher(this.notSelectors).match(o,null)),t&&e&&(!this.listContext||!this.listContext.alreadyMatched)&&(this.listContext&&(this.listContext.alreadyMatched=!0),e(this.selector,this.cbContext)),t}},yx=class{registry;constructor(o){this.registry=o}match(o){return this.registry.has(o)?this.registry.get(o):[]}},mi=(function(n){return n[n.NONE=0]="NONE",n[n.HTML=1]="HTML",n[n.STYLE=2]="STYLE",n[n.SCRIPT=3]="SCRIPT",n[n.URL=4]="URL",n[n.RESOURCE_URL=5]="RESOURCE_URL",n[n.ATTRIBUTE_NO_BINDING=6]="ATTRIBUTE_NO_BINDING",n})(mi||{}),gx,f5="svg",fH="math";function CH(){return gx||(gx={},dp(mi.HTML,void 0,[["iframe",["srcdoc"]],["*",["innerHTML","outerHTML"]]]),dp(mi.STYLE,void 0,[["*",["style"]]]),dp(mi.URL,void 0,[["*",["formAction"]],["area",["href"]],["a",["href","xlink:href"]],["form",["action"]],["img",["src"]],["video",["src"]]]),dp(mi.URL,fH,[["*",["href","xlink:href"]],["annotation",["href","xlink:href"]],["annotation-xml",["href","xlink:href"]],["maction",["href","xlink:href"]],["malignmark",["href","xlink:href"]],["math",["href","xlink:href"]],["mroot",["href","xlink:href"]],["msqrt",["href","xlink:href"]],["merror",["href","xlink:href"]],["mfrac",["href","xlink:href"]],["mglyph",["href","xlink:href"]],["msub",["href","xlink:href"]],["msup",["href","xlink:href"]],["msubsup",["href","xlink:href"]],["mmultiscripts",["href","xlink:href"]],["mprescripts",["href","xlink:href"]],["mi",["href","xlink:href"]],["mn",["href","xlink:href"]],["mo",["href","xlink:href"]],["mpadded",["href","xlink:href"]],["mphantom",["href","xlink:href"]],["mrow",["href","xlink:href"]],["ms",["href","xlink:href"]],["mspace",["href","xlink:href"]],["mstyle",["href","xlink:href"]],["mtable",["href","xlink:href"]],["mtd",["href","xlink:href"]],["mtr",["href","xlink:href"]],["mtext",["href","xlink:href"]],["mover",["href","xlink:href"]],["munder",["href","xlink:href"]],["munderover",["href","xlink:href"]],["semantics",["href","xlink:href"]],["none",["href","xlink:href"]]]),dp(mi.RESOURCE_URL,void 0,[["base",["href"]],["embed",["src"]],["frame",["src"]],["iframe",["src"]],["link",["href"]],["object",["codebase","data"]]]),dp(mi.URL,f5,[["a",["href","xlink:href"]]]),dp(mi.ATTRIBUTE_NO_BINDING,f5,[["animate",["attributeName","values","to","from"]],["set",["to","attributeName"]],["animateMotion",["attributeName"]],["animateTransform",["attributeName"]]]),dp(mi.ATTRIBUTE_NO_BINDING,void 0,[["unknown",["attributeName","values","to","from","sandbox","allow","allowFullscreen","referrerPolicy","csp","fetchPriority"]],["iframe",["sandbox","allow","allowFullscreen","referrerPolicy","csp","fetchPriority"]]])),gx}function dp(n,o,e){for(let[t,i]of e){let r=o&&t!=="unknown"?`:${o}:${t}`:t;r=r.toLowerCase();for(let s of i)gx[`${r}|${s.toLowerCase()}`]=n}}var zm=(function(n){return n[n.Emulated=0]="Emulated",n[n.None=2]="None",n[n.ShadowDom=3]="ShadowDom",n[n.ExperimentalIsolatedShadowDom=4]="ExperimentalIsolatedShadowDom",n})(zm||{}),IT=(function(n){return n[n.OnPush=0]="OnPush",n[n.Default=1]="Default",n[n.Eager=1]="Eager",n})(IT||{}),vf=(function(n){return n[n.None=0]="None",n[n.SignalBased=1]="SignalBased",n[n.HasDecoratorInputTransform=2]="HasDecoratorInputTransform",n})(vf||{}),C5={name:"custom-elements"},v5={name:"no-errors-schema"};function vH(n){let o=n.classNames&&n.classNames.length?[8,...n.classNames]:[];return[n.element&&n.element!=="*"?n.element:"",...n.attrs,...o]}function xH(n){let o=n.classNames&&n.classNames.length?[8,...n.classNames]:[];return n.element?[5,n.element,...n.attrs,...o]:n.attrs.length?[3,...n.attrs,...o]:n.classNames&&n.classNames.length?[9,...n.classNames]:[]}function yH(n){let o=vH(n),e=n.notSelectors&&n.notSelectors.length?n.notSelectors.map(t=>xH(t)):[];return o.concat(...e)}function AT(n){return n?Ag.parse(n).map(yH):[]}var kd=(function(n){return n[n.Directive=0]="Directive",n[n.Component=1]="Component",n[n.Injectable=2]="Injectable",n[n.Pipe=3]="Pipe",n[n.NgModule=4]="NgModule",n})(kd||{});var bx;function bH(n){return kH(SH(n.nodes).join("")+`[${n.meaning}]`)}function wH(n){return n.id||mN(n)}function mN(n){let o=new oP,e=n.nodes.map(t=>t.visit(o,null));return uN(e.join(""),n.meaning)}var wx=class{visitText(o,e){return o.value}visitContainer(o,e){return`[${o.children.map(t=>t.visit(this)).join(", ")}]`}visitIcu(o,e){let t=Object.keys(o.cases).map(i=>`${i} {${o.cases[i].visit(this)}}`);return`{${o.expression}, ${o.type}, ${t.join(", ")}}`}visitTagPlaceholder(o,e){return o.isVoid?``:`${o.children.map(t=>t.visit(this)).join(", ")}`}visitPlaceholder(o,e){return o.value?`${o.value}`:``}visitIcuPlaceholder(o,e){return`${o.value.visit(this)}`}visitBlockPlaceholder(o,e){return`${o.children.map(t=>t.visit(this)).join(", ")}`}},MH=new wx;function SH(n){return n.map(o=>o.visit(MH,null))}var oP=class extends wx{visitIcu(o){let e=Object.keys(o.cases).map(t=>`${t} {${o.cases[t].visit(this)}}`);return`{${o.type}, ${e.join(", ")}}`}};function kH(n){bx??=new TextEncoder;let o=[...bx.encode(n)],e=EH(o,NT.Big),t=o.length*8,i=new Uint32Array(80),r=1732584193,s=4023233417,d=2562383102,m=271733878,u=3285377520;e[t>>5]|=128<<24-t%32,e[(t+64>>9<<4)+15]=t;for(let h=0;h>>0).toString(16).padStart(8,"0")}function PH(n,o,e,t){return n<20?[o&e|~o&t,1518500249]:n<40?[o^e^t,1859775393]:n<60?[o&e|o&t|e&t,2400959708]:[o^e^t,3395469782]}function x5(n){bx??=new TextEncoder;let o=bx.encode(n),e=new DataView(o.buffer,o.byteOffset,o.byteLength),t=y5(e,o.length,0),i=y5(e,o.length,102072);return t==0&&(i==0||i==1)&&(t=t^319790063,i=i^-1801410264),BigInt.asUintN(32,BigInt(t))<>BigInt(63)&BigInt(1),e+=x5(o)),BigInt.asUintN(63,e).toString()}function y5(n,o,e){let t=2654435769,i=2654435769,r=0,s=o-12;for(;r<=s;r+=12){t+=n.getUint32(r,!0),i+=n.getUint32(r+4,!0),e+=n.getUint32(r+8,!0);let m=b5(t,i,e);t=m[0],i=m[1],e=m[2]}let d=o-r;return e+=o,d>=4?(t+=n.getUint32(r,!0),r+=4,d>=8?(i+=n.getUint32(r,!0),r+=4,d>=9&&(e+=n.getUint8(r++)<<8),d>=10&&(e+=n.getUint8(r++)<<16),d===11&&(e+=n.getUint8(r++)<<24)):(d>=5&&(i+=n.getUint8(r++)),d>=6&&(i+=n.getUint8(r++)<<8),d===7&&(i+=n.getUint8(r++)<<16))):(d>=1&&(t+=n.getUint8(r++)),d>=2&&(t+=n.getUint8(r++)<<8),d===3&&(t+=n.getUint8(r++)<<16)),b5(t,i,e)[2]}function b5(n,o,e){return n-=o,n-=e,n^=e>>>13,o-=e,o-=n,o^=n<<8,e-=n,e-=o,e^=o>>>13,n-=o,n-=e,n^=e>>>12,o-=e,o-=n,o^=n<<16,e-=n,e-=o,e^=o>>>5,n-=o,n-=e,n^=e>>>3,o-=e,o-=n,o^=n<<10,e-=n,e-=o,e^=o>>>15,[n,o,e]}var NT=(function(n){return n[n.Little=0]="Little",n[n.Big=1]="Big",n})(NT||{});function _g(n,o){return TH(n,o)[1]}function TH(n,o){let e=(n&65535)+(o&65535),t=(n>>>16)+(o>>>16)+(e>>>16);return[t>>>16,t<<16|e&65535]}function Fk(n,o){return n<>>32-o}function EH(n,o){let e=n.length+3>>>2,t=[];for(let i=0;i=n.length?0:n[o]}function OH(n,o,e){let t=0;if(e===NT.Big)for(let i=0;i<4;i++)t+=w5(n,o+i)<<24-8*i;else for(let i=0;i<4;i++)t+=w5(n,o+i)<<8*i;return t}var gN=(function(n){return n[n.None=0]="None",n[n.Const=1]="Const",n})(gN||{}),Mx=class{modifiers;constructor(o=gN.None){this.modifiers=o}hasModifier(o){return(this.modifiers&o)!==0}},Fd=(function(n){return n[n.Dynamic=0]="Dynamic",n[n.Bool=1]="Bool",n[n.String=2]="String",n[n.Int=3]="Int",n[n.Number=4]="Number",n[n.Function=5]="Function",n[n.Inferred=6]="Inferred",n[n.None=7]="None",n})(Fd||{}),Fc=class extends Mx{name;constructor(o,e){super(e),this.name=o}visitType(o,e){return o.visitBuiltinType(this,e)}},xl=class extends Mx{value;typeParams;constructor(o,e,t=null){super(e),this.value=o,this.typeParams=t}visitType(o,e){return o.visitExpressionType(this,e)}};var xs=new Fc(Fd.Dynamic),ec=new Fc(Fd.Inferred),DH=new Fc(Fd.Bool),vje=new Fc(Fd.Int),Jm=new Fc(Fd.Number),FT=new Fc(Fd.String),xje=new Fc(Fd.Function),Rc=new Fc(Fd.None),Nf=(function(n){return n[n.Minus=0]="Minus",n[n.Plus=1]="Plus",n})(Nf||{}),lt=(function(n){return n[n.Equals=0]="Equals",n[n.NotEquals=1]="NotEquals",n[n.Assign=2]="Assign",n[n.Identical=3]="Identical",n[n.NotIdentical=4]="NotIdentical",n[n.Minus=5]="Minus",n[n.Plus=6]="Plus",n[n.Divide=7]="Divide",n[n.Multiply=8]="Multiply",n[n.Modulo=9]="Modulo",n[n.And=10]="And",n[n.Or=11]="Or",n[n.BitwiseOr=12]="BitwiseOr",n[n.BitwiseAnd=13]="BitwiseAnd",n[n.Lower=14]="Lower",n[n.LowerEquals=15]="LowerEquals",n[n.Bigger=16]="Bigger",n[n.BiggerEquals=17]="BiggerEquals",n[n.NullishCoalesce=18]="NullishCoalesce",n[n.Exponentiation=19]="Exponentiation",n[n.In=20]="In",n[n.InstanceOf=21]="InstanceOf",n[n.AdditionAssignment=22]="AdditionAssignment",n[n.SubtractionAssignment=23]="SubtractionAssignment",n[n.MultiplicationAssignment=24]="MultiplicationAssignment",n[n.DivisionAssignment=25]="DivisionAssignment",n[n.RemainderAssignment=26]="RemainderAssignment",n[n.ExponentiationAssignment=27]="ExponentiationAssignment",n[n.AndAssignment=28]="AndAssignment",n[n.OrAssignment=29]="OrAssignment",n[n.NullishCoalesceAssignment=30]="NullishCoalesceAssignment",n})(lt||{});function IH(n,o){return n==null||o==null?n==o:n.isEquivalent(o)}function hN(n,o,e){let t=n.length;if(t!==o.length)return!1;for(let i=0;ie.isEquivalent(t))}var Wi=class{type;sourceSpan;constructor(o,e){this.type=o||null,this.sourceSpan=e||null}prop(o,e){return new Ks(this,o,null,e)}key(o,e,t){return new Id(this,o,e,t)}callFn(o,e,t){return new ys(this,o,null,e,t)}instantiate(o,e,t){return new Rf(this,o,e,t)}conditional(o,e=null,t){return new Lc(this,o,e,null,t)}equals(o,e){return new Si(lt.Equals,this,o,null,e)}notEquals(o,e){return new Si(lt.NotEquals,this,o,null,e)}identical(o,e){return new Si(lt.Identical,this,o,null,e)}notIdentical(o,e){return new Si(lt.NotIdentical,this,o,null,e)}minus(o,e){return new Si(lt.Minus,this,o,null,e)}plus(o,e){return new Si(lt.Plus,this,o,null,e)}divide(o,e){return new Si(lt.Divide,this,o,null,e)}multiply(o,e){return new Si(lt.Multiply,this,o,null,e)}modulo(o,e){return new Si(lt.Modulo,this,o,null,e)}power(o,e){return new Si(lt.Exponentiation,this,o,null,e)}and(o,e){return new Si(lt.And,this,o,null,e)}bitwiseOr(o,e){return new Si(lt.BitwiseOr,this,o,null,e)}bitwiseAnd(o,e){return new Si(lt.BitwiseAnd,this,o,null,e)}or(o,e){return new Si(lt.Or,this,o,null,e)}lower(o,e){return new Si(lt.Lower,this,o,null,e)}lowerEquals(o,e){return new Si(lt.LowerEquals,this,o,null,e)}bigger(o,e){return new Si(lt.Bigger,this,o,null,e)}biggerEquals(o,e){return new Si(lt.BiggerEquals,this,o,null,e)}isBlank(o){return this.equals(LH,o)}nullishCoalesce(o,e){return new Si(lt.NullishCoalesce,this,o,null,e)}toStmt(){return new fa(this,null)}},tc=class n extends Wi{name;constructor(o,e,t){super(e,t),this.name=o}isEquivalent(o){return o instanceof n&&this.name===o.name}isConstant(){return!1}visitExpression(o,e){return o.visitReadVarExpr(this,e)}clone(){return new n(this.name,this.type,this.sourceSpan)}set(o){return new Si(lt.Assign,this,o,null,this.sourceSpan)}},Ng=class n extends Wi{expr;constructor(o,e,t){super(e,t),this.expr=o}visitExpression(o,e){return o.visitTypeofExpr(this,e)}isEquivalent(o){return o instanceof n&&o.expr.isEquivalent(this.expr)}isConstant(){return this.expr.isConstant()}clone(){return new n(this.expr.clone())}},Sx=class n extends Wi{expr;constructor(o,e,t){super(e,t),this.expr=o}visitExpression(o,e){return o.visitVoidExpr(this,e)}isEquivalent(o){return o instanceof n&&o.expr.isEquivalent(this.expr)}isConstant(){return this.expr.isConstant()}clone(){return new n(this.expr.clone())}},hi=class n extends Wi{node;constructor(o,e,t){super(e,t),this.node=o}isEquivalent(o){return o instanceof n&&this.node===o.node}isConstant(){return!1}visitExpression(o,e){return o.visitWrappedNodeExpr(this,e)}clone(){return new n(this.node,this.type,this.sourceSpan)}},ys=class n extends Wi{fn;args;pure;constructor(o,e,t,i,r=!1){super(t,i),this.fn=o,this.args=e,this.pure=r}get receiver(){return this.fn}isEquivalent(o){return o instanceof n&&this.fn.isEquivalent(o.fn)&&Qs(this.args,o.args)&&this.pure===o.pure}isConstant(){return!1}visitExpression(o,e){return o.visitInvokeFunctionExpr(this,e)}clone(){return new n(this.fn.clone(),this.args.map(o=>o.clone()),this.type,this.sourceSpan,this.pure)}},Ff=class n extends Wi{tag;template;constructor(o,e,t,i){super(t,i),this.tag=o,this.template=e}isEquivalent(o){return o instanceof n&&this.tag.isEquivalent(o.tag)&&this.template.isEquivalent(o.template)}isConstant(){return!1}visitExpression(o,e){return o.visitTaggedTemplateLiteralExpr(this,e)}clone(){return new n(this.tag.clone(),this.template.clone(),this.type,this.sourceSpan)}},Rf=class n extends Wi{classExpr;args;constructor(o,e,t,i){super(t,i),this.classExpr=o,this.args=e}isEquivalent(o){return o instanceof n&&this.classExpr.isEquivalent(o.classExpr)&&Qs(this.args,o.args)}isConstant(){return!1}visitExpression(o,e){return o.visitInstantiateExpr(this,e)}clone(){return new n(this.classExpr.clone(),this.args.map(o=>o.clone()),this.type,this.sourceSpan)}},Fg=class n extends Wi{body;flags;constructor(o,e,t){super(null,t),this.body=o,this.flags=e}isEquivalent(o){return o instanceof n&&this.body===o.body&&this.flags===o.flags}isConstant(){return!0}visitExpression(o,e){return o.visitRegularExpressionLiteral(this,e)}clone(){return new n(this.body,this.flags,this.sourceSpan)}},_a=class n extends Wi{value;constructor(o,e,t){super(e,t),this.value=o}isEquivalent(o){return o instanceof n&&this.value===o.value}isConstant(){return!0}visitExpression(o,e){return o.visitLiteralExpr(this,e)}clone(){return new n(this.value,this.type,this.sourceSpan)}},Lf=class n extends Wi{elements;expressions;constructor(o,e,t){super(null,t),this.elements=o,this.expressions=e}isEquivalent(o){return o instanceof n&&hN(this.elements,o.elements,(e,t)=>e.text===t.text)&&Qs(this.expressions,o.expressions)}isConstant(){return!1}visitExpression(o,e){return o.visitTemplateLiteralExpr(this,e)}clone(){return new n(this.elements.map(o=>o.clone()),this.expressions.map(o=>o.clone()))}},kx=class n extends Wi{text;rawText;constructor(o,e,t){super(FT,e),this.text=o,this.rawText=t??rP(hx(o))}visitExpression(o,e){return o.visitTemplateLiteralElementExpr(this,e)}isEquivalent(o){return o instanceof n&&o.text===this.text&&o.rawText===this.rawText}isConstant(){return!0}clone(){return new n(this.text,this.sourceSpan,this.rawText)}},Gm=class{text;sourceSpan;constructor(o,e){this.text=o,this.sourceSpan=e}},Og=class{text;sourceSpan;associatedMessage;constructor(o,e,t){this.text=o,this.sourceSpan=e,this.associatedMessage=t}},AH="|",M5="@@",NH="\u241F",Px=class n extends Wi{metaBlock;messageParts;placeHolderNames;expressions;constructor(o,e,t,i,r){super(FT,r),this.metaBlock=o,this.messageParts=e,this.placeHolderNames=t,this.expressions=i}isEquivalent(o){return!1}isConstant(){return!1}visitExpression(o,e){return o.visitLocalizedString(this,e)}clone(){return new n(this.metaBlock,this.messageParts,this.placeHolderNames,this.expressions.map(o=>o.clone()),this.sourceSpan)}serializeI18nHead(){let o=this.metaBlock.description||"";return this.metaBlock.meaning&&(o=`${this.metaBlock.meaning}${AH}${o}`),this.metaBlock.customId&&(o=`${o}${M5}${this.metaBlock.customId}`),this.metaBlock.legacyIds&&this.metaBlock.legacyIds.forEach(e=>{o=`${o}${NH}${e}`}),S5(o,this.messageParts[0].text,this.getMessagePartSourceSpan(0))}getMessagePartSourceSpan(o){return this.messageParts[o]?.sourceSpan??this.sourceSpan}getPlaceholderSourceSpan(o){return this.placeHolderNames[o]?.sourceSpan??this.expressions[o]?.sourceSpan??this.sourceSpan}serializeI18nTemplatePart(o){let e=this.placeHolderNames[o-1],t=this.messageParts[o],i=e.text;return e.associatedMessage?.legacyIds.length===0&&(i+=`${M5}${uN(e.associatedMessage.messageString,e.associatedMessage.meaning)}`),S5(i,t.text,this.getMessagePartSourceSpan(o))}},hx=n=>n.replace(/\\/g,"\\\\"),FH=n=>n.replace(/^:/,"\\:"),RH=n=>n.replace(/:/g,"\\:"),rP=n=>n.replace(/`/g,"\\`").replace(/\${/g,"$\\{");function S5(n,o,e){return n===""?{cooked:o,raw:rP(FH(hx(o))),range:e}:{cooked:`:${n}:${o}`,raw:rP(`:${RH(hx(n))}:${hx(o)}`),range:e}}var eu=class n extends Wi{value;typeParams;constructor(o,e,t=null,i){super(e,i),this.value=o,this.typeParams=t}isEquivalent(o){return o instanceof n&&this.value.name===o.value.name&&this.value.moduleName===o.value.moduleName}isConstant(){return!1}visitExpression(o,e){return o.visitExternalExpr(this,e)}clone(){return new n(this.value,this.type,this.typeParams,this.sourceSpan)}};var Lc=class n extends Wi{condition;falseCase;trueCase;constructor(o,e,t=null,i,r){super(i||e.type,r),this.condition=o,this.falseCase=t,this.trueCase=e}isEquivalent(o){return o instanceof n&&this.condition.isEquivalent(o.condition)&&this.trueCase.isEquivalent(o.trueCase)&&IH(this.falseCase,o.falseCase)}isConstant(){return!1}visitExpression(o,e){return o.visitConditionalExpr(this,e)}clone(){return new n(this.condition.clone(),this.trueCase.clone(),this.falseCase?.clone(),this.type,this.sourceSpan)}};var zf=class n extends Wi{condition;constructor(o,e){super(DH,e),this.condition=o}isEquivalent(o){return o instanceof n&&this.condition.isEquivalent(o.condition)}isConstant(){return!1}visitExpression(o,e){return o.visitNotExpr(this,e)}clone(){return new n(this.condition.clone(),this.sourceSpan)}},Er=class n{name;type;constructor(o,e=null){this.name=o,this.type=e}isEquivalent(o){return this.name===o.name}clone(){return new n(this.name,this.type)}},xp=class n extends Wi{params;statements;name;constructor(o,e,t,i,r){super(t,i),this.params=o,this.statements=e,this.name=r}isEquivalent(o){return(o instanceof n||o instanceof Vf)&&Qs(this.params,o.params)&&Qs(this.statements,o.statements)}isConstant(){return!1}visitExpression(o,e){return o.visitFunctionExpr(this,e)}toDeclStmt(o,e){return new Vf(o,this.params,this.statements,this.type,e,this.sourceSpan)}clone(){return new n(this.params.map(o=>o.clone()),this.statements,this.type,this.sourceSpan,this.name)}},gu=class aP extends Wi{params;body;constructor(o,e,t,i){super(t,i),this.params=o,this.body=e}isEquivalent(o){return!(o instanceof aP)||!Qs(this.params,o.params)?!1:this.body instanceof Wi&&o.body instanceof Wi?this.body.isEquivalent(o.body):Array.isArray(this.body)&&Array.isArray(o.body)?Qs(this.body,o.body):!1}isConstant(){return!1}visitExpression(o,e){return o.visitArrowFunctionExpr(this,e)}clone(){return new aP(this.params.map(o=>o.clone()),Array.isArray(this.body)?this.body:this.body.clone(),this.type,this.sourceSpan)}toDeclStmt(o,e){return new Ur(o,this,ec,e,this.sourceSpan)}},tu=class n extends Wi{operator;expr;parens;constructor(o,e,t,i,r=!0){super(t||Jm,i),this.operator=o,this.expr=e,this.parens=r}isEquivalent(o){return o instanceof n&&this.operator===o.operator&&this.expr.isEquivalent(o.expr)}isConstant(){return!1}visitExpression(o,e){return o.visitUnaryOperatorExpr(this,e)}clone(){return new n(this.operator,this.expr.clone(),this.type,this.sourceSpan,this.parens)}},nc=class n extends Wi{expr;constructor(o,e,t){super(e,t),this.expr=o}visitExpression(o,e){return o.visitParenthesizedExpr(this,e)}isEquivalent(o){return o instanceof n&&o.expr.isEquivalent(this.expr)}isConstant(){return this.expr.isConstant()}clone(){return new n(this.expr.clone())}},Si=class n extends Wi{operator;rhs;lhs;constructor(o,e,t,i,r){super(i||e.type,r),this.operator=o,this.rhs=t,this.lhs=e}isEquivalent(o){return o instanceof n&&this.operator===o.operator&&this.lhs.isEquivalent(o.lhs)&&this.rhs.isEquivalent(o.rhs)}isConstant(){return!1}visitExpression(o,e){return o.visitBinaryOperatorExpr(this,e)}clone(){return new n(this.operator,this.lhs.clone(),this.rhs.clone(),this.type,this.sourceSpan)}isAssignment(){let o=this.operator;return o===lt.Assign||o===lt.AdditionAssignment||o===lt.SubtractionAssignment||o===lt.MultiplicationAssignment||o===lt.DivisionAssignment||o===lt.RemainderAssignment||o===lt.ExponentiationAssignment||o===lt.AndAssignment||o===lt.OrAssignment||o===lt.NullishCoalesceAssignment}},Ks=class n extends Wi{receiver;name;constructor(o,e,t,i){super(t,i),this.receiver=o,this.name=e}get index(){return this.name}isEquivalent(o){return o instanceof n&&this.receiver.isEquivalent(o.receiver)&&this.name===o.name}isConstant(){return!1}visitExpression(o,e){return o.visitReadPropExpr(this,e)}set(o){return new Si(lt.Assign,this.receiver.prop(this.name),o,null,this.sourceSpan)}clone(){return new n(this.receiver.clone(),this.name,this.type,this.sourceSpan)}},Id=class n extends Wi{receiver;index;constructor(o,e,t,i){super(t,i),this.receiver=o,this.index=e}isEquivalent(o){return o instanceof n&&this.receiver.isEquivalent(o.receiver)&&this.index.isEquivalent(o.index)}isConstant(){return!1}visitExpression(o,e){return o.visitReadKeyExpr(this,e)}set(o){return new Si(lt.Assign,this.receiver.key(this.index),o,null,this.sourceSpan)}clone(){return new n(this.receiver.clone(),this.index.clone(),this.type,this.sourceSpan)}},zc=class n extends Wi{entries;constructor(o,e,t){super(e,t),this.entries=o}isConstant(){return this.entries.every(o=>o.isConstant())}isEquivalent(o){return o instanceof n&&Qs(this.entries,o.entries)}visitExpression(o,e){return o.visitLiteralArrayExpr(this,e)}clone(){return new n(this.entries.map(o=>o.clone()),this.type,this.sourceSpan)}},Rg=class n{key;value;quoted;constructor(o,e,t){this.key=o,this.value=e,this.quoted=t}isEquivalent(o){return this.key===o.key&&this.value.isEquivalent(o.value)}clone(){return new n(this.key,this.value.clone(),this.quoted)}isConstant(){return this.value.isConstant()}},yp=class n{expression;constructor(o){this.expression=o}isEquivalent(o){return o instanceof n&&this.expression.isEquivalent(o.expression)}clone(){return new n(this.expression.clone())}isConstant(){return this.expression.isConstant()}},ic=class n extends Wi{entries;valueType=null;constructor(o,e,t){super(e,t),this.entries=o,e&&(this.valueType=e.valueType)}isEquivalent(o){return o instanceof n&&Qs(this.entries,o.entries)}isConstant(){return this.entries.every(o=>o.isConstant())}visitExpression(o,e){return o.visitLiteralMapExpr(this,e)}clone(){let o=this.entries.map(e=>e.clone());return new n(o,this.type,this.sourceSpan)}};var nu=class n extends Wi{expression;constructor(o,e){super(null,e),this.expression=o}isEquivalent(o){return o instanceof n&&this.expression.isEquivalent(o.expression)}isConstant(){return this.expression.isConstant()}visitExpression(o,e){return o.visitSpreadElementExpr(this,e)}clone(){return new n(this.expression.clone(),this.sourceSpan)}},Lg=new _a(null,null,null),LH=new _a(null,ec,null),ua=(function(n){return n[n.None=0]="None",n[n.Final=1]="Final",n[n.Private=2]="Private",n[n.Exported=4]="Exported",n[n.Static=8]="Static",n})(ua||{}),sP=class{text;multiline;trailingNewline;constructor(o,e,t){this.text=o,this.multiline=e,this.trailingNewline=t}toString(){return this.multiline?` ${this.text} `:this.text}},Tx=class extends sP{tags;constructor(o){super("",!0,!0),this.tags=o}toString(){return $H(this.tags)}},iu=class{modifiers;sourceSpan;leadingComments;constructor(o=ua.None,e=null,t){this.modifiers=o,this.sourceSpan=e,this.leadingComments=t}hasModifier(o){return(this.modifiers&o)!==0}addLeadingComment(o){this.leadingComments=this.leadingComments??[],this.leadingComments.push(o)}},Ur=class n extends iu{name;value;type;constructor(o,e,t,i,r,s){super(i,r,s),this.name=o,this.value=e,this.type=t||e&&e.type||null}isEquivalent(o){return o instanceof n&&this.name===o.name&&(this.value?!!o.value&&this.value.isEquivalent(o.value):!o.value)}visitStatement(o,e){return o.visitDeclareVarStmt(this,e)}},Vf=class n extends iu{name;params;statements;type;constructor(o,e,t,i,r,s,d){super(r,s,d),this.name=o,this.params=e,this.statements=t,this.type=i||null}isEquivalent(o){return o instanceof n&&Qs(this.params,o.params)&&Qs(this.statements,o.statements)}visitStatement(o,e){return o.visitDeclareFunctionStmt(this,e)}},fa=class n extends iu{expr;constructor(o,e,t){super(ua.None,e,t),this.expr=o}isEquivalent(o){return o instanceof n&&this.expr.isEquivalent(o.expr)}visitStatement(o,e){return o.visitExpressionStmt(this,e)}},Or=class n extends iu{value;constructor(o,e=null,t){super(ua.None,e,t),this.value=o}isEquivalent(o){return o instanceof n&&this.value.isEquivalent(o.value)}visitStatement(o,e){return o.visitReturnStmt(this,e)}},Ex=class n extends iu{condition;trueCase;falseCase;constructor(o,e,t=[],i,r){super(ua.None,i,r),this.condition=o,this.trueCase=e,this.falseCase=t}isEquivalent(o){return o instanceof n&&this.condition.isEquivalent(o.condition)&&Qs(this.trueCase,o.trueCase)&&Qs(this.falseCase,o.falseCase)}visitStatement(o,e){return o.visitIfStmt(this,e)}};function zH(n=[]){return new Tx(n)}function si(n,o,e){return new tc(n,o,e)}function Vt(n,o=null,e){return new eu(n,null,o,e)}function ga(n,o,e){return new xl(n,o,e)}function D0(n){return new Ng(n)}function Ji(n,o,e){return new zc(n,o,e)}function yl(n,o=null){return new ic(n.map(e=>new Rg(e.key,e.value,e.quoted)),o,null)}function VH(n,o){return new zf(n,o)}function bp(n,o,e,t,i){return new xp(n,o,e,t,i)}function Xs(n,o,e,t){return new gu(n,o,e,t)}function T1(n,o,e,t,i){return new Ex(n,o,e,t,i)}function BH(n,o,e,t){return new Ff(n,o,e,t)}function Oe(n,o,e){return new _a(n,o,e)}function jH(n,o,e,t,i){return new Px(n,o,e,t,i)}function k5(n){let o="";if(n.tagName&&(o+=` @${n.tagName}`),n.text){if(n.text.match(/\/\*|\*\//))throw new Error('JSDoc text cannot contain "/*" and "*/"');o+=" "+n.text.replace(/@/g,"\\@")}return o}function $H(n){if(n.length===0)return"";if(n.length===1&&n[0].tagName&&!n[0].text)return`*${k5(n[0])} `;let o=`* +`;for(let e of n)o+=" *",o+=k5(e).replace(/\n/g,` + * `),o+=` +`;return o+=" ",o}var HH="_c",UH={},WH=50,Ox=class n extends Wi{resolved;original;shared=!1;constructor(o){super(o.type),this.resolved=o,this.original=o}visitExpression(o,e){return e===UH?this.original.visitExpression(o,e):this.resolved.visitExpression(o,e)}isEquivalent(o){return o instanceof n&&this.resolved.isEquivalent(o.resolved)}isConstant(){return!0}clone(){throw new Error("Not supported.")}fixup(o){this.resolved=o,this.shared=!0}},Dx=class{isClosureCompilerEnabled;statements=[];literals=new Map;literalFactories=new Map;sharedConstants=new Map;_claimedNames=new Map;nextNameIndex=0;constructor(o=!1){this.isClosureCompilerEnabled=o}getConstLiteral(o,e){if(o instanceof _a&&!P5(o)||o instanceof Ox)return o;let t=Bf.INSTANCE.keyOf(o),i=this.literals.get(t),r=!1;if(i||(i=new Ox(o),this.literals.set(t,i),r=!0),!r&&!i.shared||r&&e){let s=this.freshName(),d,m;this.isClosureCompilerEnabled&&P5(o)?(d=new xp([],[new Or(o)]),m=si(s).callFn([])):(d=o,m=si(s)),this.statements.push(new Ur(s,d,ec,ua.Final)),i.fixup(m)}return i}getSharedConstant(o,e){let t=o.keyOf(e);if(!this.sharedConstants.has(t)){let i=this.freshName();this.sharedConstants.set(t,si(i)),this.statements.push(o.toSharedConstantDeclaration(i,e))}return this.sharedConstants.get(t)}getSharedFunctionReference(o,e,t=!0){let i=o instanceof gu;for(let s of this.statements)if(i&&s instanceof Ur&&s.value?.isEquivalent(o)||!i&&s instanceof Vf&&o instanceof xp&&o.isEquivalent(s))return si(s.name);let r=t?this.uniqueName(e):e;return this.statements.push(o instanceof xp?o.toDeclStmt(r,ua.Final):new Ur(r,o,ec,ua.Final,o.sourceSpan)),si(r)}uniqueName(o,e=!0){let t=this._claimedNames.get(o)??0,i=t===0&&!e?`${o}`:`${o}${t}`;return this._claimedNames.set(o,t+1),i}freshName(){return this.uniqueName(HH)}},Bf=class n{static INSTANCE=new n;keyOf(o){if(o instanceof _a&&typeof o.value=="string")return`"${o.value}"`;if(o instanceof _a)return String(o.value);if(o instanceof Fg)return`/${o.body}/${o.flags??""}`;if(o instanceof zc){let e=[];for(let t of o.entries)e.push(this.keyOf(t));return`[${e.join(",")}]`}else if(o instanceof ic){let e=[];for(let t of o.entries)if(t instanceof yp)e.push("..."+this.keyOf(t.expression));else{let i=t.key;t.quoted&&(i=`"${i}"`),e.push(i+":"+this.keyOf(t.value))}return`{${e.join(",")}}`}else{if(o instanceof eu)return`import("${o.value.moduleName}", ${o.value.name})`;if(o instanceof tc)return`read(${o.name})`;if(o instanceof Ng)return`typeof(${this.keyOf(o.expr)})`;if(o instanceof nu)return`...${this.keyOf(o.expression)}`;throw new Error(`${this.constructor.name} does not handle expressions of type ${o.constructor.name}`)}}};function P5(n){return n instanceof _a&&typeof n.value=="string"&&n.value.length>=WH}var xe="@angular/core",ge=(()=>{class n{static core={name:null,moduleName:xe};static namespaceHTML={name:"\u0275\u0275namespaceHTML",moduleName:xe};static namespaceMathML={name:"\u0275\u0275namespaceMathML",moduleName:xe};static namespaceSVG={name:"\u0275\u0275namespaceSVG",moduleName:xe};static element={name:"\u0275\u0275element",moduleName:xe};static elementStart={name:"\u0275\u0275elementStart",moduleName:xe};static elementEnd={name:"\u0275\u0275elementEnd",moduleName:xe};static domElement={name:"\u0275\u0275domElement",moduleName:xe};static domElementStart={name:"\u0275\u0275domElementStart",moduleName:xe};static domElementEnd={name:"\u0275\u0275domElementEnd",moduleName:xe};static domElementContainer={name:"\u0275\u0275domElementContainer",moduleName:xe};static domElementContainerStart={name:"\u0275\u0275domElementContainerStart",moduleName:xe};static domElementContainerEnd={name:"\u0275\u0275domElementContainerEnd",moduleName:xe};static domTemplate={name:"\u0275\u0275domTemplate",moduleName:xe};static domListener={name:"\u0275\u0275domListener",moduleName:xe};static advance={name:"\u0275\u0275advance",moduleName:xe};static syntheticHostProperty={name:"\u0275\u0275syntheticHostProperty",moduleName:xe};static syntheticHostListener={name:"\u0275\u0275syntheticHostListener",moduleName:xe};static attribute={name:"\u0275\u0275attribute",moduleName:xe};static classProp={name:"\u0275\u0275classProp",moduleName:xe};static elementContainerStart={name:"\u0275\u0275elementContainerStart",moduleName:xe};static elementContainerEnd={name:"\u0275\u0275elementContainerEnd",moduleName:xe};static elementContainer={name:"\u0275\u0275elementContainer",moduleName:xe};static styleMap={name:"\u0275\u0275styleMap",moduleName:xe};static classMap={name:"\u0275\u0275classMap",moduleName:xe};static styleProp={name:"\u0275\u0275styleProp",moduleName:xe};static interpolate={name:"\u0275\u0275interpolate",moduleName:xe};static interpolate1={name:"\u0275\u0275interpolate1",moduleName:xe};static interpolate2={name:"\u0275\u0275interpolate2",moduleName:xe};static interpolate3={name:"\u0275\u0275interpolate3",moduleName:xe};static interpolate4={name:"\u0275\u0275interpolate4",moduleName:xe};static interpolate5={name:"\u0275\u0275interpolate5",moduleName:xe};static interpolate6={name:"\u0275\u0275interpolate6",moduleName:xe};static interpolate7={name:"\u0275\u0275interpolate7",moduleName:xe};static interpolate8={name:"\u0275\u0275interpolate8",moduleName:xe};static interpolateV={name:"\u0275\u0275interpolateV",moduleName:xe};static nextContext={name:"\u0275\u0275nextContext",moduleName:xe};static resetView={name:"\u0275\u0275resetView",moduleName:xe};static templateCreate={name:"\u0275\u0275template",moduleName:xe};static defer={name:"\u0275\u0275defer",moduleName:xe};static deferWhen={name:"\u0275\u0275deferWhen",moduleName:xe};static deferOnIdle={name:"\u0275\u0275deferOnIdle",moduleName:xe};static deferOnImmediate={name:"\u0275\u0275deferOnImmediate",moduleName:xe};static deferOnTimer={name:"\u0275\u0275deferOnTimer",moduleName:xe};static deferOnHover={name:"\u0275\u0275deferOnHover",moduleName:xe};static deferOnInteraction={name:"\u0275\u0275deferOnInteraction",moduleName:xe};static deferOnViewport={name:"\u0275\u0275deferOnViewport",moduleName:xe};static deferPrefetchWhen={name:"\u0275\u0275deferPrefetchWhen",moduleName:xe};static deferPrefetchOnIdle={name:"\u0275\u0275deferPrefetchOnIdle",moduleName:xe};static deferPrefetchOnImmediate={name:"\u0275\u0275deferPrefetchOnImmediate",moduleName:xe};static deferPrefetchOnTimer={name:"\u0275\u0275deferPrefetchOnTimer",moduleName:xe};static deferPrefetchOnHover={name:"\u0275\u0275deferPrefetchOnHover",moduleName:xe};static deferPrefetchOnInteraction={name:"\u0275\u0275deferPrefetchOnInteraction",moduleName:xe};static deferPrefetchOnViewport={name:"\u0275\u0275deferPrefetchOnViewport",moduleName:xe};static deferHydrateWhen={name:"\u0275\u0275deferHydrateWhen",moduleName:xe};static deferHydrateNever={name:"\u0275\u0275deferHydrateNever",moduleName:xe};static deferHydrateOnIdle={name:"\u0275\u0275deferHydrateOnIdle",moduleName:xe};static deferHydrateOnImmediate={name:"\u0275\u0275deferHydrateOnImmediate",moduleName:xe};static deferHydrateOnTimer={name:"\u0275\u0275deferHydrateOnTimer",moduleName:xe};static deferHydrateOnHover={name:"\u0275\u0275deferHydrateOnHover",moduleName:xe};static deferHydrateOnInteraction={name:"\u0275\u0275deferHydrateOnInteraction",moduleName:xe};static deferHydrateOnViewport={name:"\u0275\u0275deferHydrateOnViewport",moduleName:xe};static deferEnableTimerScheduling={name:"\u0275\u0275deferEnableTimerScheduling",moduleName:xe};static conditionalCreate={name:"\u0275\u0275conditionalCreate",moduleName:xe};static conditionalBranchCreate={name:"\u0275\u0275conditionalBranchCreate",moduleName:xe};static conditional={name:"\u0275\u0275conditional",moduleName:xe};static repeater={name:"\u0275\u0275repeater",moduleName:xe};static repeaterCreate={name:"\u0275\u0275repeaterCreate",moduleName:xe};static repeaterTrackByIndex={name:"\u0275\u0275repeaterTrackByIndex",moduleName:xe};static repeaterTrackByIdentity={name:"\u0275\u0275repeaterTrackByIdentity",moduleName:xe};static componentInstance={name:"\u0275\u0275componentInstance",moduleName:xe};static text={name:"\u0275\u0275text",moduleName:xe};static enableBindings={name:"\u0275\u0275enableBindings",moduleName:xe};static disableBindings={name:"\u0275\u0275disableBindings",moduleName:xe};static getCurrentView={name:"\u0275\u0275getCurrentView",moduleName:xe};static textInterpolate={name:"\u0275\u0275textInterpolate",moduleName:xe};static textInterpolate1={name:"\u0275\u0275textInterpolate1",moduleName:xe};static textInterpolate2={name:"\u0275\u0275textInterpolate2",moduleName:xe};static textInterpolate3={name:"\u0275\u0275textInterpolate3",moduleName:xe};static textInterpolate4={name:"\u0275\u0275textInterpolate4",moduleName:xe};static textInterpolate5={name:"\u0275\u0275textInterpolate5",moduleName:xe};static textInterpolate6={name:"\u0275\u0275textInterpolate6",moduleName:xe};static textInterpolate7={name:"\u0275\u0275textInterpolate7",moduleName:xe};static textInterpolate8={name:"\u0275\u0275textInterpolate8",moduleName:xe};static textInterpolateV={name:"\u0275\u0275textInterpolateV",moduleName:xe};static restoreView={name:"\u0275\u0275restoreView",moduleName:xe};static pureFunction0={name:"\u0275\u0275pureFunction0",moduleName:xe};static pureFunction1={name:"\u0275\u0275pureFunction1",moduleName:xe};static pureFunction2={name:"\u0275\u0275pureFunction2",moduleName:xe};static pureFunction3={name:"\u0275\u0275pureFunction3",moduleName:xe};static pureFunction4={name:"\u0275\u0275pureFunction4",moduleName:xe};static pureFunction5={name:"\u0275\u0275pureFunction5",moduleName:xe};static pureFunction6={name:"\u0275\u0275pureFunction6",moduleName:xe};static pureFunction7={name:"\u0275\u0275pureFunction7",moduleName:xe};static pureFunction8={name:"\u0275\u0275pureFunction8",moduleName:xe};static pureFunctionV={name:"\u0275\u0275pureFunctionV",moduleName:xe};static pipeBind1={name:"\u0275\u0275pipeBind1",moduleName:xe};static pipeBind2={name:"\u0275\u0275pipeBind2",moduleName:xe};static pipeBind3={name:"\u0275\u0275pipeBind3",moduleName:xe};static pipeBind4={name:"\u0275\u0275pipeBind4",moduleName:xe};static pipeBindV={name:"\u0275\u0275pipeBindV",moduleName:xe};static domProperty={name:"\u0275\u0275domProperty",moduleName:xe};static ariaProperty={name:"\u0275\u0275ariaProperty",moduleName:xe};static property={name:"\u0275\u0275property",moduleName:xe};static control={name:"\u0275\u0275control",moduleName:xe};static controlCreate={name:"\u0275\u0275controlCreate",moduleName:xe};static animationEnterListener={name:"\u0275\u0275animateEnterListener",moduleName:xe};static animationLeaveListener={name:"\u0275\u0275animateLeaveListener",moduleName:xe};static animationEnter={name:"\u0275\u0275animateEnter",moduleName:xe};static animationLeave={name:"\u0275\u0275animateLeave",moduleName:xe};static i18n={name:"\u0275\u0275i18n",moduleName:xe};static i18nAttributes={name:"\u0275\u0275i18nAttributes",moduleName:xe};static i18nExp={name:"\u0275\u0275i18nExp",moduleName:xe};static i18nStart={name:"\u0275\u0275i18nStart",moduleName:xe};static i18nEnd={name:"\u0275\u0275i18nEnd",moduleName:xe};static i18nApply={name:"\u0275\u0275i18nApply",moduleName:xe};static i18nPostprocess={name:"\u0275\u0275i18nPostprocess",moduleName:xe};static pipe={name:"\u0275\u0275pipe",moduleName:xe};static projection={name:"\u0275\u0275projection",moduleName:xe};static projectionDef={name:"\u0275\u0275projectionDef",moduleName:xe};static reference={name:"\u0275\u0275reference",moduleName:xe};static inject={name:"\u0275\u0275inject",moduleName:xe};static injectAttribute={name:"\u0275\u0275injectAttribute",moduleName:xe};static directiveInject={name:"\u0275\u0275directiveInject",moduleName:xe};static invalidFactory={name:"\u0275\u0275invalidFactory",moduleName:xe};static invalidFactoryDep={name:"\u0275\u0275invalidFactoryDep",moduleName:xe};static templateRefExtractor={name:"\u0275\u0275templateRefExtractor",moduleName:xe};static forwardRef={name:"forwardRef",moduleName:xe};static resolveForwardRef={name:"resolveForwardRef",moduleName:xe};static replaceMetadata={name:"\u0275\u0275replaceMetadata",moduleName:xe};static getReplaceMetadataURL={name:"\u0275\u0275getReplaceMetadataURL",moduleName:xe};static \u0275\u0275defineInjectable={name:"\u0275\u0275defineInjectable",moduleName:xe};static declareInjectable={name:"\u0275\u0275ngDeclareInjectable",moduleName:xe};static InjectableDeclaration={name:"\u0275\u0275InjectableDeclaration",moduleName:xe};static resolveWindow={name:"\u0275\u0275resolveWindow",moduleName:xe};static resolveDocument={name:"\u0275\u0275resolveDocument",moduleName:xe};static resolveBody={name:"\u0275\u0275resolveBody",moduleName:xe};static getComponentDepsFactory={name:"\u0275\u0275getComponentDepsFactory",moduleName:xe};static defineComponent={name:"\u0275\u0275defineComponent",moduleName:xe};static declareComponent={name:"\u0275\u0275ngDeclareComponent",moduleName:xe};static setComponentScope={name:"\u0275\u0275setComponentScope",moduleName:xe};static ChangeDetectionStrategy={name:"ChangeDetectionStrategy",moduleName:xe};static ViewEncapsulation={name:"ViewEncapsulation",moduleName:xe};static ComponentDeclaration={name:"\u0275\u0275ComponentDeclaration",moduleName:xe};static FactoryDeclaration={name:"\u0275\u0275FactoryDeclaration",moduleName:xe};static declareFactory={name:"\u0275\u0275ngDeclareFactory",moduleName:xe};static FactoryTarget={name:"\u0275\u0275FactoryTarget",moduleName:xe};static defineDirective={name:"\u0275\u0275defineDirective",moduleName:xe};static declareDirective={name:"\u0275\u0275ngDeclareDirective",moduleName:xe};static DirectiveDeclaration={name:"\u0275\u0275DirectiveDeclaration",moduleName:xe};static InjectorDef={name:"\u0275\u0275InjectorDef",moduleName:xe};static InjectorDeclaration={name:"\u0275\u0275InjectorDeclaration",moduleName:xe};static defineInjector={name:"\u0275\u0275defineInjector",moduleName:xe};static declareInjector={name:"\u0275\u0275ngDeclareInjector",moduleName:xe};static NgModuleDeclaration={name:"\u0275\u0275NgModuleDeclaration",moduleName:xe};static ModuleWithProviders={name:"ModuleWithProviders",moduleName:xe};static defineNgModule={name:"\u0275\u0275defineNgModule",moduleName:xe};static declareNgModule={name:"\u0275\u0275ngDeclareNgModule",moduleName:xe};static setNgModuleScope={name:"\u0275\u0275setNgModuleScope",moduleName:xe};static registerNgModuleType={name:"\u0275\u0275registerNgModuleType",moduleName:xe};static PipeDeclaration={name:"\u0275\u0275PipeDeclaration",moduleName:xe};static definePipe={name:"\u0275\u0275definePipe",moduleName:xe};static declarePipe={name:"\u0275\u0275ngDeclarePipe",moduleName:xe};static declareClassMetadata={name:"\u0275\u0275ngDeclareClassMetadata",moduleName:xe};static declareClassMetadataAsync={name:"\u0275\u0275ngDeclareClassMetadataAsync",moduleName:xe};static setClassMetadata={name:"\u0275setClassMetadata",moduleName:xe};static setClassMetadataAsync={name:"\u0275setClassMetadataAsync",moduleName:xe};static setClassDebugInfo={name:"\u0275setClassDebugInfo",moduleName:xe};static queryRefresh={name:"\u0275\u0275queryRefresh",moduleName:xe};static viewQuery={name:"\u0275\u0275viewQuery",moduleName:xe};static loadQuery={name:"\u0275\u0275loadQuery",moduleName:xe};static contentQuery={name:"\u0275\u0275contentQuery",moduleName:xe};static viewQuerySignal={name:"\u0275\u0275viewQuerySignal",moduleName:xe};static contentQuerySignal={name:"\u0275\u0275contentQuerySignal",moduleName:xe};static queryAdvance={name:"\u0275\u0275queryAdvance",moduleName:xe};static twoWayProperty={name:"\u0275\u0275twoWayProperty",moduleName:xe};static twoWayBindingSet={name:"\u0275\u0275twoWayBindingSet",moduleName:xe};static twoWayListener={name:"\u0275\u0275twoWayListener",moduleName:xe};static declareLet={name:"\u0275\u0275declareLet",moduleName:xe};static storeLet={name:"\u0275\u0275storeLet",moduleName:xe};static readContextLet={name:"\u0275\u0275readContextLet",moduleName:xe};static arrowFunction={name:"\u0275\u0275arrowFunction",moduleName:xe};static attachSourceLocations={name:"\u0275\u0275attachSourceLocations",moduleName:xe};static NgOnChangesFeature={name:"\u0275\u0275NgOnChangesFeature",moduleName:xe};static ControlFeature={name:"\u0275\u0275ControlFeature",moduleName:xe};static InheritDefinitionFeature={name:"\u0275\u0275InheritDefinitionFeature",moduleName:xe};static ProvidersFeature={name:"\u0275\u0275ProvidersFeature",moduleName:xe};static HostDirectivesFeature={name:"\u0275\u0275HostDirectivesFeature",moduleName:xe};static ExternalStylesFeature={name:"\u0275\u0275ExternalStylesFeature",moduleName:xe};static listener={name:"\u0275\u0275listener",moduleName:xe};static getInheritedFactory={name:"\u0275\u0275getInheritedFactory",moduleName:xe};static sanitizeHtml={name:"\u0275\u0275sanitizeHtml",moduleName:xe};static sanitizeStyle={name:"\u0275\u0275sanitizeStyle",moduleName:xe};static validateAttribute={name:"\u0275\u0275validateAttribute",moduleName:xe};static sanitizeResourceUrl={name:"\u0275\u0275sanitizeResourceUrl",moduleName:xe};static sanitizeScript={name:"\u0275\u0275sanitizeScript",moduleName:xe};static sanitizeUrl={name:"\u0275\u0275sanitizeUrl",moduleName:xe};static sanitizeUrlOrResourceUrl={name:"\u0275\u0275sanitizeUrlOrResourceUrl",moduleName:xe};static trustConstantHtml={name:"\u0275\u0275trustConstantHtml",moduleName:xe};static trustConstantResourceUrl={name:"\u0275\u0275trustConstantResourceUrl",moduleName:xe};static inputDecorator={name:"Input",moduleName:xe};static outputDecorator={name:"Output",moduleName:xe};static viewChildDecorator={name:"ViewChild",moduleName:xe};static viewChildrenDecorator={name:"ViewChildren",moduleName:xe};static contentChildDecorator={name:"ContentChild",moduleName:xe};static contentChildrenDecorator={name:"ContentChildren",moduleName:xe};static InputSignalBrandWriteType={name:"\u0275INPUT_SIGNAL_BRAND_WRITE_TYPE",moduleName:xe};static UnwrapDirectiveSignalInputs={name:"\u0275UnwrapDirectiveSignalInputs",moduleName:xe};static unwrapWritableSignal={name:"\u0275unwrapWritableSignal",moduleName:xe};static assertType={name:"\u0275assertType",moduleName:xe}}return n})(),GH=/-+([a-z0-9])/g;function qH(n){return n.replace(GH,(...o)=>o[1].toUpperCase())}function QH(n,o){return _N(n,":",o)}function KH(n,o){return _N(n,".",o)}function _N(n,o,e){let t=n.indexOf(o);return t==-1?e:[n.slice(0,t).trim(),n.slice(t+1).trim()]}function XH(n){let o=[];for(let e=0;e=55296&&t<=56319&&n.length>e+1){let i=n.charCodeAt(e+1);i>=56320&&i<=57343&&(e++,t=(t-55296<<10)+i-56320+65536)}t<=127?o.push(t):t<=2047?o.push(t>>6&31|192,t&63|128):t<=65535?o.push(t>>12|224,t>>6&63|128,t&63|128):t<=2097151&&o.push(t>>18&7|240,t>>12&63|128,t>>6&63|128,t&63|128)}return o}function fN(n){if(typeof n=="string")return n;if(Array.isArray(n))return`[${n.map(fN).join(", ")}]`;if(n==null)return""+n;let o=n.overriddenName||n.name;if(o)return`${o}`;if(!n.toString)return"object";let e=n.toString();if(e==null)return""+e;let t=e.indexOf(` +`);return t>=0?e.slice(0,t):e}var lP=class{full;major;minor;patch;constructor(o){this.full=o;let e=o.split(".");this.major=e[0],this.minor=e[1],this.patch=e.slice(2).join(".")}},Sf=globalThis,YH=/^([1-9]|1[0-8])\./;function CN(n){return n.startsWith("0.")?!0:!YH.test(n)}var ZH=3,JH="# sourceMappingURL=data:application/json;base64,",cP=class{file;sourcesContent=new Map;lines=[];lastCol0=0;hasMappings=!1;constructor(o=null){this.file=o}addSource(o,e=null){return this.sourcesContent.has(o)||this.sourcesContent.set(o,e),this}addLine(){return this.lines.push([]),this.lastCol0=0,this}addMapping(o,e,t,i){if(!this.currentLine)throw new Error("A line must be added before mappings can be added");if(e!=null&&!this.sourcesContent.has(e))throw new Error(`Unknown source file "${e}"`);if(o==null)throw new Error("The column in the generated code must be provided");if(o{o.set(u,h),e.push(u),t.push(this.sourcesContent.get(u)||null)});let i="",r=0,s=0,d=0,m=0;return this.lines.forEach(u=>{r=0,i+=u.map(h=>{let v=nx(h.col0-r);return r=h.col0,h.sourceUrl!=null&&(v+=nx(o.get(h.sourceUrl)-s),s=o.get(h.sourceUrl),v+=nx(h.sourceLine0-d),d=h.sourceLine0,v+=nx(h.sourceCol0-m),m=h.sourceCol0),v}).join(","),i+=";"}),i=i.slice(0,-1),{file:this.file||"",version:ZH,sourceRoot:"",sources:e,sourcesContent:t,mappings:i}}toJsComment(){return this.hasMappings?"//"+JH+eU(JSON.stringify(this,null,0)):""}};function eU(n){let o="",e=XH(n);for(let t=0;t>2),o+=xf((i&3)<<4|(r===null?0:r>>4)),o+=r===null?"=":xf((r&15)<<2|(s===null?0:s>>6)),o+=r===null||s===null?"=":xf(s&63)}return o}function nx(n){n=n<0?(-n<<1)+1:n<<1;let o="";do{let e=n&31;n=n>>5,n>0&&(e=e|32),o+=xf(e)}while(n>0);return o}var tU="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";function xf(n){if(n<0||n>=64)throw new Error("Can only encode value in the range [0, 63]");return tU[n]}var nU=/'|\\|\n|\r|\$/g,iU=/^[$A-Z_][0-9A-Z_$]*$/i,dP=" ",Ix=class{indent;partsLength=0;parts=[];srcSpans=[];constructor(o){this.indent=o}},oU=new Map([[lt.And,"&&"],[lt.Bigger,">"],[lt.BiggerEquals,">="],[lt.BitwiseOr,"|"],[lt.BitwiseAnd,"&"],[lt.Divide,"/"],[lt.Assign,"="],[lt.Equals,"=="],[lt.Identical,"==="],[lt.Lower,"<"],[lt.LowerEquals,"<="],[lt.Minus,"-"],[lt.Modulo,"%"],[lt.Exponentiation,"**"],[lt.Multiply,"*"],[lt.NotEquals,"!="],[lt.NotIdentical,"!=="],[lt.NullishCoalesce,"??"],[lt.Or,"||"],[lt.Plus,"+"],[lt.In,"in"],[lt.InstanceOf,"instanceof"],[lt.AdditionAssignment,"+="],[lt.SubtractionAssignment,"-="],[lt.MultiplicationAssignment,"*="],[lt.DivisionAssignment,"/="],[lt.RemainderAssignment,"%="],[lt.ExponentiationAssignment,"**="],[lt.AndAssignment,"&&="],[lt.OrAssignment,"||="],[lt.NullishCoalesceAssignment,"??="]]),pP=class n{_indent;static createRoot(){return new n(0)}_lines;constructor(o){this._indent=o,this._lines=[new Ix(o)]}get _currentLine(){return this._lines[this._lines.length-1]}println(o,e=""){this.print(o||null,e,!0)}lineIsEmpty(){return this._currentLine.parts.length===0}lineLength(){return this._currentLine.indent*dP.length+this._currentLine.partsLength}print(o,e,t=!1){e.length>0&&(this._currentLine.parts.push(e),this._currentLine.partsLength+=e.length,this._currentLine.srcSpans.push(o&&o.sourceSpan||null)),t&&this._lines.push(new Ix(this._indent))}removeEmptyLastLine(){this.lineIsEmpty()&&this._lines.pop()}incIndent(){this._indent++,this.lineIsEmpty()&&(this._currentLine.indent=this._indent)}decIndent(){this._indent--,this.lineIsEmpty()&&(this._currentLine.indent=this._indent)}toSource(){return this.sourceLines.map(o=>o.parts.length>0?T5(o.indent)+o.parts.join(""):"").join(` +`)}toSourceMapGenerator(o,e=0){let t=new cP(o),i=!1,r=()=>{i||(t.addSource(o," ").addMapping(0,o,0,0),i=!0)};for(let s=0;s{t.addLine();let m=s.srcSpans,u=s.parts,h=s.indent*dP.length,v=0;for(;vi)return t.srcSpans[r];i-=s.length}}return null}get sourceLines(){return this._lines.length&&this._lines[this._lines.length-1].parts.length===0?this._lines.slice(0,-1):this._lines}},mP=class{_escapeDollarInStrings;lastIfCondition=null;constructor(o){this._escapeDollarInStrings=o}printLeadingComments(o,e){if(o.leadingComments!==void 0)for(let t of o.leadingComments)t instanceof Tx?e.print(o,`/*${t.toString()}*/`,t.trailingNewline):t.multiline?e.print(o,`/* ${t.text} */`,t.trailingNewline):t.text.split(` +`).forEach(i=>{e.println(o,`// ${i}`)})}visitExpressionStmt(o,e){return this.printLeadingComments(o,e),o.expr.visitExpression(this,e),e.println(o,";"),null}visitReturnStmt(o,e){return this.printLeadingComments(o,e),e.print(o,"return "),o.value.visitExpression(this,e),e.println(o,";"),null}visitIfStmt(o,e){this.printLeadingComments(o,e),e.print(o,"if ("),this.lastIfCondition=o.condition,o.condition.visitExpression(this,e),this.lastIfCondition=null,e.print(o,") {");let t=o.falseCase!=null&&o.falseCase.length>0;return o.trueCase.length<=1&&!t?(e.print(o," "),this.visitAllStatements(o.trueCase,e),e.removeEmptyLastLine(),e.print(o," ")):(e.println(),e.incIndent(),this.visitAllStatements(o.trueCase,e),e.decIndent(),t&&(e.println(o,"} else {"),e.incIndent(),this.visitAllStatements(o.falseCase,e),e.decIndent())),e.println(o,"}"),null}visitInvokeFunctionExpr(o,e){let t=o.fn instanceof gu;return t&&e.print(o.fn,"("),o.fn.visitExpression(this,e),t&&e.print(o.fn,")"),e.print(o,"("),this.visitAllExpressions(o.args,e,","),e.print(o,")"),null}visitTaggedTemplateLiteralExpr(o,e){return o.tag.visitExpression(this,e),o.template.visitExpression(this,e),null}visitTemplateLiteralExpr(o,e){e.print(o,"`");for(let t=0;t{t instanceof yp?(e.print(o,"..."),t.expression.visitExpression(this,e)):(e.print(o,`${$m(t.key,this._escapeDollarInStrings,t.quoted)}:`),t.value.visitExpression(this,e))},o.entries,e,","),e.print(o,"}"),null}visitCommaExpr(o,e){return e.print(o,"("),this.visitAllExpressions(o.parts,e,","),e.print(o,")"),null}visitParenthesizedExpr(o,e){o.expr.visitExpression(this,e)}visitSpreadElementExpr(o,e){e.print(o,"..."),o.expression.visitExpression(this,e)}visitAllExpressions(o,e,t){this.visitAllObjects(i=>i.visitExpression(this,e),o,e,t)}visitAllObjects(o,e,t,i){let r=!1;for(let s=0;s0&&(t.lineLength()>80?(t.print(null,i,!0),r||(t.incIndent(),t.incIndent(),r=!0)):t.print(null,i,!1)),o(e[s]);r&&(t.decIndent(),t.decIndent())}visitAllStatements(o,e){o.forEach(t=>t.visitStatement(this,e))}};function $m(n,o,e=!0){if(n==null)return null;let t=n.replace(nU,(...r)=>r[0]=="$"?o?"\\$":"$":r[0]==` +`?"\\n":r[0]=="\r"?"\\r":`\\${r[0]}`);return e||!iU.test(t)?`'${t}'`:t}function T5(n){let o="";for(let e=0;et.value));return o?Xs([],e):e}function RT(n,o){return{expression:n,forwardRef:o}}function lU({expression:n,forwardRef:o}){switch(o){case 0:case 1:return n;case 2:return cU(n)}}function cU(n){return Vt(ge.forwardRef).callFn([Xs([],n)])}var Ax=(function(n){return n[n.Class=0]="Class",n[n.Function=1]="Function",n})(Ax||{});function Vm(n){let o=si("__ngFactoryType__"),e=null,t=O5(n)?o:new Si(lt.Or,o,n.type.value),i=null;n.deps!==null?n.deps!=="invalid"&&(i=new Rf(t,E5(n.deps,n.target))):(e=si(`\u0275${n.name}_BaseFactory`),i=e.callFn([t]));let r=[],s=null;function d(u){let h=si("__ngConditionalFactory__");r.push(new Ur(h.name,Lg,ec));let v=i!==null?h.set(i).toStmt():Vt(ge.invalidFactory).callFn([]).toStmt();return r.push(T1(o,[v],[h.set(u).toStmt()])),h}if(O5(n)){let u=E5(n.delegateDeps,n.target),h=new(n.delegateType===Ax.Class?Rf:ys)(n.delegate,u);s=d(h)}else gU(n)?s=d(n.expression):s=i;if(s===null)r.push(Vt(ge.invalidFactory).callFn([]).toStmt());else if(e!==null){let u=Vt(ge.getInheritedFactory).callFn([n.type.value]),h=new Si(lt.Or,e,e.set(u));r.push(new Or(h.callFn([t])))}else r.push(new Or(s));let m=bp([new Er(o.name,xs)],r,ec,void 0,`${n.name}_Factory`);return e!==null&&(m=Xs([],[new Ur(e.name),new Or(m)]).callFn([],void 0,!0)),{expression:m,statements:[],type:dU(n)}}function dU(n){let o=n.deps!==null&&n.deps!=="invalid"?mU(n.deps):Rc;return ga(Vt(ge.FactoryDeclaration,[E1(n.type.type,n.typeArgumentCount),o]))}function E5(n,o){return n.map((e,t)=>pU(e,o,t))}function pU(n,o,e){if(n.token===null)return Vt(ge.invalidFactoryDep).callFn([Oe(e)]);if(n.attributeNameType===null){let t=0|(n.self?2:0)|(n.skipSelf?4:0)|(n.host?1:0)|(n.optional?8:0)|(o===kd.Pipe?16:0),i=t!==0||n.optional?Oe(t):null,r=[n.token];i&&r.push(i);let s=hU(o);return Vt(s).callFn(r)}else return Vt(ge.injectAttribute).callFn([n.token])}function mU(n){let o=!1,e=n.map(t=>{let i=uU(t);return i!==null?(o=!0,i):Oe(null)});return o?ga(Ji(e)):Rc}function uU(n){let o=[];return n.attributeNameType!==null&&o.push({key:"attribute",value:n.attributeNameType,quoted:!1}),n.optional&&o.push({key:"optional",value:Oe(!0),quoted:!1}),n.host&&o.push({key:"host",value:Oe(!0),quoted:!1}),n.self&&o.push({key:"self",value:Oe(!0),quoted:!1}),n.skipSelf&&o.push({key:"skipSelf",value:Oe(!0),quoted:!1}),o.length>0?yl(o):null}function O5(n){return n.delegateType!==void 0}function gU(n){return n.expression!==void 0}function hU(n){switch(n){case kd.Component:case kd.Directive:case kd.Pipe:return ge.directiveInject;case kd.NgModule:case kd.Injectable:default:return ge.inject}}var ou=class{start;end;constructor(o,e){this.start=o,this.end=e}toAbsolute(o){return new Gs(o+this.start,o+this.end)}},so=class{span;sourceSpan;constructor(o,e){this.span=o,this.sourceSpan=e}toString(){return"AST"}},jf=class extends so{nameSpan;constructor(o,e,t){super(o,e),this.nameSpan=t}},Ea=class extends so{visit(o,e=null){return o.visitEmptyExpr?.(this,e)}},Vc=class extends so{visit(o,e=null){return o.visitImplicitReceiver(this,e)}},$f=class extends so{visit(o,e=null){return o.visitThisReceiver?.(this,e)}},zg=class extends so{expressions;constructor(o,e,t){super(o,e),this.expressions=t}visit(o,e=null){return o.visitChain(this,e)}},Nx=class extends so{condition;trueExp;falseExp;constructor(o,e,t,i,r){super(o,e),this.condition=t,this.trueExp=i,this.falseExp=r}visit(o,e=null){return o.visitConditional(this,e)}},Ac=class extends jf{receiver;name;constructor(o,e,t,i,r){super(o,e,t),this.receiver=i,this.name=r}visit(o,e=null){return o.visitPropertyRead(this,e)}},Hf=class extends jf{receiver;name;constructor(o,e,t,i,r){super(o,e,t),this.receiver=i,this.name=r}visit(o,e=null){return o.visitSafePropertyRead(this,e)}},ru=class extends so{receiver;key;constructor(o,e,t,i){super(o,e),this.receiver=t,this.key=i}visit(o,e=null){return o.visitKeyedRead(this,e)}},Uf=class extends so{receiver;key;constructor(o,e,t,i){super(o,e),this.receiver=t,this.key=i}visit(o,e=null){return o.visitSafeKeyedRead(this,e)}},_x=(function(n){return n[n.ReferencedByName=0]="ReferencedByName",n[n.ReferencedDirectly=1]="ReferencedDirectly",n})(_x||{}),Fx=class extends jf{exp;name;args;type;constructor(o,e,t,i,r,s,d){super(o,e,d),this.exp=t,this.name=i,this.args=r,this.type=s}visit(o,e=null){return o.visitPipe(this,e)}},_s=class extends so{value;constructor(o,e,t){super(o,e),this.value=t}visit(o,e=null){return o.visitLiteralPrimitive(this,e)}},Wf=class extends so{expressions;constructor(o,e,t){super(o,e),this.expressions=t}visit(o,e=null){return o.visitLiteralArray(this,e)}},Rx=class extends so{expression;constructor(o,e,t){super(o,e),this.expression=t}visit(o,e=null){return o.visitSpreadElement(this,e)}},au=class extends so{keys;values;constructor(o,e,t,i){super(o,e),this.keys=t,this.values=i}visit(o,e=null){return o.visitLiteralMap(this,e)}},I0=class extends so{strings;expressions;constructor(o,e,t,i){super(o,e),this.strings=t,this.expressions=i}visit(o,e=null){return o.visitInterpolation(this,e)}},qa=class extends so{operation;left;right;constructor(o,e,t,i,r){super(o,e),this.operation=t,this.left=i,this.right=r}visit(o,e=null){return o.visitBinary(this,e)}static isAssignmentOperation(o){return o==="="||o==="+="||o==="-="||o==="*="||o==="/="||o==="%="||o==="**="||o==="&&="||o==="||="||o==="??="}},Dg=class n extends qa{operator;expr;left=null;right=null;operation=null;static createMinus(o,e,t){return new n(o,e,"-",t,"-",new _s(o,e,0),t)}static createPlus(o,e,t){return new n(o,e,"+",t,"-",t,new _s(o,e,0))}constructor(o,e,t,i,r,s,d){super(o,e,r,s,d),this.operator=t,this.expr=i}visit(o,e=null){return o.visitUnary!==void 0?o.visitUnary(this,e):o.visitBinary(this,e)}},Gf=class extends so{expression;constructor(o,e,t){super(o,e),this.expression=t}visit(o,e=null){return o.visitPrefixNot(this,e)}},qf=class extends so{expression;constructor(o,e,t){super(o,e),this.expression=t}visit(o,e=null){return o.visitTypeofExpression(this,e)}},Qf=class extends so{expression;constructor(o,e,t){super(o,e),this.expression=t}visit(o,e=null){return o.visitVoidExpression(this,e)}},Kf=class extends so{expression;constructor(o,e,t){super(o,e),this.expression=t}visit(o,e=null){return o.visitNonNullAssert(this,e)}},Vg=class extends so{receiver;args;argumentSpan;constructor(o,e,t,i,r){super(o,e),this.receiver=t,this.args=i,this.argumentSpan=r}visit(o,e=null){return o.visitCall(this,e)}},Lx=class extends so{receiver;args;argumentSpan;constructor(o,e,t,i,r){super(o,e),this.receiver=t,this.args=i,this.argumentSpan=r}visit(o,e=null){return o.visitSafeCall(this,e)}},Xf=class extends so{tag;template;constructor(o,e,t,i){super(o,e),this.tag=t,this.template=i}visit(o,e){return o.visitTaggedTemplateLiteral(this,e)}},Yf=class extends so{elements;expressions;constructor(o,e,t,i){super(o,e),this.elements=t,this.expressions=i}visit(o,e){return o.visitTemplateLiteral(this,e)}},zx=class extends so{text;constructor(o,e,t){super(o,e),this.text=t}visit(o,e){return o.visitTemplateLiteralElement(this,e)}},Zf=class extends so{expression;constructor(o,e,t){super(o,e),this.expression=t}visit(o,e){return o.visitParenthesizedExpression(this,e)}},uP=class{name;span;sourceSpan;constructor(o,e,t){this.name=o,this.span=e,this.sourceSpan=t}},Vx=class extends so{parameters;body;constructor(o,e,t,i){super(o,e),this.parameters=t,this.body=i}visit(o,e){return o.visitArrowFunction(this,e)}},Bx=class extends so{body;flags;constructor(o,e,t,i){super(o,e),this.body=t,this.flags=i}visit(o,e){return o.visitRegularExpressionLiteral(this,e)}},Gs=class{start;end;constructor(o,e){this.start=o,this.end=e}},Cs=class extends so{ast;source;location;errors;constructor(o,e,t,i,r){super(new ou(0,e===null?0:e.length),new Gs(i,e===null?i:i+e.length)),this.ast=o,this.source=e,this.location=t,this.errors=r}visit(o,e=null){return o.visitASTWithSource?o.visitASTWithSource(this,e):this.ast.visit(o,e)}toString(){return`${this.source} in ${this.location}`}},Jf=class{sourceSpan;key;value;constructor(o,e,t){this.sourceSpan=o,this.key=e,this.value=t}},gP=class{sourceSpan;key;value;constructor(o,e,t){this.sourceSpan=o,this.key=e,this.value=t}},Bg=class{visit(o,e){o.visit(this,e)}visitUnary(o,e){this.visit(o.expr,e)}visitBinary(o,e){this.visit(o.left,e),this.visit(o.right,e)}visitChain(o,e){this.visitAll(o.expressions,e)}visitConditional(o,e){this.visit(o.condition,e),this.visit(o.trueExp,e),this.visit(o.falseExp,e)}visitPipe(o,e){this.visit(o.exp,e),this.visitAll(o.args,e)}visitImplicitReceiver(o,e){}visitThisReceiver(o,e){}visitInterpolation(o,e){this.visitAll(o.expressions,e)}visitKeyedRead(o,e){this.visit(o.receiver,e),this.visit(o.key,e)}visitLiteralArray(o,e){this.visitAll(o.expressions,e)}visitLiteralMap(o,e){this.visitAll(o.values,e)}visitLiteralPrimitive(o,e){}visitPrefixNot(o,e){this.visit(o.expression,e)}visitTypeofExpression(o,e){this.visit(o.expression,e)}visitVoidExpression(o,e){this.visit(o.expression,e)}visitNonNullAssert(o,e){this.visit(o.expression,e)}visitPropertyRead(o,e){this.visit(o.receiver,e)}visitSafePropertyRead(o,e){this.visit(o.receiver,e)}visitSafeKeyedRead(o,e){this.visit(o.receiver,e),this.visit(o.key,e)}visitCall(o,e){this.visit(o.receiver,e),this.visitAll(o.args,e)}visitSafeCall(o,e){this.visit(o.receiver,e),this.visitAll(o.args,e)}visitTemplateLiteral(o,e){for(let t=0;tt!==null);Rk(o,e)}visitTriggers(o,e,t){Rk(t,o.map(i=>e[i]))}},Gx=class extends bs{expression;groups;unknownBlocks;exhaustiveCheck;constructor(o,e,t,i,r,s,d,m){super(m,r,s,d),this.expression=o,this.groups=e,this.unknownBlocks=t,this.exhaustiveCheck=i}visit(o){return o.visitSwitchBlock(this)}},MP=class extends bs{expression;constructor(o,e,t,i,r){super(r,e,t,i),this.expression=o}visit(o){return o.visitSwitchBlockCase(this)}},o0=class extends bs{cases;children;i18n;constructor(o,e,t,i,r,s,d){super(s,t,i,r),this.cases=o,this.children=e,this.i18n=d}visit(o){return o.visitSwitchBlockCaseGroup(this)}},SP=class extends bs{constructor(o,e,t,i){super(i,o,e,t)}visit(o){return o.visitSwitchExhaustiveCheck(this)}},Hg=class extends bs{item;expression;trackBy;trackKeywordSpan;contextVariables;children;empty;mainBlockSpan;i18n;constructor(o,e,t,i,r,s,d,m,u,h,v,b,y){super(b,m,h,v),this.item=o,this.expression=e,this.trackBy=t,this.trackKeywordSpan=i,this.contextVariables=r,this.children=s,this.empty=d,this.mainBlockSpan=u,this.i18n=y}visit(o){return o.visitForLoopBlock(this)}},r0=class extends bs{children;i18n;constructor(o,e,t,i,r,s){super(r,e,t,i),this.children=o,this.i18n=s}visit(o){return o.visitForLoopBlockEmpty(this)}},qx=class extends bs{branches;constructor(o,e,t,i,r){super(r,e,t,i),this.branches=o}visit(o){return o.visitIfBlock(this)}},qm=class extends bs{expression;children;expressionAlias;i18n;constructor(o,e,t,i,r,s,d,m){super(d,i,r,s),this.expression=o,this.children=e,this.expressionAlias=t,this.i18n=m}visit(o){return o.visitIfBlockBranch(this)}},Qx=class{name;sourceSpan;nameSpan;constructor(o,e,t){this.name=o,this.sourceSpan=e,this.nameSpan=t}visit(o){return o.visitUnknownBlock(this)}},LT=class{name;value;sourceSpan;nameSpan;valueSpan;constructor(o,e,t,i,r){this.name=o,this.value=e,this.sourceSpan=t,this.nameSpan=i,this.valueSpan=r}visit(o){return o.visitLetDeclaration(this)}},kf=class{componentName;tagName;fullName;attributes;inputs;outputs;directives;children;references;isSelfClosing;sourceSpan;startSourceSpan;endSourceSpan;i18n;constructor(o,e,t,i,r,s,d,m,u,h,v,b,y,x){this.componentName=o,this.tagName=e,this.fullName=t,this.attributes=i,this.inputs=r,this.outputs=s,this.directives=d,this.children=m,this.references=u,this.isSelfClosing=h,this.sourceSpan=v,this.startSourceSpan=b,this.endSourceSpan=y,this.i18n=x}visit(o){return o.visitComponent(this)}},vN=class{name;attributes;inputs;outputs;references;sourceSpan;startSourceSpan;endSourceSpan;i18n;constructor(o,e,t,i,r,s,d,m,u){this.name=o,this.attributes=e,this.inputs=t,this.outputs=i,this.references=r,this.sourceSpan=s,this.startSourceSpan=d,this.endSourceSpan=m,this.i18n=u}visit(o){return o.visitDirective(this)}},qs=class{tagName;attributes;inputs;outputs;directives;templateAttrs;children;references;variables;isSelfClosing;sourceSpan;startSourceSpan;endSourceSpan;i18n;constructor(o,e,t,i,r,s,d,m,u,h,v,b,y,x){this.tagName=o,this.attributes=e,this.inputs=t,this.outputs=i,this.directives=r,this.templateAttrs=s,this.children=d,this.references=m,this.variables=u,this.isSelfClosing=h,this.sourceSpan=v,this.startSourceSpan=b,this.endSourceSpan=y,this.i18n=x}visit(o){return o.visitTemplate(this)}},Ug=class{selector;attributes;children;isSelfClosing;sourceSpan;startSourceSpan;endSourceSpan;i18n;name="ng-content";constructor(o,e,t,i,r,s,d,m){this.selector=o,this.attributes=e,this.children=t,this.isSelfClosing=i,this.sourceSpan=r,this.startSourceSpan=s,this.endSourceSpan=d,this.i18n=m}visit(o){return o.visitContent(this)}},wp=class{name;value;sourceSpan;keySpan;valueSpan;constructor(o,e,t,i,r){this.name=o,this.value=e,this.sourceSpan=t,this.keySpan=i,this.valueSpan=r}visit(o){return o.visitVariable(this)}},a0=class{name;value;sourceSpan;keySpan;valueSpan;constructor(o,e,t,i,r){this.name=o,this.value=e,this.sourceSpan=t,this.keySpan=i,this.valueSpan=r}visit(o){return o.visitReference(this)}},xN=class{vars;placeholders;sourceSpan;i18n;constructor(o,e,t,i){this.vars=o,this.placeholders=e,this.sourceSpan=t,this.i18n=i}visit(o){return o.visitIcu(this)}},s0=class{tagNames;bindings;listeners;sourceSpan;constructor(o,e,t,i){if(this.tagNames=o,this.bindings=e,this.listeners=t,this.sourceSpan=i,o.length===0)throw new Error("HostElement must have at least one tag name.")}visit(){throw new Error("HostElement cannot be visited")}};function Rk(n,o){let e=[];if(n.visit)for(let t of o)n.visit(t);else for(let t of o){let i=t.visit(n);i&&e.push(i)}return e}var Ja=class{nodes;placeholders;placeholderToMessage;meaning;description;customId;sources;id;legacyIds=[];messageString;constructor(o,e,t,i,r,s){this.nodes=o,this.placeholders=e,this.placeholderToMessage=t,this.meaning=i,this.description=r,this.customId=s,this.id=this.customId,this.messageString=fU(this.nodes),o.length?this.sources=[{filePath:o[0].sourceSpan.start.file.url,startLine:o[0].sourceSpan.start.line+1,startCol:o[0].sourceSpan.start.col+1,endLine:o[o.length-1].sourceSpan.end.line+1,endCol:o[0].sourceSpan.start.col+1}]:this.sources=[]}},df=class{value;sourceSpan;constructor(o,e){this.value=o,this.sourceSpan=e}visit(o,e){return o.visitText(this,e)}},Ed=class{children;sourceSpan;constructor(o,e){this.children=o,this.sourceSpan=e}visit(o,e){return o.visitContainer(this,e)}},Kx=class{expression;type;cases;sourceSpan;expressionPlaceholder;constructor(o,e,t,i,r){this.expression=o,this.type=e,this.cases=t,this.sourceSpan=i,this.expressionPlaceholder=r}visit(o,e){return o.visitIcu(this,e)}},Mp=class{tag;attrs;startName;closeName;children;isVoid;sourceSpan;startSourceSpan;endSourceSpan;constructor(o,e,t,i,r,s,d,m,u){this.tag=o,this.attrs=e,this.startName=t,this.closeName=i,this.children=r,this.isVoid=s,this.sourceSpan=d,this.startSourceSpan=m,this.endSourceSpan=u}visit(o,e){return o.visitTagPlaceholder(this,e)}},l0=class{value;name;sourceSpan;constructor(o,e,t){this.value=o,this.name=e,this.sourceSpan=t}visit(o,e){return o.visitPlaceholder(this,e)}},Wg=class{value;name;sourceSpan;previousMessage;constructor(o,e,t){this.value=o,this.name=e,this.sourceSpan=t}visit(o,e){return o.visitIcuPlaceholder(this,e)}},Sp=class{name;parameters;startName;closeName;children;sourceSpan;startSourceSpan;endSourceSpan;constructor(o,e,t,i,r,s,d,m){this.name=o,this.parameters=e,this.startName=t,this.closeName=i,this.children=r,this.sourceSpan=s,this.startSourceSpan=d,this.endSourceSpan=m}visit(o,e){return o.visitBlockPlaceholder(this,e)}};function fU(n){let o=new kP;return n.map(t=>t.visit(o)).join("")}var kP=class{visitText(o){return o.value}visitContainer(o){return o.children.map(e=>e.visit(this)).join("")}visitIcu(o){let e=Object.keys(o.cases).map(t=>`${t} {${o.cases[t].visit(this)}}`);return`{${o.expressionPlaceholder}, ${o.type}, ${e.join(" ")}}`}visitTagPlaceholder(o){let e=o.children.map(t=>t.visit(this)).join("");return`{$${o.startName}}${e}{$${o.closeName}}`}visitPlaceholder(o){return`{$${o.name}}`}visitIcuPlaceholder(o){return`{$${o.name}}`}visitBlockPlaceholder(o){let e=o.children.map(t=>t.visit(this)).join("");return`{$${o.startName}}${e}{$${o.closeName}}`}};var CU=class{visitTag(o){let e=this._serializeAttributes(o.attrs);if(o.children.length==0)return`<${o.name}${e}/>`;let t=o.children.map(i=>i.visit(this));return`<${o.name}${e}>${t.join("")}`}visitText(o){return o.value}visitDeclaration(o){return``}_serializeAttributes(o){let e=Object.keys(o).map(t=>`${t}="${o[t]}"`).join(" ");return e.length>0?" "+e:""}visitDoctype(o){return``}},Ije=new CU;function vU(n){return n.toUpperCase().replace(/[^A-Z0-9_]/g,"_")}var yN="i18n",PP="i18n-",xU="VAR_";function bN(n){return n===yN||n.startsWith(PP)}function yU(n){return n.attrs.some(o=>bN(o.name))}function wN(n){return n.nodes[0]}function zT(n={},o){let e={};return n&&Object.keys(n).length&&Object.keys(n).forEach(t=>e[A0(t,o)]=n[t]),e}function A0(n,o=!0){let e=vU(n);if(!o)return e;let t=e.split("_");if(t.length===1)return n.toLowerCase();let i;/^\d+$/.test(t[t.length-1])&&(i=t.pop());let r=t.shift().toLowerCase();return t.length&&(r+=t.map(s=>s.charAt(0).toUpperCase()+s.slice(1).toLowerCase()).join("")),i?`${r}_${i}`:r}var bU=/[-.]/,VT="_t",Ys="ctx",Jg="rf";function MN(n,o){let e=null;return()=>(e||(n(new Ur(VT,void 0,xs)),e=si(o)),e)}function kg(n){return Array.isArray(n)?Ji(n.map(kg)):Oe(n,ec)}function I5(n,o){let e=Object.getOwnPropertyNames(n);return e.length===0?null:yl(e.map(t=>{let i=n[t],r,s,d,m;if(typeof i=="string")r=t,d=t,s=i,m=kg(s);else{d=t,r=i.classPropertyName,s=i.bindingPropertyName;let u=s!==r,h=i.transformFunction!==null,v=vf.None;if(i.isSignal&&(v|=vf.SignalBased),h&&(v|=vf.HasDecoratorInputTransform),o&&(u||h||v!==vf.None)){let b=[Oe(v),kg(s)];(u||h)&&(b.push(kg(r)),h&&b.push(i.transformFunction)),m=Ji(b)}else m=kg(s)}return{key:d,quoted:bU.test(d),value:m}}))}var kp=class{values=[];set(o,e){if(e){let t=this.values.find(i=>i.key===o);t?t.value=e:this.values.push({key:o,value:e,quoted:!1})}}toLiteralMap(){return yl(this.values)}};function wU(n){let o=n instanceof Bc?n.name:"ng-template",e=MU(n),t=new Ag,i=ha(o)[1];return t.setElement(i),Object.getOwnPropertyNames(e).forEach(r=>{let s=ha(r)[1],d=e[r];t.addAttribute(s,d),r.toLowerCase()==="class"&&d.trim().split(/\s+/).forEach(u=>t.addClassName(u))}),t}function MU(n){let o={};return n instanceof qs&&n.tagName!=="ng-template"?n.templateAttrs.forEach(e=>o[e.name]=""):(n.attributes.forEach(e=>{bN(e.name)||(o[e.name]=e.value)}),n.inputs.forEach(e=>{(e.type===Fi.Property||e.type===Fi.TwoWay)&&(o[e.name]="")}),n.outputs.forEach(e=>{o[e.name]=""})),o}function A5(n,o){let e=null,t={name:n.name,type:n.type,typeArgumentCount:n.typeArgumentCount,deps:[],target:kd.Injectable};if(n.useClass!==void 0){let d=n.useClass.expression.isEquivalent(n.type.value),m;n.deps!==void 0&&(m=n.deps),m!==void 0?e=Vm(Le(q({},t),{delegate:n.useClass.expression,delegateDeps:m,delegateType:Ax.Class})):d?e=Vm(t):e={statements:[],expression:N5(n.type.value,n.useClass.expression,o)}}else n.useFactory!==void 0?n.deps!==void 0?e=Vm(Le(q({},t),{delegate:n.useFactory,delegateDeps:n.deps||[],delegateType:Ax.Function})):e={statements:[],expression:Xs([],n.useFactory.callFn([]))}:n.useValue!==void 0?e=Vm(Le(q({},t),{expression:n.useValue.expression})):n.useExisting!==void 0?e=Vm(Le(q({},t),{expression:Vt(ge.inject).callFn([n.useExisting.expression])})):e={statements:[],expression:N5(n.type.value,n.type.value,o)};let i=n.type.value,r=new kp;return r.set("token",i),r.set("factory",e.expression),n.providedIn.expression.value!==null&&r.set("providedIn",lU(n.providedIn)),{expression:Vt(ge.\u0275\u0275defineInjectable).callFn([r.toLiteralMap()],void 0,!0),type:SU(n),statements:e.statements}}function SU(n){return new xl(Vt(ge.InjectableDeclaration,[E1(n.type.type,n.typeArgumentCount)]))}function N5(n,o,e){if(n.node===o.node)return o.prop("\u0275fac");if(!e)return F5(o);let t=Vt(ge.resolveForwardRef).callFn([o]);return F5(t)}function F5(n){let o=new Er("__ngFactoryType__",xs);return Xs([o],n.prop("\u0275fac").callFn([si(o.name)]))}var ia=0,kU=8,BT=9,Qm=10,SN=11,kN=12,jT=13,PN=32,TP=33,c0=34,TN=35,D1=36,PU=37,Xx=38,d0=39,Ya=40,Pr=41,R5=42,EN=43,Oa=44,Yx=45,Lm=46,ul=47,Dc=58,fs=59,Ig=60,ta=61,Us=62,L5=63,$T=48,TU=55,ON=57,Ip=65,EU=69,OU=70,DU=88,eh=90,Nc=91,Km=92,Pd=93,IU=94,Ap=95,lu=97,AU=98,NU=101,HT=102,DN=110,IN=114,AN=116,NN=117,FN=118,RN=120,N0=122,_l=123,z5=124,Ka=125,LN=160,fg=64,EP=96;function p0(n){return n>=BT&&n<=PN||n==LN}function gl(n){return $T<=n&&n<=ON}function Pp(n){return n>=lu&&n<=N0||n>=Ip&&n<=eh}function FU(n){return n>=lu&&n<=HT||n>=Ip&&n<=OU||gl(n)}function Zx(n){return n===Qm||n===jT}function V5(n){return $T<=n&&n<=TU}function Pf(n){return n===d0||n===c0||n===EP}var m0=class n{file;offset;line;col;constructor(o,e,t,i){this.file=o,this.offset=e,this.line=t,this.col=i}toString(){return this.offset!=null?`${this.file.url}@${this.line}:${this.col}`:this.file.url}moveBy(o){let e=this.file.content,t=e.length,i=this.offset,r=this.line,s=this.col;for(;i>0&&o<0;)if(i--,o++,e.charCodeAt(i)==Qm){r--;let m=e.substring(0,i-1).lastIndexOf(String.fromCharCode(Qm));s=m>0?i-m:i}else s--;for(;i0;){let d=e.charCodeAt(i);i++,o--,d==Qm?(r++,s=0):s++}return new n(this.file,i,r,s)}getContext(o,e){let t=this.file.content,i=this.offset;if(i!=null){i>t.length-1&&(i=t.length-1);let r=i,s=0,d=0;for(;s0&&(i--,s++,!(t[i]==` +`&&++d==e)););for(s=0,d=0;s]${o.after}")`:this.msg}toString(){let o=this.span.details?`, ${this.span.details}`:"";return`${this.contextualMessage()}: ${this.span.start}${o}`}};function RU(n,o,e){let t=`in ${n} ${o} in ${e}`,i=new Jx("",t);return new On(new m0(i,-1,-1,-1),new m0(i,-1,-1,-1))}var LU=0;function zU(n){if(!n||!n.reference)return null;let o=n.reference;if(o.__anonymousType)return o.__anonymousType;if(o.__forward_ref__)return"__forward_ref__";let e=fN(o);return e.indexOf("(")>=0?(e=`anonymous_${LU++}`,o.__anonymousType=e):e=Bm(e),e}function Bm(n){return n.replace(/\W/g,"_")}var B5='(this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})',OP=class extends mP{constructor(){super(!1)}visitWrappedNodeExpr(o,e){throw new Error("Cannot emit a WrappedNodeExpr in Javascript.")}visitDeclareVarStmt(o,e){return e.print(o,`var ${o.name}`),o.value&&(e.print(o," = "),o.value.visitExpression(this,e)),e.println(o,";"),null}visitTaggedTemplateLiteralExpr(o,e){let t=o.template.elements;return o.tag.visitExpression(this,e),e.print(o,`(${B5}(`),e.print(o,`[${t.map(i=>$m(i.text,!1)).join(", ")}], `),e.print(o,`[${t.map(i=>$m(i.rawText,!1)).join(", ")}])`),o.template.expressions.forEach(i=>{e.print(o,", "),i.visitExpression(this,e)}),e.print(o,")"),null}visitTemplateLiteralExpr(o,e){e.print(o,"`");for(let t=0;t"),Array.isArray(o.body))e.println(o,"{"),e.incIndent(),this.visitAllStatements(o.body,e),e.decIndent(),e.print(o,"}");else{let t=o.body instanceof ic;t&&e.print(o,"("),o.body.visitExpression(this,e),t&&e.print(o,")")}return null}visitDeclareFunctionStmt(o,e){return e.print(o,`function ${o.name}(`),this._visitParams(o.params,e),e.println(o,") {"),e.incIndent(),this.visitAllStatements(o.statements,e),e.decIndent(),e.println(o,"}"),null}visitLocalizedString(o,e){e.print(o,`$localize(${B5}(`);let t=[o.serializeI18nHead()];for(let i=1;i$m(i.cooked,!1)).join(", ")}], `),e.print(o,`[${t.map(i=>$m(i.raw,!1)).join(", ")}])`),o.expressions.forEach(i=>{e.print(o,", "),i.visitExpression(this,e)}),e.print(o,")"),null}_visitParams(o,e){this.visitAllObjects(t=>e.print(null,t.name),o,e,",")}},ix;function VU(){if(ix===void 0){let n=Sf.trustedTypes;if(ix=null,n)try{ix=n.createPolicy("angular#unsafe-jit",{createScript:o=>o})}catch{}}return ix}function BU(n){return VU()?.createScript(n)||n}function j5(...n){if(!Sf.trustedTypes)return new Function(...n);let o=n.slice(0,-1).join(","),e=n[n.length-1],t=`(function anonymous(${o} +) { ${e} +})`,i=Sf.eval(BU(t));return i.bind===void 0?new Function(...n):(i.toString=()=>t,i.bind(Sf))}var DP=class{evaluateStatements(o,e,t,i){let r=new IP(t),s=pP.createRoot();return e.length>0&&!jU(e[0])&&(e=[Oe("use strict").toStmt(),...e]),r.visitAllStatements(e,s),r.createReturnStmt(s),this.evaluateCode(o,s,r.getArgs(),i)}evaluateCode(o,e,t,i){let r=`"use strict";${e.toSource()} +//# sourceURL=${o}`,s=[],d=[];for(let u in t)d.push(t[u]),s.push(u);if(i){let u=j5(...s.concat("return null;")).toString(),h=u.slice(0,u.indexOf("return null;")).split(` +`).length-1;r+=` +${e.toSourceMapGenerator(o,h).toJsComment()}`}let m=j5(...s.concat(r));return this.executeFunction(m,d)}executeFunction(o,e){return o(...e)}},IP=class extends OP{refResolver;_evalArgNames=[];_evalArgValues=[];_evalExportedVars=[];constructor(o){super(),this.refResolver=o}createReturnStmt(o){new Or(new ic(this._evalExportedVars.map(t=>new Rg(t,si(t),!1)))).visitStatement(this,o)}getArgs(){let o={};for(let e=0;e0&&o.set("imports",Ji(n.imports));let e=Vt(ge.defineInjector).callFn([o.toLiteralMap()],void 0,!0),t=$U(n);return{expression:e,type:t,statements:[]}}function $U(n){return new xl(Vt(ge.InjectorDeclaration,[new xl(n.type.type)]))}var AP=class{context;constructor(o){this.context=o}resolveExternalReference(o){if(o.moduleName!=="@angular/core")throw new Error(`Cannot resolve external reference to ${o.moduleName}, only references to @angular/core are supported.`);if(!this.context.hasOwnProperty(o.name))throw new Error(`No value provided for @angular/core symbol '${o.name}'.`);return this.context[o.name]}},e1=(function(n){return n[n.Inline=0]="Inline",n[n.SideEffect=1]="SideEffect",n[n.Omit=2]="Omit",n})(e1||{}),vp=(function(n){return n[n.Global=0]="Global",n[n.Local=1]="Local",n})(vp||{});function HU(n){let o=[],e=new kp;if(e.set("type",n.type.value),n.kind===vp.Global&&n.bootstrap.length>0&&e.set("bootstrap",Hm(n.bootstrap,n.containsForwardDecls)),n.selectorScopeMode===e1.Inline)n.declarations.length>0&&e.set("declarations",Hm(n.declarations,n.containsForwardDecls)),n.imports.length>0&&e.set("imports",Hm(n.imports,n.containsForwardDecls)),n.exports.length>0&&e.set("exports",Hm(n.exports,n.containsForwardDecls));else if(n.selectorScopeMode===e1.SideEffect){let r=GU(n);r!==null&&o.push(r)}n.schemas!==null&&n.schemas.length>0&&e.set("schemas",Ji(n.schemas.map(r=>r.value))),n.id!==null&&(e.set("id",n.id),o.push(Vt(ge.registerNgModuleType).callFn([n.type.value,n.id]).toStmt()));let t=Vt(ge.defineNgModule).callFn([e.toLiteralMap()],void 0,!0),i=WU(n);return{expression:t,type:i,statements:o}}function UU(n){let o=new kp;return o.set("type",new hi(n.type)),n.bootstrap!==void 0&&o.set("bootstrap",new hi(n.bootstrap)),n.declarations!==void 0&&o.set("declarations",new hi(n.declarations)),n.imports!==void 0&&o.set("imports",new hi(n.imports)),n.exports!==void 0&&o.set("exports",new hi(n.exports)),n.schemas!==void 0&&o.set("schemas",new hi(n.schemas)),n.id!==void 0&&o.set("id",new hi(n.id)),Vt(ge.defineNgModule).callFn([o.toLiteralMap()])}function WU(n){if(n.kind===vp.Local)return new xl(n.type.value);let{type:o,declarations:e,exports:t,imports:i,includeImportTypes:r,publicDeclarationTypes:s}=n;return new xl(Vt(ge.NgModuleDeclaration,[new xl(o.type),s===null?Lk(e):qU(s),r?Lk(i):Rc,Lk(t)]))}function GU(n){let o=new kp;if(n.kind===vp.Global?n.declarations.length>0&&o.set("declarations",Hm(n.declarations,n.containsForwardDecls)):n.declarationsExpression&&o.set("declarations",n.declarationsExpression),n.kind===vp.Global?n.imports.length>0&&o.set("imports",Hm(n.imports,n.containsForwardDecls)):n.importsExpression&&o.set("imports",n.importsExpression),n.kind===vp.Global?n.exports.length>0&&o.set("exports",Hm(n.exports,n.containsForwardDecls)):n.exportsExpression&&o.set("exports",n.exportsExpression),n.kind===vp.Local&&n.bootstrapExpression&&o.set("bootstrap",n.bootstrapExpression),Object.keys(o.values).length===0)return null;let e=new ys(Vt(ge.setNgModuleScope),[n.type.value,o.toLiteralMap()]),t=aU(e),i=new xp([],[t.toStmt()]);return new ys(i,[]).toStmt()}function Lk(n){let o=n.map(e=>D0(e.type));return n.length>0?ga(Ji(o)):Rc}function qU(n){let o=n.map(e=>D0(e));return n.length>0?ga(Ji(o)):Rc}function H5(n){let o=[];o.push({key:"name",value:Oe(n.pipeName??n.name),quoted:!1}),o.push({key:"type",value:n.type.value,quoted:!1}),o.push({key:"pure",value:Oe(n.pure),quoted:!1}),n.isStandalone===!1&&o.push({key:"standalone",value:Oe(!1),quoted:!1});let e=Vt(ge.definePipe).callFn([yl(o)],void 0,!0),t=QU(n);return{expression:e,type:t,statements:[]}}function QU(n){return new xl(Vt(ge.PipeDeclaration,[E1(n.type.type,n.typeArgumentCount),new xl(new _a(n.pipeName)),new xl(new _a(n.isStandalone))]))}var Gg=(function(n){return n[n.Directive=0]="Directive",n[n.Pipe=1]="Pipe",n[n.NgModule=2]="NgModule",n})(Gg||{}),KU=new Set(["inherit","initial","revert","unset","alternate","alternate-reverse","normal","reverse","backwards","both","forwards","none","paused","running","ease","ease-in","ease-in-out","ease-out","linear","step-start","step-end","end","jump-both","jump-end","jump-none","jump-start","start"]),XU=["@media","@supports","@document","@layer","@container","@scope","@starting-style"],NP=class{shimCssText(o,e,t=""){let i=[];o=o.replace(mW,d=>{if(d.match(uW))i.push(d);else{let m=d.match(pW);i.push(m?.join("")??"")}return WT}),o=this._insertDirectives(o);let r=this._scopeCssText(o,e,t),s=0;return r.replace(gW,()=>i[s++])}_insertDirectives(o){return o=this._insertPolyfillDirectivesInCssText(o),this._insertPolyfillRulesInCssText(o)}_scopeKeyframesRelatedCss(o,e){let t=new Set,i=ox(o,r=>this._scopeLocalKeyframeDeclarations(r,e,t));return ox(i,r=>this._scopeAnimationRule(r,e,t))}_scopeLocalKeyframeDeclarations(o,e,t){return Le(q({},o),{selector:o.selector.replace(/(^@(?:-webkit-)?keyframes(?:\s+))(['"]?)(.+)\2(\s*)$/,(i,r,s,d,m)=>(t.add(G5(d,s)),`${r}${s}${e}_${d}${s}${m}`))})}_scopeAnimationKeyframe(o,e,t){return o.replace(/^(\s*)(['"]?)(.+?)\2(\s*)$/,(i,r,s,d,m)=>(d=`${t.has(G5(d,s))?e+"_":""}${d}`,`${r}${s}${d}${s}${m}`))}_animationDeclarationKeyframesRe=/(^|\s+|,)(?:(?:(['"])((?:\\\\|\\\2|(?!\2).)+)\2)|(-?[A-Za-z][\w\-]*))(?=[,\s]|$)/g;_scopeAnimationRule(o,e,t){let i=o.content.replace(/((?:^|\s+|;)(?:-webkit-)?animation\s*:\s*),*([^;]+)/g,(r,s,d)=>s+d.replace(this._animationDeclarationKeyframesRe,(m,u,h="",v,b)=>v?`${u}${this._scopeAnimationKeyframe(`${h}${v}${h}`,e,t)}`:KU.has(b)?m:`${u}${this._scopeAnimationKeyframe(b,e,t)}`));return i=i.replace(/((?:^|\s+|;)(?:-webkit-)?animation-name(?:\s*):(?:\s*))([^;]+)/g,(r,s,d)=>`${s}${d.split(",").map(m=>this._scopeAnimationKeyframe(m,e,t)).join(",")}`),Le(q({},o),{content:i})}_insertPolyfillDirectivesInCssText(o){return o.replace(ZU,function(...e){return e[2]+"{"})}_insertPolyfillRulesInCssText(o){return o.replace(JU,(...e)=>{let t=e[0].replace(e[1],"").replace(e[2],"");return e[4]+t})}_scopeCssText(o,e,t){let i=this._extractUnscopedRulesFromCssText(o);return o=this._insertPolyfillHostInCssText(o),o=this._convertColonHost(o),o=this._convertColonHostContext(o),o=this._convertShadowDOMSelectors(o),e&&(o=this._scopeKeyframesRelatedCss(o,e),o=this._scopeSelectors(o,e,t)),o=o+` +`+i,o.trim()}_extractUnscopedRulesFromCssText(o){let e="",t;for(U5.lastIndex=0;(t=U5.exec(o))!==null;){let i=t[0].replace(t[2],"").replace(t[1],t[4]);e+=i+` + +`}return e}_convertColonHost(o){return o.replace(iW,(e,t,i)=>{if(t){let r=[];for(let s of this._splitOnTopLevelCommas(t,!0)){let d=s.trim();if(!d)break;let m=hp+d.replace(t1,"")+i;r.push(m)}return r.join(",")}else return hp+i})}*_splitOnTopLevelCommas(o,e){let t=o.length,i=0,r=0;for(let s=0;s{let i=[[]],r=e.indexOf(wg);for(;r!==-1;){let s=e.substring(r+wg.length);if(!s||s[0]!=="("){e=s,r=e.indexOf(wg);continue}let d=[],m=0;for(let h of this._splitOnTopLevelCommas(s.substring(1),!0)){m=m+h.length+1;let v=h.trim();v&&d.push(v)}let u=i.length;SW(i,d.length);for(let h=0;hMW(s,e,t)).join(", ")})}_convertShadowDOMSelectors(o){return sW.reduce((e,t)=>e.replace(t," "),o)}_scopeSelectors(o,e,t){return ox(o,i=>{let r=i.selector,s=i.content;return i.selector[0]!=="@"?r=this._scopeSelector({selector:r,scopeSelector:e,hostSelector:t,isParentSelector:!0}):XU.some(d=>i.selector.startsWith(d))?s=this._scopeSelectors(i.content,e,t):(i.selector.startsWith("@font-face")||i.selector.startsWith("@page"))&&(s=this._stripScopingSelectors(i.content)),new u0(r,s)})}_stripScopingSelectors(o){return ox(o,e=>{let t=e.selector.replace(W5," ").replace(zk," ");return new u0(t,e.content)})}_safeSelector;_shouldScopeIndicator;_scopeSelector({selector:o,scopeSelector:e,hostSelector:t,isParentSelector:i=!1}){let r=/ ?,(?!(?:[^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\))) ?/;return o.split(r).map(s=>s.split(W5)).map(s=>{let[d,...m]=s;return[(h=>this._selectorNeedsScoping(h,e)?this._applySelectorScope({selector:h,scopeSelector:e,hostSelector:t,isParentSelector:i}):h)(d),...m].join(" ")}).join(", ")}_selectorNeedsScoping(o,e){return!this._makeScopeMatcher(e).test(o)}_makeScopeMatcher(o){let e=/\[/g,t=/\]/g;return o=o.replace(e,"\\[").replace(t,"\\]"),new RegExp("^("+o+")"+lW,"m")}_applySimpleSelectorScope(o,e,t){if(Pg.lastIndex=0,Pg.test(o)){let i=`[${t}]`,r=o;for(;r.match(zk);)r=r.replace(zk,(s,d)=>d.replace(/([^:\)]*)(:*)(.*)/,(m,u,h,v)=>u+i+h+v));return r.replace(Pg,i)}return e+" "+o}_applySelectorScope({selector:o,scopeSelector:e,hostSelector:t,isParentSelector:i}){let r=/\[is=([^\]]*)\]/g;e=e.replace(r,(P,...k)=>k[0]);let s=`[${e}]`,d=P=>{let k=P.trim();if(!k)return P;if(P.includes(hp)){if(k=this._applySimpleSelectorScope(P,e,t),!P.match(aW)){let[S,T,A,D]=k.match(/([^:]*)(:*)([\s\S]*)/);k=T+s+A+D}}else{let S=P.replace(Pg,"");if(S.length>0){let T=S.match(/([^:]*)(:*)([\s\S]*)/);T&&(k=T[1]+s+T[2]+T[3])}}return k},m=P=>{let k="",S=[],T;for(;(T=pf.exec(P))!==null;){let A=1,D=pf.lastIndex;for(;D{let[D]=A.match(pf)??[],F=A.slice(D?.length,-1);F.includes(hp)&&(this._shouldScopeIndicator=!0);let I=this._scopeSelector({selector:F,scopeSelector:e,hostSelector:t});return`${D}${I})`}).join(""):(this._shouldScopeIndicator=this._shouldScopeIndicator||P.includes(hp),k=this._shouldScopeIndicator?d(P):P),k};i&&(this._safeSelector=new FP(o),o=this._safeSelector.content());let u="",h=0,v,b=/( |>|\+|~(?!=))(?!([^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\)))\s*/g,y=o.includes(hp);for((i||this._shouldScopeIndicator)&&(this._shouldScopeIndicator=!y);(v=b.exec(o))!==null;){let P=v[1],k=o.slice(h,v.index);if(k.match(/__esc-ph-(\d+)__/)&&o[v.index+1]?.match(/[a-fA-F\d]/))continue;let S=m(k);u+=`${S} ${P} `,h=b.lastIndex}let x=o.substring(h);return u+=m(x),this._safeSelector.restore(u)}_insertPolyfillHostInCssText(o){return o.replace(dW,wg).replace(cW,t1)}},FP=class{placeholders=[];index=0;_content;constructor(o){o=this._escapeRegexMatches(o,/(\[[^\]]*\])/g),o=o.replace(/(\\.)/g,(e,t)=>{let i=`__esc-ph-${this.index}__`;return this.placeholders.push(t),this.index++,i}),this._content=o.replace(nW,(e,t,i)=>{let r=`__ph-${this.index}__`;return this.placeholders.push(`(${i})`),this.index++,t+r})}restore(o){return o.replace(/__(?:ph|esc-ph)-(\d+)__/g,(e,t)=>this.placeholders[+t])}content(){return this._content}_escapeRegexMatches(o,e){return o.replace(e,(t,i)=>{let r=`__ph-${this.index}__`;return this.placeholders.push(i),this.index++,r})}},YU="(:(where|is)\\()?",pf=/:(where|is)\(/gi,ZU=/polyfill-next-selector[^}]*content:[\s]*?(['"])(.*?)\1[;\s]*}([^{]*?){/gim,JU=/(polyfill-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim,U5=/(polyfill-unscoped-rule)[^}]*(content:[\s]*(['"])(.*?)\3)[;\s]*[^}]*}/gim,t1="-shadowcsshost",wg="-shadowcsscontext",RP="[^)(]*",eW=String.raw`(?:\(${RP}\)|${RP})+?`,tW=String.raw`(?:\(${eW}\)|${RP})+?`,UT=String.raw`(?:\((${tW})\))`,nW=new RegExp(String.raw`(:nth-[-\w]+)`+UT,"g"),iW=new RegExp(t1+UT+"?([^,{]*)","gim"),oW=wg+UT+"?([^{]*)",rW=new RegExp(`${YU}(${oW})`,"gim"),hp=t1+"-no-combinator",aW=new RegExp(`${hp}(?![^(]*\\))`,"g"),zk=/-shadowcsshost-no-combinator([^\s,]*)/,sW=[/::shadow/g,/::content/g,/\/shadow-deep\//g,/\/shadow\//g],W5=/(?:>>>)|(?:\/deep\/)|(?:::ng-deep)/g,lW="([>\\s~+[.,{:][\\s\\S]*)?$",Pg=/-shadowcsshost/gim,cW=/:host/gim,dW=/:host-context/gim,pW=/\r?\n/g,mW=/\/\*[\s\S]*?\*\//g,uW=/\/\*\s*#\s*source(Mapping)?URL=/g,WT="%COMMENT%",gW=new RegExp(WT,"g"),Vk="%BLOCK%",hW=new RegExp(`(\\s*(?:${WT}\\s*)*)([^;\\{\\}]+?)(\\s*)((?:{%BLOCK%}?\\s*;?)|(?:\\s*;))`,"g"),_W=new Map([["{","}"]]),zN="%COMMA_IN_PLACEHOLDER%",VN="%SEMI_IN_PLACEHOLDER%",BN="%COLON_IN_PLACEHOLDER%",fW=new RegExp(zN,"g"),CW=new RegExp(VN,"g"),vW=new RegExp(BN,"g"),u0=class{selector;content;constructor(o,e){this.selector=o,this.content=e}};function ox(n,o){let e=bW(n),t=xW(e,_W,Vk),i=0,r=t.escapedString.replace(hW,(...s)=>{let d=s[2],m="",u=s[4],h="";u&&u.startsWith("{"+Vk)&&(m=t.blocks[i++],u=u.substring(Vk.length+1),h="{");let v=o(new u0(d,m));return`${s[1]}${v.selector}${s[3]}${h}${v.content}${u}`});return wW(r)}var LP=class{escapedString;blocks;constructor(o,e){this.escapedString=o,this.blocks=e}};function xW(n,o,e){let t=[],i=[],r=0,s=0,d=-1,m,u;for(let h=0;h0;){let s=r.length,d=n.pop();for(let m=0;mi?`${e}${s}${o}`:`${e}${s}${t}${o}, ${e}${s} ${t}${o}`).join(",")}function SW(n,o){let e=n.length;for(let t=1;t{class n{static nextListId=0;debugListId=n.nextListId++;head={kind:j.ListEnd,next:null,prev:null,debugListId:this.debugListId};tail={kind:j.ListEnd,next:null,prev:null,debugListId:this.debugListId};constructor(){this.head.next=this.tail,this.tail.prev=this.head}push(e){if(Array.isArray(e)){for(let i of e)this.push(i);return}n.assertIsNotEnd(e),n.assertIsUnowned(e),e.debugListId=this.debugListId;let t=this.tail.prev;e.prev=t,t.next=e,e.next=this.tail,this.tail.prev=e}prepend(e){if(e.length===0)return;for(let r of e)n.assertIsNotEnd(r),n.assertIsUnowned(r),r.debugListId=this.debugListId;let t=this.head.next,i=this.head;for(let r of e)i.next=r,r.prev=i,i=r;i.next=t,t.prev=i}*[Symbol.iterator](){let e=this.head.next;for(;e!==this.tail;){n.assertIsOwned(e,this.debugListId);let t=e.next;yield e,e=t}}*reversed(){let e=this.tail.prev;for(;e!==this.head;){n.assertIsOwned(e,this.debugListId);let t=e.prev;yield e,e=t}}static replace(e,t){n.assertIsNotEnd(e),n.assertIsNotEnd(t),n.assertIsOwned(e),n.assertIsUnowned(t),t.debugListId=e.debugListId,e.prev!==null&&(e.prev.next=t,t.prev=e.prev),e.next!==null&&(e.next.prev=t,t.next=e.next),e.debugListId=null,e.prev=null,e.next=null}static replaceWithMany(e,t){if(t.length===0){n.remove(e);return}n.assertIsNotEnd(e),n.assertIsOwned(e);let i=e.debugListId;e.debugListId=null;for(let h of t)n.assertIsNotEnd(h),n.assertIsUnowned(h);let{prev:r,next:s}=e;e.prev=null,e.next=null;let d=r;for(let h of t)n.assertIsUnowned(h),h.debugListId=i,d.next=h,h.prev=d,h.next=null,d=h;let m=t[0],u=d;r!==null&&(r.next=m,m.prev=r),s!==null&&(s.prev=u,u.next=s)}static remove(e){n.assertIsNotEnd(e),n.assertIsOwned(e),e.prev.next=e.next,e.next.prev=e.prev,e.debugListId=null,e.prev=null,e.next=null}static insertBefore(e,t){if(Array.isArray(e)){for(let i of e)n.insertBefore(i,t);return}if(n.assertIsOwned(t),t.prev===null)throw new Error("AssertionError: illegal operation on list start");n.assertIsNotEnd(e),n.assertIsUnowned(e),e.debugListId=t.debugListId,e.prev=null,t.prev.next=e,e.prev=t.prev,e.next=t,t.prev=e}static insertAfter(e,t){if(n.assertIsOwned(t),t.next===null)throw new Error("AssertionError: illegal operation on list end");n.assertIsNotEnd(e),n.assertIsUnowned(e),e.debugListId=t.debugListId,t.next.prev=e,e.next=t.next,e.prev=t,t.next=e}static assertIsUnowned(e){if(e.debugListId!==null)throw new Error(`AssertionError: illegal operation on owned node: ${j[e.kind]}`)}static assertIsOwned(e,t){if(e.debugListId===null)throw new Error(`AssertionError: illegal operation on unowned node: ${j[e.kind]}`);if(t!==void 0&&e.debugListId!==t)throw new Error(`AssertionError: node belongs to the wrong list (expected ${t}, actual ${e.debugListId})`)}static assertIsNotEnd(e){if(e.kind===j.ListEnd)throw new Error("AssertionError: illegal operation on list head or tail")}}return n})();function Zs(n){return q({kind:j.Statement,statement:n},Dn)}function fp(n,o,e,t){return q({kind:j.Variable,xref:n,variable:o,initializer:e,flags:t},Dn)}var Dn={debugListId:null,prev:null,next:null},jN=Symbol("ConsumesSlot"),GT=Symbol("DependsOnSlotContext"),hu=Symbol("ConsumesVars"),F0=Symbol("UsesVarOffset"),bl={[jN]:!0,numSlotsUsed:1},ws={[GT]:!0},Ms={[hu]:!0};function nh(n){return n[jN]===!0}function h0(n){return n[GT]===!0}function Bk(n){return n[hu]===!0}function q5(n){return n[F0]===!0}function kW(n,o,e){return q(q(q({kind:j.InterpolateText,target:n,interpolation:o,sourceSpan:e},ws),Ms),Dn)}var nr=class{strings;expressions;i18nPlaceholders;constructor(o,e,t){if(this.strings=o,this.expressions=e,this.i18nPlaceholders=t,t.length!==0&&t.length!==e.length)throw new Error(`Expected ${e.length} placeholders to match interpolation expression count, but got ${t.length}`)}};function cu(n,o,e,t,i,r,s,d,m,u,h){return q({kind:j.Binding,bindingKind:o,target:n,name:e,expression:t,unit:i,securityContext:r,isTextAttribute:s,isStructuralTemplateAttribute:d,templateKind:m,i18nContext:null,i18nMessage:u,sourceSpan:h},Dn)}function PW(n,o,e,t,i,r,s,d,m,u){return q(q(q({kind:j.Property,target:n,name:o,expression:e,bindingKind:t,securityContext:i,sanitizer:null,isStructuralTemplateAttribute:r,templateKind:s,i18nContext:d,i18nMessage:m,sourceSpan:u},ws),Ms),Dn)}function TW(n,o,e,t,i,r,s,d,m){return q(q(q({kind:j.TwoWayProperty,target:n,name:o,expression:e,securityContext:t,sanitizer:null,isStructuralTemplateAttribute:i,templateKind:r,i18nContext:s,i18nMessage:d,sourceSpan:m},ws),Ms),Dn)}function EW(n,o,e,t,i){return q(q(q({kind:j.StyleProp,target:n,name:o,expression:e,unit:t,sourceSpan:i},ws),Ms),Dn)}function OW(n,o,e,t){return q(q(q({kind:j.ClassProp,target:n,name:o,expression:e,sourceSpan:t},ws),Ms),Dn)}function DW(n,o,e){return q(q(q({kind:j.StyleMap,target:n,expression:o,sourceSpan:e},ws),Ms),Dn)}function IW(n,o,e){return q(q(q({kind:j.ClassMap,target:n,expression:o,sourceSpan:e},ws),Ms),Dn)}function Q5(n,o,e,t,i,r,s,d,m,u){return q(q(q({kind:j.Attribute,target:n,namespace:o,name:e,expression:t,securityContext:i,sanitizer:null,isTextAttribute:r,isStructuralTemplateAttribute:s,templateKind:d,i18nContext:null,i18nMessage:m,sourceSpan:u},ws),Ms),Dn)}function AW(n,o){return q({kind:j.Advance,delta:n,sourceSpan:o},Dn)}function $N(n,o,e,t){return q(q(q({kind:j.Conditional,target:n,test:o,conditions:e,processed:null,sourceSpan:t,contextValue:null},Dn),ws),Ms)}function NW(n,o,e,t){return q(q({kind:j.Repeater,target:n,targetSlot:o,collection:e,sourceSpan:t},Dn),ws)}function K5(n,o,e,t,i,r,s){return q({kind:j.AnimationBinding,name:n,target:o,animationKind:e,expression:t,i18nMessage:null,securityContext:i,sanitizer:null,sourceSpan:r,animationBindingKind:s},Dn)}function FW(n,o,e,t){return q(q(q({kind:j.DeferWhen,target:n,expr:o,modifier:e,sourceSpan:t},Dn),ws),Ms)}function HN(n,o,e,t,i,r,s,d,m,u,h){return q(q(q({kind:j.I18nExpression,context:n,target:o,i18nOwner:e,handle:t,expression:i,icuPlaceholder:r,i18nPlaceholder:s,resolutionTime:d,usage:m,name:u,sourceSpan:h},Dn),Ms),ws)}function RW(n,o,e){return q({kind:j.I18nApply,owner:n,handle:o,sourceSpan:e},Dn)}function LW(n,o,e,t){return q(q(q({kind:j.StoreLet,target:n,declaredName:o,value:e,sourceSpan:t},ws),Ms),Dn)}function zW(n,o){return q(q({kind:j.Control,sourceSpan:o,target:n},ws),Dn)}function $c(n){return n instanceof eo}var eo=class extends Wi{constructor(o=null){super(null,o)}},na=class n extends eo{name;kind=Jt.LexicalRead;constructor(o){super(),this.name=o}visitExpression(o,e){}isEquivalent(o){return this.name===o.name}isConstant(){return!1}transformInternalExpressions(){}clone(){return new n(this.name)}},n1=class n extends eo{target;targetSlot;offset;kind=Jt.Reference;constructor(o,e,t){super(),this.target=o,this.targetSlot=e,this.offset=t}visitExpression(){}isEquivalent(o){return o instanceof n&&o.target===this.target}isConstant(){return!1}transformInternalExpressions(){}clone(){return new n(this.target,this.targetSlot,this.offset)}},_0=class n extends eo{target;value;sourceSpan;kind=Jt.StoreLet;[hu]=!0;[GT]=!0;constructor(o,e,t){super(),this.target=o,this.value=e,this.sourceSpan=t}visitExpression(){}isEquivalent(o){return o instanceof n&&o.target===this.target&&o.value.isEquivalent(this.value)}isConstant(){return!1}transformInternalExpressions(o,e){this.value=Rt(this.value,o,e)}clone(){return new n(this.target,this.value,this.sourceSpan)}},f0=class n extends eo{target;targetSlot;kind=Jt.ContextLetReference;constructor(o,e){super(),this.target=o,this.targetSlot=e}visitExpression(){}isEquivalent(o){return o instanceof n&&o.target===this.target}isConstant(){return!1}transformInternalExpressions(){}clone(){return new n(this.target,this.targetSlot)}},Tp=class n extends eo{view;kind=Jt.Context;constructor(o){super(),this.view=o}visitExpression(){}isEquivalent(o){return o instanceof n&&o.view===this.view}isConstant(){return!1}transformInternalExpressions(){}clone(){return new n(this.view)}},zP=class n extends eo{view;kind=Jt.TrackContext;constructor(o){super(),this.view=o}visitExpression(){}isEquivalent(o){return o instanceof n&&o.view===this.view}isConstant(){return!1}transformInternalExpressions(){}clone(){return new n(this.view)}},i1=class n extends eo{kind=Jt.NextContext;steps=1;visitExpression(){}isEquivalent(o){return o instanceof n&&o.steps===this.steps}isConstant(){return!1}transformInternalExpressions(){}clone(){let o=new n;return o.steps=this.steps,o}},VP=class n extends eo{kind=Jt.GetCurrentView;constructor(){super()}visitExpression(){}isEquivalent(o){return o instanceof n}isConstant(){return!1}transformInternalExpressions(){}clone(){return new n}},C0=class n extends eo{view;kind=Jt.RestoreView;constructor(o){super(),this.view=o}visitExpression(o,e){typeof this.view!="number"&&this.view.visitExpression(o,e)}isEquivalent(o){return!(o instanceof n)||typeof o.view!=typeof this.view?!1:typeof this.view=="number"?this.view===o.view:this.view.isEquivalent(o.view)}isConstant(){return!1}transformInternalExpressions(o,e){typeof this.view!="number"&&(this.view=Rt(this.view,o,e))}clone(){return new n(this.view instanceof Wi?this.view.clone():this.view)}},o1=class n extends eo{expr;kind=Jt.ResetView;constructor(o){super(),this.expr=o}visitExpression(o,e){this.expr.visitExpression(o,e)}isEquivalent(o){return o instanceof n&&this.expr.isEquivalent(o.expr)}isConstant(){return!1}transformInternalExpressions(o,e){this.expr=Rt(this.expr,o,e)}clone(){return new n(this.expr.clone())}},r1=class n extends eo{target;value;kind=Jt.TwoWayBindingSet;constructor(o,e){super(),this.target=o,this.value=e}visitExpression(o,e){this.target.visitExpression(o,e),this.value.visitExpression(o,e)}isEquivalent(o){return this.target.isEquivalent(o.target)&&this.value.isEquivalent(o.value)}isConstant(){return!1}transformInternalExpressions(o,e){this.target=Rt(this.target,o,e),this.value=Rt(this.value,o,e)}clone(){return new n(this.target,this.value)}},Od=class n extends eo{xref;kind=Jt.ReadVariable;name=null;constructor(o){super(),this.xref=o}visitExpression(){}isEquivalent(o){return o instanceof n&&o.xref===this.xref}isConstant(){return!1}transformInternalExpressions(){}clone(){let o=new n(this.xref);return o.name=this.name,o}},du=class n extends eo{kind=Jt.PureFunctionExpr;[hu]=!0;[F0]=!0;varOffset=null;body;args;fn=null;constructor(o,e){super(),this.body=o,this.args=e}visitExpression(o,e){this.body?.visitExpression(o,e);for(let t of this.args)t.visitExpression(o,e)}isEquivalent(o){return!(o instanceof n)||o.args.length!==this.args.length?!1:o.body!==null&&this.body!==null&&o.body.isEquivalent(this.body)&&o.args.every((e,t)=>e.isEquivalent(this.args[t]))}isConstant(){return!1}transformInternalExpressions(o,e){this.body!==null?this.body=Rt(this.body,o,e|ii.InChildOperation):this.fn!==null&&(this.fn=Rt(this.fn,o,e));for(let t=0;te.clone()));return o.fn=this.fn?.clone()??null,o.varOffset=this.varOffset,o}},Ep=class n extends eo{index;kind=Jt.PureFunctionParameterExpr;constructor(o){super(),this.index=o}visitExpression(){}isEquivalent(o){return o instanceof n&&o.index===this.index}isConstant(){return!0}transformInternalExpressions(){}clone(){return new n(this.index)}},pu=class n extends eo{target;targetSlot;name;args;kind=Jt.PipeBinding;[hu]=!0;[F0]=!0;varOffset=null;constructor(o,e,t,i){super(),this.target=o,this.targetSlot=e,this.name=t,this.args=i}visitExpression(o,e){for(let t of this.args)t.visitExpression(o,e)}isEquivalent(){return!1}isConstant(){return!1}transformInternalExpressions(o,e){for(let t=0;te.clone()));return o.varOffset=this.varOffset,o}},v0=class n extends eo{target;targetSlot;name;args;numArgs;kind=Jt.PipeBindingVariadic;[hu]=!0;[F0]=!0;varOffset=null;constructor(o,e,t,i,r){super(),this.target=o,this.targetSlot=e,this.name=t,this.args=i,this.numArgs=r}visitExpression(o,e){this.args.visitExpression(o,e)}isEquivalent(){return!1}isConstant(){return!1}transformInternalExpressions(o,e){this.args=Rt(this.args,o,e)}clone(){let o=new n(this.target,this.targetSlot,this.name,this.args.clone(),this.numArgs);return o.varOffset=this.varOffset,o}},qg=class n extends eo{receiver;name;kind=Jt.SafePropertyRead;constructor(o,e){super(),this.receiver=o,this.name=e}get index(){return this.name}visitExpression(o,e){this.receiver.visitExpression(o,e)}isEquivalent(){return!1}isConstant(){return!1}transformInternalExpressions(o,e){this.receiver=Rt(this.receiver,o,e)}clone(){return new n(this.receiver.clone(),this.name)}},Qg=class n extends eo{receiver;index;kind=Jt.SafeKeyedRead;constructor(o,e,t){super(t),this.receiver=o,this.index=e}visitExpression(o,e){this.receiver.visitExpression(o,e),this.index.visitExpression(o,e)}isEquivalent(){return!1}isConstant(){return!1}transformInternalExpressions(o,e){this.receiver=Rt(this.receiver,o,e),this.index=Rt(this.index,o,e)}clone(){return new n(this.receiver.clone(),this.index.clone(),this.sourceSpan)}},mu=class n extends eo{receiver;args;kind=Jt.SafeInvokeFunction;constructor(o,e){super(),this.receiver=o,this.args=e}visitExpression(o,e){this.receiver.visitExpression(o,e);for(let t of this.args)t.visitExpression(o,e)}isEquivalent(){return!1}isConstant(){return!1}transformInternalExpressions(o,e){this.receiver=Rt(this.receiver,o,e);for(let t=0;to.clone()))}},Kg=class n extends eo{guard;expr;kind=Jt.SafeTernaryExpr;constructor(o,e){super(),this.guard=o,this.expr=e}visitExpression(o,e){this.guard.visitExpression(o,e),this.expr.visitExpression(o,e)}isEquivalent(){return!1}isConstant(){return!1}transformInternalExpressions(o,e){this.guard=Rt(this.guard,o,e),this.expr=Rt(this.expr,o,e)}clone(){return new n(this.guard.clone(),this.expr.clone())}},x0=class n extends eo{kind=Jt.EmptyExpr;visitExpression(o,e){}isEquivalent(o){return o instanceof n}isConstant(){return!0}clone(){return new n}transformInternalExpressions(){}},Hc=class n extends eo{expr;xref;kind=Jt.AssignTemporaryExpr;name=null;constructor(o,e){super(),this.expr=o,this.xref=e}visitExpression(o,e){this.expr.visitExpression(o,e)}isEquivalent(){return!1}isConstant(){return!1}transformInternalExpressions(o,e){this.expr=Rt(this.expr,o,e)}clone(){let o=new n(this.expr.clone(),this.xref);return o.name=this.name,o}},Op=class n extends eo{xref;kind=Jt.ReadTemporaryExpr;name=null;constructor(o){super(),this.xref=o}visitExpression(o,e){}isEquivalent(){return this.xref===this.xref}isConstant(){return!1}transformInternalExpressions(o,e){}clone(){let o=new n(this.xref);return o.name=this.name,o}},a1=class n extends eo{slot;kind=Jt.SlotLiteralExpr;constructor(o){super(),this.slot=o}visitExpression(o,e){}isEquivalent(o){return o instanceof n&&o.slot===this.slot}isConstant(){return!0}clone(){return new n(this.slot)}transformInternalExpressions(){}},s1=class n extends eo{expr;target;targetSlot;alias;kind=Jt.ConditionalCase;constructor(o,e,t,i=null){super(),this.expr=o,this.target=e,this.targetSlot=t,this.alias=i}visitExpression(o,e){this.expr!==null&&this.expr.visitExpression(o,e)}isEquivalent(o){return o instanceof n&&o.expr===this.expr}isConstant(){return!0}clone(){return new n(this.expr,this.target,this.targetSlot)}transformInternalExpressions(o,e){this.expr!==null&&(this.expr=Rt(this.expr,o,e))}},y0=class n extends eo{expr;kind=Jt.ConstCollected;constructor(o){super(),this.expr=o}transformInternalExpressions(o,e){this.expr=o(this.expr,e)}visitExpression(o,e){this.expr.visitExpression(o,e)}isEquivalent(o){return o instanceof n?this.expr.isEquivalent(o.expr):!1}isConstant(){return this.expr.isConstant()}clone(){return new n(this.expr)}},BP=class n extends eo{parameters;body;kind=Jt.ArrowFunction;[hu]=!0;[F0]=!0;contextName=Ys;currentViewName="view";varOffset=null;ops;constructor(o,e){super(),this.parameters=o,this.body=e,this.ops=new Je,this.ops.push([Zs(new Or(e,e.sourceSpan))])}visitExpression(o,e){for(let t of this.ops)xr(t,i=>{i.visitExpression(o,e)})}isEquivalent(o){return o instanceof n&&o.parameters.length===this.parameters.length&&o.parameters.every((e,t)=>e.isEquivalent(this.parameters[t]))&&o.body.isEquivalent(this.body)}isConstant(){return!1}transformInternalExpressions(o,e){for(let t of this.ops)ir(t,o,e|(ii.InChildOperation|ii.InArrowFunctionOperation))}clone(){let o=new n(this.parameters,this.body);return o.varOffset=this.varOffset,o.ops=this.ops,o}};function xr(n,o){ir(n,(e,t)=>(o(e,t),e),ii.None)}var ii=(function(n){return n[n.None=0]="None",n[n.InChildOperation=1]="InChildOperation",n[n.InArrowFunctionOperation=2]="InArrowFunctionOperation",n})(ii||{});function jk(n,o,e){for(let t=0;tRt(t,o,e));else if(n instanceof gu)if(Array.isArray(n.body))for(let t=0;t{!s&&h0(d)&&d.target!==r.xref&&(s=!0)}),s)break;e=e.next}}}}function pG(n){if(!(!n.enableDebugLocations||n.relativeTemplatePath===null))for(let o of n.units){let e=[];for(let t of o.create)if(t.kind===j.ElementStart||t.kind===j.Element){let i=t.startSourceSpan.start;e.push({targetSlot:t.handle,offset:i.offset,line:i.line,column:i.col})}e.length>0&&o.create.push(iG(n.relativeTemplatePath,e))}}function JN(n){let o=new Map;for(let e of n.create)nh(e)&&(o.set(e.xref,e),e.kind===j.RepeaterCreate&&e.emptyView!==null&&o.set(e.emptyView,e));return o}function mG(n){for(let o of n.units){let e=JN(o);for(let t of o.ops())switch(t.kind){case j.Attribute:uG(o,t,e);break;case j.Property:if(t.bindingKind!==qt.LegacyAnimation&&t.bindingKind!==qt.Animation){let i;t.i18nMessage!==null&&t.templateKind===null?i=qt.I18n:t.isStructuralTemplateAttribute?i=qt.Template:i=qt.Property,Je.insertBefore(Cl(t.target,i,null,t.name,null,null,null,t.securityContext),Mg(e,t.target))}break;case j.TwoWayProperty:Je.insertBefore(Cl(t.target,qt.TwoWayProperty,null,t.name,null,null,null,t.securityContext),Mg(e,t.target));break;case j.StyleProp:case j.ClassProp:t.expression instanceof x0&&Je.insertBefore(Cl(t.target,qt.Property,null,t.name,null,null,null,mi.STYLE),Mg(e,t.target));break;case j.Listener:if(!t.isLegacyAnimationListener){let i=Cl(t.target,qt.Property,null,t.name,null,null,null,mi.NONE);if(n.kind===Et.Host)break;Je.insertBefore(i,Mg(e,t.target))}break;case j.TwoWayListener:if(n.kind!==Et.Host){let i=Cl(t.target,qt.Property,null,t.name,null,null,null,mi.NONE);Je.insertBefore(i,Mg(e,t.target))}break}}}function Mg(n,o){let e=n.get(o);if(e===void 0)throw new Error("All attributes should have an element-like target.");return e}function uG(n,o,e){if(!(o.expression instanceof nr)&&o.isTextAttribute){let t=Cl(o.target,o.isStructuralTemplateAttribute?qt.Template:qt.Attribute,o.namespace,o.name,o.expression,o.i18nContext,o.i18nMessage,o.securityContext);if(n.job.kind===Et.Host)n.create.push(t);else{let i=Mg(e,o.target);Je.insertBefore(t,i)}Je.remove(o)}}var X5="aria-";function eF(n){return n.startsWith(X5)&&n.length>X5.length}function gG(n,o){let e=n.get(o);if(e===void 0)throw new Error("All attributes should have an element-like target.");return e}function hG(n){let o=new Map;for(let e of n.units)for(let t of e.create)Dp(t)&&o.set(t.xref,t);for(let e of n.units)for(let t of e.ops())if(t.kind===j.Binding)switch(t.bindingKind){case qt.Attribute:if(t.name==="ngNonBindable"){Je.remove(t);let i=gG(o,t.target);i.nonBindable=!0}else if(t.name.startsWith("animate."))Je.replace(t,K5(t.name,t.target,t.name==="animate.enter"?"enter":"leave",t.expression,t.securityContext,t.sourceSpan,0));else{let[i,r]=ha(t.name);Je.replace(t,Q5(t.target,i,r,t.expression,t.securityContext,t.isTextAttribute,t.isStructuralTemplateAttribute,t.templateKind,t.i18nMessage,t.sourceSpan))}break;case qt.Animation:Je.replace(t,K5(t.name,t.target,t.name==="animate.enter"?"enter":"leave",t.expression,t.securityContext,t.sourceSpan,1));break;case qt.Property:case qt.LegacyAnimation:n.mode===hs.DomOnly&&eF(t.name)?Je.replace(t,Q5(t.target,null,t.name,t.expression,t.securityContext,!1,t.isStructuralTemplateAttribute,t.templateKind,t.i18nMessage,t.sourceSpan)):n.kind===Et.Host?Je.replace(t,rG(t.name,t.expression,t.bindingKind,t.i18nContext,t.securityContext,t.sourceSpan)):Je.replace(t,PW(t.target,t.name,t.expression,t.bindingKind,t.securityContext,t.isStructuralTemplateAttribute,t.templateKind,t.i18nContext,t.i18nMessage,t.sourceSpan));break;case qt.TwoWayProperty:if(!(t.expression instanceof Wi))throw new Error(`Expected value of two-way property binding "${t.name}" to be an expression`);Je.replace(t,TW(t.target,t.name,t.expression,t.securityContext,t.isStructuralTemplateAttribute,t.templateKind,t.i18nContext,t.i18nMessage,t.sourceSpan));break;case qt.I18n:case qt.ClassName:case qt.StyleProperty:throw new Error(`Unhandled binding of kind ${qt[t.bindingKind]}`)}}var Y5=new Map([[ge.ariaProperty,ge.ariaProperty],[ge.attribute,ge.attribute],[ge.classProp,ge.classProp],[ge.element,ge.element],[ge.elementContainer,ge.elementContainer],[ge.elementContainerEnd,ge.elementContainerEnd],[ge.elementContainerStart,ge.elementContainerStart],[ge.elementEnd,ge.elementEnd],[ge.elementStart,ge.elementStart],[ge.domProperty,ge.domProperty],[ge.i18nExp,ge.i18nExp],[ge.listener,ge.listener],[ge.listener,ge.listener],[ge.property,ge.property],[ge.styleProp,ge.styleProp],[ge.syntheticHostListener,ge.syntheticHostListener],[ge.syntheticHostProperty,ge.syntheticHostProperty],[ge.templateCreate,ge.templateCreate],[ge.twoWayProperty,ge.twoWayProperty],[ge.twoWayListener,ge.twoWayListener],[ge.declareLet,ge.declareLet],[ge.conditionalCreate,ge.conditionalBranchCreate],[ge.conditionalBranchCreate,ge.conditionalBranchCreate],[ge.domElement,ge.domElement],[ge.domElementStart,ge.domElementStart],[ge.domElementEnd,ge.domElementEnd],[ge.domElementContainer,ge.domElementContainer],[ge.domElementContainerStart,ge.domElementContainerStart],[ge.domElementContainerEnd,ge.domElementContainerEnd],[ge.domListener,ge.domListener],[ge.domTemplate,ge.domTemplate],[ge.animationEnter,ge.animationEnter],[ge.animationLeave,ge.animationLeave],[ge.animationEnterListener,ge.animationEnterListener],[ge.animationLeaveListener,ge.animationLeaveListener]]),_G=256;function fG(n){for(let o of n.units)Z5(o.create),Z5(o.update)}function Z5(n){let o=null;for(let e of n){if(e.kind!==j.Statement||!(e.statement instanceof fa)){o=null;continue}if(!(e.statement.expr instanceof ys)||!(e.statement.expr.fn instanceof eu)){o=null;continue}let t=e.statement.expr.fn.value;if(!Y5.has(t)){o=null;continue}if(o!==null&&Y5.get(o.instruction)===t&&o.length<_G){let i=o.expression.callFn(e.statement.expr.args,e.statement.expr.sourceSpan,e.statement.expr.pure);o.expression=i,o.op.statement=i.toStmt(),o.length++,Je.remove(e)}else o={op:e,instruction:t,expression:e.statement.expr,length:1}}}function CG(n){for(let o of n.units)for(let e of o.update)(e.kind===j.Attribute||e.kind===j.StyleProp||e.kind==j.StyleMap||e.kind===j.ClassMap)&&e.expression instanceof nr&&e.expression.strings.length===2&&e.expression.strings.every(i=>i==="")&&(e.expression=e.expression.expressions[0])}function vG(n){for(let o of n.units)for(let e of o.ops()){if(e.kind!==j.Conditional)continue;let t,i=e.conditions.findIndex(d=>d.expr===null);if(i>=0){let d=e.conditions.splice(i,1)[0].targetSlot;t=new a1(d)}else t=Oe(-1);let r=e.test==null?null:new Hc(e.test,n.allocateXrefId()),s=null;for(let d=e.conditions.length-1;d>=0;d--){let m=e.conditions[d];if(m.expr!==null){if(r!==null){let u=d===0?r:new Op(r.xref);m.expr=new Si(lt.Identical,u,m.expr)}else m.alias!==null&&(s??=n.allocateXrefId(),m.expr=new Hc(m.expr,s),e.contextValue=new Op(s));t=new Lc(m.expr,new a1(m.targetSlot),t)}}e.processed=t,e.conditions=[]}}var xG=new Map([["&&",lt.And],[">",lt.Bigger],[">=",lt.BiggerEquals],["|",lt.BitwiseOr],["&",lt.BitwiseAnd],["/",lt.Divide],["=",lt.Assign],["==",lt.Equals],["===",lt.Identical],["<",lt.Lower],["<=",lt.LowerEquals],["-",lt.Minus],["%",lt.Modulo],["**",lt.Exponentiation],["*",lt.Multiply],["!=",lt.NotEquals],["!==",lt.NotIdentical],["??",lt.NullishCoalesce],["||",lt.Or],["+",lt.Plus],["in",lt.In],["instanceof",lt.InstanceOf],["+=",lt.AdditionAssignment],["-=",lt.SubtractionAssignment],["*=",lt.MultiplicationAssignment],["/=",lt.DivisionAssignment],["%=",lt.RemainderAssignment],["**=",lt.ExponentiationAssignment],["&&=",lt.AndAssignment],["||=",lt.OrAssignment],["??=",lt.NullishCoalesceAssignment]]);function tF(n){let o=new Map([["svg",Tr.SVG],["math",Tr.Math]]);return n===null?Tr.HTML:o.get(n)??Tr.HTML}function yG(n){let o=new Map([["svg",Tr.SVG],["math",Tr.Math]]);for(let[e,t]of o.entries())if(t===n)return e;return null}function bG(n,o){return o===Tr.HTML?n:`:${yG(o)}:${n}`}function Xg(n){return Array.isArray(n)?Ji(n.map(Xg)):Oe(n)}function wG(n){let o=new Map;for(let e of n.units)for(let t of e.create)if(t.kind===j.ExtractedAttribute){let i=o.get(t.target)||new $P;o.set(t.target,i),i.add(t.bindingKind,t.name,t.expression,t.namespace,t.trustedValueFn),Je.remove(t)}if(n instanceof b0)for(let e of n.units)for(let t of e.create)if(t.kind==j.Projection){let i=o.get(t.xref);if(i!==void 0){let r=HP(i);r.entries.length>0&&(t.attributes=n.pool.getConstLiteral(r,!0))}}else Dp(t)&&(t.attributes=J5(n,o,t.xref),t.kind===j.RepeaterCreate&&t.emptyView!==null&&(t.emptyAttributes=J5(n,o,t.emptyView)));else if(n instanceof p1)for(let[e,t]of o.entries()){if(e!==n.root.xref)throw new Error("An attribute would be const collected into the host binding's template function, but is not associated with the root xref.");let i=HP(t);i.entries.length>0&&(n.root.attributes=i)}}function J5(n,o,e){let t=o.get(e);if(t!==void 0){let i=HP(t);if(i.entries.length>0)return n.addConst(i)}return null}var Cg=Object.freeze([]),$P=class{known=new Map;byKind=new Map;propertyBindings=null;projectAs=null;get attributes(){return this.byKind.get(qt.Attribute)??Cg}get classes(){return this.byKind.get(qt.ClassName)??Cg}get styles(){return this.byKind.get(qt.StyleProperty)??Cg}get bindings(){return this.propertyBindings??Cg}get template(){return this.byKind.get(qt.Template)??Cg}get i18n(){return this.byKind.get(qt.I18n)??Cg}isKnown(o,e){let t=this.known.get(o)??new Set;return this.known.set(o,t),t.has(e)?!0:(t.add(e),!1)}add(o,e,t,i,r){if(!(o===qt.Attribute||o===qt.ClassName||o===qt.StyleProperty)&&this.isKnown(o,e))return;if(e==="ngProjectAs"){if(t===null||!(t instanceof _a)||t.value==null||typeof t.value?.toString()!="string")throw Error("ngProjectAs must have a string literal value");this.projectAs=t.value.toString()}let d=this.arrayFor(o);if(d.push(...MG(i,e)),o===qt.Attribute||o===qt.StyleProperty){if(t===null)throw Error("Attribute, i18n attribute, & style element attributes must have a value");r!==null&&UN(t)?d.push(BH(r,new Lf([new kx(t.value)],[]),void 0,t.sourceSpan)):d.push(t)}}arrayFor(o){return o===qt.Property||o===qt.TwoWayProperty?(this.propertyBindings??=[],this.propertyBindings):(this.byKind.has(o)||this.byKind.set(o,[]),this.byKind.get(o))}};function MG(n,o){let e=Oe(o);return n?[Oe(0),Oe(n),e]:[e]}function HP({attributes:n,bindings:o,classes:e,i18n:t,projectAs:i,styles:r,template:s}){let d=[...n];if(i!==null){let m=AT(i)[0];d.push(Oe(5),Xg(m))}return e.length>0&&d.push(Oe(1),...e),r.length>0&&d.push(Oe(2),...r),o.length>0&&d.push(Oe(3),...o),s.length>0&&d.push(Oe(4),...s),t.length>0&&d.push(Oe(6),...t),Ji(d)}function SG(n,o){let e=n.get(o);if(e===void 0)throw new Error("All attributes should have an element-like target.");return e}function kG(n){let o=new Map;for(let e of n.units)for(let t of e.create)Dp(t)&&o.set(t.xref,t);for(let e of n.units)for(let t of e.ops())if(t.kind===j.AnimationBinding){let i=PG(t);n.kind===Et.Host?e.create.push(i):Je.insertAfter(i,SG(o,t.target)),Je.remove(t)}}function PG(n){if(n.animationBindingKind===0)return WW(n.name,n.target,n.name==="animate.enter"?"enter":"leave",n.expression,n.securityContext,n.sourceSpan);{let o=n.expression;return GW(n.name,n.target,n.name==="animate.enter"?"enter":"leave",[Zs(new Or(o,o.sourceSpan))],n.securityContext,n.sourceSpan)}}function TG(n){let o=new Map;for(let e of n.units){for(let t of e.create)t.kind===j.I18nAttributes&&o.set(t.target,t);for(let t of e.update)switch(t.kind){case j.Property:case j.Attribute:if(t.i18nContext===null||!(t.expression instanceof nr))continue;let i=o.get(t.target);if(i===void 0)throw new Error("AssertionError: An i18n attribute binding instruction requires the owning element to have an I18nAttributes create instruction");if(i.target!==t.target)throw new Error("AssertionError: Expected i18nAttributes target element to match binding target element");let r=[];for(let s=0;sHG(t,{job:n}),ii.None),ir(e,UG,ii.None)}function Hs(n){return n instanceof tu?Hs(n.expr):n instanceof Si?Hs(n.lhs)||Hs(n.rhs):n instanceof Lc?n.falseCase&&Hs(n.falseCase)?!0:Hs(n.condition)||Hs(n.trueCase):n instanceof zf?Hs(n.condition):n instanceof Hc?Hs(n.expr):n instanceof Ks?Hs(n.receiver):n instanceof Id?Hs(n.receiver)||Hs(n.index):n instanceof nc?Hs(n.expr):n instanceof ys||n instanceof zc||n instanceof ic||n instanceof mu||n instanceof pu}function zG(n){let o=new Set;return Rt(n,e=>(e instanceof Hc&&o.add(e.xref),e),ii.None),o}function VG(n,o,e){return Rt(n,t=>{if(t instanceof Hc&&o.has(t.xref)){let i=new Op(t.xref);return new Hc(i,i.xref)}return t},ii.None),n}function vg(n,o,e){let t;if(Hs(n)){let i=e.job.allocateXrefId();t=[new Hc(n,i),new Op(i)]}else t=[n,n.clone()],VG(t[1],zG(t[0]));return new Kg(t[0],o(t[1]))}function BG(n){return n instanceof qg||n instanceof Qg||n instanceof mu}function jG(n){return n instanceof Ks||n instanceof Id||n instanceof ys}function nF(n){return BG(n)||jG(n)}function $G(n){if(nF(n)&&n.receiver instanceof Kg){let o=n.receiver;for(;o.expr instanceof Kg;)o=o.expr;return o}return null}function HG(n,o){if(!nF(n))return n;let e=$G(n);if(e){if(n instanceof ys)return e.expr=e.expr.callFn(n.args),n.receiver;if(n instanceof Ks)return e.expr=e.expr.prop(n.name),n.receiver;if(n instanceof Id)return e.expr=e.expr.key(n.index),n.receiver;if(n instanceof mu)return e.expr=vg(e.expr,t=>t.callFn(n.args),o),n.receiver;if(n instanceof qg)return e.expr=vg(e.expr,t=>t.prop(n.name),o),n.receiver;if(n instanceof Qg)return e.expr=vg(e.expr,t=>t.key(n.index),o),n.receiver}else{if(n instanceof mu)return vg(n.receiver,t=>t.callFn(n.args),o);if(n instanceof qg)return vg(n.receiver,t=>t.prop(n.name),o);if(n instanceof Qg)return vg(n.receiver,t=>t.key(n.index),o)}return n}function UG(n){return n instanceof Kg?new nc(new Lc(new Si(lt.Equals,n.guard,Lg),Lg,n.expr)):n}var e6="\uFFFD",WG="#",GG="*",qG="/",QG=":",KG="[",XG="]",YG="|";function ZG(n){let o=new Map,e=new Map,t=new Map;for(let r of n.units)for(let s of r.create)switch(s.kind){case j.I18nContext:let d=JG(n,s);r.create.push(d),o.set(s.xref,d),t.set(s.xref,s);break;case j.I18nStart:e.set(s.xref,s);break}let i=null;for(let r of n.units)for(let s of r.create)switch(s.kind){case j.IcuStart:i=s,Je.remove(s);let d=t.get(s.context);if(d.contextKind!==Wm.Icu)continue;let m=e.get(d.i18nBlock);if(m.context===d.xref)continue;let u=e.get(m.root),h=o.get(u.context);if(h===void 0)throw Error("AssertionError: ICU sub-message should belong to a root message.");let v=o.get(d.xref);v.messagePlaceholder=s.messagePlaceholder,h.subMessages.push(v.xref);break;case j.IcuEnd:i=null,Je.remove(s);break;case j.IcuPlaceholder:if(i===null||i.context==null)throw Error("AssertionError: Unexpected ICU placeholder outside of i18n context");o.get(i.context).postprocessingParams.set(s.name,Oe(eq(s))),Je.remove(s);break}}function JG(n,o,e){let t=t6(o.params),i=t6(o.postprocessingParams),r=[...o.params.values()].some(s=>s.length>1);return JW(n.allocateXrefId(),o.xref,o.i18nBlock,o.message,null,t,i,r)}function eq(n){if(n.strings.length!==n.expressionPlaceholders.length+1)throw Error(`AssertionError: Invalid ICU placeholder with ${n.strings.length} strings and ${n.expressionPlaceholders.length} expressions`);let o=n.expressionPlaceholders.map(Tg);return n.strings.flatMap((e,t)=>[e,o[t]||""]).join("")}function t6(n){let o=new Map;for(let[e,t]of n){let i=tq(t);i!==null&&o.set(e,Oe(i))}return o}function tq(n){if(n.length===0)return null;let o=n.map(e=>Tg(e));return o.length===1?o[0]:`${KG}${o.join(YG)}${XG}`}function Tg(n){if(n.flags&mo.ElementTag&&n.flags&mo.TemplateTag){if(typeof n.value!="object")throw Error("AssertionError: Expected i18n param value to have an element and template slot");let i=Tg(Le(q({},n),{value:n.value.element,flags:n.flags&~mo.TemplateTag})),r=Tg(Le(q({},n),{value:n.value.template,flags:n.flags&~mo.ElementTag}));return n.flags&mo.OpenTag&&n.flags&mo.CloseTag?`${r}${i}${r}`:n.flags&mo.CloseTag?`${i}${r}`:`${r}${i}`}if(n.flags&mo.OpenTag&&n.flags&mo.CloseTag)return`${Tg(Le(q({},n),{flags:n.flags&~mo.CloseTag}))}${Tg(Le(q({},n),{flags:n.flags&~mo.OpenTag}))}`;if(n.flags===mo.None)return`${n.value}`;let o="",e="";n.flags&mo.ElementTag?o=WG:n.flags&mo.TemplateTag&&(o=GG),o!==""&&(e=n.flags&mo.CloseTag?qG:"");let t=n.subTemplateIndex===null?"":`${QG}${n.subTemplateIndex}`;return`${e6}${e}${o}${n.value}${t}${e6}`}function nq(n){for(let o of n.units){let e=new Map;for(let i of o.create){if(nh(i)){if(i.handle.slot===null)throw new Error("AssertionError: expected slots to have been allocated before generating advance() calls")}else continue;e.set(i.xref,i.handle.slot)}let t=0;for(let i of o.update){let r=null;if(h0(i)?r=i:xr(i,d=>{r===null&&h0(d)&&(r=d)}),r===null)continue;if(!e.has(r.target))throw new Error(`AssertionError: reference to unknown slot for target ${r.target}`);let s=e.get(r.target);if(t!==s){let d=s-t;if(d<0)throw new Error("AssertionError: slot counter should never need to move backwards");Je.insertBefore(AW(d,r.sourceSpan),i),t=s}}}}function iq(n){for(let o of n.units)for(let e of o.update){if(e.kind!==j.StoreLet)continue;let t={kind:oa.Identifier,name:null,identifier:e.declaredName,local:!0};Je.replace(e,fp(n.allocateXrefId(),t,new _0(e.target,e.value,e.sourceSpan),fl.None))}}function oq(n){let e=[],t=0;for(let i of n.units)for(let r of i.create)r.kind===j.Projection&&(e.push(r.selector),r.projectionSlotIndex=t++);if(e.length>0){let i=null;if(e.length>1||e[0]!=="*"){let r=e.map(s=>s==="*"?s:AT(s));i=n.pool.getConstLiteral(Xg(r),!0)}n.contentSelectors=n.pool.getConstLiteral(Xg(e),!0),n.root.create.prepend([KW(i)])}}function rq(n){yf(n.root,null)}function yf(n,o){let e=n6(n,o);for(let t of n.create)switch(t.kind){case j.ConditionalCreate:case j.ConditionalBranchCreate:case j.Template:yf(n.job.views.get(t.xref),e);break;case j.Projection:t.fallbackView!==null&&yf(n.job.views.get(t.fallbackView),e);break;case j.RepeaterCreate:yf(n.job.views.get(t.xref),e),t.emptyView&&yf(n.job.views.get(t.emptyView),e),t.trackByOps!==null&&t.trackByOps.prepend(bf(n,e,!1));break;case j.Animation:case j.AnimationListener:case j.Listener:case j.TwoWayListener:t.handlerOps.prepend(bf(n,e,!0));break}n.update.prepend(bf(n,e,!1));for(let t of n.functions)t.ops.prepend(bf(n,n6(n,o),!0))}function n6(n,o){let e={view:n.xref,viewContextVariable:{kind:oa.Context,name:null,view:n.xref},contextVariables:new Map,aliases:n.aliases,references:[],letDeclarations:[],parent:o};for(let t of n.contextVariables.keys())e.contextVariables.set(t,{kind:oa.Identifier,name:null,identifier:t,local:!1});for(let t of n.create)switch(t.kind){case j.ElementStart:case j.ConditionalCreate:case j.ConditionalBranchCreate:case j.Template:if(!Array.isArray(t.localRefs))throw new Error("AssertionError: expected localRefs to be an array");for(let i=0;it instanceof y0?Oe(n.addConst(t.expr)):t,ii.None)}var i6="style.",o6="class.",sq="style!",r6="class!",a6="!important";function lq(n){for(let o of n.root.update)if(o.kind===j.Binding&&o.bindingKind===qt.Property)if(o.name.endsWith(a6)&&(o.name=o.name.substring(0,o.name.length-a6.length)),o.name.startsWith(i6)){o.bindingKind=qt.StyleProperty,o.name=o.name.substring(i6.length),cq(o.name)||(o.name=dq(o.name));let{property:e,suffix:t}=Hk(o.name);o.name=e,o.unit=t}else o.name.startsWith(sq)?(o.bindingKind=qt.StyleProperty,o.name="style"):o.name.startsWith(o6)?(o.bindingKind=qt.ClassName,o.name=Hk(o.name.substring(o6.length)).property):o.name.startsWith(r6)&&(o.bindingKind=qt.ClassName,o.name=Hk(o.name.substring(r6.length)).property)}function cq(n){return n.startsWith("--")}function dq(n){return n.replace(/[a-z][A-Z]/g,o=>o.charAt(0)+"-"+o.charAt(1)).toLowerCase()}function Hk(n){let o=n.indexOf("!important");o!==-1&&(n=o>0?n.substring(0,o):"");let e=null,t=n,i=n.lastIndexOf(".");return i>0&&(e=n.slice(i+1),t=n.substring(0,i)),{property:t,suffix:e}}function UP(n,o=!1){return yl(Object.keys(n).map(e=>({key:e,quoted:o,value:n[e]})))}var WP=class{visitText(o){return o.value}visitContainer(o){return o.children.map(e=>e.visit(this)).join("")}visitIcu(o){let e=Object.keys(o.cases).map(i=>`${i} {${o.cases[i].visit(this)}}`);return`{${o.expressionPlaceholder}, ${o.type}, ${e.join(" ")}}`}visitTagPlaceholder(o){return o.isVoid?this.formatPh(o.startName):`${this.formatPh(o.startName)}${o.children.map(e=>e.visit(this)).join("")}${this.formatPh(o.closeName)}`}visitPlaceholder(o){return this.formatPh(o.name)}visitBlockPlaceholder(o){return`${this.formatPh(o.startName)}${o.children.map(e=>e.visit(this)).join("")}${this.formatPh(o.closeName)}`}visitIcuPlaceholder(o,e){return this.formatPh(o.name)}formatPh(o){return`{${A0(o,!1)}}`}},pq=new WP;function iF(n){return n.visit(pq)}var Ad=class{sourceSpan;i18n;constructor(o,e){this.sourceSpan=o,this.i18n=e}},uu=class extends Ad{value;tokens;constructor(o,e,t,i){super(e,i),this.value=o,this.tokens=t}visit(o,e){return o.visitText(this,e)}},Xm=class extends Ad{switchValue;type;cases;switchValueSourceSpan;constructor(o,e,t,i,r,s){super(i,s),this.switchValue=o,this.type=e,this.cases=t,this.switchValueSourceSpan=r}visit(o,e){return o.visitExpansion(this,e)}},m1=class{value;expression;sourceSpan;valueSourceSpan;expSourceSpan;constructor(o,e,t,i,r){this.value=o,this.expression=e,this.sourceSpan=t,this.valueSourceSpan=i,this.expSourceSpan=r}visit(o,e){return o.visitExpansionCase(this,e)}},GP=class extends Ad{name;value;keySpan;valueSpan;valueTokens;constructor(o,e,t,i,r,s,d){super(t,d),this.name=o,this.value=e,this.keySpan=i,this.valueSpan=r,this.valueTokens=s}visit(o,e){return o.visitAttribute(this,e)}},ml=class extends Ad{name;attrs;directives;children;isSelfClosing;startSourceSpan;endSourceSpan;isVoid;constructor(o,e,t,i,r,s,d,m=null,u,h){super(s,h),this.name=o,this.attrs=e,this.directives=t,this.children=i,this.isSelfClosing=r,this.startSourceSpan=d,this.endSourceSpan=m,this.isVoid=u}visit(o,e){return o.visitElement(this,e)}},w0=class{value;sourceSpan;constructor(o,e){this.value=o,this.sourceSpan=e}visit(o,e){return o.visitComment(this,e)}},hl=class extends Ad{name;parameters;children;nameSpan;startSourceSpan;endSourceSpan;constructor(o,e,t,i,r,s,d=null,m){super(i,m),this.name=o,this.parameters=e,this.children=t,this.nameSpan=r,this.startSourceSpan=s,this.endSourceSpan=d}visit(o,e){return o.visitBlock(this,e)}},Qa=class extends Ad{componentName;tagName;fullName;attrs;directives;children;isSelfClosing;startSourceSpan;endSourceSpan;constructor(o,e,t,i,r,s,d,m,u,h=null,v){super(m,v),this.componentName=o,this.tagName=e,this.fullName=t,this.attrs=i,this.directives=r,this.children=s,this.isSelfClosing=d,this.startSourceSpan=u,this.endSourceSpan=h}visit(o,e){return o.visitComponent(this,e)}},qP=class{name;attrs;sourceSpan;startSourceSpan;endSourceSpan;constructor(o,e,t,i,r=null){this.name=o,this.attrs=e,this.sourceSpan=t,this.startSourceSpan=i,this.endSourceSpan=r}visit(o,e){return o.visitDirective(this,e)}},u1=class{expression;sourceSpan;constructor(o,e){this.expression=o,this.sourceSpan=e}visit(o,e){return o.visitBlockParameter(this,e)}},g1=class{name;value;sourceSpan;nameSpan;valueSpan;constructor(o,e,t,i,r){this.name=o,this.value=e,this.sourceSpan=t,this.nameSpan=i,this.valueSpan=r}visit(o,e){return o.visitLetDeclaration(this,e)}};function Mo(n,o,e=null){let t=[],i=n.visit?r=>n.visit(r,e)||r.visit(n,e):r=>r.visit(n,e);return o.forEach(r=>{let s=i(r);s&&t.push(s)}),t}var M0={AElig:"\xC6",AMP:"&",amp:"&",Aacute:"\xC1",Abreve:"\u0102",Acirc:"\xC2",Acy:"\u0410",Afr:"\u{1D504}",Agrave:"\xC0",Alpha:"\u0391",Amacr:"\u0100",And:"\u2A53",Aogon:"\u0104",Aopf:"\u{1D538}",ApplyFunction:"\u2061",af:"\u2061",Aring:"\xC5",angst:"\xC5",Ascr:"\u{1D49C}",Assign:"\u2254",colone:"\u2254",coloneq:"\u2254",Atilde:"\xC3",Auml:"\xC4",Backslash:"\u2216",setminus:"\u2216",setmn:"\u2216",smallsetminus:"\u2216",ssetmn:"\u2216",Barv:"\u2AE7",Barwed:"\u2306",doublebarwedge:"\u2306",Bcy:"\u0411",Because:"\u2235",becaus:"\u2235",because:"\u2235",Bernoullis:"\u212C",Bscr:"\u212C",bernou:"\u212C",Beta:"\u0392",Bfr:"\u{1D505}",Bopf:"\u{1D539}",Breve:"\u02D8",breve:"\u02D8",Bumpeq:"\u224E",HumpDownHump:"\u224E",bump:"\u224E",CHcy:"\u0427",COPY:"\xA9",copy:"\xA9",Cacute:"\u0106",Cap:"\u22D2",CapitalDifferentialD:"\u2145",DD:"\u2145",Cayleys:"\u212D",Cfr:"\u212D",Ccaron:"\u010C",Ccedil:"\xC7",Ccirc:"\u0108",Cconint:"\u2230",Cdot:"\u010A",Cedilla:"\xB8",cedil:"\xB8",CenterDot:"\xB7",centerdot:"\xB7",middot:"\xB7",Chi:"\u03A7",CircleDot:"\u2299",odot:"\u2299",CircleMinus:"\u2296",ominus:"\u2296",CirclePlus:"\u2295",oplus:"\u2295",CircleTimes:"\u2297",otimes:"\u2297",ClockwiseContourIntegral:"\u2232",cwconint:"\u2232",CloseCurlyDoubleQuote:"\u201D",rdquo:"\u201D",rdquor:"\u201D",CloseCurlyQuote:"\u2019",rsquo:"\u2019",rsquor:"\u2019",Colon:"\u2237",Proportion:"\u2237",Colone:"\u2A74",Congruent:"\u2261",equiv:"\u2261",Conint:"\u222F",DoubleContourIntegral:"\u222F",ContourIntegral:"\u222E",conint:"\u222E",oint:"\u222E",Copf:"\u2102",complexes:"\u2102",Coproduct:"\u2210",coprod:"\u2210",CounterClockwiseContourIntegral:"\u2233",awconint:"\u2233",Cross:"\u2A2F",Cscr:"\u{1D49E}",Cup:"\u22D3",CupCap:"\u224D",asympeq:"\u224D",DDotrahd:"\u2911",DJcy:"\u0402",DScy:"\u0405",DZcy:"\u040F",Dagger:"\u2021",ddagger:"\u2021",Darr:"\u21A1",Dashv:"\u2AE4",DoubleLeftTee:"\u2AE4",Dcaron:"\u010E",Dcy:"\u0414",Del:"\u2207",nabla:"\u2207",Delta:"\u0394",Dfr:"\u{1D507}",DiacriticalAcute:"\xB4",acute:"\xB4",DiacriticalDot:"\u02D9",dot:"\u02D9",DiacriticalDoubleAcute:"\u02DD",dblac:"\u02DD",DiacriticalGrave:"`",grave:"`",DiacriticalTilde:"\u02DC",tilde:"\u02DC",Diamond:"\u22C4",diam:"\u22C4",diamond:"\u22C4",DifferentialD:"\u2146",dd:"\u2146",Dopf:"\u{1D53B}",Dot:"\xA8",DoubleDot:"\xA8",die:"\xA8",uml:"\xA8",DotDot:"\u20DC",DotEqual:"\u2250",doteq:"\u2250",esdot:"\u2250",DoubleDownArrow:"\u21D3",Downarrow:"\u21D3",dArr:"\u21D3",DoubleLeftArrow:"\u21D0",Leftarrow:"\u21D0",lArr:"\u21D0",DoubleLeftRightArrow:"\u21D4",Leftrightarrow:"\u21D4",hArr:"\u21D4",iff:"\u21D4",DoubleLongLeftArrow:"\u27F8",Longleftarrow:"\u27F8",xlArr:"\u27F8",DoubleLongLeftRightArrow:"\u27FA",Longleftrightarrow:"\u27FA",xhArr:"\u27FA",DoubleLongRightArrow:"\u27F9",Longrightarrow:"\u27F9",xrArr:"\u27F9",DoubleRightArrow:"\u21D2",Implies:"\u21D2",Rightarrow:"\u21D2",rArr:"\u21D2",DoubleRightTee:"\u22A8",vDash:"\u22A8",DoubleUpArrow:"\u21D1",Uparrow:"\u21D1",uArr:"\u21D1",DoubleUpDownArrow:"\u21D5",Updownarrow:"\u21D5",vArr:"\u21D5",DoubleVerticalBar:"\u2225",par:"\u2225",parallel:"\u2225",shortparallel:"\u2225",spar:"\u2225",DownArrow:"\u2193",ShortDownArrow:"\u2193",darr:"\u2193",downarrow:"\u2193",DownArrowBar:"\u2913",DownArrowUpArrow:"\u21F5",duarr:"\u21F5",DownBreve:"\u0311",DownLeftRightVector:"\u2950",DownLeftTeeVector:"\u295E",DownLeftVector:"\u21BD",leftharpoondown:"\u21BD",lhard:"\u21BD",DownLeftVectorBar:"\u2956",DownRightTeeVector:"\u295F",DownRightVector:"\u21C1",rhard:"\u21C1",rightharpoondown:"\u21C1",DownRightVectorBar:"\u2957",DownTee:"\u22A4",top:"\u22A4",DownTeeArrow:"\u21A7",mapstodown:"\u21A7",Dscr:"\u{1D49F}",Dstrok:"\u0110",ENG:"\u014A",ETH:"\xD0",Eacute:"\xC9",Ecaron:"\u011A",Ecirc:"\xCA",Ecy:"\u042D",Edot:"\u0116",Efr:"\u{1D508}",Egrave:"\xC8",Element:"\u2208",in:"\u2208",isin:"\u2208",isinv:"\u2208",Emacr:"\u0112",EmptySmallSquare:"\u25FB",EmptyVerySmallSquare:"\u25AB",Eogon:"\u0118",Eopf:"\u{1D53C}",Epsilon:"\u0395",Equal:"\u2A75",EqualTilde:"\u2242",eqsim:"\u2242",esim:"\u2242",Equilibrium:"\u21CC",rightleftharpoons:"\u21CC",rlhar:"\u21CC",Escr:"\u2130",expectation:"\u2130",Esim:"\u2A73",Eta:"\u0397",Euml:"\xCB",Exists:"\u2203",exist:"\u2203",ExponentialE:"\u2147",ee:"\u2147",exponentiale:"\u2147",Fcy:"\u0424",Ffr:"\u{1D509}",FilledSmallSquare:"\u25FC",FilledVerySmallSquare:"\u25AA",blacksquare:"\u25AA",squarf:"\u25AA",squf:"\u25AA",Fopf:"\u{1D53D}",ForAll:"\u2200",forall:"\u2200",Fouriertrf:"\u2131",Fscr:"\u2131",GJcy:"\u0403",GT:">",gt:">",Gamma:"\u0393",Gammad:"\u03DC",Gbreve:"\u011E",Gcedil:"\u0122",Gcirc:"\u011C",Gcy:"\u0413",Gdot:"\u0120",Gfr:"\u{1D50A}",Gg:"\u22D9",ggg:"\u22D9",Gopf:"\u{1D53E}",GreaterEqual:"\u2265",ge:"\u2265",geq:"\u2265",GreaterEqualLess:"\u22DB",gel:"\u22DB",gtreqless:"\u22DB",GreaterFullEqual:"\u2267",gE:"\u2267",geqq:"\u2267",GreaterGreater:"\u2AA2",GreaterLess:"\u2277",gl:"\u2277",gtrless:"\u2277",GreaterSlantEqual:"\u2A7E",geqslant:"\u2A7E",ges:"\u2A7E",GreaterTilde:"\u2273",gsim:"\u2273",gtrsim:"\u2273",Gscr:"\u{1D4A2}",Gt:"\u226B",NestedGreaterGreater:"\u226B",gg:"\u226B",HARDcy:"\u042A",Hacek:"\u02C7",caron:"\u02C7",Hat:"^",Hcirc:"\u0124",Hfr:"\u210C",Poincareplane:"\u210C",HilbertSpace:"\u210B",Hscr:"\u210B",hamilt:"\u210B",Hopf:"\u210D",quaternions:"\u210D",HorizontalLine:"\u2500",boxh:"\u2500",Hstrok:"\u0126",HumpEqual:"\u224F",bumpe:"\u224F",bumpeq:"\u224F",IEcy:"\u0415",IJlig:"\u0132",IOcy:"\u0401",Iacute:"\xCD",Icirc:"\xCE",Icy:"\u0418",Idot:"\u0130",Ifr:"\u2111",Im:"\u2111",image:"\u2111",imagpart:"\u2111",Igrave:"\xCC",Imacr:"\u012A",ImaginaryI:"\u2148",ii:"\u2148",Int:"\u222C",Integral:"\u222B",int:"\u222B",Intersection:"\u22C2",bigcap:"\u22C2",xcap:"\u22C2",InvisibleComma:"\u2063",ic:"\u2063",InvisibleTimes:"\u2062",it:"\u2062",Iogon:"\u012E",Iopf:"\u{1D540}",Iota:"\u0399",Iscr:"\u2110",imagline:"\u2110",Itilde:"\u0128",Iukcy:"\u0406",Iuml:"\xCF",Jcirc:"\u0134",Jcy:"\u0419",Jfr:"\u{1D50D}",Jopf:"\u{1D541}",Jscr:"\u{1D4A5}",Jsercy:"\u0408",Jukcy:"\u0404",KHcy:"\u0425",KJcy:"\u040C",Kappa:"\u039A",Kcedil:"\u0136",Kcy:"\u041A",Kfr:"\u{1D50E}",Kopf:"\u{1D542}",Kscr:"\u{1D4A6}",LJcy:"\u0409",LT:"<",lt:"<",Lacute:"\u0139",Lambda:"\u039B",Lang:"\u27EA",Laplacetrf:"\u2112",Lscr:"\u2112",lagran:"\u2112",Larr:"\u219E",twoheadleftarrow:"\u219E",Lcaron:"\u013D",Lcedil:"\u013B",Lcy:"\u041B",LeftAngleBracket:"\u27E8",lang:"\u27E8",langle:"\u27E8",LeftArrow:"\u2190",ShortLeftArrow:"\u2190",larr:"\u2190",leftarrow:"\u2190",slarr:"\u2190",LeftArrowBar:"\u21E4",larrb:"\u21E4",LeftArrowRightArrow:"\u21C6",leftrightarrows:"\u21C6",lrarr:"\u21C6",LeftCeiling:"\u2308",lceil:"\u2308",LeftDoubleBracket:"\u27E6",lobrk:"\u27E6",LeftDownTeeVector:"\u2961",LeftDownVector:"\u21C3",dharl:"\u21C3",downharpoonleft:"\u21C3",LeftDownVectorBar:"\u2959",LeftFloor:"\u230A",lfloor:"\u230A",LeftRightArrow:"\u2194",harr:"\u2194",leftrightarrow:"\u2194",LeftRightVector:"\u294E",LeftTee:"\u22A3",dashv:"\u22A3",LeftTeeArrow:"\u21A4",mapstoleft:"\u21A4",LeftTeeVector:"\u295A",LeftTriangle:"\u22B2",vartriangleleft:"\u22B2",vltri:"\u22B2",LeftTriangleBar:"\u29CF",LeftTriangleEqual:"\u22B4",ltrie:"\u22B4",trianglelefteq:"\u22B4",LeftUpDownVector:"\u2951",LeftUpTeeVector:"\u2960",LeftUpVector:"\u21BF",uharl:"\u21BF",upharpoonleft:"\u21BF",LeftUpVectorBar:"\u2958",LeftVector:"\u21BC",leftharpoonup:"\u21BC",lharu:"\u21BC",LeftVectorBar:"\u2952",LessEqualGreater:"\u22DA",leg:"\u22DA",lesseqgtr:"\u22DA",LessFullEqual:"\u2266",lE:"\u2266",leqq:"\u2266",LessGreater:"\u2276",lessgtr:"\u2276",lg:"\u2276",LessLess:"\u2AA1",LessSlantEqual:"\u2A7D",leqslant:"\u2A7D",les:"\u2A7D",LessTilde:"\u2272",lesssim:"\u2272",lsim:"\u2272",Lfr:"\u{1D50F}",Ll:"\u22D8",Lleftarrow:"\u21DA",lAarr:"\u21DA",Lmidot:"\u013F",LongLeftArrow:"\u27F5",longleftarrow:"\u27F5",xlarr:"\u27F5",LongLeftRightArrow:"\u27F7",longleftrightarrow:"\u27F7",xharr:"\u27F7",LongRightArrow:"\u27F6",longrightarrow:"\u27F6",xrarr:"\u27F6",Lopf:"\u{1D543}",LowerLeftArrow:"\u2199",swarr:"\u2199",swarrow:"\u2199",LowerRightArrow:"\u2198",searr:"\u2198",searrow:"\u2198",Lsh:"\u21B0",lsh:"\u21B0",Lstrok:"\u0141",Lt:"\u226A",NestedLessLess:"\u226A",ll:"\u226A",Map:"\u2905",Mcy:"\u041C",MediumSpace:"\u205F",Mellintrf:"\u2133",Mscr:"\u2133",phmmat:"\u2133",Mfr:"\u{1D510}",MinusPlus:"\u2213",mnplus:"\u2213",mp:"\u2213",Mopf:"\u{1D544}",Mu:"\u039C",NJcy:"\u040A",Nacute:"\u0143",Ncaron:"\u0147",Ncedil:"\u0145",Ncy:"\u041D",NegativeMediumSpace:"\u200B",NegativeThickSpace:"\u200B",NegativeThinSpace:"\u200B",NegativeVeryThinSpace:"\u200B",ZeroWidthSpace:"\u200B",NewLine:` +`,Nfr:"\u{1D511}",NoBreak:"\u2060",NonBreakingSpace:"\xA0",nbsp:"\xA0",Nopf:"\u2115",naturals:"\u2115",Not:"\u2AEC",NotCongruent:"\u2262",nequiv:"\u2262",NotCupCap:"\u226D",NotDoubleVerticalBar:"\u2226",npar:"\u2226",nparallel:"\u2226",nshortparallel:"\u2226",nspar:"\u2226",NotElement:"\u2209",notin:"\u2209",notinva:"\u2209",NotEqual:"\u2260",ne:"\u2260",NotEqualTilde:"\u2242\u0338",nesim:"\u2242\u0338",NotExists:"\u2204",nexist:"\u2204",nexists:"\u2204",NotGreater:"\u226F",ngt:"\u226F",ngtr:"\u226F",NotGreaterEqual:"\u2271",nge:"\u2271",ngeq:"\u2271",NotGreaterFullEqual:"\u2267\u0338",ngE:"\u2267\u0338",ngeqq:"\u2267\u0338",NotGreaterGreater:"\u226B\u0338",nGtv:"\u226B\u0338",NotGreaterLess:"\u2279",ntgl:"\u2279",NotGreaterSlantEqual:"\u2A7E\u0338",ngeqslant:"\u2A7E\u0338",nges:"\u2A7E\u0338",NotGreaterTilde:"\u2275",ngsim:"\u2275",NotHumpDownHump:"\u224E\u0338",nbump:"\u224E\u0338",NotHumpEqual:"\u224F\u0338",nbumpe:"\u224F\u0338",NotLeftTriangle:"\u22EA",nltri:"\u22EA",ntriangleleft:"\u22EA",NotLeftTriangleBar:"\u29CF\u0338",NotLeftTriangleEqual:"\u22EC",nltrie:"\u22EC",ntrianglelefteq:"\u22EC",NotLess:"\u226E",nless:"\u226E",nlt:"\u226E",NotLessEqual:"\u2270",nle:"\u2270",nleq:"\u2270",NotLessGreater:"\u2278",ntlg:"\u2278",NotLessLess:"\u226A\u0338",nLtv:"\u226A\u0338",NotLessSlantEqual:"\u2A7D\u0338",nleqslant:"\u2A7D\u0338",nles:"\u2A7D\u0338",NotLessTilde:"\u2274",nlsim:"\u2274",NotNestedGreaterGreater:"\u2AA2\u0338",NotNestedLessLess:"\u2AA1\u0338",NotPrecedes:"\u2280",npr:"\u2280",nprec:"\u2280",NotPrecedesEqual:"\u2AAF\u0338",npre:"\u2AAF\u0338",npreceq:"\u2AAF\u0338",NotPrecedesSlantEqual:"\u22E0",nprcue:"\u22E0",NotReverseElement:"\u220C",notni:"\u220C",notniva:"\u220C",NotRightTriangle:"\u22EB",nrtri:"\u22EB",ntriangleright:"\u22EB",NotRightTriangleBar:"\u29D0\u0338",NotRightTriangleEqual:"\u22ED",nrtrie:"\u22ED",ntrianglerighteq:"\u22ED",NotSquareSubset:"\u228F\u0338",NotSquareSubsetEqual:"\u22E2",nsqsube:"\u22E2",NotSquareSuperset:"\u2290\u0338",NotSquareSupersetEqual:"\u22E3",nsqsupe:"\u22E3",NotSubset:"\u2282\u20D2",nsubset:"\u2282\u20D2",vnsub:"\u2282\u20D2",NotSubsetEqual:"\u2288",nsube:"\u2288",nsubseteq:"\u2288",NotSucceeds:"\u2281",nsc:"\u2281",nsucc:"\u2281",NotSucceedsEqual:"\u2AB0\u0338",nsce:"\u2AB0\u0338",nsucceq:"\u2AB0\u0338",NotSucceedsSlantEqual:"\u22E1",nsccue:"\u22E1",NotSucceedsTilde:"\u227F\u0338",NotSuperset:"\u2283\u20D2",nsupset:"\u2283\u20D2",vnsup:"\u2283\u20D2",NotSupersetEqual:"\u2289",nsupe:"\u2289",nsupseteq:"\u2289",NotTilde:"\u2241",nsim:"\u2241",NotTildeEqual:"\u2244",nsime:"\u2244",nsimeq:"\u2244",NotTildeFullEqual:"\u2247",ncong:"\u2247",NotTildeTilde:"\u2249",nap:"\u2249",napprox:"\u2249",NotVerticalBar:"\u2224",nmid:"\u2224",nshortmid:"\u2224",nsmid:"\u2224",Nscr:"\u{1D4A9}",Ntilde:"\xD1",Nu:"\u039D",OElig:"\u0152",Oacute:"\xD3",Ocirc:"\xD4",Ocy:"\u041E",Odblac:"\u0150",Ofr:"\u{1D512}",Ograve:"\xD2",Omacr:"\u014C",Omega:"\u03A9",ohm:"\u03A9",Omicron:"\u039F",Oopf:"\u{1D546}",OpenCurlyDoubleQuote:"\u201C",ldquo:"\u201C",OpenCurlyQuote:"\u2018",lsquo:"\u2018",Or:"\u2A54",Oscr:"\u{1D4AA}",Oslash:"\xD8",Otilde:"\xD5",Otimes:"\u2A37",Ouml:"\xD6",OverBar:"\u203E",oline:"\u203E",OverBrace:"\u23DE",OverBracket:"\u23B4",tbrk:"\u23B4",OverParenthesis:"\u23DC",PartialD:"\u2202",part:"\u2202",Pcy:"\u041F",Pfr:"\u{1D513}",Phi:"\u03A6",Pi:"\u03A0",PlusMinus:"\xB1",plusmn:"\xB1",pm:"\xB1",Popf:"\u2119",primes:"\u2119",Pr:"\u2ABB",Precedes:"\u227A",pr:"\u227A",prec:"\u227A",PrecedesEqual:"\u2AAF",pre:"\u2AAF",preceq:"\u2AAF",PrecedesSlantEqual:"\u227C",prcue:"\u227C",preccurlyeq:"\u227C",PrecedesTilde:"\u227E",precsim:"\u227E",prsim:"\u227E",Prime:"\u2033",Product:"\u220F",prod:"\u220F",Proportional:"\u221D",prop:"\u221D",propto:"\u221D",varpropto:"\u221D",vprop:"\u221D",Pscr:"\u{1D4AB}",Psi:"\u03A8",QUOT:'"',quot:'"',Qfr:"\u{1D514}",Qopf:"\u211A",rationals:"\u211A",Qscr:"\u{1D4AC}",RBarr:"\u2910",drbkarow:"\u2910",REG:"\xAE",circledR:"\xAE",reg:"\xAE",Racute:"\u0154",Rang:"\u27EB",Rarr:"\u21A0",twoheadrightarrow:"\u21A0",Rarrtl:"\u2916",Rcaron:"\u0158",Rcedil:"\u0156",Rcy:"\u0420",Re:"\u211C",Rfr:"\u211C",real:"\u211C",realpart:"\u211C",ReverseElement:"\u220B",SuchThat:"\u220B",ni:"\u220B",niv:"\u220B",ReverseEquilibrium:"\u21CB",leftrightharpoons:"\u21CB",lrhar:"\u21CB",ReverseUpEquilibrium:"\u296F",duhar:"\u296F",Rho:"\u03A1",RightAngleBracket:"\u27E9",rang:"\u27E9",rangle:"\u27E9",RightArrow:"\u2192",ShortRightArrow:"\u2192",rarr:"\u2192",rightarrow:"\u2192",srarr:"\u2192",RightArrowBar:"\u21E5",rarrb:"\u21E5",RightArrowLeftArrow:"\u21C4",rightleftarrows:"\u21C4",rlarr:"\u21C4",RightCeiling:"\u2309",rceil:"\u2309",RightDoubleBracket:"\u27E7",robrk:"\u27E7",RightDownTeeVector:"\u295D",RightDownVector:"\u21C2",dharr:"\u21C2",downharpoonright:"\u21C2",RightDownVectorBar:"\u2955",RightFloor:"\u230B",rfloor:"\u230B",RightTee:"\u22A2",vdash:"\u22A2",RightTeeArrow:"\u21A6",map:"\u21A6",mapsto:"\u21A6",RightTeeVector:"\u295B",RightTriangle:"\u22B3",vartriangleright:"\u22B3",vrtri:"\u22B3",RightTriangleBar:"\u29D0",RightTriangleEqual:"\u22B5",rtrie:"\u22B5",trianglerighteq:"\u22B5",RightUpDownVector:"\u294F",RightUpTeeVector:"\u295C",RightUpVector:"\u21BE",uharr:"\u21BE",upharpoonright:"\u21BE",RightUpVectorBar:"\u2954",RightVector:"\u21C0",rharu:"\u21C0",rightharpoonup:"\u21C0",RightVectorBar:"\u2953",Ropf:"\u211D",reals:"\u211D",RoundImplies:"\u2970",Rrightarrow:"\u21DB",rAarr:"\u21DB",Rscr:"\u211B",realine:"\u211B",Rsh:"\u21B1",rsh:"\u21B1",RuleDelayed:"\u29F4",SHCHcy:"\u0429",SHcy:"\u0428",SOFTcy:"\u042C",Sacute:"\u015A",Sc:"\u2ABC",Scaron:"\u0160",Scedil:"\u015E",Scirc:"\u015C",Scy:"\u0421",Sfr:"\u{1D516}",ShortUpArrow:"\u2191",UpArrow:"\u2191",uarr:"\u2191",uparrow:"\u2191",Sigma:"\u03A3",SmallCircle:"\u2218",compfn:"\u2218",Sopf:"\u{1D54A}",Sqrt:"\u221A",radic:"\u221A",Square:"\u25A1",squ:"\u25A1",square:"\u25A1",SquareIntersection:"\u2293",sqcap:"\u2293",SquareSubset:"\u228F",sqsub:"\u228F",sqsubset:"\u228F",SquareSubsetEqual:"\u2291",sqsube:"\u2291",sqsubseteq:"\u2291",SquareSuperset:"\u2290",sqsup:"\u2290",sqsupset:"\u2290",SquareSupersetEqual:"\u2292",sqsupe:"\u2292",sqsupseteq:"\u2292",SquareUnion:"\u2294",sqcup:"\u2294",Sscr:"\u{1D4AE}",Star:"\u22C6",sstarf:"\u22C6",Sub:"\u22D0",Subset:"\u22D0",SubsetEqual:"\u2286",sube:"\u2286",subseteq:"\u2286",Succeeds:"\u227B",sc:"\u227B",succ:"\u227B",SucceedsEqual:"\u2AB0",sce:"\u2AB0",succeq:"\u2AB0",SucceedsSlantEqual:"\u227D",sccue:"\u227D",succcurlyeq:"\u227D",SucceedsTilde:"\u227F",scsim:"\u227F",succsim:"\u227F",Sum:"\u2211",sum:"\u2211",Sup:"\u22D1",Supset:"\u22D1",Superset:"\u2283",sup:"\u2283",supset:"\u2283",SupersetEqual:"\u2287",supe:"\u2287",supseteq:"\u2287",THORN:"\xDE",TRADE:"\u2122",trade:"\u2122",TSHcy:"\u040B",TScy:"\u0426",Tab:" ",Tau:"\u03A4",Tcaron:"\u0164",Tcedil:"\u0162",Tcy:"\u0422",Tfr:"\u{1D517}",Therefore:"\u2234",there4:"\u2234",therefore:"\u2234",Theta:"\u0398",ThickSpace:"\u205F\u200A",ThinSpace:"\u2009",thinsp:"\u2009",Tilde:"\u223C",sim:"\u223C",thicksim:"\u223C",thksim:"\u223C",TildeEqual:"\u2243",sime:"\u2243",simeq:"\u2243",TildeFullEqual:"\u2245",cong:"\u2245",TildeTilde:"\u2248",ap:"\u2248",approx:"\u2248",asymp:"\u2248",thickapprox:"\u2248",thkap:"\u2248",Topf:"\u{1D54B}",TripleDot:"\u20DB",tdot:"\u20DB",Tscr:"\u{1D4AF}",Tstrok:"\u0166",Uacute:"\xDA",Uarr:"\u219F",Uarrocir:"\u2949",Ubrcy:"\u040E",Ubreve:"\u016C",Ucirc:"\xDB",Ucy:"\u0423",Udblac:"\u0170",Ufr:"\u{1D518}",Ugrave:"\xD9",Umacr:"\u016A",UnderBar:"_",lowbar:"_",UnderBrace:"\u23DF",UnderBracket:"\u23B5",bbrk:"\u23B5",UnderParenthesis:"\u23DD",Union:"\u22C3",bigcup:"\u22C3",xcup:"\u22C3",UnionPlus:"\u228E",uplus:"\u228E",Uogon:"\u0172",Uopf:"\u{1D54C}",UpArrowBar:"\u2912",UpArrowDownArrow:"\u21C5",udarr:"\u21C5",UpDownArrow:"\u2195",updownarrow:"\u2195",varr:"\u2195",UpEquilibrium:"\u296E",udhar:"\u296E",UpTee:"\u22A5",bot:"\u22A5",bottom:"\u22A5",perp:"\u22A5",UpTeeArrow:"\u21A5",mapstoup:"\u21A5",UpperLeftArrow:"\u2196",nwarr:"\u2196",nwarrow:"\u2196",UpperRightArrow:"\u2197",nearr:"\u2197",nearrow:"\u2197",Upsi:"\u03D2",upsih:"\u03D2",Upsilon:"\u03A5",Uring:"\u016E",Uscr:"\u{1D4B0}",Utilde:"\u0168",Uuml:"\xDC",VDash:"\u22AB",Vbar:"\u2AEB",Vcy:"\u0412",Vdash:"\u22A9",Vdashl:"\u2AE6",Vee:"\u22C1",bigvee:"\u22C1",xvee:"\u22C1",Verbar:"\u2016",Vert:"\u2016",VerticalBar:"\u2223",mid:"\u2223",shortmid:"\u2223",smid:"\u2223",VerticalLine:"|",verbar:"|",vert:"|",VerticalSeparator:"\u2758",VerticalTilde:"\u2240",wr:"\u2240",wreath:"\u2240",VeryThinSpace:"\u200A",hairsp:"\u200A",Vfr:"\u{1D519}",Vopf:"\u{1D54D}",Vscr:"\u{1D4B1}",Vvdash:"\u22AA",Wcirc:"\u0174",Wedge:"\u22C0",bigwedge:"\u22C0",xwedge:"\u22C0",Wfr:"\u{1D51A}",Wopf:"\u{1D54E}",Wscr:"\u{1D4B2}",Xfr:"\u{1D51B}",Xi:"\u039E",Xopf:"\u{1D54F}",Xscr:"\u{1D4B3}",YAcy:"\u042F",YIcy:"\u0407",YUcy:"\u042E",Yacute:"\xDD",Ycirc:"\u0176",Ycy:"\u042B",Yfr:"\u{1D51C}",Yopf:"\u{1D550}",Yscr:"\u{1D4B4}",Yuml:"\u0178",ZHcy:"\u0416",Zacute:"\u0179",Zcaron:"\u017D",Zcy:"\u0417",Zdot:"\u017B",Zeta:"\u0396",Zfr:"\u2128",zeetrf:"\u2128",Zopf:"\u2124",integers:"\u2124",Zscr:"\u{1D4B5}",aacute:"\xE1",abreve:"\u0103",ac:"\u223E",mstpos:"\u223E",acE:"\u223E\u0333",acd:"\u223F",acirc:"\xE2",acy:"\u0430",aelig:"\xE6",afr:"\u{1D51E}",agrave:"\xE0",alefsym:"\u2135",aleph:"\u2135",alpha:"\u03B1",amacr:"\u0101",amalg:"\u2A3F",and:"\u2227",wedge:"\u2227",andand:"\u2A55",andd:"\u2A5C",andslope:"\u2A58",andv:"\u2A5A",ang:"\u2220",angle:"\u2220",ange:"\u29A4",angmsd:"\u2221",measuredangle:"\u2221",angmsdaa:"\u29A8",angmsdab:"\u29A9",angmsdac:"\u29AA",angmsdad:"\u29AB",angmsdae:"\u29AC",angmsdaf:"\u29AD",angmsdag:"\u29AE",angmsdah:"\u29AF",angrt:"\u221F",angrtvb:"\u22BE",angrtvbd:"\u299D",angsph:"\u2222",angzarr:"\u237C",aogon:"\u0105",aopf:"\u{1D552}",apE:"\u2A70",apacir:"\u2A6F",ape:"\u224A",approxeq:"\u224A",apid:"\u224B",apos:"'",aring:"\xE5",ascr:"\u{1D4B6}",ast:"*",midast:"*",atilde:"\xE3",auml:"\xE4",awint:"\u2A11",bNot:"\u2AED",backcong:"\u224C",bcong:"\u224C",backepsilon:"\u03F6",bepsi:"\u03F6",backprime:"\u2035",bprime:"\u2035",backsim:"\u223D",bsim:"\u223D",backsimeq:"\u22CD",bsime:"\u22CD",barvee:"\u22BD",barwed:"\u2305",barwedge:"\u2305",bbrktbrk:"\u23B6",bcy:"\u0431",bdquo:"\u201E",ldquor:"\u201E",bemptyv:"\u29B0",beta:"\u03B2",beth:"\u2136",between:"\u226C",twixt:"\u226C",bfr:"\u{1D51F}",bigcirc:"\u25EF",xcirc:"\u25EF",bigodot:"\u2A00",xodot:"\u2A00",bigoplus:"\u2A01",xoplus:"\u2A01",bigotimes:"\u2A02",xotime:"\u2A02",bigsqcup:"\u2A06",xsqcup:"\u2A06",bigstar:"\u2605",starf:"\u2605",bigtriangledown:"\u25BD",xdtri:"\u25BD",bigtriangleup:"\u25B3",xutri:"\u25B3",biguplus:"\u2A04",xuplus:"\u2A04",bkarow:"\u290D",rbarr:"\u290D",blacklozenge:"\u29EB",lozf:"\u29EB",blacktriangle:"\u25B4",utrif:"\u25B4",blacktriangledown:"\u25BE",dtrif:"\u25BE",blacktriangleleft:"\u25C2",ltrif:"\u25C2",blacktriangleright:"\u25B8",rtrif:"\u25B8",blank:"\u2423",blk12:"\u2592",blk14:"\u2591",blk34:"\u2593",block:"\u2588",bne:"=\u20E5",bnequiv:"\u2261\u20E5",bnot:"\u2310",bopf:"\u{1D553}",bowtie:"\u22C8",boxDL:"\u2557",boxDR:"\u2554",boxDl:"\u2556",boxDr:"\u2553",boxH:"\u2550",boxHD:"\u2566",boxHU:"\u2569",boxHd:"\u2564",boxHu:"\u2567",boxUL:"\u255D",boxUR:"\u255A",boxUl:"\u255C",boxUr:"\u2559",boxV:"\u2551",boxVH:"\u256C",boxVL:"\u2563",boxVR:"\u2560",boxVh:"\u256B",boxVl:"\u2562",boxVr:"\u255F",boxbox:"\u29C9",boxdL:"\u2555",boxdR:"\u2552",boxdl:"\u2510",boxdr:"\u250C",boxhD:"\u2565",boxhU:"\u2568",boxhd:"\u252C",boxhu:"\u2534",boxminus:"\u229F",minusb:"\u229F",boxplus:"\u229E",plusb:"\u229E",boxtimes:"\u22A0",timesb:"\u22A0",boxuL:"\u255B",boxuR:"\u2558",boxul:"\u2518",boxur:"\u2514",boxv:"\u2502",boxvH:"\u256A",boxvL:"\u2561",boxvR:"\u255E",boxvh:"\u253C",boxvl:"\u2524",boxvr:"\u251C",brvbar:"\xA6",bscr:"\u{1D4B7}",bsemi:"\u204F",bsol:"\\",bsolb:"\u29C5",bsolhsub:"\u27C8",bull:"\u2022",bullet:"\u2022",bumpE:"\u2AAE",cacute:"\u0107",cap:"\u2229",capand:"\u2A44",capbrcup:"\u2A49",capcap:"\u2A4B",capcup:"\u2A47",capdot:"\u2A40",caps:"\u2229\uFE00",caret:"\u2041",ccaps:"\u2A4D",ccaron:"\u010D",ccedil:"\xE7",ccirc:"\u0109",ccups:"\u2A4C",ccupssm:"\u2A50",cdot:"\u010B",cemptyv:"\u29B2",cent:"\xA2",cfr:"\u{1D520}",chcy:"\u0447",check:"\u2713",checkmark:"\u2713",chi:"\u03C7",cir:"\u25CB",cirE:"\u29C3",circ:"\u02C6",circeq:"\u2257",cire:"\u2257",circlearrowleft:"\u21BA",olarr:"\u21BA",circlearrowright:"\u21BB",orarr:"\u21BB",circledS:"\u24C8",oS:"\u24C8",circledast:"\u229B",oast:"\u229B",circledcirc:"\u229A",ocir:"\u229A",circleddash:"\u229D",odash:"\u229D",cirfnint:"\u2A10",cirmid:"\u2AEF",cirscir:"\u29C2",clubs:"\u2663",clubsuit:"\u2663",colon:":",comma:",",commat:"@",comp:"\u2201",complement:"\u2201",congdot:"\u2A6D",copf:"\u{1D554}",copysr:"\u2117",crarr:"\u21B5",cross:"\u2717",cscr:"\u{1D4B8}",csub:"\u2ACF",csube:"\u2AD1",csup:"\u2AD0",csupe:"\u2AD2",ctdot:"\u22EF",cudarrl:"\u2938",cudarrr:"\u2935",cuepr:"\u22DE",curlyeqprec:"\u22DE",cuesc:"\u22DF",curlyeqsucc:"\u22DF",cularr:"\u21B6",curvearrowleft:"\u21B6",cularrp:"\u293D",cup:"\u222A",cupbrcap:"\u2A48",cupcap:"\u2A46",cupcup:"\u2A4A",cupdot:"\u228D",cupor:"\u2A45",cups:"\u222A\uFE00",curarr:"\u21B7",curvearrowright:"\u21B7",curarrm:"\u293C",curlyvee:"\u22CE",cuvee:"\u22CE",curlywedge:"\u22CF",cuwed:"\u22CF",curren:"\xA4",cwint:"\u2231",cylcty:"\u232D",dHar:"\u2965",dagger:"\u2020",daleth:"\u2138",dash:"\u2010",hyphen:"\u2010",dbkarow:"\u290F",rBarr:"\u290F",dcaron:"\u010F",dcy:"\u0434",ddarr:"\u21CA",downdownarrows:"\u21CA",ddotseq:"\u2A77",eDDot:"\u2A77",deg:"\xB0",delta:"\u03B4",demptyv:"\u29B1",dfisht:"\u297F",dfr:"\u{1D521}",diamondsuit:"\u2666",diams:"\u2666",digamma:"\u03DD",gammad:"\u03DD",disin:"\u22F2",div:"\xF7",divide:"\xF7",divideontimes:"\u22C7",divonx:"\u22C7",djcy:"\u0452",dlcorn:"\u231E",llcorner:"\u231E",dlcrop:"\u230D",dollar:"$",dopf:"\u{1D555}",doteqdot:"\u2251",eDot:"\u2251",dotminus:"\u2238",minusd:"\u2238",dotplus:"\u2214",plusdo:"\u2214",dotsquare:"\u22A1",sdotb:"\u22A1",drcorn:"\u231F",lrcorner:"\u231F",drcrop:"\u230C",dscr:"\u{1D4B9}",dscy:"\u0455",dsol:"\u29F6",dstrok:"\u0111",dtdot:"\u22F1",dtri:"\u25BF",triangledown:"\u25BF",dwangle:"\u29A6",dzcy:"\u045F",dzigrarr:"\u27FF",eacute:"\xE9",easter:"\u2A6E",ecaron:"\u011B",ecir:"\u2256",eqcirc:"\u2256",ecirc:"\xEA",ecolon:"\u2255",eqcolon:"\u2255",ecy:"\u044D",edot:"\u0117",efDot:"\u2252",fallingdotseq:"\u2252",efr:"\u{1D522}",eg:"\u2A9A",egrave:"\xE8",egs:"\u2A96",eqslantgtr:"\u2A96",egsdot:"\u2A98",el:"\u2A99",elinters:"\u23E7",ell:"\u2113",els:"\u2A95",eqslantless:"\u2A95",elsdot:"\u2A97",emacr:"\u0113",empty:"\u2205",emptyset:"\u2205",emptyv:"\u2205",varnothing:"\u2205",emsp13:"\u2004",emsp14:"\u2005",emsp:"\u2003",eng:"\u014B",ensp:"\u2002",eogon:"\u0119",eopf:"\u{1D556}",epar:"\u22D5",eparsl:"\u29E3",eplus:"\u2A71",epsi:"\u03B5",epsilon:"\u03B5",epsiv:"\u03F5",straightepsilon:"\u03F5",varepsilon:"\u03F5",equals:"=",equest:"\u225F",questeq:"\u225F",equivDD:"\u2A78",eqvparsl:"\u29E5",erDot:"\u2253",risingdotseq:"\u2253",erarr:"\u2971",escr:"\u212F",eta:"\u03B7",eth:"\xF0",euml:"\xEB",euro:"\u20AC",excl:"!",fcy:"\u0444",female:"\u2640",ffilig:"\uFB03",fflig:"\uFB00",ffllig:"\uFB04",ffr:"\u{1D523}",filig:"\uFB01",fjlig:"fj",flat:"\u266D",fllig:"\uFB02",fltns:"\u25B1",fnof:"\u0192",fopf:"\u{1D557}",fork:"\u22D4",pitchfork:"\u22D4",forkv:"\u2AD9",fpartint:"\u2A0D",frac12:"\xBD",half:"\xBD",frac13:"\u2153",frac14:"\xBC",frac15:"\u2155",frac16:"\u2159",frac18:"\u215B",frac23:"\u2154",frac25:"\u2156",frac34:"\xBE",frac35:"\u2157",frac38:"\u215C",frac45:"\u2158",frac56:"\u215A",frac58:"\u215D",frac78:"\u215E",frasl:"\u2044",frown:"\u2322",sfrown:"\u2322",fscr:"\u{1D4BB}",gEl:"\u2A8C",gtreqqless:"\u2A8C",gacute:"\u01F5",gamma:"\u03B3",gap:"\u2A86",gtrapprox:"\u2A86",gbreve:"\u011F",gcirc:"\u011D",gcy:"\u0433",gdot:"\u0121",gescc:"\u2AA9",gesdot:"\u2A80",gesdoto:"\u2A82",gesdotol:"\u2A84",gesl:"\u22DB\uFE00",gesles:"\u2A94",gfr:"\u{1D524}",gimel:"\u2137",gjcy:"\u0453",glE:"\u2A92",gla:"\u2AA5",glj:"\u2AA4",gnE:"\u2269",gneqq:"\u2269",gnap:"\u2A8A",gnapprox:"\u2A8A",gne:"\u2A88",gneq:"\u2A88",gnsim:"\u22E7",gopf:"\u{1D558}",gscr:"\u210A",gsime:"\u2A8E",gsiml:"\u2A90",gtcc:"\u2AA7",gtcir:"\u2A7A",gtdot:"\u22D7",gtrdot:"\u22D7",gtlPar:"\u2995",gtquest:"\u2A7C",gtrarr:"\u2978",gvertneqq:"\u2269\uFE00",gvnE:"\u2269\uFE00",hardcy:"\u044A",harrcir:"\u2948",harrw:"\u21AD",leftrightsquigarrow:"\u21AD",hbar:"\u210F",hslash:"\u210F",planck:"\u210F",plankv:"\u210F",hcirc:"\u0125",hearts:"\u2665",heartsuit:"\u2665",hellip:"\u2026",mldr:"\u2026",hercon:"\u22B9",hfr:"\u{1D525}",hksearow:"\u2925",searhk:"\u2925",hkswarow:"\u2926",swarhk:"\u2926",hoarr:"\u21FF",homtht:"\u223B",hookleftarrow:"\u21A9",larrhk:"\u21A9",hookrightarrow:"\u21AA",rarrhk:"\u21AA",hopf:"\u{1D559}",horbar:"\u2015",hscr:"\u{1D4BD}",hstrok:"\u0127",hybull:"\u2043",iacute:"\xED",icirc:"\xEE",icy:"\u0438",iecy:"\u0435",iexcl:"\xA1",ifr:"\u{1D526}",igrave:"\xEC",iiiint:"\u2A0C",qint:"\u2A0C",iiint:"\u222D",tint:"\u222D",iinfin:"\u29DC",iiota:"\u2129",ijlig:"\u0133",imacr:"\u012B",imath:"\u0131",inodot:"\u0131",imof:"\u22B7",imped:"\u01B5",incare:"\u2105",infin:"\u221E",infintie:"\u29DD",intcal:"\u22BA",intercal:"\u22BA",intlarhk:"\u2A17",intprod:"\u2A3C",iprod:"\u2A3C",iocy:"\u0451",iogon:"\u012F",iopf:"\u{1D55A}",iota:"\u03B9",iquest:"\xBF",iscr:"\u{1D4BE}",isinE:"\u22F9",isindot:"\u22F5",isins:"\u22F4",isinsv:"\u22F3",itilde:"\u0129",iukcy:"\u0456",iuml:"\xEF",jcirc:"\u0135",jcy:"\u0439",jfr:"\u{1D527}",jmath:"\u0237",jopf:"\u{1D55B}",jscr:"\u{1D4BF}",jsercy:"\u0458",jukcy:"\u0454",kappa:"\u03BA",kappav:"\u03F0",varkappa:"\u03F0",kcedil:"\u0137",kcy:"\u043A",kfr:"\u{1D528}",kgreen:"\u0138",khcy:"\u0445",kjcy:"\u045C",kopf:"\u{1D55C}",kscr:"\u{1D4C0}",lAtail:"\u291B",lBarr:"\u290E",lEg:"\u2A8B",lesseqqgtr:"\u2A8B",lHar:"\u2962",lacute:"\u013A",laemptyv:"\u29B4",lambda:"\u03BB",langd:"\u2991",lap:"\u2A85",lessapprox:"\u2A85",laquo:"\xAB",larrbfs:"\u291F",larrfs:"\u291D",larrlp:"\u21AB",looparrowleft:"\u21AB",larrpl:"\u2939",larrsim:"\u2973",larrtl:"\u21A2",leftarrowtail:"\u21A2",lat:"\u2AAB",latail:"\u2919",late:"\u2AAD",lates:"\u2AAD\uFE00",lbarr:"\u290C",lbbrk:"\u2772",lbrace:"{",lcub:"{",lbrack:"[",lsqb:"[",lbrke:"\u298B",lbrksld:"\u298F",lbrkslu:"\u298D",lcaron:"\u013E",lcedil:"\u013C",lcy:"\u043B",ldca:"\u2936",ldrdhar:"\u2967",ldrushar:"\u294B",ldsh:"\u21B2",le:"\u2264",leq:"\u2264",leftleftarrows:"\u21C7",llarr:"\u21C7",leftthreetimes:"\u22CB",lthree:"\u22CB",lescc:"\u2AA8",lesdot:"\u2A7F",lesdoto:"\u2A81",lesdotor:"\u2A83",lesg:"\u22DA\uFE00",lesges:"\u2A93",lessdot:"\u22D6",ltdot:"\u22D6",lfisht:"\u297C",lfr:"\u{1D529}",lgE:"\u2A91",lharul:"\u296A",lhblk:"\u2584",ljcy:"\u0459",llhard:"\u296B",lltri:"\u25FA",lmidot:"\u0140",lmoust:"\u23B0",lmoustache:"\u23B0",lnE:"\u2268",lneqq:"\u2268",lnap:"\u2A89",lnapprox:"\u2A89",lne:"\u2A87",lneq:"\u2A87",lnsim:"\u22E6",loang:"\u27EC",loarr:"\u21FD",longmapsto:"\u27FC",xmap:"\u27FC",looparrowright:"\u21AC",rarrlp:"\u21AC",lopar:"\u2985",lopf:"\u{1D55D}",loplus:"\u2A2D",lotimes:"\u2A34",lowast:"\u2217",loz:"\u25CA",lozenge:"\u25CA",lpar:"(",lparlt:"\u2993",lrhard:"\u296D",lrm:"\u200E",lrtri:"\u22BF",lsaquo:"\u2039",lscr:"\u{1D4C1}",lsime:"\u2A8D",lsimg:"\u2A8F",lsquor:"\u201A",sbquo:"\u201A",lstrok:"\u0142",ltcc:"\u2AA6",ltcir:"\u2A79",ltimes:"\u22C9",ltlarr:"\u2976",ltquest:"\u2A7B",ltrPar:"\u2996",ltri:"\u25C3",triangleleft:"\u25C3",lurdshar:"\u294A",luruhar:"\u2966",lvertneqq:"\u2268\uFE00",lvnE:"\u2268\uFE00",mDDot:"\u223A",macr:"\xAF",strns:"\xAF",male:"\u2642",malt:"\u2720",maltese:"\u2720",marker:"\u25AE",mcomma:"\u2A29",mcy:"\u043C",mdash:"\u2014",mfr:"\u{1D52A}",mho:"\u2127",micro:"\xB5",midcir:"\u2AF0",minus:"\u2212",minusdu:"\u2A2A",mlcp:"\u2ADB",models:"\u22A7",mopf:"\u{1D55E}",mscr:"\u{1D4C2}",mu:"\u03BC",multimap:"\u22B8",mumap:"\u22B8",nGg:"\u22D9\u0338",nGt:"\u226B\u20D2",nLeftarrow:"\u21CD",nlArr:"\u21CD",nLeftrightarrow:"\u21CE",nhArr:"\u21CE",nLl:"\u22D8\u0338",nLt:"\u226A\u20D2",nRightarrow:"\u21CF",nrArr:"\u21CF",nVDash:"\u22AF",nVdash:"\u22AE",nacute:"\u0144",nang:"\u2220\u20D2",napE:"\u2A70\u0338",napid:"\u224B\u0338",napos:"\u0149",natur:"\u266E",natural:"\u266E",ncap:"\u2A43",ncaron:"\u0148",ncedil:"\u0146",ncongdot:"\u2A6D\u0338",ncup:"\u2A42",ncy:"\u043D",ndash:"\u2013",neArr:"\u21D7",nearhk:"\u2924",nedot:"\u2250\u0338",nesear:"\u2928",toea:"\u2928",nfr:"\u{1D52B}",nharr:"\u21AE",nleftrightarrow:"\u21AE",nhpar:"\u2AF2",nis:"\u22FC",nisd:"\u22FA",njcy:"\u045A",nlE:"\u2266\u0338",nleqq:"\u2266\u0338",nlarr:"\u219A",nleftarrow:"\u219A",nldr:"\u2025",nopf:"\u{1D55F}",not:"\xAC",notinE:"\u22F9\u0338",notindot:"\u22F5\u0338",notinvb:"\u22F7",notinvc:"\u22F6",notnivb:"\u22FE",notnivc:"\u22FD",nparsl:"\u2AFD\u20E5",npart:"\u2202\u0338",npolint:"\u2A14",nrarr:"\u219B",nrightarrow:"\u219B",nrarrc:"\u2933\u0338",nrarrw:"\u219D\u0338",nscr:"\u{1D4C3}",nsub:"\u2284",nsubE:"\u2AC5\u0338",nsubseteqq:"\u2AC5\u0338",nsup:"\u2285",nsupE:"\u2AC6\u0338",nsupseteqq:"\u2AC6\u0338",ntilde:"\xF1",nu:"\u03BD",num:"#",numero:"\u2116",numsp:"\u2007",nvDash:"\u22AD",nvHarr:"\u2904",nvap:"\u224D\u20D2",nvdash:"\u22AC",nvge:"\u2265\u20D2",nvgt:">\u20D2",nvinfin:"\u29DE",nvlArr:"\u2902",nvle:"\u2264\u20D2",nvlt:"<\u20D2",nvltrie:"\u22B4\u20D2",nvrArr:"\u2903",nvrtrie:"\u22B5\u20D2",nvsim:"\u223C\u20D2",nwArr:"\u21D6",nwarhk:"\u2923",nwnear:"\u2927",oacute:"\xF3",ocirc:"\xF4",ocy:"\u043E",odblac:"\u0151",odiv:"\u2A38",odsold:"\u29BC",oelig:"\u0153",ofcir:"\u29BF",ofr:"\u{1D52C}",ogon:"\u02DB",ograve:"\xF2",ogt:"\u29C1",ohbar:"\u29B5",olcir:"\u29BE",olcross:"\u29BB",olt:"\u29C0",omacr:"\u014D",omega:"\u03C9",omicron:"\u03BF",omid:"\u29B6",oopf:"\u{1D560}",opar:"\u29B7",operp:"\u29B9",or:"\u2228",vee:"\u2228",ord:"\u2A5D",order:"\u2134",orderof:"\u2134",oscr:"\u2134",ordf:"\xAA",ordm:"\xBA",origof:"\u22B6",oror:"\u2A56",orslope:"\u2A57",orv:"\u2A5B",oslash:"\xF8",osol:"\u2298",otilde:"\xF5",otimesas:"\u2A36",ouml:"\xF6",ovbar:"\u233D",para:"\xB6",parsim:"\u2AF3",parsl:"\u2AFD",pcy:"\u043F",percnt:"%",period:".",permil:"\u2030",pertenk:"\u2031",pfr:"\u{1D52D}",phi:"\u03C6",phiv:"\u03D5",straightphi:"\u03D5",varphi:"\u03D5",phone:"\u260E",pi:"\u03C0",piv:"\u03D6",varpi:"\u03D6",planckh:"\u210E",plus:"+",plusacir:"\u2A23",pluscir:"\u2A22",plusdu:"\u2A25",pluse:"\u2A72",plussim:"\u2A26",plustwo:"\u2A27",pointint:"\u2A15",popf:"\u{1D561}",pound:"\xA3",prE:"\u2AB3",prap:"\u2AB7",precapprox:"\u2AB7",precnapprox:"\u2AB9",prnap:"\u2AB9",precneqq:"\u2AB5",prnE:"\u2AB5",precnsim:"\u22E8",prnsim:"\u22E8",prime:"\u2032",profalar:"\u232E",profline:"\u2312",profsurf:"\u2313",prurel:"\u22B0",pscr:"\u{1D4C5}",psi:"\u03C8",puncsp:"\u2008",qfr:"\u{1D52E}",qopf:"\u{1D562}",qprime:"\u2057",qscr:"\u{1D4C6}",quatint:"\u2A16",quest:"?",rAtail:"\u291C",rHar:"\u2964",race:"\u223D\u0331",racute:"\u0155",raemptyv:"\u29B3",rangd:"\u2992",range:"\u29A5",raquo:"\xBB",rarrap:"\u2975",rarrbfs:"\u2920",rarrc:"\u2933",rarrfs:"\u291E",rarrpl:"\u2945",rarrsim:"\u2974",rarrtl:"\u21A3",rightarrowtail:"\u21A3",rarrw:"\u219D",rightsquigarrow:"\u219D",ratail:"\u291A",ratio:"\u2236",rbbrk:"\u2773",rbrace:"}",rcub:"}",rbrack:"]",rsqb:"]",rbrke:"\u298C",rbrksld:"\u298E",rbrkslu:"\u2990",rcaron:"\u0159",rcedil:"\u0157",rcy:"\u0440",rdca:"\u2937",rdldhar:"\u2969",rdsh:"\u21B3",rect:"\u25AD",rfisht:"\u297D",rfr:"\u{1D52F}",rharul:"\u296C",rho:"\u03C1",rhov:"\u03F1",varrho:"\u03F1",rightrightarrows:"\u21C9",rrarr:"\u21C9",rightthreetimes:"\u22CC",rthree:"\u22CC",ring:"\u02DA",rlm:"\u200F",rmoust:"\u23B1",rmoustache:"\u23B1",rnmid:"\u2AEE",roang:"\u27ED",roarr:"\u21FE",ropar:"\u2986",ropf:"\u{1D563}",roplus:"\u2A2E",rotimes:"\u2A35",rpar:")",rpargt:"\u2994",rppolint:"\u2A12",rsaquo:"\u203A",rscr:"\u{1D4C7}",rtimes:"\u22CA",rtri:"\u25B9",triangleright:"\u25B9",rtriltri:"\u29CE",ruluhar:"\u2968",rx:"\u211E",sacute:"\u015B",scE:"\u2AB4",scap:"\u2AB8",succapprox:"\u2AB8",scaron:"\u0161",scedil:"\u015F",scirc:"\u015D",scnE:"\u2AB6",succneqq:"\u2AB6",scnap:"\u2ABA",succnapprox:"\u2ABA",scnsim:"\u22E9",succnsim:"\u22E9",scpolint:"\u2A13",scy:"\u0441",sdot:"\u22C5",sdote:"\u2A66",seArr:"\u21D8",sect:"\xA7",semi:";",seswar:"\u2929",tosa:"\u2929",sext:"\u2736",sfr:"\u{1D530}",sharp:"\u266F",shchcy:"\u0449",shcy:"\u0448",shy:"\xAD",sigma:"\u03C3",sigmaf:"\u03C2",sigmav:"\u03C2",varsigma:"\u03C2",simdot:"\u2A6A",simg:"\u2A9E",simgE:"\u2AA0",siml:"\u2A9D",simlE:"\u2A9F",simne:"\u2246",simplus:"\u2A24",simrarr:"\u2972",smashp:"\u2A33",smeparsl:"\u29E4",smile:"\u2323",ssmile:"\u2323",smt:"\u2AAA",smte:"\u2AAC",smtes:"\u2AAC\uFE00",softcy:"\u044C",sol:"/",solb:"\u29C4",solbar:"\u233F",sopf:"\u{1D564}",spades:"\u2660",spadesuit:"\u2660",sqcaps:"\u2293\uFE00",sqcups:"\u2294\uFE00",sscr:"\u{1D4C8}",star:"\u2606",sub:"\u2282",subset:"\u2282",subE:"\u2AC5",subseteqq:"\u2AC5",subdot:"\u2ABD",subedot:"\u2AC3",submult:"\u2AC1",subnE:"\u2ACB",subsetneqq:"\u2ACB",subne:"\u228A",subsetneq:"\u228A",subplus:"\u2ABF",subrarr:"\u2979",subsim:"\u2AC7",subsub:"\u2AD5",subsup:"\u2AD3",sung:"\u266A",sup1:"\xB9",sup2:"\xB2",sup3:"\xB3",supE:"\u2AC6",supseteqq:"\u2AC6",supdot:"\u2ABE",supdsub:"\u2AD8",supedot:"\u2AC4",suphsol:"\u27C9",suphsub:"\u2AD7",suplarr:"\u297B",supmult:"\u2AC2",supnE:"\u2ACC",supsetneqq:"\u2ACC",supne:"\u228B",supsetneq:"\u228B",supplus:"\u2AC0",supsim:"\u2AC8",supsub:"\u2AD4",supsup:"\u2AD6",swArr:"\u21D9",swnwar:"\u292A",szlig:"\xDF",target:"\u2316",tau:"\u03C4",tcaron:"\u0165",tcedil:"\u0163",tcy:"\u0442",telrec:"\u2315",tfr:"\u{1D531}",theta:"\u03B8",thetasym:"\u03D1",thetav:"\u03D1",vartheta:"\u03D1",thorn:"\xFE",times:"\xD7",timesbar:"\u2A31",timesd:"\u2A30",topbot:"\u2336",topcir:"\u2AF1",topf:"\u{1D565}",topfork:"\u2ADA",tprime:"\u2034",triangle:"\u25B5",utri:"\u25B5",triangleq:"\u225C",trie:"\u225C",tridot:"\u25EC",triminus:"\u2A3A",triplus:"\u2A39",trisb:"\u29CD",tritime:"\u2A3B",trpezium:"\u23E2",tscr:"\u{1D4C9}",tscy:"\u0446",tshcy:"\u045B",tstrok:"\u0167",uHar:"\u2963",uacute:"\xFA",ubrcy:"\u045E",ubreve:"\u016D",ucirc:"\xFB",ucy:"\u0443",udblac:"\u0171",ufisht:"\u297E",ufr:"\u{1D532}",ugrave:"\xF9",uhblk:"\u2580",ulcorn:"\u231C",ulcorner:"\u231C",ulcrop:"\u230F",ultri:"\u25F8",umacr:"\u016B",uogon:"\u0173",uopf:"\u{1D566}",upsi:"\u03C5",upsilon:"\u03C5",upuparrows:"\u21C8",uuarr:"\u21C8",urcorn:"\u231D",urcorner:"\u231D",urcrop:"\u230E",uring:"\u016F",urtri:"\u25F9",uscr:"\u{1D4CA}",utdot:"\u22F0",utilde:"\u0169",uuml:"\xFC",uwangle:"\u29A7",vBar:"\u2AE8",vBarv:"\u2AE9",vangrt:"\u299C",varsubsetneq:"\u228A\uFE00",vsubne:"\u228A\uFE00",varsubsetneqq:"\u2ACB\uFE00",vsubnE:"\u2ACB\uFE00",varsupsetneq:"\u228B\uFE00",vsupne:"\u228B\uFE00",varsupsetneqq:"\u2ACC\uFE00",vsupnE:"\u2ACC\uFE00",vcy:"\u0432",veebar:"\u22BB",veeeq:"\u225A",vellip:"\u22EE",vfr:"\u{1D533}",vopf:"\u{1D567}",vscr:"\u{1D4CB}",vzigzag:"\u299A",wcirc:"\u0175",wedbar:"\u2A5F",wedgeq:"\u2259",weierp:"\u2118",wp:"\u2118",wfr:"\u{1D534}",wopf:"\u{1D568}",wscr:"\u{1D4CC}",xfr:"\u{1D535}",xi:"\u03BE",xnis:"\u22FB",xopf:"\u{1D569}",xscr:"\u{1D4CD}",yacute:"\xFD",yacy:"\u044F",ycirc:"\u0177",ycy:"\u044B",yen:"\xA5",yfr:"\u{1D536}",yicy:"\u0457",yopf:"\u{1D56A}",yscr:"\u{1D4CE}",yucy:"\u044E",yuml:"\xFF",zacute:"\u017A",zcaron:"\u017E",zcy:"\u0437",zdot:"\u017C",zeta:"\u03B6",zfr:"\u{1D537}",zhcy:"\u0436",zigrarr:"\u21DD",zopf:"\u{1D56B}",zscr:"\u{1D4CF}",zwj:"\u200D",zwnj:"\u200C"},oF="\uE500";M0.ngsp=oF;var QP=class{tokens;errors;nonNormalizedIcuExpressions;constructor(o,e,t){this.tokens=o,this.errors=e,this.nonNormalizedIcuExpressions=t}};function mq(n,o,e,t={}){let i=new XP(new Jx(n,o),e,t);return i.tokenize(),new QP(bq(i.tokens),i.errors,i.nonNormalizedIcuExpressions)}var uq=/\r\n?/g;function xg(n){return`Unexpected character "${n===ia?"EOF":String.fromCharCode(n)}"`}function s6(n){return`Unknown entity "${n}" - use the "&#;" or "&#x;" syntax`}function gq(n,o){return`Unable to parse entity "${o}" - ${n} character reference entities must end with ";"`}var KP=(function(n){return n.HEX="hexadecimal",n.DEC="decimal",n})(KP||{}),hq=["@if","@else","@for","@switch","@case","@default","@empty","@defer","@placeholder","@loading","@error"],mf={start:"{{",end:"}}"},XP=class{_getTagDefinition;_cursor;_tokenizeIcu;_leadingTriviaCodePoints;_currentTokenStart=null;_currentTokenType=null;_expansionCaseStack=[];_openDirectiveCount=0;_inInterpolation=!1;_preserveLineEndings;_i18nNormalizeLineEndingsInICUs;_tokenizeBlocks;_tokenizeLet;_selectorlessEnabled;tokens=[];errors=[];nonNormalizedIcuExpressions=[];constructor(o,e,t){this._getTagDefinition=e,this._tokenizeIcu=t.tokenizeExpansionForms||!1,this._leadingTriviaCodePoints=t.leadingTriviaChars&&t.leadingTriviaChars.map(r=>r.codePointAt(0)||0);let i=t.range||{endPos:o.content.length,startPos:0,startLine:0,startCol:0};this._cursor=t.escapedString?new YP(o,i):new h1(o,i),this._preserveLineEndings=t.preserveLineEndings||!1,this._i18nNormalizeLineEndingsInICUs=t.i18nNormalizeLineEndingsInICUs||!1,this._tokenizeBlocks=t.tokenizeBlocks??!0,this._tokenizeLet=t.tokenizeLet??!0,this._selectorlessEnabled=t.selectorlessEnabled??!1;try{this._cursor.init()}catch(r){this.handleError(r)}}_processCarriageReturns(o){return this._preserveLineEndings?o:o.replace(uq,` +`)}tokenize(){for(;this._cursor.peek()!==ia;){let o=this._cursor.clone();try{this._attemptCharCode(Ig)?this._attemptCharCode(TP)?this._attemptCharCode(Nc)?this._consumeCdata(o):this._attemptCharCode(Yx)?this._consumeComment(o):this._consumeDocType(o):this._attemptCharCode(ul)?this._consumeTagClose(o):this._consumeTagOpen(o):this._tokenizeLet&&this._cursor.peek()===fg&&!this._inInterpolation&&this._isLetStart()?this._consumeLetDeclaration(o):this._tokenizeBlocks&&this._isBlockStart()?this._consumeBlockStart(o):this._tokenizeBlocks&&!this._inInterpolation&&!this._isInExpansionCase()&&!this._isInExpansionForm()&&this._attemptCharCode(Ka)?this._consumeBlockEnd(o):this._tokenizeIcu&&this._tokenizeExpansionForm()||this._consumeWithInterpolation(5,8,()=>this._isTextEnd(),()=>this._isTagStart())}catch(e){this.handleError(e)}}this._beginToken(41),this._endToken([])}_getBlockName(){let o=!1,e=this._cursor.clone();return this._attemptCharCodeUntilFn(t=>p0(t)?!o:yq(t)?(o=!0,!1):!0),this._cursor.getChars(e).trim()}_consumeBlockStart(o){this._requireCharCode(fg),this._beginToken(24,o);let e=this._endToken([this._getBlockName()]);if(e.parts[0]==="default never"&&this._attemptCharCode(fs)){this._beginToken(25),this._endToken([]),this._beginToken(26),this._endToken([]);return}if(this._cursor.peek()===Ya)if(this._cursor.advance(),this._consumeBlockParameters(),this._attemptCharCodeUntilFn(Bo),this._attemptCharCode(Pr))this._attemptCharCodeUntilFn(Bo);else{e.type=28;return}this._attemptCharCode(_l)?(this._beginToken(25),this._endToken([])):this._isBlockStart()&&(e.parts[0]==="case"||e.parts[0]==="default")?(this._beginToken(25),this._endToken([]),this._beginToken(26),this._endToken([])):e.type=28}_consumeBlockEnd(o){this._beginToken(26,o),this._endToken([])}_consumeBlockParameters(){for(this._attemptCharCodeUntilFn(c6);this._cursor.peek()!==Pr&&this._cursor.peek()!==ia;){this._beginToken(27);let o=this._cursor.clone(),e=null,t=0;for(;this._cursor.peek()!==fs&&this._cursor.peek()!==ia||e!==null;){let i=this._cursor.peek();if(i===Km)this._cursor.advance();else if(i===e)e=null;else if(e===null&&Pf(i))e=i;else if(i===Ya&&e===null)t++;else if(i===Pr&&e===null){if(t===0)break;t>0&&t--}this._cursor.advance()}this._endToken([this._cursor.getChars(o)]),this._attemptCharCodeUntilFn(c6)}}_consumeLetDeclaration(o){if(this._requireStr("@let"),this._beginToken(29,o),p0(this._cursor.peek()))this._attemptCharCodeUntilFn(Bo);else{let i=this._endToken([this._cursor.getChars(o)]);i.type=32;return}let e=this._endToken([this._getLetDeclarationName()]);if(this._attemptCharCodeUntilFn(Bo),!this._attemptCharCode(ta)){e.type=32;return}this._attemptCharCodeUntilFn(i=>Bo(i)&&!Zx(i)),this._consumeLetDeclarationValue(),this._cursor.peek()===fs?(this._beginToken(31),this._cursor.advance(),this._endToken([])):(e.type=32,e.sourceSpan=this._cursor.getSpan(o))}_getLetDeclarationName(){let o=this._cursor.clone(),e=!1;return this._attemptCharCodeUntilFn(t=>Pp(t)||t===D1||t===Ap||e&&gl(t)?(e=!0,!1):!0),this._cursor.getChars(o).trim()}_consumeLetDeclarationValue(){let o=this._cursor.clone();for(this._beginToken(30,o);this._cursor.peek()!==ia;){let e=this._cursor.peek();if(e===fs)break;Pf(e)&&(this._cursor.advance(),this._attemptCharCodeUntilFn(t=>t===Km?(this._cursor.advance(),!1):t===e)),this._cursor.advance()}this._endToken([this._cursor.getChars(o)])}_tokenizeExpansionForm(){if(this.isExpansionFormStart())return this._consumeExpansionFormStart(),!0;if(vq(this._cursor.peek())&&this._isInExpansionForm())return this._consumeExpansionCaseStart(),!0;if(this._cursor.peek()===Ka){if(this._isInExpansionCase())return this._consumeExpansionCaseEnd(),!0;if(this._isInExpansionForm())return this._consumeExpansionFormEnd(),!0}return!1}_beginToken(o,e=this._cursor.clone()){this._currentTokenStart=e,this._currentTokenType=o}_endToken(o,e){if(this._currentTokenStart===null)throw new dn(this._cursor.getSpan(e),"Programming error - attempted to end a token when there was no start to the token");if(this._currentTokenType===null)throw new dn(this._cursor.getSpan(this._currentTokenStart),"Programming error - attempted to end a token which has no token type");let t={type:this._currentTokenType,parts:o,sourceSpan:(e??this._cursor).getSpan(this._currentTokenStart,this._leadingTriviaCodePoints)};return this.tokens.push(t),this._currentTokenStart=null,this._currentTokenType=null,t}_createError(o,e){this._isInExpansionForm()&&(o+=` (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)`);let t=new dn(e,o);return this._currentTokenStart=null,this._currentTokenType=null,t}handleError(o){if(o instanceof S0&&(o=this._createError(o.msg,this._cursor.getSpan(o.cursor))),o instanceof dn)this.errors.push(o);else throw o}_attemptCharCode(o){return this._cursor.peek()===o?(this._cursor.advance(),!0):!1}_attemptCharCodeCaseInsensitive(o){return xq(this._cursor.peek(),o)?(this._cursor.advance(),!0):!1}_requireCharCode(o){let e=this._cursor.clone();if(!this._attemptCharCode(o))throw this._createError(xg(this._cursor.peek()),this._cursor.getSpan(e))}_attemptStr(o){let e=o.length;if(this._cursor.charsLeft()this._peekStr(o))}_isLetStart(){return this._cursor.peek()===fg&&this._peekStr("@let")}_consumeEntity(o){this._beginToken(9);let e=this._cursor.clone();if(this._cursor.advance(),this._attemptCharCode(TN)){let t=this._attemptCharCode(RN)||this._attemptCharCode(DU),i=this._cursor.clone();if(this._attemptCharCodeUntilFn(fq),this._cursor.peek()!=fs){this._cursor.advance();let s=t?KP.HEX:KP.DEC;throw this._createError(gq(s,this._cursor.getChars(e)),this._cursor.getSpan())}let r=this._cursor.getChars(i);this._cursor.advance();try{let s=parseInt(r,t?16:10);this._endToken([String.fromCodePoint(s),this._cursor.getChars(e)])}catch{throw this._createError(s6(this._cursor.getChars(e)),this._cursor.getSpan())}}else{let t=this._cursor.clone();if(this._attemptCharCodeUntilFn(Cq),this._cursor.peek()!=fs)this._beginToken(o,e),this._cursor=t,this._endToken(["&"]);else{let i=this._cursor.getChars(t);this._cursor.advance();let r=M0.hasOwnProperty(i)&&M0[i];if(!r)throw this._createError(s6(i),this._cursor.getSpan(e));this._endToken([r,`&${i};`])}}}_consumeRawText(o,e){this._beginToken(o?6:7);let t=[];for(;;){let i=this._cursor.clone(),r=e();if(this._cursor=i,r)break;o&&this._cursor.peek()===Xx?(this._endToken([this._processCarriageReturns(t.join(""))]),t.length=0,this._consumeEntity(6),this._beginToken(6)):t.push(this._readChar())}this._endToken([this._processCarriageReturns(t.join(""))])}_consumeComment(o){this._beginToken(10,o),this._requireCharCode(Yx),this._endToken([]),this._consumeRawText(!1,()=>this._attemptStr("-->")),this._beginToken(11),this._requireStr("-->"),this._endToken([])}_consumeCdata(o){this._beginToken(12,o),this._requireStr("CDATA["),this._endToken([]),this._consumeRawText(!1,()=>this._attemptStr("]]>")),this._beginToken(13),this._requireStr("]]>"),this._endToken([])}_consumeDocType(o){this._beginToken(18,o);let e=this._cursor.clone();this._attemptUntilChar(Us);let t=this._cursor.getChars(e);this._cursor.advance(),this._endToken([t])}_consumePrefixAndName(o){let e=this._cursor.clone(),t="";for(;this._cursor.peek()!==Dc&&!_q(this._cursor.peek());)this._cursor.advance();let i;this._cursor.peek()===Dc?(t=this._cursor.getChars(e),this._cursor.advance(),i=this._cursor.clone()):i=e,this._requireCharCodeUntilFn(o,t===""?0:1);let r=this._cursor.getChars(i);return[t,r]}_consumeTagOpen(o){let e,t,i,r;try{if(this._selectorlessEnabled&&rx(this._cursor.peek()))r=this._consumeComponentOpenStart(o),[i,t,e]=r.parts,t&&(i+=`:${t}`),e&&(i+=`:${e}`),this._attemptCharCodeUntilFn(Bo);else{if(!Pp(this._cursor.peek()))throw this._createError(xg(this._cursor.peek()),this._cursor.getSpan(o));r=this._consumeTagOpenStart(o),t=r.parts[0],e=i=r.parts[1],this._attemptCharCodeUntilFn(Bo)}for(;!p6(this._cursor.peek());)if(this._selectorlessEnabled&&this._cursor.peek()===fg){let d=this._cursor.clone(),m=d.clone();m.advance(),rx(m.peek())&&this._consumeDirective(d,m)}else this._consumeAttribute();r.type===33?this._consumeComponentOpenEnd():this._consumeTagOpenEnd()}catch(d){if(d instanceof dn){r?r.type=r.type===33?37:4:(this._beginToken(5,o),this._endToken(["<"]));return}throw d}let s=this._getTagDefinition(e).getContentType(t);s===Oc.RAW_TEXT?this._consumeRawTextWithTagClose(r,i,!1):s===Oc.ESCAPABLE_RAW_TEXT&&this._consumeRawTextWithTagClose(r,i,!0)}_consumeRawTextWithTagClose(o,e,t){this._consumeRawText(t,()=>!this._attemptCharCode(Ig)||!this._attemptCharCode(ul)||(this._attemptCharCodeUntilFn(Bo),!this._attemptStrCaseInsensitive(e))?!1:(this._attemptCharCodeUntilFn(Bo),this._attemptCharCode(Us))),this._beginToken(o.type===33?36:3),this._requireCharCodeUntilFn(i=>i===Us,3),this._cursor.advance(),this._endToken(o.parts)}_consumeTagOpenStart(o){this._beginToken(0,o);let e=this._consumePrefixAndName(Rm);return this._endToken(e)}_consumeComponentOpenStart(o){this._beginToken(33,o);let e=this._consumeComponentName();return this._endToken(e)}_consumeComponentName(){let o=this._cursor.clone();for(;d6(this._cursor.peek());)this._cursor.advance();let e=this._cursor.getChars(o),t="",i="";return this._cursor.peek()===Dc&&(this._cursor.advance(),[t,i]=this._consumePrefixAndName(Rm)),[e,t,i]}_consumeAttribute(){this._consumeAttributeName(),this._attemptCharCodeUntilFn(Bo),this._attemptCharCode(ta)&&(this._attemptCharCodeUntilFn(Bo),this._consumeAttributeValue()),this._attemptCharCodeUntilFn(Bo)}_consumeAttributeName(){let o=this._cursor.peek();if(o===d0||o===c0)throw this._createError(xg(o),this._cursor.getSpan());this._beginToken(14);let e;if(this._openDirectiveCount>0){let i=0;e=r=>{if(this._openDirectiveCount>0){if(r===Ya)i++;else if(r===Pr){if(i===0)return!0;i--}}return Rm(r)}}else if(o===Nc){let i=0;e=r=>(r===Nc?i++:r===Pd&&i--,i<=0?Rm(r):Zx(r))}else e=Rm;let t=this._consumePrefixAndName(e);this._endToken(t)}_consumeAttributeValue(){if(this._cursor.peek()===d0||this._cursor.peek()===c0){let o=this._cursor.peek();this._consumeQuote(o);let e=()=>this._cursor.peek()===o;this._consumeWithInterpolation(16,17,e,e),this._consumeQuote(o)}else{let o=()=>Rm(this._cursor.peek());this._consumeWithInterpolation(16,17,o,o)}}_consumeQuote(o){this._beginToken(15),this._requireCharCode(o),this._endToken([String.fromCodePoint(o)])}_consumeTagOpenEnd(){let o=this._attemptCharCode(ul)?2:1;this._beginToken(o),this._requireCharCode(Us),this._endToken([])}_consumeComponentOpenEnd(){let o=this._attemptCharCode(ul)?35:34;this._beginToken(o),this._requireCharCode(Us),this._endToken([])}_consumeTagClose(o){if(this._selectorlessEnabled){let t=o.clone();for(;t.peek()!==Us&&!rx(t.peek());)t.advance();if(rx(t.peek())){this._beginToken(36,o);let i=this._consumeComponentName();this._attemptCharCodeUntilFn(Bo),this._requireCharCode(Us),this._endToken(i);return}}this._beginToken(3,o),this._attemptCharCodeUntilFn(Bo);let e=this._consumePrefixAndName(Rm);this._attemptCharCodeUntilFn(Bo),this._requireCharCode(Us),this._endToken(e)}_consumeExpansionFormStart(){this._beginToken(19),this._requireCharCode(_l),this._endToken([]),this._expansionCaseStack.push(19),this._beginToken(7);let o=this._readUntil(Oa),e=this._processCarriageReturns(o);if(this._i18nNormalizeLineEndingsInICUs)this._endToken([e]);else{let i=this._endToken([o]);e!==o&&this.nonNormalizedIcuExpressions.push(i)}this._requireCharCode(Oa),this._attemptCharCodeUntilFn(Bo),this._beginToken(7);let t=this._readUntil(Oa);this._endToken([t]),this._requireCharCode(Oa),this._attemptCharCodeUntilFn(Bo)}_consumeExpansionCaseStart(){this._beginToken(20);let o=this._readUntil(_l).trim();this._endToken([o]),this._attemptCharCodeUntilFn(Bo),this._beginToken(21),this._requireCharCode(_l),this._endToken([]),this._attemptCharCodeUntilFn(Bo),this._expansionCaseStack.push(21)}_consumeExpansionCaseEnd(){this._beginToken(22),this._requireCharCode(Ka),this._endToken([]),this._attemptCharCodeUntilFn(Bo),this._expansionCaseStack.pop()}_consumeExpansionFormEnd(){this._beginToken(23),this._requireCharCode(Ka),this._endToken([]),this._expansionCaseStack.pop()}_consumeWithInterpolation(o,e,t,i){this._beginToken(o);let r=[];for(;!t();){let s=this._cursor.clone();this._attemptStr(mf.start)?(this._endToken([this._processCarriageReturns(r.join(""))],s),r.length=0,this._consumeInterpolation(e,s,i),this._beginToken(o)):this._cursor.peek()===Xx?(this._endToken([this._processCarriageReturns(r.join(""))]),r.length=0,this._consumeEntity(o),this._beginToken(o)):r.push(this._readChar())}this._inInterpolation=!1,this._endToken([this._processCarriageReturns(r.join(""))])}_consumeInterpolation(o,e,t){let i=[];this._beginToken(o,e),i.push(mf.start);let r=this._cursor.clone(),s=null,d=!1;for(;this._cursor.peek()!==ia&&(t===null||!t());){let m=this._cursor.clone();if(this._isTagStart()){this._cursor=m,i.push(this._getProcessedChars(r,m)),this._endToken(i);return}if(s===null)if(this._attemptStr(mf.end)){i.push(this._getProcessedChars(r,m)),i.push(mf.end),this._endToken(i);return}else this._attemptStr("//")&&(d=!0);let u=this._cursor.peek();this._cursor.advance(),u===Km?this._cursor.advance():u===s?s=null:!d&&s===null&&Pf(u)&&(s=u)}i.push(this._getProcessedChars(r,this._cursor)),this._endToken(i)}_consumeDirective(o,e){for(this._requireCharCode(fg),this._cursor.advance();d6(this._cursor.peek());)this._cursor.advance();this._beginToken(38,o);let t=this._cursor.getChars(e);if(this._endToken([t]),this._attemptCharCodeUntilFn(Bo),this._cursor.peek()===Ya){for(this._openDirectiveCount++,this._beginToken(39),this._cursor.advance(),this._endToken([]),this._attemptCharCodeUntilFn(Bo);!p6(this._cursor.peek())&&this._cursor.peek()!==Pr;)this._consumeAttribute();if(this._attemptCharCodeUntilFn(Bo),this._openDirectiveCount--,this._cursor.peek()!==Pr){if(this._cursor.peek()===Us||this._cursor.peek()===ul)return;throw this._createError(xg(this._cursor.peek()),this._cursor.getSpan(o))}this._beginToken(40),this._cursor.advance(),this._endToken([]),this._attemptCharCodeUntilFn(Bo)}}_getProcessedChars(o,e){return this._processCarriageReturns(e.getChars(o))}_isTextEnd(){return!!(this._isTagStart()||this._cursor.peek()===ia||this._tokenizeIcu&&!this._inInterpolation&&(this.isExpansionFormStart()||this._cursor.peek()===Ka&&this._isInExpansionCase())||this._tokenizeBlocks&&!this._inInterpolation&&!this._isInExpansion()&&(this._isBlockStart()||this._isLetStart()||this._cursor.peek()===Ka))}_isTagStart(){if(this._cursor.peek()===Ig){let o=this._cursor.clone();o.advance();let e=o.peek();if(lu<=e&&e<=N0||Ip<=e&&e<=eh||e===ul||e===TP)return!0}return!1}_readUntil(o){let e=this._cursor.clone();return this._attemptUntilChar(o),this._cursor.getChars(e)}_isInExpansion(){return this._isInExpansionCase()||this._isInExpansionForm()}_isInExpansionCase(){return this._expansionCaseStack.length>0&&this._expansionCaseStack[this._expansionCaseStack.length-1]===21}_isInExpansionForm(){return this._expansionCaseStack.length>0&&this._expansionCaseStack[this._expansionCaseStack.length-1]===19}isExpansionFormStart(){if(this._cursor.peek()!==_l)return!1;let o=this._cursor.clone(),e=this._attemptStr(mf.start);return this._cursor=o,!e}};function Bo(n){return!p0(n)||n===ia}function Rm(n){return p0(n)||n===Us||n===Ig||n===ul||n===d0||n===c0||n===ta||n===ia}function _q(n){return(nON)}function fq(n){return n===fs||n===ia||!FU(n)}function Cq(n){return n===fs||n===ia||!(Pp(n)||gl(n))}function vq(n){return n!==Ka}function xq(n,o){return l6(n)===l6(o)}function l6(n){return n>=lu&&n<=N0?n-lu+Ip:n}function yq(n){return Pp(n)||gl(n)||n===Ap}function c6(n){return n!==fs&&Bo(n)}function rx(n){return n===Ap||n>=Ip&&n<=eh}function d6(n){return Pp(n)||gl(n)||n===Ap}function p6(n){return n===ul||n===Us||n===Ig||n===ia}function bq(n){let o=[],e;for(let t=0;t0&&e.indexOf(o.peek())!==-1;)t===o&&(o=o.clone()),o.advance();let i=this.locationFromCursor(o),r=this.locationFromCursor(this),s=t!==o?this.locationFromCursor(t):i;return new On(i,r,s)}getChars(o){return this.input.substring(o.state.offset,this.state.offset)}charAt(o){return this.input.charCodeAt(o)}advanceState(o){if(o.offset>=this.end)throw this.state=o,new S0('Unexpected character "EOF"',this);let e=this.charAt(o.offset);e===Qm?(o.line++,o.column=0):Zx(e)||o.column++,o.offset++,this.updatePeek(o)}updatePeek(o){o.peek=o.offset>=this.end?ia:this.charAt(o.offset)}locationFromCursor(o){return new m0(o.file,o.state.offset,o.state.line,o.state.column)}},YP=class n extends h1{internalState;constructor(o,e){o instanceof n?(super(o),this.internalState=q({},o.internalState)):(super(o,e),this.internalState=this.state)}advance(){this.state=this.internalState,super.advance(),this.processEscapeSequence()}init(){super.init(),this.processEscapeSequence()}clone(){return new n(this)}getChars(o){let e=o.clone(),t="";for(;e.internalState.offsetthis.internalState.peek;if(o()===Km)if(this.internalState=q({},this.state),this.advanceState(this.internalState),o()===DN)this.state.peek=Qm;else if(o()===IN)this.state.peek=jT;else if(o()===FN)this.state.peek=SN;else if(o()===AN)this.state.peek=BT;else if(o()===AU)this.state.peek=kU;else if(o()===HT)this.state.peek=kN;else if(o()===NN)if(this.advanceState(this.internalState),o()===_l){this.advanceState(this.internalState);let e=this.clone(),t=0;for(;o()!==Ka;)this.advanceState(this.internalState),t++;this.state.peek=this.decodeHexDigits(e,t)}else{let e=this.clone();this.advanceState(this.internalState),this.advanceState(this.internalState),this.advanceState(this.internalState),this.state.peek=this.decodeHexDigits(e,4)}else if(o()===RN){this.advanceState(this.internalState);let e=this.clone();this.advanceState(this.internalState),this.state.peek=this.decodeHexDigits(e,2)}else if(V5(o())){let e="",t=0,i=this.clone();for(;V5(o())&&t<3;)i=this.clone(),e+=String.fromCodePoint(o()),this.advanceState(this.internalState),t++;this.state.peek=parseInt(e,8),this.internalState=i.internalState}else Zx(this.internalState.peek)?(this.advanceState(this.internalState),this.state=this.internalState):this.state.peek=this.internalState.peek}decodeHexDigits(o,e){let t=this.input.slice(o.internalState.offset,o.internalState.offset+e),i=parseInt(t,16);if(isNaN(i))throw o.state=o.internalState,new S0("Invalid hexadecimal escape sequence",o);return i}},S0=class extends Error{msg;cursor;constructor(o,e){super(o),this.msg=o,this.cursor=e,Object.setPrototypeOf(this,new.target.prototype)}},vr=class n extends dn{elementName;static create(o,e,t){return new n(o,e,t)}constructor(o,e,t){super(e,t),this.elementName=o}},_1=class{rootNodes;errors;constructor(o,e){this.rootNodes=o,this.errors=e}},wq=class{getTagDefinition;constructor(o){this.getTagDefinition=o}parse(o,e,t){let i=mq(o,e,this.getTagDefinition,t),r=new ZP(i.tokens,this.getTagDefinition);return r.build(),new _1(r.rootNodes,[...i.errors,...r.errors])}},ZP=class n{tokens;tagDefinitionResolver;_index=-1;_peek;_containerStack=[];rootNodes=[];errors=[];constructor(o,e){this.tokens=o,this.tagDefinitionResolver=e,this._advance()}build(){for(;this._peek.type!==41;)this._peek.type===0||this._peek.type===4?this._consumeElementStartTag(this._advance()):this._peek.type===3?this._consumeElementEndTag(this._advance()):this._peek.type===12?(this._closeVoidElement(),this._consumeCdata(this._advance())):this._peek.type===10?(this._closeVoidElement(),this._consumeComment(this._advance())):this._peek.type===5||this._peek.type===7||this._peek.type===6?(this._closeVoidElement(),this._consumeText(this._advance())):this._peek.type===19?this._consumeExpansion(this._advance()):this._peek.type===24?(this._closeVoidElement(),this._consumeBlockOpen(this._advance())):this._peek.type===26?(this._closeVoidElement(),this._consumeBlockClose(this._advance())):this._peek.type===28?(this._closeVoidElement(),this._consumeIncompleteBlock(this._advance())):this._peek.type===29?(this._closeVoidElement(),this._consumeLet(this._advance())):this._peek.type===32?(this._closeVoidElement(),this._consumeIncompleteLet(this._advance())):this._peek.type===33||this._peek.type===37?this._consumeComponentStartTag(this._advance()):this._peek.type===36?this._consumeComponentEndTag(this._advance()):this._advance();for(let o of this._containerStack)o instanceof hl&&this.errors.push(vr.create(o.name,o.sourceSpan,`Unclosed block "${o.name}"`))}_advance(){let o=this._peek;return this._index0)return this.errors=this.errors.concat(r.errors),null;let s=new On(o.sourceSpan.start,i.sourceSpan.end,o.sourceSpan.fullStart),d=new On(e.sourceSpan.start,i.sourceSpan.end,e.sourceSpan.fullStart);return new m1(o.parts[0],r.rootNodes,s,o.sourceSpan,d)}_collectExpansionExpTokens(o){let e=[],t=[21];for(;;){if((this._peek.type===19||this._peek.type===21)&&t.push(this._peek.type),this._peek.type===22)if(m6(t,21)){if(t.pop(),t.length===0)return e}else return this.errors.push(vr.create(null,o.sourceSpan,"Invalid ICU message. Missing '}'.")),null;if(this._peek.type===23)if(m6(t,19))t.pop();else return this.errors.push(vr.create(null,o.sourceSpan,"Invalid ICU message. Missing '}'.")),null;if(this._peek.type===41)return this.errors.push(vr.create(null,o.sourceSpan,"Invalid ICU message. Missing '}'.")),null;e.push(this._advance())}}_consumeText(o){let e=[o],t=o.sourceSpan,i=o.parts[0];if(i.length>0&&i[0]===` +`){let r=this._getContainer();r!=null&&r.children.length===0&&this._getTagDefinition(r)?.ignoreFirstLf&&(i=i.substring(1),e[0]={type:o.type,sourceSpan:o.sourceSpan,parts:[i]})}for(;this._peek.type===8||this._peek.type===5||this._peek.type===9;)o=this._advance(),e.push(o),o.type===8?i+=o.parts.join("").replace(/&([^;]+);/g,u6):o.type===9?i+=o.parts[0]:i+=o.parts.join("");if(i.length>0){let r=o.sourceSpan;this._addToParent(new uu(i,new On(t.start,r.end,t.fullStart,t.details),e))}}_closeVoidElement(){let o=this._getContainer();o!==null&&this._getTagDefinition(o)?.isVoid&&this._containerStack.pop()}_consumeElementStartTag(o){let e=[],t=[];this._consumeAttributesAndDirectives(e,t);let i=this._getElementFullName(o,this._getClosestElementLikeParent()),r=this._getTagDefinition(i),s=!1;this._peek.type===2?(this._advance(),s=!0,r?.canSelfClose||fP(i)!==null||r?.isVoid||this.errors.push(vr.create(i,o.sourceSpan,`Only void, custom and foreign elements can be self closed "${o.parts[1]}"`))):this._peek.type===1&&(this._advance(),s=!1);let d=this._peek.sourceSpan.fullStart,m=new On(o.sourceSpan.start,d,o.sourceSpan.fullStart),u=new On(o.sourceSpan.start,d,o.sourceSpan.fullStart),h=new ml(i,e,t,[],s,m,u,void 0,r?.isVoid??!1),v=this._getContainer(),b=v!==null&&!!this._getTagDefinition(v)?.isClosedByChild(h.name);this._pushContainer(h,b),s?this._popContainer(i,ml,m):o.type===4&&(this._popContainer(i,ml,null),this.errors.push(vr.create(i,m,`Opening tag "${i}" not terminated.`)))}_consumeComponentStartTag(o){let e=o.parts[0],t=[],i=[];this._consumeAttributesAndDirectives(t,i);let r=this._getClosestElementLikeParent(),s=this._getComponentTagName(o,r),d=this._getComponentFullName(o,r),m=this._peek.type===35;this._advance();let u=this._peek.sourceSpan.fullStart,h=new On(o.sourceSpan.start,u,o.sourceSpan.fullStart),v=new On(o.sourceSpan.start,u,o.sourceSpan.fullStart),b=new Qa(e,s,d,t,i,[],m,h,v,void 0),y=this._getContainer(),x=y!==null&&b.tagName!==null&&!!this._getTagDefinition(y)?.isClosedByChild(b.tagName);this._pushContainer(b,x),m?this._popContainer(d,Qa,h):o.type===37&&(this._popContainer(d,Qa,null),this.errors.push(vr.create(d,h,`Opening tag "${d}" not terminated.`)))}_consumeAttributesAndDirectives(o,e){for(;this._peek.type===14||this._peek.type===38;)this._peek.type===38?e.push(this._consumeDirective(this._peek)):o.push(this._consumeAttr(this._advance()))}_consumeComponentEndTag(o){let e=this._getComponentFullName(o,this._getClosestElementLikeParent());if(!this._popContainer(e,Qa,o.sourceSpan)){let t=this._containerStack[this._containerStack.length-1],i;t instanceof Qa&&t.componentName===o.parts[0]?i=`, did you mean "${t.fullName}"?`:i=". It may happen when the tag has already been closed by another tag.";let r=`Unexpected closing tag "${e}"${i}`;this.errors.push(vr.create(e,o.sourceSpan,r))}}_getTagDefinition(o){return typeof o=="string"?this.tagDefinitionResolver(o):o instanceof ml?this.tagDefinitionResolver(o.name):o instanceof Qa&&o.tagName!==null?this.tagDefinitionResolver(o.tagName):null}_pushContainer(o,e){e&&this._containerStack.pop(),this._addToParent(o),this._containerStack.push(o)}_consumeElementEndTag(o){let e=this._getElementFullName(o,this._getClosestElementLikeParent());if(this._getTagDefinition(e)?.isVoid)this.errors.push(vr.create(e,o.sourceSpan,`Void elements do not have end tags "${o.parts[1]}"`));else if(!this._popContainer(e,ml,o.sourceSpan)){let t=`Unexpected closing tag "${e}". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags`;this.errors.push(vr.create(e,o.sourceSpan,t))}}_popContainer(o,e,t){let i=!1;for(let r=this._containerStack.length-1;r>=0;r--){let s=this._containerStack[r];if(((s instanceof Qa?s.fullName:s.name)===o||o===null)&&s instanceof e)return s.endSourceSpan=t,s.sourceSpan.end=t!==null?t.end:s.sourceSpan.end,this._containerStack.splice(r,this._containerStack.length-r),!i;(s instanceof hl||!this._getTagDefinition(s)?.closedByParent)&&(i=!0)}return!1}_consumeAttr(o){let e=fx(o.parts[0],o.parts[1]),t=o.sourceSpan.end;this._peek.type===15&&this._advance();let i="",r=[],s,d;if(this._peek.type===16)for(s=this._peek.sourceSpan,d=this._peek.sourceSpan.end;this._peek.type===16||this._peek.type===17||this._peek.type===9;){let h=this._advance();r.push(h),h.type===17?i+=h.parts.join("").replace(/&([^;]+);/g,u6):h.type===9?i+=h.parts[0]:i+=h.parts.join(""),d=t=h.sourceSpan.end}this._peek.type===15&&(t=this._advance().sourceSpan.end);let u=s&&d&&new On(s.start,d,s.fullStart);return new GP(e,i,new On(o.sourceSpan.start,t,o.sourceSpan.fullStart),o.sourceSpan,u,r.length>0?r:void 0,void 0)}_consumeDirective(o){let e=[],t=o.sourceSpan.end,i=null;if(this._advance(),this._peek.type===39){for(t=this._peek.sourceSpan.end,this._advance();this._peek.type===14;)e.push(this._consumeAttr(this._advance()));this._peek.type===40?(i=this._peek.sourceSpan,this._advance()):this.errors.push(vr.create(null,o.sourceSpan,"Unterminated directive definition"))}let r=new On(o.sourceSpan.start,t,o.sourceSpan.fullStart),s=new On(r.start,i===null?o.sourceSpan.end:i.end,r.fullStart);return new qP(o.parts[0],e,s,r,i)}_consumeBlockOpen(o){let e=[];for(;this._peek.type===27;){let d=this._advance();e.push(new u1(d.parts[0],d.sourceSpan))}this._peek.type===25&&this._advance();let t=this._peek.sourceSpan.fullStart,i=new On(o.sourceSpan.start,t,o.sourceSpan.fullStart),r=new On(o.sourceSpan.start,t,o.sourceSpan.fullStart),s=new hl(o.parts[0],e,[],i,o.sourceSpan,r);this._pushContainer(s,!1)}_consumeBlockClose(o){let e=this._containerStack.length,t=this._containerStack[e-1];if(!this._popContainer(null,hl,o.sourceSpan)){if(this._containerStack.length element? If you meant to write the \`}\` character, you should use the "}" HTML entity instead.`));return}this.errors.push(vr.create(null,o.sourceSpan,'Unexpected closing block. The block may have been closed earlier. If you meant to write the `}` character, you should use the "}" HTML entity instead.'))}}_consumeIncompleteBlock(o){let e=[];for(;this._peek.type===27;){let d=this._advance();e.push(new u1(d.parts[0],d.sourceSpan))}let t=this._peek.sourceSpan.fullStart,i=new On(o.sourceSpan.start,t,o.sourceSpan.fullStart),r=new On(o.sourceSpan.start,t,o.sourceSpan.fullStart),s=new hl(o.parts[0],e,[],i,o.sourceSpan,r);this._pushContainer(s,!1),this._popContainer(null,hl,null),this.errors.push(vr.create(o.parts[0],i,`Incomplete block "${o.parts[0]}". If you meant to write the @ character, you should use the "@" HTML entity instead.`))}_consumeLet(o){let e=o.parts[0],t,i;if(this._peek.type!==30){this.errors.push(vr.create(o.parts[0],o.sourceSpan,`Invalid @let declaration "${e}". Declaration must have a value.`));return}else t=this._advance();if(this._peek.type!==31){this.errors.push(vr.create(o.parts[0],o.sourceSpan,`Unterminated @let declaration "${e}". Declaration must be terminated with a semicolon.`));return}else i=this._advance();let r=i.sourceSpan.end,s=new On(o.sourceSpan.start,r,o.sourceSpan.fullStart),d=o.sourceSpan.toString().lastIndexOf(e),m=o.sourceSpan.start.moveBy(d),u=new On(m,o.sourceSpan.end),h=new g1(e,t.parts[0],s,u,t.sourceSpan);this._addToParent(h)}_consumeIncompleteLet(o){let e=o.parts[0]??"",t=e?` "${e}"`:"";if(e.length>0){let i=o.sourceSpan.toString().lastIndexOf(e),r=o.sourceSpan.start.moveBy(i),s=new On(r,o.sourceSpan.end),d=new On(o.sourceSpan.start,o.sourceSpan.start.moveBy(0)),m=new g1(e,"",o.sourceSpan,s,d);this._addToParent(m)}this.errors.push(vr.create(o.parts[0],o.sourceSpan,`Incomplete @let declaration${t}. @let declarations must be written as \`@let = ;\``))}_getContainer(){return this._containerStack.length>0?this._containerStack[this._containerStack.length-1]:null}_getClosestElementLikeParent(){for(let o=this._containerStack.length-1;o>-1;o--){let e=this._containerStack[o];if(e instanceof ml||e instanceof Qa)return e}return null}_addToParent(o){let e=this._getContainer();e===null?this.rootNodes.push(o):e.children.push(o)}_getElementFullName(o,e){let t=this._getPrefix(o,e);return fx(t,o.parts[1])}_getComponentFullName(o,e){let t=o.parts[0],i=this._getComponentTagName(o,e);return i===null?t:i.startsWith(":")?t+i:`${t}:${i}`}_getComponentTagName(o,e){let t=this._getPrefix(o,e),i=o.parts[2];return!t&&!i?null:!t&&i?i:fx(t,i||"ng-component")}_getPrefix(o,e){let t,i;if(o.type===33||o.type===37||o.type===36?(t=o.parts[1],i=o.parts[2]):(t=o.parts[0],i=o.parts[1]),t=t||this._getTagDefinition(i)?.implicitNamespacePrefix||"",!t&&e){let r=e instanceof ml?e.name:e.tagName;if(r!==null){let s=ha(r)[1],d=this._getTagDefinition(s);d!==null&&!d.preventNamespaceInheritance&&(t=fP(r))}}return t}};function m6(n,o){return n.length>0&&n[n.length-1]===o}function u6(n,o){return M0[o]!==void 0?M0[o]||n:/^#x[a-f0-9]+$/i.test(o)?String.fromCodePoint(parseInt(o.slice(2),16)):/^#\d+$/.test(o)?String.fromCodePoint(parseInt(o.slice(1),10)):n}var rF="ngPreserveWhitespaces",g6=new Set(["pre","template","textarea","script","style"]),aF=` \f +\r \v\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF`,Mq=new RegExp(`[^${aF}]`),Sq=new RegExp(`[${aF}]{2,}`,"g");function h6(n){return n.some(o=>o.name===rF)}function sF(n){return n.replace(new RegExp(oF,"g")," ")}var f1=class{preserveSignificantWhitespace;originalNodeMap;requireContext;icuExpansionDepth=0;constructor(o,e,t=!0){this.preserveSignificantWhitespace=o,this.originalNodeMap=e,this.requireContext=t}visitElement(o,e){if(g6.has(o.name)||h6(o.attrs)){let i=new ml(o.name,Pc(this,o.attrs),Pc(this,o.directives),o.children,o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,o.isVoid,o.i18n);return this.originalNodeMap?.set(i,o),i}let t=new ml(o.name,o.attrs,o.directives,Pc(this,o.children),o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,o.isVoid,o.i18n);return this.originalNodeMap?.set(t,o),t}visitAttribute(o,e){return o.name!==rF?o:null}visitText(o,e){let t=o.value.match(Mq),i=e&&(e.prev instanceof Xm||e.next instanceof Xm);if(this.icuExpansionDepth>0&&this.preserveSignificantWhitespace)return o;if(t||i){let s=o.tokens.map(h=>h.type===5?Eq(h):h);if(!this.preserveSignificantWhitespace&&s.length>0){let h=s[0];s.splice(0,1,kq(h,e));let v=s[s.length-1];s.splice(s.length-1,1,Pq(v,e))}let d=cF(o.value),m=this.preserveSignificantWhitespace?d:Tq(d,e),u=new uu(m,o.sourceSpan,s,o.i18n);return this.originalNodeMap?.set(u,o),u}return null}visitComment(o,e){return o}visitExpansion(o,e){this.icuExpansionDepth++;let t;try{t=new Xm(o.switchValue,o.type,Pc(this,o.cases),o.sourceSpan,o.switchValueSourceSpan,o.i18n)}finally{this.icuExpansionDepth--}return this.originalNodeMap?.set(t,o),t}visitExpansionCase(o,e){let t=new m1(o.value,Pc(this,o.expression),o.sourceSpan,o.valueSourceSpan,o.expSourceSpan);return this.originalNodeMap?.set(t,o),t}visitBlock(o,e){let t=new hl(o.name,o.parameters,Pc(this,o.children),o.sourceSpan,o.nameSpan,o.startSourceSpan,o.endSourceSpan);return this.originalNodeMap?.set(t,o),t}visitBlockParameter(o,e){return o}visitLetDeclaration(o,e){return o}visitComponent(o,e){if(o.tagName&&g6.has(o.tagName)||h6(o.attrs)){let i=new Qa(o.componentName,o.tagName,o.fullName,Pc(this,o.attrs),Pc(this,o.directives),o.children,o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,o.i18n);return this.originalNodeMap?.set(i,o),i}let t=new Qa(o.componentName,o.tagName,o.fullName,o.attrs,o.directives,Pc(this,o.children),o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,o.i18n);return this.originalNodeMap?.set(t,o),t}visitDirective(o,e){return o}visit(o,e){if(this.requireContext&&!e)throw new Error("WhitespaceVisitor requires context. Visit via `visitAllWithSiblings` to get this context.");return!1}};function kq(n,o){return n.type!==5||!!o?.prev?n:lF(n,t=>t.trimStart())}function Pq(n,o){return n.type!==5||!!o?.next?n:lF(n,t=>t.trimEnd())}function Tq(n,o){let e=!o?.prev,t=!o?.next,i=e?n.trimStart():n;return t?i.trimEnd():i}function Eq({type:n,parts:o,sourceSpan:e}){return{type:n,parts:[cF(o[0])],sourceSpan:e}}function lF({type:n,parts:o,sourceSpan:e},t){return{type:n,parts:[t(o[0])],sourceSpan:e}}function cF(n){return sF(n).replace(Sq," ")}function Pc(n,o){let e=[];return o.forEach((t,i)=>{let r={prev:o[i-1],next:o[i+1]},s=t.visit(n,r);s&&e.push(s)}),e}var Cn=(function(n){return n[n.Character=0]="Character",n[n.Identifier=1]="Identifier",n[n.PrivateIdentifier=2]="PrivateIdentifier",n[n.Keyword=3]="Keyword",n[n.String=4]="String",n[n.Operator=5]="Operator",n[n.Number=6]="Number",n[n.RegExpBody=7]="RegExpBody",n[n.RegExpFlags=8]="RegExpFlags",n[n.Error=9]="Error",n})(Cn||{}),Ym=(function(n){return n[n.Plain=0]="Plain",n[n.TemplateLiteralPart=1]="TemplateLiteralPart",n[n.TemplateLiteralEnd=2]="TemplateLiteralEnd",n})(Ym||{}),Oq=["var","let","as","null","undefined","true","false","if","else","this","typeof","void","in","instanceof"],k0=class{tokenize(o){return new JP(o).scan()}},Js=class{index;end;type;numValue;strValue;constructor(o,e,t,i,r){this.index=o,this.end=e,this.type=t,this.numValue=i,this.strValue=r}isCharacter(o){return this.type===Cn.Character&&this.numValue===o}isNumber(){return this.type===Cn.Number}isString(){return this.type===Cn.String}isOperator(o){return this.type===Cn.Operator&&this.strValue===o}isIdentifier(){return this.type===Cn.Identifier}isPrivateIdentifier(){return this.type===Cn.PrivateIdentifier}isKeyword(){return this.type===Cn.Keyword}isKeywordLet(){return this.type===Cn.Keyword&&this.strValue==="let"}isKeywordAs(){return this.type===Cn.Keyword&&this.strValue==="as"}isKeywordNull(){return this.type===Cn.Keyword&&this.strValue==="null"}isKeywordUndefined(){return this.type===Cn.Keyword&&this.strValue==="undefined"}isKeywordTrue(){return this.type===Cn.Keyword&&this.strValue==="true"}isKeywordFalse(){return this.type===Cn.Keyword&&this.strValue==="false"}isKeywordThis(){return this.type===Cn.Keyword&&this.strValue==="this"}isKeywordTypeof(){return this.type===Cn.Keyword&&this.strValue==="typeof"}isKeywordVoid(){return this.type===Cn.Keyword&&this.strValue==="void"}isKeywordIn(){return this.type===Cn.Keyword&&this.strValue==="in"}isKeywordInstanceOf(){return this.type===Cn.Keyword&&this.strValue==="instanceof"}isError(){return this.type===Cn.Error}isRegExpBody(){return this.type===Cn.RegExpBody}isRegExpFlags(){return this.type===Cn.RegExpFlags}toNumber(){return this.type===Cn.Number?this.numValue:-1}isTemplateLiteralPart(){return this.isString()&&this.kind===Ym.TemplateLiteralPart}isTemplateLiteralEnd(){return this.isString()&&this.kind===Ym.TemplateLiteralEnd}isTemplateLiteralInterpolationStart(){return this.isOperator("${")}toString(){switch(this.type){case Cn.Character:case Cn.Identifier:case Cn.Keyword:case Cn.Operator:case Cn.PrivateIdentifier:case Cn.String:case Cn.Error:case Cn.RegExpBody:case Cn.RegExpFlags:return this.strValue;case Cn.Number:return this.numValue.toString();default:return null}}},Tf=class extends Js{kind;constructor(o,e,t,i){super(o,e,Cn.String,0,t),this.kind=i}};function uf(n,o,e){return new Js(n,o,Cn.Character,e,String.fromCharCode(e))}function Dq(n,o,e){return new Js(n,o,Cn.Identifier,0,e)}function Iq(n,o,e){return new Js(n,o,Cn.PrivateIdentifier,0,e)}function Aq(n,o,e){return new Js(n,o,Cn.Keyword,0,e)}function pp(n,o,e){return new Js(n,o,Cn.Operator,0,e)}function Nq(n,o,e){return new Js(n,o,Cn.Number,e,"")}function Fq(n,o,e){return new Js(n,o,Cn.Error,0,e)}function Rq(n,o,e){return new Js(n,o,Cn.RegExpBody,0,e)}function Lq(n,o,e){return new Js(n,o,Cn.RegExpFlags,0,e)}var gf=new Js(-1,-1,Cn.Character,0,""),JP=class{input;tokens=[];length;peek=0;index=-1;braceStack=[];constructor(o){this.input=o,this.length=o.length,this.advance()}scan(){let o=this.scanToken();for(;o!==null;)this.tokens.push(o),o=this.scanToken();return this.tokens}advance(){this.peek=++this.index>=this.length?ia:this.input.charCodeAt(this.index)}scanToken(){let o=this.input,e=this.length,t=this.peek,i=this.index;for(;t<=PN;)if(++i>=e){t=ia;break}else t=o.charCodeAt(i);if(this.peek=t,this.index=i,i>=e)return null;if(_6(t))return this.scanIdentifier();if(gl(t))return this.scanNumber(i);let r=i;switch(t){case Lm:return this.advance(),gl(this.peek)?this.scanNumber(r):this.peek!==Lm?uf(r,this.index,Lm):(this.advance(),this.peek===Lm?(this.advance(),pp(r,this.index,"...")):this.error(`Unexpected character [${String.fromCharCode(t)}]`,0));case Ya:case Pr:case Nc:case Pd:case Oa:case Dc:case fs:return this.scanCharacter(r,t);case _l:return this.scanOpenBrace(r,t);case Ka:return this.scanCloseBrace(r,t);case d0:case c0:return this.scanString();case EP:return this.advance(),this.scanTemplateLiteralPart(r);case TN:return this.scanPrivateIdentifier();case EN:return this.scanComplexOperator(r,"+",ta,"=");case Yx:return this.scanComplexOperator(r,"-",ta,"=");case ul:return this.isStartOfRegex()?this.scanRegex(i):this.scanComplexOperator(r,"/",ta,"=");case PU:return this.scanComplexOperator(r,"%",ta,"=");case IU:return this.scanOperator(r,"^");case R5:return this.scanStar(r);case L5:return this.scanQuestion(r);case Ig:case Us:return this.scanComplexOperator(r,String.fromCharCode(t),ta,"=");case TP:return this.scanComplexOperator(r,"!",ta,"=",ta,"=");case ta:return this.scanEquals(r);case Xx:return this.scanComplexOperator(r,"&",Xx,"&",ta,"=");case z5:return this.scanComplexOperator(r,"|",z5,"|",ta,"=");case LN:for(;p0(this.peek);)this.advance();return this.scanToken()}return this.advance(),this.error(`Unexpected character [${String.fromCharCode(t)}]`,0)}scanCharacter(o,e){return this.advance(),uf(o,this.index,e)}scanOperator(o,e){return this.advance(),pp(o,this.index,e)}scanOpenBrace(o,e){return this.braceStack.push("expression"),this.advance(),uf(o,this.index,e)}scanCloseBrace(o,e){return this.advance(),this.braceStack.pop()==="interpolation"?(this.tokens.push(uf(o,this.index,Ka)),this.scanTemplateLiteralPart(this.index)):uf(o,this.index,e)}scanComplexOperator(o,e,t,i,r,s){this.advance();let d=e;return this.peek==t&&(this.advance(),d+=i),r!=null&&this.peek==r&&(this.advance(),d+=s),pp(o,this.index,d)}scanEquals(o){this.advance();let e="=";if(this.peek===ta)this.advance(),e+="=";else if(this.peek===Us)return this.advance(),e+=">",pp(o,this.index,e);return this.peek===ta&&(this.advance(),e+="="),pp(o,this.index,e)}scanIdentifier(){let o=this.index;for(this.advance();f6(this.peek);)this.advance();let e=this.input.substring(o,this.index);return Oq.indexOf(e)>-1?Aq(o,this.index,e):Dq(o,this.index,e)}scanPrivateIdentifier(){let o=this.index;if(this.advance(),!_6(this.peek))return this.error("Invalid character [#]",-1);for(;f6(this.peek);)this.advance();let e=this.input.substring(o,this.index);return Iq(o,this.index,e)}scanNumber(o){let e=this.index===o,t=!1;for(this.advance();;){if(!gl(this.peek))if(this.peek===Ap){if(!gl(this.input.charCodeAt(this.index-1))||!gl(this.input.charCodeAt(this.index+1)))return this.error("Invalid numeric separator",0);t=!0}else if(this.peek===Lm)e=!1;else if(zq(this.peek)){if(this.advance(),Vq(this.peek)&&this.advance(),!gl(this.peek))return this.error("Invalid exponent",-1);e=!1}else break;this.advance()}let i=this.input.substring(o,this.index);t&&(i=i.replace(/_/g,""));let r=e?jq(i):parseFloat(i);return Nq(o,this.index,r)}scanString(){let o=this.index,e=this.peek;this.advance();let t="",i=this.index,r=this.input;for(;this.peek!=e;)if(this.peek==Km){let d=this.scanStringBackslash(t,i);if(typeof d!="string")return d;t=d,i=this.index}else{if(this.peek==ia)return this.error("Unterminated quote",0);this.advance()}let s=r.substring(i,this.index);return this.advance(),new Tf(o,this.index,t+s,Ym.Plain)}scanQuestion(o){this.advance();let e="?";return this.peek===L5?(e+="?",this.advance(),this.peek===ta&&(e+="=",this.advance())):this.peek===Lm&&(e+=".",this.advance()),pp(o,this.index,e)}scanTemplateLiteralPart(o){let e="",t=this.index;for(;this.peek!==EP;)if(this.peek===Km){let r=this.scanStringBackslash(e,t);if(typeof r!="string")return r;e=r,t=this.index}else if(this.peek===D1){let r=this.index;if(this.advance(),this.peek===_l)return this.braceStack.push("interpolation"),this.tokens.push(new Tf(o,r,e+this.input.substring(t,r),Ym.TemplateLiteralPart)),this.advance(),pp(r,this.index,this.input.substring(r,this.index))}else{if(this.peek===ia)return this.error("Unterminated template literal",0);this.advance()}let i=this.input.substring(t,this.index);return this.advance(),new Tf(o,this.index,e+i,Ym.TemplateLiteralEnd)}error(o,e){let t=this.index+e;return Fq(t,this.index,`Lexer Error: ${o} at column ${t} in expression [${this.input}]`)}scanStringBackslash(o,e){o+=this.input.substring(e,this.index);let t;if(this.advance(),this.peek===NN){let i=this.input.substring(this.index+1,this.index+5);if(/^[0-9a-f]+$/i.test(i))t=parseInt(i,16);else return this.error(`Invalid unicode escape [\\u${i}]`,0);for(let r=0;r<5;r++)this.advance()}else t=Bq(this.peek),this.advance();return o+=String.fromCharCode(t),o}scanStar(o){this.advance();let e="*";return this.peek===R5?(e+="*",this.advance(),this.peek===ta&&(e+="=",this.advance())):this.peek===ta&&(e+="=",this.advance()),pp(o,this.index,e)}isStartOfRegex(){if(this.tokens.length===0)return!0;let o=this.tokens[this.tokens.length-1];if(o.isOperator("!")){let e=this.tokens.length>1?this.tokens[this.tokens.length-2]:null;return e===null||e.type!==Cn.Identifier&&!e.isCharacter(Pr)&&!e.isCharacter(Pd)}return o.type===Cn.Operator||o.isCharacter(Ya)||o.isCharacter(Nc)||o.isCharacter(Oa)||o.isCharacter(Dc)}scanRegex(o){this.advance();let e=this.index,t=!1,i=!1;for(;;){let m=this.peek;if(m===ia)return this.error("Unterminated regular expression",0);if(t)t=!1;else if(m===Km)t=!0;else if(m===Nc)i=!0;else if(m===Pd)i=!1;else if(m===ul&&!i)break;this.advance()}let r=this.input.substring(e,this.index);this.advance();let s=Rq(o,this.index,r),d=this.scanRegexFlags(this.index);return d!==null?(this.tokens.push(s),d):s}scanRegexFlags(o){if(!Pp(this.peek))return null;for(;Pp(this.peek);)this.advance();return Lq(o,this.index,this.input.substring(o,this.index))}};function _6(n){return lu<=n&&n<=N0||Ip<=n&&n<=eh||n==Ap||n==D1}function f6(n){return Pp(n)||gl(n)||n==Ap||n==D1}function zq(n){return n==NU||n==EU}function Vq(n){return n==Yx||n==EN}function Bq(n){switch(n){case DN:return Qm;case HT:return kN;case IN:return jT;case AN:return BT;case FN:return SN;default:return n}}function jq(n){let o=parseInt(n);if(isNaN(o))throw new Error("Invalid integer literal when parsing "+n);return o}var eT=class{strings;expressions;offsets;constructor(o,e,t){this.strings=o,this.expressions=e,this.offsets=t}},tT=class{templateBindings;warnings;errors;constructor(o,e,t){this.templateBindings=o,this.warnings=e,this.errors=t}};function _p(n){return n.start.toString()||"(unknown)"}var C1=class{_lexer;_supportsDirectPipeReferences;constructor(o,e=!1){this._lexer=o,this._supportsDirectPipeReferences=e}parseAction(o,e,t){let i=[];this._checkNoInterpolation(i,o,e);let{stripped:r}=this._stripComments(o),s=this._lexer.tokenize(r),d=new jm(o,e,t,s,1,i,0,this._supportsDirectPipeReferences).parseChain();return new Cs(d,o,_p(e),t,i)}parseBinding(o,e,t){let i=[],r=this._parseBindingAst(o,e,t,i);return new Cs(r,o,_p(e),t,i)}checkSimpleExpression(o){let e=new nT;return o.visit(e),e.errors}parseSimpleBinding(o,e,t){let i=[],r=this._parseBindingAst(o,e,t,i),s=this.checkSimpleExpression(r);return s.length>0&&i.push(Eg(`Host binding expression cannot contain ${s.join(" ")}`,o,"",e)),new Cs(r,o,_p(e),t,i)}_parseBindingAst(o,e,t,i){this._checkNoInterpolation(i,o,e);let{stripped:r}=this._stripComments(o),s=this._lexer.tokenize(r);return new jm(o,e,t,s,0,i,0,this._supportsDirectPipeReferences).parseChain()}parseTemplateBindings(o,e,t,i,r){let s=this._lexer.tokenize(e),d=[];return new jm(e,t,r,s,0,d,0,this._supportsDirectPipeReferences).parseTemplateBindings({source:o,span:new Gs(i,i+o.length)})}parseInterpolation(o,e,t,i){let r=[],{strings:s,expressions:d,offsets:m}=this.splitInterpolation(o,e,r,i);if(d.length===0)return null;let u=[];for(let h=0;hh.text),u,o,_p(e),t,r)}parseInterpolationExpression(o,e,t){let{stripped:i}=this._stripComments(o),r=this._lexer.tokenize(i),s=[],d=new jm(o,e,t,r,0,s,0,this._supportsDirectPipeReferences).parseChain(),m=["",""];return this.createInterpolationAst(m,[d],o,_p(e),t,s)}createInterpolationAst(o,e,t,i,r,s){let d=new ou(0,t.length),m=new I0(d,d.toAbsolute(r),o,e);return new Cs(m,t,i,r,s)}splitInterpolation(o,e,t,i){let r=[],s=[],d=[],m=i?$q(i):null,u=0,h=!1,v=!1,b="{{",y="}}";for(;u-1)break;i>-1&&r>-1&&o.push(Eg("Got interpolation ({{}}) where expression was expected",e,`at column ${i} in`,t))}_getInterpolationEndIndex(o,e,t){for(let i of this._forEachUnquotedChar(o,t)){if(o.startsWith(e,i))return i;if(o.startsWith("//",i))return o.indexOf(e,i)}return-1}*_forEachUnquotedChar(o,e){let t=null,i=0;for(let r=e;r=this.tokens.length}get inputIndex(){return this.atEOF?this.currentEndIndex:this.next.index+this.offset}get currentEndIndex(){return this.index>0?this.peek(-1).end+this.offset:this.tokens.length===0?this.input.length+this.offset:this.next.index+this.offset}get currentAbsoluteOffset(){return this.absoluteOffset+this.inputIndex}span(o,e){let t=this.currentEndIndex;if(e!==void 0&&e>this.currentEndIndex&&(t=e),o>t){let i=t;t=o,o=i}return new ou(o,t)}sourceSpan(o,e){let t=`${o}@${this.inputIndex}:${e}`;return this.sourceSpanCache.has(t)||this.sourceSpanCache.set(t,this.span(o,e).toAbsolute(this.absoluteOffset)),this.sourceSpanCache.get(t)}advance(){this.index++}withContext(o,e){this.context|=o;let t=e();return this.context^=o,t}consumeOptionalCharacter(o){return this.next.isCharacter(o)?(this.advance(),!0):!1}peekKeywordLet(){return this.next.isKeywordLet()}peekKeywordAs(){return this.next.isKeywordAs()}expectCharacter(o){this.consumeOptionalCharacter(o)||this.error(`Missing expected ${String.fromCharCode(o)}`)}consumeOptionalOperator(o){return this.next.isOperator(o)?(this.advance(),!0):!1}isAssignmentOperator(o){return o.type===Cn.Operator&&qa.isAssignmentOperation(o.strValue)}expectOperator(o){this.consumeOptionalOperator(o)||this.error(`Missing expected operator ${o}`)}prettyPrintToken(o){return o===gf?"end of input":`token ${o}`}expectIdentifierOrKeyword(){let o=this.next;return!o.isIdentifier()&&!o.isKeyword()?(o.isPrivateIdentifier()?this._reportErrorForPrivateIdentifier(o,"expected identifier or keyword"):this.error(`Unexpected ${this.prettyPrintToken(o)}, expected identifier or keyword`),null):(this.advance(),o.toString())}expectIdentifierOrKeywordOrString(){let o=this.next;return!o.isIdentifier()&&!o.isKeyword()&&!o.isString()?(o.isPrivateIdentifier()?this._reportErrorForPrivateIdentifier(o,"expected identifier, keyword or string"):this.error(`Unexpected ${this.prettyPrintToken(o)}, expected identifier, keyword, or string`),""):(this.advance(),o.toString())}parseChain(){let o=[],e=this.inputIndex;for(;this.index=Ip&&u<=eh?_x.ReferencedDirectly:_x.ReferencedByName}else m=_x.ReferencedByName;e=new Fx(this.span(o),this.sourceSpan(o,s),e,i,d,m,r)}while(this.consumeOptionalOperator("|"))}return e}parseExpression(){return this.parseConditional()}parseConditional(){let o=this.inputIndex,e=this.parseLogicalOr();if(this.consumeOptionalOperator("?")){let t=this.parsePipe(),i;if(this.consumeOptionalCharacter(Dc))i=this.parsePipe();else{let r=this.inputIndex,s=this.input.substring(o,r);this.error(`Conditional expression ${s} requires all 3 expressions`),i=new Ea(this.span(o),this.sourceSpan(o))}return new Nx(this.span(o),this.sourceSpan(o),e,t,i)}else return e}parseLogicalOr(){let o=this.inputIndex,e=this.parseLogicalAnd();for(;this.consumeOptionalOperator("||");){let t=this.parseLogicalAnd();e=new qa(this.span(o),this.sourceSpan(o),"||",e,t)}return e}parseLogicalAnd(){let o=this.inputIndex,e=this.parseNullishCoalescing();for(;this.consumeOptionalOperator("&&");){let t=this.parseNullishCoalescing();e=new qa(this.span(o),this.sourceSpan(o),"&&",e,t)}return e}parseNullishCoalescing(){let o=this.inputIndex,e=this.parseEquality();for(;this.consumeOptionalOperator("??");){let t=this.parseEquality();e=new qa(this.span(o),this.sourceSpan(o),"??",e,t)}return e}parseEquality(){let o=this.inputIndex,e=this.parseRelational();for(;this.next.type==Cn.Operator;){let t=this.next.strValue;switch(t){case"==":case"===":case"!=":case"!==":this.advance();let i=this.parseRelational();e=new qa(this.span(o),this.sourceSpan(o),t,e,i);continue}break}return e}parseRelational(){let o=this.inputIndex,e=this.parseAdditive();for(;this.next.type==Cn.Operator||this.next.isKeywordIn()||this.next.isKeywordInstanceOf();){let t=this.next.strValue;switch(t){case"<":case">":case"<=":case">=":case"in":case"instanceof":this.advance();let i=this.parseAdditive();e=new qa(this.span(o),this.sourceSpan(o),t,e,i);continue}break}return e}parseAdditive(){let o=this.inputIndex,e=this.parseMultiplicative();for(;this.next.type==Cn.Operator;){let t=this.next.strValue;switch(t){case"+":case"-":this.advance();let i=this.parseMultiplicative();e=new qa(this.span(o),this.sourceSpan(o),t,e,i);continue}break}return e}parseMultiplicative(){let o=this.inputIndex,e=this.parseExponentiation();for(;this.next.type==Cn.Operator;){let t=this.next.strValue;switch(t){case"*":case"%":case"/":this.advance();let i=this.parseExponentiation();e=new qa(this.span(o),this.sourceSpan(o),t,e,i);continue}break}return e}parseExponentiation(){let o=this.inputIndex,e=this.parsePrefix();for(;this.next.type==Cn.Operator&&this.next.strValue==="**";){(e instanceof Dg||e instanceof Gf||e instanceof qf||e instanceof Qf)&&this.error("Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence"),this.advance();let t=this.parseExponentiation();e=new qa(this.span(o),this.sourceSpan(o),"**",e,t)}return e}parsePrefix(){if(this.next.type==Cn.Operator){let o=this.inputIndex,e=this.next.strValue,t;switch(e){case"+":return this.advance(),t=this.parsePrefix(),Dg.createPlus(this.span(o),this.sourceSpan(o),t);case"-":return this.advance(),t=this.parsePrefix(),Dg.createMinus(this.span(o),this.sourceSpan(o),t);case"!":return this.advance(),t=this.parsePrefix(),new Gf(this.span(o),this.sourceSpan(o),t)}}else if(this.next.isKeywordTypeof()){let o=this.inputIndex;this.advance();let e=this.parsePrefix();return new qf(this.span(o),this.sourceSpan(o),e)}else if(this.next.isKeywordVoid()){let o=this.inputIndex;this.advance();let e=this.parsePrefix();return new Qf(this.span(o),this.sourceSpan(o),e)}return this.parseCallChain()}parseCallChain(){let o=this.inputIndex,e=this.parsePrimary();for(;;)if(this.consumeOptionalCharacter(Lm))e=this.parseAccessMember(e,o,!1);else if(this.consumeOptionalOperator("?."))this.consumeOptionalCharacter(Ya)?e=this.parseCall(e,o,!0):e=this.consumeOptionalCharacter(Nc)?this.parseKeyedReadOrWrite(e,o,!0):this.parseAccessMember(e,o,!0);else if(this.consumeOptionalCharacter(Nc))e=this.parseKeyedReadOrWrite(e,o,!1);else if(this.consumeOptionalCharacter(Ya))e=this.parseCall(e,o,!1);else if(this.consumeOptionalOperator("!"))e=new Kf(this.span(o),this.sourceSpan(o),e);else if(this.next.isTemplateLiteralEnd())e=this.parseNoInterpolationTaggedTemplateLiteral(e,o);else if(this.next.isTemplateLiteralPart())e=this.parseTaggedTemplateLiteral(e,o);else return e}parsePrimary(){let o=this.inputIndex;if(this.isArrowFunction())return this.parseArrowFunction(o);if(this.consumeOptionalCharacter(Ya)){this.rparensExpected++;let e=this.parsePipe();return this.consumeOptionalCharacter(Pr)||(this.error("Missing closing parentheses"),this.consumeOptionalCharacter(Pr)),this.rparensExpected--,new Zf(this.span(o),this.sourceSpan(o),e)}else{if(this.next.isKeywordNull())return this.advance(),new _s(this.span(o),this.sourceSpan(o),null);if(this.next.isKeywordUndefined())return this.advance(),new _s(this.span(o),this.sourceSpan(o),void 0);if(this.next.isKeywordTrue())return this.advance(),new _s(this.span(o),this.sourceSpan(o),!0);if(this.next.isKeywordFalse())return this.advance(),new _s(this.span(o),this.sourceSpan(o),!1);if(this.next.isKeywordIn())return this.advance(),new _s(this.span(o),this.sourceSpan(o),"in");if(this.next.isKeywordThis())return this.advance(),new $f(this.span(o),this.sourceSpan(o));if(this.consumeOptionalCharacter(Nc))return this.parseLiteralArray(o);if(this.next.isCharacter(_l))return this.parseLiteralMap();if(this.next.isIdentifier())return this.parseAccessMember(new Vc(this.span(o),this.sourceSpan(o)),o,!1);if(this.next.isNumber()){let e=this.next.toNumber();return this.advance(),new _s(this.span(o),this.sourceSpan(o),e)}else{if(this.next.isTemplateLiteralEnd())return this.parseNoInterpolationTemplateLiteral();if(this.next.isTemplateLiteralPart())return this.parseTemplateLiteral();if(this.next.isString()&&this.next.kind===Ym.Plain){let e=this.next.toString();return this.advance(),new _s(this.span(o),this.sourceSpan(o),e)}else return this.next.isPrivateIdentifier()?(this._reportErrorForPrivateIdentifier(this.next,null),new Ea(this.span(o),this.sourceSpan(o))):this.next.isRegExpBody()?this.parseRegularExpressionLiteral():this.index>=this.tokens.length?(this.error(`Unexpected end of expression: ${this.input}`),new Ea(this.span(o),this.sourceSpan(o))):(this.error(`Unexpected token ${this.next}`),new Ea(this.span(o),this.sourceSpan(o)))}}}parseLiteralArray(o){this.rbracketsExpected++;let e=[];do if(this.next.isOperator("..."))e.push(this.parseSpreadElement());else if(!this.next.isCharacter(Pd))e.push(this.parsePipe());else break;while(this.consumeOptionalCharacter(Oa));return this.rbracketsExpected--,this.expectCharacter(Pd),new Wf(this.span(o),this.sourceSpan(o),e)}parseLiteralMap(){let o=[],e=[],t=this.inputIndex;if(this.expectCharacter(_l),!this.consumeOptionalCharacter(Ka)){this.rbracesExpected++;do{let i=this.inputIndex;if(this.next.isOperator("...")){this.advance(),o.push({kind:"spread",span:this.span(i),sourceSpan:this.sourceSpan(i)}),e.push(this.parsePipe());continue}let r=this.next.isString(),s=this.expectIdentifierOrKeywordOrString(),d=this.span(i),m=this.sourceSpan(i),u={kind:"property",key:s,quoted:r,span:d,sourceSpan:m};o.push(u),r?(this.expectCharacter(Dc),e.push(this.parsePipe())):this.consumeOptionalCharacter(Dc)?e.push(this.parsePipe()):(u.isShorthandInitialized=!0,e.push(new Ac(d,m,m,new Vc(d,m),s)))}while(this.consumeOptionalCharacter(Oa)&&!this.next.isCharacter(Ka));this.rbracesExpected--,this.expectCharacter(Ka)}return new au(this.span(t),this.sourceSpan(t),o,e)}parseAccessMember(o,e,t){let i=this.inputIndex,r=this.withContext(wf.Writable,()=>{let d=this.expectIdentifierOrKeyword()??"";return d.length===0&&this.error("Expected identifier for property access",o.span.end),d}),s=this.sourceSpan(i);if(t)return this.isAssignmentOperator(this.next)?(this.advance(),this.error("The '?.' operator cannot be used in the assignment"),new Ea(this.span(e),this.sourceSpan(e))):new Hf(this.span(e),this.sourceSpan(e),s,o,r);if(this.isAssignmentOperator(this.next)){let d=this.next.strValue;if(!(this.parseFlags&1))return this.advance(),this.error("Bindings cannot contain assignments"),new Ea(this.span(e),this.sourceSpan(e));let m=new Ac(this.span(e),this.sourceSpan(e),s,o,r);this.advance();let u=this.parseConditional();return new qa(this.span(e),this.sourceSpan(e),d,m,u)}else return new Ac(this.span(e),this.sourceSpan(e),s,o,r)}parseCall(o,e,t){let i=this.inputIndex;this.rparensExpected++;let r=this.parseCallArguments(),s=this.span(i,this.inputIndex).toAbsolute(this.absoluteOffset);this.expectCharacter(Pr),this.rparensExpected--;let d=this.span(e),m=this.sourceSpan(e);return t?new Lx(d,m,o,r,s):new Vg(d,m,o,r,s)}parseCallArguments(){if(this.next.isCharacter(Pr))return[];let o=[];do o.push(this.next.isOperator("...")?this.parseSpreadElement():this.parsePipe());while(this.consumeOptionalCharacter(Oa));return o}parseSpreadElement(){this.next.isOperator("...")||this.error("Spread element must start with '...' operator");let o=this.inputIndex;this.advance();let e=this.parsePipe(),t=this.span(o),i=this.sourceSpan(o);return new Rx(t,i,e)}expectTemplateBindingKey(){let o="",e=!1,t=this.currentAbsoluteOffset;do o+=this.expectIdentifierOrKeywordOrString(),e=this.consumeOptionalOperator("-"),e&&(o+="-");while(e);return{source:o,span:new Gs(t,t+o.length)}}parseTemplateBindings(o){let e=[];for(e.push(...this.parseDirectiveKeywordBindings(o));this.index{this.rbracketsExpected++;let i=this.parsePipe();if(i instanceof Ea&&this.error("Key access cannot be empty"),this.rbracketsExpected--,this.expectCharacter(Pd),this.isAssignmentOperator(this.next)){let r=this.next.strValue;if(t)this.advance(),this.error("The '?.' operator cannot be used in the assignment");else{let s=new ru(this.span(e),this.sourceSpan(e),o,i);this.advance();let d=this.parseConditional();return new qa(this.span(e),this.sourceSpan(e),r,s,d)}}else return t?new Uf(this.span(e),this.sourceSpan(e),o,i):new ru(this.span(e),this.sourceSpan(e),o,i);return new Ea(this.span(e),this.sourceSpan(e))})}parseDirectiveKeywordBindings(o){let e=[];this.consumeOptionalCharacter(Dc);let t=this.getDirectiveBoundTarget(),i=this.currentAbsoluteOffset,r=this.parseAsBinding(o);r||(this.consumeStatementTerminator(),i=this.currentAbsoluteOffset);let s=new Gs(o.span.start,i);return e.push(new gP(s,o,t)),r&&e.push(r),e}getDirectiveBoundTarget(){if(this.next===gf||this.peekKeywordAs()||this.peekKeywordLet())return null;let o=this.parsePipe(),{start:e,end:t}=o.span,i=this.input.substring(e,t);return new Cs(o,i,_p(this.parseSourceSpan),this.absoluteOffset+e,this.errors)}parseAsBinding(o){if(!this.peekKeywordAs())return null;this.advance();let e=this.expectTemplateBindingKey();this.consumeStatementTerminator();let t=new Gs(o.span.start,this.currentAbsoluteOffset);return new Jf(t,e,o)}parseLetBinding(){if(!this.peekKeywordLet())return null;let o=this.currentAbsoluteOffset;this.advance();let e=this.expectTemplateBindingKey(),t=null;this.consumeOptionalOperator("=")&&(t=this.expectTemplateBindingKey()),this.consumeStatementTerminator();let i=new Gs(o,this.currentAbsoluteOffset);return new Jf(i,e,t)}parseNoInterpolationTaggedTemplateLiteral(o,e){let t=this.parseNoInterpolationTemplateLiteral();return new Xf(this.span(e),this.sourceSpan(e),o,t)}parseNoInterpolationTemplateLiteral(){let o=this.next.strValue,e=this.inputIndex;this.advance();let t=this.span(e),i=this.sourceSpan(e);return new Yf(t,i,[new zx(t,i,o)],[])}parseTaggedTemplateLiteral(o,e){let t=this.parseTemplateLiteral();return new Xf(this.span(e),this.sourceSpan(e),o,t)}parseTemplateLiteral(){let o=[],e=[],t=this.inputIndex;for(;this.next!==gf;){let i=this.next;if(i.isTemplateLiteralPart()||i.isTemplateLiteralEnd()){let r=this.inputIndex;if(this.advance(),o.push(new zx(this.span(r),this.sourceSpan(r),i.strValue)),i.isTemplateLiteralEnd())break}else if(i.isTemplateLiteralInterpolationStart()){this.advance(),this.rbracesExpected++;let r=this.parsePipe();r instanceof Ea?this.error("Template literal interpolation cannot be empty"):e.push(r),this.rbracesExpected--}else this.advance()}return new Yf(this.span(t),this.sourceSpan(t),o,e)}parseRegularExpressionLiteral(){let o=this.next;if(this.advance(),!o.isRegExpBody())return new Ea(this.span(this.inputIndex),this.sourceSpan(this.inputIndex));let e=null;if(this.next.isRegExpFlags()){e=this.next,this.advance();let r=new Set;for(let s=0;s`"${m}"`).join(", "),e.index+s)}}let t=o.index,i=e?e.end:o.end;return new Bx(this.span(t,i),this.sourceSpan(t,i),o.strValue,e?e.strValue:null)}parseArrowFunction(o){let e;if(this.next.isIdentifier()){let i=this.next;this.advance(),e=[this.getArrowFunctionIdentifierArg(i)]}else this.next.isCharacter(Ya)?(this.rparensExpected++,this.advance(),e=this.parseArrowFunctionParameters(),this.rparensExpected--):(e=[],this.error(`Unexpected token ${this.next}`));this.expectOperator("=>");let t;if(this.next.isCharacter(_l))this.error("Multi-line arrow functions are not supported. If you meant to return an object literal, wrap it with parentheses."),t=new Ea(this.span(o),this.sourceSpan(o));else{let i=this.parseFlags;this.parseFlags=1,t=this.parseExpression(),this.parseFlags=i}return new Vx(this.span(o),this.sourceSpan(o),e,t)}parseArrowFunctionParameters(){let o=[];if(!this.consumeOptionalCharacter(Pr))for(;this.next!==gf;)if(this.next.isIdentifier()){let e=this.next;if(this.advance(),o.push(this.getArrowFunctionIdentifierArg(e)),this.consumeOptionalCharacter(Pr))break;this.expectCharacter(Oa)}else{this.error(`Unexpected token ${this.next}`);break}return o}getArrowFunctionIdentifierArg(o){return new uP(o.strValue,this.span(o.index),this.sourceSpan(o.index))}isArrowFunction(){let o=this.index,e=this.tokens;if(o>e.length-2)return!1;if(e[o].isIdentifier()&&e[o+1].isOperator("=>"))return!0;if(e[o].isCharacter(Ya)){let t=o+1;for(t;t")}return!1}consumeStatementTerminator(){this.consumeOptionalCharacter(fs)||this.consumeOptionalCharacter(Oa)}error(o,e=this.index){this.errors.push(Eg(o,this.input,this.getErrorLocationText(e),this.parseSourceSpan)),this.skip()}getErrorLocationText(o){return o0&&(e=` ${e} `);let i=_p(t),r=`Parser Error: ${n}${e}[${o}] in ${i}`;return new dn(t,r)}var nT=class extends Bg{errors=[];visitPipe(){this.errors.push("pipes")}};function $q(n){let o=new Map,e=0,t=0,i=0;for(;id+m.length,0);t+=s,e+=s}o.set(t,e),i++}return o}function Hq(n){return n.visit(new iT)}var iT=class{visitUnary(o,e){return`${o.operator}${o.expr.visit(this,e)}`}visitBinary(o,e){return`${o.left.visit(this,e)} ${o.operation} ${o.right.visit(this,e)}`}visitChain(o,e){return o.expressions.map(t=>t.visit(this,e)).join("; ")}visitConditional(o,e){return`${o.condition.visit(this,e)} ? ${o.trueExp.visit(this,e)} : ${o.falseExp.visit(this,e)}`}visitThisReceiver(){return"this"}visitImplicitReceiver(){return""}visitInterpolation(o,e){return Wq(o.strings,o.expressions.map(t=>t.visit(this,e))).join("")}visitKeyedRead(o,e){return`${o.receiver.visit(this,e)}[${o.key.visit(this,e)}]`}visitLiteralArray(o,e){return`[${o.expressions.map(t=>t.visit(this,e)).join(", ")}]`}visitLiteralMap(o,e){return`{${Uq(o.keys.map(t=>t.kind==="spread"?"...":t.quoted?`'${t.key}'`:t.key),o.values.map(t=>t.visit(this,e))).map(([t,i])=>`${t}: ${i}`).join(", ")}}`}visitLiteralPrimitive(o){if(o.value===null)return"null";switch(typeof o.value){case"number":case"boolean":return o.value.toString();case"undefined":return"undefined";case"string":return`'${o.value.replace(/'/g,"\\'")}'`;default:throw new Error(`Unsupported primitive type: ${o.value}`)}}visitPipe(o,e){return`${o.exp.visit(this,e)} | ${o.name}`}visitPrefixNot(o,e){return`!${o.expression.visit(this,e)}`}visitNonNullAssert(o,e){return`${o.expression.visit(this,e)}!`}visitPropertyRead(o,e){return o.receiver instanceof Vc||o.receiver instanceof $f?o.name:`${o.receiver.visit(this,e)}.${o.name}`}visitSafePropertyRead(o,e){return`${o.receiver.visit(this,e)}?.${o.name}`}visitSafeKeyedRead(o,e){return`${o.receiver.visit(this,e)}?.[${o.key.visit(this,e)}]`}visitCall(o,e){return`${o.receiver.visit(this,e)}(${o.args.map(t=>t.visit(this,e)).join(", ")})`}visitSafeCall(o,e){return`${o.receiver.visit(this,e)}?.(${o.args.map(t=>t.visit(this,e)).join(", ")})`}visitTypeofExpression(o,e){return`typeof ${o.expression.visit(this,e)}`}visitVoidExpression(o,e){return`void ${o.expression.visit(this,e)}`}visitRegularExpressionLiteral(o,e){return`/${o.body}/${o.flags||""}`}visitArrowFunction(o,e){let t;return o.parameters.length===1?t=o.parameters[0].name:t=`(${o.parameters.map(i=>i.name).join(", ")})`,`${t} => ${o.body.visit(this,e)}`}visitASTWithSource(o,e){return o.ast.visit(this,e)}visitTemplateLiteral(o,e){let t="";for(let i=0;i[e,o[t]])}function Wq(n,o){let e=[];for(let t=0;t(n.set(o,e),n),new Map),Yg=class extends oT{_schema=new Map;_eventSchema=new Map;constructor(){super(),Xq.forEach(o=>{let e=new Map,t=new Set,[i,r]=o.split("|"),s=r.split(","),[d,m]=i.split("^");d.split(",").forEach(h=>{this._schema.set(h.toLowerCase(),e),this._eventSchema.set(h.toLowerCase(),t)});let u=m&&this._schema.get(m.toLowerCase());if(u){for(let[h,v]of u)e.set(h,v);for(let h of this._eventSchema.get(m.toLowerCase()))t.add(h)}s.forEach(h=>{if(h.length>0)switch(h[0]){case"*":t.add(h.substring(1));break;case"!":e.set(h.substring(1),Gq);break;case"#":e.set(h.substring(1),qq);break;case"%":e.set(h.substring(1),Kq);break;default:e.set(h,Qq)}})})}hasProperty(o,e,t){if(t.some(s=>s.name===v5.name))return!0;let i=hf(o);if(i.includes("-")){if(D5(i)||_P(i))return!1;if(t.some(s=>s.name===C5.name))return!0}return(this._schema.get(i)||this._schema.get("unknown")).has(e)}hasElement(o,e){if(e.some(i=>i.name===v5.name))return!0;let t=hf(o);return t.includes("-")&&(D5(t)||_P(t)||e.some(i=>i.name===C5.name))?!0:this._schema.has(t)}securityContext(o,e,t){t&&(e=this.getMappedPropName(e));let i=hf(o);e=e.toLowerCase();let[r]=ha(i,!1),s=CH();return s[i+"|"+e]??(r?s[`:${r}:*|${e}`]:void 0)??s["*|"+e]??mi.NONE}getMappedPropName(o){return dF.get(o)??o}getDefaultComponentElementName(){return"ng-component"}validateProperty(o){return o.toLowerCase().startsWith("on")?{error:!0,msg:`Binding to event property '${o}' is disallowed for security reasons, please use (${o.slice(2)})=... +If '${o}' is a directive input, make sure the directive is imported by the current module.`}:{error:!1}}validateAttribute(o){return o.toLowerCase().startsWith("on")?{error:!0,msg:`Binding to event attribute '${o}' is disallowed for security reasons, please use (${o.slice(2)})=...`}:{error:!1}}allKnownElementNames(){return Array.from(this._schema.keys())}allKnownAttributesOfElement(o){let e=hf(o),t=this._schema.get(e)||this._schema.get("unknown");return Array.from(t.keys()).map(i=>Yq.get(i)??i)}allKnownEventsOfElement(o){let e=hf(o);return Array.from(this._eventSchema.get(e)??[])}normalizeAnimationStyleProperty(o){return qH(o)}normalizeAnimationStyleValue(o,e,t){let i="",r=t.toString().trim(),s=null;if(Zq(o)&&t!==0&&t!=="0")if(typeof t=="number")i="px";else{let d=t.match(/^[+-]?[\d\.]+([a-z]*)$/);d&&d[1].length==0&&(s=`Please provide a CSS unit value for ${e}:${t}`)}return{error:s,value:r+i}}};function Zq(n){switch(n){case"width":case"height":case"minWidth":case"minHeight":case"maxWidth":case"maxHeight":case"left":case"top":case"bottom":case"right":case"fontSize":case"outlineWidth":case"outlineOffset":case"paddingTop":case"paddingLeft":case"paddingBottom":case"paddingRight":case"marginTop":case"marginLeft":case"marginBottom":case"marginRight":case"borderRadius":case"borderWidth":case"borderTopWidth":case"borderLeftWidth":case"borderRightWidth":case"borderBottomWidth":case"textIndent":return!0;default:return!1}}var ai=class{closedByChildren={};contentType;closedByParent=!1;implicitNamespacePrefix;isVoid;ignoreFirstLf;canSelfClose;preventNamespaceInheritance;constructor({closedByChildren:o,implicitNamespacePrefix:e,contentType:t=Oc.PARSABLE_DATA,closedByParent:i=!1,isVoid:r=!1,ignoreFirstLf:s=!1,preventNamespaceInheritance:d=!1,canSelfClose:m=!1}={}){o&&o.length>0&&o.forEach(u=>this.closedByChildren[u]=!0),this.isVoid=r,this.closedByParent=i||r,this.implicitNamespacePrefix=e||null,this.contentType=t,this.ignoreFirstLf=s,this.preventNamespaceInheritance=d,this.canSelfClose=m??r}isClosedByChild(o){return this.isVoid||o.toLowerCase()in this.closedByChildren}getContentType(o){return typeof this.contentType=="object"?(o===void 0?void 0:this.contentType[o])??this.contentType.default:this.contentType}},v6,yg;function rT(n){return yg||(v6=new ai({canSelfClose:!0}),yg=Object.assign(Object.create(null),{base:new ai({isVoid:!0}),meta:new ai({isVoid:!0}),area:new ai({isVoid:!0}),embed:new ai({isVoid:!0}),link:new ai({isVoid:!0}),img:new ai({isVoid:!0}),input:new ai({isVoid:!0}),param:new ai({isVoid:!0}),hr:new ai({isVoid:!0}),br:new ai({isVoid:!0}),source:new ai({isVoid:!0}),track:new ai({isVoid:!0}),wbr:new ai({isVoid:!0}),p:new ai({closedByChildren:["address","article","aside","blockquote","div","dl","fieldset","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","hr","main","nav","ol","p","pre","section","table","ul"],closedByParent:!0}),thead:new ai({closedByChildren:["tbody","tfoot"]}),tbody:new ai({closedByChildren:["tbody","tfoot"],closedByParent:!0}),tfoot:new ai({closedByChildren:["tbody"],closedByParent:!0}),tr:new ai({closedByChildren:["tr"],closedByParent:!0}),td:new ai({closedByChildren:["td","th"],closedByParent:!0}),th:new ai({closedByChildren:["td","th"],closedByParent:!0}),col:new ai({isVoid:!0}),svg:new ai({implicitNamespacePrefix:"svg"}),foreignObject:new ai({implicitNamespacePrefix:"svg",preventNamespaceInheritance:!0}),math:new ai({implicitNamespacePrefix:"math"}),li:new ai({closedByChildren:["li"],closedByParent:!0}),dt:new ai({closedByChildren:["dt","dd"]}),dd:new ai({closedByChildren:["dt","dd"],closedByParent:!0}),rb:new ai({closedByChildren:["rb","rt","rtc","rp"],closedByParent:!0}),rt:new ai({closedByChildren:["rb","rt","rtc","rp"],closedByParent:!0}),rtc:new ai({closedByChildren:["rb","rtc","rp"],closedByParent:!0}),rp:new ai({closedByChildren:["rb","rt","rtc","rp"],closedByParent:!0}),optgroup:new ai({closedByChildren:["optgroup"],closedByParent:!0}),option:new ai({closedByChildren:["option","optgroup"],closedByParent:!0}),pre:new ai({ignoreFirstLf:!0}),listing:new ai({ignoreFirstLf:!0}),style:new ai({contentType:Oc.RAW_TEXT}),script:new ai({contentType:Oc.RAW_TEXT}),title:new ai({contentType:{default:Oc.ESCAPABLE_RAW_TEXT,svg:Oc.PARSABLE_DATA}}),textarea:new ai({contentType:Oc.ESCAPABLE_RAW_TEXT,ignoreFirstLf:!0})}),new Yg().allKnownElementNames().forEach(o=>{!yg[o]&&fP(o)===null&&(yg[o]=new ai({canSelfClose:!1}))})),yg[n]??yg[n.toLowerCase()]??v6}var x6={A:"LINK",B:"BOLD_TEXT",BR:"LINE_BREAK",EM:"EMPHASISED_TEXT",H1:"HEADING_LEVEL1",H2:"HEADING_LEVEL2",H3:"HEADING_LEVEL3",H4:"HEADING_LEVEL4",H5:"HEADING_LEVEL5",H6:"HEADING_LEVEL6",HR:"HORIZONTAL_RULE",I:"ITALIC_TEXT",LI:"LIST_ITEM",LINK:"MEDIA_LINK",OL:"ORDERED_LIST",P:"PARAGRAPH",Q:"QUOTATION",S:"STRIKETHROUGH_TEXT",SMALL:"SMALL_TEXT",SUB:"SUBSTRIPT",SUP:"SUPERSCRIPT",TBODY:"TABLE_BODY",TD:"TABLE_CELL",TFOOT:"TABLE_FOOTER",TH:"TABLE_HEADER_CELL",THEAD:"TABLE_HEADER",TR:"TABLE_ROW",TT:"MONOSPACED_TEXT",U:"UNDERLINED_TEXT",UL:"UNORDERED_LIST"},aT=class{_placeHolderNameCounts={};_signatureToName={};getStartTagPlaceholderName(o,e,t){let i=this._hashTag(o,e,t);if(this._signatureToName[i])return this._signatureToName[i];let r=o.toUpperCase(),s=x6[r]||`TAG_${r}`,d=this._generateUniqueName(t?s:`START_${s}`);return this._signatureToName[i]=d,d}getCloseTagPlaceholderName(o){let e=this._hashClosingTag(o);if(this._signatureToName[e])return this._signatureToName[e];let t=o.toUpperCase(),i=x6[t]||`TAG_${t}`,r=this._generateUniqueName(`CLOSE_${i}`);return this._signatureToName[e]=r,r}getPlaceholderName(o,e){let t=o.toUpperCase(),i=`PH: ${t}=${e}`;if(this._signatureToName[i])return this._signatureToName[i];let r=this._generateUniqueName(t);return this._signatureToName[i]=r,r}getUniquePlaceholder(o){return this._generateUniqueName(o.toUpperCase())}getStartBlockPlaceholderName(o,e){let t=this._hashBlock(o,e);if(this._signatureToName[t])return this._signatureToName[t];let i=this._generateUniqueName(`START_BLOCK_${this._toSnakeCase(o)}`);return this._signatureToName[t]=i,i}getCloseBlockPlaceholderName(o){let e=this._hashClosingBlock(o);if(this._signatureToName[e])return this._signatureToName[e];let t=this._generateUniqueName(`CLOSE_BLOCK_${this._toSnakeCase(o)}`);return this._signatureToName[e]=t,t}_hashTag(o,e,t){let i=`<${o}`,r=Object.keys(e).sort().map(d=>` ${d}=${e[d]}`).join(""),s=t?"/>":`>`;return i+r+s}_hashClosingTag(o){return this._hashTag(`/${o}`,{},!1)}_hashBlock(o,e){let t=e.length===0?"":` (${e.sort().join("; ")})`;return`@${o}${t} {}`}_hashClosingBlock(o){return this._hashBlock(`close_${o}`,[])}_toSnakeCase(o){return o.toUpperCase().replace(/[^A-Z0-9]/g,"_")}_generateUniqueName(o){if(!this._placeHolderNameCounts.hasOwnProperty(o))return this._placeHolderNameCounts[o]=1,o;let t=this._placeHolderNameCounts[o];return this._placeHolderNameCounts[o]=t+1,`${o}_${t}`}},Jq=new C1(new k0);function eQ(n,o){let e=new sT(Jq,n,o);return(t,i,r,s,d)=>e.toI18nMessage(t,i,r,s,d)}function tQ(n,o){return o}var sT=class{_expressionParser;_retainEmptyTokens;_preserveExpressionWhitespace;constructor(o,e,t){this._expressionParser=o,this._retainEmptyTokens=e,this._preserveExpressionWhitespace=t}toI18nMessage(o,e="",t="",i="",r){let s={isIcu:o.length==1&&o[0]instanceof Xm,icuDepth:0,placeholderRegistry:new aT,placeholderToContent:{},placeholderToMessage:{},visitNodeFn:r||tQ},d=Mo(this,o,s);return new Ja(d,s.placeholderToContent,s.placeholderToMessage,e,t,i)}visitElement(o,e){return this._visitElementLike(o,e)}visitComponent(o,e){return this._visitElementLike(o,e)}visitDirective(o,e){throw new Error("Unreachable code")}visitAttribute(o,e){let t=o.valueTokens===void 0||o.valueTokens.length===1?new df(o.value,o.valueSpan||o.sourceSpan):this._visitTextWithInterpolation(o.valueTokens,o.valueSpan||o.sourceSpan,e,o.i18n);return e.visitNodeFn(o,t)}visitText(o,e){let t=o.tokens.length===1?new df(o.value,o.sourceSpan):this._visitTextWithInterpolation(o.tokens,o.sourceSpan,e,o.i18n);return e.visitNodeFn(o,t)}visitComment(o,e){return null}visitExpansion(o,e){e.icuDepth++;let t={},i=new Kx(o.switchValue,o.type,t,o.sourceSpan);if(o.cases.forEach(d=>{t[d.value]=new Ed(d.expression.map(m=>m.visit(this,e)),d.expSourceSpan)}),e.icuDepth--,e.isIcu||e.icuDepth>0){let d=e.placeholderRegistry.getUniquePlaceholder(`VAR_${o.type}`);return i.expressionPlaceholder=d,e.placeholderToContent[d]={text:o.switchValue,sourceSpan:o.switchValueSourceSpan},e.visitNodeFn(o,i)}let r=e.placeholderRegistry.getPlaceholderName("ICU",o.sourceSpan.toString());e.placeholderToMessage[r]=this.toI18nMessage([o],"","","",void 0);let s=new Wg(i,r,o.sourceSpan);return e.visitNodeFn(o,s)}visitExpansionCase(o,e){throw new Error("Unreachable code")}visitBlock(o,e){let t=Mo(this,o.children,e);if(o.name==="switch")return new Ed(t,o.sourceSpan);let i=o.parameters.map(m=>m.expression),r=e.placeholderRegistry.getStartBlockPlaceholderName(o.name,i),s=e.placeholderRegistry.getCloseBlockPlaceholderName(o.name);e.placeholderToContent[r]={text:o.startSourceSpan.toString(),sourceSpan:o.startSourceSpan},e.placeholderToContent[s]={text:o.endSourceSpan?o.endSourceSpan.toString():"}",sourceSpan:o.endSourceSpan??o.sourceSpan};let d=new Sp(o.name,i,r,s,t,o.sourceSpan,o.startSourceSpan,o.endSourceSpan);return e.visitNodeFn(o,d)}visitBlockParameter(o,e){throw new Error("Unreachable code")}visitLetDeclaration(o,e){return null}_visitElementLike(o,e){let t=Mo(this,o.children,e),i={},r=v=>{i[v.name]=v.value},s,d;o instanceof ml?(s=o.name,d=rT(o.name).isVoid):(s=o.fullName,d=o.tagName?rT(o.tagName).isVoid:!1),o.attrs.forEach(r),o.directives.forEach(v=>v.attrs.forEach(r));let m=e.placeholderRegistry.getStartTagPlaceholderName(s,i,d);e.placeholderToContent[m]={text:o.startSourceSpan.toString(),sourceSpan:o.startSourceSpan};let u="";d||(u=e.placeholderRegistry.getCloseTagPlaceholderName(s),e.placeholderToContent[u]={text:``,sourceSpan:o.endSourceSpan??o.sourceSpan});let h=new Mp(s,i,m,u,t,d,o.sourceSpan,o.startSourceSpan,o.endSourceSpan);return e.visitNodeFn(o,h)}_visitTextWithInterpolation(o,e,t,i){let r=[],s=!1;for(let d of o)switch(d.type){case 8:case 17:s=!0;let[m,u,h]=d.parts,v=aQ(u)||"INTERPOLATION",b=t.placeholderRegistry.getPlaceholderName(v,u);if(this._preserveExpressionWhitespace)t.placeholderToContent[b]={text:d.parts.join(""),sourceSpan:d.sourceSpan},r.push(new l0(u,b,d.sourceSpan));else{let y=this.normalizeExpression(d);t.placeholderToContent[b]={text:`${m}${y}${h}`,sourceSpan:d.sourceSpan},r.push(new l0(y,b,d.sourceSpan))}break;default:if(d.parts[0].length>0||this._retainEmptyTokens){let y=r[r.length-1];y instanceof df?(y.value+=d.parts[0],y.sourceSpan=new On(y.sourceSpan.start,d.sourceSpan.end,y.sourceSpan.fullStart,y.sourceSpan.details)):r.push(new df(d.parts[0],d.sourceSpan))}else this._retainEmptyTokens&&r.push(new df(d.parts[0],d.sourceSpan));break}return s?(nQ(r,i),new Ed(r,e)):r[0]}normalizeExpression(o){let e=o.parts[1],t=this._expressionParser.parseBinding(e,o.sourceSpan,o.sourceSpan.start.offset);return Hq(t)}};function nQ(n,o){if(o instanceof Ja&&(iQ(o),o=o.nodes[0]),o instanceof Ed){oQ(o.children,n);for(let e=0;e`"${e.sourceSpan.toString()}"`).join(` +`)} + +Second pass (${o.length} tokens): +${o.map(e=>`"${e.sourceSpan.toString()}"`).join(` +`)} + `.trim());if(n.some((e,t)=>o[t].constructor!==e.constructor))throw new Error("The types of the i18n message children changed between first and second pass.")}var rQ=/\/\/[\s\S]*i18n[\s\S]*\([\s\S]*ph[\s\S]*=[\s\S]*("|')([\s\S]*?)\1[\s\S]*\)/g;function aQ(n){return n.split(rQ)[2]}var y6=new Set(["iframe|srcdoc","*|innerhtml","*|outerhtml","embed|src","iframe|src","object|codebase","object|data"]);function b6(n,o){return n=n.toLowerCase(),o=o.toLowerCase(),y6.has(n+"|"+o)||y6.has("*|"+o)}var sQ=n=>(o,e)=>{let t=n.get(o)??o;return t instanceof Ad&&(e instanceof Wg&&t.i18n instanceof Ja&&(e.previousMessage=t.i18n),t.i18n=e),e},v1=class{keepI18nAttrs;enableI18nLegacyMessageIdFormat;preserveSignificantWhitespace;retainEmptyTokens;hasI18nMeta=!1;_errors=[];constructor(o=!1,e=!1,t=!0,i=!t){this.keepI18nAttrs=o,this.enableI18nLegacyMessageIdFormat=e,this.preserveSignificantWhitespace=t,this.retainEmptyTokens=i}_generateI18nMessage(o,e="",t){let{meaning:i,description:r,customId:s}=this._parseMetadata(e),m=eQ(this.retainEmptyTokens,this.preserveSignificantWhitespace)(o,i,r,s,t);return this._setMessageId(m,e),this._setLegacyIds(m,e),m}visitAllWithErrors(o){let e=o.map(t=>t.visit(this,null));return new _1(e,this._errors)}visitElement(o){return this._visitElementLike(o),o}visitComponent(o,e){return this._visitElementLike(o),o}visitExpansion(o,e){let t,i=o.i18n;if(this.hasI18nMeta=!0,i instanceof Wg){let r=i.name;t=this._generateI18nMessage([o],i);let s=wN(t);s.name=r,e!==null&&(e.placeholderToMessage[r]=t)}else t=this._generateI18nMessage([o],e||i);return o.i18n=t,o}visitText(o){return o}visitAttribute(o){return o}visitComment(o){return o}visitExpansionCase(o){return o}visitBlock(o,e){return Mo(this,o.children,e),o}visitBlockParameter(o,e){return o}visitLetDeclaration(o,e){return o}visitDirective(o,e){return o}_visitElementLike(o){let e;if(yU(o)){this.hasI18nMeta=!0;let t=[],i={};for(let r of o.attrs)if(r.name===yN){let s=o.i18n||r.value,d=new Map,m=this.preserveSignificantWhitespace?o.children:Pc(new f1(!1,d),o.children);e=this._generateI18nMessage(m,s,sQ(d)),e.nodes.length===0&&(e=void 0),o.i18n=e}else if(r.name.startsWith(PP)){let s=r.name.slice(PP.length),d;o instanceof Qa?d=o.tagName===null?!1:b6(o.tagName,s):d=b6(o.name,s),d||mQ(s)?this._reportError(r,`Translating attribute '${s}' is disallowed for security reasons.`):i[s]=r.value}else t.push(r);if(Object.keys(i).length)for(let r of t){let s=i[r.name];s!==void 0&&r.value&&(r.i18n=this._generateI18nMessage([r],r.i18n||s))}this.keepI18nAttrs||(o.attrs=t)}Mo(this,o.children,e)}_parseMetadata(o){return typeof o=="string"?dQ(o):o instanceof Ja?o:{}}_setMessageId(o,e){o.id||(o.id=e instanceof Ja&&e.id||wH(o))}_setLegacyIds(o,e){if(this.enableI18nLegacyMessageIdFormat)o.legacyIds=[bH(o),mN(o)];else if(typeof e!="string"){let t=e instanceof Ja?e:e instanceof Wg?e.previousMessage:void 0;o.legacyIds=t?t.legacyIds:[]}}_reportError(o,e){this._errors.push(new dn(o.sourceSpan,e))}},lQ="|",cQ="@@";function dQ(n=""){let o,e,t;if(n=n.trim(),n){let i=n.indexOf(cQ),r=n.indexOf(lQ),s;[s,o]=i>-1?[n.slice(0,i),n.slice(i+2)]:[n,""],[e,t]=r>-1?[s.slice(0,r),s.slice(r+1)]:["",s]}return{customId:o,meaning:e,description:t}}function pQ(n){let o=[];return n.description?o.push({tagName:"desc",text:n.description}):o.push({tagName:"suppress",text:"{msgDescriptions}"}),n.meaning&&o.push({tagName:"meaning",text:n.meaning}),zH(o)}function mQ(n){let o=n.toLowerCase();return o.length>2&&o!=="only"&&o.startsWith("on")}var uQ="goog.getMsg";function gQ(n,o,e,t){let i=_Q(o),r=[Oe(i)];Object.keys(t).length&&(r.push(UP(zT(t,!0),!0)),r.push(UP({original_code:yl(Object.keys(t).map(m=>({key:A0(m),quoted:!0,value:o.placeholders[m]?Oe(o.placeholders[m].sourceSpan.toString()):Oe(o.placeholderToMessage[m].nodes.map(u=>u.sourceSpan.toString()).join(""))})))})));let s=new Ur(e.name,si(uQ).callFn(r),ec,ua.Final);s.addLeadingComment(pQ(o));let d=new fa(n.set(e));return[s,d]}var lT=class{formatPh(o){return`{$${A0(o)}}`}visitText(o){return o.value}visitContainer(o){return o.children.map(e=>e.visit(this)).join("")}visitIcu(o){return iF(o)}visitTagPlaceholder(o){return o.isVoid?this.formatPh(o.startName):`${this.formatPh(o.startName)}${o.children.map(e=>e.visit(this)).join("")}${this.formatPh(o.closeName)}`}visitPlaceholder(o){return this.formatPh(o.name)}visitBlockPlaceholder(o){return`${this.formatPh(o.startName)}${o.children.map(e=>e.visit(this)).join("")}${this.formatPh(o.closeName)}`}visitIcuPlaceholder(o,e){return this.formatPh(o.name)}},hQ=new lT;function _Q(n){return n.nodes.map(o=>o.visit(hQ,null)).join("")}function fQ(n,o,e){let{messageParts:t,placeHolders:i}=CQ(o),r=vQ(o),s=i.map(u=>e[u.text]),d=jH(o,t,i,s,r),m=n.set(d);return[new fa(m)]}var cT=class{placeholderToMessage;pieces;constructor(o,e){this.placeholderToMessage=o,this.pieces=e}visitText(o){if(this.pieces[this.pieces.length-1]instanceof Gm)this.pieces[this.pieces.length-1].text+=o.value;else{let e=new On(o.sourceSpan.fullStart,o.sourceSpan.end,o.sourceSpan.fullStart,o.sourceSpan.details);this.pieces.push(new Gm(o.value,e))}}visitContainer(o){o.children.forEach(e=>e.visit(this))}visitIcu(o){this.pieces.push(new Gm(iF(o),o.sourceSpan))}visitTagPlaceholder(o){this.pieces.push(this.createPlaceholderPiece(o.startName,o.startSourceSpan??o.sourceSpan)),o.isVoid||(o.children.forEach(e=>e.visit(this)),this.pieces.push(this.createPlaceholderPiece(o.closeName,o.endSourceSpan??o.sourceSpan)))}visitPlaceholder(o){this.pieces.push(this.createPlaceholderPiece(o.name,o.sourceSpan))}visitBlockPlaceholder(o){this.pieces.push(this.createPlaceholderPiece(o.startName,o.startSourceSpan??o.sourceSpan)),o.children.forEach(e=>e.visit(this)),this.pieces.push(this.createPlaceholderPiece(o.closeName,o.endSourceSpan??o.sourceSpan))}visitIcuPlaceholder(o){this.pieces.push(this.createPlaceholderPiece(o.name,o.sourceSpan,this.placeholderToMessage[o.name]))}createPlaceholderPiece(o,e,t){return new Og(A0(o,!1),e,t)}};function CQ(n){let o=[],e=new cT(n.placeholderToMessage,o);return n.nodes.forEach(t=>t.visit(e)),xQ(o)}function vQ(n){let o=n.nodes[0],e=n.nodes[n.nodes.length-1];return new On(o.sourceSpan.fullStart,e.sourceSpan.end,o.sourceSpan.fullStart,o.sourceSpan.details)}function xQ(n){let o=[],e=[];n[0]instanceof Og&&o.push(Uk(n[0].sourceSpan.start));for(let t=0;t{let P=b.has(x.name);return b.add(x.name),!P});let y=v.flatMap(x=>{let P=s.get(x.context);if(P===void 0)throw new Error("AssertionError: Could not find i18n expression's value");return[Oe(x.name),P]});h.i18nAttributesConfig=n.addConst(new zc(y))}for(let m of n.units)for(let u of m.create)if(u.kind===j.I18nStart){let h=d.get(u.root);if(h===void 0)throw new Error("AssertionError: Could not find corresponding i18n block index for an i18n message op; was an i18n message incorrectly assumed to correspond to an attribute?");u.messageIndex=h}}function pF(n,o,e,t){let i=[],r=new Map;for(let u of t.subMessages){let h=e.get(u),{mainVar:v,statements:b}=pF(n,o,e,h);i.push(...b);let y=r.get(h.messagePlaceholder)??[];y.push(v),r.set(h.messagePlaceholder,y)}kQ(t,r),t.params=new Map([...t.params.entries()].sort());let s=si(n.pool.uniqueName(yQ)),d=EQ(n.pool,t.message.id,o,n.i18nUseExternalIds),m;if(t.needsPostprocessing||t.postprocessingParams.size>0){let u=Object.fromEntries([...t.postprocessingParams.entries()].sort()),h=zT(u,!1),v=[];t.postprocessingParams.size>0&&v.push(UP(h,!0)),m=b=>Vt(ge.i18nPostprocess).callFn([b,...v])}return i.push(...PQ(t.message,s,d,t.params,m)),{mainVar:s,statements:i}}function kQ(n,o){for(let[e,t]of o)t.length===1?n.params.set(e,t[0]):(n.params.set(e,Oe(`${M6}${bQ}${e}${M6}`)),n.postprocessingParams.set(e,Ji(t)))}function PQ(n,o,e,t,i){let r=Object.fromEntries(t),s=[MQ(o),T1(TQ(),gQ(o,n,e,r),fQ(o,n,zT(r,!1)))];return i&&s.push(new fa(o.set(i(o)))),s}function TQ(){return D0(si(w6)).notIdentical(Oe("undefined",FT)).and(si(w6))}function EQ(n,o,e,t){let i,r=e;if(t){let s=S6("EXTERNAL_"),d=n.uniqueName(r);i=`${s}${Bm(o)}$$${d}`}else{let s=S6(r);i=n.uniqueName(s)}return si(i)}function OQ(n){for(let o of n.units){let e=null,t=null,i=new Map,r=new Map,s=new Map;for(let d of o.create)switch(d.kind){case j.I18nStart:if(d.context===null)throw Error("I18n op should have its context set.");e=d;break;case j.I18nEnd:e=null;break;case j.IcuStart:if(d.context===null)throw Error("Icu op should have its context set.");t=d;break;case j.IcuEnd:t=null;break;case j.Text:if(e!==null)if(i.set(d.xref,e),r.set(d.xref,t),d.icuPlaceholder!==null){let m=nG(n.allocateXrefId(),d.icuPlaceholder,[d.initialValue]);Je.replace(d,m),s.set(d.xref,m)}else Je.remove(d);break}for(let d of o.update)switch(d.kind){case j.InterpolateText:if(!i.has(d.target))continue;let m=i.get(d.target),u=r.get(d.target),h=s.get(d.target),v=u?u.context:m.context,b=u?g0.Postproccessing:g0.Creation,y=[];for(let x=0;x0){let t=IQ(e.localRefs);e.localRefs=n.addConst(t)}else e.localRefs=null;break}}function IQ(n){let o=[];for(let e of n)o.push(Oe(e.name),Oe(e.target));return Ji(o)}function AQ(n){for(let o of n.units){let e=Tr.HTML;for(let t of o.create)t.kind===j.ElementStart&&t.namespace!==e&&(Je.insertBefore(QW(t.namespace),t),e=t.namespace)}}function NQ(n){let o=[],e=0,t=0,i=0,r=0,s=0,d=null;for(;e0&&t===0&&i===0){let u=n.substring(r,e-1).trim();o.push(d,u),s=e,r=0,d=null}break}if(d&&r){let m=n.slice(r).trim();o.push(d,m)}return o}function mF(n){return n.replace(/[a-z][A-Z]/g,o=>o.charAt(0)+"-"+o.charAt(1)).toLowerCase()}function FQ(n){let o=new Map;for(let e of n.units)for(let t of e.create)Dp(t)&&o.set(t.xref,t);for(let e of n.units)for(let t of e.create)if(t.kind===j.ExtractedAttribute&&t.bindingKind===qt.Attribute&&UN(t.expression)){let i=o.get(t.target);if(i!==void 0&&(i.kind===j.Template||i.kind===j.ConditionalCreate||i.kind===j.ConditionalBranchCreate)&&i.templateKind===vs.Structural)continue;if(t.name==="style"){let r=NQ(t.expression.value);for(let s=0;s{if(!(!(r instanceof Od)||r.name!==null)){if(!t.has(r.xref))throw new Error(`Variable ${r.xref} not yet named`);r.name=t.get(r.xref)}})}function LQ(n,o){if(n.name===null)switch(n.kind){case oa.Context:n.name=`ctx_r${o.index++}`;break;case oa.Identifier:let e=n.identifier===Ys?"i":"";n.name=`${n.identifier}_${e}r${++o.index}`;break;default:n.name=`_r${++o.index}`;break}return n.name}function zQ(n){return n.startsWith("--")?n:mF(n)}function k6(n){let o=n.indexOf("!important");return o>-1?n.substring(0,o):n}function VQ(n){for(let o of n.units){for(let e of o.functions)Wk(e.ops);for(let e of o.create)(e.kind===j.Listener||e.kind===j.Animation||e.kind===j.AnimationListener||e.kind===j.TwoWayListener)&&Wk(e.handlerOps);Wk(o.update)}}function Wk(n){for(let o of n){if(o.kind!==j.Statement||!(o.statement instanceof fa)||!(o.statement.expr instanceof i1))continue;let e=o.statement.expr.steps,t=!0;for(let i=o.next;i.kind!==j.ListEnd&&t;i=i.next)xr(i,(r,s)=>{if(!$c(r))return r;if(t&&!(s&ii.InChildOperation))switch(r.kind){case Jt.NextContext:r.steps+=e,Je.remove(o),t=!1;break;case Jt.GetCurrentView:case Jt.Reference:case Jt.ContextLetReference:t=!1;break}})}}var BQ="ng-container";function jQ(n){for(let o of n.units){let e=new Set;for(let t of o.create)t.kind===j.ElementStart&&t.tag===BQ&&(t.kind=j.ContainerStart,e.add(t.xref)),t.kind===j.ElementEnd&&e.has(t.xref)&&(t.kind=j.ContainerEnd)}}function $Q(n,o){let e=n.get(o);if(e===void 0)throw new Error("All attributes should have an element-like target.");return e}function HQ(n){let o=new Map;for(let e of n.units)for(let t of e.create)Dp(t)&&o.set(t.xref,t);for(let e of n.units)for(let t of e.create)(t.kind===j.ElementStart||t.kind===j.ContainerStart)&&t.nonBindable&&Je.insertAfter(HW(t.xref),t),(t.kind===j.ElementEnd||t.kind===j.ContainerEnd)&&$Q(o,t.xref).nonBindable&&Je.insertBefore(UW(t.xref),t)}function Ic(n){return o=>o.kind===n}function Ef(n,o){return e=>e.kind===n&&o===e.expression instanceof nr}function UQ(n){return n.kind===j.Listener&&!(n.hostListener&&n.isLegacyAnimationListener)||n.kind===j.TwoWayListener||n.kind===j.Animation||n.kind===j.AnimationListener}function WQ(n){return(n.kind===j.Property||n.kind===j.TwoWayProperty)&&!(n.expression instanceof nr)}var GQ=[{test:n=>n.kind===j.Listener&&n.hostListener&&n.isLegacyAnimationListener},{test:UQ}],qQ=[{test:Ic(j.StyleMap),transform:x1},{test:Ic(j.ClassMap),transform:x1},{test:Ic(j.StyleProp)},{test:Ic(j.ClassProp)},{test:Ef(j.Attribute,!0)},{test:Ef(j.Property,!0)},{test:WQ},{test:Ef(j.Attribute,!1)},{test:Ic(j.Control)}],QQ=[{test:Ef(j.DomProperty,!0)},{test:Ef(j.DomProperty,!1)},{test:Ic(j.Attribute)},{test:Ic(j.StyleMap),transform:x1},{test:Ic(j.ClassMap),transform:x1},{test:Ic(j.StyleProp)},{test:Ic(j.ClassProp)}],P6=new Set([j.Listener,j.TwoWayListener,j.AnimationListener,j.StyleMap,j.ClassMap,j.StyleProp,j.ClassProp,j.Property,j.TwoWayProperty,j.DomProperty,j.Attribute,j.Animation,j.Control]);function KQ(n){for(let o of n.units){T6(o.create,GQ);let e=o.job.kind===Et.Host?QQ:qQ;T6(o.update,e)}}function T6(n,o){let e=[],t=null;for(let i of n){let r=h0(i)?i.target:null;(!P6.has(i.kind)||r!==t&&t!==null&&r!==null)&&(Je.insertBefore(E6(e,o),i),e=[],t=null),P6.has(i.kind)&&(e.push(i),Je.remove(i),t=r??t)}n.push(E6(e,o))}function E6(n,o){let e=Array.from(o,()=>new Array);for(let t of n){let i=o.findIndex(r=>r.test(t));e[i].push(t)}return e.flatMap((t,i)=>{let r=o[i].transform;return r?r(t):t})}function x1(n){return n.slice(n.length-1)}function XQ(n){for(let o of n.units){let e=JN(o);for(let t of o.ops())if(t.kind===j.Binding){let i=ZQ(e,t.target);YQ(t.name)&&i.kind===j.Projection&&Je.remove(t)}}}function YQ(n){return n.toLowerCase()==="select"}function ZQ(n,o){let e=n.get(o);if(e===void 0)throw new Error("All attributes should have an slottable target.");return e}function JQ(n){for(let o of n.units)eK(o)}function eK(n){for(let o of n.update)xr(o,(e,t)=>{if(!$c(e)||e.kind!==Jt.PipeBinding)return;if(t&ii.InChildOperation)throw new Error("AssertionError: pipe bindings should not appear in child expressions");if(o.target==null)throw new Error("AssertionError: expected slot handle to be assigned for pipe creation");tK(n,o.target,e)})}function tK(n,o,e){for(let t=n.create.head.next;t.kind!==j.ListEnd;t=t.next){if(!nh(t)||t.xref!==o)continue;for(;t.next.kind===j.Pipe;)t=t.next;let i=qW(e.target,e.targetSlot,e.name);Je.insertBefore(i,t.next);return}throw new Error(`AssertionError: unable to find insertion point for pipe ${e.name}`)}function nK(n){for(let o of n.units)for(let e of o.update)ir(e,t=>!(t instanceof pu)||t.args.length<=4?t:new v0(t.target,t.targetSlot,t.name,Ji(t.args),t.args.length),ii.None)}function iK(n){uF(n.root,0)}function uF(n,o){let e=null;for(let t of n.create)switch(t.kind){case j.I18nStart:t.subTemplateIndex=o===0?null:o,e=t;break;case j.I18nEnd:e.subTemplateIndex===null&&(o=0),e=null;break;case j.ConditionalCreate:case j.ConditionalBranchCreate:case j.Template:o=ax(n.job.views.get(t.xref),e,t.i18nPlaceholder,o);break;case j.RepeaterCreate:let i=n.job.views.get(t.xref);o=ax(i,e,t.i18nPlaceholder,o),t.emptyView!==null&&(o=ax(n.job.views.get(t.emptyView),e,t.emptyI18nPlaceholder,o));break;case j.Projection:t.fallbackView!==null&&(o=ax(n.job.views.get(t.fallbackView),e,t.fallbackViewI18nPlaceholder,o));break}return o}function ax(n,o,e,t){if(e!==void 0){if(o===null)throw Error("Expected template with i18n placeholder to be in an i18n block.");t++,oK(n,o)}return uF(n,t)}function oK(n,o){if(n.create.head.next?.kind!==j.I18nStart){let e=n.job.allocateXrefId();Je.insertAfter(I1(e,o.message,o.root,null),n.create.head),Je.insertBefore(A1(e,null),n.create.tail)}}function rK(n){for(let o of n.units)for(let e of o.ops())xr(e,t=>{if(!(t instanceof du)||t.body===null)return;let i=new dT(t.args.length);t.fn=n.pool.getSharedConstant(i,t.body),t.body=null})}var dT=class extends Bf{numArgs;constructor(o){super(),this.numArgs=o}keyOf(o){return o instanceof Ep?`param(${o.index})`:super.keyOf(o)}toSharedConstantDeclaration(o,e){let t=[];for(let r=0;rr instanceof Ep?si("a"+r.index):r,ii.None);return new Ur(o,new gu(t,i),void 0,ua.Final)}};function aK(n){for(let o of n.units)for(let e of o.update)ir(e,(t,i)=>i&ii.InChildOperation?t:t instanceof zc?sK(t):t instanceof ic?lK(t):t,ii.None)}function sK(n){let o=[],e=[];for(let t of n.entries){if(t instanceof nu){if(t.expression.isConstant())o.push(t);else{let i=e.length;e.push(t.expression),o.push(new nu(new Ep(i)))}continue}if(t.isConstant())o.push(t);else{let i=e.length;e.push(t),o.push(new Ep(i))}}return new du(Ji(o),e)}function lK(n){let o=[],e=[];for(let t of n.entries){if(t instanceof yp){if(t.expression.isConstant())o.push(t);else{let i=e.length;e.push(t.expression),o.push(new yp(new Ep(i)))}continue}if(t.value.isConstant())o.push(t);else{let i=e.length;e.push(t.value),o.push(new Rg(t.key,new Ep(i),t.quoted))}}return new du(new ic(o),e)}function cK(n){for(let o of n.units)for(let e of o.ops())ir(e,t=>t instanceof Fg&&(t.flags===null||!t.flags.includes("g"))?n.pool.getSharedConstant(new pT,t):t,ii.None)}var pT=class extends Bf{toSharedConstantDeclaration(o,e){return new Ur(o,e,void 0,ua.Final)}};function dK(n,o,e,t,i){return Np(ge.element,n,o,e,t,i)}function pK(n,o,e,t,i){return Np(ge.elementStart,n,o,e,t,i)}function Np(n,o,e,t,i,r){let s=[Oe(o)];return e!==null&&s.push(Oe(e)),i!==null?s.push(Oe(t),Oe(i)):t!==null&&s.push(Oe(t)),jn(n,s,r)}function gF(n,o,e,t,i,r,s,d,m){let u=[Oe(o),e,Oe(t),Oe(i),Oe(r),Oe(s)];for(d!==null&&(u.push(Oe(d)),u.push(Vt(ge.templateRefExtractor)));u[u.length-1].isEquivalent(Lg);)u.pop();return jn(n,u,m)}function QT(n,o,e,t,i){let r=[Oe(o)];return e instanceof nr?r.push(ih(e,i)):r.push(e),t!==null&&r.push(t),jn(n,r,i)}function mK(n){return jn(ge.elementEnd,[],n)}function uK(n,o,e,t){return Np(ge.elementContainerStart,n,null,o,e,t)}function gK(n,o,e,t){return Np(ge.elementContainer,n,null,o,e,t)}function hK(){return jn(ge.elementContainerEnd,[],null)}function _K(n,o,e,t,i,r,s,d){return gF(ge.templateCreate,n,o,e,t,i,r,s,d)}function fK(){return jn(ge.disableBindings,[],null)}function CK(){return jn(ge.enableBindings,[],null)}function vK(n,o,e,t,i){let r=[Oe(n),o];return e!==null&&r.push(Vt(e)),jn(t?ge.syntheticHostListener:ge.listener,r,i)}function O6(n,o){return Vt(ge.twoWayBindingSet).callFn([n,o])}function xK(n,o,e){return jn(ge.twoWayListener,[Oe(n),o],e)}function yK(n,o){return jn(ge.pipe,[Oe(n),Oe(o)],null)}function bK(){return jn(ge.namespaceHTML,[],null)}function wK(){return jn(ge.namespaceSVG,[],null)}function MK(){return jn(ge.namespaceMathML,[],null)}function SK(n,o){return jn(ge.advance,n>1?[Oe(n)]:[],o)}function kK(n){return Vt(ge.reference).callFn([Oe(n)])}function PK(n){return Vt(ge.nextContext).callFn(n===1?[]:[Oe(n)])}function TK(){return Vt(ge.getCurrentView).callFn([])}function EK(n){return Vt(ge.restoreView).callFn([n])}function OK(n){return Vt(ge.resetView).callFn([n])}function DK(n,o,e){let t=[Oe(n,null)];return o!==""&&t.push(Oe(o)),jn(ge.text,t,e)}function IK(n,o,e,t,i,r,s,d,m,u,h){let v=[Oe(n),Oe(o),e??Oe(null),Oe(t),Oe(i),Oe(r),s??Oe(null),d??Oe(null),m?Vt(ge.deferEnableTimerScheduling):Oe(null),Oe(h)],b;for(;(b=v[v.length-1])!==null&&b instanceof _a&&b.value===null;)v.pop();return jn(ge.defer,v,u)}var AK=new Map([[ao.Idle,{none:ge.deferOnIdle,prefetch:ge.deferPrefetchOnIdle,hydrate:ge.deferHydrateOnIdle}],[ao.Immediate,{none:ge.deferOnImmediate,prefetch:ge.deferPrefetchOnImmediate,hydrate:ge.deferHydrateOnImmediate}],[ao.Timer,{none:ge.deferOnTimer,prefetch:ge.deferPrefetchOnTimer,hydrate:ge.deferHydrateOnTimer}],[ao.Hover,{none:ge.deferOnHover,prefetch:ge.deferPrefetchOnHover,hydrate:ge.deferHydrateOnHover}],[ao.Interaction,{none:ge.deferOnInteraction,prefetch:ge.deferPrefetchOnInteraction,hydrate:ge.deferHydrateOnInteraction}],[ao.Viewport,{none:ge.deferOnViewport,prefetch:ge.deferPrefetchOnViewport,hydrate:ge.deferHydrateOnViewport}],[ao.Never,{none:ge.deferHydrateNever,prefetch:ge.deferHydrateNever,hydrate:ge.deferHydrateNever}]]);function NK(n,o,e,t){let i=AK.get(n)?.[e];if(i===void 0)throw new Error(`Unable to determine instruction for trigger ${n}`);return jn(i,o,t)}function FK(n){return jn(ge.projectionDef,n?[n]:[],null)}function RK(n,o,e,t,i,r,s){let d=[Oe(n)];return(o!==0||e!==null||t!==null)&&(d.push(Oe(o)),e!==null&&d.push(e),t!==null&&(e===null&&d.push(Oe(null)),d.push(si(t),Oe(i),Oe(r)))),jn(ge.projection,d,s)}function LK(n,o,e,t){let i=[Oe(n),Oe(o)];return e!==null&&i.push(Oe(e)),jn(ge.i18nStart,i,t)}function zK(n,o,e,t,i,r,s,d){let m=[Oe(n),o,Oe(e),Oe(t),Oe(i),Oe(r)];for(s!==null&&(m.push(Oe(s)),m.push(Vt(ge.templateRefExtractor)));m[m.length-1].isEquivalent(Lg);)m.pop();return jn(ge.conditionalCreate,m,d)}function VK(n,o,e,t,i,r,s,d){let m=[Oe(n),o,Oe(e),Oe(t),Oe(i),Oe(r)];for(s!==null&&(m.push(Oe(s)),m.push(Vt(ge.templateRefExtractor)));m[m.length-1].isEquivalent(Lg);)m.pop();return jn(ge.conditionalBranchCreate,m,d)}function BK(n,o,e,t,i,r,s,d,m,u,h,v,b,y){let x=[Oe(n),si(o),Oe(e),Oe(t),Oe(i),Oe(r),s];return(d||m!==null)&&(x.push(Oe(d)),m!==null&&(x.push(si(m),Oe(u),Oe(h)),(v!==null||b!==null)&&x.push(Oe(v)),b!==null&&x.push(Oe(b)))),jn(ge.repeaterCreate,x,y)}function jK(n,o){return jn(ge.repeater,[n],o)}function $K(n,o,e){return n==="prefetch"?jn(ge.deferPrefetchWhen,[o],e):n==="hydrate"?jn(ge.deferHydrateWhen,[o],e):jn(ge.deferWhen,[o],e)}function HK(n,o){return jn(ge.declareLet,[Oe(n)],o)}function UK(n,o){return Vt(ge.storeLet).callFn([n],o)}function WK(n){return Vt(ge.readContextLet).callFn([Oe(n)])}function GK(n,o,e,t){let i=[Oe(n),Oe(o)];return e&&i.push(Oe(e)),jn(ge.i18n,i,t)}function qK(n){return jn(ge.i18nEnd,[],n)}function QK(n,o){let e=[Oe(n),Oe(o)];return jn(ge.i18nAttributes,e,null)}function KK(n,o,e){return QT(ge.ariaProperty,n,o,null,e)}function XK(n,o,e,t){return QT(ge.property,n,o,e,t)}function YK(n){return jn(ge.control,[],n)}function ZK(n){return jn(ge.controlCreate,[],n)}function JK(n,o,e,t){let i=[Oe(n),o];return e!==null&&i.push(e),jn(ge.twoWayProperty,i,t)}function eX(n,o,e,t,i){let r=[Oe(n)];return o instanceof nr?r.push(ih(o,i)):r.push(o),(e!==null||t!==null)&&r.push(e??Oe(null)),t!==null&&r.push(Oe(t)),jn(ge.attribute,r,null)}function tX(n,o,e,t){let i=[Oe(n)];return o instanceof nr?i.push(ih(o,t)):i.push(o),e!==null&&i.push(Oe(e)),jn(ge.styleProp,i,t)}function nX(n,o,e){return jn(ge.classProp,[Oe(n),o],e)}function iX(n,o){let e=n instanceof nr?ih(n,o):n;return jn(ge.styleMap,[e],o)}function oX(n,o){let e=n instanceof nr?ih(n,o):n;return jn(ge.classMap,[e],o)}function rX(n,o,e,t,i){return Np(ge.domElement,n,o,e,t,i)}function aX(n,o,e,t,i){return Np(ge.domElementStart,n,o,e,t,i)}function sX(n){return jn(ge.domElementEnd,[],n)}function lX(n,o,e,t){return Np(ge.domElementContainerStart,n,null,o,e,t)}function cX(n,o,e,t){return Np(ge.domElementContainer,n,null,o,e,t)}function dX(){return jn(ge.domElementContainerEnd,[],null)}function pX(n,o,e,t){let i=[Oe(n),o];return e!==null&&i.push(Vt(e)),jn(ge.domListener,i,t)}function mX(n,o,e,t,i,r,s,d){return gF(ge.domTemplate,n,o,e,t,i,r,s,d)}var D6=[ge.pipeBind1,ge.pipeBind2,ge.pipeBind3,ge.pipeBind4];function uX(n,o,e){if(e.length<1||e.length>D6.length)throw new Error("pipeBind() argument count out of bounds");let t=D6[e.length-1];return Vt(t).callFn([Oe(n),Oe(o),...e])}function gX(n,o,e){return Vt(ge.pipeBindV).callFn([Oe(n),Oe(o),e])}function hX(n,o,e){let t=hF(n,o);return OX(PX,[],t,e)}function _X(n,o){return jn(ge.i18nExp,[n],o)}function fX(n,o){return jn(ge.i18nApply,[Oe(n)],o)}function CX(n,o,e,t){return QT(ge.domProperty,n,o,e,t)}function vX(n,o,e,t){let i=[o];e!==null&&i.push(e);let r=n==="enter"?ge.animationEnter:ge.animationLeave;return jn(r,i,t)}function xX(n,o,e,t){let r=[o instanceof nr?ih(o,t):o];e!==null&&r.push(e);let s=n==="enter"?ge.animationEnter:ge.animationLeave;return jn(s,r,t)}function yX(n,o,e,t){let i=[o],r=n==="enter"?ge.animationEnterListener:ge.animationLeaveListener;return jn(r,i,t)}function bX(n,o,e){return jn(ge.syntheticHostProperty,[Oe(n),o],e)}function wX(n,o,e){return KT(EX,[Oe(n),o],e,null)}function MX(n,o){return jn(ge.attachSourceLocations,[Oe(n),o],null)}function SX(n,o,e){return Vt(ge.arrowFunction).callFn([Oe(n),o,e])}function hF(n,o){if(n.length<1||o.length!==n.length-1)throw new Error("AssertionError: expected specific shape of args for strings/expressions in interpolation");let e=[];if(o.length===1&&n[0]===""&&n[1]==="")e.push(o[0]);else{let t;for(t=0;t{if(n%2===0)throw new Error("Expected odd number of arguments");return(n-1)/2}},TX={constant:[ge.interpolate,ge.interpolate1,ge.interpolate2,ge.interpolate3,ge.interpolate4,ge.interpolate5,ge.interpolate6,ge.interpolate7,ge.interpolate8],variable:ge.interpolateV,mapping:n=>{if(n%2===0)throw new Error("Expected odd number of arguments");return(n-1)/2}},EX={constant:[ge.pureFunction0,ge.pureFunction1,ge.pureFunction2,ge.pureFunction3,ge.pureFunction4,ge.pureFunction5,ge.pureFunction6,ge.pureFunction7,ge.pureFunction8],variable:ge.pureFunctionV,mapping:n=>n};function KT(n,o,e,t){let i=n.mapping(e.length),r=e.at(-1);if(e.length>1&&r instanceof _a&&r.value===""&&e.pop(),i_F(n,t),ii.None),e.kind){case j.Text:Je.replace(e,DK(e.handle.slot,e.initialValue,e.sourceSpan));break;case j.ElementStart:Je.replace(e,n.job.mode===hs.DomOnly?aX(e.handle.slot,e.tag,e.attributes,e.localRefs,e.startSourceSpan):pK(e.handle.slot,e.tag,e.attributes,e.localRefs,e.startSourceSpan));break;case j.Element:Je.replace(e,n.job.mode===hs.DomOnly?rX(e.handle.slot,e.tag,e.attributes,e.localRefs,e.wholeSourceSpan):dK(e.handle.slot,e.tag,e.attributes,e.localRefs,e.wholeSourceSpan));break;case j.ElementEnd:Je.replace(e,n.job.mode===hs.DomOnly?sX(e.sourceSpan):mK(e.sourceSpan));break;case j.ContainerStart:Je.replace(e,n.job.mode===hs.DomOnly?lX(e.handle.slot,e.attributes,e.localRefs,e.startSourceSpan):uK(e.handle.slot,e.attributes,e.localRefs,e.startSourceSpan));break;case j.Container:Je.replace(e,n.job.mode===hs.DomOnly?cX(e.handle.slot,e.attributes,e.localRefs,e.wholeSourceSpan):gK(e.handle.slot,e.attributes,e.localRefs,e.wholeSourceSpan));break;case j.ContainerEnd:Je.replace(e,n.job.mode===hs.DomOnly?dX():hK());break;case j.I18nStart:Je.replace(e,LK(e.handle.slot,e.messageIndex,e.subTemplateIndex,e.sourceSpan));break;case j.I18nEnd:Je.replace(e,qK(e.sourceSpan));break;case j.I18n:Je.replace(e,GK(e.handle.slot,e.messageIndex,e.subTemplateIndex,e.sourceSpan));break;case j.I18nAttributes:if(e.i18nAttributesConfig===null)throw new Error("AssertionError: i18nAttributesConfig was not set");Je.replace(e,QK(e.handle.slot,e.i18nAttributesConfig));break;case j.Template:if(!(n instanceof vl))throw new Error("AssertionError: must be compiling a component");if(Array.isArray(e.localRefs))throw new Error("AssertionError: local refs array should have been extracted into a constant");let t=n.job.views.get(e.xref);Je.replace(e,e.templateKind===vs.Block||n.job.mode===hs.DomOnly?mX(e.handle.slot,si(t.fnName),t.decls,t.vars,e.tag,e.attributes,e.localRefs,e.startSourceSpan):_K(e.handle.slot,si(t.fnName),t.decls,t.vars,e.tag,e.attributes,e.localRefs,e.startSourceSpan));break;case j.DisableBindings:Je.replace(e,fK());break;case j.EnableBindings:Je.replace(e,CK());break;case j.Pipe:Je.replace(e,yK(e.handle.slot,e.name));break;case j.DeclareLet:Je.replace(e,HK(e.handle.slot,e.sourceSpan));break;case j.AnimationString:Je.replace(e,xX(e.animationKind,e.expression,e.sanitizer,e.sourceSpan));break;case j.Animation:let i=sx(n,e.handlerFnName,e.handlerOps,!1);Je.replace(e,vX(e.animationKind,i,e.sanitizer,e.sourceSpan));break;case j.AnimationListener:let r=sx(n,e.handlerFnName,e.handlerOps,e.consumesDollarEvent);Je.replace(e,yX(e.animationKind,r,null,e.sourceSpan));break;case j.Listener:let s=sx(n,e.handlerFnName,e.handlerOps,e.consumesDollarEvent),d=e.eventTarget?DX.get(e.eventTarget):null;if(d===void 0)throw new Error(`Unexpected global target '${e.eventTarget}' defined for '${e.name}' event. Supported list of global targets: window,document,body.`);Je.replace(e,n.job.mode===hs.DomOnly&&!e.hostListener&&!e.isLegacyAnimationListener?pX(e.name,s,d,e.sourceSpan):vK(e.name,s,d,e.hostListener&&e.isLegacyAnimationListener,e.sourceSpan));break;case j.TwoWayListener:Je.replace(e,xK(e.name,sx(n,e.handlerFnName,e.handlerOps,!0),e.sourceSpan));break;case j.Variable:if(e.variable.name===null)throw new Error(`AssertionError: unnamed variable ${e.xref}`);Je.replace(e,Zs(new Ur(e.variable.name,e.initializer,void 0,ua.Final)));break;case j.Namespace:switch(e.active){case Tr.HTML:Je.replace(e,bK());break;case Tr.SVG:Je.replace(e,wK());break;case Tr.Math:Je.replace(e,MK());break}break;case j.Defer:let m=!!e.loadingMinimumTime||!!e.loadingAfterTime||!!e.placeholderMinimumTime;Je.replace(e,IK(e.handle.slot,e.mainSlot.slot,e.resolverFn,e.loadingSlot?.slot??null,e.placeholderSlot?.slot??null,e.errorSlot?.slot??null,e.loadingConfig,e.placeholderConfig,m,e.sourceSpan,e.flags));break;case j.DeferOn:let u=[];switch(e.trigger.kind){case ao.Never:case ao.Idle:case ao.Immediate:break;case ao.Timer:u=[Oe(e.trigger.delay)];break;case ao.Viewport:e.modifier==="hydrate"?u=e.trigger.options?[e.trigger.options]:[]:(u=[Oe(e.trigger.targetSlot?.slot??null)],e.trigger.targetSlotViewSteps!==0?u.push(Oe(e.trigger.targetSlotViewSteps)):e.trigger.options&&u.push(Oe(null)),e.trigger.options&&u.push(e.trigger.options));break;case ao.Interaction:case ao.Hover:e.modifier==="hydrate"?u=[]:(u=[Oe(e.trigger.targetSlot?.slot??null)],e.trigger.targetSlotViewSteps!==0&&u.push(Oe(e.trigger.targetSlotViewSteps)));break;default:throw new Error(`AssertionError: Unsupported reification of defer trigger kind ${e.trigger.kind}`)}Je.replace(e,NK(e.trigger.kind,u,e.modifier,e.sourceSpan));break;case j.ProjectionDef:Je.replace(e,FK(e.def));break;case j.Projection:if(e.handle.slot===null)throw new Error("No slot was assigned for project instruction");let h=null,v=null,b=null;if(e.fallbackView!==null){if(!(n instanceof vl))throw new Error("AssertionError: must be compiling a component");let D=n.job.views.get(e.fallbackView);if(D===void 0)throw new Error("AssertionError: projection had fallback view xref, but fallback view was not found");if(D.fnName===null||D.decls===null||D.vars===null)throw new Error("AssertionError: expected projection fallback view to have been named and counted");h=D.fnName,v=D.decls,b=D.vars}Je.replace(e,RK(e.handle.slot,e.projectionSlotIndex,e.attributes,h,v,b,e.sourceSpan));break;case j.ConditionalCreate:if(!(n instanceof vl))throw new Error("AssertionError: must be compiling a component");if(Array.isArray(e.localRefs))throw new Error("AssertionError: local refs array should have been extracted into a constant");let y=n.job.views.get(e.xref);Je.replace(e,zK(e.handle.slot,si(y.fnName),y.decls,y.vars,e.tag,e.attributes,e.localRefs,e.startSourceSpan));break;case j.ConditionalBranchCreate:if(!(n instanceof vl))throw new Error("AssertionError: must be compiling a component");if(Array.isArray(e.localRefs))throw new Error("AssertionError: local refs array should have been extracted into a constant");let x=n.job.views.get(e.xref);Je.replace(e,VK(e.handle.slot,si(x.fnName),x.decls,x.vars,e.tag,e.attributes,e.localRefs,e.startSourceSpan));break;case j.RepeaterCreate:if(e.handle.slot===null)throw new Error("No slot was assigned for repeater instruction");if(!(n instanceof vl))throw new Error("AssertionError: must be compiling a component");let P=n.job.views.get(e.xref);if(P.fnName===null)throw new Error("AssertionError: expected repeater primary view to have been named");let k=null,S=null,T=null;if(e.emptyView!==null){let D=n.job.views.get(e.emptyView);if(D===void 0)throw new Error("AssertionError: repeater had empty view xref, but empty view was not found");if(D.fnName===null||D.decls===null||D.vars===null)throw new Error("AssertionError: expected repeater empty view to have been named and counted");k=D.fnName,S=D.decls,T=D.vars}Je.replace(e,BK(e.handle.slot,P.fnName,e.decls,e.vars,e.tag,e.attributes,LX(n,e),e.usesComponentInstance,k,S,T,e.emptyTag,e.emptyAttributes,e.wholeSourceSpan));break;case j.SourceLocation:let A=Ji(e.locations.map(({targetSlot:D,offset:F,line:I,column:V})=>{if(D.slot===null)throw new Error("No slot was assigned for source location");return Ji([Oe(D.slot),Oe(F),Oe(I),Oe(V)])}));Je.replace(e,MX(e.templatePath,A));break;case j.ControlCreate:Je.replace(e,ZK(e.sourceSpan));break;case j.Statement:break;default:throw new Error(`AssertionError: Unsupported reification of create op ${j[e.kind]}`)}}function R1(n,o){for(let e of o)switch(ir(e,t=>_F(n,t),ii.None),e.kind){case j.Advance:Je.replace(e,SK(e.delta,e.sourceSpan));break;case j.Property:Je.replace(e,n.job.mode===hs.DomOnly&&e.bindingKind!==qt.LegacyAnimation&&e.bindingKind!==qt.Animation?I6(e):FX(e));break;case j.Control:Je.replace(e,RX(e));break;case j.TwoWayProperty:Je.replace(e,JK(e.name,e.expression,e.sanitizer,e.sourceSpan));break;case j.StyleProp:Je.replace(e,tX(e.name,e.expression,e.unit,e.sourceSpan));break;case j.ClassProp:Je.replace(e,nX(e.name,e.expression,e.sourceSpan));break;case j.StyleMap:Je.replace(e,iX(e.expression,e.sourceSpan));break;case j.ClassMap:Je.replace(e,oX(e.expression,e.sourceSpan));break;case j.I18nExpression:Je.replace(e,_X(e.expression,e.sourceSpan));break;case j.I18nApply:Je.replace(e,fX(e.handle.slot,e.sourceSpan));break;case j.InterpolateText:Je.replace(e,hX(e.interpolation.strings,e.interpolation.expressions,e.sourceSpan));break;case j.Attribute:Je.replace(e,eX(e.name,e.expression,e.sanitizer,e.namespace,e.sourceSpan));break;case j.DomProperty:if(e.expression instanceof nr)throw new Error("not yet handled");e.bindingKind===qt.LegacyAnimation||e.bindingKind===qt.Animation?Je.replace(e,bX(e.name,e.expression,e.sourceSpan)):Je.replace(e,I6(e));break;case j.Variable:if(e.variable.name===null)throw new Error(`AssertionError: unnamed variable ${e.xref}`);Je.replace(e,Zs(new Ur(e.variable.name,e.initializer,void 0,ua.Final)));break;case j.Conditional:if(e.processed===null)throw new Error("Conditional test was not set.");Je.replace(e,kX(e.processed,e.contextValue,e.sourceSpan));break;case j.Repeater:Je.replace(e,jK(e.collection,e.sourceSpan));break;case j.DeferWhen:Je.replace(e,$K(e.modifier,e.expr,e.sourceSpan));break;case j.StoreLet:throw new Error(`AssertionError: unexpected storeLet ${e.declaredName}`);case j.Statement:break;default:throw new Error(`AssertionError: Unsupported reification of update op ${j[e.kind]}`)}}function I6(n){return CX(IX.get(n.name)??n.name,n.expression,n.sanitizer,n.sourceSpan)}function FX(n){return eF(n.name)?KK(n.name,n.expression,n.sourceSpan):XK(n.name,n.expression,n.sanitizer,n.sourceSpan)}function RX(n){return YK(n.sourceSpan)}function _F(n,o){if(!$c(o))return o;switch(o.kind){case Jt.NextContext:return PK(o.steps);case Jt.Reference:return kK(o.targetSlot.slot+1+o.offset);case Jt.LexicalRead:throw new Error(`AssertionError: unresolved LexicalRead of ${o.name}`);case Jt.TwoWayBindingSet:throw new Error("AssertionError: unresolved TwoWayBindingSet");case Jt.RestoreView:if(typeof o.view=="number")throw new Error("AssertionError: unresolved RestoreView");return EK(o.view);case Jt.ResetView:return OK(o.expr);case Jt.GetCurrentView:return TK();case Jt.ReadVariable:if(o.name===null)throw new Error(`Read of unnamed variable ${o.xref}`);return si(o.name);case Jt.ReadTemporaryExpr:if(o.name===null)throw new Error(`Read of unnamed temporary ${o.xref}`);return si(o.name);case Jt.AssignTemporaryExpr:if(o.name===null)throw new Error(`Assign of unnamed temporary ${o.xref}`);return si(o.name).set(o.expr);case Jt.PureFunctionExpr:if(o.fn===null)throw new Error("AssertionError: expected PureFunctions to have been extracted");return wX(o.varOffset,o.fn,o.args);case Jt.PureFunctionParameterExpr:throw new Error("AssertionError: expected PureFunctionParameterExpr to have been extracted");case Jt.PipeBinding:return uX(o.targetSlot.slot,o.varOffset,o.args);case Jt.PipeBindingVariadic:return gX(o.targetSlot.slot,o.varOffset,o.args);case Jt.SlotLiteralExpr:return Oe(o.slot.slot);case Jt.ContextLetReference:return WK(o.targetSlot.slot);case Jt.StoreLet:return UK(o.value,o.sourceSpan);case Jt.TrackContext:return si("this");case Jt.ArrowFunction:if(o.varOffset===null)throw new Error("AssertionError: variable offset was not assigned to arrow function");return SX(o.varOffset,n.job.pool.getSharedFunctionReference(zX(n,o),"arrowFn"),si(Ys));default:throw new Error(`AssertionError: Unsupported reification of ir.Expression kind: ${Jt[o.kind]}`)}}function sx(n,o,e,t){R1(n,e);let i=[];for(let s of e){if(s.kind!==j.Statement)throw new Error(`AssertionError: expected reified statements, but found op ${j[s.kind]}`);i.push(s.statement)}let r=[];return t&&r.push(new Er("$event",xs)),bp(r,i,void 0,void 0,o)}function LX(n,o){if(o.trackByFn!==null)return o.trackByFn;let e=[new Er("$index",Jm),new Er("$item",xs)],t;if(o.trackByOps===null)t=o.usesComponentInstance?bp(e,[new Or(o.track)]):Xs(e,o.track);else{R1(n,o.trackByOps);let i=[];for(let r of o.trackByOps){if(r.kind!==j.Statement)throw new Error(`AssertionError: expected reified statements, but found op ${j[r.kind]}`);i.push(r.statement)}t=o.usesComponentInstance||i.length!==1||!(i[0]instanceof Or)?bp(e,i):Xs(e,i[0].value)}return o.trackByFn=n.job.pool.getSharedFunctionReference(t,"_forTrack"),o.trackByFn}function zX(n,o){R1(n,o.ops);let e=[];for(let i of o.ops){if(i.kind!==j.Statement)throw new Error(`AssertionError: expected reified statements, but found op ${j[i.kind]}`);e.push(i.statement)}let t=e.length===1&&e[0]instanceof Or?e[0].value:e;return Xs([new Er(o.contextName,xs),new Er(o.currentViewName,xs)],Xs(o.parameters,t))}function VX(n){for(let o of n.units)for(let e of o.update)switch(e.kind){case j.Attribute:case j.Binding:case j.ClassProp:case j.ClassMap:case j.Property:case j.StyleProp:case j.StyleMap:e.expression instanceof x0&&Je.remove(e);break}}function BX(n){for(let o of n.units)for(let e of o.create)switch(e.kind){case j.I18nContext:Je.remove(e);break;case j.I18nStart:e.context=null;break}}function jX(n){for(let o of n.units)for(let e of o.update){if(e.kind!==j.Variable||e.variable.kind!==oa.Identifier||!(e.initializer instanceof _0))continue;let t=e.variable.identifier,i=e;for(;i&&i.kind!==j.ListEnd;)ir(i,r=>r instanceof na&&r.name===t?Oe(void 0):r,ii.None),i=i.prev}}function $X(n){for(let o of n.units){let e=new Set;for(let t of o.update)t.kind===j.I18nExpression&&e.add(t.i18nOwner);for(let t of o.create)switch(t.kind){case j.I18nAttributes:if(e.has(t.xref))continue;Je.remove(t)}}}function HX(n){for(let o of n.units){for(let e of o.functions)Of(o,e.ops);Of(o,o.create),Of(o,o.update)}}function Of(n,o){let e=new Map;e.set(n.xref,si(Ys));for(let t of o)switch(t.kind){case j.Variable:t.variable.kind===oa.Context&&e.set(t.variable.view,new Od(t.xref));break;case j.Animation:case j.AnimationListener:case j.Listener:case j.TwoWayListener:Of(n,t.handlerOps);break;case j.RepeaterCreate:t.trackByOps!==null&&Of(n,t.trackByOps);break}n===n.job.root&&e.set(n.xref,si(Ys));for(let t of o)ir(t,i=>{if(i instanceof Tp){if(!e.has(i.view))throw new Error(`No context found for reference to view ${i.view} from view ${n.xref}`);return e.get(i.view)}else return i},ii.None)}function UX(n){for(let o of n.units)for(let e of o.create)if(e.kind===j.Defer){if(e.resolverFn!==null)continue;if(e.ownResolverFn!==null){if(e.handle.slot===null)throw new Error("AssertionError: slot must be assigned before extracting defer deps functions");let t=o.fnName?.replace("_Template","");e.resolverFn=n.pool.getSharedFunctionReference(e.ownResolverFn,`${t}_Defer_${e.handle.slot}_DepsFn`,!1)}}}function WX(n){for(let o of n.units)A6(o.create),A6(o.update)}function A6(n){for(let o of n)(o.kind===j.Listener||o.kind===j.TwoWayListener||o.kind===j.AnimationListener)&&ir(o,e=>e instanceof na&&e.name==="$event"?((o.kind===j.Listener||o.kind===j.AnimationListener)&&(o.consumesDollarEvent=!0),new tc(e.name)):e,ii.InChildOperation)}function GX(n){let o=new Map,e=new Map;for(let t of n.units)for(let i of t.create)switch(i.kind){case j.I18nContext:o.set(i.xref,i);break;case j.ElementStart:e.set(i.xref,i);break}Tc(n,n.root,o,e)}function Tc(n,o,e,t,i){let r=null,s=new Map;for(let d of o.create)switch(d.kind){case j.I18nStart:if(!d.context)throw Error("Could not find i18n context for i18n op");r={i18nBlock:d,i18nContext:e.get(d.context)};break;case j.I18nEnd:r=null;break;case j.ElementStart:if(d.i18nPlaceholder!==void 0){if(r===null)throw Error("i18n tag placeholder should only occur inside an i18n block");N6(d,r.i18nContext,r.i18nBlock,i),i&&d.i18nPlaceholder.closeName&&s.set(d.xref,i),i=void 0}break;case j.ElementEnd:let m=t.get(d.xref);if(m&&m.i18nPlaceholder!==void 0){if(r===null)throw Error("AssertionError: i18n tag placeholder should only occur inside an i18n block");F6(m,r.i18nContext,r.i18nBlock,s.get(d.xref)),s.delete(d.xref)}break;case j.Projection:if(d.i18nPlaceholder!==void 0){if(r===null)throw Error("i18n tag placeholder should only occur inside an i18n block");N6(d,r.i18nContext,r.i18nBlock,i),F6(d,r.i18nContext,r.i18nBlock,i),i=void 0}if(d.fallbackView!==null){let b=n.views.get(d.fallbackView);if(d.fallbackViewI18nPlaceholder===void 0)Tc(n,b,e,t);else{if(r===null)throw Error("i18n tag placeholder should only occur inside an i18n block");lx(n,b,d.handle.slot,d.fallbackViewI18nPlaceholder,r.i18nContext,r.i18nBlock,i),Tc(n,b,e,t),cx(n,b,d.handle.slot,d.fallbackViewI18nPlaceholder,r.i18nContext,r.i18nBlock,i),i=void 0}}break;case j.ConditionalCreate:case j.ConditionalBranchCreate:case j.Template:let u=n.views.get(d.xref);if(d.i18nPlaceholder===void 0)Tc(n,u,e,t);else{if(r===null)throw Error("i18n tag placeholder should only occur inside an i18n block");d.templateKind===vs.Structural?Tc(n,u,e,t,d):(lx(n,u,d.handle.slot,d.i18nPlaceholder,r.i18nContext,r.i18nBlock,i),Tc(n,u,e,t),cx(n,u,d.handle.slot,d.i18nPlaceholder,r.i18nContext,r.i18nBlock,i),i=void 0)}break;case j.RepeaterCreate:if(i!==void 0)throw Error("AssertionError: Unexpected structural directive associated with @for block");let h=d.handle.slot+1,v=n.views.get(d.xref);if(d.i18nPlaceholder===void 0)Tc(n,v,e,t);else{if(r===null)throw Error("i18n tag placeholder should only occur inside an i18n block");lx(n,v,h,d.i18nPlaceholder,r.i18nContext,r.i18nBlock,i),Tc(n,v,e,t),cx(n,v,h,d.i18nPlaceholder,r.i18nContext,r.i18nBlock,i),i=void 0}if(d.emptyView!==null){let b=d.handle.slot+2,y=n.views.get(d.emptyView);if(d.emptyI18nPlaceholder===void 0)Tc(n,y,e,t);else{if(r===null)throw Error("i18n tag placeholder should only occur inside an i18n block");lx(n,y,b,d.emptyI18nPlaceholder,r.i18nContext,r.i18nBlock,i),Tc(n,y,e,t),cx(n,y,b,d.emptyI18nPlaceholder,r.i18nContext,r.i18nBlock,i),i=void 0}}break}}function N6(n,o,e,t){let{startName:i,closeName:r}=n.i18nPlaceholder,s=mo.ElementTag|mo.OpenTag,d=n.handle.slot;t!==void 0&&(s|=mo.TemplateTag,d={element:d,template:t.handle.slot}),r||(s|=mo.CloseTag),Zg(o.params,i,d,e.subTemplateIndex,s)}function F6(n,o,e,t){let{closeName:i}=n.i18nPlaceholder;if(i){let r=mo.ElementTag|mo.CloseTag,s=n.handle.slot;t!==void 0&&(r|=mo.TemplateTag,s={element:s,template:t.handle.slot}),Zg(o.params,i,s,e.subTemplateIndex,r)}}function lx(n,o,e,t,i,r,s){let{startName:d,closeName:m}=t,u=mo.TemplateTag|mo.OpenTag;m||(u|=mo.CloseTag),s!==void 0&&Zg(i.params,d,s.handle.slot,r.subTemplateIndex,u),Zg(i.params,d,e,fF(n,r,o),u)}function cx(n,o,e,t,i,r,s){let{closeName:d}=t,m=mo.TemplateTag|mo.CloseTag;d&&(Zg(i.params,d,e,fF(n,r,o),m),s!==void 0&&Zg(i.params,d,s.handle.slot,r.subTemplateIndex,m))}function fF(n,o,e){for(let t of e.create)if(t.kind===j.I18nStart)return t.subTemplateIndex;return o.subTemplateIndex}function Zg(n,o,e,t,i){let r=n.get(o)??[];r.push({value:e,subTemplateIndex:t,flags:i}),n.set(o,r)}function qX(n){let o=new Map,e=new Map,t=new Map;for(let s of n.units)for(let d of s.create)switch(d.kind){case j.I18nStart:o.set(d.xref,d.subTemplateIndex);break;case j.I18nContext:e.set(d.xref,d);break;case j.IcuPlaceholder:t.set(d.xref,d);break}let i=new Map,r=s=>s.usage===th.I18nText?s.i18nOwner:s.context;for(let s of n.units)for(let d of s.update)if(d.kind===j.I18nExpression){let m=i.get(r(d))||0,u=o.get(d.i18nOwner)??null,h={value:m,subTemplateIndex:u,flags:mo.ExpressionIndex};QX(d,h,e,t),i.set(r(d),m+1)}}function QX(n,o,e,t){if(n.i18nPlaceholder!==null){let i=e.get(n.context),r=n.resolutionTime===g0.Creation?i.params:i.postprocessingParams,s=r.get(n.i18nPlaceholder)||[];s.push(o),r.set(n.i18nPlaceholder,s)}n.icuPlaceholder!==null&&t.get(n.icuPlaceholder)?.expressionPlaceholders.push(o)}function KX(n){let o=new Map;for(let e of n.units)for(let t of e.ops())if(t.kind===j.ElementStart||t.kind===j.Template){let i=t.tag??"";switch(t.namespace){case Tr.SVG:i=`:${N1}:${i}`;break;case Tr.Math:i=`:${F1}:${i}`;break}o.set(t.xref,i)}for(let e of n.units)for(let t of e.create)if(t.kind===j.ExtractedAttribute&&t.i18nContext!==null&&t.expression!==null){let i=o.get(t.target)??"",r=t.expression;switch(t.securityContext){case mi.HTML:r=Vt(ge.sanitizeHtml).callFn([r]);break;case mi.STYLE:r=Vt(ge.sanitizeStyle).callFn([r]);break;case mi.SCRIPT:r=Vt(ge.sanitizeScript).callFn([r]);break;case mi.URL:r=Vt(ge.sanitizeUrl).callFn([r]);break;case mi.RESOURCE_URL:r=Vt(ge.sanitizeResourceUrl).callFn([r]);break;case mi.ATTRIBUTE_NO_BINDING:r=Vt(ge.validateAttribute).callFn([r,Oe(i),Oe(t.name)]);break}t.expression=r}}function XX(n){for(let o of n.units){for(let e of o.functions)Df(o,e.ops,null);Df(o,o.create,null),Df(o,o.update,null)}}function Df(n,o,e){let t=new Map,i=new Map;for(let r of o)switch(r.kind){case j.Variable:switch(r.variable.kind){case oa.Identifier:if(r.variable.local){if(i.has(r.variable.identifier))continue;i.set(r.variable.identifier,r.xref)}else if(t.has(r.variable.identifier))continue;t.set(r.variable.identifier,r.xref);break;case oa.Alias:if(t.has(r.variable.identifier))continue;t.set(r.variable.identifier,r.xref);break;case oa.SavedView:e={view:r.variable.view,variable:r.xref};break}break;case j.Animation:case j.AnimationListener:case j.Listener:case j.TwoWayListener:Df(n,r.handlerOps,e);break;case j.RepeaterCreate:r.trackByOps!==null&&Df(n,r.trackByOps,e);break}for(let r of o)r.kind===j.Listener||r.kind===j.TwoWayListener||r.kind===j.Animation||r.kind===j.AnimationListener||ir(r,s=>{if(s instanceof na)return i.has(s.name)?new Od(i.get(s.name)):t.has(s.name)?new Od(t.get(s.name)):new Ks(new Tp(n.job.root.xref),s.name);if(s instanceof C0&&typeof s.view=="number"){if(e===null||e.view!==s.view)throw new Error(`AssertionError: no saved view ${s.view} from view ${n.xref}`);return s.view=new Od(e.variable),s}else return s},ii.None);for(let r of o)xr(r,s=>{if(s instanceof na)throw new Error(`AssertionError: no lexical reads should remain, but found read of ${s.name}`)})}var YX=new Map([[mi.HTML,ge.sanitizeHtml],[mi.RESOURCE_URL,ge.sanitizeResourceUrl],[mi.SCRIPT,ge.sanitizeScript],[mi.STYLE,ge.sanitizeStyle],[mi.URL,ge.sanitizeUrl],[mi.ATTRIBUTE_NO_BINDING,ge.validateAttribute]]),ZX=new Map([[mi.HTML,ge.trustConstantHtml],[mi.RESOURCE_URL,ge.trustConstantResourceUrl]]);function JX(n){for(let o of n.units){if(n.kind!==Et.Host){for(let e of o.create)if(e.kind===j.ExtractedAttribute){let t=ZX.get(R6(e.securityContext))??null;e.trustedValueFn=t!==null?Vt(t):null}}for(let e of o.update)switch(e.kind){case j.Property:case j.Attribute:case j.DomProperty:case j.TwoWayProperty:let t=null;Array.isArray(e.securityContext)&&eY(e.securityContext)?t=ge.sanitizeUrlOrResourceUrl:t=YX.get(R6(e.securityContext))??null,e.sanitizer=t!==null?Vt(t):null;break}}}function eY(n){let o=!1,e=!1,t=!1;for(let i of n)switch(i){case mi.URL:o=!0;break;case mi.RESOURCE_URL:e=!0;break;case mi.NONE:t=!0;break;default:return!1}return(o||e)&&t||o&&e}function R6(n){if(!Array.isArray(n))return n;if(n.length<2)return n[0]??mi.NONE;let o=n.filter(e=>e!==mi.NONE);if(o.length>1)throw Error("AssertionError: Ambiguous security context");return o[0]??mi.NONE}function tY(n){for(let o of n.units){for(let e of o.functions)L6(n,o,e.ops)&&z6(o,e.ops,si(e.currentViewName));o.create.prepend([fp(o.job.allocateXrefId(),{kind:oa.SavedView,name:null,view:o.xref},new VP,fl.None)]);for(let e of o.create)(e.kind===j.Listener||e.kind===j.TwoWayListener||e.kind===j.Animation||e.kind===j.AnimationListener)&&L6(n,o,e.handlerOps)&&z6(o,e.handlerOps,o.xref)}}function L6(n,o,e){let t=o!==n.root;if(!t)for(let i of e)xr(i,r=>{(r instanceof n1||r instanceof f0)&&(t=!0)});return t}function z6(n,o,e){o.prepend([fp(n.job.allocateXrefId(),{kind:oa.Context,name:null,view:n.xref},new C0(e),fl.None)]);for(let t of o)t.kind===j.Statement&&t.statement instanceof Or&&(t.statement.value=new o1(t.statement.value))}function nY(n){let o=new Map;for(let e of n.units){let t=0;for(let i of e.create)nh(i)&&(i.handle.slot=t,o.set(i.xref,i.handle.slot),t+=i.numSlotsUsed);e.decls=t}for(let e of n.units)for(let t of e.ops())if(t.kind===j.Template||t.kind===j.ConditionalCreate||t.kind===j.ConditionalBranchCreate||t.kind===j.RepeaterCreate){let i=n.views.get(t.xref);t.decls=i.decls}}function iY(n){let o=new Set,e=new Map;for(let t of n.units)for(let i of t.ops())i.kind===j.DeclareLet&&e.set(i.xref,i),xr(i,r=>{r instanceof f0&&o.add(r.target)});for(let t of n.units)for(let i of t.update)ir(i,r=>r instanceof _0&&!o.has(r.target)?(oY(r)||Je.remove(e.get(r.target)),r.value):r,ii.None)}function oY(n){let o=!1;return Rt(n,e=>((e instanceof pu||e instanceof v0)&&(o=!0),e),ii.None),o}function rY(n){let o=new Set;for(let e of n.units)for(let t of e.ops())xr(t,i=>{if(i instanceof Si)switch(i.operator){case lt.Exponentiation:aY(i,o);break;case lt.NullishCoalesce:sY(i,o);break;case lt.And:case lt.Or:lY(i,o)}});for(let e of n.units)for(let t of e.ops())ir(t,i=>i instanceof nc?o.has(i)?i:i.expr:i,ii.None)}function aY(n,o){n.lhs instanceof nc&&n.lhs.expr instanceof tu&&o.add(n.lhs)}function sY(n,o){n.lhs instanceof nc&&(V6(n.lhs.expr)||n.lhs.expr instanceof Lc)&&o.add(n.lhs),n.rhs instanceof nc&&(V6(n.rhs.expr)||n.rhs.expr instanceof Lc)&&o.add(n.rhs)}function lY(n,o){n.lhs instanceof nc&&n.lhs.expr instanceof Si&&n.lhs.expr.operator===lt.NullishCoalesce&&o.add(n.lhs)}function V6(n){return n instanceof Si&&(n.operator===lt.And||n.operator===lt.Or)}function cY(n){for(let o of n.units)for(let e of o.update)if(e.kind===j.Binding)switch(e.bindingKind){case qt.ClassName:if(e.expression instanceof nr)throw new Error("Unexpected interpolation in ClassName binding");Je.replace(e,OW(e.target,e.name,e.expression,e.sourceSpan));break;case qt.StyleProperty:Je.replace(e,EW(e.target,e.name,e.expression,e.unit,e.sourceSpan));break;case qt.Property:case qt.Template:e.name==="style"?Je.replace(e,DW(e.target,e.expression,e.sourceSpan)):e.name==="class"&&Je.replace(e,IW(e.target,e.expression,e.sourceSpan));break}}function dY(n){for(let o of n.units){o.create.prepend(If(o.create)),o.update.prepend(If(o.update));for(let e of o.functions)e.ops.prepend(If(e.ops))}}function If(n){let o=0,e=[];for(let t of n){let i=new Map;xr(t,(u,h)=>{h&ii.InChildOperation||u instanceof Op&&i.set(u.xref,u)});let r=0,s=new Set,d=new Set,m=new Map;xr(t,(u,h)=>{h&ii.InChildOperation||(u instanceof Hc?(s.has(u.xref)||(s.add(u.xref),m.set(u.xref,`tmp_${o}_${r++}`)),B6(m,u)):u instanceof Op&&(i.get(u.xref)===u&&(d.add(u.xref),r--),B6(m,u)))}),e.push(...Array.from(new Set(m.values())).map(u=>Zs(new Ur(u)))),o++,t.kind===j.Listener||t.kind===j.Animation||t.kind===j.AnimationListener||t.kind===j.TwoWayListener?t.handlerOps.prepend(If(t.handlerOps)):t.kind===j.RepeaterCreate&&t.trackByOps!==null&&t.trackByOps.prepend(If(t.trackByOps))}return e}function B6(n,o){let e=n.get(o.xref);if(e===void 0)throw new Error(`Found xref with unassigned name: ${o.xref}`);o.name=e}function pY(n){for(let o of n.units)for(let e of o.create)if(e.kind===j.RepeaterCreate)if(e.track instanceof tc&&e.track.name==="$index")e.trackByFn=Vt(ge.repeaterTrackByIndex);else if(e.track instanceof tc&&e.track.name==="$item")e.trackByFn=Vt(ge.repeaterTrackByIdentity);else if(mY(n.root.xref,e.track))e.usesComponentInstance=!0,e.track.receiver.receiver.view===o.xref?e.trackByFn=e.track.receiver:(e.trackByFn=Vt(ge.componentInstance).callFn([]).prop(e.track.receiver.name),e.track=e.trackByFn);else{e.track=Rt(e.track,i=>{if(i instanceof pu||i instanceof v0)throw new Error("Illegal State: Pipes are not allowed in this context");return i instanceof Tp?(e.usesComponentInstance=!0,new zP(i.view)):i},ii.None);let t=new Je;t.push(Zs(new Or(e.track,e.track.sourceSpan))),e.trackByOps=t}}function mY(n,o){if(!(o instanceof ys)||o.args.length===0||o.args.length>2||!(o.receiver instanceof Ks&&o.receiver.receiver instanceof Tp)||o.receiver.receiver.view!==n)return!1;let[e,t]=o.args;return!(e instanceof tc)||e.name!=="$index"?!1:o.args.length===1?!0:!(!(t instanceof tc)||t.name!=="$item")}function uY(n){for(let o of n.units)for(let e of o.create)e.kind===j.RepeaterCreate&&(e.track=Rt(e.track,t=>{if(t instanceof na){if(e.varNames.$index.has(t.name))return si("$index");if(t.name===e.varNames.$implicit)return si("$item")}return t},ii.None))}function gY(n){for(let o of n.units)for(let e of o.create)e.kind===j.TwoWayListener&&ir(e,t=>{if(!(t instanceof r1))return t;let{target:i,value:r}=t;if(i instanceof Ks||i instanceof Id)return O6(i,r).or(i.set(r));if(i instanceof Od)return O6(i,r);throw new Error("Unsupported expression in two-way action binding.")},ii.InChildOperation)}function hY(n){for(let o of n.units){let e=0;for(let r of o.ops())Bk(r)&&(e+=_Y(r));let t=r=>{$c(r)&&(r instanceof du||(q5(r)&&(r.varOffset=e),Bk(r)&&(e+=j6(r))))},i=r=>{!$c(r)||!(r instanceof du)||(q5(r)&&(r.varOffset=e),Bk(r)&&(e+=j6(r)))};for(let r of o.create)xr(r,t);for(let r of o.update)xr(r,t);for(let r of o.create)xr(r,i);for(let r of o.update)xr(r,i);o.vars=e}if(n instanceof b0)for(let o of n.units)for(let e of o.create){if(e.kind!==j.Template&&e.kind!==j.RepeaterCreate&&e.kind!==j.ConditionalCreate&&e.kind!==j.ConditionalBranchCreate)continue;let t=n.views.get(e.xref);e.vars=t.vars}}function _Y(n){let o;switch(n.kind){case j.Attribute:return o=1,n.expression instanceof nr&&!fY(n.expression)&&(o+=n.expression.expressions.length),o;case j.Property:case j.DomProperty:return o=1,n.expression instanceof nr&&(o+=n.expression.expressions.length),o;case j.Control:return 2;case j.TwoWayProperty:return 1;case j.StyleProp:case j.ClassProp:case j.StyleMap:case j.ClassMap:return o=2,n.expression instanceof nr&&(o+=n.expression.expressions.length),o;case j.InterpolateText:return n.interpolation.expressions.length;case j.I18nExpression:case j.Conditional:case j.DeferWhen:case j.StoreLet:return 1;case j.RepeaterCreate:return n.emptyView?1:0;default:throw new Error(`Unhandled op: ${j[n.kind]}`)}}function j6(n){switch(n.kind){case Jt.PureFunctionExpr:return 1+n.args.length;case Jt.PipeBinding:return 1+n.args.length;case Jt.PipeBindingVariadic:return 1+n.numArgs;case Jt.StoreLet:case Jt.ArrowFunction:return 1;default:throw new Error(`AssertionError: unhandled ConsumesVarsTrait expression ${n.constructor.name}`)}}function fY(n){return!(n.expressions.length!==1||n.strings.length!==2||n.strings[0]!==""||n.strings[1]!=="")}function CY(n){for(let o of n.units){for(let e of o.functions)_f(e.ops);_f(o.create),_f(o.update);for(let e of o.create)e.kind===j.Listener||e.kind===j.Animation||e.kind===j.AnimationListener||e.kind===j.TwoWayListener?_f(e.handlerOps):e.kind===j.RepeaterCreate&&e.trackByOps!==null&&_f(e.trackByOps);for(let e of o.functions)ff(e.ops,null),$6(e.ops);for(let e of o.create)e.kind===j.Listener||e.kind===j.Animation||e.kind===j.AnimationListener||e.kind===j.TwoWayListener?(ff(e.handlerOps,dx),$6(e.handlerOps)):e.kind===j.RepeaterCreate&&e.trackByOps!==null&&ff(e.trackByOps,dx);ff(o.create,dx),ff(o.update,dx)}}var Hr=(function(n){return n[n.None=0]="None",n[n.ViewContextRead=1]="ViewContextRead",n[n.ViewContextWrite=2]="ViewContextWrite",n[n.SideEffectful=4]="SideEffectful",n})(Hr||{});function dx(n){return!(n&ii.InArrowFunctionOperation)}function _f(n){let o=new Map;for(let e of n)e.kind===j.Variable&&e.flags&fl.AlwaysInline&&(xr(e,t=>{if($c(t)&&XT(t)!==Hr.None)throw new Error("AssertionError: A context-sensitive variable was marked AlwaysInline")}),o.set(e.xref,e)),ir(e,t=>t instanceof Od&&o.has(t.xref)?o.get(t.xref).initializer.clone():t,ii.None);for(let e of o.values())Je.remove(e)}function ff(n,o){let e=new Map,t=new Map,i=new Set,r=new Map;for(let u of n){if(u.kind===j.Variable){if(e.has(u.xref)||t.has(u.xref))throw new Error(`Should not see two declarations of the same variable: ${u.xref}`);e.set(u.xref,u),t.set(u.xref,0)}r.set(u,vY(u,o)),xY(u,t,i,o)}let s=!1;for(let u of n.reversed()){let h=r.get(u);if(u.kind===j.Variable&&t.get(u.xref)===0){if(s&&h.fences&Hr.ViewContextWrite||h.fences&Hr.SideEffectful){let v=Zs(u.initializer.toStmt());r.set(v,h),Je.replace(u,v)}else yY(u,t),Je.remove(u);r.delete(u),e.delete(u.xref),t.delete(u.xref);continue}h.fences&Hr.ViewContextRead&&(s=!0)}let d=[];for(let[u,h]of t){let b=!!(e.get(u).flags&fl.AlwaysInline);h!==1||b||i.has(u)||d.push(u)}let m;for(;m=d.pop();){let u=e.get(m),h=r.get(u);if(!!(u.flags&fl.AlwaysInline))throw new Error("AssertionError: Found an 'AlwaysInline' variable after the always inlining pass.");for(let b=u.next;b.kind!==j.ListEnd;b=b.next){let y=r.get(b);if(y.variablesUsed.has(m)){if(!wY(u,b))break;if(bY(m,u.initializer,b,h.fences)){y.variablesUsed.delete(m);for(let x of h.variablesUsed)y.variablesUsed.add(x);y.fences|=h.fences,e.delete(m),t.delete(m),r.delete(u),Je.remove(u)}break}if(!CF(y.fences,h.fences))break}}}function XT(n){switch(n.kind){case Jt.NextContext:return Hr.ViewContextRead|Hr.ViewContextWrite;case Jt.RestoreView:return Hr.ViewContextRead|Hr.ViewContextWrite|Hr.SideEffectful;case Jt.StoreLet:return Hr.SideEffectful;case Jt.Reference:case Jt.ContextLetReference:return Hr.ViewContextRead;default:return Hr.None}}function vY(n,o){let e=Hr.None,t=new Set;return xr(n,(i,r)=>{!$c(i)||o!==null&&!o(r)||(i.kind===Jt.ReadVariable?t.add(i.xref):e|=XT(i))}),{fences:e,variablesUsed:t}}function xY(n,o,e,t){xr(n,(i,r)=>{if(!$c(i)||t!==null&&!t(r)||i.kind!==Jt.ReadVariable)return;let s=o.get(i.xref);s!==void 0&&(o.set(i.xref,s+1),r&ii.InChildOperation&&e.add(i.xref))})}function yY(n,o){xr(n,e=>{if(!$c(e)||e.kind!==Jt.ReadVariable)return;let t=o.get(e.xref);if(t!==void 0){if(t===0)throw new Error(`Inaccurate variable count: ${e.xref} - found another read but count is already 0`);o.set(e.xref,t-1)}})}function CF(n,o){if(n&Hr.ViewContextWrite){if(o&Hr.ViewContextRead)return!1}else if(n&Hr.ViewContextRead&&o&Hr.ViewContextWrite)return!1;return!0}function bY(n,o,e,t){let i=!1,r=!0;return ir(e,(s,d)=>{if(!$c(s)||i||!r)return s;if(d&ii.InChildOperation&&t&Hr.ViewContextRead)return s;switch(s.kind){case Jt.ReadVariable:if(s.xref===n)return i=!0,o;break;default:let m=XT(s);r=r&&CF(m,t);break}return s},ii.None),i}function wY(n,o){switch(n.variable.kind){case oa.Identifier:return n.initializer instanceof tc&&n.initializer.name===Ys;case oa.Context:return o.kind===j.Variable;default:return!0}}function $6(n){let o=n.head.next,e=n.tail.prev;o!==null&&e!==null&&o.next===e&&o.kind===j.Statement&&o.statement instanceof fa&&o.statement.expr instanceof C0&&e.kind===j.Statement&&e.statement instanceof Or&&e.statement.value instanceof o1&&(Je.remove(o),e.statement.value=e.statement.value.expr)}function MY(n){for(let o of n.units){let e=null,t=null;for(let i of o.create)switch(i.kind){case j.I18nStart:e=i;break;case j.I18nEnd:e=null;break;case j.IcuStart:e===null&&(t=n.allocateXrefId(),Je.insertBefore(I1(t,i.message,void 0,null),i));break;case j.IcuEnd:t!==null&&(Je.insertAfter(A1(t,null),i),t=null);break}}}function SY(n){for(let o of n.units){for(let e of o.create)e.kind!==j.Animation&&e.kind!==j.AnimationListener&&e.kind!==j.Listener&&e.kind!==j.TwoWayListener&&H6(o,e);for(let e of o.update)H6(o,e)}}function H6(n,o){ir(o,(e,t)=>{if(!(e instanceof gu)||t&ii.InChildOperation)return e;if(Array.isArray(e.body))throw new Error("AssertionError: unexpected multi-line arrow function");let i=new BP(e.params,e.body);return n.functions.add(i),i},ii.None)}var kY=new Set(["formField"]);function PY(n){for(let o of n.units)TY(o)}function TY(n){for(let o of n.update)o.kind===j.Property&&kY.has(o.name)&&IY(n,o)}var EY=new Set([j.Container,j.ContainerStart,j.ContainerEnd,j.Element,j.ElementStart,j.ElementEnd,j.Template]);function OY(n){return EY.has(n.kind)}function DY(n,o){let e=null;for(let t of n.create)!OY(t)||t.xref!==o||(e=t);return e}function IY(n,o){let e=DY(n,o.target);if(e===null)throw new Error(`No create instruction found for control target ${o.target}`);let t=oG(o.sourceSpan);Je.insertAfter(t,e),Je.insertAfter(zW(o.target,o.sourceSpan),o)}var AY=[{kind:Et.Tmpl,fn:XQ},{kind:Et.Both,fn:cK},{kind:Et.Host,fn:lq},{kind:Et.Tmpl,fn:AQ},{kind:Et.Tmpl,fn:iK},{kind:Et.Tmpl,fn:MY},{kind:Et.Both,fn:OG},{kind:Et.Both,fn:cY},{kind:Et.Both,fn:hG},{kind:Et.Tmpl,fn:PY},{kind:Et.Both,fn:kG},{kind:Et.Both,fn:mG},{kind:Et.Tmpl,fn:EG},{kind:Et.Both,fn:FQ},{kind:Et.Tmpl,fn:VX},{kind:Et.Both,fn:CG},{kind:Et.Both,fn:KQ},{kind:Et.Tmpl,fn:vG},{kind:Et.Tmpl,fn:JQ},{kind:Et.Tmpl,fn:DG},{kind:Et.Tmpl,fn:nK},{kind:Et.Both,fn:SY},{kind:Et.Both,fn:aK},{kind:Et.Tmpl,fn:oq},{kind:Et.Tmpl,fn:iq},{kind:Et.Tmpl,fn:rq},{kind:Et.Tmpl,fn:tY},{kind:Et.Both,fn:aG},{kind:Et.Both,fn:WX},{kind:Et.Tmpl,fn:uY},{kind:Et.Tmpl,fn:jX},{kind:Et.Both,fn:XX},{kind:Et.Tmpl,fn:IG},{kind:Et.Tmpl,fn:gY},{kind:Et.Tmpl,fn:pY},{kind:Et.Both,fn:HX},{kind:Et.Both,fn:JX},{kind:Et.Tmpl,fn:DQ},{kind:Et.Both,fn:LG},{kind:Et.Both,fn:rY},{kind:Et.Both,fn:dY},{kind:Et.Both,fn:CY},{kind:Et.Both,fn:iY},{kind:Et.Tmpl,fn:OQ},{kind:Et.Tmpl,fn:TG},{kind:Et.Tmpl,fn:$X},{kind:Et.Tmpl,fn:dG},{kind:Et.Tmpl,fn:lG},{kind:Et.Tmpl,fn:nY},{kind:Et.Tmpl,fn:GX},{kind:Et.Tmpl,fn:qX},{kind:Et.Tmpl,fn:ZG},{kind:Et.Tmpl,fn:SQ},{kind:Et.Tmpl,fn:KX},{kind:Et.Tmpl,fn:aq},{kind:Et.Both,fn:wG},{kind:Et.Tmpl,fn:BX},{kind:Et.Both,fn:hY},{kind:Et.Tmpl,fn:nq},{kind:Et.Both,fn:RQ},{kind:Et.Tmpl,fn:UX},{kind:Et.Tmpl,fn:VQ},{kind:Et.Tmpl,fn:jQ},{kind:Et.Tmpl,fn:RG},{kind:Et.Tmpl,fn:pG},{kind:Et.Tmpl,fn:HQ},{kind:Et.Both,fn:rK},{kind:Et.Both,fn:AX},{kind:Et.Both,fn:fG}];function vF(n,o){for(let e of AY)(e.kind===o||e.kind===Et.Both)&&e.fn(n)}function NY(n,o){let e=yF(n.root);return xF(n.root,o),e}function xF(n,o){for(let e of n.job.units){if(e.parent!==n.xref)continue;xF(e,o);let t=yF(e);o.statements.push(t.toDeclStmt(t.name))}}function yF(n){if(n.fnName===null)throw new Error(`AssertionError: view ${n.xref} is unnamed`);let o=[];for(let r of n.create){if(r.kind!==j.Statement)throw new Error(`AssertionError: expected all create ops to have been compiled, but got ${j[r.kind]}`);o.push(r.statement)}let e=[];for(let r of n.update){if(r.kind!==j.Statement)throw new Error(`AssertionError: expected all update ops to have been compiled, but got ${j[r.kind]}`);e.push(r.statement)}let t=y1(1,o),i=y1(2,e);return bp([new Er(Jg,Jm),new Er(Ys,xs)],[...t,...i],void 0,void 0,n.fnName)}function y1(n,o){return o.length===0?[]:[T1(new Si(lt.BitwiseAnd,si(Jg),Oe(n)),o)]}function FY(n){if(n.root.fnName===null)throw new Error("AssertionError: host binding function is unnamed");let o=[];for(let r of n.root.create){if(r.kind!==j.Statement)throw new Error(`AssertionError: expected all create ops to have been compiled, but got ${j[r.kind]}`);o.push(r.statement)}let e=[];for(let r of n.root.update){if(r.kind!==j.Statement)throw new Error(`AssertionError: expected all update ops to have been compiled, but got ${j[r.kind]}`);e.push(r.statement)}if(o.length===0&&e.length===0)return null;let t=y1(1,o),i=y1(2,e);return bp([new Er(Jg,Jm),new Er(Ys,xs)],[...t,...i],void 0,void 0,n.root.fnName)}var px=".",RY="attr",Gk="animate",LY="class",zY="style",VY="*",qk="animate-",mT=class{_exprParser;_schemaRegistry;errors;constructor(o,e,t){this._exprParser=o,this._schemaRegistry=e,this.errors=t}createBoundHostProperties(o,e){let t=[];for(let i of Object.keys(o)){let r=o[i];typeof r=="string"?this.parsePropertyBinding(i,r,!0,!1,e,e.start.offset,void 0,[],t,e):this._reportError(`Value of the host property binding "${i}" needs to be a string representing an expression but got "${r}" (${typeof r})`,e)}return t}createDirectiveHostEventAsts(o,e){let t=[];for(let i of Object.keys(o)){let r=o[i];typeof r=="string"?this.parseEvent(i,r,!1,e,e,[],t,e):this._reportError(`Value of the host listener "${i}" needs to be a string representing an expression but got "${r}" (${typeof r})`,e)}return t}parseInterpolation(o,e,t){let i=e.fullStart.offset;try{let r=this._exprParser.parseInterpolation(o,e,i,t);return r&&this.errors.push(...r.errors),r}catch(r){return this._reportError(`${r}`,e),this._exprParser.wrapLiteralPrimitive("ERROR",e,i)}}parseInterpolationExpression(o,e){let t=e.start.offset;try{let i=this._exprParser.parseInterpolationExpression(o,e,t);return i&&this.errors.push(...i.errors),i}catch(i){return this._reportError(`${i}`,e),this._exprParser.wrapLiteralPrimitive("ERROR",e,t)}}parseInlineTemplateBinding(o,e,t,i,r,s,d,m){let u=t.start.offset+VY.length,h=this._parseTemplateBindings(o,e,t,u,i);for(let v of h){let b=mp(t,v.sourceSpan),y=v.key.source,x=mp(t,v.key.span);if(v instanceof Jf){let P=v.value?v.value.source:"$implicit",k=v.value?mp(t,v.value.span):void 0;d.push(new hP(y,P,b,x,k))}else if(v.value){let P=m?b:t,k=mp(t,v.value.ast.sourceSpan);this._parsePropertyAst(y,v.value,!1,P,x,k,r,s)}else r.push([y,""]),this.parseLiteralAttr(y,null,x,i,void 0,r,s,x)}}_parseTemplateBindings(o,e,t,i,r){try{let s=this._exprParser.parseTemplateBindings(o,e,t,i,r);return s.errors.forEach(d=>this.errors.push(d)),s.warnings.forEach(d=>{this._reportError(d,t,Cp.WARNING)}),s.templateBindings}catch(s){return this._reportError(`${s}`,t),[]}}parseLiteralAttr(o,e,t,i,r,s,d,m){Qk(o)?(o=o.substring(1),m!==void 0&&(m=mp(m,new Gs(m.start.offset+1,m.end.offset))),e&&this._reportError('Assigning animation triggers via @prop="exp" attributes with an expression is invalid. Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.',t,Cp.ERROR),this._parseLegacyAnimation(o,e,t,i,m,r,s,d)):d.push(new Sg(o,this._exprParser.wrapLiteralPrimitive(e,"",i),Ec.LITERAL_ATTR,t,m,r))}parsePropertyBinding(o,e,t,i,r,s,d,m,u,h){o.length===0&&this._reportError("Property name is missing in binding",r);let v=!1;o.startsWith(qk)?(v=!0,o=o.substring(qk.length),h!==void 0&&(h=mp(h,new Gs(h.start.offset+qk.length,h.end.offset)))):Qk(o)&&(v=!0,o=o.substring(1),h!==void 0&&(h=mp(h,new Gs(h.start.offset+1,h.end.offset)))),v?this._parseLegacyAnimation(o,e,r,s,h,d,m,u):o.startsWith(`${Gk}${px}`)?this._parseAnimation(o,this.parseBinding(e,t,d||r,s),r,h,d,m,u):this._parsePropertyAst(o,this.parseBinding(e,t,d||r,s),i,r,h,d,m,u)}parsePropertyInterpolation(o,e,t,i,r,s,d,m){let u=this.parseInterpolation(e,i||t,m);return u?(this._parsePropertyAst(o,u,!1,t,d,i,r,s),!0):!1}_parsePropertyAst(o,e,t,i,r,s,d,m){d.push([o,e.source]),m.push(new Sg(o,e,t?Ec.TWO_WAY:Ec.DEFAULT,i,r,s))}_parseAnimation(o,e,t,i,r,s,d){s.push([o,e.source]),d.push(new Sg(o,e,Ec.ANIMATION,t,i,r))}_parseLegacyAnimation(o,e,t,i,r,s,d,m){o.length===0&&this._reportError("Animation trigger is missing",t);let u=this.parseBinding(e||"undefined",!1,s||t,i);d.push([o,u.source]),m.push(new Sg(o,u,Ec.LEGACY_ANIMATION,t,r,s))}parseBinding(o,e,t,i){try{let r=e?this._exprParser.parseSimpleBinding(o,t,i):this._exprParser.parseBinding(o,t,i);return r&&this.errors.push(...r.errors),r}catch(r){return this._reportError(`${r}`,t),this._exprParser.wrapLiteralPrimitive("ERROR",t,i)}}createBoundElementProperty(o,e,t=!1,i=!0){if(e.isLegacyAnimation)return new $x(e.name,Fi.LegacyAnimation,mi.NONE,e.expression,null,e.sourceSpan,e.keySpan,e.valueSpan);let r=null,s,d=null,m=e.name.split(px),u;if(m.length>1)if(m[0]==RY){d=m.slice(1).join(px),t||this._validatePropertyOrAttributeName(d,e.sourceSpan,!0),u=Cx(this._schemaRegistry,o,d,!0);let h=d.indexOf(":");if(h>-1){let v=d.substring(0,h),b=d.substring(h+1);d=fx(v,b)}s=Fi.Attribute}else m[0]==LY?(d=m[1],s=Fi.Class,u=[mi.NONE]):m[0]==zY?(r=m.length>2?m[2]:null,d=m[1],s=Fi.Style,u=[mi.STYLE]):m[0]==Gk&&(d=e.name,s=Fi.Animation,u=[mi.NONE]);if(d===null){let h=this._schemaRegistry.getMappedPropName(e.name);d=i?h:e.name,u=Cx(this._schemaRegistry,o,h,!1),s=e.type===Ec.TWO_WAY?Fi.TwoWay:Fi.Property,t||this._validatePropertyOrAttributeName(h,e.sourceSpan,!1)}return new $x(d,s,u[0],e.expression,r,e.sourceSpan,e.keySpan,e.valueSpan)}parseEvent(o,e,t,i,r,s,d,m){o.length===0&&this._reportError("Event name is missing in binding",i),Qk(o)?(o=o.slice(1),m!==void 0&&(m=mp(m,new Gs(m.start.offset+1,m.end.offset))),this._parseLegacyAnimationEvent(o,e,i,r,d,m)):this._parseRegularEvent(o,e,t,i,r,s,d,m)}calcPossibleSecurityContexts(o,e,t){let i=this._schemaRegistry.getMappedPropName(e);return Cx(this._schemaRegistry,o,i,t)}parseEventListenerName(o){let[e,t]=QH(o,[null,o]);return{eventName:t,target:e}}parseLegacyAnimationEventName(o){let e=KH(o,[o,null]);return{eventName:e[0],phase:e[1]===null?null:e[1].toLowerCase()}}_parseLegacyAnimationEvent(o,e,t,i,r,s){let{eventName:d,phase:m}=this.parseLegacyAnimationEventName(o),u=this._parseAction(e,i);r.push(new jx(d,m,Za.LegacyAnimation,u,t,i,s)),d.length===0&&this._reportError("Animation event name is missing in binding",t),m?m!=="start"&&m!=="done"&&this._reportError(`The provided animation output phase value "${m}" for "@${d}" is not supported (use start or done)`,t):this._reportError(`The animation trigger output event (@${d}) is missing its phase value name (start or done are currently supported)`,t)}_parseRegularEvent(o,e,t,i,r,s,d,m){let{eventName:u,target:h}=this.parseEventListenerName(o),v=this.errors.length,b=this._parseAction(e,r),y=this.errors.length===v;s.push([o,b.source]),t&&y&&!this._isAllowedAssignmentEvent(b)&&this._reportError("Unsupported expression in a two-way binding",i);let x=Za.Regular;t&&(x=Za.TwoWay),o.startsWith(`${Gk}${px}`)&&(x=Za.Animation),d.push(new jx(u,h,x,b,i,r,m))}_parseAction(o,e){let t=e&&e.start?e.start.offset:0;try{let i=this._exprParser.parseAction(o,e,t);return i&&this.errors.push(...i.errors),!i||i.ast instanceof Ea?(this._reportError("Empty expressions are not allowed",e),this._exprParser.wrapLiteralPrimitive("ERROR",e,t)):i}catch(i){return this._reportError(`${i}`,e),this._exprParser.wrapLiteralPrimitive("ERROR",e,t)}}_reportError(o,e,t=Cp.ERROR){this.errors.push(new dn(e,o,t))}_validatePropertyOrAttributeName(o,e,t){let i=t?this._schemaRegistry.validateAttribute(o):this._schemaRegistry.validateProperty(o);i.error&&this._reportError(i.msg,e,Cp.ERROR)}_isAllowedAssignmentEvent(o){return o instanceof Cs?this._isAllowedAssignmentEvent(o.ast):o instanceof Kf?this._isAllowedAssignmentEvent(o.expression):o instanceof Vg&&o.args.length===1&&o.receiver instanceof Ac&&o.receiver.name==="$any"&&o.receiver.receiver instanceof Vc?this._isAllowedAssignmentEvent(o.args[0]):(o instanceof Ac||o instanceof ru)&&!uT(o)}};function uT(n){return n instanceof Hf||n instanceof Uf?!0:n instanceof Zf?uT(n.expression):n instanceof Ac||n instanceof ru||n instanceof Vg?uT(n.receiver):!1}function Qk(n){return n[0]=="@"}function Cx(n,o,e,t){let i,[r,s]=o?ha(o,!1):[null,o],d=u=>{let[h,v]=ha(u,!1),b=h??r,y=b?`:${b}:${v}`:v;return n.securityContext(y,e,t)},m=n.allKnownElementNames();return s===null?i=m.map(d):(i=[],Ag.parse(s).forEach(u=>{let h=u.element?[u.element]:m;if(u.element&&!n.hasElement(u.element,[])){let y=`:${N1}:${u.element}`,x=`:${F1}:${u.element}`;n.hasElement(y,[])?h=[y]:n.hasElement(x,[])&&(h=[x])}let v=new Set(u.notSelectors.filter(y=>y.isElementSelector()).map(y=>y.element?.toLowerCase())),b=h.filter(y=>{let x=y.toLowerCase();return!v.has(x)&&!v.has(ha(x)[1])});i.push(...b.map(d))})),i.length===0?[mi.NONE]:Array.from(new Set(i)).sort()}function mp(n,o){let e=o.start-n.start.offset,t=o.end-n.end.offset;return new On(n.start.moveBy(e),n.end.moveBy(t),n.fullStart.moveBy(e),n.details)}var Dd=new Yg,Zm="ng-template",BY="animate.";function vx(n){return n instanceof Ja}function jY(n){return vx(n)&&n.nodes.length===1&&n.nodes[0]instanceof Kx}function $Y(n,o,e,t,i,r,s,d,m,u){let h=new b0(n,e,t,i,r,s,d,m,u);return Nd(h.root,o),h}function HY(n,o,e){let t=new p1(n.componentName,e,hs.DomOnly);for(let i of n.properties??[]){let r=qt.Property;i.name.startsWith("attr.")&&(i.name=i.name.substring(5),r=qt.Attribute),i.isLegacyAnimation&&(r=qt.LegacyAnimation),i.isAnimation&&(r=qt.Animation);let s=U6(o,n.componentSelector,i.name,r===qt.Attribute);UY(t,i,r,s)}for(let[i,r]of Object.entries(n.attributes)??[]){let s=U6(o,n.componentSelector,i,!0);WY(t,i,r,s)}for(let i of n.events??[])GY(t,i);return t}function U6(n,o,e,t){let i=n.calcPossibleSecurityContexts(o,e,t),r=Cx(Dd,null,Dd.getMappedPropName(e),t),s=r.filter(u=>u!==mi.NONE),d=s.length;return d!==r.length&&d>0||s.some(u=>!i.includes(u))?r:i.filter(u=>u!==mi.NONE)}function UY(n,o,e,t){let i,r=o.expression.ast;r instanceof I0?i=new nr(r.strings,r.expressions.map(s=>Qn(s,n,o.sourceSpan)),[]):i=Qn(r,n,o.sourceSpan),n.root.update.push(cu(n.root.xref,e,o.name,i,null,t,!1,!1,null,null,o.sourceSpan))}function WY(n,o,e,t){let i=cu(n.root.xref,qt.Attribute,o,e,null,t,!0,!1,null,null,e.sourceSpan);n.root.update.push(i)}function GY(n,o){let e;if(o.type===Za.Animation)e=KN(n.root.xref,new Ca,o.name,null,P0(n.root,o.handler,o.handlerSpan),o.name.endsWith("enter")?"enter":"leave",o.targetOrPhase,!0,o.sourceSpan);else{let[t,i]=o.type!==Za.LegacyAnimation?[null,o.targetOrPhase]:[o.targetOrPhase,null];e=qT(n.root.xref,new Ca,o.name,null,P0(n.root,o.handler,o.handlerSpan),t,i,!0,o.sourceSpan)}n.root.create.push(e)}function Nd(n,o){for(let e of o)if(e instanceof Bc)qY(n,e);else if(e instanceof qs)QY(n,e);else if(e instanceof Ug)KY(n,e);else if(e instanceof Um)bF(n,e,null);else if(e instanceof jg)wF(n,e,null);else if(e instanceof qx)XY(n,e);else if(e instanceof Gx)YY(n,e);else if(e instanceof su)ZY(n,e);else if(e instanceof xN)eZ(n,e);else if(e instanceof Hg)tZ(n,e);else if(e instanceof LT)iZ(n,e);else if(!(e instanceof kf))throw new Error(`Unsupported template node: ${e.constructor.name}`)}function qY(n,o){if(o.i18n!==void 0&&!(o.i18n instanceof Ja||o.i18n instanceof Mp))throw Error(`Unhandled i18n metadata type for element: ${o.i18n.constructor.name}`);let e=n.job.allocateXrefId(),[t,i]=ha(o.name),r=BW(i,e,tF(t),o.i18n instanceof Mp?o.i18n:void 0,o.startSourceSpan,o.sourceSpan);n.create.push(r),rZ(n,r,o),kF(r,o);let s=null;o.i18n instanceof Ja&&(s=n.job.allocateXrefId(),n.create.push(I1(s,o.i18n,void 0,o.startSourceSpan))),Nd(n,o.children);let d=$W(e,o.endSourceSpan??o.startSourceSpan);n.create.push(d),s!==null&&Je.insertBefore(A1(s,o.endSourceSpan??o.startSourceSpan),d)}function QY(n,o){if(o.i18n!==void 0&&!(o.i18n instanceof Ja||o.i18n instanceof Mp))throw Error(`Unhandled i18n metadata type for template: ${o.i18n.constructor.name}`);let e=n.job.allocateView(n.xref),t=o.tagName,i="";o.tagName&&([i,t]=ha(o.tagName));let r=o.i18n instanceof Mp?o.i18n:void 0,s=tF(i),d=t===null?"":bG(t,s),m=oZ(o)?vs.NgTemplate:vs.Structural,u=WN(e.xref,m,t,d,s,r,o.startSourceSpan,o.sourceSpan);n.create.push(u),aZ(n,u,o,m),kF(u,o),Nd(e,o.children);for(let{name:h,value:v}of o.variables)e.contextVariables.set(h,v!==""?v:"$implicit");if(m===vs.NgTemplate&&o.i18n instanceof Ja){let h=n.job.allocateXrefId();Je.insertAfter(I1(h,o.i18n,void 0,o.startSourceSpan),e.create.head),Je.insertBefore(A1(h,o.endSourceSpan??o.startSourceSpan),e.create.tail)}}function KY(n,o){if(o.i18n!==void 0&&!(o.i18n instanceof Mp))throw Error(`Unhandled i18n metadata type for element: ${o.i18n.constructor.name}`);let e=null;o.children.some(r=>!(r instanceof O1)&&(!(r instanceof Um)||r.value.trim().length>0))&&(e=n.job.allocateView(n.xref),Nd(e,o.children));let t=n.job.allocateXrefId(),i=XW(t,o.selector,o.i18n,e?.xref??null,o.sourceSpan);for(let r of o.attributes){let s=Dd.securityContext(o.name,r.name,!0);n.update.push(cu(i.xref,qt.Attribute,r.name,Oe(r.value),null,s,!0,!1,null,Td(r.i18n),r.sourceSpan))}n.create.push(i)}function bF(n,o,e){n.create.push(QN(n.job.allocateXrefId(),o.value,e,o.sourceSpan))}function wF(n,o,e){let t=o.value;if(t instanceof Cs&&(t=t.ast),!(t instanceof I0))throw new Error(`AssertionError: expected Interpolation for BoundText node, got ${t.constructor.name}`);if(o.i18n!==void 0&&!(o.i18n instanceof Ed))throw Error(`Unhandled i18n metadata type for text interpolation: ${o.i18n?.constructor.name}`);let i=o.i18n instanceof Ed?o.i18n.children.filter(s=>s instanceof l0).map(s=>s.name):[];if(i.length>0&&i.length!==t.expressions.length)throw Error(`Unexpected number of i18n placeholders (${t.expressions.length}) for BoundText with ${t.expressions.length} expressions`);let r=n.job.allocateXrefId();n.create.push(QN(r,"",e,o.sourceSpan)),n.update.push(kW(r,new nr(t.strings,t.expressions.map(s=>Qn(s,n.job,null)),i),o.sourceSpan))}function XY(n,o){let e=null,t=[];for(let i=0;ib.modifier==="none")||h.some(b=>b.modifier==="none")||u.push(gp(d,{kind:ao.Idle},"none",null)),n.create.push(u),n.update.push(h)}function JY(n){return Object.keys(n.hydrateTriggers).length>0?1:null}function Kk(n,o,e,t,i,r){if(o.idle!==void 0){let s=gp(r,{kind:ao.Idle},n,o.idle.sourceSpan);e.push(s)}if(o.immediate!==void 0){let s=gp(r,{kind:ao.Immediate},n,o.immediate.sourceSpan);e.push(s)}if(o.timer!==void 0){let s=gp(r,{kind:ao.Timer,delay:o.timer.delay},n,o.timer.sourceSpan);e.push(s)}if(o.hover!==void 0){let s=gp(r,{kind:ao.Hover,targetName:o.hover.reference,targetXref:null,targetSlot:null,targetView:null,targetSlotViewSteps:null},n,o.hover.sourceSpan);e.push(s)}if(o.interaction!==void 0){let s=gp(r,{kind:ao.Interaction,targetName:o.interaction.reference,targetXref:null,targetSlot:null,targetView:null,targetSlotViewSteps:null},n,o.interaction.sourceSpan);e.push(s)}if(o.viewport!==void 0){let s=gp(r,{kind:ao.Viewport,targetName:o.viewport.reference,targetXref:null,targetSlot:null,targetView:null,targetSlotViewSteps:null,options:o.viewport.options?Qn(o.viewport.options,i.job,o.viewport.sourceSpan):null},n,o.viewport.sourceSpan);e.push(s)}if(o.never!==void 0){let s=gp(r,{kind:ao.Never},n,o.never.sourceSpan);e.push(s)}if(o.when!==void 0){if(o.when.value instanceof I0)throw new Error("Unexpected interpolation in defer block when trigger");let s=FW(r,Qn(o.when.value,i.job,o.when.sourceSpan),n,o.when.sourceSpan);t.push(s)}}function eZ(n,o){if(o.i18n instanceof Ja&&jY(o.i18n)){let e=n.job.allocateXrefId();n.create.push(eG(e,o.i18n,wN(o.i18n).name,null));for(let[t,i]of Object.entries(q(q({},o.vars),o.placeholders)))i instanceof jg?wF(n,i,t):bF(n,i,t);n.create.push(tG(e))}else throw Error(`Unhandled i18n metadata type for ICU: ${o.i18n?.constructor.name}`)}function tZ(n,o){let e=n.job.allocateView(n.xref),t=`\u0275$index_${e.xref}`,i=`\u0275$count_${e.xref}`,r=new Set;e.contextVariables.set(o.item.name,o.item.value);for(let S of o.contextVariables)S.value==="$index"&&r.add(S.name),S.name==="$index"?e.contextVariables.set("$index",S.value).set(t,S.value):S.name==="$count"?e.contextVariables.set("$count",S.value).set(i,S.value):e.aliases.add({kind:oa.Alias,name:null,identifier:S.name,expression:nZ(S,t,i)});let s=jr(o.trackBy.span,o.sourceSpan),d=Qn(o.trackBy,n.job,s);Nd(e,o.children);let m=null,u=null;o.empty!==null&&(m=n.job.allocateView(n.xref),Nd(m,o.empty.children),u=b1(n,m.xref,o.empty));let h={$index:r,$implicit:o.item.name};if(o.i18n!==void 0&&!(o.i18n instanceof Sp))throw Error("AssertionError: Unhandled i18n metadata type or @for");if(o.empty?.i18n!==void 0&&!(o.empty.i18n instanceof Sp))throw Error("AssertionError: Unhandled i18n metadata type or @empty");let v=o.i18n,b=o.empty?.i18n,y=b1(n,e.xref,o),x=jW(e.xref,m?.xref??null,y,d,h,u,v,b,o.startSourceSpan,o.sourceSpan);n.create.push(x);let P=Qn(o.expression,n.job,jr(o.expression.span,o.sourceSpan)),k=NW(x.xref,x.handle,P,o.sourceSpan);n.update.push(k)}function nZ(n,o,e){switch(n.value){case"$index":return new na(o);case"$count":return new na(e);case"$first":return new na(o).identical(Oe(0));case"$last":return new na(o).identical(new na(e).minus(Oe(1)));case"$even":return new na(o).modulo(Oe(2)).identical(Oe(0));case"$odd":return new na(o).modulo(Oe(2)).notIdentical(Oe(0));default:throw new Error(`AssertionError: unknown @for loop variable ${n.value}`)}}function iZ(n,o){let e=n.job.allocateXrefId();n.create.push(ZW(e,o.name,o.sourceSpan)),n.update.push(LW(e,o.name,Qn(o.value,n.job,o.valueSpan),o.sourceSpan))}function Qn(n,o,e){if(n instanceof Cs)return Qn(n.ast,o,e);if(n instanceof Ac)return n.receiver instanceof Vc?new na(n.name):new Ks(Qn(n.receiver,o,e),n.name,null,jr(n.span,e));if(n instanceof Vg){if(n.receiver instanceof Vc)throw new Error("Unexpected ImplicitReceiver");return new ys(Qn(n.receiver,o,e),n.args.map(t=>Qn(t,o,e)),void 0,jr(n.span,e))}else{if(n instanceof _s)return Oe(n.value,void 0,jr(n.span,e));if(n instanceof Dg)switch(n.operator){case"+":return new tu(Nf.Plus,Qn(n.expr,o,e),void 0,jr(n.span,e));case"-":return new tu(Nf.Minus,Qn(n.expr,o,e),void 0,jr(n.span,e));default:throw new Error(`AssertionError: unknown unary operator ${n.operator}`)}else if(n instanceof qa){let t=xG.get(n.operation);if(t===void 0)throw new Error(`AssertionError: unknown binary operator ${n.operation}`);return new Si(t,Qn(n.left,o,e),Qn(n.right,o,e),void 0,jr(n.span,e))}else{if(n instanceof $f)return new Tp(o.root.xref);if(n instanceof ru)return new Id(Qn(n.receiver,o,e),Qn(n.key,o,e),void 0,jr(n.span,e));if(n instanceof zg)throw new Error("AssertionError: Chain in unknown context");if(n instanceof au){let t=n.keys.map((i,r)=>{let s=Qn(n.values[r],o,e);return i.kind==="spread"?new yp(s):new Rg(i.key,s,i.quoted)});return new ic(t,void 0,jr(n.span,e))}else{if(n instanceof Wf)return new zc(n.expressions.map(t=>Qn(t,o,e)));if(n instanceof Nx)return new Lc(Qn(n.condition,o,e),Qn(n.trueExp,o,e),Qn(n.falseExp,o,e),void 0,jr(n.span,e));if(n instanceof Kf)return Qn(n.expression,o,e);if(n instanceof Fx)return new pu(o.allocateXrefId(),new Ca,n.name,[Qn(n.exp,o,e),...n.args.map(t=>Qn(t,o,e))]);if(n instanceof Uf)return new Qg(Qn(n.receiver,o,e),Qn(n.key,o,e),jr(n.span,e));if(n instanceof Hf)return new qg(Qn(n.receiver,o,e),n.name);if(n instanceof Lx)return new mu(Qn(n.receiver,o,e),n.args.map(t=>Qn(t,o,e)));if(n instanceof Ea)return new x0(jr(n.span,e));if(n instanceof Gf)return VH(Qn(n.expression,o,e),jr(n.span,e));if(n instanceof qf)return D0(Qn(n.expression,o,e));if(n instanceof Qf)return new Sx(Qn(n.expression,o,e),void 0,jr(n.span,e));if(n instanceof Yf)return W6(n,o,e);if(n instanceof Xf)return new Ff(Qn(n.tag,o,e),W6(n.template,o,e),void 0,jr(n.span,e));if(n instanceof Zf)return new nc(Qn(n.expression,o,e),void 0,jr(n.span,e));if(n instanceof Bx)return new Fg(n.body,n.flags,e);if(n instanceof Rx)return new nu(Qn(n.expression,o,e));if(n instanceof Vx)return lZ(Xs(n.parameters.map(t=>new Er(t.name,xs)),Qn(n.body,o,e)));throw new Error(`Unhandled expression type "${n.constructor.name}" in file "${e?.start.file.url}"`)}}}}function W6(n,o,e){return new Lf(n.elements.map(t=>new kx(t.text,jr(t.span,e))),n.expressions.map(t=>Qn(t,o,e)),jr(n.span,e))}function gT(n,o,e,t){let i;return o instanceof I0?i=new nr(o.strings,o.expressions.map(r=>Qn(r,n,null)),Object.keys(Td(e)?.placeholders??{})):o instanceof so?i=Qn(o,n,null):i=Oe(o),i}var MF=new Map([[Fi.Property,qt.Property],[Fi.TwoWay,qt.TwoWayProperty],[Fi.Attribute,qt.Attribute],[Fi.Class,qt.ClassName],[Fi.Style,qt.StyleProperty],[Fi.LegacyAnimation,qt.LegacyAnimation],[Fi.Animation,qt.Animation]]);function oZ(n){return ha(n.tagName??"")[1]===Zm}function Td(n){if(n==null)return null;if(!(n instanceof Ja))throw Error(`Expected i18n meta to be a Message, but got: ${n.constructor.name}`);return n}function rZ(n,o,e){let t=new Array,i=new Set;for(let r of e.attributes){let[s,d]=ha(e.name),m=s;if(!s)switch(o.namespace){case Tr.SVG:m=N1;break;case Tr.Math:m=F1;break}let u=Dd.securityContext(m?`:${m}:${d}`:d,r.name,!0);t.push(cu(o.xref,qt.Attribute,r.name,gT(n.job,r.value,r.i18n),null,u,!0,!1,null,Td(r.i18n),r.sourceSpan)),r.i18n&&i.add(r.name)}for(let r of e.inputs)i.has(r.name)&&console.error(`On component ${n.job.componentName}, the binding ${r.name} is both an i18n attribute and a property. You may want to remove the property binding. This will become a compilation error in future versions of Angular.`),t.push(cu(o.xref,MF.get(r.type),r.name,gT(n.job,T0(r.value),r.i18n),r.unit,r.securityContext,!1,!1,null,Td(r.i18n)??null,r.sourceSpan));n.create.push(t.filter(r=>r?.kind===j.ExtractedAttribute)),n.update.push(t.filter(r=>r?.kind===j.Binding));for(let r of e.outputs){if(r.type===Za.LegacyAnimation&&r.phase===null)throw Error("Animation listener should have a phase");r.type===Za.TwoWay?n.create.push(XN(o.xref,o.handle,r.name,o.tag,SF(n,r.handler,r.handlerSpan),r.sourceSpan)):r.type===Za.Animation?n.create.push(KN(o.xref,o.handle,r.name,o.tag,P0(n,r.handler,r.handlerSpan),r.name.endsWith("enter")?"enter":"leave",r.target,!1,r.sourceSpan)):n.create.push(qT(o.xref,o.handle,r.name,o.tag,P0(n,r.handler,r.handlerSpan),r.phase,r.target,!1,r.sourceSpan))}t.some(r=>r?.i18nMessage)!==null&&n.create.push(YN(n.job.allocateXrefId(),new Ca,o.xref))}function aZ(n,o,e,t){let i=new Array;for(let r of e.templateAttrs)if(r instanceof $g){let s=Dd.securityContext(Zm,r.name,!0);i.push(ux(n,o.xref,Fi.Attribute,r.name,r.value,null,s,!0,t,Td(r.i18n),r.sourceSpan))}else i.push(ux(n,o.xref,r.type,r.name,T0(r.value),r.unit,r.securityContext,!0,t,Td(r.i18n),r.sourceSpan));for(let r of e.attributes){let s=Dd.securityContext(Zm,r.name,!0);i.push(ux(n,o.xref,Fi.Attribute,r.name,r.value,null,s,!1,t,Td(r.i18n),r.sourceSpan))}for(let r of e.inputs)i.push(ux(n,o.xref,r.type,r.name,T0(r.value),r.unit,r.securityContext,!1,t,Td(r.i18n),r.sourceSpan));n.create.push(i.filter(r=>r?.kind===j.ExtractedAttribute)),n.update.push(i.filter(r=>r?.kind===j.Binding));for(let r of e.outputs){if(r.type===Za.LegacyAnimation&&r.phase===null)throw Error("Animation listener should have a phase");if(t===vs.NgTemplate&&(r.type===Za.TwoWay?n.create.push(XN(o.xref,o.handle,r.name,o.tag,SF(n,r.handler,r.handlerSpan),r.sourceSpan)):n.create.push(qT(o.xref,o.handle,r.name,o.tag,P0(n,r.handler,r.handlerSpan),r.phase,r.target,!1,r.sourceSpan))),t===vs.Structural&&r.type!==Za.LegacyAnimation){let s=Dd.securityContext(Zm,r.name,!1);n.create.push(Cl(o.xref,qt.Property,null,r.name,null,null,null,s))}}i.some(r=>r?.i18nMessage)!==null&&n.create.push(YN(n.job.allocateXrefId(),new Ca,o.xref))}function ux(n,o,e,t,i,r,s,d,m,u,h){let v=typeof i=="string";if(m===vs.Structural){if(!d)switch(e){case Fi.Property:case Fi.Class:case Fi.Style:return Cl(o,qt.Property,null,t,null,null,u,s);case Fi.TwoWay:return Cl(o,qt.TwoWayProperty,null,t,null,null,u,s)}if(!v&&(e===Fi.Attribute||e===Fi.LegacyAnimation||e===Fi.Animation))return null}let b=MF.get(e);return m===vs.NgTemplate&&(e===Fi.Class||e===Fi.Style||e===Fi.Attribute&&!v)&&(b=qt.Property),cu(o,b,t,gT(n.job,i,u),r,s,v,d,m,u,h)}function P0(n,o,e){o=T0(o);let t=new Array,i=o instanceof zg?o.expressions:[o];if(i.length===0)throw new Error("Expected listener to have non-empty expression list.");let r=i.map(d=>Qn(d,n.job,e)),s=r.pop();return t.push(...r.map(d=>Zs(new fa(d,d.sourceSpan)))),t.push(Zs(new Or(s,s.sourceSpan))),t}function SF(n,o,e){o=T0(o);let t=new Array;if(o instanceof zg)if(o.expressions.length===1)o=o.expressions[0];else throw new Error("Expected two-way listener to have a single expression.");let i=Qn(o,n.job,e),r=new na("$event"),s=new r1(i,r);return t.push(Zs(new fa(s))),t.push(Zs(new Or(r))),t}function T0(n){return n instanceof Cs?n.ast:n}function kF(n,o){sZ(n.localRefs);for(let{name:e,value:t}of o.references)n.localRefs.push({name:e,target:t})}function sZ(n){if(!Array.isArray(n))throw new Error("AssertionError: expected an array")}function jr(n,o){if(o===null)return null;let e=o.start.moveBy(n.start),t=o.start.moveBy(n.end),i=o.fullStart.moveBy(n.start);return new On(e,t,i)}function b1(n,o,e){let t=null;for(let i of e.children)if(!(i instanceof O1||i instanceof LT)){if(t!==null)return null;if(i instanceof Bc||i instanceof qs&&i.tagName!==null)t=i;else return null}if(t!==null){for(let r of t.attributes)if(!r.name.startsWith(BY)){let s=Dd.securityContext(Zm,r.name,!0);n.update.push(cu(o,qt.Attribute,r.name,Oe(r.value),null,s,!0,!1,null,Td(r.i18n),r.sourceSpan))}for(let r of t.inputs)if(r.type!==Fi.LegacyAnimation&&r.type!==Fi.Animation&&r.type!==Fi.Attribute){let s=Dd.securityContext(Zm,r.name,!0);n.create.push(Cl(o,qt.Property,null,r.name,null,null,null,s))}let i=t instanceof Bc?t.name:t.tagName;return i===Zm?null:i}return null}function lZ(n){let o=new Set(n.params.map(e=>e.name));return Rt(n,e=>{if(e instanceof gu)for(let t of e.params)o.add(t.name);else if(e instanceof na&&o.has(e.name))return si(e.name);return e},ii.None)}var cZ=!1;function dZ(){return cZ}function w1(n,o){return T1(si(Jg).bitwiseAnd(Oe(n),null),o)}function pZ(n){return(n.descendants?1:0)|(n.static?2:0)|(n.emitDistinctChangesOnly?4:0)}function mZ(n,o){if(Array.isArray(n.predicate)){let e=[];return n.predicate.forEach(t=>{let i=t.split(",").map(r=>Oe(r.trim()));e.push(...i)}),o.getConstLiteral(Ji(e),!0)}else switch(n.predicate.forwardRef){case 0:case 2:return n.predicate.expression;case 1:return Vt(ge.resolveForwardRef).callFn([n.predicate.expression])}}function PF(n,o,e){let t=[];return e!==void 0&&t.push(...e),n.isSignal&&t.push(new Ks(si(Ys),n.propertyName)),t.push(mZ(n,o),Oe(pZ(n))),n.read&&t.push(n.read),t}var YT=Symbol("queryAdvancePlaceholder");function TF(n){let o=[],e=0,t=()=>{e>0&&(o.unshift(Vt(ge.queryAdvance).callFn(e===1?[]:[Oe(e)]).toStmt()),e=0)};for(let i=n.length-1;i>=0;i--){let r=n[i];r===YT?e++:(t(),o.unshift(r))}return t(),o}function uZ(n,o,e){let t=[],i=[],r=MN(u=>i.push(u),VT),s=null,d=null;n.forEach(u=>{let h=PF(u,o);if(u.isSignal?(s??=Vt(ge.viewQuerySignal),s=s.callFn(h)):(d??=Vt(ge.viewQuery),d=d.callFn(h)),u.isSignal){i.push(YT);return}let v=r(),b=Vt(ge.loadQuery).callFn([]),y=Vt(ge.queryRefresh).callFn([v.set(b)]),x=si(Ys).prop(u.propertyName).set(u.first?v.prop("first"):v);i.push(y.and(x).toStmt())}),s!==null&&t.push(new fa(s)),d!==null&&t.push(new fa(d));let m=e?`${e}_Query`:null;return bp([new Er(Jg,Jm),new Er(Ys,xs)],[w1(1,t),w1(2,TF(i))],ec,null,m)}function gZ(n,o,e){let t=[],i=[],r=MN(u=>i.push(u),VT),s=null,d=null;for(let u of n){let h=PF(u,o,[si("dirIndex")]);if(u.isSignal?(s??=Vt(ge.contentQuerySignal),s=s.callFn(h)):(d??=Vt(ge.contentQuery),d=d.callFn(h)),u.isSignal){i.push(YT);continue}let v=r(),b=Vt(ge.loadQuery).callFn([]),y=Vt(ge.queryRefresh).callFn([v.set(b)]),x=si(Ys).prop(u.propertyName).set(u.first?v.prop("first"):v);i.push(y.and(x).toStmt())}s!==null&&t.push(new fa(s)),d!==null&&t.push(new fa(d));let m=e?`${e}_ContentQueries`:null;return bp([new Er(Jg,Jm),new Er(Ys,xs),new Er("dirIndex",Jm)],[w1(1,t),w1(2,TF(i))],ec,null,m)}var hT=class extends wq{constructor(){super(rT)}parse(o,e,t){return super.parse(o,e,t)}};function hZ(n){if(n==null||n.length===0||n[0]=="/")return!1;let o=n.match(_Z);return o===null||o[1]=="package"||o[1]=="asset"}var _Z=/^([^:/?#]+):/,fZ="select",CZ="link",vZ="rel",xZ="href",yZ="stylesheet",bZ="style",wZ=new Set([":svg:script","script"]),MZ="ngNonBindable",SZ="ngProjectAs";function EF(n){let o=null,e=null,t=null,i=!1,r="";for(let m of n.attrs){let u=m.name.toLowerCase();u==fZ?o=m.value:u==xZ?e=m.value:u==vZ?t=m.value:m.name==MZ?i=!0:m.name==SZ&&m.value.length>0&&(r=m.value)}o||="*";let s=n.name.toLowerCase(),d=Ws.OTHER;return _P(s)?d=Ws.NG_CONTENT:bZ===s?d=Ws.STYLE:wZ.has(s)?d=Ws.SCRIPT:s==CZ&&t==yZ&&(d=Ws.STYLESHEET),new _T(d,o,e,i,r)}var Ws=(function(n){return n[n.NG_CONTENT=0]="NG_CONTENT",n[n.STYLE=1]="STYLE",n[n.STYLESHEET=2]="STYLESHEET",n[n.SCRIPT=3]="SCRIPT",n[n.OTHER=4]="OTHER",n})(Ws||{}),_T=class{type;selectAttr;hrefAttr;nonBindable;projectAs;constructor(o,e,t,i,r){this.type=o,this.selectAttr=e,this.hrefAttr=t,this.nonBindable=i,this.projectAs=r}},kZ=/^\s*([0-9A-Za-z_$]*)\s+of\s+([\S\s]*)/,PZ=/^track\s+([\S\s]*)/,TZ=/^(as\s+)(.*)/,L1=/^else[^\S\r\n]+if/,EZ=/^let\s+([\S\s]*)/,OZ=/^[$A-Z_][0-9A-Z_$]*$/i,G6=/(\s*)(\S+)(\s*)/,Af=new Set(["$index","$first","$last","$even","$odd","$count"]);function q6(n){return n==="empty"}function Q6(n){return n==="else"||L1.test(n)}function DZ(n,o,e,t){let i=LZ(o),r=[],s=K6(n,i,t);s!==null&&r.push(new qm(s.expression,Mo(e,n.children,n.children),s.expressionAlias,n.sourceSpan,n.startSourceSpan,n.endSourceSpan,n.nameSpan,n.i18n));for(let v of o)if(L1.test(v.name)){let b=K6(v,i,t);if(b!==null){let y=Mo(e,v.children,v.children);r.push(new qm(b.expression,y,b.expressionAlias,v.sourceSpan,v.startSourceSpan,v.endSourceSpan,v.nameSpan,v.i18n))}}else if(v.name==="else"){let b=Mo(e,v.children,v.children);r.push(new qm(null,b,null,v.sourceSpan,v.startSourceSpan,v.endSourceSpan,v.nameSpan,v.i18n))}let d=r.length>0?r[0].startSourceSpan:n.startSourceSpan,m=r.length>0?r[r.length-1].endSourceSpan:n.endSourceSpan,u=n.sourceSpan,h=r[r.length-1];return h!==void 0&&(u=new On(d.start,h.sourceSpan.end)),{node:new qx(r,u,n.startSourceSpan,m,n.nameSpan),errors:i}}function IZ(n,o,e,t){let i=[],r=NZ(n,i,t),s=null,d=null;for(let m of o)m.name==="empty"?d!==null?i.push(new dn(m.sourceSpan,"@for loop can only have one @empty block")):m.parameters.length>0?i.push(new dn(m.sourceSpan,"@empty block cannot have parameters")):d=new r0(Mo(e,m.children,m.children),m.sourceSpan,m.startSourceSpan,m.endSourceSpan,m.nameSpan,m.i18n):i.push(new dn(m.sourceSpan,`Unrecognized @for loop block "${m.name}"`));if(r!==null)if(r.trackBy===null)i.push(new dn(n.startSourceSpan,'@for loop must have a "track" expression'));else{let m=d?.endSourceSpan??n.endSourceSpan,u=new On(n.sourceSpan.start,m?.end??n.sourceSpan.end);FZ(r.trackBy.expression,r.trackBy.keywordSpan,i),s=new Hg(r.itemName,r.expression,r.trackBy.expression,r.trackBy.keywordSpan,r.context,Mo(e,n.children,n.children),d,u,n.sourceSpan,n.startSourceSpan,m,n.nameSpan,n.i18n)}return{node:s,errors:i}}function AZ(n,o,e){let t=zZ(n),i=n.parameters.length>0?E0(n.parameters[0],e):e.parseBinding("",!1,n.sourceSpan,0),r=[],s=[],d=[],m=null,u=null;for(let v of n.children){if(!(v instanceof hl))continue;if((v.name!=="case"||v.parameters.length===0)&&v.name!=="default"&&v.name!=="default never"){s.push(new Qx(v.name,v.sourceSpan,v.nameSpan));continue}u!==null&&t.push(new dn(v.sourceSpan,'@default block with "never" parameter must be the last case in a switch'));let b=v.name==="case",y=null;if(b)y=E0(v.parameters[0],e);else if(v.name==="default never"){(v.children.length>0||v.endSourceSpan!==null&&v.endSourceSpan.start.offset!==v.endSourceSpan.end.offset)&&t.push(new dn(v.sourceSpan,'@default block with "never" parameter cannot have a body')),d.length>0&&t.push(new dn(v.sourceSpan,'A @case block with no body cannot be followed by a @default block with "never" parameter')),u=new SP(v.sourceSpan,v.startSourceSpan,v.endSourceSpan,v.nameSpan);continue}let x=new MP(y,v.sourceSpan,v.startSourceSpan,v.endSourceSpan,v.nameSpan);if(d.push(x),v.children.length===0&&v.endSourceSpan!==null&&v.endSourceSpan.start.offset===v.endSourceSpan.end.offset){m===null&&(m=v.sourceSpan);continue}let k=v.sourceSpan,S=v.startSourceSpan;m!==null&&(k=new On(m.start,v.sourceSpan.end),S=new On(m.start,v.startSourceSpan.end),m=null);let T=new o0(d,Mo(o,v.children,v.children),k,S,v.endSourceSpan,v.nameSpan,v.i18n);r.push(T),d=[]}return{node:new Gx(i,r,s,u,n.sourceSpan,n.startSourceSpan,n.endSourceSpan,n.nameSpan),errors:t}}function NZ(n,o,e){if(n.parameters.length===0)return o.push(new dn(n.startSourceSpan,"@for loop does not have an expression")),null;let[t,...i]=n.parameters,r=VZ(t,o)?.match(kZ);if(!r||r[2].trim().length===0)return o.push(new dn(t.sourceSpan,'Cannot parse expression. @for loop expression must match the pattern " of "')),null;let[,s,d]=r;Af.has(s)&&o.push(new dn(t.sourceSpan,`@for loop item name cannot be one of ${Array.from(Af).join(", ")}.`));let m=t.expression.split(" ")[0],u=new On(t.sourceSpan.start,t.sourceSpan.start.moveBy(m.length)),h={itemName:new wp(s,"$implicit",u,u),trackBy:null,expression:E0(t,e,d),context:Array.from(Af,v=>{let b=new On(n.startSourceSpan.end,n.startSourceSpan.end);return new wp(v,v,b,b)})};for(let v of i){let b=v.expression.match(EZ);if(b!==null){let x=new On(v.sourceSpan.start.moveBy(b[0].length-b[1].length),v.sourceSpan.end);RZ(v.sourceSpan,b[1],x,s,h.context,o);continue}let y=v.expression.match(PZ);if(y!==null){if(h.trackBy!==null)o.push(new dn(v.sourceSpan,'@for loop can only have one "track" expression'));else{let x=E0(v,e,y[1]);x.ast instanceof Ea&&o.push(new dn(n.startSourceSpan,'@for loop must have a "track" expression'));let P=new On(v.sourceSpan.start,v.sourceSpan.start.moveBy(5));h.trackBy={expression:x,keywordSpan:P}}continue}o.push(new dn(v.sourceSpan,`Unrecognized @for loop parameter "${v.expression}"`))}return h}function FZ(n,o,e){let t=new fT;n.ast.visit(t),t.hasPipe&&e.push(new dn(o,"Cannot use pipes in track expressions"))}function RZ(n,o,e,t,i,r){let s=o.split(","),d=e.start;for(let m of s){let u=m.split("="),h=u.length===2?u[0].trim():"",v=u.length===2?u[1].trim():"";if(h.length===0||v.length===0)r.push(new dn(n,'Invalid @for loop "let" parameter. Parameter should match the pattern " = "'));else if(!Af.has(v))r.push(new dn(n,`Unknown "let" parameter variable "${v}". The allowed variables are: ${Array.from(Af).join(", ")}`));else if(h===t)r.push(new dn(n,`Invalid @for loop "let" parameter. Variable cannot be called "${t}"`));else if(i.some(b=>b.name===h))r.push(new dn(n,`Duplicate "let" parameter variable "${v}"`));else{let[,b,y]=u[0].match(G6)??[],x=b!==void 0&&u.length===2?new On(d.moveBy(b.length),d.moveBy(b.length+y.length)):e,P;if(u.length===2){let[,S,T]=u[1].match(G6)??[];P=S!==void 0?new On(d.moveBy(u[0].length+1+S.length),d.moveBy(u[0].length+1+S.length+T.length)):void 0}let k=new On(x.start,P?.end??x.end);i.push(new wp(h,v,k,x,P))}d=d.moveBy(m.length+1)}}function LZ(n){let o=[],e=!1;for(let t=0;t1&&t0&&o.push(new dn(i.startSourceSpan,"@else block cannot have parameters")),e=!0):L1.test(i.name)||o.push(new dn(i.startSourceSpan,`Unrecognized conditional block @${i.name}`))}return o}function zZ(n){let o=[],e=!1;if(n.parameters.length!==1)return o.push(new dn(n.startSourceSpan,"@switch block must have exactly one parameter")),o;for(let t of n.children)if(!(t instanceof w0||t instanceof uu&&t.value.trim().length===0)){if(!(t instanceof hl)||t.name!=="case"&&t.name!=="default"&&t.name!=="default never"){o.push(new dn(t.sourceSpan,"@switch block can only contain @case and @default blocks"));continue}t.name==="default never"?(e&&o.push(new dn(t.startSourceSpan,"@switch block can only have one @default block")),e=!0):t.name==="default"?(e?o.push(new dn(t.startSourceSpan,"@switch block can only have one @default block")):t.parameters.length>0&&o.push(new dn(t.startSourceSpan,"@default block cannot have parameters")),e=!0):t.name==="case"&&t.parameters.length!==1&&o.push(new dn(t.startSourceSpan,"@case block must have exactly one parameter"))}return o}function E0(n,o,e){let t,i;return typeof e=="string"?(t=Math.max(0,n.expression.lastIndexOf(e)),i=t+e.length):(t=0,i=n.expression.length),o.parseBinding(n.expression.slice(t,i),!1,n.sourceSpan,n.sourceSpan.start.offset+t)}function K6(n,o,e){if(n.parameters.length===0)return o.push(new dn(n.startSourceSpan,"Conditional block does not have an expression")),null;let t=E0(n.parameters[0],e),i=null;for(let r=1;r-1;d--){let m=e[d];if(m===")"){if(s=d,i--,i===0)break}else{if(t.test(m))continue;break}}return i!==0?(o.push(new dn(n.sourceSpan,"Unclosed parentheses in expression")),null):e.slice(r,s)}var fT=class extends Bg{hasPipe=!1;visitPipe(){this.hasPipe=!0}},BZ=/^\d+\.?\d*(ms|s)?$/,jZ=/^\s$/,X6=new Map([[_l,Ka],[Nc,Pd],[Ya,Pr]]),Xa=(function(n){return n.IDLE="idle",n.TIMER="timer",n.INTERACTION="interaction",n.IMMEDIATE="immediate",n.HOVER="hover",n.VIEWPORT="viewport",n.NEVER="never",n})(Xa||{});function $Z({expression:n,sourceSpan:o},e,t){let i=n.indexOf("never"),r=new On(o.start.moveBy(i),o.start.moveBy(i+5)),s=ZT(n,o),d=JT(n,o);i===-1?t.push(new dn(o,'Could not find "never" keyword in expression')):eE("never",e,t,new xP(r,o,s,null,d))}function Xk({expression:n,sourceSpan:o},e,t,i){let r=n.indexOf("when"),s=new On(o.start.moveBy(r),o.start.moveBy(r+4)),d=ZT(n,o),m=JT(n,o);if(r===-1)i.push(new dn(o,'Could not find "when" keyword in expression'));else{let u=O0(n,r+1),h=e.parseBinding(n.slice(u),!1,o,o.start.offset+u);eE("when",t,i,new Hx(h,o,d,s,m))}}function Yk({expression:n,sourceSpan:o},e,t,i,r){let s=n.indexOf("on"),d=new On(o.start.moveBy(s),o.start.moveBy(s+2)),m=ZT(n,o),u=JT(n,o);if(s===-1)i.push(new dn(o,'Could not find "on" keyword in expression'));else{let h=O0(n,s+1),v=n.startsWith("hydrate");new CT(n,e,h,o,t,i,v?XZ:KZ,v,m,d,u).parse()}}function ZT(n,o){return n.startsWith("prefetch")?new On(o.start,o.start.moveBy(8)):null}function JT(n,o){return n.startsWith("hydrate")?new On(o.start,o.start.moveBy(7)):null}var CT=class{expression;bindingParser;start;span;triggers;errors;validator;isHydrationTrigger;prefetchSpan;onSourceSpan;hydrateSpan;index=0;tokens;constructor(o,e,t,i,r,s,d,m,u,h,v){this.expression=o,this.bindingParser=e,this.start=t,this.span=i,this.triggers=r,this.errors=s,this.validator=d,this.isHydrationTrigger=m,this.prefetchSpan=u,this.onSourceSpan=h,this.hydrateSpan=v,this.tokens=new k0().tokenize(o.slice(t))}parse(){for(;this.tokens.length>0&&this.index0&&i.isCharacter(e[e.length-1])&&e.pop(),e.length===0&&i.isCharacter(Oa)&&t.length>0){o.push({expression:this.tokenRangeText(t),start:t[0].index}),this.advance(),t=[];continue}t.push(i),this.advance()}return(!this.token().isCharacter(Pr)||e.length>0)&&this.error(this.token(),"Unexpected end of expression"),this.index0)throw new Error(`"${Xa.IDLE}" trigger cannot have parameters`);return new yP(o,e,t,i,r)}function UZ(n,o,e,t,i,r){if(n.length!==1)throw new Error(`"${Xa.TIMER}" trigger must have exactly one parameter`);let s=M1(n[0].expression);if(s===null)throw new Error(`Could not parse time value of trigger "${Xa.TIMER}"`);return new wP(s,o,e,t,i,r)}function WZ(n,o,e,t,i,r){if(n.length>0)throw new Error(`"${Xa.IMMEDIATE}" trigger cannot have parameters`);return new bP(o,e,t,i,r)}function GZ(n,o,e,t,i,r,s){return s(Xa.HOVER,n),new Ux(n[0]?.expression??null,o,e,t,i,r)}function qZ(n,o,e,t,i,r,s){return s(Xa.INTERACTION,n),new Wx(n[0]?.expression??null,o,e,t,i,r)}function QZ(n,o,e,t,i,r,s,d,m,u){u(Xa.VIEWPORT,t);let h,v;if(t.length===0)h=v=null;else if(!t[0].expression.startsWith("{"))h=t[0].expression,v=null;else{let b=e.parseBinding(t[0].expression,!1,r,r.start.offset+n+t[0].start);if(b.ast instanceof au){if(b.ast.keys.some(x=>x.kind==="spread"))throw new Error("Spread operator are not allowed in this context");if(b.ast.keys.some(x=>x.kind==="property"&&x.key==="root"))throw new Error('The "root" option is not supported in the options parameter of the "viewport" trigger')}else throw new Error('Options parameter of the "viewport" trigger must be an object literal');let y=b.ast.keys.findIndex(x=>x.kind==="property"&&x.key==="trigger");if(y===-1)h=null,v=b.ast;else{let x=b.ast.values[y],P=(k,S)=>S!==y;if(!(x instanceof Ac)||!(x.receiver instanceof Vc))throw new Error('"trigger" option of the "viewport" trigger must be an identifier');h=x.name,v=new au(b.ast.span,b.ast.sourceSpan,b.ast.keys.filter(P),b.ast.values.filter(P))}}if(o&&h!==null)throw new Error('"viewport" hydration trigger cannot have a "trigger"');if(v){let b=vT.findDynamicNode(v);if(b!==null)throw new Error(`Options of the "viewport" trigger must be an object literal containing only literal values, but "${b.constructor.name}" was found`)}return new e0(h,v,i,r,s,d,m)}function KZ(n,o){if(o.length>1)throw new Error(`"${n}" trigger can only have zero or one parameters`)}function XZ(n,o){if(n===Xa.VIEWPORT){if(o.length>1)throw new Error(`Hydration trigger "${n}" cannot have more than one parameter`);return}if(o.length>0)throw new Error(`Hydration trigger "${n}" cannot have parameters`)}function O0(n,o=0){let e=!1;for(let t=o;t0){let P=o[o.length-1];v=P.endSourceSpan,b=P.sourceSpan.end}let y=new On(n.sourceSpan.start,b);return{node:new su(Mo(e,n.children,n.children),m,u,h,r,s,d,n.nameSpan,y,n.sourceSpan,n.startSourceSpan,v,n.i18n),errors:i}}function aJ(n,o,e){let t=null,i=null,r=null;for(let s of n)try{if(!xT(s.name)){o.push(new dn(s.startSourceSpan,`Unrecognized block "@${s.name}"`));break}switch(s.name){case"placeholder":t!==null?o.push(new dn(s.startSourceSpan,"@defer block can only have one @placeholder block")):t=sJ(s,e);break;case"loading":i!==null?o.push(new dn(s.startSourceSpan,"@defer block can only have one @loading block")):i=lJ(s,e);break;case"error":r!==null?o.push(new dn(s.startSourceSpan,"@defer block can only have one @error block")):r=cJ(s,e);break}}catch(d){o.push(new dn(s.startSourceSpan,d.message))}return{placeholder:t,loading:i,error:r}}function sJ(n,o){let e=null;for(let t of n.parameters)if(OF.test(t.expression)){if(e!=null)throw new Error('@placeholder block can only have one "minimum" parameter');let i=M1(t.expression.slice(O0(t.expression)));if(i===null)throw new Error('Could not parse time value of parameter "minimum"');e=i}else throw new Error(`Unrecognized parameter in @placeholder block: "${t.expression}"`);return new t0(Mo(o,n.children,n.children),e,n.nameSpan,n.sourceSpan,n.startSourceSpan,n.endSourceSpan,n.i18n)}function lJ(n,o){let e=null,t=null;for(let i of n.parameters)if(nJ.test(i.expression)){if(e!=null)throw new Error('@loading block can only have one "after" parameter');let r=M1(i.expression.slice(O0(i.expression)));if(r===null)throw new Error('Could not parse time value of parameter "after"');e=r}else if(OF.test(i.expression)){if(t!=null)throw new Error('@loading block can only have one "minimum" parameter');let r=M1(i.expression.slice(O0(i.expression)));if(r===null)throw new Error('Could not parse time value of parameter "minimum"');t=r}else throw new Error(`Unrecognized parameter in @loading block: "${i.expression}"`);return new n0(Mo(o,n.children,n.children),e,t,n.nameSpan,n.sourceSpan,n.startSourceSpan,n.endSourceSpan,n.i18n)}function cJ(n,o){if(n.parameters.length>0)throw new Error("@error block cannot have parameters");return new i0(Mo(o,n.children,n.children),n.nameSpan,n.sourceSpan,n.startSourceSpan,n.endSourceSpan,n.i18n)}function dJ(n,o,e,t){let i={},r={},s={};for(let d of n.parameters)iJ.test(d.expression)?Xk(d,o,i,e):oJ.test(d.expression)?Yk(d,o,i,e):YZ.test(d.expression)?Xk(d,o,r,e):ZZ.test(d.expression)?Yk(d,o,r,e):JZ.test(d.expression)?Xk(d,o,s,e):eJ.test(d.expression)?Yk(d,o,s,e):tJ.test(d.expression)?$Z(d,s,e):e.push(new dn(d.sourceSpan,"Unrecognized trigger"));return s.never&&Object.keys(s).length>1&&e.push(new dn(n.startSourceSpan,"Cannot specify additional `hydrate` triggers if `hydrate never` is present")),{triggers:i,prefetchTriggers:r,hydrateTriggers:s}}var pJ=/^(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.*)$/,Y6=1,Z6=2,J6=3,eN=4,tN=5,mJ=6,Cf=7,up={BANANA_BOX:{start:"[(",end:")]"},PROPERTY:{start:"[",end:"]"},EVENT:{start:"(",end:")"}},Zk="*",uJ=new Set(["link","style","script","ng-template","ng-container","ng-content"]),gJ=new Set(["ngProjectAs","ngNonBindable"]);function hJ(n,o,e){let t=new yT(o,e),i=Mo(t,n,n),r=o.errors.concat(t.errors),s={nodes:i,errors:r,styleUrls:t.styleUrls,styles:t.styles,ngContentSelectors:t.ngContentSelectors};return e.collectCommentNodes&&(s.commentNodes=t.commentNodes),s}var yT=class{bindingParser;options;errors=[];styles=[];styleUrls=[];ngContentSelectors=[];commentNodes=[];inI18nBlock=!1;processedNodes=new Set;constructor(o,e){this.bindingParser=o,this.options=e}visitElement(o){let e=vx(o.i18n);e&&(this.inI18nBlock&&this.reportError("Cannot mark an element as translatable inside of a translatable section. Please remove the nested i18n marker.",o.sourceSpan),this.inI18nBlock=!0);let t=EF(o);if(t.type===Ws.SCRIPT)return null;if(t.type===Ws.STYLE){let S=_J(o);return S!==null&&this.styles.push(S),null}else if(t.type===Ws.STYLESHEET&&hZ(t.hrefAttr))return this.styleUrls.push(t.hrefAttr),null;let i=_U(o.name),{attributes:r,boundEvents:s,references:d,variables:m,templateVariables:u,elementHasInlineTemplate:h,parsedProperties:v,templateParsedProperties:b,i18nAttrsMeta:y}=this.prepareAttributes(o.attrs,i),x=this.extractDirectives(o),P;t.nonBindable?P=Mo(nN,o.children).flat(1/0):P=Mo(this,o.children,o.children);let k;if(t.type===Ws.NG_CONTENT){let S=t.selectAttr,T=o.attrs.map(A=>this.visitAttribute(A));k=new Ug(S,T,P,o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,o.i18n),this.ngContentSelectors.push(S)}else if(i){let S=this.categorizePropertyAttributes(o.name,v,y);k=new qs(o.name,r,S.bound,s,x,[],P,d,m,o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,o.i18n)}else{let S=this.categorizePropertyAttributes(o.name,v,y);if(o.name==="ng-container")for(let T of S.bound)T.type===Fi.Attribute&&this.reportError("Attribute bindings are not supported on ng-container. Use property bindings instead.",T.sourceSpan);k=new Bc(o.name,r,S.bound,s,x,P,d,o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,o.isVoid,o.i18n)}return h&&(k=this.wrapInTemplate(k,b,u,y,i,e)),e&&(this.inI18nBlock=!1),k}visitAttribute(o){return new $g(o.name,o.value,o.sourceSpan,o.keySpan,o.valueSpan,o.i18n)}visitText(o){return this.processedNodes.has(o)?null:this._visitTextWithInterpolation(o.value,o.sourceSpan,o.tokens,o.i18n)}visitExpansion(o){if(!o.i18n)return null;if(!vx(o.i18n))throw new Error(`Invalid type "${o.i18n.constructor}" for "i18n" property of ${o.sourceSpan.toString()}. Expected a "Message"`);let e=o.i18n,t={},i={};return Object.keys(e.placeholders).forEach(r=>{let s=e.placeholders[r];if(r.startsWith(xU)){let d=r.trim(),m=this.bindingParser.parseInterpolationExpression(s.text,s.sourceSpan);t[d]=new jg(m,s.sourceSpan)}else i[r]=this._visitTextWithInterpolation(s.text,s.sourceSpan,null)}),new xN(t,i,o.sourceSpan,e)}visitExpansionCase(o){return null}visitComment(o){return this.options.collectCommentNodes&&this.commentNodes.push(new O1(o.value||"",o.sourceSpan)),null}visitLetDeclaration(o,e){let t=this.bindingParser.parseBinding(o.value,!1,o.valueSpan,o.valueSpan.start.offset);return t.errors.length===0&&t.ast instanceof Ea&&this.reportError("@let declaration value cannot be empty",o.valueSpan),new LT(o.name,t,o.sourceSpan,o.nameSpan,o.valueSpan)}visitComponent(o){let e=vx(o.i18n);if(e&&(this.inI18nBlock&&this.reportError("Cannot mark a component as translatable inside of a translatable section. Please remove the nested i18n marker.",o.sourceSpan),this.inI18nBlock=!0),o.tagName!==null&&uJ.has(o.tagName))return this.reportError(`Tag name "${o.tagName}" cannot be used as a component tag`,o.startSourceSpan),null;let{attributes:t,boundEvents:i,references:r,templateVariables:s,elementHasInlineTemplate:d,parsedProperties:m,templateParsedProperties:u,i18nAttrsMeta:h}=this.prepareAttributes(o.attrs,!1);this.validateSelectorlessReferences(r);let v=this.extractDirectives(o),b;o.attrs.find(P=>P.name==="ngNonBindable")?b=Mo(nN,o.children).flat(1/0):b=Mo(this,o.children,o.children);let y=this.categorizePropertyAttributes(o.tagName,m,h),x=new kf(o.componentName,o.tagName,o.fullName,t,y.bound,i,v,b,r,o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,o.i18n);return d&&(x=this.wrapInTemplate(x,u,s,h,!1,e)),e&&(this.inI18nBlock=!1),x}visitDirective(){return null}visitBlockParameter(){return null}visitBlock(o,e){let t=Array.isArray(e)?e.indexOf(o):-1;if(t===-1)throw new Error("Visitor invoked incorrectly. Expecting visitBlock to be invoked siblings array as its context");if(this.processedNodes.has(o))return null;let i=null;switch(o.name){case"defer":i=rJ(o,this.findConnectedBlocks(t,e,xT),this,this.bindingParser);break;case"switch":i=AZ(o,this,this.bindingParser);break;case"for":i=IZ(o,this.findConnectedBlocks(t,e,q6),this,this.bindingParser);break;case"if":i=DZ(o,this.findConnectedBlocks(t,e,Q6),this,this.bindingParser);break;default:let r;xT(o.name)?(r=`@${o.name} block can only be used after an @defer block.`,this.processedNodes.add(o)):q6(o.name)?(r=`@${o.name} block can only be used after an @for block.`,this.processedNodes.add(o)):Q6(o.name)?(r=`@${o.name} block can only be used after an @if or @else if block.`,this.processedNodes.add(o)):r=`Unrecognized block @${o.name}.`,i={node:new Qx(o.name,o.sourceSpan,o.nameSpan),errors:[new dn(o.sourceSpan,r)]};break}return this.errors.push(...i.errors),i.node}findConnectedBlocks(o,e,t){let i=[];for(let r=o+1;r{let d=t[s.name];if(s.isLiteral)r.push(new $g(s.name,s.expression.source||"",s.sourceSpan,s.keySpan,s.valueSpan,d));else{let m=s.name.toLowerCase().startsWith("attr.on"),u=this.bindingParser.createBoundElementProperty(o,s,!m,!1);i.push(CP.fromBoundElementProperty(u,d))}}),{bound:i,literal:r}}prepareAttributes(o,e){let t=[],i=[],r=[],s=[],d=[],m={},u=[],h=[],v=!1;for(let b of o){let y=!1,x=iN(b.name),P=!1;if(b.i18n&&(m[b.name]=b.i18n),x.startsWith(Zk)){v&&this.reportError("Can't have multiple template bindings on one element. Use only one attribute prefixed with *",b.sourceSpan),P=!0,v=!0;let k=b.value,S=x.substring(Zk.length),T=[],A=b.valueSpan?b.valueSpan.fullStart.offset:b.sourceSpan.fullStart.offset+b.name.length;this.bindingParser.parseInlineTemplateBinding(S,k,b.sourceSpan,A,[],u,T,!0),h.push(...T.map(D=>new wp(D.name,D.value,D.sourceSpan,D.keySpan,D.valueSpan)))}else y=this.parseAttribute(e,b,[],t,i,r,s);!y&&!P&&d.push(this.visitAttribute(b))}return{attributes:d,boundEvents:i,references:s,variables:r,templateVariables:h,elementHasInlineTemplate:v,parsedProperties:t,templateParsedProperties:u,i18nAttrsMeta:m}}parseAttribute(o,e,t,i,r,s,d){let m=iN(e.name),u=e.value,h=e.sourceSpan,v=e.valueSpan?e.valueSpan.fullStart.offset:h.fullStart.offset;function b(S,T,A){let D=e.name.length-m.length,F=S.start.moveBy(T.length+D),I=F.moveBy(A.length);return new On(F,I,F,A)}let y=m.match(pJ);if(y){if(y[Y6]!=null){let S=y[Cf],T=b(h,y[Y6],S);this.bindingParser.parsePropertyBinding(S,u,!1,!1,h,v,e.valueSpan,t,i,T)}else if(y[Z6])if(o){let S=y[Cf],T=b(h,y[Z6],S);this.parseVariable(S,u,h,T,e.valueSpan,s)}else this.reportError('"let-" is only supported on ng-template elements.',h);else if(y[J6]){let S=y[Cf],T=b(h,y[J6],S);this.parseReference(S,u,h,T,e.valueSpan,d)}else if(y[eN]){let S=[],T=y[Cf],A=b(h,y[eN],T);this.bindingParser.parseEvent(T,u,!1,h,e.valueSpan||h,t,S,A),Jk(S,r)}else if(y[tN]){let S=y[Cf],T=b(h,y[tN],S);this.bindingParser.parsePropertyBinding(S,u,!1,!0,h,v,e.valueSpan,t,i,T),this.parseAssignmentEvent(S,u,h,e.valueSpan,t,r,T,v)}else if(y[mJ]){let S=b(h,"",m);this.bindingParser.parseLiteralAttr(m,u,h,v,e.valueSpan,t,i,S)}return!0}let x=null;if(m.startsWith(up.BANANA_BOX.start)?x=up.BANANA_BOX:m.startsWith(up.PROPERTY.start)?x=up.PROPERTY:m.startsWith(up.EVENT.start)&&(x=up.EVENT),x!==null&&m.endsWith(x.end)&&m.length>x.start.length+x.end.length){let S=m.substring(x.start.length,m.length-x.end.length),T=b(h,x.start,S);if(x.start===up.BANANA_BOX.start)this.bindingParser.parsePropertyBinding(S,u,!1,!0,h,v,e.valueSpan,t,i,T),this.parseAssignmentEvent(S,u,h,e.valueSpan,t,r,T,v);else if(x.start===up.PROPERTY.start)this.bindingParser.parsePropertyBinding(S,u,!1,!1,h,v,e.valueSpan,t,i,T);else{let A=[];this.bindingParser.parseEvent(S,u,!1,h,e.valueSpan||h,t,A,T),Jk(A,r)}return!0}let P=b(h,"",m);return this.bindingParser.parsePropertyInterpolation(m,u,h,e.valueSpan,t,i,P,e.valueTokens??null)}extractDirectives(o){let e=o instanceof Qa?o.tagName:o.name,t=[],i=new Set;for(let r of o.directives){let s=!1;for(let y of r.attrs)y.name.startsWith(Zk)?(s=!0,this.reportError(`Shorthand template syntax "${y.name}" is not supported inside a directive context`,y.sourceSpan)):gJ.has(y.name)&&(s=!0,this.reportError(`Attribute "${y.name}" is not supported in a directive context`,y.sourceSpan));if(!s&&i.has(r.name)&&(s=!0,this.reportError(`Cannot apply directive "${r.name}" multiple times on the same element`,r.sourceSpan)),s)continue;let{attributes:d,parsedProperties:m,boundEvents:u,references:h,i18nAttrsMeta:v}=this.prepareAttributes(r.attrs,!1);this.validateSelectorlessReferences(h);let{bound:b}=this.categorizePropertyAttributes(e,m,v);for(let y of b)y.type!==Fi.Property&&y.type!==Fi.TwoWay&&(s=!0,this.reportError("Binding is not supported in a directive context",y.sourceSpan));s||(i.add(r.name),t.push(new vN(r.name,d,b,u,h,r.sourceSpan,r.startSourceSpan,r.endSourceSpan,void 0)))}return t}filterAnimationAttributes(o){return o.filter(e=>!e.name.startsWith("animate."))}filterAnimationInputs(o){return o.filter(e=>e.type!==Fi.Animation)}wrapInTemplate(o,e,t,i,r,s){let d=this.categorizePropertyAttributes("ng-template",e,i),m=[];d.literal.forEach(b=>m.push(b)),d.bound.forEach(b=>m.push(b));let u={attributes:[],inputs:[],outputs:[]};(o instanceof Bc||o instanceof kf)&&(u.attributes.push(...this.filterAnimationAttributes(o.attributes)),u.inputs.push(...this.filterAnimationInputs(o.inputs)),u.outputs.push(...o.outputs));let h=r&&s?void 0:o.i18n,v;return o instanceof kf?v=o.tagName:o instanceof qs?v=null:v=o.name,new qs(v,u.attributes,u.inputs,u.outputs,[],m,[o],[],t,!1,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,h)}_visitTextWithInterpolation(o,e,t,i){let r=sF(o),s=this.bindingParser.parseInterpolation(r,e,t);return s?new jg(s,e,i):new Um(r,e)}parseVariable(o,e,t,i,r,s){o.indexOf("-")>-1?this.reportError('"-" is not allowed in variable names',t):o.length===0&&this.reportError("Variable does not have a name",t),s.push(new wp(o,e,t,i,r))}parseReference(o,e,t,i,r,s){o.indexOf("-")>-1?this.reportError('"-" is not allowed in reference names',t):o.length===0?this.reportError("Reference does not have a name",t):s.some(d=>d.name===o)&&this.reportError(`Reference "#${o}" is defined more than once`,t),s.push(new a0(o,e,t,i,r))}parseAssignmentEvent(o,e,t,i,r,s,d,m){let u=[];this.bindingParser.parseEvent(`${o}Change`,e,!0,t,i||t,r,u,d),Jk(u,s)}validateSelectorlessReferences(o){if(o.length===0)return;let e=new Set;for(let t of o)t.value.length>0?this.reportError("Cannot specify a value for a local reference in this context",t.valueSpan||t.sourceSpan):e.has(t.name)?this.reportError("Duplicate reference names are not allowed",t.sourceSpan):e.add(t.name)}reportError(o,e,t=Cp.ERROR){this.errors.push(new dn(e,o,t))}},bT=class{visitElement(o){let e=EF(o);if(e.type===Ws.SCRIPT||e.type===Ws.STYLE||e.type===Ws.STYLESHEET)return null;let t=Mo(this,o.children,null);return new Bc(o.name,Mo(this,o.attrs),[],[],[],t,[],o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,o.isVoid)}visitComment(o){return null}visitAttribute(o){return new $g(o.name,o.value,o.sourceSpan,o.keySpan,o.valueSpan,o.i18n)}visitText(o){return new Um(o.value,o.sourceSpan)}visitExpansion(o){return null}visitExpansionCase(o){return null}visitBlock(o,e){let t=[new Um(o.startSourceSpan.toString(),o.startSourceSpan),...Mo(this,o.children)];return o.endSourceSpan!==null&&t.push(new Um(o.endSourceSpan.toString(),o.endSourceSpan)),t}visitBlockParameter(o,e){return null}visitLetDeclaration(o,e){return new Um(`@let ${o.name} = ${o.value};`,o.sourceSpan)}visitComponent(o,e){let t=Mo(this,o.children,null);return new Bc(o.fullName,Mo(this,o.attrs),[],[],[],t,[],o.isSelfClosing,o.sourceSpan,o.startSourceSpan,o.endSourceSpan,!1)}visitDirective(o,e){return null}},nN=new bT;function iN(n){return/^data-/i.test(n)?n.substring(5):n}function Jk(n,o){o.push(...n.map(e=>vP.fromParsedEvent(e)))}function _J(n){return n.children.length!==1||!(n.children[0]instanceof uu)?null:n.children[0].value}var fJ=[" ",` +`,"\r"," "];function CJ(n,o,e={}){let{preserveWhitespaces:t,enableI18nLegacyMessageIdFormat:i}=e,r=e.enableSelectorless??!1,s=S1(r),m=new hT().parse(n,o,Le(q({leadingTriviaChars:fJ},e),{tokenizeExpansionForms:!0,tokenizeBlocks:e.enableBlockSyntax??!0,tokenizeLet:e.enableLetSyntax??!0,selectorlessEnabled:r}));if(!e.alwaysAttemptHtmlToR3AstConversion&&m.errors&&m.errors.length>0){let D={preserveWhitespaces:t,errors:m.errors,nodes:[],styleUrls:[],styles:[],ngContentSelectors:[]};return e.collectCommentNodes&&(D.commentNodes=[]),D}let u=m.rootNodes,h=!(e.preserveSignificantWhitespace??!0),v=new v1(!t,i,e.preserveSignificantWhitespace,h),b=v.visitAllWithErrors(u);if(!e.alwaysAttemptHtmlToR3AstConversion&&b.errors&&b.errors.length>0){let D={preserveWhitespaces:t,errors:b.errors,nodes:[],styleUrls:[],styles:[],ngContentSelectors:[]};return e.collectCommentNodes&&(D.commentNodes=[]),D}u=b.rootNodes,t||(u=Mo(new f1(!0,void 0,!1),u),v.hasI18nMeta&&(u=Mo(new v1(!1,void 0,!0,h),u)));let{nodes:y,errors:x,styleUrls:P,styles:k,ngContentSelectors:S,commentNodes:T}=hJ(u,s,{collectCommentNodes:!!e.collectCommentNodes});x.push(...m.errors,...b.errors);let A={preserveWhitespaces:t,errors:x.length>0?x:null,nodes:y,styleUrls:P,styles:k,ngContentSelectors:S};return e.collectCommentNodes&&(A.commentNodes=T),A}var vJ=new Yg;function S1(n=!1){return new mT(new C1(new k0,n),vJ,[])}var DF="%COMP%",xJ=`_nghost-${DF}`,yJ=`_ngcontent-${DF}`;function IF(n,o,e){let t=new kp,i=AT(n.selector);return t.set("type",n.type.value),i.length>0&&t.set("selectors",kg(i)),n.queries.length>0&&t.set("contentQueries",gZ(n.queries,o,n.name)),n.viewQueries.length&&t.set("viewQuery",uZ(n.viewQueries,o,n.name)),t.set("hostBindings",EJ(n.host,n.typeSourceSpan,e,o,n.selector||"",n.name,t)),t.set("inputs",I5(n.inputs,!0)),t.set("outputs",I5(n.outputs)),n.exportAs!==null&&t.set("exportAs",Ji(n.exportAs.map(r=>Oe(r)))),n.isStandalone===!1&&t.set("standalone",Oe(!1)),n.isSignal&&t.set("signals",Oe(!0)),t}function AF(n,o){let e=[],t=o.providers,i=o.viewProviders;if(t||i){let r=[t||new zc([])];i&&r.push(i),e.push(Vt(ge.ProvidersFeature).callFn(r))}if(o.hostDirectives?.length&&e.push(Vt(ge.HostDirectivesFeature).callFn([NJ(o.hostDirectives)])),o.usesInheritance&&e.push(Vt(ge.InheritDefinitionFeature)),o.lifecycle.usesOnChanges&&e.push(Vt(ge.NgOnChangesFeature)),o.controlCreate!==null&&e.push(Vt(ge.ControlFeature).callFn([Oe(o.controlCreate.passThroughInput)])),"externalStyles"in o&&o.externalStyles?.length){let r=o.externalStyles.map(s=>Oe(s));e.push(Vt(ge.ExternalStylesFeature).callFn([Ji(r)]))}e.length&&n.set("features",Ji(e))}function bJ(n,o,e){let t=IF(n,o,e);AF(t,n);let i=Vt(ge.defineDirective).callFn([t.toLiteralMap()],void 0,!0),r=TJ(n);return{expression:i,type:r,statements:[]}}function wJ(n,o,e){let t=IF(n,o,e);AF(t,n);let i=n.selector&&Ag.parse(n.selector),r=i&&i[0];if(r){let x=r.getAttrs();x.length&&t.set("attrs",o.getConstLiteral(Ji(x.map(P=>P!=null?Oe(P):Oe(void 0))),!0))}let s=n.name,d=null;if(n.defer.mode===1&&n.defer.dependenciesFn!==null){let x=`${s}_DeferFn`;o.statements.push(new Ur(x,n.defer.dependenciesFn,void 0,ua.Final)),d=si(x)}let m=n.isStandalone&&!n.hasDirectiveDependencies?hs.DomOnly:hs.Full,u=$Y(n.name,n.template.nodes,o,m,n.relativeContextFilePath,n.i18nUseExternalIds,n.defer,d,n.relativeTemplatePath,dZ());vF(u,Et.Tmpl);let h=NY(u,o);if(u.contentSelectors!==null&&t.set("ngContentSelectors",u.contentSelectors),t.set("decls",Oe(u.root.decls)),t.set("vars",Oe(u.root.vars)),u.consts.length>0&&(u.constsInitializers.length>0?t.set("consts",Xs([],[...u.constsInitializers,new Or(Ji(u.consts))])):t.set("consts",Ji(u.consts))),t.set("template",h),n.declarationListEmitMode!==3&&n.declarations.length>0)t.set("dependencies",SJ(Ji(n.declarations.map(x=>x.type)),n.declarationListEmitMode));else if(n.declarationListEmitMode===3){let x=[n.type.value];n.rawImports&&x.push(n.rawImports),t.set("dependencies",Vt(ge.getComponentDepsFactory).callFn(x))}n.encapsulation===null&&(n.encapsulation=zm.Emulated);let v=!!n.externalStyles?.length;if(n.styles&&n.styles.length){let P=(n.encapsulation==zm.Emulated?AJ(n.styles,yJ,xJ):n.styles).reduce((k,S)=>(S.trim().length>0&&k.push(o.getConstLiteral(Oe(S))),k),[]);P.length>0&&(v=!0,t.set("styles",Ji(P)))}!v&&n.encapsulation===zm.Emulated&&(n.encapsulation=zm.None),n.encapsulation!==zm.Emulated&&t.set("encapsulation",Oe(n.encapsulation)),n.animations!==null&&t.set("data",yl([{key:"animation",value:n.animations,quoted:!1}])),n.changeDetection!==null&&(typeof n.changeDetection=="number"&&n.changeDetection!==IT.Default?t.set("changeDetection",Oe(n.changeDetection)):typeof n.changeDetection=="object"&&t.set("changeDetection",n.changeDetection));let b=Vt(ge.defineComponent).callFn([t.toLiteralMap()],void 0,!0),y=MJ(n);return{expression:b,type:y,statements:[]}}function MJ(n){let o=NF(n);return o.push(MT(n.template.ngContentSelectors)),o.push(ga(Oe(n.isStandalone))),o.push(FF(n)),n.isSignal&&o.push(ga(Oe(n.isSignal))),ga(Vt(ge.ComponentDeclaration,o))}function SJ(n,o){switch(o){case 0:return n;case 1:return Xs([],n);case 2:let e=n.prop("map").callFn([Vt(ge.resolveForwardRef)]);return Xs([],e);case 3:throw new Error("Unsupported with an array of pre-resolved dependencies")}}function kJ(n){return ga(Oe(n))}function wT(n){let o=Object.keys(n).map(e=>{let t=Array.isArray(n[e])?n[e][0]:n[e];return{key:e,value:Oe(t),quoted:!0}});return yl(o)}function MT(n){return n.length>0?ga(Ji(n.map(o=>Oe(o)))):Rc}function NF(n){let o=n.selector!==null?n.selector.replace(/\n/g,""):null;return[E1(n.type.type,n.typeArgumentCount),o!==null?kJ(o):Rc,n.exportAs!==null?MT(n.exportAs):Rc,ga(PJ(n)),ga(wT(n.outputs)),MT(n.queries.map(e=>e.propertyName))]}function PJ(n){return yl(Object.keys(n.inputs).map(o=>{let e=n.inputs[o],t=[{key:"alias",value:Oe(e.bindingPropertyName),quoted:!0},{key:"required",value:Oe(e.required),quoted:!0}];return e.isSignal&&t.push({key:"isSignal",value:Oe(e.isSignal),quoted:!0}),{key:o,value:yl(t),quoted:!0}}))}function TJ(n){let o=NF(n);return o.push(Rc),o.push(ga(Oe(n.isStandalone))),o.push(FF(n)),n.isSignal&&o.push(ga(Oe(n.isSignal))),ga(Vt(ge.DirectiveDeclaration,o))}function EJ(n,o,e,t,i,r,s){let d=e.createBoundHostProperties(n.properties,o),m=e.createDirectiveHostEventAsts(n.listeners,o);n.specialAttributes.styleAttr&&(n.attributes.style=Oe(n.specialAttributes.styleAttr)),n.specialAttributes.classAttr&&(n.attributes.class=Oe(n.specialAttributes.classAttr));let u=HY({componentName:r,componentSelector:i,properties:d,events:m,attributes:n.attributes},e,t);vF(u,Et.Host),s.set("hostAttrs",u.root.attributes);let h=u.root.vars;return h!==null&&h>0&&s.set("hostVars",Oe(h)),FY(u)}function OJ(n){let o={},e={},t={},i={};for(let r of Object.keys(n)){let s=n[r];if(r.startsWith("(")&&r.endsWith(")")){if(typeof s!="string")throw new Error("Event binding must be string");e[r.slice(1,-1)]=s}else if(r.startsWith("[")&&r.endsWith("]")){if(typeof s!="string")throw new Error("Property binding must be string");t[r.slice(1,-1)]=s}else switch(r){case"class":if(typeof s!="string")throw new Error("Class binding must be string");i.classAttr=s;break;case"style":if(typeof s!="string")throw new Error("Style binding must be string");i.styleAttr=s;break;default:typeof s=="string"?o[r]=Oe(s):o[r]=s}}return{attributes:o,listeners:e,properties:t,specialAttributes:i}}function DJ(n,o){let e=S1();return e.createDirectiveHostEventAsts(n.listeners,o),e.createBoundHostProperties(n.properties,o),IJ(n,e,o),e.errors}function IJ(n,o,e){for(let t in n.properties){let i=t.startsWith("attr."),r=i?t.slice(5):t;if(r.toLowerCase().startsWith("on")){let s=i?"attribute":"property",d=`(${r.slice(2)})=...`,m=`Binding to event ${s} '${r}' is disallowed for security reasons, please use ${d}`;i||(m+=` +If '${t}' is a directive input, make sure the directive is imported by the current module.`),o.errors.push(new dn(e,m))}}}function AJ(n,o,e){let t=new NP;return n.map(i=>t.shimCssText(i,o,e))}function FF(n){return n.hostDirectives?.length?ga(Ji(n.hostDirectives.map(o=>yl([{key:"directive",value:D0(o.directive.type),quoted:!1},{key:"inputs",value:wT(o.inputs||{}),quoted:!1},{key:"outputs",value:wT(o.outputs||{}),quoted:!1}])))):Rc}function NJ(n){let o=[],e=!1;for(let t of n){if(!t.inputs&&!t.outputs)o.push(t.directive.type);else{let i=[{key:"directive",value:t.directive.type,quoted:!1}];if(t.inputs){let r=oN(t.inputs);r&&i.push({key:"inputs",value:r,quoted:!1})}if(t.outputs){let r=oN(t.outputs);r&&i.push({key:"outputs",value:r,quoted:!1})}o.push(yl(i))}t.isForwardReference&&(e=!0)}return e?new xp([],[new Or(Ji(o))]):Ji(o)}function oN(n){let o=[];for(let e in n)n.hasOwnProperty(e)&&o.push(Oe(e),Oe(n[e]));return o.length>0?Ji(o):null}var ST=class extends Bg{visit(o){o instanceof Cs?this.visit(o.ast):o.visit(this)}visitElement(o){this.visitAllTemplateNodes(o.attributes),this.visitAllTemplateNodes(o.inputs),this.visitAllTemplateNodes(o.outputs),this.visitAllTemplateNodes(o.directives),this.visitAllTemplateNodes(o.references),this.visitAllTemplateNodes(o.children)}visitTemplate(o){this.visitAllTemplateNodes(o.attributes),this.visitAllTemplateNodes(o.inputs),this.visitAllTemplateNodes(o.outputs),this.visitAllTemplateNodes(o.directives),this.visitAllTemplateNodes(o.templateAttrs),this.visitAllTemplateNodes(o.variables),this.visitAllTemplateNodes(o.references),this.visitAllTemplateNodes(o.children)}visitContent(o){this.visitAllTemplateNodes(o.children)}visitBoundAttribute(o){this.visit(o.value)}visitBoundEvent(o){this.visit(o.handler)}visitBoundText(o){this.visit(o.value)}visitIcu(o){Object.keys(o.vars).forEach(e=>this.visit(o.vars[e])),Object.keys(o.placeholders).forEach(e=>this.visit(o.placeholders[e]))}visitDeferredBlock(o){o.visitAll(this)}visitDeferredTrigger(o){o instanceof Hx?this.visit(o.value):o instanceof e0&&o.options!==null&&this.visit(o.options)}visitDeferredBlockPlaceholder(o){this.visitAllTemplateNodes(o.children)}visitDeferredBlockError(o){this.visitAllTemplateNodes(o.children)}visitDeferredBlockLoading(o){this.visitAllTemplateNodes(o.children)}visitSwitchBlock(o){this.visit(o.expression),this.visitAllTemplateNodes(o.groups)}visitSwitchBlockCase(o){o.expression&&this.visit(o.expression)}visitSwitchBlockCaseGroup(o){this.visitAllTemplateNodes(o.cases),this.visitAllTemplateNodes(o.children)}visitSwitchExhaustiveCheck(o){}visitForLoopBlock(o){o.item.visit(this),this.visitAllTemplateNodes(o.contextVariables),this.visit(o.expression),this.visitAllTemplateNodes(o.children),o.empty?.visit(this)}visitForLoopBlockEmpty(o){this.visitAllTemplateNodes(o.children)}visitIfBlock(o){this.visitAllTemplateNodes(o.branches)}visitIfBlockBranch(o){o.expression&&this.visit(o.expression),o.expressionAlias?.visit(this),this.visitAllTemplateNodes(o.children)}visitLetDeclaration(o){this.visit(o.value)}visitComponent(o){this.visitAllTemplateNodes(o.attributes),this.visitAllTemplateNodes(o.inputs),this.visitAllTemplateNodes(o.outputs),this.visitAllTemplateNodes(o.directives),this.visitAllTemplateNodes(o.references),this.visitAllTemplateNodes(o.children)}visitDirective(o){this.visitAllTemplateNodes(o.attributes),this.visitAllTemplateNodes(o.inputs),this.visitAllTemplateNodes(o.outputs),this.visitAllTemplateNodes(o.references)}visitVariable(o){}visitReference(o){}visitTextAttribute(o){}visitText(o){}visitUnknownBlock(o){}visitAllTemplateNodes(o){for(let e of o)this.visit(e)}};var kT=class{directiveMatcher;constructor(o){this.directiveMatcher=o}bind(o){if(!o.template&&!o.host)throw new Error("Empty bound targets are not supported");let e=new Map,t=[],i=new Set,r=new Map,s=new Map,d=new Map,m=new Map,u=new Map,h=new Map,v=new Set,b=new Set,y=[];if(o.template){let x=k1.apply(o.template);FJ(x,d),PT.apply(o.template,this.directiveMatcher,e,t,i,r,s),P1.applyWithScope(o.template,x,m,u,h,v,b,y)}return o.host&&(e.set(o.host.node,o.host.directives),P1.applyWithScope(o.host.node,k1.apply(o.host.node),m,u,h,v,b,y)),new TT(o,e,t,i,r,s,m,u,h,d,v,b,y)}},k1=class n{parentScope;rootNode;namedEntities=new Map;elementLikeInScope=new Set;childScopes=new Map;isDeferred;constructor(o,e){this.parentScope=o,this.rootNode=e,this.isDeferred=o!==null&&o.isDeferred?!0:e instanceof su}static newRootScope(){return new n(null,null)}static apply(o){let e=n.newRootScope();return e.ingest(o),e}ingest(o){o instanceof qs?(o.variables.forEach(e=>this.visitVariable(e)),o.children.forEach(e=>e.visit(this))):o instanceof qm?(o.expressionAlias!==null&&this.visitVariable(o.expressionAlias),o.children.forEach(e=>e.visit(this))):o instanceof Hg?(this.visitVariable(o.item),o.contextVariables.forEach(e=>this.visitVariable(e)),o.children.forEach(e=>e.visit(this))):o instanceof o0||o instanceof r0||o instanceof su||o instanceof i0||o instanceof t0||o instanceof n0||o instanceof Ug?o.children.forEach(e=>e.visit(this)):o instanceof s0||o.forEach(e=>e.visit(this))}visitElement(o){this.visitElementLike(o)}visitTemplate(o){o.directives.forEach(e=>e.visit(this)),o.references.forEach(e=>this.visitReference(e)),this.ingestScopedNode(o)}visitVariable(o){this.maybeDeclare(o)}visitReference(o){this.maybeDeclare(o)}visitDeferredBlock(o){this.ingestScopedNode(o),o.placeholder?.visit(this),o.loading?.visit(this),o.error?.visit(this)}visitDeferredBlockPlaceholder(o){this.ingestScopedNode(o)}visitDeferredBlockError(o){this.ingestScopedNode(o)}visitDeferredBlockLoading(o){this.ingestScopedNode(o)}visitSwitchBlock(o){o.groups.forEach(e=>e.visit(this))}visitSwitchBlockCase(o){}visitSwitchBlockCaseGroup(o){this.ingestScopedNode(o)}visitSwitchExhaustiveCheck(o){}visitForLoopBlock(o){this.ingestScopedNode(o),o.empty?.visit(this)}visitForLoopBlockEmpty(o){this.ingestScopedNode(o)}visitIfBlock(o){o.branches.forEach(e=>e.visit(this))}visitIfBlockBranch(o){this.ingestScopedNode(o)}visitContent(o){this.ingestScopedNode(o)}visitLetDeclaration(o){this.maybeDeclare(o)}visitComponent(o){this.visitElementLike(o)}visitDirective(o){o.references.forEach(e=>this.visitReference(e))}visitBoundAttribute(o){}visitBoundEvent(o){}visitBoundText(o){}visitText(o){}visitTextAttribute(o){}visitIcu(o){}visitDeferredTrigger(o){}visitUnknownBlock(o){}visitElementLike(o){o.directives.forEach(e=>e.visit(this)),o.references.forEach(e=>this.visitReference(e)),o.children.forEach(e=>e.visit(this)),this.elementLikeInScope.add(o)}maybeDeclare(o){this.namedEntities.has(o.name)||this.namedEntities.set(o.name,o)}lookup(o){return this.namedEntities.has(o)?this.namedEntities.get(o):this.parentScope!==null?this.parentScope.lookup(o):null}getChildScope(o){let e=this.childScopes.get(o);if(e===void 0)throw new Error(`Assertion error: child scope for ${o} not found`);return e}ingestScopedNode(o){let e=new n(this,o);e.ingest(o),this.childScopes.set(o,e)}},PT=class n{directiveMatcher;directives;eagerDirectives;missingDirectives;bindings;references;isInDeferBlock=!1;constructor(o,e,t,i,r,s){this.directiveMatcher=o,this.directives=e,this.eagerDirectives=t,this.missingDirectives=i,this.bindings=r,this.references=s}static apply(o,e,t,i,r,s,d){new n(e,t,i,r,s,d).ingest(o)}ingest(o){o.forEach(e=>e.visit(this))}visitElement(o){this.visitElementOrTemplate(o)}visitTemplate(o){this.visitElementOrTemplate(o)}visitDeferredBlock(o){let e=this.isInDeferBlock;this.isInDeferBlock=!0,o.children.forEach(t=>t.visit(this)),this.isInDeferBlock=e,o.placeholder?.visit(this),o.loading?.visit(this),o.error?.visit(this)}visitDeferredBlockPlaceholder(o){o.children.forEach(e=>e.visit(this))}visitDeferredBlockError(o){o.children.forEach(e=>e.visit(this))}visitDeferredBlockLoading(o){o.children.forEach(e=>e.visit(this))}visitSwitchBlock(o){o.groups.forEach(e=>e.visit(this))}visitSwitchBlockCase(o){}visitSwitchBlockCaseGroup(o){o.children.forEach(e=>e.visit(this))}visitSwitchExhaustiveCheck(o){}visitForLoopBlock(o){o.item.visit(this),o.contextVariables.forEach(e=>e.visit(this)),o.children.forEach(e=>e.visit(this)),o.empty?.visit(this)}visitForLoopBlockEmpty(o){o.children.forEach(e=>e.visit(this))}visitIfBlock(o){o.branches.forEach(e=>e.visit(this))}visitIfBlockBranch(o){o.expressionAlias?.visit(this),o.children.forEach(e=>e.visit(this))}visitContent(o){o.children.forEach(e=>e.visit(this))}visitComponent(o){if(this.directiveMatcher instanceof yx){let e=this.directiveMatcher.match(o.componentName);e.length>0?this.trackSelectorlessMatchesAndDirectives(o,e):this.missingDirectives.add(o.componentName)}o.directives.forEach(e=>e.visit(this)),o.children.forEach(e=>e.visit(this))}visitDirective(o){if(this.directiveMatcher instanceof yx){let e=this.directiveMatcher.match(o.name);e.length>0?this.trackSelectorlessMatchesAndDirectives(o,e):this.missingDirectives.add(o.name)}}visitElementOrTemplate(o){if(this.directiveMatcher instanceof xx){let e=[],t=wU(o);this.directiveMatcher.match(t,(i,r)=>e.push(...r)),this.trackSelectorBasedBindingsAndDirectives(o,e)}else o.references.forEach(e=>{e.value.trim()===""&&this.references.set(e,o)});o.directives.forEach(e=>e.visit(this)),o.children.forEach(e=>e.visit(this))}trackMatchedDirectives(o,e){e.length>0&&(this.directives.set(o,e),this.isInDeferBlock||this.eagerDirectives.push(...e))}trackSelectorlessMatchesAndDirectives(o,e){if(e.length===0)return;this.trackMatchedDirectives(o,e);let t=(i,r,s)=>{i[s].hasBindingPropertyName(r.name)&&this.bindings.set(r,i)};for(let i of e)o.inputs.forEach(r=>t(i,r,"inputs")),o.attributes.forEach(r=>t(i,r,"inputs")),o.outputs.forEach(r=>t(i,r,"outputs"));o.references.forEach(i=>this.references.set(i,{directive:e[0],node:o}))}trackSelectorBasedBindingsAndDirectives(o,e){this.trackMatchedDirectives(o,e),o.references.forEach(i=>{let r=null;if(i.value.trim()==="")r=e.find(s=>s.isComponent)||null;else if(r=e.find(s=>s.exportAs!==null&&s.exportAs.some(d=>d===i.value))||null,r===null)return;r!==null?this.references.set(i,{directive:r,node:o}):this.references.set(i,o)});let t=(i,r)=>{let s=e.find(m=>m[r].hasBindingPropertyName(i.name)),d=s!==void 0?s:o;this.bindings.set(i,d)};o.inputs.forEach(i=>t(i,"inputs")),o.attributes.forEach(i=>t(i,"inputs")),o instanceof qs&&o.templateAttrs.forEach(i=>t(i,"inputs")),o.outputs.forEach(i=>t(i,"outputs"))}visitVariable(o){}visitReference(o){}visitTextAttribute(o){}visitBoundAttribute(o){}visitBoundEvent(o){}visitBoundAttributeOrEvent(o){}visitText(o){}visitBoundText(o){}visitIcu(o){}visitDeferredTrigger(o){}visitUnknownBlock(o){}visitLetDeclaration(o){}},P1=class n extends ST{bindings;symbols;usedPipes;eagerPipes;deferBlocks;nestingLevel;scope;rootNode;level;visitNode=o=>o.visit(this);constructor(o,e,t,i,r,s,d,m,u){super(),this.bindings=o,this.symbols=e,this.usedPipes=t,this.eagerPipes=i,this.deferBlocks=r,this.nestingLevel=s,this.scope=d,this.rootNode=m,this.level=u}static applyWithScope(o,e,t,i,r,s,d,m){let u=o instanceof qs?o:null;new n(t,i,s,d,m,r,e,u,0).ingest(o)}ingest(o){if(o instanceof qs)o.variables.forEach(this.visitNode),o.children.forEach(this.visitNode),this.nestingLevel.set(o,this.level);else if(o instanceof qm)o.expressionAlias!==null&&this.visitNode(o.expressionAlias),o.children.forEach(this.visitNode),this.nestingLevel.set(o,this.level);else if(o instanceof Hg)this.visitNode(o.item),o.contextVariables.forEach(e=>this.visitNode(e)),o.trackBy.visit(this),o.children.forEach(this.visitNode),this.nestingLevel.set(o,this.level);else if(o instanceof su){if(this.scope.rootNode!==o)throw new Error(`Assertion error: resolved incorrect scope for deferred block ${o}`);this.deferBlocks.push([o,this.scope]),o.children.forEach(e=>e.visit(this)),this.nestingLevel.set(o,this.level)}else o instanceof o0||o instanceof r0||o instanceof i0||o instanceof t0||o instanceof n0||o instanceof Ug?(o.children.forEach(e=>e.visit(this)),this.nestingLevel.set(o,this.level)):o instanceof s0?this.nestingLevel.set(o,0):o.forEach(this.visitNode)}visitTemplate(o){o.inputs.forEach(this.visitNode),o.outputs.forEach(this.visitNode),o.directives.forEach(this.visitNode),o.templateAttrs.forEach(this.visitNode),o.references.forEach(this.visitNode),this.ingestScopedNode(o)}visitVariable(o){this.rootNode!==null&&this.symbols.set(o,this.rootNode)}visitReference(o){this.rootNode!==null&&this.symbols.set(o,this.rootNode)}visitDeferredBlock(o){this.ingestScopedNode(o),o.triggers.when?.value.visit(this),o.prefetchTriggers.when?.value.visit(this),o.hydrateTriggers.when?.value.visit(this),o.hydrateTriggers.never?.visit(this),o.placeholder&&this.visitNode(o.placeholder),o.loading&&this.visitNode(o.loading),o.error&&this.visitNode(o.error)}visitDeferredBlockPlaceholder(o){this.ingestScopedNode(o)}visitDeferredBlockError(o){this.ingestScopedNode(o)}visitDeferredBlockLoading(o){this.ingestScopedNode(o)}visitSwitchBlockCase(o){o.expression?.visit(this)}visitSwitchBlockCaseGroup(o){o.cases.forEach(e=>e.visit(this)),this.ingestScopedNode(o)}visitSwitchExhaustiveCheck(o){}visitForLoopBlock(o){o.expression.visit(this),this.ingestScopedNode(o),o.empty?.visit(this)}visitForLoopBlockEmpty(o){this.ingestScopedNode(o)}visitIfBlockBranch(o){o.expression?.visit(this),this.ingestScopedNode(o)}visitContent(o){this.ingestScopedNode(o)}visitLetDeclaration(o){super.visitLetDeclaration(o),this.rootNode!==null&&this.symbols.set(o,this.rootNode)}visitPipe(o,e){return this.usedPipes.add(o.name),this.scope.isDeferred||this.eagerPipes.add(o.name),super.visitPipe(o,e)}visitPropertyRead(o,e){return this.maybeMap(o,o.name),super.visitPropertyRead(o,e)}visitSafePropertyRead(o,e){return this.maybeMap(o,o.name),super.visitSafePropertyRead(o,e)}ingestScopedNode(o){let e=this.scope.getChildScope(o);new n(this.bindings,this.symbols,this.usedPipes,this.eagerPipes,this.deferBlocks,this.nestingLevel,e,o,this.level+1).ingest(o)}maybeMap(o,e){if(!(o.receiver instanceof Vc))return;let t=this.scope.lookup(e);t!==null&&this.bindings.set(o,t)}},TT=class{target;directives;eagerDirectives;missingDirectives;bindings;references;exprTargets;symbols;nestingLevel;scopedNodeEntities;usedPipes;eagerPipes;deferredBlocks;deferredScopes;constructor(o,e,t,i,r,s,d,m,u,h,v,b,y){this.target=o,this.directives=e,this.eagerDirectives=t,this.missingDirectives=i,this.bindings=r,this.references=s,this.exprTargets=d,this.symbols=m,this.nestingLevel=u,this.scopedNodeEntities=h,this.usedPipes=v,this.eagerPipes=b,this.deferredBlocks=y.map(x=>x[0]),this.deferredScopes=new Map(y)}getEntitiesInScope(o){return this.scopedNodeEntities.get(o)??new Set}getDirectivesOfNode(o){return this.directives.get(o)||null}getReferenceTarget(o){return this.references.get(o)||null}getConsumerOfBinding(o){return this.bindings.get(o)||null}getExpressionTarget(o){return this.exprTargets.get(o)||null}getDefinitionNodeOfSymbol(o){return this.symbols.get(o)||null}getNestingLevel(o){return this.nestingLevel.get(o)||0}getUsedDirectives(){let o=new Set;return this.directives.forEach(e=>e.forEach(t=>o.add(t))),Array.from(o.values())}getEagerlyUsedDirectives(){let o=new Set(this.eagerDirectives);return Array.from(o.values())}getUsedPipes(){return Array.from(this.usedPipes)}getEagerlyUsedPipes(){return Array.from(this.eagerPipes)}getDeferBlocks(){return this.deferredBlocks}getDeferredTriggerTarget(o,e){if(!(e instanceof Wx)&&!(e instanceof e0)&&!(e instanceof Ux))return null;let t=e.reference;if(t===null){let r=null;if(o.placeholder!==null){for(let s of o.placeholder.children)if(!(s instanceof O1)){if(r!==null)return null;s instanceof Bc&&(r=s)}}return r}let i=this.findEntityInScope(o,t);if(i instanceof a0&&this.getDefinitionNodeOfSymbol(i)!==o){let r=this.getReferenceTarget(i);if(r!==null)return this.referenceTargetToElement(r)}if(o.placeholder!==null){let r=this.findEntityInScope(o.placeholder,t),s=r instanceof a0?this.getReferenceTarget(r):null;if(s!==null)return this.referenceTargetToElement(s)}return null}isDeferred(o){for(let e of this.deferredBlocks){if(!this.deferredScopes.has(e))continue;let t=[this.deferredScopes.get(e)];for(;t.length>0;){let i=t.pop();if(i.elementLikeInScope.has(o))return!0;t.push(...i.childScopes.values())}}return!1}referencedDirectiveExists(o){return!this.missingDirectives.has(o)}findEntityInScope(o,e){let t=this.getEntitiesInScope(o);for(let i of t)if(i.name===e)return i;return null}referenceTargetToElement(o){return o instanceof Bc?o:o instanceof qs||o.node instanceof kf||o.node instanceof vN||o.node instanceof s0?null:this.referenceTargetToElement(o.node)}};function FJ(n,o){let e=new Map;function t(r){if(e.has(r.rootNode))return e.get(r.rootNode);let s=r.namedEntities,d;return r.parentScope!==null?d=new Map([...t(r.parentScope),...s]):d=new Map(s),e.set(r.rootNode,d),d}let i=[n];for(;i.length>0;){let r=i.pop();for(let s of r.childScopes.values())i.push(s);t(r)}for(let[r,s]of e)o.set(r,new Set(s.values()))}var ET=class{},OT=class{jitEvaluator;FactoryTarget=kd;ResourceLoader=ET;elementSchemaRegistry=new Yg;constructor(o=new DP){this.jitEvaluator=o}compilePipe(o,e,t){let i={name:t.name,type:$r(t.type),typeArgumentCount:0,pipeName:t.pipeName,pure:t.pure,isStandalone:t.isStandalone},r=H5(i);return this.jitExpression(r.expression,o,e,[])}compilePipeDeclaration(o,e,t){let i=ZJ(t),r=H5(i);return this.jitExpression(r.expression,o,e,[])}compileInjectable(o,e,t){let{expression:i,statements:r}=A5({name:t.name,type:$r(t.type),typeArgumentCount:t.typeArgumentCount,providedIn:dN(t.providedIn),useClass:bg(t,"useClass"),useFactory:cN(t,"useFactory"),useValue:bg(t,"useValue"),useExisting:bg(t,"useExisting"),deps:t.deps?.map(VF)},!0);return this.jitExpression(i,o,e,r)}compileInjectableDeclaration(o,e,t){let{expression:i,statements:r}=A5({name:t.type.name,type:$r(t.type),typeArgumentCount:0,providedIn:dN(t.providedIn),useClass:bg(t,"useClass"),useFactory:cN(t,"useFactory"),useValue:bg(t,"useValue"),useExisting:bg(t,"useExisting"),deps:t.deps?.map(pN)},!0);return this.jitExpression(i,o,e,r)}compileInjector(o,e,t){let i={type:$r(t.type),providers:t.providers&&t.providers.length>0?new hi(t.providers):null,imports:t.imports.map(s=>new hi(s))},r=$5(i);return this.jitExpression(r.expression,o,e,[])}compileInjectorDeclaration(o,e,t){let i=JJ(t),r=$5(i);return this.jitExpression(r.expression,o,e,[])}compileNgModule(o,e,t){let i={kind:vp.Global,type:$r(t.type),bootstrap:t.bootstrap.map($r),declarations:t.declarations.map($r),publicDeclarationTypes:null,imports:t.imports.map($r),includeImportTypes:!0,exports:t.exports.map($r),selectorScopeMode:e1.Inline,containsForwardDecls:!1,schemas:t.schemas?t.schemas.map($r):null,id:t.id?new hi(t.id):null},r=HU(i);return this.jitExpression(r.expression,o,e,[])}compileNgModuleDeclaration(o,e,t){let i=UU(t);return this.jitExpression(i,o,e,[])}compileDirective(o,e,t){let i=sN(t);return this.compileDirectiveFromMeta(o,e,i)}compileDirectiveDeclaration(o,e,t){let i=this.createParseSourceSpan("Directive",t.type.name,e),r=LF(t,i);return this.compileDirectiveFromMeta(o,e,r)}compileDirectiveFromMeta(o,e,t){let i=new Dx,r=S1(),s=bJ(t,i,r);return this.jitExpression(s.expression,o,e,i.statements)}compileComponent(o,e,t){let{template:i,defer:r}=zF(t.template,t.name,e,t.preserveWhitespaces,void 0),s=Le(q(q({},t),sN(t)),{selector:t.selector||this.elementSchemaRegistry.getDefaultComponentElementName(),template:i,declarations:t.declarations.map(VJ),declarationListEmitMode:0,defer:r,styles:[...t.styles,...i.styles],encapsulation:t.encapsulation,changeDetection:t.changeDetection??null,animations:t.animations!=null?new hi(t.animations):null,viewProviders:t.viewProviders!=null?new hi(t.viewProviders):null,relativeContextFilePath:"",i18nUseExternalIds:!0,relativeTemplatePath:null}),d=`ng:///${t.name}.js`;return this.compileComponentFromMeta(o,d,s)}compileComponentDeclaration(o,e,t){let i=this.createParseSourceSpan("Component",t.type.name,e),r=zJ(t,i,e);return this.compileComponentFromMeta(o,e,r)}compileComponentFromMeta(o,e,t){let i=new Dx,r=S1(),s=wJ(t,i,r);return this.jitExpression(s.expression,o,e,i.statements)}compileFactory(o,e,t){let i=Vm({name:t.name,type:$r(t.type),typeArgumentCount:t.typeArgumentCount,deps:$J(t.deps),target:t.target});return this.jitExpression(i.expression,o,e,i.statements)}compileFactoryDeclaration(o,e,t){let i=Vm({name:t.type.name,type:$r(t.type),typeArgumentCount:0,deps:Array.isArray(t.deps)?t.deps.map(pN):t.deps,target:t.target});return this.jitExpression(i.expression,o,e,i.statements)}createParseSourceSpan(o,e,t){return RU(o,e,t)}jitExpression(o,e,t,i){let r=[...i,new Ur("$def",o,void 0,ua.Exported)];return this.jitEvaluator.evaluateStatements(t,r,new AP(e),!0).$def}};function rN(n){return Le(q({},n),{isSignal:n.isSignal,predicate:RF(n.predicate),read:n.read?new hi(n.read):null,static:n.static,emitDistinctChangesOnly:n.emitDistinctChangesOnly})}function aN(n){return{propertyName:n.propertyName,first:n.first??!1,predicate:RF(n.predicate),descendants:n.descendants??!1,read:n.read?new hi(n.read):null,static:n.static??!1,emitDistinctChangesOnly:n.emitDistinctChangesOnly??!0,isSignal:!!n.isSignal}}function RF(n){return Array.isArray(n)?n:RT(new hi(n),1)}function sN(n){let o=YJ(n.inputs||[]),e=tP(n.outputs||[]),t=n.propMetadata,i={},r={};for(let d in t)t.hasOwnProperty(d)&&t[d].forEach(m=>{qJ(m)?i[d]={bindingPropertyName:m.alias||d,classPropertyName:d,required:m.required||!1,isSignal:!!m.isSignal,transformFunction:m.transform!=null?new hi(m.transform):null}:QJ(m)&&(r[d]=m.alias||d)});let s=n.hostDirectives?.length?n.hostDirectives.map(d=>typeof d=="function"?{directive:$r(d),inputs:null,outputs:null,isForwardReference:!1}:{directive:$r(d.directive),isForwardReference:!1,inputs:d.inputs?tP(d.inputs):null,outputs:d.outputs?tP(d.outputs):null}):null;return Le(q({},n),{typeArgumentCount:0,typeSourceSpan:n.typeSourceSpan,type:$r(n.type),deps:null,host:q({},UJ(n.propMetadata,n.typeSourceSpan,n.host)),inputs:q(q({},o),i),outputs:q(q({},e),r),queries:n.queries.map(rN),providers:n.providers!=null?new hi(n.providers):null,viewQueries:n.viewQueries.map(rN),hostDirectives:s})}function LF(n,o){let e=n.hostDirectives?.length?n.hostDirectives.map(t=>({directive:$r(t.directive),isForwardReference:!1,inputs:t.inputs?lN(t.inputs):null,outputs:t.outputs?lN(t.outputs):null})):null;return{name:n.type.name,type:$r(n.type),typeSourceSpan:o,selector:n.selector??null,inputs:n.inputs?KJ(n.inputs):{},outputs:n.outputs??{},host:RJ(n.host),queries:(n.queries??[]).map(aN),viewQueries:(n.viewQueries??[]).map(aN),providers:n.providers!==void 0?new hi(n.providers):null,exportAs:n.exportAs??null,usesInheritance:n.usesInheritance??!1,controlCreate:n.controlCreate??null,lifecycle:{usesOnChanges:n.usesOnChanges??!1},deps:null,typeArgumentCount:0,isStandalone:n.isStandalone??CN(n.version),isSignal:n.isSignal??!1,hostDirectives:e}}function RJ(n={}){return{attributes:LJ(n.attributes??{}),listeners:n.listeners??{},properties:n.properties??{},specialAttributes:{classAttr:n.classAttribute,styleAttr:n.styleAttribute}}}function lN(n){let o=null;for(let e=1;eeP(d,!0))),n.directives&&r.push(...n.directives.map(d=>eP(d))),n.pipes&&r.push(...BJ(n.pipes)));let s=r.some(({kind:d})=>d===Gg.Directive||d===Gg.NgModule);return Le(q({},LF(n,o)),{template:t,styles:n.styles??[],declarations:r,viewProviders:n.viewProviders!==void 0?new hi(n.viewProviders):null,animations:n.animations!==void 0?new hi(n.animations):null,defer:i,changeDetection:n.changeDetection??IT.Default,encapsulation:n.encapsulation??zm.Emulated,declarationListEmitMode:2,relativeContextFilePath:"",i18nUseExternalIds:!0,relativeTemplatePath:null,hasDirectiveDependencies:s})}function VJ(n){return Le(q({},n),{type:new hi(n.type)})}function eP(n,o=null){return{kind:Gg.Directive,isComponent:o||n.kind==="component",selector:n.selector,type:new hi(n.type),inputs:n.inputs??[],outputs:n.outputs??[],exportAs:n.exportAs??null}}function BJ(n){return n?Object.keys(n).map(o=>({kind:Gg.Pipe,name:o,type:new hi(n[o])})):[]}function jJ(n){return{kind:Gg.Pipe,name:n.name,type:new hi(n.type)}}function zF(n,o,e,t,i){let r=CJ(n,e,{preserveWhitespaces:t});if(r.errors!==null){let m=r.errors.map(u=>u.toString()).join(", ");throw new Error(`Errors during JIT compilation of template for ${o}: ${m}`)}let d=new kT(null).bind({template:r.nodes});return{template:r,defer:HJ(d,i)}}function bg(n,o){if(n.hasOwnProperty(o))return RT(new hi(n[o]),0)}function cN(n,o){if(n.hasOwnProperty(o))return new hi(n[o])}function dN(n){let o=typeof n=="function"?new hi(n):new _a(n??null);return RT(o,0)}function $J(n){return n==null?null:n.map(VF)}function VF(n){let o=n.attribute!=null,e=n.token===null?null:new hi(n.token),t=o?new hi(n.attribute):e;return BF(t,o,n.host,n.optional,n.self,n.skipSelf)}function pN(n){let o=n.attribute??!1,e=n.token===null?null:new hi(n.token);return BF(e,o,n.host??!1,n.optional??!1,n.self??!1,n.skipSelf??!1)}function BF(n,o,e,t,i,r){let s=o?Oe("unknown"):null;return{token:n,attributeNameType:s,host:e,optional:t,self:i,skipSelf:r}}function HJ(n,o){let e=n.getDeferBlocks(),t=new Map;for(let i=0;i{WJ(s)?t.properties[s.hostPropertyName||r]=rU("this",r):GJ(s)&&(t.listeners[s.eventName||r]=`${r}(${(s.args||[]).join(",")})`)});let i=DJ(t,o);if(i.length)throw new Error(i.map(r=>r.msg).join(` +`));return t}function WJ(n){return n.ngMetadataName==="HostBinding"}function GJ(n){return n.ngMetadataName==="HostListener"}function qJ(n){return n.ngMetadataName==="Input"}function QJ(n){return n.ngMetadataName==="Output"}function KJ(n){return Object.keys(n).reduce((o,e)=>{let t=n[e];return typeof t=="string"||Array.isArray(t)?o[e]=XJ(t):o[e]={bindingPropertyName:t.publicName,classPropertyName:e,transformFunction:t.transformFunction!==null?new hi(t.transformFunction):null,required:t.isRequired,isSignal:t.isSignal},o},{})}function XJ(n){return typeof n=="string"?{bindingPropertyName:n,classPropertyName:n,transformFunction:null,required:!1,isSignal:!1}:{bindingPropertyName:n[0],classPropertyName:n[1],transformFunction:n[2]?new hi(n[2]):null,required:!1,isSignal:!1}}function YJ(n){return n.reduce((o,e)=>{if(typeof e=="string"){let[t,i]=jF(e);o[i]={bindingPropertyName:t,classPropertyName:i,required:!1,isSignal:!1,transformFunction:null}}else o[e.name]={bindingPropertyName:e.alias||e.name,classPropertyName:e.name,required:e.required||!1,isSignal:!1,transformFunction:e.transform!=null?new hi(e.transform):null};return o},{})}function tP(n){return n.reduce((o,e)=>{let[t,i]=jF(e);return o[i]=t,o},{})}function jF(n){let[o,e]=n.split(":",2).map(t=>t.trim());return[e??o,o]}function ZJ(n){return{name:n.type.name,type:$r(n.type),typeArgumentCount:0,pipeName:n.name,deps:null,pure:n.pure??!0,isStandalone:n.isStandalone??CN(n.version)}}function JJ(n){return{name:n.type.name,type:$r(n.type),providers:n.providers!==void 0&&n.providers.length>0?new hi(n.providers):null,imports:n.imports!==void 0?n.imports.map(o=>new hi(o)):[]}}function eee(n){let o=n.ng||(n.ng={});o.\u0275compilerFacade=new OT}var DT=class{closedByParent=!1;implicitNamespacePrefix=null;isVoid=!1;ignoreFirstLf=!1;canSelfClose=!0;preventNamespaceInheritance=!1;requireExtraParent(o){return!1}isClosedByChild(o){return!1}getContentType(){return Oc.PARSABLE_DATA}},Fje=new DT;var Rje=new lP("21.2.21");eee(Sf);function iE(n){let o=n.cloneNode(!0),e=o.querySelectorAll("[id]"),t=n.nodeName.toLowerCase();o.removeAttribute("id");for(let i=0;i=t&&e<=i&&o>=r&&o<=s}function nee(n,o){let e=o.leftn.right,i=o.topn.bottom;return e||t||i||r}function z0(n,o,e){n.top+=o,n.bottom=n.top+n.height,n.left+=e,n.right=n.left+n.width}function WF(n,o,e,t){let{top:i,right:r,bottom:s,left:d,width:m,height:u}=n,h=m*o,v=u*o;return t>i-v&&td-h&&e{this.positions.set(e,{scrollPosition:{top:e.scrollTop,left:e.scrollLeft},clientRect:dE(e)})})}handleScroll(o){let e=Im(o),t=this.positions.get(e);if(!t)return null;let i=t.scrollPosition,r,s;if(e===this._document){let u=this.getViewportScrollPosition();r=u.top,s=u.left}else r=e.scrollTop,s=e.scrollLeft;let d=i.top-r,m=i.left-s;return this.positions.forEach((u,h)=>{u.clientRect&&e!==h&&e.contains(h)&&z0(u.clientRect,d,m)}),i.top=r,i.left=s,{top:d,left:m}}getViewportScrollPosition(){return{top:window.scrollY,left:window.scrollX}}};function n8(n,o){let e=n.rootNodes;if(e.length===1&&e[0].nodeType===o.ELEMENT_NODE)return e[0];let t=o.createElement("div");return e.forEach(i=>t.appendChild(i)),t}function pE(n,o,e){for(let t in o)if(o.hasOwnProperty(t)){let i=o[t];i?n.setProperty(t,i,e?.has(t)?"important":""):n.removeProperty(t)}return n}function oh(n,o){let e=o?"":"none";pE(n.style,{"touch-action":o?"":"none","-webkit-user-drag":o?"":"none","-webkit-tap-highlight-color":o?"":"transparent","user-select":e,"-ms-user-select":e,"-webkit-user-select":e,"-moz-user-select":e})}function GF(n,o,e){pE(n.style,{position:o?"":"fixed",top:o?"":"0",opacity:o?"":"0",left:o?"":"-999em"},e)}function V1(n,o){return o&&o!="none"?n+" "+o:n}function qF(n,o){n.style.width=`${o.width}px`,n.style.height=`${o.height}px`,n.style.transform=V0(o.left,o.top)}function V0(n,o){return`translate3d(${Math.round(n)}px, ${Math.round(o)}px, 0)`}var R0={capture:!0},tE={passive:!1,capture:!0},iee=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["ng-component"]],hostAttrs:["cdk-drag-resets-container",""],decls:0,vars:0,template:function(t,i){},styles:[`@layer cdk-resets { + .cdk-drag-preview { + background: none; + border: none; + padding: 0; + color: inherit; + inset: auto; + } +} +.cdk-drag-placeholder *, +.cdk-drag-preview * { + pointer-events: none !important; +} +`],encapsulation:2,changeDetection:0})}return n})(),j1=(()=>{class n{_ngZone=f(Ki);_document=f(Eo);_styleLoader=f(kr);_renderer=f(Dm).createRenderer(null,null);_cleanupDocumentTouchmove;_scroll=new Ye;_dropInstances=new Set;_dragInstances=new Set;_activeDragInstances=R([]);_globalListeners;_draggingPredicate=e=>e.isDragging();_domNodesToDirectives=null;pointerMove=new Ye;pointerUp=new Ye;constructor(){}registerDropContainer(e){this._dropInstances.has(e)||this._dropInstances.add(e)}registerDragItem(e){this._dragInstances.add(e),this._dragInstances.size===1&&this._ngZone.runOutsideAngular(()=>{this._cleanupDocumentTouchmove?.(),this._cleanupDocumentTouchmove=this._renderer.listen(this._document,"touchmove",this._persistentTouchmoveListener,tE)})}removeDropContainer(e){this._dropInstances.delete(e)}removeDragItem(e){this._dragInstances.delete(e),this.stopDragging(e),this._dragInstances.size===0&&this._cleanupDocumentTouchmove?.()}startDragging(e,t){if(!(this._activeDragInstances().indexOf(e)>-1)&&(this._styleLoader.load(iee),this._activeDragInstances.update(i=>[...i,e]),this._activeDragInstances().length===1)){let i=t.type.startsWith("touch"),r=d=>this.pointerUp.next(d),s=[["scroll",d=>this._scroll.next(d),R0],["selectstart",this._preventDefaultWhileDragging,tE]];i?s.push(["touchend",r,R0],["touchcancel",r,R0]):s.push(["mouseup",r,R0]),i||s.push(["mousemove",d=>this.pointerMove.next(d),tE]),this._ngZone.runOutsideAngular(()=>{this._globalListeners=s.map(([d,m,u])=>this._renderer.listen(this._document,d,m,u))})}}stopDragging(e){this._activeDragInstances.update(t=>{let i=t.indexOf(e);return i>-1?(t.splice(i,1),[...t]):t}),this._activeDragInstances().length===0&&this._clearGlobalListeners()}isDragging(e){return this._activeDragInstances().indexOf(e)>-1}scrolled(e){let t=[this._scroll];return e&&e!==this._document&&t.push(new ls(i=>this._ngZone.runOutsideAngular(()=>{let r=this._renderer.listen(e,"scroll",s=>{this._activeDragInstances().length&&i.next(s)},R0);return()=>{r()}}))),io(...t)}registerDirectiveNode(e,t){this._domNodesToDirectives??=new WeakMap,this._domNodesToDirectives.set(e,t)}removeDirectiveNode(e){this._domNodesToDirectives?.delete(e)}getDragDirectiveForNode(e){return this._domNodesToDirectives?.get(e)||null}ngOnDestroy(){this._dragInstances.forEach(e=>this.removeDragItem(e)),this._dropInstances.forEach(e=>this.removeDropContainer(e)),this._domNodesToDirectives=null,this._clearGlobalListeners(),this.pointerMove.complete(),this.pointerUp.complete()}_preventDefaultWhileDragging=e=>{this._activeDragInstances().length>0&&e.preventDefault()};_persistentTouchmoveListener=e=>{this._activeDragInstances().length>0&&(this._activeDragInstances().some(this._draggingPredicate)&&e.preventDefault(),this.pointerMove.next(e))};_clearGlobalListeners(){this._globalListeners?.forEach(e=>e()),this._globalListeners=void 0}static \u0275fac=function(t){return new(t||n)};static \u0275prov=J({token:n,factory:n.\u0275fac,providedIn:"root"})}return n})();function QF(n){let o=n.toLowerCase().indexOf("ms")>-1?1:1e3;return parseFloat(n)*o}function oee(n){let o=getComputedStyle(n),e=nE(o,"transition-property"),t=e.find(d=>d==="transform"||d==="all");if(!t)return 0;let i=e.indexOf(t),r=nE(o,"transition-duration"),s=nE(o,"transition-delay");return QF(r[i])+QF(s[i])}function nE(n,o){return n.getPropertyValue(o).split(",").map(t=>t.trim())}var ree=new Set(["position"]),rE=class{_document;_rootElement;_direction;_initialDomRect;_previewTemplate;_previewClass;_pickupPositionOnPage;_initialTransform;_zIndex;_renderer;_previewEmbeddedView=null;_preview;get element(){return this._preview}constructor(o,e,t,i,r,s,d,m,u,h){this._document=o,this._rootElement=e,this._direction=t,this._initialDomRect=i,this._previewTemplate=r,this._previewClass=s,this._pickupPositionOnPage=d,this._initialTransform=m,this._zIndex=u,this._renderer=h}attach(o){this._preview=this._createPreview(),o.appendChild(this._preview),KF(this._preview)&&this._preview.showPopover()}destroy(){this._preview.remove(),this._previewEmbeddedView?.destroy(),this._preview=this._previewEmbeddedView=null}setTransform(o){this._preview.style.transform=o}getBoundingClientRect(){return this._preview.getBoundingClientRect()}addClass(o){this._preview.classList.add(o)}getTransitionDuration(){return oee(this._preview)}addEventListener(o,e){return this._renderer.listen(this._preview,o,e)}_createPreview(){let o=this._previewTemplate,e=this._previewClass,t=o?o.template:null,i;if(t&&o){let r=o.matchSize?this._initialDomRect:null,s=o.viewContainer.createEmbeddedView(t,o.context);s.detectChanges(),i=n8(s,this._document),this._previewEmbeddedView=s,o.matchSize?qF(i,r):i.style.transform=V0(this._pickupPositionOnPage.x,this._pickupPositionOnPage.y)}else i=iE(this._rootElement),qF(i,this._initialDomRect),this._initialTransform&&(i.style.transform=this._initialTransform);return pE(i.style,{"pointer-events":"none",margin:KF(i)?"0 auto 0 0":"0",position:"fixed",top:"0",left:"0","z-index":this._zIndex+""},ree),oh(i,!1),i.classList.add("cdk-drag-preview"),i.setAttribute("popover","manual"),i.setAttribute("dir",this._direction),e&&(Array.isArray(e)?e.forEach(r=>i.classList.add(r)):i.classList.add(e)),i}};function KF(n){return"showPopover"in n}var aee={passive:!0},XF={passive:!1},see={passive:!1,capture:!0},lee=800,YF="cdk-drag-placeholder",ZF=new Set(["position"]);function i8(n,o,e={dragStartThreshold:5,pointerDirectionChangeThreshold:5}){let t=n.get(Ui,null,{optional:!0})||n.get(Dm).createRenderer(null,null);return new aE(o,e,n.get(Eo),n.get(Ki),n.get(wd),n.get(j1),t)}var aE=class{_config;_document;_ngZone;_viewportRuler;_dragDropRegistry;_renderer;_rootElementCleanups;_cleanupShadowRootSelectStart;_preview=null;_previewContainer;_placeholderRef=null;_placeholder;_pickupPositionInElement;_pickupPositionOnPage;_marker;_anchor=null;_passiveTransform={x:0,y:0};_activeTransform={x:0,y:0};_initialTransform;_hasStartedDragging=R(!1);_hasMoved=!1;_initialContainer;_initialIndex;_parentPositions;_moveEvents=new Ye;_pointerDirectionDelta;_pointerPositionAtLastDirectionChange;_lastKnownPointerPosition;_rootElement;_ownerSVGElement=null;_rootElementTapHighlight;_pointerMoveSubscription=To.EMPTY;_pointerUpSubscription=To.EMPTY;_scrollSubscription=To.EMPTY;_resizeSubscription=To.EMPTY;_lastTouchEventTime;_dragStartTime;_boundaryElement=null;_nativeInteractionsEnabled=!0;_initialDomRect;_previewRect;_boundaryRect;_previewTemplate;_placeholderTemplate;_handles=[];_disabledHandles=new Set;_dropContainer;_direction="ltr";_parentDragRef=null;_cachedShadowRoot;lockAxis=null;dragStartDelay=0;previewClass;scale=1;get disabled(){return this._disabled||!!(this._dropContainer&&this._dropContainer.disabled)}set disabled(o){o!==this._disabled&&(this._disabled=o,this._toggleNativeDragInteractions(),this._handles.forEach(e=>oh(e,o)))}_disabled=!1;beforeStarted=new Ye;started=new Ye;released=new Ye;ended=new Ye;entered=new Ye;exited=new Ye;dropped=new Ye;moved=this._moveEvents;data;constrainPosition;constructor(o,e,t,i,r,s,d){this._config=e,this._document=t,this._ngZone=i,this._viewportRuler=r,this._dragDropRegistry=s,this._renderer=d,this.withRootElement(o).withParent(e.parentDragRef||null),this._parentPositions=new z1(t),s.registerDragItem(this)}getPlaceholderElement(){return this._placeholder}getRootElement(){return this._rootElement}getVisibleElement(){return this.isDragging()?this.getPlaceholderElement():this.getRootElement()}withHandles(o){this._handles=o.map(t=>sl(t)),this._handles.forEach(t=>oh(t,this.disabled)),this._toggleNativeDragInteractions();let e=new Set;return this._disabledHandles.forEach(t=>{this._handles.indexOf(t)>-1&&e.add(t)}),this._disabledHandles=e,this}withPreviewTemplate(o){return this._previewTemplate=o,this}withPlaceholderTemplate(o){return this._placeholderTemplate=o,this}withRootElement(o){let e=sl(o);if(e!==this._rootElement){this._removeRootElementListeners();let t=this._renderer;this._rootElementCleanups=this._ngZone.runOutsideAngular(()=>[t.listen(e,"mousedown",this._pointerDown,XF),t.listen(e,"touchstart",this._pointerDown,aee),t.listen(e,"dragstart",this._nativeDragStart,XF)]),this._initialTransform=void 0,this._rootElement=e}return typeof SVGElement<"u"&&this._rootElement instanceof SVGElement&&(this._ownerSVGElement=this._rootElement.ownerSVGElement),this}withBoundaryElement(o){return this._boundaryElement=o?sl(o):null,this._resizeSubscription.unsubscribe(),o&&(this._resizeSubscription=this._viewportRuler.change(10).subscribe(()=>this._containInsideBoundaryOnResize())),this}withParent(o){return this._parentDragRef=o,this}dispose(){this._removeRootElementListeners(),this.isDragging()&&this._rootElement?.remove(),this._marker?.remove(),this._destroyPreview(),this._destroyPlaceholder(),this._dragDropRegistry.removeDragItem(this),this._removeListeners(),this.beforeStarted.complete(),this.started.complete(),this.released.complete(),this.ended.complete(),this.entered.complete(),this.exited.complete(),this.dropped.complete(),this._moveEvents.complete(),this._handles=[],this._disabledHandles.clear(),this._dropContainer=void 0,this._resizeSubscription.unsubscribe(),this._parentPositions.clear(),this._boundaryElement=this._rootElement=this._ownerSVGElement=this._placeholderTemplate=this._previewTemplate=this._marker=this._parentDragRef=null}isDragging(){return this._hasStartedDragging()&&this._dragDropRegistry.isDragging(this)}reset(){this._rootElement.style.transform=this._initialTransform||"",this._activeTransform={x:0,y:0},this._passiveTransform={x:0,y:0}}resetToBoundary(){if(this._boundaryElement&&this._rootElement&&nee(this._boundaryElement.getBoundingClientRect(),this._rootElement.getBoundingClientRect())){let o=this._boundaryElement.getBoundingClientRect(),e=this._rootElement.getBoundingClientRect(),t=0,i=0;e.lefto.right&&(t=o.right-e.right),e.topo.bottom&&(i=o.bottom-e.bottom);let r=this._activeTransform.x,s=this._activeTransform.y,d=r+t,m=s+i;this._rootElement.style.transform=V0(d,m),this._activeTransform={x:d,y:m},this._passiveTransform={x:d,y:m}}}disableHandle(o){!this._disabledHandles.has(o)&&this._handles.indexOf(o)>-1&&(this._disabledHandles.add(o),oh(o,!0))}enableHandle(o){this._disabledHandles.has(o)&&(this._disabledHandles.delete(o),oh(o,this.disabled))}withDirection(o){return this._direction=o,this}_withDropContainer(o){this._dropContainer=o}getFreeDragPosition(){let o=this.isDragging()?this._activeTransform:this._passiveTransform;return{x:o.x,y:o.y}}setFreeDragPosition(o){return this._activeTransform={x:0,y:0},this._passiveTransform.x=o.x,this._passiveTransform.y=o.y,this._dropContainer||this._applyRootElementTransform(o.x,o.y),this}withPreviewContainer(o){return this._previewContainer=o,this}_sortFromLastPointerPosition(){let o=this._lastKnownPointerPosition;o&&this._dropContainer&&this._updateActiveDropContainer(this._getConstrainedPointerPosition(o),o)}_removeListeners(){this._pointerMoveSubscription.unsubscribe(),this._pointerUpSubscription.unsubscribe(),this._scrollSubscription.unsubscribe(),this._cleanupShadowRootSelectStart?.(),this._cleanupShadowRootSelectStart=void 0}_destroyPreview(){this._preview?.destroy(),this._preview=null}_destroyPlaceholder(){this._anchor?.remove(),this._placeholder?.remove(),this._placeholderRef?.destroy(),this._placeholder=this._anchor=this._placeholderRef=null}_pointerDown=o=>{if(this.beforeStarted.next(),this._handles.length){let e=this._getTargetHandle(o);e&&!this._disabledHandles.has(e)&&!this.disabled&&this._initializeDragSequence(e,o)}else this.disabled||this._initializeDragSequence(this._rootElement,o)};_pointerMove=o=>{let e=this._getPointerPositionOnPage(o);if(!this._hasStartedDragging()){let i=Math.abs(e.x-this._pickupPositionOnPage.x),r=Math.abs(e.y-this._pickupPositionOnPage.y);if(i+r>=this._config.dragStartThreshold){let d=Date.now()>=this._dragStartTime+this._getDragStartDelay(o),m=this._dropContainer;if(!d){this._endDragSequence(o);return}(!m||!m.isDragging()&&!m.isReceiving())&&(o.cancelable&&o.preventDefault(),this._hasStartedDragging.set(!0),this._ngZone.run(()=>this._startDragSequence(o)))}return}o.cancelable&&o.preventDefault();let t=this._getConstrainedPointerPosition(e);if(this._hasMoved=!0,this._lastKnownPointerPosition=e,this._updatePointerDirectionDelta(t),this._dropContainer)this._updateActiveDropContainer(t,e);else{let i=this.constrainPosition?this._initialDomRect:this._pickupPositionOnPage,r=this._activeTransform;r.x=t.x-i.x+this._passiveTransform.x,r.y=t.y-i.y+this._passiveTransform.y,this._applyRootElementTransform(r.x,r.y)}this._moveEvents.observers.length&&this._ngZone.run(()=>{this._moveEvents.next({source:this,pointerPosition:t,event:o,distance:this._getDragDistance(t),delta:this._pointerDirectionDelta})})};_pointerUp=o=>{this._endDragSequence(o)};_endDragSequence(o){if(this._dragDropRegistry.isDragging(this)&&(this._removeListeners(),this._dragDropRegistry.stopDragging(this),this._toggleNativeDragInteractions(),this._handles&&(this._rootElement.style.webkitTapHighlightColor=this._rootElementTapHighlight),!!this._hasStartedDragging()))if(this.released.next({source:this,event:o}),this._dropContainer)this._dropContainer._stopScrolling(),this._animatePreviewToPlaceholder().then(()=>{this._cleanupDragArtifacts(o),this._cleanupCachedDimensions(),this._dragDropRegistry.stopDragging(this)});else{this._passiveTransform.x=this._activeTransform.x;let e=this._getPointerPositionOnPage(o);this._passiveTransform.y=this._activeTransform.y,this._ngZone.run(()=>{this.ended.next({source:this,distance:this._getDragDistance(e),dropPoint:e,event:o})}),this._cleanupCachedDimensions(),this._dragDropRegistry.stopDragging(this)}}_startDragSequence(o){L0(o)&&(this._lastTouchEventTime=Date.now()),this._toggleNativeDragInteractions();let e=this._getShadowRoot(),t=this._dropContainer;if(e&&this._ngZone.runOutsideAngular(()=>{this._cleanupShadowRootSelectStart=this._renderer.listen(e,"selectstart",cee,see)}),t){let i=this._rootElement,r=i.parentNode,s=this._placeholder=this._createPlaceholderElement(),d=this._marker=this._marker||this._document.createComment("");r.insertBefore(d,i),this._initialTransform=i.style.transform||"",this._preview=new rE(this._document,this._rootElement,this._direction,this._initialDomRect,this._previewTemplate||null,this.previewClass||null,this._pickupPositionOnPage,this._initialTransform,this._config.zIndex||1e3,this._renderer),this._preview.attach(this._getPreviewInsertionPoint(r,e)),GF(i,!1,ZF),this._document.body.appendChild(r.replaceChild(s,i)),this.started.next({source:this,event:o}),t.start(),this._initialContainer=t,this._initialIndex=t.getItemIndex(this)}else this.started.next({source:this,event:o}),this._initialContainer=this._initialIndex=void 0;this._parentPositions.cache(t?t.getScrollableParents():[])}_initializeDragSequence(o,e){this._parentDragRef&&e.stopPropagation();let t=this.isDragging(),i=L0(e),r=!i&&e.button!==0,s=this._rootElement,d=Im(e),m=!i&&this._lastTouchEventTime&&this._lastTouchEventTime+lee>Date.now(),u=i?_A(e):hA(e);if(d&&d.draggable&&e.type==="mousedown"&&e.preventDefault(),t||r||m||u)return;if(this._handles.length){let b=s.style;this._rootElementTapHighlight=b.webkitTapHighlightColor||"",b.webkitTapHighlightColor="transparent"}this._hasMoved=!1,this._hasStartedDragging.set(this._hasMoved),this._removeListeners(),this._initialDomRect=this._rootElement.getBoundingClientRect(),this._pointerMoveSubscription=this._dragDropRegistry.pointerMove.subscribe(this._pointerMove),this._pointerUpSubscription=this._dragDropRegistry.pointerUp.subscribe(this._pointerUp),this._scrollSubscription=this._dragDropRegistry.scrolled(this._getShadowRoot()).subscribe(b=>this._updateOnScroll(b)),this._boundaryElement&&(this._boundaryRect=dE(this._boundaryElement));let h=this._previewTemplate;this._pickupPositionInElement=h&&h.template&&!h.matchSize?{x:0,y:0}:this._getPointerPositionInElement(this._initialDomRect,o,e);let v=this._pickupPositionOnPage=this._lastKnownPointerPosition=this._getPointerPositionOnPage(e);this._pointerDirectionDelta={x:0,y:0},this._pointerPositionAtLastDirectionChange={x:v.x,y:v.y},this._dragStartTime=Date.now(),this._dragDropRegistry.startDragging(this,e)}_cleanupDragArtifacts(o){GF(this._rootElement,!0,ZF),this._marker.parentNode.replaceChild(this._rootElement,this._marker),this._destroyPreview(),this._destroyPlaceholder(),this._initialDomRect=this._boundaryRect=this._previewRect=this._initialTransform=void 0,this._ngZone.run(()=>{let e=this._dropContainer,t=e.getItemIndex(this),i=this._getPointerPositionOnPage(o),r=this._getDragDistance(i),s=e._isOverContainer(i.x,i.y);this.ended.next({source:this,distance:r,dropPoint:i,event:o}),this.dropped.next({item:this,currentIndex:t,previousIndex:this._initialIndex,container:e,previousContainer:this._initialContainer,isPointerOverContainer:s,distance:r,dropPoint:i,event:o}),e.drop(this,t,this._initialIndex,this._initialContainer,s,r,i,o),this._dropContainer=this._initialContainer})}_updateActiveDropContainer({x:o,y:e},{x:t,y:i}){let r=this._initialContainer._getSiblingContainerFromPosition(this,o,e);!r&&this._dropContainer!==this._initialContainer&&this._initialContainer._isOverContainer(o,e)&&(r=this._initialContainer),r&&r!==this._dropContainer&&this._ngZone.run(()=>{let s=this._dropContainer.getItemIndex(this),d=this._dropContainer.getItemAtIndex(s+1)?.getVisibleElement()||null;this.exited.next({item:this,container:this._dropContainer}),this._dropContainer.exit(this),this._conditionallyInsertAnchor(r,this._dropContainer,d),this._dropContainer=r,this._dropContainer.enter(this,o,e,r===this._initialContainer&&r.sortingDisabled?this._initialIndex:void 0),this.entered.next({item:this,container:r,currentIndex:r.getItemIndex(this)})}),this.isDragging()&&(this._dropContainer._startScrollingIfNecessary(t,i),this._dropContainer._sortItem(this,o,e,this._pointerDirectionDelta),this.constrainPosition?this._applyPreviewTransform(o,e):this._applyPreviewTransform(o-this._pickupPositionInElement.x,e-this._pickupPositionInElement.y))}_animatePreviewToPlaceholder(){if(!this._hasMoved)return Promise.resolve();let o=this._placeholder.getBoundingClientRect();this._preview.addClass("cdk-drag-animating"),this._applyPreviewTransform(o.left,o.top);let e=this._preview.getTransitionDuration();return e===0?Promise.resolve():this._ngZone.runOutsideAngular(()=>new Promise(t=>{let i=d=>{(!d||this._preview&&Im(d)===this._preview.element&&d.propertyName==="transform")&&(s(),t(),clearTimeout(r))},r=setTimeout(i,e*1.5),s=this._preview.addEventListener("transitionend",i)}))}_createPlaceholderElement(){let o=this._placeholderTemplate,e=o?o.template:null,t;return e?(this._placeholderRef=o.viewContainer.createEmbeddedView(e,o.context),this._placeholderRef.detectChanges(),t=n8(this._placeholderRef,this._document)):t=iE(this._rootElement),t.style.pointerEvents="none",t.classList.add(YF),t}_getPointerPositionInElement(o,e,t){let i=e===this._rootElement?null:e,r=i?i.getBoundingClientRect():o,s=L0(t)?t.targetTouches[0]:t,d=this._getViewportScrollPosition(),m=s.pageX-r.left-d.left,u=s.pageY-r.top-d.top;return{x:r.left-o.left+m,y:r.top-o.top+u}}_getPointerPositionOnPage(o){let e=this._getViewportScrollPosition(),t=L0(o)?o.touches[0]||o.changedTouches[0]||{pageX:0,pageY:0}:o,i=t.pageX-e.left,r=t.pageY-e.top;if(this._ownerSVGElement){let s=this._ownerSVGElement.getScreenCTM();if(s){let d=this._ownerSVGElement.createSVGPoint();return d.x=i,d.y=r,d.matrixTransform(s.inverse())}}return{x:i,y:r}}_getConstrainedPointerPosition(o){let e=this._dropContainer?this._dropContainer.lockAxis:null,{x:t,y:i}=this.constrainPosition?this.constrainPosition(o,this,this._initialDomRect,this._pickupPositionInElement):o;if(this.lockAxis==="x"||e==="x"?i=this._pickupPositionOnPage.y-(this.constrainPosition?this._pickupPositionInElement.y:0):(this.lockAxis==="y"||e==="y")&&(t=this._pickupPositionOnPage.x-(this.constrainPosition?this._pickupPositionInElement.x:0)),this._boundaryRect){let{x:r,y:s}=this.constrainPosition?{x:0,y:0}:this._pickupPositionInElement,d=this._boundaryRect,{width:m,height:u}=this._getPreviewRect(),h=d.top+s,v=d.bottom-(u-s),b=d.left+r,y=d.right-(m-r);t=JF(t,b,y),i=JF(i,h,v)}return{x:t,y:i}}_updatePointerDirectionDelta(o){let{x:e,y:t}=o,i=this._pointerDirectionDelta,r=this._pointerPositionAtLastDirectionChange,s=Math.abs(e-r.x),d=Math.abs(t-r.y);return s>this._config.pointerDirectionChangeThreshold&&(i.x=e>r.x?1:-1,r.x=e),d>this._config.pointerDirectionChangeThreshold&&(i.y=t>r.y?1:-1,r.y=t),i}_toggleNativeDragInteractions(){if(!this._rootElement||!this._handles)return;let o=this._handles.length>0||!this.isDragging();o!==this._nativeInteractionsEnabled&&(this._nativeInteractionsEnabled=o,oh(this._rootElement,o))}_removeRootElementListeners(){this._rootElementCleanups?.forEach(o=>o()),this._rootElementCleanups=void 0}_applyRootElementTransform(o,e){let t=1/this.scale,i=V0(o*t,e*t),r=this._rootElement.style;this._initialTransform==null&&(this._initialTransform=r.transform&&r.transform!="none"?r.transform:""),r.transform=V1(i,this._initialTransform)}_applyPreviewTransform(o,e){let t=this._previewTemplate?.template?void 0:this._initialTransform,i=V0(o,e);this._preview.setTransform(V1(i,t))}_getDragDistance(o){let e=this._pickupPositionOnPage;return e?{x:o.x-e.x,y:o.y-e.y}:{x:0,y:0}}_cleanupCachedDimensions(){this._boundaryRect=this._previewRect=void 0,this._parentPositions.clear()}_containInsideBoundaryOnResize(){let{x:o,y:e}=this._passiveTransform;if(o===0&&e===0||this.isDragging()||!this._boundaryElement)return;let t=this._rootElement.getBoundingClientRect(),i=this._boundaryElement.getBoundingClientRect();if(i.width===0&&i.height===0||t.width===0&&t.height===0)return;let r=i.left-t.left,s=t.right-i.right,d=i.top-t.top,m=t.bottom-i.bottom;i.width>t.width?(r>0&&(o+=r),s>0&&(o-=s)):o=0,i.height>t.height?(d>0&&(e+=d),m>0&&(e-=m)):e=0,(o!==this._passiveTransform.x||e!==this._passiveTransform.y)&&this.setFreeDragPosition({y:e,x:o})}_getDragStartDelay(o){let e=this.dragStartDelay;return typeof e=="number"?e:L0(o)?e.touch:e?e.mouse:0}_updateOnScroll(o){let e=this._parentPositions.handleScroll(o);if(e){let t=Im(o);this._boundaryRect&&t!==this._boundaryElement&&t.contains(this._boundaryElement)&&z0(this._boundaryRect,e.top,e.left),this._pickupPositionOnPage.x+=e.left,this._pickupPositionOnPage.y+=e.top,this._dropContainer||(this._activeTransform.x-=e.left,this._activeTransform.y-=e.top,this._applyRootElementTransform(this._activeTransform.x,this._activeTransform.y))}}_getViewportScrollPosition(){return this._parentPositions.positions.get(this._document)?.scrollPosition||this._parentPositions.getViewportScrollPosition()}_getShadowRoot(){return this._cachedShadowRoot===void 0&&(this._cachedShadowRoot=Rv(this._rootElement)),this._cachedShadowRoot}_getPreviewInsertionPoint(o,e){let t=this._previewContainer||"global";if(t==="parent")return o;if(t==="global"){let i=this._document;return e||i.fullscreenElement||i.webkitFullscreenElement||i.mozFullScreenElement||i.msFullscreenElement||i.body}return sl(t)}_getPreviewRect(){return(!this._previewRect||!this._previewRect.width&&!this._previewRect.height)&&(this._previewRect=this._preview?this._preview.getBoundingClientRect():this._initialDomRect),this._previewRect}_nativeDragStart=o=>{if(this._handles.length){let e=this._getTargetHandle(o);e&&!this._disabledHandles.has(e)&&!this.disabled&&o.preventDefault()}else this.disabled||o.preventDefault()};_getTargetHandle(o){return this._handles.find(e=>o.target&&(o.target===e||e.contains(o.target)))}_conditionallyInsertAnchor(o,e,t){if(o===this._initialContainer)this._anchor?.remove(),this._anchor=null;else if(e===this._initialContainer&&e.hasAnchor){let i=this._anchor??=iE(this._placeholder);i.classList.remove(YF),i.classList.add("cdk-drag-anchor"),i.style.transform="",t?t.before(i):sl(e.element).appendChild(i)}}};function JF(n,o,e){return Math.max(o,Math.min(e,n))}function L0(n){return n.type[0]==="t"}function cee(n){n.preventDefault()}function o8(n,o,e){let t=e8(o,n.length-1),i=e8(e,n.length-1);if(t===i)return;let r=n[t],s=i0)return null;let d=this.orientation==="horizontal",m=r.findIndex(k=>k.drag===o),u=r[s],h=r[m].clientRect,v=u.clientRect,b=m>s?1:-1,y=this._getItemOffsetPx(h,v,b),x=this._getSiblingOffsetPx(m,r,b),P=r.slice();return o8(r,m,s),r.forEach((k,S)=>{if(P[S]===k)return;let T=k.drag===o,A=T?y:x,D=T?o.getPlaceholderElement():k.drag.getRootElement();k.offset+=A;let F=Math.round(k.offset*(1/k.drag.scale));d?(D.style.transform=V1(`translate3d(${F}px, 0, 0)`,k.initialTransform),z0(k.clientRect,0,A)):(D.style.transform=V1(`translate3d(0, ${F}px, 0)`,k.initialTransform),z0(k.clientRect,A,0))}),this._previousSwap.overlaps=oE(v,e,t),this._previousSwap.drag=u.drag,this._previousSwap.delta=d?i.x:i.y,{previousIndex:m,currentIndex:s}}enter(o,e,t,i){let r=this._activeDraggables,s=r.indexOf(o),d=o.getPlaceholderElement();s>-1&&r.splice(s,1);let m=i==null||i<0?this._getItemIndexFromPointerPosition(o,e,t):i,u=r[m];if(u===o&&(u=r[m+1]),!u&&(m==null||m===-1||m{let e=o.getRootElement();if(e){let t=this._itemPositions.find(i=>i.drag===o)?.initialTransform;e.style.transform=t||""}}),this._itemPositions=[],this._activeDraggables=[],this._previousSwap.drag=null,this._previousSwap.delta=0,this._previousSwap.overlaps=!1}getActiveItemsSnapshot(){return this._activeDraggables}getItemIndex(o){return this._getVisualItemPositions().findIndex(e=>e.drag===o)}getItemAtIndex(o){return this._getVisualItemPositions()[o]?.drag||null}updateOnScroll(o,e){this._itemPositions.forEach(({clientRect:t})=>{z0(t,o,e)}),this._itemPositions.forEach(({drag:t})=>{this._dragDropRegistry.isDragging(t)&&t._sortFromLastPointerPosition()})}withElementContainer(o){this._element=o}_cacheItemPositions(){let o=this.orientation==="horizontal";this._itemPositions=this._activeDraggables.map(e=>{let t=e.getVisibleElement();return{drag:e,offset:0,initialTransform:t.style.transform||"",clientRect:dE(t)}}).sort((e,t)=>o?e.clientRect.left-t.clientRect.left:e.clientRect.top-t.clientRect.top)}_getVisualItemPositions(){return this.orientation==="horizontal"&&this.direction==="rtl"?this._itemPositions.slice().reverse():this._itemPositions}_getItemOffsetPx(o,e,t){let i=this.orientation==="horizontal",r=i?e.left-o.left:e.top-o.top;return t===-1&&(r+=i?e.width-o.width:e.height-o.height),r}_getSiblingOffsetPx(o,e,t){let i=this.orientation==="horizontal",r=e[o].clientRect,s=e[o+t*-1],d=r[i?"width":"height"]*t;if(s){let m=i?"left":"top",u=i?"right":"bottom";t===-1?d-=s.clientRect[m]-r[u]:d+=r[m]-s.clientRect[u]}return d}_shouldEnterAsFirstChild(o,e){if(!this._activeDraggables.length)return!1;let t=this._itemPositions,i=this.orientation==="horizontal";if(t[0].drag!==this._activeDraggables[0]){let s=t[t.length-1].clientRect;return i?o>=s.right:e>=s.bottom}else{let s=t[0].clientRect;return i?o<=s.left:e<=s.top}}_getItemIndexFromPointerPosition(o,e,t,i){let r=this.orientation==="horizontal",s=this._itemPositions.findIndex(({drag:d,clientRect:m})=>{if(d===o)return!1;if(i){let u=r?i.x:i.y;if(d===this._previousSwap.drag&&this._previousSwap.overlaps&&u===this._previousSwap.delta)return!1}return r?e>=Math.floor(m.left)&&e=Math.floor(m.top)&&tm?h.after(u):h.before(u),o8(this._activeItems,m,r);let v=this._getRootNode().elementFromPoint(e,t);return s.deltaX=i.x,s.deltaY=i.y,s.drag=d,s.overlaps=h===v||h.contains(v),{previousIndex:m,currentIndex:r}}enter(o,e,t,i){let r=this._activeItems.indexOf(o);r>-1&&this._activeItems.splice(r,1);let s=i==null||i<0?this._getItemIndexFromPointerPosition(o,e,t):i;s===-1&&(s=this._getClosestItemIndexToPointer(o,e,t));let d=this._activeItems[s];d&&!this._dragDropRegistry.isDragging(d)?(this._activeItems.splice(s,0,o),d.getRootElement().before(o.getPlaceholderElement())):(this._activeItems.push(o),this._element.appendChild(o.getPlaceholderElement()))}withItems(o){this._activeItems=o.slice()}withSortPredicate(o){this._sortPredicate=o}reset(){let o=this._element,e=this._previousSwap;for(let t=this._relatedNodes.length-1;t>-1;t--){let[i,r]=this._relatedNodes[t];i.parentNode===o&&i.nextSibling!==r&&(r===null?o.appendChild(i):r.parentNode===o&&o.insertBefore(i,r))}this._relatedNodes=[],this._activeItems=[],e.drag=null,e.deltaX=e.deltaY=0,e.overlaps=!1}getActiveItemsSnapshot(){return this._activeItems}getItemIndex(o){return this._activeItems.indexOf(o)}getItemAtIndex(o){return this._activeItems[o]||null}updateOnScroll(){this._activeItems.forEach(o=>{this._dragDropRegistry.isDragging(o)&&o._sortFromLastPointerPosition()})}withElementContainer(o){o!==this._element&&(this._element=o,this._rootNode=void 0)}_getItemIndexFromPointerPosition(o,e,t){let i=this._getRootNode().elementFromPoint(Math.floor(e),Math.floor(t)),r=i?this._activeItems.findIndex(s=>{let d=s.getRootElement();return i===d||d.contains(i)}):-1;return r===-1||!this._sortPredicate(r,o)?-1:r}_getRootNode(){return this._rootNode||(this._rootNode=Rv(this._element)||this._document),this._rootNode}_getClosestItemIndexToPointer(o,e,t){if(this._activeItems.length===0)return-1;if(this._activeItems.length===1)return 0;let i=1/0,r=-1;for(let s=0;s!0;sortPredicate=()=>!0;beforeStarted=new Ye;entered=new Ye;exited=new Ye;dropped=new Ye;sorted=new Ye;receivingStarted=new Ye;receivingStopped=new Ye;data;_container;_isDragging=!1;_parentPositions;_sortStrategy;_domRect;_draggables=[];_siblings=[];_activeSiblings=new Set;_viewportScrollSubscription=To.EMPTY;_verticalScrollDirection=wl.NONE;_horizontalScrollDirection=es.NONE;_scrollNode;_stopScrollTimers=new Ye;_cachedShadowRoot=null;_document;_scrollableElements=[];_initialScrollSnap;_direction="ltr";constructor(o,e,t,i,r){this._dragDropRegistry=e,this._ngZone=i,this._viewportRuler=r;let s=this.element=sl(o);this._document=t,this.withOrientation("vertical").withElementContainer(s),e.registerDropContainer(this),this._parentPositions=new z1(t)}dispose(){this._stopScrolling(),this._stopScrollTimers.complete(),this._viewportScrollSubscription.unsubscribe(),this.beforeStarted.complete(),this.entered.complete(),this.exited.complete(),this.dropped.complete(),this.sorted.complete(),this.receivingStarted.complete(),this.receivingStopped.complete(),this._activeSiblings.clear(),this._scrollNode=null,this._parentPositions.clear(),this._dragDropRegistry.removeDropContainer(this)}isDragging(){return this._isDragging}start(){this._draggingStarted(),this._notifyReceivingSiblings()}enter(o,e,t,i){this._draggingStarted(),i==null&&this.sortingDisabled&&(i=this._draggables.indexOf(o)),this._sortStrategy.enter(o,e,t,i),this._cacheParentPositions(),this._notifyReceivingSiblings(),this.entered.next({item:o,container:this,currentIndex:this.getItemIndex(o)})}exit(o){this._reset(),this.exited.next({item:o,container:this})}drop(o,e,t,i,r,s,d,m={}){this._reset(),this.dropped.next({item:o,currentIndex:e,previousIndex:t,container:this,previousContainer:i,isPointerOverContainer:r,distance:s,dropPoint:d,event:m})}withItems(o){let e=this._draggables;return this._draggables=o,o.forEach(t=>t._withDropContainer(this)),this.isDragging()&&(e.filter(i=>i.isDragging()).every(i=>o.indexOf(i)===-1)?this._reset():this._sortStrategy.withItems(this._draggables)),this}withDirection(o){return this._direction=o,this._sortStrategy instanceof B1&&(this._sortStrategy.direction=o),this}connectedTo(o){return this._siblings=o.slice(),this}withOrientation(o){if(o==="mixed")this._sortStrategy=new sE(this._document,this._dragDropRegistry);else{let e=new B1(this._dragDropRegistry);e.direction=this._direction,e.orientation=o,this._sortStrategy=e}return this._sortStrategy.withElementContainer(this._container),this._sortStrategy.withSortPredicate((e,t)=>this.sortPredicate(e,t,this)),this}withScrollableParents(o){let e=this._container;return this._scrollableElements=o.indexOf(e)===-1?[e,...o]:o.slice(),this}withElementContainer(o){if(o===this._container)return this;let e=sl(this.element),t=this._scrollableElements.indexOf(this._container),i=this._scrollableElements.indexOf(o);return t>-1&&this._scrollableElements.splice(t,1),i>-1&&this._scrollableElements.splice(i,1),this._sortStrategy&&this._sortStrategy.withElementContainer(o),this._cachedShadowRoot=null,this._scrollableElements.unshift(o),this._container=o,this}getScrollableParents(){return this._scrollableElements}getItemIndex(o){return this._isDragging?this._sortStrategy.getItemIndex(o):this._draggables.indexOf(o)}getItemAtIndex(o){return this._isDragging?this._sortStrategy.getItemAtIndex(o):this._draggables[o]||null}isReceiving(){return this._activeSiblings.size>0}_sortItem(o,e,t,i){if(this.sortingDisabled||!this._domRect||!WF(this._domRect,t8,e,t))return;let r=this._sortStrategy.sort(o,e,t,i);r&&this.sorted.next({previousIndex:r.previousIndex,currentIndex:r.currentIndex,container:this,item:o})}_startScrollingIfNecessary(o,e){if(this.autoScrollDisabled)return;let t,i=wl.NONE,r=es.NONE;if(this._parentPositions.positions.forEach((s,d)=>{d===this._document||!s.clientRect||t||WF(s.clientRect,t8,o,e)&&([i,r]=pee(d,s.clientRect,this._direction,o,e),(i||r)&&(t=d))}),!i&&!r){let{width:s,height:d}=this._viewportRuler.getViewportSize(),m={width:s,height:d,top:0,right:s,bottom:d,left:0};i=a8(m,e),r=s8(m,o),t=window}t&&(i!==this._verticalScrollDirection||r!==this._horizontalScrollDirection||t!==this._scrollNode)&&(this._verticalScrollDirection=i,this._horizontalScrollDirection=r,this._scrollNode=t,(i||r)&&t?this._ngZone.runOutsideAngular(this._startScrollInterval):this._stopScrolling())}_stopScrolling(){this._stopScrollTimers.next()}_draggingStarted(){let o=this._container.style;this.beforeStarted.next(),this._isDragging=!0,this._initialScrollSnap=o.msScrollSnapType||o.scrollSnapType||"",o.scrollSnapType=o.msScrollSnapType="none",this._sortStrategy.start(this._draggables),this._cacheParentPositions(),this._viewportScrollSubscription.unsubscribe(),this._listenToScrollEvents()}_cacheParentPositions(){this._parentPositions.cache(this._scrollableElements),this._domRect=this._parentPositions.positions.get(this._container).clientRect}_reset(){this._isDragging=!1;let o=this._container.style;o.scrollSnapType=o.msScrollSnapType=this._initialScrollSnap,this._siblings.forEach(e=>e._stopReceiving(this)),this._sortStrategy.reset(),this._stopScrolling(),this._viewportScrollSubscription.unsubscribe(),this._parentPositions.clear()}_startScrollInterval=()=>{this._stopScrolling(),kv(0,ol).pipe($e(this._stopScrollTimers)).subscribe(()=>{let o=this._scrollNode,e=this.autoScrollStep;this._verticalScrollDirection===wl.UP?o.scrollBy(0,-e):this._verticalScrollDirection===wl.DOWN&&o.scrollBy(0,e),this._horizontalScrollDirection===es.LEFT?o.scrollBy(-e,0):this._horizontalScrollDirection===es.RIGHT&&o.scrollBy(e,0)})};_isOverContainer(o,e){return this._domRect!=null&&oE(this._domRect,o,e)}_getSiblingContainerFromPosition(o,e,t){return this._siblings.find(i=>i._canReceive(o,e,t))}_canReceive(o,e,t){if(!this._domRect||!oE(this._domRect,e,t)||!this.enterPredicate(o,this))return!1;let i=this._getShadowRoot().elementFromPoint(e,t);return i?i===this._container||this._container.contains(i):!1}_startReceiving(o,e){let t=this._activeSiblings;!t.has(o)&&e.every(i=>this.enterPredicate(i,this)||this._draggables.indexOf(i)>-1)&&(t.add(o),this._cacheParentPositions(),this._listenToScrollEvents(),this.receivingStarted.next({initiator:o,receiver:this,items:e}))}_stopReceiving(o){this._activeSiblings.delete(o),this._viewportScrollSubscription.unsubscribe(),this.receivingStopped.next({initiator:o,receiver:this})}_listenToScrollEvents(){this._viewportScrollSubscription=this._dragDropRegistry.scrolled(this._getShadowRoot()).subscribe(o=>{if(this.isDragging()){let e=this._parentPositions.handleScroll(o);e&&this._sortStrategy.updateOnScroll(e.top,e.left)}else this.isReceiving()&&this._cacheParentPositions()})}_getShadowRoot(){if(!this._cachedShadowRoot){let o=Rv(this._container);this._cachedShadowRoot=o||this._document}return this._cachedShadowRoot}_notifyReceivingSiblings(){let o=this._sortStrategy.getActiveItemsSnapshot().filter(e=>e.isDragging());this._siblings.forEach(e=>e._startReceiving(this,o))}};function a8(n,o){let{top:e,bottom:t,height:i}=n,r=i*r8;return o>=e-r&&o<=e+r?wl.UP:o>=t-r&&o<=t+r?wl.DOWN:wl.NONE}function s8(n,o){let{left:e,right:t,width:i}=n,r=i*r8;return o>=e-r&&o<=e+r?es.LEFT:o>=t-r&&o<=t+r?es.RIGHT:es.NONE}function pee(n,o,e,t,i){let r=a8(o,i),s=s8(o,t),d=wl.NONE,m=es.NONE;if(r){let u=n.scrollTop;r===wl.UP?u>0&&(d=wl.UP):n.scrollHeight-u>n.clientHeight&&(d=wl.DOWN)}if(s){let u=n.scrollLeft;e==="rtl"?s===es.RIGHT?u<0&&(m=es.RIGHT):n.scrollWidth+u>n.clientWidth&&(m=es.LEFT):s===es.LEFT?u>0&&(m=es.LEFT):n.scrollWidth-u>n.clientWidth&&(m=es.RIGHT)}return[d,m]}var mee=(()=>{class n{_injector=f(Cr);constructor(){}createDrag(e,t){return i8(this._injector,e,t)}createDropList(e){return dee(this._injector,e)}static \u0275fac=function(t){return new(t||n)};static \u0275prov=J({token:n,factory:n.\u0275fac,providedIn:"root"})}return n})(),cE=new mn("CDK_DRAG_PARENT");var l8=new mn("CdkDragHandle"),c8=(()=>{class n{element=f(Qt);_parentDrag=f(cE,{optional:!0,skipSelf:!0});_dragDropRegistry=f(j1);_stateChanges=new Ye;get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._stateChanges.next(this)}_disabled=!1;constructor(){this._parentDrag?._addHandle(this)}ngAfterViewInit(){if(!this._parentDrag){let e=this.element.nativeElement.parentElement;for(;e;){let t=this._dragDropRegistry.getDragDirectiveForNode(e);if(t){this._parentDrag=t,t._addHandle(this);break}e=e.parentElement}}}ngOnDestroy(){this._parentDrag?._removeHandle(this),this._stateChanges.complete()}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkDragHandle",""]],hostAttrs:[1,"cdk-drag-handle"],inputs:{disabled:[2,"cdkDragHandleDisabled","disabled",At]},features:[un([{provide:l8,useExisting:n}])]})}return n})(),uee=new mn("CDK_DRAG_CONFIG"),gee=new mn("CdkDropList"),d8=(()=>{class n{element=f(Qt);dropContainer=f(gee,{optional:!0,skipSelf:!0});_ngZone=f(Ki);_viewContainerRef=f($o);_dir=f(Ua,{optional:!0});_changeDetectorRef=f(ie);_selfHandle=f(l8,{optional:!0,self:!0});_parentDrag=f(cE,{optional:!0,skipSelf:!0});_dragDropRegistry=f(j1);_destroyed=new Ye;_handles=new sn([]);_previewTemplate=null;_placeholderTemplate=null;_dragRef;data;lockAxis=null;rootElementSelector;boundaryElement;dragStartDelay;freeDragPosition;get disabled(){return this._disabled||!!(this.dropContainer&&this.dropContainer.disabled)}set disabled(e){this._disabled=e,this._dragRef.disabled=this._disabled}_disabled=!1;constrainPosition;previewClass;previewContainer;scale=1;started=new ve;released=new ve;ended=new ve;entered=new ve;exited=new ve;dropped=new ve;moved=new ls(e=>{let t=this._dragRef.moved.pipe(Pt(i=>({source:this,pointerPosition:i.pointerPosition,event:i.event,delta:i.delta,distance:i.distance}))).subscribe(e);return()=>{t.unsubscribe()}});_injector=f(Cr);constructor(){let e=this.dropContainer,t=f(uee,{optional:!0});this._dragRef=i8(this._injector,this.element,{dragStartThreshold:t&&t.dragStartThreshold!=null?t.dragStartThreshold:5,pointerDirectionChangeThreshold:t&&t.pointerDirectionChangeThreshold!=null?t.pointerDirectionChangeThreshold:5,zIndex:t?.zIndex}),this._dragRef.data=this,this._dragDropRegistry.registerDirectiveNode(this.element.nativeElement,this),t&&this._assignDefaults(t),e&&(e.addItem(this),e._dropListRef.beforeStarted.pipe($e(this._destroyed)).subscribe(()=>{this._dragRef.scale=this.scale})),this._syncInputs(this._dragRef),this._handleEvents(this._dragRef)}getPlaceholderElement(){return this._dragRef.getPlaceholderElement()}getRootElement(){return this._dragRef.getRootElement()}reset(){this._dragRef.reset()}resetToBoundary(){this._dragRef.resetToBoundary()}getFreeDragPosition(){return this._dragRef.getFreeDragPosition()}setFreeDragPosition(e){this._dragRef.setFreeDragPosition(e)}ngAfterViewInit(){Kr(()=>{this._updateRootElement(),this._setupHandlesListener(),this._dragRef.scale=this.scale,this.freeDragPosition&&this._dragRef.setFreeDragPosition(this.freeDragPosition)},{injector:this._injector})}ngOnChanges(e){let t=e.rootElementSelector,i=e.freeDragPosition;t&&!t.firstChange&&this._updateRootElement(),this._dragRef.scale=this.scale,i&&!i.firstChange&&this.freeDragPosition&&this._dragRef.setFreeDragPosition(this.freeDragPosition)}ngOnDestroy(){this.dropContainer&&this.dropContainer.removeItem(this),this._dragDropRegistry.removeDirectiveNode(this.element.nativeElement),this._ngZone.runOutsideAngular(()=>{this._handles.complete(),this._destroyed.next(),this._destroyed.complete(),this._dragRef.dispose()})}_addHandle(e){let t=this._handles.getValue();t.push(e),this._handles.next(t)}_removeHandle(e){let t=this._handles.getValue(),i=t.indexOf(e);i>-1&&(t.splice(i,1),this._handles.next(t))}_setPreviewTemplate(e){this._previewTemplate=e}_resetPreviewTemplate(e){e===this._previewTemplate&&(this._previewTemplate=null)}_setPlaceholderTemplate(e){this._placeholderTemplate=e}_resetPlaceholderTemplate(e){e===this._placeholderTemplate&&(this._placeholderTemplate=null)}_updateRootElement(){let e=this.element.nativeElement,t=e;this.rootElementSelector&&(t=e.closest!==void 0?e.closest(this.rootElementSelector):e.parentElement?.closest(this.rootElementSelector)),this._dragRef.withRootElement(t||e)}_getBoundaryElement(){let e=this.boundaryElement;return e?typeof e=="string"?this.element.nativeElement.closest(e):sl(e):null}_syncInputs(e){e.beforeStarted.subscribe(()=>{if(!e.isDragging()){let t=this._dir,i=this.dragStartDelay,r=this._placeholderTemplate?{template:this._placeholderTemplate.templateRef,context:this._placeholderTemplate.data,viewContainer:this._viewContainerRef}:null,s=this._previewTemplate?{template:this._previewTemplate.templateRef,context:this._previewTemplate.data,matchSize:this._previewTemplate.matchSize,viewContainer:this._viewContainerRef}:null;e.disabled=this.disabled,e.lockAxis=this.lockAxis,e.scale=this.scale,e.dragStartDelay=typeof i=="object"&&i?i:ap(i),e.constrainPosition=this.constrainPosition,e.previewClass=this.previewClass,e.withBoundaryElement(this._getBoundaryElement()).withPlaceholderTemplate(r).withPreviewTemplate(s).withPreviewContainer(this.previewContainer||"global"),t&&e.withDirection(t.value)}}),e.beforeStarted.pipe($a(1)).subscribe(()=>{if(this._parentDrag){e.withParent(this._parentDrag._dragRef);return}let t=this.element.nativeElement.parentElement;for(;t;){let i=this._dragDropRegistry.getDragDirectiveForNode(t);if(i){e.withParent(i._dragRef);break}t=t.parentElement}})}_handleEvents(e){e.started.subscribe(t=>{this.started.emit({source:this,event:t.event}),this._changeDetectorRef.markForCheck()}),e.released.subscribe(t=>{this.released.emit({source:this,event:t.event})}),e.ended.subscribe(t=>{this.ended.emit({source:this,distance:t.distance,dropPoint:t.dropPoint,event:t.event}),this._changeDetectorRef.markForCheck()}),e.entered.subscribe(t=>{this.entered.emit({container:t.container.data,item:this,currentIndex:t.currentIndex})}),e.exited.subscribe(t=>{this.exited.emit({container:t.container.data,item:this})}),e.dropped.subscribe(t=>{this.dropped.emit({previousIndex:t.previousIndex,currentIndex:t.currentIndex,previousContainer:t.previousContainer.data,container:t.container.data,isPointerOverContainer:t.isPointerOverContainer,item:this,distance:t.distance,dropPoint:t.dropPoint,event:t.event})})}_assignDefaults(e){let{lockAxis:t,dragStartDelay:i,constrainPosition:r,previewClass:s,boundaryElement:d,draggingDisabled:m,rootElementSelector:u,previewContainer:h}=e;this.disabled=m??!1,this.dragStartDelay=i||0,this.lockAxis=t||null,r&&(this.constrainPosition=r),s&&(this.previewClass=s),d&&(this.boundaryElement=d),u&&(this.rootElementSelector=u),h&&(this.previewContainer=h)}_setupHandlesListener(){this._handles.pipe(ei(e=>{let t=e.map(i=>i.element);this._selfHandle&&this.rootElementSelector&&t.push(this.element),this._dragRef.withHandles(t)}),$n(e=>io(...e.map(t=>t._stateChanges.pipe(Oi(t))))),$e(this._destroyed)).subscribe(e=>{let t=this._dragRef,i=e.element.nativeElement;e.disabled?t.disableHandle(i):t.enableHandle(i)})}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkDrag",""]],hostAttrs:[1,"cdk-drag"],hostVars:4,hostBindings:function(t,i){t&2&&se("cdk-drag-disabled",i.disabled)("cdk-drag-dragging",i._dragRef.isDragging())},inputs:{data:[0,"cdkDragData","data"],lockAxis:[0,"cdkDragLockAxis","lockAxis"],rootElementSelector:[0,"cdkDragRootElement","rootElementSelector"],boundaryElement:[0,"cdkDragBoundary","boundaryElement"],dragStartDelay:[0,"cdkDragStartDelay","dragStartDelay"],freeDragPosition:[0,"cdkDragFreeDragPosition","freeDragPosition"],disabled:[2,"cdkDragDisabled","disabled",At],constrainPosition:[0,"cdkDragConstrainPosition","constrainPosition"],previewClass:[0,"cdkDragPreviewClass","previewClass"],previewContainer:[0,"cdkDragPreviewContainer","previewContainer"],scale:[2,"cdkDragScale","scale",Ho]},outputs:{started:"cdkDragStarted",released:"cdkDragReleased",ended:"cdkDragEnded",entered:"cdkDragEntered",exited:"cdkDragExited",dropped:"cdkDragDropped",moved:"cdkDragMoved"},exportAs:["cdkDrag"],features:[un([{provide:cE,useExisting:n}]),In]})}return n})();var $1=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({providers:[mee],imports:[Nm]})}return n})();var _ee=[[["caption"]],[["colgroup"],["col"]],"*"],fee=["caption","colgroup, col","*"];function Cee(n,o){n&1&&Zt(0,2)}function vee(n,o){n&1&&(a(0,"thead",0),Ci(1,1),l(),a(2,"tbody",0),Ci(3,2)(4,3),l(),a(5,"tfoot",0),Ci(6,4),l())}function xee(n,o){n&1&&Ci(0,1)(1,2)(2,3)(3,4)}var oc=new mn("CDK_TABLE");var W1=(()=>{class n{template=f(lr);constructor(){}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkCellDef",""]]})}return n})(),G1=(()=>{class n{template=f(lr);constructor(){}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkHeaderCellDef",""]]})}return n})(),u8=(()=>{class n{template=f(lr);constructor(){}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkFooterCellDef",""]]})}return n})(),Fp=(()=>{class n{_table=f(oc,{optional:!0});_hasStickyChanged=!1;get name(){return this._name}set name(e){this._setNameInput(e)}_name;get sticky(){return this._sticky}set sticky(e){e!==this._sticky&&(this._sticky=e,this._hasStickyChanged=!0)}_sticky=!1;get stickyEnd(){return this._stickyEnd}set stickyEnd(e){e!==this._stickyEnd&&(this._stickyEnd=e,this._hasStickyChanged=!0)}_stickyEnd=!1;cell;headerCell;footerCell;cssClassFriendlyName;_columnCssClassName;constructor(){}hasStickyChanged(){let e=this._hasStickyChanged;return this.resetStickyChanged(),e}resetStickyChanged(){this._hasStickyChanged=!1}_updateColumnCssClassName(){this._columnCssClassName=[`cdk-column-${this.cssClassFriendlyName}`]}_setNameInput(e){e&&(this._name=e,this.cssClassFriendlyName=e.replace(/[^a-z0-9_-]/gi,"-"),this._updateColumnCssClassName())}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkColumnDef",""]],contentQueries:function(t,i,r){if(t&1&&Xi(r,W1,5)(r,G1,5)(r,u8,5),t&2){let s;gt(s=ht())&&(i.cell=s.first),gt(s=ht())&&(i.headerCell=s.first),gt(s=ht())&&(i.footerCell=s.first)}},inputs:{name:[0,"cdkColumnDef","name"],sticky:[2,"sticky","sticky",At],stickyEnd:[2,"stickyEnd","stickyEnd",At]}})}return n})(),U1=class{constructor(o,e){e.nativeElement.classList.add(...o._columnCssClassName)}},g8=(()=>{class n extends U1{constructor(){super(f(Fp),f(Qt))}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["cdk-header-cell"],["th","cdk-header-cell",""]],hostAttrs:["role","columnheader",1,"cdk-header-cell"],features:[li]})}return n})();var h8=(()=>{class n extends U1{constructor(){let e=f(Fp),t=f(Qt);super(e,t);let i=e._table?._getCellRole();i&&t.nativeElement.setAttribute("role",i)}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["cdk-cell"],["td","cdk-cell",""]],hostAttrs:[1,"cdk-cell"],features:[li]})}return n})();var uE=(()=>{class n{template=f(lr);_differs=f(yd);columns;_columnsDiffer;constructor(){}ngOnChanges(e){if(!this._columnsDiffer){let t=e.columns&&e.columns.currentValue||[];this._columnsDiffer=this._differs.find(t).create(),this._columnsDiffer.diff(t)}}getColumnsDiff(){return this._columnsDiffer.diff(this.columns)}extractCellTemplate(e){return this instanceof j0?e.headerCell.template:this instanceof gE?e.footerCell.template:e.cell.template}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,features:[In]})}return n})(),j0=(()=>{class n extends uE{_table=f(oc,{optional:!0});_hasStickyChanged=!1;get sticky(){return this._sticky}set sticky(e){e!==this._sticky&&(this._sticky=e,this._hasStickyChanged=!0)}_sticky=!1;constructor(){super(f(lr),f(yd))}ngOnChanges(e){super.ngOnChanges(e)}hasStickyChanged(){let e=this._hasStickyChanged;return this.resetStickyChanged(),e}resetStickyChanged(){this._hasStickyChanged=!1}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkHeaderRowDef",""]],inputs:{columns:[0,"cdkHeaderRowDef","columns"],sticky:[2,"cdkHeaderRowDefSticky","sticky",At]},features:[li,In]})}return n})(),gE=(()=>{class n extends uE{_table=f(oc,{optional:!0});_hasStickyChanged=!1;get sticky(){return this._sticky}set sticky(e){e!==this._sticky&&(this._sticky=e,this._hasStickyChanged=!0)}_sticky=!1;constructor(){super(f(lr),f(yd))}ngOnChanges(e){super.ngOnChanges(e)}hasStickyChanged(){let e=this._hasStickyChanged;return this.resetStickyChanged(),e}resetStickyChanged(){this._hasStickyChanged=!1}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkFooterRowDef",""]],inputs:{columns:[0,"cdkFooterRowDef","columns"],sticky:[2,"cdkFooterRowDefSticky","sticky",At]},features:[li,In]})}return n})(),q1=(()=>{class n extends uE{_table=f(oc,{optional:!0});when;constructor(){super(f(lr),f(yd))}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkRowDef",""]],inputs:{columns:[0,"cdkRowDefColumns","columns"],when:[0,"cdkRowDefWhen","when"]},features:[li]})}return n})(),_u=(()=>{class n{_viewContainer=f($o);cells;context;static mostRecentCellOutlet=null;constructor(){n.mostRecentCellOutlet=this}ngOnDestroy(){n.mostRecentCellOutlet===this&&(n.mostRecentCellOutlet=null)}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","cdkCellOutlet",""]]})}return n})(),hE=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["cdk-header-row"],["tr","cdk-header-row",""]],hostAttrs:["role","row",1,"cdk-header-row"],decls:1,vars:0,consts:[["cdkCellOutlet",""]],template:function(t,i){t&1&&Ci(0,0)},dependencies:[_u],encapsulation:2})}return n})();var _E=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["cdk-row"],["tr","cdk-row",""]],hostAttrs:["role","row",1,"cdk-row"],decls:1,vars:0,consts:[["cdkCellOutlet",""]],template:function(t,i){t&1&&Ci(0,0)},dependencies:[_u],encapsulation:2})}return n})(),_8=(()=>{class n{templateRef=f(lr);_contentClassNames=["cdk-no-data-row","cdk-row"];_cellClassNames=["cdk-cell","cdk-no-data-cell"];_cellSelector="td, cdk-cell, [cdk-cell], .cdk-cell";constructor(){}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["ng-template","cdkNoDataRow",""]]})}return n})(),p8=["top","bottom","left","right"],mE=class{_isNativeHtmlTable;_stickCellCss;_isBrowser;_needsPositionStickyOnElement;direction;_positionListener;_tableInjector;_elemSizeCache=new WeakMap;_resizeObserver=globalThis?.ResizeObserver?new globalThis.ResizeObserver(o=>this._updateCachedSizes(o)):null;_updatedStickyColumnsParamsToReplay=[];_stickyColumnsReplayTimeout=null;_cachedCellWidths=[];_borderCellCss;_destroyed=!1;constructor(o,e,t=!0,i=!0,r,s,d){this._isNativeHtmlTable=o,this._stickCellCss=e,this._isBrowser=t,this._needsPositionStickyOnElement=i,this.direction=r,this._positionListener=s,this._tableInjector=d,this._borderCellCss={top:`${e}-border-elem-top`,bottom:`${e}-border-elem-bottom`,left:`${e}-border-elem-left`,right:`${e}-border-elem-right`}}clearStickyPositioning(o,e){(e.includes("left")||e.includes("right"))&&this._removeFromStickyColumnReplayQueue(o);let t=[];for(let i of o)i.nodeType===i.ELEMENT_NODE&&t.push(i,...Array.from(i.children));Kr({write:()=>{for(let i of t)this._removeStickyStyle(i,e)}},{injector:this._tableInjector})}updateStickyColumns(o,e,t,i=!0,r=!0){if(!o.length||!this._isBrowser||!(e.some(k=>k)||t.some(k=>k))){this._positionListener?.stickyColumnsUpdated({sizes:[]}),this._positionListener?.stickyEndColumnsUpdated({sizes:[]});return}let s=o[0],d=s.children.length,m=this.direction==="rtl",u=m?"right":"left",h=m?"left":"right",v=e.lastIndexOf(!0),b=t.indexOf(!0),y,x,P;r&&this._updateStickyColumnReplayQueue({rows:[...o],stickyStartStates:[...e],stickyEndStates:[...t]}),Kr({earlyRead:()=>{y=this._getCellWidths(s,i),x=this._getStickyStartColumnPositions(y,e),P=this._getStickyEndColumnPositions(y,t)},write:()=>{for(let k of o)for(let S=0;S!!k)&&(this._positionListener.stickyColumnsUpdated({sizes:v===-1?[]:y.slice(0,v+1).map((k,S)=>e[S]?k:null)}),this._positionListener.stickyEndColumnsUpdated({sizes:b===-1?[]:y.slice(b).map((k,S)=>t[S+b]?k:null).reverse()}))}},{injector:this._tableInjector})}stickRows(o,e,t){if(!this._isBrowser)return;let i=t==="bottom"?o.slice().reverse():o,r=t==="bottom"?e.slice().reverse():e,s=[],d=[],m=[];Kr({earlyRead:()=>{for(let u=0,h=0;u{let u=r.lastIndexOf(!0);for(let h=0;h{let t=o.querySelector("tfoot");t&&(e.some(i=>!i)?this._removeStickyStyle(t,["bottom"]):this._addStickyStyle(t,"bottom",0,!1))}},{injector:this._tableInjector})}destroy(){this._stickyColumnsReplayTimeout&&clearTimeout(this._stickyColumnsReplayTimeout),this._resizeObserver?.disconnect(),this._destroyed=!0}_removeStickyStyle(o,e){if(!o.classList.contains(this._stickCellCss))return;for(let i of e)o.style[i]="",o.classList.remove(this._borderCellCss[i]);p8.some(i=>e.indexOf(i)===-1&&o.style[i])?o.style.zIndex=this._getCalculatedZIndex(o):(o.style.zIndex="",this._needsPositionStickyOnElement&&(o.style.position=""),o.classList.remove(this._stickCellCss))}_addStickyStyle(o,e,t,i){o.classList.add(this._stickCellCss),i&&o.classList.add(this._borderCellCss[e]),o.style[e]=`${t}px`,o.style.zIndex=this._getCalculatedZIndex(o),this._needsPositionStickyOnElement&&(o.style.cssText+="position: -webkit-sticky; position: sticky; ")}_getCalculatedZIndex(o){let e={top:100,bottom:10,left:1,right:1},t=0;for(let i of p8)o.style[i]&&(t+=e[i]);return t?`${t}`:""}_getCellWidths(o,e=!0){if(!e&&this._cachedCellWidths.length)return this._cachedCellWidths;let t=[],i=o.children;for(let r=0;r0;r--)e[r]&&(t[r]=i,i+=o[r]);return t}_retrieveElementSize(o){let e=this._elemSizeCache.get(o);if(e)return e;let t=o.getBoundingClientRect(),i={width:t.width,height:t.height};return this._resizeObserver&&(this._elemSizeCache.set(o,i),this._resizeObserver.observe(o,{box:"border-box"})),i}_updateStickyColumnReplayQueue(o){this._removeFromStickyColumnReplayQueue(o.rows),this._stickyColumnsReplayTimeout||this._updatedStickyColumnsParamsToReplay.push(o)}_removeFromStickyColumnReplayQueue(o){let e=new Set(o);for(let t of this._updatedStickyColumnsParamsToReplay)t.rows=t.rows.filter(i=>!e.has(i));this._updatedStickyColumnsParamsToReplay=this._updatedStickyColumnsParamsToReplay.filter(t=>!!t.rows.length)}_updateCachedSizes(o){let e=!1;for(let t of o){let i=t.borderBoxSize?.length?{width:t.borderBoxSize[0].inlineSize,height:t.borderBoxSize[0].blockSize}:{width:t.contentRect.width,height:t.contentRect.height};i.width!==this._elemSizeCache.get(t.target)?.width&&yee(t.target)&&(e=!0),this._elemSizeCache.set(t.target,i)}e&&this._updatedStickyColumnsParamsToReplay.length&&(this._stickyColumnsReplayTimeout&&clearTimeout(this._stickyColumnsReplayTimeout),this._stickyColumnsReplayTimeout=setTimeout(()=>{if(!this._destroyed){for(let t of this._updatedStickyColumnsParamsToReplay)this.updateStickyColumns(t.rows,t.stickyStartStates,t.stickyEndStates,!0,!1);this._updatedStickyColumnsParamsToReplay=[],this._stickyColumnsReplayTimeout=null}},0))}};function yee(n){return["cdk-cell","cdk-header-cell","cdk-footer-cell"].some(o=>n.classList.contains(o))}var B0=new mn("STICKY_POSITIONING_LISTENER");var fE=(()=>{class n{viewContainer=f($o);elementRef=f(Qt);constructor(){let e=f(oc);e._rowOutlet=this,e._outletAssigned()}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","rowOutlet",""]]})}return n})(),CE=(()=>{class n{viewContainer=f($o);elementRef=f(Qt);constructor(){let e=f(oc);e._headerRowOutlet=this,e._outletAssigned()}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","headerRowOutlet",""]]})}return n})(),vE=(()=>{class n{viewContainer=f($o);elementRef=f(Qt);constructor(){let e=f(oc);e._footerRowOutlet=this,e._outletAssigned()}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","footerRowOutlet",""]]})}return n})(),xE=(()=>{class n{viewContainer=f($o);elementRef=f(Qt);constructor(){let e=f(oc);e._noDataRowOutlet=this,e._outletAssigned()}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["","noDataRowOutlet",""]]})}return n})(),yE=(()=>{class n{_differs=f(yd);_changeDetectorRef=f(ie);_elementRef=f(Qt);_dir=f(Ua,{optional:!0});_platform=f(Vr);_viewRepeater;_viewportRuler=f(wd);_injector=f(Cr);_virtualScrollViewport=f(yA,{optional:!0,host:!0});_positionListener=f(B0,{optional:!0})||f(B0,{optional:!0,skipSelf:!0});_document=f(Eo);_data;_renderedRange;_onDestroy=new Ye;_renderRows;_renderChangeSubscription=null;_columnDefsByName=new Map;_rowDefs;_headerRowDefs;_footerRowDefs;_dataDiffer;_defaultRowDef=null;_customColumnDefs=new Set;_customRowDefs=new Set;_customHeaderRowDefs=new Set;_customFooterRowDefs=new Set;_customNoDataRow=null;_headerRowDefChanged=!0;_footerRowDefChanged=!0;_stickyColumnStylesNeedReset=!0;_forceRecalculateCellWidths=!0;_cachedRenderRowsMap=new Map;_isNativeHtmlTable;_stickyStyler;stickyCssClass="cdk-table-sticky";needsPositionStickyOnElement=!0;_isServer;_isShowingNoDataRow=!1;_hasAllOutlets=!1;_hasInitialized=!1;_headerRowStickyUpdates=new Ye;_footerRowStickyUpdates=new Ye;_disableVirtualScrolling=!1;_getCellRole(){if(this._cellRoleInternal===void 0){let e=this._elementRef.nativeElement.getAttribute("role");return e==="grid"||e==="treegrid"?"gridcell":"cell"}return this._cellRoleInternal}_cellRoleInternal=void 0;get trackBy(){return this._trackByFn}set trackBy(e){this._trackByFn=e}_trackByFn;get dataSource(){return this._dataSource}set dataSource(e){this._dataSource!==e&&(this._switchDataSource(e),this._changeDetectorRef.markForCheck())}_dataSource;_dataSourceChanges=new Ye;_dataStream=new Ye;get multiTemplateDataRows(){return this._multiTemplateDataRows}set multiTemplateDataRows(e){this._multiTemplateDataRows=e,this._rowOutlet&&this._rowOutlet.viewContainer.length&&(this._forceRenderDataRows(),this.updateStickyColumnStyles())}_multiTemplateDataRows=!1;get fixedLayout(){return this._virtualScrollEnabled()?!0:this._fixedLayout}set fixedLayout(e){this._fixedLayout=e,this._forceRecalculateCellWidths=!0,this._stickyColumnStylesNeedReset=!0}_fixedLayout=!1;recycleRows=!1;contentChanged=new ve;viewChange=new sn({start:0,end:Number.MAX_VALUE});_rowOutlet;_headerRowOutlet;_footerRowOutlet;_noDataRowOutlet;_contentColumnDefs;_contentRowDefs;_contentHeaderRowDefs;_contentFooterRowDefs;_noDataRow;constructor(){f(new op("role"),{optional:!0})||this._elementRef.nativeElement.setAttribute("role","table"),this._isServer=!this._platform.isBrowser,this._isNativeHtmlTable=this._elementRef.nativeElement.nodeName==="TABLE",this._dataDiffer=this._differs.find([]).create((t,i)=>this.trackBy?this.trackBy(i.dataIndex,i.data):i)}ngOnInit(){this._setupStickyStyler(),this._viewportRuler.change().pipe($e(this._onDestroy)).subscribe(()=>{this._forceRecalculateCellWidths=!0})}ngAfterContentInit(){this._viewRepeater=this.recycleRows||this._virtualScrollEnabled()?new xA:new TA,this._virtualScrollEnabled()&&this._setupVirtualScrolling(this._virtualScrollViewport),this._hasInitialized=!0}ngAfterContentChecked(){this._canRender()&&this._render()}ngOnDestroy(){this._stickyStyler?.destroy(),[this._rowOutlet?.viewContainer,this._headerRowOutlet?.viewContainer,this._footerRowOutlet?.viewContainer,this._cachedRenderRowsMap,this._customColumnDefs,this._customRowDefs,this._customHeaderRowDefs,this._customFooterRowDefs,this._columnDefsByName].forEach(e=>{e?.clear()}),this._headerRowDefs=[],this._footerRowDefs=[],this._defaultRowDef=null,this._headerRowStickyUpdates.complete(),this._footerRowStickyUpdates.complete(),this._onDestroy.next(),this._onDestroy.complete(),og(this.dataSource)&&this.dataSource.disconnect(this)}renderRows(){this._renderRows=this._getAllRenderRows();let e=this._dataDiffer.diff(this._renderRows);if(!e){this._updateNoDataRow(),this.contentChanged.next();return}let t=this._rowOutlet.viewContainer;this._viewRepeater.applyChanges(e,t,(i,r,s)=>this._getEmbeddedViewArgs(i.item,s),i=>i.item.data,i=>{i.operation===vA.INSERTED&&i.context&&this._renderCellTemplateForItem(i.record.item.rowDef,i.context)}),this._updateRowIndexContext(),e.forEachIdentityChange(i=>{let r=t.get(i.currentIndex);r.context.$implicit=i.item.data}),this._updateNoDataRow(),this.contentChanged.next(),this.updateStickyColumnStyles()}addColumnDef(e){this._customColumnDefs.add(e)}removeColumnDef(e){this._customColumnDefs.delete(e)}addRowDef(e){this._customRowDefs.add(e)}removeRowDef(e){this._customRowDefs.delete(e)}addHeaderRowDef(e){this._customHeaderRowDefs.add(e),this._headerRowDefChanged=!0}removeHeaderRowDef(e){this._customHeaderRowDefs.delete(e),this._headerRowDefChanged=!0}addFooterRowDef(e){this._customFooterRowDefs.add(e),this._footerRowDefChanged=!0}removeFooterRowDef(e){this._customFooterRowDefs.delete(e),this._footerRowDefChanged=!0}setNoDataRow(e){this._customNoDataRow=e}updateStickyHeaderRowStyles(){let e=this._getRenderedRows(this._headerRowOutlet);if(this._isNativeHtmlTable){let i=m8(this._headerRowOutlet,"thead");i&&(i.style.display=e.length?"":"none")}let t=this._headerRowDefs.map(i=>i.sticky);this._stickyStyler.clearStickyPositioning(e,["top"]),this._stickyStyler.stickRows(e,t,"top"),this._headerRowDefs.forEach(i=>i.resetStickyChanged())}updateStickyFooterRowStyles(){let e=this._getRenderedRows(this._footerRowOutlet);if(this._isNativeHtmlTable){let i=m8(this._footerRowOutlet,"tfoot");i&&(i.style.display=e.length?"":"none")}let t=this._footerRowDefs.map(i=>i.sticky);this._stickyStyler.clearStickyPositioning(e,["bottom"]),this._stickyStyler.stickRows(e,t,"bottom"),this._stickyStyler.updateStickyFooterContainer(this._elementRef.nativeElement,t),this._footerRowDefs.forEach(i=>i.resetStickyChanged())}updateStickyColumnStyles(){let e=this._getRenderedRows(this._headerRowOutlet),t=this._getRenderedRows(this._rowOutlet),i=this._getRenderedRows(this._footerRowOutlet);(this._isNativeHtmlTable&&!this.fixedLayout||this._stickyColumnStylesNeedReset)&&(this._stickyStyler.clearStickyPositioning([...e,...t,...i],["left","right"]),this._stickyColumnStylesNeedReset=!1),e.forEach((r,s)=>{this._addStickyColumnStyles([r],this._headerRowDefs[s])}),this._rowDefs.forEach(r=>{let s=[];for(let d=0;d{this._addStickyColumnStyles([r],this._footerRowDefs[s])}),Array.from(this._columnDefsByName.values()).forEach(r=>r.resetStickyChanged())}stickyColumnsUpdated(e){this._positionListener?.stickyColumnsUpdated(e)}stickyEndColumnsUpdated(e){this._positionListener?.stickyEndColumnsUpdated(e)}stickyHeaderRowsUpdated(e){this._headerRowStickyUpdates.next(e),this._positionListener?.stickyHeaderRowsUpdated(e)}stickyFooterRowsUpdated(e){this._footerRowStickyUpdates.next(e),this._positionListener?.stickyFooterRowsUpdated(e)}_outletAssigned(){!this._hasAllOutlets&&this._rowOutlet&&this._headerRowOutlet&&this._footerRowOutlet&&this._noDataRowOutlet&&(this._hasAllOutlets=!0,this._canRender()&&this._render())}_canRender(){return this._hasAllOutlets&&this._hasInitialized}_render(){this._cacheRowDefs(),this._cacheColumnDefs(),!this._headerRowDefs.length&&!this._footerRowDefs.length&&this._rowDefs.length;let t=this._renderUpdatedColumns()||this._headerRowDefChanged||this._footerRowDefChanged;this._stickyColumnStylesNeedReset=this._stickyColumnStylesNeedReset||t,this._forceRecalculateCellWidths=t,this._headerRowDefChanged&&(this._forceRenderHeaderRows(),this._headerRowDefChanged=!1),this._footerRowDefChanged&&(this._forceRenderFooterRows(),this._footerRowDefChanged=!1),this.dataSource&&this._rowDefs.length>0&&!this._renderChangeSubscription?this._observeRenderChanges():this._stickyColumnStylesNeedReset&&this.updateStickyColumnStyles(),this._checkStickyStates()}_getAllRenderRows(){if(!Array.isArray(this._data)||!this._renderedRange)return[];let e=[],t=Math.min(this._data.length,this._renderedRange.end),i=this._cachedRenderRowsMap;this._cachedRenderRowsMap=new Map;for(let r=this._renderedRange.start;r{let d=i&&i.has(s)?i.get(s):[];if(d.length){let m=d.shift();return m.dataIndex=t,m}else return{data:e,rowDef:s,dataIndex:t}})}_cacheColumnDefs(){this._columnDefsByName.clear(),H1(this._getOwnDefs(this._contentColumnDefs),this._customColumnDefs).forEach(t=>{this._columnDefsByName.has(t.name),this._columnDefsByName.set(t.name,t)})}_cacheRowDefs(){this._headerRowDefs=H1(this._getOwnDefs(this._contentHeaderRowDefs),this._customHeaderRowDefs),this._footerRowDefs=H1(this._getOwnDefs(this._contentFooterRowDefs),this._customFooterRowDefs),this._rowDefs=H1(this._getOwnDefs(this._contentRowDefs),this._customRowDefs);let e=this._rowDefs.filter(t=>!t.when);this._defaultRowDef=e[0]}_renderUpdatedColumns(){let e=(s,d)=>{let m=!!d.getColumnsDiff();return s||m},t=this._rowDefs.reduce(e,!1);t&&this._forceRenderDataRows();let i=this._headerRowDefs.reduce(e,!1);i&&this._forceRenderHeaderRows();let r=this._footerRowDefs.reduce(e,!1);return r&&this._forceRenderFooterRows(),t||i||r}_switchDataSource(e){this._data=[],og(this.dataSource)&&this.dataSource.disconnect(this),this._renderChangeSubscription&&(this._renderChangeSubscription.unsubscribe(),this._renderChangeSubscription=null),e||(this._dataDiffer&&this._dataDiffer.diff([]),this._rowOutlet&&this._rowOutlet.viewContainer.clear()),this._dataSource=e}_observeRenderChanges(){if(!this.dataSource)return;let e;og(this.dataSource)?e=this.dataSource.connect(this):nf(this.dataSource)?e=this.dataSource:Array.isArray(this.dataSource)&&(e=Ut(this.dataSource)),this._renderChangeSubscription=da([e,this.viewChange]).pipe($e(this._onDestroy)).subscribe(([t,i])=>{this._data=t||[],this._renderedRange=i,this._dataStream.next(t),this.renderRows()})}_forceRenderHeaderRows(){this._headerRowOutlet.viewContainer.length>0&&this._headerRowOutlet.viewContainer.clear(),this._headerRowDefs.forEach((e,t)=>this._renderRow(this._headerRowOutlet,e,t)),this.updateStickyHeaderRowStyles()}_forceRenderFooterRows(){this._footerRowOutlet.viewContainer.length>0&&this._footerRowOutlet.viewContainer.clear(),this._footerRowDefs.forEach((e,t)=>this._renderRow(this._footerRowOutlet,e,t)),this.updateStickyFooterRowStyles()}_addStickyColumnStyles(e,t){let i=Array.from(t?.columns||[]).map(d=>{let m=this._columnDefsByName.get(d);return m}),r=i.map(d=>d.sticky),s=i.map(d=>d.stickyEnd);this._stickyStyler.updateStickyColumns(e,r,s,!this.fixedLayout||this._forceRecalculateCellWidths)}_getRenderedRows(e){let t=[];for(let i=0;i!r.when||r.when(t,e));else{let r=this._rowDefs.find(s=>s.when&&s.when(t,e))||this._defaultRowDef;r&&i.push(r)}return i.length,i}_getEmbeddedViewArgs(e,t){let i=e.rowDef,r={$implicit:e.data};return{templateRef:i.template,context:r,index:t}}_renderRow(e,t,i,r={}){let s=e.viewContainer.createEmbeddedView(t.template,r,i);return this._renderCellTemplateForItem(t,r),s}_renderCellTemplateForItem(e,t){for(let i of this._getCellTemplates(e))_u.mostRecentCellOutlet&&_u.mostRecentCellOutlet._viewContainer.createEmbeddedView(i,t);this._changeDetectorRef.markForCheck()}_updateRowIndexContext(){let e=this._rowOutlet.viewContainer;for(let t=0,i=e.length;t{let i=this._columnDefsByName.get(t);return e.extractCellTemplate(i)})}_forceRenderDataRows(){this._dataDiffer.diff([]),this._rowOutlet.viewContainer.clear(),this.renderRows()}_checkStickyStates(){let e=(t,i)=>t||i.hasStickyChanged();this._headerRowDefs.reduce(e,!1)&&this.updateStickyHeaderRowStyles(),this._footerRowDefs.reduce(e,!1)&&this.updateStickyFooterRowStyles(),Array.from(this._columnDefsByName.values()).reduce(e,!1)&&(this._stickyColumnStylesNeedReset=!0,this.updateStickyColumnStyles())}_setupStickyStyler(){let e=this._dir?this._dir.value:"ltr",t=this._injector;this._stickyStyler=new mE(this._isNativeHtmlTable,this.stickyCssClass,this._platform.isBrowser,this.needsPositionStickyOnElement,e,this,t),(this._dir?this._dir.change:Ut()).pipe($e(this._onDestroy)).subscribe(i=>{this._stickyStyler.direction=i,this.updateStickyColumnStyles()})}_setupVirtualScrolling(e){let t=typeof requestAnimationFrame<"u"?ol:j4;this.viewChange.next({start:0,end:0}),e.renderedRangeStream.pipe(rl(0,t),$e(this._onDestroy)).subscribe(this.viewChange),e.attach({dataStream:this._dataStream,measureRangeSize:(i,r)=>this._measureRangeSize(i,r)}),da([e.renderedContentOffset,this._headerRowStickyUpdates]).pipe($e(this._onDestroy)).subscribe(([i,r])=>{if(!(!r.sizes||!r.offsets||!r.elements))for(let s=0;s{if(!(!r.sizes||!r.offsets||!r.elements))for(let s=0;s!t._table||t._table===this)}_updateNoDataRow(){let e=this._customNoDataRow||this._noDataRow;if(!e)return;let t=this._rowOutlet.viewContainer.length===0;if(t===this._isShowingNoDataRow)return;let i=this._noDataRowOutlet.viewContainer;if(t){let r=i.createEmbeddedView(e.templateRef),s=r.rootNodes[0];if(r.rootNodes.length===1&&s?.nodeType===this._document.ELEMENT_NODE){s.setAttribute("role","row"),s.classList.add(...e._contentClassNames);let d=s.querySelectorAll(e._cellSelector);for(let m=0;m=e.end||t!=="vertical")return 0;let i=this.viewChange.value,r=this._rowOutlet.viewContainer;e.starti.end;let s=e.start-i.start,d=e.end-e.start,m,u;for(let b=0;b-1;b--){let y=r.get(b+s);if(y&&y.rootNodes.length){u=y.rootNodes[y.rootNodes.length-1];break}}let h=m?.getBoundingClientRect?.(),v=u?.getBoundingClientRect?.();return h&&v?v.bottom-h.top:0}_virtualScrollEnabled(){return!this._disableVirtualScrolling&&this._virtualScrollViewport!=null}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["cdk-table"],["table","cdk-table",""]],contentQueries:function(t,i,r){if(t&1&&Xi(r,_8,5)(r,Fp,5)(r,q1,5)(r,j0,5)(r,gE,5),t&2){let s;gt(s=ht())&&(i._noDataRow=s.first),gt(s=ht())&&(i._contentColumnDefs=s),gt(s=ht())&&(i._contentRowDefs=s),gt(s=ht())&&(i._contentHeaderRowDefs=s),gt(s=ht())&&(i._contentFooterRowDefs=s)}},hostAttrs:[1,"cdk-table"],hostVars:2,hostBindings:function(t,i){t&2&&se("cdk-table-fixed-layout",i.fixedLayout)},inputs:{trackBy:"trackBy",dataSource:"dataSource",multiTemplateDataRows:[2,"multiTemplateDataRows","multiTemplateDataRows",At],fixedLayout:[2,"fixedLayout","fixedLayout",At],recycleRows:[2,"recycleRows","recycleRows",At]},outputs:{contentChanged:"contentChanged"},exportAs:["cdkTable"],features:[un([{provide:oc,useExisting:n},{provide:B0,useValue:null}])],ngContentSelectors:fee,decls:5,vars:2,consts:[["role","rowgroup"],["headerRowOutlet",""],["rowOutlet",""],["noDataRowOutlet",""],["footerRowOutlet",""]],template:function(t,i){t&1&&(ci(_ee),Zt(0),Zt(1,1),E(2,Cee,1,0),E(3,vee,7,0)(4,xee,4,0)),t&2&&(p(2),O(i._isServer?2:-1),p(),O(i._isNativeHtmlTable?3:4))},dependencies:[CE,fE,xE,vE],styles:[`.cdk-table-fixed-layout { + table-layout: fixed; +} +`],encapsulation:2})}return n})();function H1(n,o){return n.concat(Array.from(o))}function m8(n,o){let e=o.toUpperCase(),t=n.viewContainer.element.nativeElement;for(;t;){let i=t.nodeType===1?t.nodeName:null;if(i===e)return t;if(i==="TABLE")break;t=t.parentNode}return null}var Q1=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({imports:[bA]})}return n})();var f8=["*"],bee=["content"],wee=[[["mat-drawer"]],[["mat-drawer-content"]],"*"],Mee=["mat-drawer","mat-drawer-content","*"];function See(n,o){if(n&1){let e=L();a(0,"div",1),_("click",function(){w(e);let i=g();return M(i._onBackdropClicked())}),l()}if(n&2){let e=g();se("mat-drawer-shown",e._isShowingBackdrop())}}function kee(n,o){n&1&&(a(0,"mat-drawer-content"),Zt(1,2),l())}var Pee=new mn("MAT_DRAWER_DEFAULT_AUTOSIZE",{providedIn:"root",factory:()=>!1}),C8=new mn("MAT_DRAWER_CONTAINER"),$0=(()=>{class n extends Lv{_platform=f(Vr);_changeDetectorRef=f(ie);_container=f(wE);constructor(){let e=f(Qt),t=f(Dk),i=f(Ki);super(e,t,i)}ngAfterContentInit(){this._container._contentMarginChanges.subscribe(()=>{this._changeDetectorRef.markForCheck()})}_shouldBeHidden(){if(this._platform.isBrowser)return!1;let{start:e,end:t}=this._container;return e!=null&&e.mode!=="over"&&e.opened||t!=null&&t.mode!=="over"&&t.opened}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["mat-drawer-content"]],hostAttrs:[1,"mat-drawer-content"],hostVars:6,hostBindings:function(t,i){t&2&&(Ot("margin-left",i._container._contentMargins.left,"px")("margin-right",i._container._contentMargins.right,"px"),se("mat-drawer-content-hidden",i._shouldBeHidden()))},features:[un([{provide:Lv,useExisting:n}]),li],ngContentSelectors:f8,decls:1,vars:0,template:function(t,i){t&1&&(ci(),Zt(0))},encapsulation:2,changeDetection:0})}return n})(),bE=(()=>{class n{_elementRef=f(Qt);_focusTrapFactory=f(zA);_focusMonitor=f(ms);_platform=f(Vr);_ngZone=f(Ki);_renderer=f(Ui);_interactivityChecker=f(LA);_doc=f(Eo);_container=f(C8,{optional:!0});_focusTrap=null;_elementFocusedBeforeDrawerWasOpened=null;_eventCleanups;_isAttached=!1;_anchor=null;get position(){return this._position}set position(e){e=e==="end"?"end":"start",e!==this._position&&(this._isAttached&&this._updatePositionInParent(e),this._position=e,this.onPositionChanged.emit())}_position="start";get mode(){return this._mode}set mode(e){this._mode=e,this._updateFocusTrapState(),this._modeChanged.next()}_mode="over";get disableClose(){return this._disableClose}set disableClose(e){this._disableClose=Pa(e)}_disableClose=!1;get autoFocus(){let e=this._autoFocus;return e??(this.mode==="side"?"dialog":"first-tabbable")}set autoFocus(e){(e==="true"||e==="false"||e==null)&&(e=Pa(e)),this._autoFocus=e}_autoFocus;get opened(){return this._opened()}set opened(e){this.toggle(Pa(e))}_opened=R(!1);_openedVia=null;_animationStarted=new Ye;_animationEnd=new Ye;openedChange=new ve(!0);_openedStream=this.openedChange.pipe(oo(e=>e),Pt(()=>{}));openedStart=this._animationStarted.pipe(oo(()=>this.opened),of(void 0));_closedStream=this.openedChange.pipe(oo(e=>!e),Pt(()=>{}));closedStart=this._animationStarted.pipe(oo(()=>!this.opened),of(void 0));_destroyed=new Ye;onPositionChanged=new ve;_content;_modeChanged=new Ye;_injector=f(Cr);_changeDetectorRef=f(ie);constructor(){this.openedChange.pipe($e(this._destroyed)).subscribe(e=>{e?(this._elementFocusedBeforeDrawerWasOpened=this._doc.activeElement,this._takeFocus()):this._isFocusWithinDrawer()&&this._restoreFocus(this._openedVia||"program")}),this._eventCleanups=this._ngZone.runOutsideAngular(()=>{let e=this._renderer,t=this._elementRef.nativeElement;return[e.listen(t,"keydown",i=>{i.keyCode===27&&!this.disableClose&&!ds(i)&&this._ngZone.run(()=>{this.close(),i.stopPropagation(),i.preventDefault()})}),e.listen(t,"transitionend",this._handleTransitionEvent),e.listen(t,"transitioncancel",this._handleTransitionEvent)]}),this._animationEnd.subscribe(()=>{this.openedChange.emit(this.opened)})}_forceFocus(e,t){this._interactivityChecker.isFocusable(e)||(e.tabIndex=-1,this._ngZone.runOutsideAngular(()=>{let i=()=>{r(),s(),e.removeAttribute("tabindex")},r=this._renderer.listen(e,"blur",i),s=this._renderer.listen(e,"mousedown",i)})),e.focus(t)}_focusByCssSelector(e,t){let i=this._elementRef.nativeElement.querySelector(e);i&&this._forceFocus(i,t)}_takeFocus(){if(!this._focusTrap)return;let e=this._elementRef.nativeElement;switch(this.autoFocus){case!1:case"dialog":return;case!0:case"first-tabbable":Kr(()=>{!this._focusTrap.focusInitialElement()&&typeof e.focus=="function"&&e.focus()},{injector:this._injector});break;case"first-heading":this._focusByCssSelector('h1, h2, h3, h4, h5, h6, [role="heading"]');break;default:this._focusByCssSelector(this.autoFocus);break}}_restoreFocus(e){this.autoFocus!=="dialog"&&(this._elementFocusedBeforeDrawerWasOpened?this._focusMonitor.focusVia(this._elementFocusedBeforeDrawerWasOpened,e):this._elementRef.nativeElement.blur(),this._elementFocusedBeforeDrawerWasOpened=null)}_isFocusWithinDrawer(){let e=this._doc.activeElement;return!!e&&this._elementRef.nativeElement.contains(e)}ngAfterViewInit(){this._isAttached=!0,this._position==="end"&&this._updatePositionInParent("end"),this._platform.isBrowser&&(this._focusTrap=this._focusTrapFactory.create(this._elementRef.nativeElement),this._updateFocusTrapState())}ngOnDestroy(){this._eventCleanups.forEach(e=>e()),this._focusTrap?.destroy(),this._anchor?.remove(),this._anchor=null,this._animationStarted.complete(),this._animationEnd.complete(),this._modeChanged.complete(),this._destroyed.next(),this._destroyed.complete()}open(e){return this.toggle(!0,e)}close(){return this.toggle(!1)}_closeViaBackdropClick(){return this._setOpen(!1,!0,"mouse")}toggle(e=!this.opened,t){e&&t&&(this._openedVia=t);let i=this._setOpen(e,!e&&this._isFocusWithinDrawer(),this._openedVia||"program");return e||(this._openedVia=null),i}_setOpen(e,t,i){return e===this.opened?Promise.resolve(e?"open":"close"):(this._opened.set(e),this._container?._transitionsEnabled?(this._setIsAnimating(!0),setTimeout(()=>this._animationStarted.next())):setTimeout(()=>{this._animationStarted.next(),this._animationEnd.next()}),this._elementRef.nativeElement.classList.toggle("mat-drawer-opened",e),!e&&t&&this._restoreFocus(i),this._changeDetectorRef.markForCheck(),this._updateFocusTrapState(),new Promise(r=>{this.openedChange.pipe($a(1)).subscribe(s=>r(s?"open":"close"))}))}_setIsAnimating(e){this._elementRef.nativeElement.classList.toggle("mat-drawer-animating",e)}_getWidth(){return this._elementRef.nativeElement.offsetWidth||0}_updateFocusTrapState(){this._focusTrap&&(this._focusTrap.enabled=this.opened&&!!this._container?._isShowingBackdrop())}_updatePositionInParent(e){if(!this._platform.isBrowser)return;let t=this._elementRef.nativeElement,i=t.parentNode;e==="end"?(this._anchor||(this._anchor=this._doc.createComment("mat-drawer-anchor"),i.insertBefore(this._anchor,t)),i.appendChild(t)):this._anchor&&this._anchor.parentNode.insertBefore(t,this._anchor)}_handleTransitionEvent=e=>{let t=this._elementRef.nativeElement;e.target===t&&this._ngZone.run(()=>{e.type==="transitionend"&&this._setIsAnimating(!1),this._animationEnd.next(e)})};static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["mat-drawer"]],viewQuery:function(t,i){if(t&1&&Gn(bee,5),t&2){let r;gt(r=ht())&&(i._content=r.first)}},hostAttrs:[1,"mat-drawer"],hostVars:12,hostBindings:function(t,i){t&2&&(Ve("align",null)("tabIndex",i.mode!=="side"?"-1":null),Ot("visibility",!i._container&&!i.opened?"hidden":null),se("mat-drawer-end",i.position==="end")("mat-drawer-over",i.mode==="over")("mat-drawer-push",i.mode==="push")("mat-drawer-side",i.mode==="side"))},inputs:{position:"position",mode:"mode",disableClose:"disableClose",autoFocus:"autoFocus",opened:"opened"},outputs:{openedChange:"openedChange",_openedStream:"opened",openedStart:"openedStart",_closedStream:"closed",closedStart:"closedStart",onPositionChanged:"positionChanged"},exportAs:["matDrawer"],ngContentSelectors:f8,decls:3,vars:0,consts:[["content",""],["cdkScrollable","",1,"mat-drawer-inner-container"]],template:function(t,i){t&1&&(ci(),a(0,"div",1,0),Zt(2),l())},dependencies:[Lv],encapsulation:2,changeDetection:0})}return n})(),wE=(()=>{class n{_dir=f(Ua,{optional:!0});_element=f(Qt);_ngZone=f(Ki);_changeDetectorRef=f(ie);_animationDisabled=Br();_transitionsEnabled=!1;_allDrawers;_drawers=new Zl;_content;_userContent;get start(){return this._start}get end(){return this._end}get autosize(){return this._autosize}set autosize(e){this._autosize=Pa(e)}_autosize=f(Pee);get hasBackdrop(){return this._drawerHasBackdrop(this._start)||this._drawerHasBackdrop(this._end)}set hasBackdrop(e){this._backdropOverride=e==null?null:Pa(e)}_backdropOverride=null;backdropClick=new ve;_start=null;_end=null;_left=null;_right=null;_destroyed=new Ye;_doCheckSubject=new Ye;_contentMargins={left:null,right:null};_contentMarginChanges=new Ye;get scrollable(){return this._userContent||this._content}_injector=f(Cr);constructor(){let e=f(Vr),t=f(wd);this._dir?.change.pipe($e(this._destroyed)).subscribe(()=>{this._validateDrawers(),this.updateContentMargins()}),t.change().pipe($e(this._destroyed)).subscribe(()=>this.updateContentMargins()),!this._animationDisabled&&e.isBrowser&&this._ngZone.runOutsideAngular(()=>{setTimeout(()=>{this._element.nativeElement.classList.add("mat-drawer-transition"),this._transitionsEnabled=!0},200)})}ngAfterContentInit(){this._allDrawers.changes.pipe(Oi(this._allDrawers),$e(this._destroyed)).subscribe(e=>{this._drawers.reset(e.filter(t=>!t._container||t._container===this)),this._drawers.notifyOnChanges()}),this._drawers.changes.pipe(Oi(null)).subscribe(()=>{this._validateDrawers(),this._drawers.forEach(e=>{this._watchDrawerToggle(e),this._watchDrawerPosition(e),this._watchDrawerMode(e)}),(!this._drawers.length||this._isDrawerOpen(this._start)||this._isDrawerOpen(this._end))&&this.updateContentMargins(),this._changeDetectorRef.markForCheck()}),this._ngZone.runOutsideAngular(()=>{this._doCheckSubject.pipe(Ju(10),$e(this._destroyed)).subscribe(()=>this.updateContentMargins())})}ngOnDestroy(){this._contentMarginChanges.complete(),this._doCheckSubject.complete(),this._drawers.destroy(),this._destroyed.next(),this._destroyed.complete()}open(){this._drawers.forEach(e=>e.open())}close(){this._drawers.forEach(e=>e.close())}updateContentMargins(){let e=0,t=0;if(this._left&&this._left.opened){if(this._left.mode=="side")e+=this._left._getWidth();else if(this._left.mode=="push"){let i=this._left._getWidth();e+=i,t-=i}}if(this._right&&this._right.opened){if(this._right.mode=="side")t+=this._right._getWidth();else if(this._right.mode=="push"){let i=this._right._getWidth();t+=i,e-=i}}e=e||null,t=t||null,(e!==this._contentMargins.left||t!==this._contentMargins.right)&&(this._contentMargins={left:e,right:t},this._ngZone.run(()=>this._contentMarginChanges.next(this._contentMargins)))}ngDoCheck(){this._autosize&&this._isPushed()&&this._ngZone.runOutsideAngular(()=>this._doCheckSubject.next())}_watchDrawerToggle(e){e._animationStarted.pipe($e(this._drawers.changes)).subscribe(()=>{this.updateContentMargins(),this._changeDetectorRef.markForCheck()}),e.mode!=="side"&&e.openedChange.pipe($e(this._drawers.changes)).subscribe(()=>this._setContainerClass(e.opened))}_watchDrawerPosition(e){e.onPositionChanged.pipe($e(this._drawers.changes)).subscribe(()=>{Kr({read:()=>this._validateDrawers()},{injector:this._injector})})}_watchDrawerMode(e){e._modeChanged.pipe($e(io(this._drawers.changes,this._destroyed))).subscribe(()=>{this.updateContentMargins(),this._changeDetectorRef.markForCheck()})}_setContainerClass(e){let t=this._element.nativeElement.classList,i="mat-drawer-container-has-open";e?t.add(i):t.remove(i)}_validateDrawers(){this._start=this._end=null,this._drawers.forEach(e=>{e.position=="end"?(this._end!=null,this._end=e):(this._start!=null,this._start=e)}),this._right=this._left=null,this._dir&&this._dir.value==="rtl"?(this._left=this._end,this._right=this._start):(this._left=this._start,this._right=this._end)}_isPushed(){return this._isDrawerOpen(this._start)&&this._start.mode!="over"||this._isDrawerOpen(this._end)&&this._end.mode!="over"}_onBackdropClicked(){this.backdropClick.emit(),this._closeModalDrawersViaBackdrop()}_closeModalDrawersViaBackdrop(){[this._start,this._end].filter(e=>e&&!e.disableClose&&this._drawerHasBackdrop(e)).forEach(e=>e._closeViaBackdropClick())}_isShowingBackdrop(){return this._isDrawerOpen(this._start)&&this._drawerHasBackdrop(this._start)||this._isDrawerOpen(this._end)&&this._drawerHasBackdrop(this._end)}_isDrawerOpen(e){return e!=null&&e.opened}_drawerHasBackdrop(e){return this._backdropOverride==null?!!e&&e.mode!=="side":this._backdropOverride}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["mat-drawer-container"]],contentQueries:function(t,i,r){if(t&1&&Xi(r,$0,5)(r,bE,5),t&2){let s;gt(s=ht())&&(i._content=s.first),gt(s=ht())&&(i._allDrawers=s)}},viewQuery:function(t,i){if(t&1&&Gn($0,5),t&2){let r;gt(r=ht())&&(i._userContent=r.first)}},hostAttrs:[1,"mat-drawer-container"],hostVars:2,hostBindings:function(t,i){t&2&&se("mat-drawer-container-explicit-backdrop",i._backdropOverride)},inputs:{autosize:"autosize",hasBackdrop:"hasBackdrop"},outputs:{backdropClick:"backdropClick"},exportAs:["matDrawerContainer"],features:[un([{provide:C8,useExisting:n}])],ngContentSelectors:Mee,decls:4,vars:2,consts:[[1,"mat-drawer-backdrop",3,"mat-drawer-shown"],[1,"mat-drawer-backdrop",3,"click"]],template:function(t,i){t&1&&(ci(wee),E(0,See,1,2,"div",0),Zt(1),Zt(2,1),E(3,kee,2,0,"mat-drawer-content")),t&2&&(O(i.hasBackdrop?0:-1),p(3),O(i._content?-1:3))},dependencies:[$0],styles:[`.mat-drawer-container { + position: relative; + z-index: 1; + color: var(--mat-sidenav-content-text-color, var(--mat-sys-on-background)); + background-color: var(--mat-sidenav-content-background-color, var(--mat-sys-background)); + box-sizing: border-box; + display: block; + overflow: hidden; +} +.mat-drawer-container[fullscreen] { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; +} +.mat-drawer-container[fullscreen].mat-drawer-container-has-open { + overflow: hidden; +} +.mat-drawer-container.mat-drawer-container-explicit-backdrop .mat-drawer-side { + z-index: 3; +} +.mat-drawer-container.ng-animate-disabled .mat-drawer-backdrop, +.mat-drawer-container.ng-animate-disabled .mat-drawer-content, .ng-animate-disabled .mat-drawer-container .mat-drawer-backdrop, +.ng-animate-disabled .mat-drawer-container .mat-drawer-content { + transition: none; +} + +.mat-drawer-backdrop { + top: 0; + left: 0; + right: 0; + bottom: 0; + position: absolute; + display: block; + z-index: 3; + visibility: hidden; +} +.mat-drawer-backdrop.mat-drawer-shown { + visibility: visible; + background-color: var(--mat-sidenav-scrim-color, color-mix(in srgb, var(--mat-sys-neutral-variant20) 40%, transparent)); +} +.mat-drawer-transition .mat-drawer-backdrop { + transition-duration: 400ms; + transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1); + transition-property: background-color, visibility; +} +@media (forced-colors: active) { + .mat-drawer-backdrop { + opacity: 0.5; + } +} + +.mat-drawer-content { + position: relative; + z-index: 1; + display: block; + height: 100%; + overflow: auto; +} +.mat-drawer-content.mat-drawer-content-hidden { + opacity: 0; +} +.mat-drawer-transition .mat-drawer-content { + transition-duration: 400ms; + transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1); + transition-property: transform, margin-left, margin-right; +} + +.mat-drawer { + position: relative; + z-index: 4; + color: var(--mat-sidenav-container-text-color, var(--mat-sys-on-surface-variant)); + box-shadow: var(--mat-sidenav-container-elevation-shadow, none); + background-color: var(--mat-sidenav-container-background-color, var(--mat-sys-surface)); + border-top-right-radius: var(--mat-sidenav-container-shape, var(--mat-sys-corner-large)); + border-bottom-right-radius: var(--mat-sidenav-container-shape, var(--mat-sys-corner-large)); + width: var(--mat-sidenav-container-width, 360px); + display: block; + position: absolute; + top: 0; + bottom: 0; + z-index: 3; + outline: 0; + box-sizing: border-box; + overflow-y: auto; + transform: translate3d(-100%, 0, 0); +} +@media (forced-colors: active) { + .mat-drawer, [dir=rtl] .mat-drawer.mat-drawer-end { + border-right: solid 1px currentColor; + } +} +@media (forced-colors: active) { + [dir=rtl] .mat-drawer, .mat-drawer.mat-drawer-end { + border-left: solid 1px currentColor; + border-right: none; + } +} +.mat-drawer.mat-drawer-side { + z-index: 2; +} +.mat-drawer.mat-drawer-end { + right: 0; + transform: translate3d(100%, 0, 0); + border-top-left-radius: var(--mat-sidenav-container-shape, var(--mat-sys-corner-large)); + border-bottom-left-radius: var(--mat-sidenav-container-shape, var(--mat-sys-corner-large)); + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +[dir=rtl] .mat-drawer { + border-top-left-radius: var(--mat-sidenav-container-shape, var(--mat-sys-corner-large)); + border-bottom-left-radius: var(--mat-sidenav-container-shape, var(--mat-sys-corner-large)); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + transform: translate3d(100%, 0, 0); +} +[dir=rtl] .mat-drawer.mat-drawer-end { + border-top-right-radius: var(--mat-sidenav-container-shape, var(--mat-sys-corner-large)); + border-bottom-right-radius: var(--mat-sidenav-container-shape, var(--mat-sys-corner-large)); + border-top-left-radius: 0; + border-bottom-left-radius: 0; + left: 0; + right: auto; + transform: translate3d(-100%, 0, 0); +} +.mat-drawer-transition .mat-drawer { + transition: transform 400ms cubic-bezier(0.25, 0.8, 0.25, 1); +} +.mat-drawer:not(.mat-drawer-opened):not(.mat-drawer-animating) { + visibility: hidden; + box-shadow: none; +} +.mat-drawer:not(.mat-drawer-opened):not(.mat-drawer-animating) .mat-drawer-inner-container { + display: none; +} +.mat-drawer.mat-drawer-opened.mat-drawer-opened { + transform: none; +} + +.mat-drawer-side { + box-shadow: none; + border-right-color: var(--mat-sidenav-container-divider-color, transparent); + border-right-width: 1px; + border-right-style: solid; +} +.mat-drawer-side.mat-drawer-end { + border-left-color: var(--mat-sidenav-container-divider-color, transparent); + border-left-width: 1px; + border-left-style: solid; + border-right: none; +} +[dir=rtl] .mat-drawer-side { + border-left-color: var(--mat-sidenav-container-divider-color, transparent); + border-left-width: 1px; + border-left-style: solid; + border-right: none; +} +[dir=rtl] .mat-drawer-side.mat-drawer-end { + border-right-color: var(--mat-sidenav-container-divider-color, transparent); + border-right-width: 1px; + border-right-style: solid; + border-left: none; +} + +.mat-drawer-inner-container { + width: 100%; + height: 100%; + overflow: auto; +} + +.mat-sidenav-fixed { + position: fixed; +} +`],encapsulation:2,changeDetection:0})}return n})();var X1=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({imports:[Nm,zi,Nm]})}return n})();var bi=(function(n){return n[n.State=0]="State",n[n.Transition=1]="Transition",n[n.Sequence=2]="Sequence",n[n.Group=3]="Group",n[n.Animate=4]="Animate",n[n.Keyframes=5]="Keyframes",n[n.Style=6]="Style",n[n.Trigger=7]="Trigger",n[n.Reference=8]="Reference",n[n.AnimateChild=9]="AnimateChild",n[n.AnimateRef=10]="AnimateRef",n[n.Query=11]="Query",n[n.Stagger=12]="Stagger",n})(bi||{}),rc="*";function v8(n,o=null){return{type:bi.Sequence,steps:n,options:o}}function ME(n){return{type:bi.Style,styles:n,offset:null}}var Rd=class{_onDoneFns=[];_onStartFns=[];_onDestroyFns=[];_originalOnDoneFns=[];_originalOnStartFns=[];_started=!1;_destroyed=!1;_finished=!1;_position=0;parentPlayer=null;totalTime;constructor(o=0,e=0){this.totalTime=o+e}_onFinish(){this._finished||(this._finished=!0,this._onDoneFns.forEach(o=>o()),this._onDoneFns=[])}onStart(o){this._originalOnStartFns.push(o),this._onStartFns.push(o)}onDone(o){this._originalOnDoneFns.push(o),this._onDoneFns.push(o)}onDestroy(o){this._onDestroyFns.push(o)}hasStarted(){return this._started}init(){}play(){this.hasStarted()||(this._onStart(),this.triggerMicrotask()),this._started=!0}triggerMicrotask(){queueMicrotask(()=>this._onFinish())}_onStart(){this._onStartFns.forEach(o=>o()),this._onStartFns=[]}pause(){}restart(){}finish(){this._onFinish()}destroy(){this._destroyed||(this._destroyed=!0,this.hasStarted()||this._onStart(),this.finish(),this._onDestroyFns.forEach(o=>o()),this._onDestroyFns=[])}reset(){this._started=!1,this._finished=!1,this._onStartFns=this._originalOnStartFns,this._onDoneFns=this._originalOnDoneFns}setPosition(o){this._position=this.totalTime?o*this.totalTime:1}getPosition(){return this.totalTime?this._position/this.totalTime:1}triggerCallback(o){let e=o=="start"?this._onStartFns:this._onDoneFns;e.forEach(t=>t()),e.length=0}},rh=class{_onDoneFns=[];_onStartFns=[];_finished=!1;_started=!1;_destroyed=!1;_onDestroyFns=[];parentPlayer=null;totalTime=0;players;constructor(o){this.players=o;let e=0,t=0,i=0,r=this.players.length;r==0?queueMicrotask(()=>this._onFinish()):this.players.forEach(s=>{s.onDone(()=>{++e==r&&this._onFinish()}),s.onDestroy(()=>{++t==r&&this._onDestroy()}),s.onStart(()=>{++i==r&&this._onStart()})}),this.totalTime=this.players.reduce((s,d)=>Math.max(s,d.totalTime),0)}_onFinish(){this._finished||(this._finished=!0,this._onDoneFns.forEach(o=>o()),this._onDoneFns=[])}init(){this.players.forEach(o=>o.init())}onStart(o){this._onStartFns.push(o)}_onStart(){this.hasStarted()||(this._started=!0,this._onStartFns.forEach(o=>o()),this._onStartFns=[])}onDone(o){this._onDoneFns.push(o)}onDestroy(o){this._onDestroyFns.push(o)}hasStarted(){return this._started}play(){this.parentPlayer||this.init(),this._onStart(),this.players.forEach(o=>o.play())}pause(){this.players.forEach(o=>o.pause())}restart(){this.players.forEach(o=>o.restart())}finish(){this._onFinish(),this.players.forEach(o=>o.finish())}destroy(){this._onDestroy()}_onDestroy(){this._destroyed||(this._destroyed=!0,this._onFinish(),this.players.forEach(o=>o.destroy()),this._onDestroyFns.forEach(o=>o()),this._onDestroyFns=[])}reset(){this.players.forEach(o=>o.reset()),this._destroyed=!1,this._finished=!1,this._started=!1}setPosition(o){let e=o*this.totalTime;this.players.forEach(t=>{let i=t.totalTime?Math.min(1,e/t.totalTime):1;t.setPosition(i)})}getPosition(){let o=this.players.reduce((e,t)=>e===null||t.totalTime>e.totalTime?t:e,null);return o!=null?o.getPosition():0}beforeDestroy(){this.players.forEach(o=>{o.beforeDestroy&&o.beforeDestroy()})}triggerCallback(o){let e=o=="start"?this._onStartFns:this._onDoneFns;e.forEach(t=>t()),e.length=0}},H0="!";function x8(n){return new Di(3e3,!1)}function Eee(){return new Di(3100,!1)}function Oee(){return new Di(3101,!1)}function Dee(n){return new Di(3001,!1)}function Iee(n){return new Di(3003,!1)}function Aee(n){return new Di(3004,!1)}function b8(n,o){return new Di(3005,!1)}function w8(){return new Di(3006,!1)}function M8(){return new Di(3007,!1)}function S8(n,o){return new Di(3008,!1)}function k8(n){return new Di(3002,!1)}function P8(n,o,e,t,i){return new Di(3010,!1)}function T8(){return new Di(3011,!1)}function E8(){return new Di(3012,!1)}function O8(){return new Di(3200,!1)}function D8(){return new Di(3202,!1)}function I8(){return new Di(3013,!1)}function A8(n){return new Di(3014,!1)}function N8(n){return new Di(3015,!1)}function F8(n){return new Di(3016,!1)}function R8(n,o){return new Di(3404,!1)}function Nee(n){return new Di(3502,!1)}function L8(n){return new Di(3503,!1)}function z8(){return new Di(3300,!1)}function V8(n){return new Di(3504,!1)}function B8(n){return new Di(3301,!1)}function j8(n,o){return new Di(3302,!1)}function $8(n){return new Di(3303,!1)}function H8(n,o){return new Di(3400,!1)}function U8(n){return new Di(3401,!1)}function W8(n){return new Di(3402,!1)}function G8(n,o){return new Di(3505,!1)}function Ld(n){switch(n.length){case 0:return new Rd;case 1:return n[0];default:return new rh(n)}}function TE(n,o,e=new Map,t=new Map){let i=[],r=[],s=-1,d=null;if(o.forEach(m=>{let u=m.get("offset"),h=u==s,v=h&&d||new Map;m.forEach((b,y)=>{let x=y,P=b;if(y!=="offset")switch(x=n.normalizePropertyName(x,i),P){case H0:P=e.get(y);break;case rc:P=t.get(y);break;default:P=n.normalizeStyleValue(y,x,P,i);break}v.set(x,P)}),h||r.push(v),d=v,s=u}),i.length)throw Nee(i);return r}function Y1(n,o,e,t){switch(o){case"start":n.onStart(()=>t(e&&SE(e,"start",n)));break;case"done":n.onDone(()=>t(e&&SE(e,"done",n)));break;case"destroy":n.onDestroy(()=>t(e&&SE(e,"destroy",n)));break}}function SE(n,o,e){let t=e.totalTime,i=!!e.disabled,r=Z1(n.element,n.triggerName,n.fromState,n.toState,o||n.phaseName,t??n.totalTime,i),s=n._data;return s!=null&&(r._data=s),r}function Z1(n,o,e,t,i="",r=0,s){return{element:n,triggerName:o,fromState:e,toState:t,phaseName:i,totalTime:r,disabled:!!s}}function Ss(n,o,e){let t=n.get(o);return t||n.set(o,t=e),t}function EE(n){let o=n.indexOf(":"),e=n.substring(1,o),t=n.slice(o+1);return[e,t]}var Fee=typeof document>"u"?null:document.documentElement;function J1(n){let o=n.parentNode||n.host||null;return o===Fee?null:o}function Ree(n){return n.substring(1,6)=="ebkit"}var fu=null,y8=!1;function q8(n){fu||(fu=Lee()||{},y8=fu.style?"WebkitAppearance"in fu.style:!1);let o=!0;return fu.style&&!Ree(n)&&(o=n in fu.style,!o&&y8&&(o="Webkit"+n.charAt(0).toUpperCase()+n.slice(1)in fu.style)),o}function Lee(){return typeof document<"u"?document.body:null}function OE(n,o){for(;o;){if(o===n)return!0;o=J1(o)}return!1}function DE(n,o,e){if(e)return Array.from(n.querySelectorAll(o));let t=n.querySelector(o);return t?[t]:[]}var zee=1e3,IE="{{",Vee="}}",AE="ng-enter",ey="ng-leave",U0="ng-trigger",W0=".ng-trigger",NE="ng-animating",ty=".ng-animating";function Uc(n){if(typeof n=="number")return n;let o=n.match(/^(-?[\.\d]+)(m?s)/);return!o||o.length<2?0:kE(parseFloat(o[1]),o[2])}function kE(n,o){return o==="s"?n*zee:n}function G0(n,o,e){return n.hasOwnProperty("duration")?n:jee(n,o,e)}var Bee=/^(-?[\.\d]+)(m?s)(?:\s+(-?[\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i;function jee(n,o,e){let t,i=0,r="";if(typeof n=="string"){let s=n.match(Bee);if(s===null)return o.push(x8(n)),{duration:0,delay:0,easing:""};t=kE(parseFloat(s[1]),s[2]);let d=s[3];d!=null&&(i=kE(parseFloat(d),s[4]));let m=s[5];m&&(r=m)}else t=n;if(!e){let s=!1,d=o.length;t<0&&(o.push(Eee()),s=!0),i<0&&(o.push(Oee()),s=!0),s&&o.splice(d,0,x8(n))}return{duration:t,delay:i,easing:r}}function Q8(n){return n.length?n[0]instanceof Map?n:n.map(o=>new Map(Object.entries(o))):[]}function ac(n,o,e){o.forEach((t,i)=>{let r=ny(i);e&&!e.has(i)&&e.set(i,n.style[r]),n.style[r]=t})}function Rp(n,o){o.forEach((e,t)=>{let i=ny(t);n.style[i]=""})}function ah(n){return Array.isArray(n)?n.length==1?n[0]:v8(n):n}function K8(n,o,e){let t=o.params||{},i=FE(n);i.length&&i.forEach(r=>{t.hasOwnProperty(r)||e.push(Dee(r))})}var PE=new RegExp(`${IE}\\s*(.+?)\\s*${Vee}`,"g");function FE(n){let o=[];if(typeof n=="string"){let e;for(;e=PE.exec(n);)o.push(e[1]);PE.lastIndex=0}return o}function sh(n,o,e){let t=`${n}`,i=t.replace(PE,(r,s)=>{let d=o[s];return d==null&&(e.push(Iee(s)),d=""),d.toString()});return i==t?n:i}var $ee=/-+([a-z0-9])/g;function ny(n){return n.replace($ee,(...o)=>o[1].toUpperCase())}function X8(n,o){return n===0||o===0}function Y8(n,o,e){if(e.size&&o.length){let t=o[0],i=[];if(e.forEach((r,s)=>{t.has(s)||i.push(s),t.set(s,r)}),i.length)for(let r=1;rs.set(d,iy(n,d)))}}return o}function ks(n,o,e){switch(o.type){case bi.Trigger:return n.visitTrigger(o,e);case bi.State:return n.visitState(o,e);case bi.Transition:return n.visitTransition(o,e);case bi.Sequence:return n.visitSequence(o,e);case bi.Group:return n.visitGroup(o,e);case bi.Animate:return n.visitAnimate(o,e);case bi.Keyframes:return n.visitKeyframes(o,e);case bi.Style:return n.visitStyle(o,e);case bi.Reference:return n.visitReference(o,e);case bi.AnimateChild:return n.visitAnimateChild(o,e);case bi.AnimateRef:return n.visitAnimateRef(o,e);case bi.Query:return n.visitQuery(o,e);case bi.Stagger:return n.visitStagger(o,e);default:throw Aee(o.type)}}function iy(n,o){return window.getComputedStyle(n)[o]}var ZE=(()=>{class n{validateStyleProperty(e){return q8(e)}containsElement(e,t){return OE(e,t)}getParentElement(e){return J1(e)}query(e,t,i){return DE(e,t,i)}computeStyle(e,t,i){return i||""}animate(e,t,i,r,s,d=[],m){return new Rd(i,r)}static \u0275fac=function(t){return new(t||n)};static \u0275prov=J({token:n,factory:n.\u0275fac})}return n})(),vu=class{static NOOP=new ZE},xu=class{};var Hee=new Set(["width","height","minWidth","minHeight","maxWidth","maxHeight","left","top","bottom","right","fontSize","outlineWidth","outlineOffset","paddingTop","paddingLeft","paddingBottom","paddingRight","marginTop","marginLeft","marginBottom","marginRight","borderRadius","borderWidth","borderTopWidth","borderLeftWidth","borderRightWidth","borderBottomWidth","textIndent","perspective"]),ly=class extends xu{normalizePropertyName(o,e){return ny(o)}normalizeStyleValue(o,e,t,i){let r="",s=t.toString().trim();if(Hee.has(e)&&t!==0&&t!=="0")if(typeof t=="number")r="px";else{let d=t.match(/^[+-]?[\d\.]+([a-z]*)$/);d&&d[1].length==0&&i.push(b8(o,t))}return s+r}};var cy="*";function Uee(n,o){let e=[];return typeof n=="string"?n.split(/\s*,\s*/).forEach(t=>Wee(t,e,o)):e.push(n),e}function Wee(n,o,e){if(n[0]==":"){let m=Gee(n,e);if(typeof m=="function"){o.push(m);return}n=m}let t=n.match(/^(\*|[-\w]+)\s*()\s*(\*|[-\w]+)$/);if(t==null||t.length<4)return e.push(N8(n)),o;let i=t[1],r=t[2],s=t[3];o.push(Z8(i,s));let d=i==cy&&s==cy;r[0]=="<"&&!d&&o.push(Z8(s,i))}function Gee(n,o){switch(n){case":enter":return"void => *";case":leave":return"* => void";case":increment":return(e,t)=>parseFloat(t)>parseFloat(e);case":decrement":return(e,t)=>parseFloat(t) *"}}var oy=new Set(["true","1"]),ry=new Set(["false","0"]);function Z8(n,o){let e=oy.has(n)||ry.has(n),t=oy.has(o)||ry.has(o);return(i,r)=>{let s=n==cy||n==i,d=o==cy||o==r;return!s&&e&&typeof i=="boolean"&&(s=i?oy.has(n):ry.has(n)),!d&&t&&typeof r=="boolean"&&(d=r?oy.has(o):ry.has(o)),s&&d}}var lR=":self",qee=new RegExp(`s*${lR}s*,?`,"g");function cR(n,o,e,t){return new jE(n).build(o,e,t)}var J8="",jE=class{_driver;constructor(o){this._driver=o}build(o,e,t){let i=new $E(e);return this._resetContextStyleTimingState(i),ks(this,ah(o),i)}_resetContextStyleTimingState(o){o.currentQuerySelector=J8,o.collectedStyles=new Map,o.collectedStyles.set(J8,new Map),o.currentTime=0}visitTrigger(o,e){let t=e.queryCount=0,i=e.depCount=0,r=[],s=[];return o.name.charAt(0)=="@"&&e.errors.push(w8()),o.definitions.forEach(d=>{if(this._resetContextStyleTimingState(e),d.type==bi.State){let m=d,u=m.name;u.toString().split(/\s*,\s*/).forEach(h=>{m.name=h,r.push(this.visitState(m,e))}),m.name=u}else if(d.type==bi.Transition){let m=this.visitTransition(d,e);t+=m.queryCount,i+=m.depCount,s.push(m)}else e.errors.push(M8())}),{type:bi.Trigger,name:o.name,states:r,transitions:s,queryCount:t,depCount:i,options:null}}visitState(o,e){let t=this.visitStyle(o.styles,e),i=o.options&&o.options.params||null;if(t.containsDynamicStyles){let r=new Set,s=i||{};t.styles.forEach(d=>{d instanceof Map&&d.forEach(m=>{FE(m).forEach(u=>{s.hasOwnProperty(u)||r.add(u)})})}),r.size&&e.errors.push(S8(o.name,[...r.values()]))}return{type:bi.State,name:o.name,style:t,options:i?{params:i}:null}}visitTransition(o,e){e.queryCount=0,e.depCount=0;let t=ks(this,ah(o.animation),e),i=Uee(o.expr,e.errors);return{type:bi.Transition,matchers:i,animation:t,queryCount:e.queryCount,depCount:e.depCount,options:Cu(o.options)}}visitSequence(o,e){return{type:bi.Sequence,steps:o.steps.map(t=>ks(this,t,e)),options:Cu(o.options)}}visitGroup(o,e){let t=e.currentTime,i=0,r=o.steps.map(s=>{e.currentTime=t;let d=ks(this,s,e);return i=Math.max(i,e.currentTime),d});return e.currentTime=i,{type:bi.Group,steps:r,options:Cu(o.options)}}visitAnimate(o,e){let t=Yee(o.timings,e.errors);e.currentAnimateTimings=t;let i,r=o.styles?o.styles:ME({});if(r.type==bi.Keyframes)i=this.visitKeyframes(r,e);else{let s=o.styles,d=!1;if(!s){d=!0;let u={};t.easing&&(u.easing=t.easing),s=ME(u)}e.currentTime+=t.duration+t.delay;let m=this.visitStyle(s,e);m.isEmptyStep=d,i=m}return e.currentAnimateTimings=null,{type:bi.Animate,timings:t,style:i,options:null}}visitStyle(o,e){let t=this._makeStyleAst(o,e);return this._validateStyleAst(t,e),t}_makeStyleAst(o,e){let t=[],i=Array.isArray(o.styles)?o.styles:[o.styles];for(let d of i)typeof d=="string"?d===rc?t.push(d):e.errors.push(k8(d)):t.push(new Map(Object.entries(d)));let r=!1,s=null;return t.forEach(d=>{if(d instanceof Map&&(d.has("easing")&&(s=d.get("easing"),d.delete("easing")),!r)){for(let m of d.values())if(m.toString().indexOf(IE)>=0){r=!0;break}}}),{type:bi.Style,styles:t,easing:s,offset:o.offset,containsDynamicStyles:r,options:null}}_validateStyleAst(o,e){let t=e.currentAnimateTimings,i=e.currentTime,r=e.currentTime;t&&r>0&&(r-=t.duration+t.delay),o.styles.forEach(s=>{typeof s!="string"&&s.forEach((d,m)=>{let u=e.collectedStyles.get(e.currentQuerySelector),h=u.get(m),v=!0;h&&(r!=i&&r>=h.startTime&&i<=h.endTime&&(e.errors.push(P8(m,h.startTime,h.endTime,r,i)),v=!1),r=h.startTime),v&&u.set(m,{startTime:r,endTime:i}),e.options&&K8(d,e.options,e.errors)})})}visitKeyframes(o,e){let t={type:bi.Keyframes,styles:[],options:null};if(!e.currentAnimateTimings)return e.errors.push(T8()),t;let i=1,r=0,s=[],d=!1,m=!1,u=0,h=o.steps.map(S=>{let T=this._makeStyleAst(S,e),A=T.offset!=null?T.offset:Xee(T.styles),D=0;return A!=null&&(r++,D=T.offset=A),m=m||D<0||D>1,d=d||D0&&r{let A=b>0?T==y?1:b*T:s[T],D=A*k;e.currentTime=x+P.delay+D,P.duration=D,this._validateStyleAst(S,e),S.offset=A,t.styles.push(S)}),t}visitReference(o,e){return{type:bi.Reference,animation:ks(this,ah(o.animation),e),options:Cu(o.options)}}visitAnimateChild(o,e){return e.depCount++,{type:bi.AnimateChild,options:Cu(o.options)}}visitAnimateRef(o,e){return{type:bi.AnimateRef,animation:this.visitReference(o.animation,e),options:Cu(o.options)}}visitQuery(o,e){let t=e.currentQuerySelector,i=o.options||{};e.queryCount++,e.currentQuery=o;let[r,s]=Qee(o.selector);e.currentQuerySelector=t.length?t+" "+r:r,Ss(e.collectedStyles,e.currentQuerySelector,new Map);let d=ks(this,ah(o.animation),e);return e.currentQuery=null,e.currentQuerySelector=t,{type:bi.Query,selector:r,limit:i.limit||0,optional:!!i.optional,includeSelf:s,animation:d,originalSelector:o.selector,options:Cu(o.options)}}visitStagger(o,e){e.currentQuery||e.errors.push(I8());let t=o.timings==="full"?{duration:0,delay:0,easing:"full"}:G0(o.timings,e.errors,!0);return{type:bi.Stagger,animation:ks(this,ah(o.animation),e),timings:t,options:null}}};function Qee(n){let o=!!n.split(/\s*,\s*/).find(e=>e==lR);return o&&(n=n.replace(qee,"")),n=n.replace(/@\*/g,W0).replace(/@\w+/g,e=>W0+"-"+e.slice(1)).replace(/:animating/g,ty),[n,o]}function Kee(n){return n?q({},n):null}var $E=class{errors;queryCount=0;depCount=0;currentTransition=null;currentQuery=null;currentQuerySelector=null;currentAnimateTimings=null;currentTime=0;collectedStyles=new Map;options=null;unsupportedCSSPropertiesFound=new Set;constructor(o){this.errors=o}};function Xee(n){if(typeof n=="string")return null;let o=null;if(Array.isArray(n))n.forEach(e=>{if(e instanceof Map&&e.has("offset")){let t=e;o=parseFloat(t.get("offset")),t.delete("offset")}});else if(n instanceof Map&&n.has("offset")){let e=n;o=parseFloat(e.get("offset")),e.delete("offset")}return o}function Yee(n,o){if(n.hasOwnProperty("duration"))return n;if(typeof n=="number"){let r=G0(n,o).duration;return RE(r,0,"")}let e=n;if(e.split(/\s+/).some(r=>r.charAt(0)=="{"&&r.charAt(1)=="{")){let r=RE(0,0,"");return r.dynamic=!0,r.strValue=e,r}let i=G0(e,o);return RE(i.duration,i.delay,i.easing)}function Cu(n){return n?(n=q({},n),n.params&&(n.params=Kee(n.params))):n={},n}function RE(n,o,e){return{duration:n,delay:o,easing:e}}function JE(n,o,e,t,i,r,s=null,d=!1){return{type:1,element:n,keyframes:o,preStyleProps:e,postStyleProps:t,duration:i,delay:r,totalTime:i+r,easing:s,subTimeline:d}}var Q0=class{_map=new Map;get(o){return this._map.get(o)||[]}append(o,e){let t=this._map.get(o);t||this._map.set(o,t=[]),t.push(...e)}has(o){return this._map.has(o)}clear(){this._map.clear()}},Zee=1,Jee=":enter",ete=new RegExp(Jee,"g"),tte=":leave",nte=new RegExp(tte,"g");function dR(n,o,e,t,i,r=new Map,s=new Map,d,m,u=[]){return new HE().buildKeyframes(n,o,e,t,i,r,s,d,m,u)}var HE=class{buildKeyframes(o,e,t,i,r,s,d,m,u,h=[]){u=u||new Q0;let v=new UE(o,e,u,i,r,h,[]);v.options=m;let b=m.delay?Uc(m.delay):0;v.currentTimeline.delayNextStep(b),v.currentTimeline.setStyles([s],null,v.errors,m),ks(this,t,v);let y=v.timelines.filter(x=>x.containsAnimation());if(y.length&&d.size){let x;for(let P=y.length-1;P>=0;P--){let k=y[P];if(k.element===e){x=k;break}}x&&!x.allowOnlyTimelineStyles()&&x.setStyles([d],null,v.errors,m)}return y.length?y.map(x=>x.buildKeyframes()):[JE(e,[],[],[],0,b,"",!1)]}visitTrigger(o,e){}visitState(o,e){}visitTransition(o,e){}visitAnimateChild(o,e){let t=e.subInstructions.get(e.element);if(t){let i=e.createSubContext(o.options),r=e.currentTimeline.currentTime,s=this._visitSubInstructions(t,i,i.options);r!=s&&e.transformIntoNewTimeline(s)}e.previousNode=o}visitAnimateRef(o,e){let t=e.createSubContext(o.options);t.transformIntoNewTimeline(),this._applyAnimationRefDelays([o.options,o.animation.options],e,t),this.visitReference(o.animation,t),e.transformIntoNewTimeline(t.currentTimeline.currentTime),e.previousNode=o}_applyAnimationRefDelays(o,e,t){for(let i of o){let r=i?.delay;if(r){let s=typeof r=="number"?r:Uc(sh(r,i?.params??{},e.errors));t.delayNextStep(s)}}}_visitSubInstructions(o,e,t){let r=e.currentTimeline.currentTime,s=t.duration!=null?Uc(t.duration):null,d=t.delay!=null?Uc(t.delay):null;return s!==0&&o.forEach(m=>{let u=e.appendInstructionToTimeline(m,s,d);r=Math.max(r,u.duration+u.delay)}),r}visitReference(o,e){e.updateOptions(o.options,!0),ks(this,o.animation,e),e.previousNode=o}visitSequence(o,e){let t=e.subContextCount,i=e,r=o.options;if(r&&(r.params||r.delay)&&(i=e.createSubContext(r),i.transformIntoNewTimeline(),r.delay!=null)){i.previousNode.type==bi.Style&&(i.currentTimeline.snapshotCurrentStyles(),i.previousNode=dy);let s=Uc(r.delay);i.delayNextStep(s)}o.steps.length&&(o.steps.forEach(s=>ks(this,s,i)),i.currentTimeline.applyStylesToKeyframe(),i.subContextCount>t&&i.transformIntoNewTimeline()),e.previousNode=o}visitGroup(o,e){let t=[],i=e.currentTimeline.currentTime,r=o.options&&o.options.delay?Uc(o.options.delay):0;o.steps.forEach(s=>{let d=e.createSubContext(o.options);r&&d.delayNextStep(r),ks(this,s,d),i=Math.max(i,d.currentTimeline.currentTime),t.push(d.currentTimeline)}),t.forEach(s=>e.currentTimeline.mergeTimelineCollectedStyles(s)),e.transformIntoNewTimeline(i),e.previousNode=o}_visitTiming(o,e){if(o.dynamic){let t=o.strValue,i=e.params?sh(t,e.params,e.errors):t;return G0(i,e.errors)}else return{duration:o.duration,delay:o.delay,easing:o.easing}}visitAnimate(o,e){let t=e.currentAnimateTimings=this._visitTiming(o.timings,e),i=e.currentTimeline;t.delay&&(e.incrementTime(t.delay),i.snapshotCurrentStyles());let r=o.style;r.type==bi.Keyframes?this.visitKeyframes(r,e):(e.incrementTime(t.duration),this.visitStyle(r,e),i.applyStylesToKeyframe()),e.currentAnimateTimings=null,e.previousNode=o}visitStyle(o,e){let t=e.currentTimeline,i=e.currentAnimateTimings;!i&&t.hasCurrentStyleProperties()&&t.forwardFrame();let r=i&&i.easing||o.easing;o.isEmptyStep?t.applyEmptyStep(r):t.setStyles(o.styles,r,e.errors,e.options),e.previousNode=o}visitKeyframes(o,e){let t=e.currentAnimateTimings,i=e.currentTimeline.duration,r=t.duration,d=e.createSubContext().currentTimeline;d.easing=t.easing,o.styles.forEach(m=>{let u=m.offset||0;d.forwardTime(u*r),d.setStyles(m.styles,m.easing,e.errors,e.options),d.applyStylesToKeyframe()}),e.currentTimeline.mergeTimelineCollectedStyles(d),e.transformIntoNewTimeline(i+r),e.previousNode=o}visitQuery(o,e){let t=e.currentTimeline.currentTime,i=o.options||{},r=i.delay?Uc(i.delay):0;r&&(e.previousNode.type===bi.Style||t==0&&e.currentTimeline.hasCurrentStyleProperties())&&(e.currentTimeline.snapshotCurrentStyles(),e.previousNode=dy);let s=t,d=e.invokeQuery(o.selector,o.originalSelector,o.limit,o.includeSelf,!!i.optional,e.errors);e.currentQueryTotal=d.length;let m=null;d.forEach((u,h)=>{e.currentQueryIndex=h;let v=e.createSubContext(o.options,u);r&&v.delayNextStep(r),u===e.element&&(m=v.currentTimeline),ks(this,o.animation,v),v.currentTimeline.applyStylesToKeyframe();let b=v.currentTimeline.currentTime;s=Math.max(s,b)}),e.currentQueryIndex=0,e.currentQueryTotal=0,e.transformIntoNewTimeline(s),m&&(e.currentTimeline.mergeTimelineCollectedStyles(m),e.currentTimeline.snapshotCurrentStyles()),e.previousNode=o}visitStagger(o,e){let t=e.parentContext,i=e.currentTimeline,r=o.timings,s=Math.abs(r.duration),d=s*(e.currentQueryTotal-1),m=s*e.currentQueryIndex;switch(r.duration<0?"reverse":r.easing){case"reverse":m=d-m;break;case"full":m=t.currentStaggerTime;break}let h=e.currentTimeline;m&&h.delayNextStep(m);let v=h.currentTime;ks(this,o.animation,e),e.previousNode=o,t.currentStaggerTime=i.currentTime-v+(i.startTime-t.currentTimeline.startTime)}},dy={},UE=class n{_driver;element;subInstructions;_enterClassName;_leaveClassName;errors;timelines;parentContext=null;currentTimeline;currentAnimateTimings=null;previousNode=dy;subContextCount=0;options={};currentQueryIndex=0;currentQueryTotal=0;currentStaggerTime=0;constructor(o,e,t,i,r,s,d,m){this._driver=o,this.element=e,this.subInstructions=t,this._enterClassName=i,this._leaveClassName=r,this.errors=s,this.timelines=d,this.currentTimeline=m||new py(this._driver,e,0),d.push(this.currentTimeline)}get params(){return this.options.params}updateOptions(o,e){if(!o)return;let t=o,i=this.options;t.duration!=null&&(i.duration=Uc(t.duration)),t.delay!=null&&(i.delay=Uc(t.delay));let r=t.params;if(r){let s=i.params;s||(s=this.options.params={}),Object.keys(r).forEach(d=>{(!e||!s.hasOwnProperty(d))&&(s[d]=sh(r[d],s,this.errors))})}}_copyOptions(){let o={};if(this.options){let e=this.options.params;if(e){let t=o.params={};Object.keys(e).forEach(i=>{t[i]=e[i]})}}return o}createSubContext(o=null,e,t){let i=e||this.element,r=new n(this._driver,i,this.subInstructions,this._enterClassName,this._leaveClassName,this.errors,this.timelines,this.currentTimeline.fork(i,t||0));return r.previousNode=this.previousNode,r.currentAnimateTimings=this.currentAnimateTimings,r.options=this._copyOptions(),r.updateOptions(o),r.currentQueryIndex=this.currentQueryIndex,r.currentQueryTotal=this.currentQueryTotal,r.parentContext=this,this.subContextCount++,r}transformIntoNewTimeline(o){return this.previousNode=dy,this.currentTimeline=this.currentTimeline.fork(this.element,o),this.timelines.push(this.currentTimeline),this.currentTimeline}appendInstructionToTimeline(o,e,t){let i={duration:e??o.duration,delay:this.currentTimeline.currentTime+(t??0)+o.delay,easing:""},r=new WE(this._driver,o.element,o.keyframes,o.preStyleProps,o.postStyleProps,i,o.stretchStartingKeyframe);return this.timelines.push(r),i}incrementTime(o){this.currentTimeline.forwardTime(this.currentTimeline.duration+o)}delayNextStep(o){o>0&&this.currentTimeline.delayNextStep(o)}invokeQuery(o,e,t,i,r,s){let d=[];if(i&&d.push(this.element),o.length>0){o=o.replace(ete,"."+this._enterClassName),o=o.replace(nte,"."+this._leaveClassName);let m=t!=1,u=this._driver.query(this.element,o,m);t!==0&&(u=t<0?u.slice(u.length+t,u.length):u.slice(0,t)),d.push(...u)}return!r&&d.length==0&&s.push(A8(e)),d}},py=class n{_driver;element;startTime;_elementTimelineStylesLookup;duration=0;easing=null;_previousKeyframe=new Map;_currentKeyframe=new Map;_keyframes=new Map;_styleSummary=new Map;_localTimelineStyles=new Map;_globalTimelineStyles;_pendingStyles=new Map;_backFill=new Map;_currentEmptyStepKeyframe=null;constructor(o,e,t,i){this._driver=o,this.element=e,this.startTime=t,this._elementTimelineStylesLookup=i,this._elementTimelineStylesLookup||(this._elementTimelineStylesLookup=new Map),this._globalTimelineStyles=this._elementTimelineStylesLookup.get(e),this._globalTimelineStyles||(this._globalTimelineStyles=this._localTimelineStyles,this._elementTimelineStylesLookup.set(e,this._localTimelineStyles)),this._loadKeyframe()}containsAnimation(){switch(this._keyframes.size){case 0:return!1;case 1:return this.hasCurrentStyleProperties();default:return!0}}hasCurrentStyleProperties(){return this._currentKeyframe.size>0}get currentTime(){return this.startTime+this.duration}delayNextStep(o){let e=this._keyframes.size===1&&this._pendingStyles.size;this.duration||e?(this.forwardTime(this.currentTime+o),e&&this.snapshotCurrentStyles()):this.startTime+=o}fork(o,e){return this.applyStylesToKeyframe(),new n(this._driver,o,e||this.currentTime,this._elementTimelineStylesLookup)}_loadKeyframe(){this._currentKeyframe&&(this._previousKeyframe=this._currentKeyframe),this._currentKeyframe=this._keyframes.get(this.duration),this._currentKeyframe||(this._currentKeyframe=new Map,this._keyframes.set(this.duration,this._currentKeyframe))}forwardFrame(){this.duration+=Zee,this._loadKeyframe()}forwardTime(o){this.applyStylesToKeyframe(),this.duration=o,this._loadKeyframe()}_updateStyle(o,e){this._localTimelineStyles.set(o,e),this._globalTimelineStyles.set(o,e),this._styleSummary.set(o,{time:this.currentTime,value:e})}allowOnlyTimelineStyles(){return this._currentEmptyStepKeyframe!==this._currentKeyframe}applyEmptyStep(o){o&&this._previousKeyframe.set("easing",o);for(let[e,t]of this._globalTimelineStyles)this._backFill.set(e,t||rc),this._currentKeyframe.set(e,rc);this._currentEmptyStepKeyframe=this._currentKeyframe}setStyles(o,e,t,i){e&&this._previousKeyframe.set("easing",e);let r=i&&i.params||{},s=ite(o,this._globalTimelineStyles);for(let[d,m]of s){let u=sh(m,r,t);this._pendingStyles.set(d,u),this._localTimelineStyles.has(d)||this._backFill.set(d,this._globalTimelineStyles.get(d)??rc),this._updateStyle(d,u)}}applyStylesToKeyframe(){this._pendingStyles.size!=0&&(this._pendingStyles.forEach((o,e)=>{this._currentKeyframe.set(e,o)}),this._pendingStyles.clear(),this._localTimelineStyles.forEach((o,e)=>{this._currentKeyframe.has(e)||this._currentKeyframe.set(e,o)}))}snapshotCurrentStyles(){for(let[o,e]of this._localTimelineStyles)this._pendingStyles.set(o,e),this._updateStyle(o,e)}getFinalKeyframe(){return this._keyframes.get(this.duration)}get properties(){let o=[];for(let e in this._currentKeyframe)o.push(e);return o}mergeTimelineCollectedStyles(o){o._styleSummary.forEach((e,t)=>{let i=this._styleSummary.get(t);(!i||e.time>i.time)&&this._updateStyle(t,e.value)})}buildKeyframes(){this.applyStylesToKeyframe();let o=new Set,e=new Set,t=this._keyframes.size===1&&this.duration===0,i=[];this._keyframes.forEach((d,m)=>{let u=new Map([...this._backFill,...d]);u.forEach((h,v)=>{h===H0?o.add(v):h===rc&&e.add(v)}),t||u.set("offset",m/this.duration),i.push(u)});let r=[...o.values()],s=[...e.values()];if(t){let d=i[0],m=new Map(d);d.set("offset",0),m.set("offset",1),i=[d,m]}return JE(this.element,i,r,s,this.duration,this.startTime,this.easing,!1)}},WE=class extends py{keyframes;preStyleProps;postStyleProps;_stretchStartingKeyframe;timings;constructor(o,e,t,i,r,s,d=!1){super(o,e,s.delay),this.keyframes=t,this.preStyleProps=i,this.postStyleProps=r,this._stretchStartingKeyframe=d,this.timings={duration:s.duration,delay:s.delay,easing:s.easing}}containsAnimation(){return this.keyframes.length>1}buildKeyframes(){let o=this.keyframes,{delay:e,duration:t,easing:i}=this.timings;if(this._stretchStartingKeyframe&&e){let r=[],s=t+e,d=e/s,m=new Map(o[0]);m.set("offset",0),r.push(m);let u=new Map(o[0]);u.set("offset",eR(d)),r.push(u);let h=o.length-1;for(let v=1;v<=h;v++){let b=new Map(o[v]),y=b.get("offset"),x=e+y*t;b.set("offset",eR(x/s)),r.push(b)}t=s,e=0,i="",o=r}return JE(this.element,o,this.preStyleProps,this.postStyleProps,t,e,i,!0)}};function eR(n,o=3){let e=Math.pow(10,o-1);return Math.round(n*e)/e}function ite(n,o){let e=new Map,t;return n.forEach(i=>{if(i==="*"){t??=o.keys();for(let r of t)e.set(r,rc)}else for(let[r,s]of i)e.set(r,s)}),e}function tR(n,o,e,t,i,r,s,d,m,u,h,v,b){return{type:0,element:n,triggerName:o,isRemovalTransition:i,fromState:e,fromStyles:r,toState:t,toStyles:s,timelines:d,queriedElements:m,preStyleProps:u,postStyleProps:h,totalTime:v,errors:b}}var LE={},my=class{_triggerName;ast;_stateStyles;constructor(o,e,t){this._triggerName=o,this.ast=e,this._stateStyles=t}match(o,e,t,i){return ote(this.ast.matchers,o,e,t,i)}buildStyles(o,e,t){let i=this._stateStyles.get("*");return o!==void 0&&(i=this._stateStyles.get(o?.toString())||i),i?i.buildStyles(e,t):new Map}build(o,e,t,i,r,s,d,m,u,h){let v=[],b=this.ast.options&&this.ast.options.params||LE,y=d&&d.params||LE,x=this.buildStyles(t,y,v),P=m&&m.params||LE,k=this.buildStyles(i,P,v),S=new Set,T=new Map,A=new Map,D=i==="void",F={params:pR(P,b),delay:this.ast.options?.delay},I=h?[]:dR(o,e,this.ast.animation,r,s,x,k,F,u,v),V=0;return I.forEach(ae=>{V=Math.max(ae.duration+ae.delay,V)}),v.length?tR(e,this._triggerName,t,i,D,x,k,[],[],T,A,V,v):(I.forEach(ae=>{let oe=ae.element,W=Ss(T,oe,new Set);ae.preStyleProps.forEach(Ce=>W.add(Ce));let G=Ss(A,oe,new Set);ae.postStyleProps.forEach(Ce=>G.add(Ce)),oe!==e&&S.add(oe)}),tR(e,this._triggerName,t,i,D,x,k,I,[...S.values()],T,A,V))}};function ote(n,o,e,t,i){return n.some(r=>r(o,e,t,i))}function pR(n,o){let e=q({},o);return Object.entries(n).forEach(([t,i])=>{i!=null&&(e[t]=i)}),e}var GE=class{styles;defaultParams;normalizer;constructor(o,e,t){this.styles=o,this.defaultParams=e,this.normalizer=t}buildStyles(o,e){let t=new Map,i=pR(o,this.defaultParams);return this.styles.styles.forEach(r=>{typeof r!="string"&&r.forEach((s,d)=>{s&&(s=sh(s,i,e));let m=this.normalizer.normalizePropertyName(d,e);s=this.normalizer.normalizeStyleValue(d,m,s,e),t.set(d,s)})}),t}};function rte(n,o,e){return new qE(n,o,e)}var qE=class{name;ast;_normalizer;transitionFactories=[];fallbackTransition;states=new Map;constructor(o,e,t){this.name=o,this.ast=e,this._normalizer=t,e.states.forEach(i=>{let r=i.options&&i.options.params||{};this.states.set(i.name,new GE(i.style,r,t))}),nR(this.states,"true","1"),nR(this.states,"false","0"),e.transitions.forEach(i=>{this.transitionFactories.push(new my(o,i,this.states))}),this.fallbackTransition=ate(o,this.states)}get containsQueries(){return this.ast.queryCount>0}matchTransition(o,e,t,i){return this.transitionFactories.find(s=>s.match(o,e,t,i))||null}matchStyles(o,e,t){return this.fallbackTransition.buildStyles(o,e,t)}};function ate(n,o,e){let t=[(s,d)=>!0],i={type:bi.Sequence,steps:[],options:null},r={type:bi.Transition,animation:i,matchers:t,options:null,queryCount:0,depCount:0};return new my(n,r,o)}function nR(n,o,e){n.has(o)?n.has(e)||n.set(e,n.get(o)):n.has(e)&&n.set(o,n.get(e))}var ste=new Q0,QE=class{bodyNode;_driver;_normalizer;_animations=new Map;_playersById=new Map;players=[];constructor(o,e,t){this.bodyNode=o,this._driver=e,this._normalizer=t}register(o,e){let t=[],i=[],r=cR(this._driver,e,t,i);if(t.length)throw L8(t);this._animations.set(o,r)}_buildPlayer(o,e,t){let i=o.element,r=TE(this._normalizer,o.keyframes,e,t);return this._driver.animate(i,r,o.duration,o.delay,o.easing,[],!0)}create(o,e,t={}){let i=[],r=this._animations.get(o),s,d=new Map;if(r?(s=dR(this._driver,e,r,AE,ey,new Map,new Map,t,ste,i),s.forEach(h=>{let v=Ss(d,h.element,new Map);h.postStyleProps.forEach(b=>v.set(b,null))})):(i.push(z8()),s=[]),i.length)throw V8(i);d.forEach((h,v)=>{h.forEach((b,y)=>{h.set(y,this._driver.computeStyle(v,y,rc))})});let m=s.map(h=>{let v=d.get(h.element);return this._buildPlayer(h,new Map,v)}),u=Ld(m);return this._playersById.set(o,u),u.onDestroy(()=>this.destroy(o)),this.players.push(u),u}destroy(o){let e=this._getPlayer(o);e.destroy(),this._playersById.delete(o);let t=this.players.indexOf(e);t>=0&&this.players.splice(t,1)}_getPlayer(o){let e=this._playersById.get(o);if(!e)throw B8(o);return e}listen(o,e,t,i){let r=Z1(e,"","","");return Y1(this._getPlayer(o),t,r,i),()=>{}}command(o,e,t,i){if(t=="register"){this.register(o,i[0]);return}if(t=="create"){let s=i[0]||{};this.create(o,e,s);return}let r=this._getPlayer(o);switch(t){case"play":r.play();break;case"pause":r.pause();break;case"reset":r.reset();break;case"restart":r.restart();break;case"finish":r.finish();break;case"init":r.init();break;case"setPosition":r.setPosition(parseFloat(i[0]));break;case"destroy":this.destroy(o);break}}},iR="ng-animate-queued",lte=".ng-animate-queued",zE="ng-animate-disabled",cte=".ng-animate-disabled",dte="ng-star-inserted",pte=".ng-star-inserted",mte=[],mR={namespaceId:"",setForRemoval:!1,setForMove:!1,hasAnimation:!1,removedBeforeQueried:!1},ute={namespaceId:"",setForMove:!1,setForRemoval:!1,hasAnimation:!1,removedBeforeQueried:!0},sc="__ng_removed",K0=class{namespaceId;value;options;get params(){return this.options.params}constructor(o,e=""){this.namespaceId=e;let t=o&&o.hasOwnProperty("value"),i=t?o.value:o;if(this.value=hte(i),t){let r=o,{value:s}=r,d=Mv(r,["value"]);this.options=d}else this.options={};this.options.params||(this.options.params={})}absorbOptions(o){let e=o.params;if(e){let t=this.options.params;Object.keys(e).forEach(i=>{t[i]==null&&(t[i]=e[i])})}}},q0="void",VE=new K0(q0),KE=class{id;hostElement;_engine;players=[];_triggers=new Map;_queue=[];_elementListeners=new Map;_hostClassName;constructor(o,e,t){this.id=o,this.hostElement=e,this._engine=t,this._hostClassName="ng-tns-"+o,Ml(e,this._hostClassName)}listen(o,e,t,i){if(!this._triggers.has(e))throw j8(t,e);if(t==null||t.length==0)throw $8(e);if(!_te(t))throw H8(t,e);let r=Ss(this._elementListeners,o,[]),s={name:e,phase:t,callback:i};r.push(s);let d=Ss(this._engine.statesByElement,o,new Map);return d.has(e)||(Ml(o,U0),Ml(o,U0+"-"+e),d.set(e,VE)),()=>{this._engine.afterFlush(()=>{let m=r.indexOf(s);m>=0&&r.splice(m,1),this._triggers.has(e)||d.delete(e)})}}register(o,e){return this._triggers.has(o)?!1:(this._triggers.set(o,e),!0)}_getTrigger(o){let e=this._triggers.get(o);if(!e)throw U8(o);return e}trigger(o,e,t,i=!0){let r=this._getTrigger(e),s=new X0(this.id,e,o),d=this._engine.statesByElement.get(o);d||(Ml(o,U0),Ml(o,U0+"-"+e),this._engine.statesByElement.set(o,d=new Map));let m=d.get(e),u=new K0(t,this.id);if(!(t&&t.hasOwnProperty("value"))&&m&&u.absorbOptions(m.options),d.set(e,u),m||(m=VE),!(u.value===q0)&&m.value===u.value){if(!vte(m.params,u.params)){let P=[],k=r.matchStyles(m.value,m.params,P),S=r.matchStyles(u.value,u.params,P);P.length?this._engine.reportError(P):this._engine.afterFlush(()=>{Rp(o,k),ac(o,S)})}return}let b=Ss(this._engine.playersByElement,o,[]);b.forEach(P=>{P.namespaceId==this.id&&P.triggerName==e&&P.queued&&P.destroy()});let y=r.matchTransition(m.value,u.value,o,u.params),x=!1;if(!y){if(!i)return;y=r.fallbackTransition,x=!0}return this._engine.totalQueuedPlayers++,this._queue.push({element:o,triggerName:e,transition:y,fromState:m,toState:u,player:s,isFallbackTransition:x}),x||(Ml(o,iR),s.onStart(()=>{lh(o,iR)})),s.onDone(()=>{let P=this.players.indexOf(s);P>=0&&this.players.splice(P,1);let k=this._engine.playersByElement.get(o);if(k){let S=k.indexOf(s);S>=0&&k.splice(S,1)}}),this.players.push(s),b.push(s),s}deregister(o){this._triggers.delete(o),this._engine.statesByElement.forEach(e=>e.delete(o)),this._elementListeners.forEach((e,t)=>{this._elementListeners.set(t,e.filter(i=>i.name!=o))})}clearElementCache(o){this._engine.statesByElement.delete(o),this._elementListeners.delete(o);let e=this._engine.playersByElement.get(o);e&&(e.forEach(t=>t.destroy()),this._engine.playersByElement.delete(o))}_signalRemovalForInnerTriggers(o,e){let t=this._engine.driver.query(o,W0,!0);t.forEach(i=>{if(i[sc])return;let r=this._engine.fetchNamespacesByElement(i);r.size?r.forEach(s=>s.triggerLeaveAnimation(i,e,!1,!0)):this.clearElementCache(i)}),this._engine.afterFlushAnimationsDone(()=>t.forEach(i=>this.clearElementCache(i)))}triggerLeaveAnimation(o,e,t,i){let r=this._engine.statesByElement.get(o),s=new Map;if(r){let d=[];if(r.forEach((m,u)=>{if(s.set(u,m.value),this._triggers.has(u)){let h=this.trigger(o,u,q0,i);h&&d.push(h)}}),d.length)return this._engine.markElementAsRemoved(this.id,o,!0,e,s),t&&Ld(d).onDone(()=>this._engine.processLeaveNode(o)),!0}return!1}prepareLeaveAnimationListeners(o){let e=this._elementListeners.get(o),t=this._engine.statesByElement.get(o);if(e&&t){let i=new Set;e.forEach(r=>{let s=r.name;if(i.has(s))return;i.add(s);let m=this._triggers.get(s).fallbackTransition,u=t.get(s)||VE,h=new K0(q0),v=new X0(this.id,s,o);this._engine.totalQueuedPlayers++,this._queue.push({element:o,triggerName:s,transition:m,fromState:u,toState:h,player:v,isFallbackTransition:!0})})}}removeNode(o,e){let t=this._engine;if(o.childElementCount&&this._signalRemovalForInnerTriggers(o,e),this.triggerLeaveAnimation(o,e,!0))return;let i=!1;if(t.totalAnimations){let r=t.players.length?t.playersByQueriedElement.get(o):[];if(r&&r.length)i=!0;else{let s=o;for(;s=s.parentNode;)if(t.statesByElement.get(s)){i=!0;break}}}if(this.prepareLeaveAnimationListeners(o),i)t.markElementAsRemoved(this.id,o,!1,e);else{let r=o[sc];(!r||r===mR)&&(t.afterFlush(()=>this.clearElementCache(o)),t.destroyInnerAnimations(o),t._onRemovalComplete(o,e))}}insertNode(o,e){Ml(o,this._hostClassName)}drainQueuedTransitions(o){let e=[];return this._queue.forEach(t=>{let i=t.player;if(i.destroyed)return;let r=t.element,s=this._elementListeners.get(r);s&&s.forEach(d=>{if(d.name==t.triggerName){let m=Z1(r,t.triggerName,t.fromState.value,t.toState.value);m._data=o,Y1(t.player,d.phase,m,d.callback)}}),i.markedForDestroy?this._engine.afterFlush(()=>{i.destroy()}):e.push(t)}),this._queue=[],e.sort((t,i)=>{let r=t.transition.ast.depCount,s=i.transition.ast.depCount;return r==0||s==0?r-s:this._engine.driver.containsElement(t.element,i.element)?1:-1})}destroy(o){this.players.forEach(e=>e.destroy()),this._signalRemovalForInnerTriggers(this.hostElement,o)}},XE=class{bodyNode;driver;_normalizer;players=[];newHostElements=new Map;playersByElement=new Map;playersByQueriedElement=new Map;statesByElement=new Map;disabledNodes=new Set;totalAnimations=0;totalQueuedPlayers=0;_namespaceLookup={};_namespaceList=[];_flushFns=[];_whenQuietFns=[];namespacesByHostElement=new Map;collectedEnterElements=[];collectedLeaveElements=[];onRemovalComplete=(o,e)=>{};_onRemovalComplete(o,e){this.onRemovalComplete(o,e)}constructor(o,e,t){this.bodyNode=o,this.driver=e,this._normalizer=t}get queuedPlayers(){let o=[];return this._namespaceList.forEach(e=>{e.players.forEach(t=>{t.queued&&o.push(t)})}),o}createNamespace(o,e){let t=new KE(o,e,this);return this.bodyNode&&this.driver.containsElement(this.bodyNode,e)?this._balanceNamespaceList(t,e):(this.newHostElements.set(e,t),this.collectEnterElement(e)),this._namespaceLookup[o]=t}_balanceNamespaceList(o,e){let t=this._namespaceList,i=this.namespacesByHostElement;if(t.length-1>=0){let s=!1,d=this.driver.getParentElement(e);for(;d;){let m=i.get(d);if(m){let u=t.indexOf(m);t.splice(u+1,0,o),s=!0;break}d=this.driver.getParentElement(d)}s||t.unshift(o)}else t.push(o);return i.set(e,o),o}register(o,e){let t=this._namespaceLookup[o];return t||(t=this.createNamespace(o,e)),t}registerTrigger(o,e,t){let i=this._namespaceLookup[o];i&&i.register(e,t)&&this.totalAnimations++}destroy(o,e){o&&(this.afterFlush(()=>{}),this.afterFlushAnimationsDone(()=>{let t=this._fetchNamespace(o);this.namespacesByHostElement.delete(t.hostElement);let i=this._namespaceList.indexOf(t);i>=0&&this._namespaceList.splice(i,1),t.destroy(e),delete this._namespaceLookup[o]}))}_fetchNamespace(o){return this._namespaceLookup[o]}fetchNamespacesByElement(o){let e=new Set,t=this.statesByElement.get(o);if(t){for(let i of t.values())if(i.namespaceId){let r=this._fetchNamespace(i.namespaceId);r&&e.add(r)}}return e}trigger(o,e,t,i){if(ay(e)){let r=this._fetchNamespace(o);if(r)return r.trigger(e,t,i),!0}return!1}insertNode(o,e,t,i){if(!ay(e))return;let r=e[sc];if(r&&r.setForRemoval){r.setForRemoval=!1,r.setForMove=!0;let s=this.collectedLeaveElements.indexOf(e);s>=0&&this.collectedLeaveElements.splice(s,1)}if(o){let s=this._fetchNamespace(o);s&&s.insertNode(e,t)}i&&this.collectEnterElement(e)}collectEnterElement(o){this.collectedEnterElements.push(o)}markElementAsDisabled(o,e){e?this.disabledNodes.has(o)||(this.disabledNodes.add(o),Ml(o,zE)):this.disabledNodes.has(o)&&(this.disabledNodes.delete(o),lh(o,zE))}removeNode(o,e,t){if(ay(e)){let i=o?this._fetchNamespace(o):null;i?i.removeNode(e,t):this.markElementAsRemoved(o,e,!1,t);let r=this.namespacesByHostElement.get(e);r&&r.id!==o&&r.removeNode(e,t)}else this._onRemovalComplete(e,t)}markElementAsRemoved(o,e,t,i,r){this.collectedLeaveElements.push(e),e[sc]={namespaceId:o,setForRemoval:i,hasAnimation:t,removedBeforeQueried:!1,previousTriggersValues:r}}listen(o,e,t,i,r){return ay(e)?this._fetchNamespace(o).listen(e,t,i,r):()=>{}}_buildInstruction(o,e,t,i,r){return o.transition.build(this.driver,o.element,o.fromState.value,o.toState.value,t,i,o.fromState.options,o.toState.options,e,r)}destroyInnerAnimations(o){let e=this.driver.query(o,W0,!0);e.forEach(t=>this.destroyActiveAnimationsForElement(t)),this.playersByQueriedElement.size!=0&&(e=this.driver.query(o,ty,!0),e.forEach(t=>this.finishActiveQueriedAnimationOnElement(t)))}destroyActiveAnimationsForElement(o){let e=this.playersByElement.get(o);e&&e.forEach(t=>{t.queued?t.markedForDestroy=!0:t.destroy()})}finishActiveQueriedAnimationOnElement(o){let e=this.playersByQueriedElement.get(o);e&&e.forEach(t=>t.finish())}whenRenderingDone(){return new Promise(o=>{if(this.players.length)return Ld(this.players).onDone(()=>o());o()})}processLeaveNode(o){let e=o[sc];if(e&&e.setForRemoval){if(o[sc]=mR,e.namespaceId){this.destroyInnerAnimations(o);let t=this._fetchNamespace(e.namespaceId);t&&t.clearElementCache(o)}this._onRemovalComplete(o,e.setForRemoval)}o.classList?.contains(zE)&&this.markElementAsDisabled(o,!1),this.driver.query(o,cte,!0).forEach(t=>{this.markElementAsDisabled(t,!1)})}flush(o=-1){let e=[];if(this.newHostElements.size&&(this.newHostElements.forEach((t,i)=>this._balanceNamespaceList(t,i)),this.newHostElements.clear()),this.totalAnimations&&this.collectedEnterElements.length)for(let t=0;tt()),this._flushFns=[],this._whenQuietFns.length){let t=this._whenQuietFns;this._whenQuietFns=[],e.length?Ld(e).onDone(()=>{t.forEach(i=>i())}):t.forEach(i=>i())}}reportError(o){throw W8(o)}_flushAnimations(o,e){let t=new Q0,i=[],r=new Map,s=[],d=new Map,m=new Map,u=new Map,h=new Set;this.disabledNodes.forEach(ue=>{h.add(ue);let H=this.driver.query(ue,lte,!0);for(let ee=0;ee{let ee=AE+P++;x.set(H,ee),ue.forEach(le=>Ml(le,ee))});let k=[],S=new Set,T=new Set;for(let ue=0;ueS.add(le)):T.add(H))}let A=new Map,D=aR(b,Array.from(S));D.forEach((ue,H)=>{let ee=ey+P++;A.set(H,ee),ue.forEach(le=>Ml(le,ee))}),o.push(()=>{y.forEach((ue,H)=>{let ee=x.get(H);ue.forEach(le=>lh(le,ee))}),D.forEach((ue,H)=>{let ee=A.get(H);ue.forEach(le=>lh(le,ee))}),k.forEach(ue=>{this.processLeaveNode(ue)})});let F=[],I=[];for(let ue=this._namespaceList.length-1;ue>=0;ue--)this._namespaceList[ue].drainQueuedTransitions(e).forEach(ee=>{let le=ee.player,de=ee.element;if(F.push(le),this.collectedEnterElements.length){let Re=de[sc];if(Re&&Re.setForMove){if(Re.previousTriggersValues&&Re.previousTriggersValues.has(ee.triggerName)){let Ze=Re.previousTriggersValues.get(ee.triggerName),ct=this.statesByElement.get(ee.element);if(ct&&ct.has(ee.triggerName)){let Tt=ct.get(ee.triggerName);Tt.value=Ze,ct.set(ee.triggerName,Tt)}}le.destroy();return}}let De=!v||!this.driver.containsElement(v,de),je=A.get(de),tt=x.get(de),et=this._buildInstruction(ee,t,tt,je,De);if(et.errors&&et.errors.length){I.push(et);return}if(De){le.onStart(()=>Rp(de,et.fromStyles)),le.onDestroy(()=>ac(de,et.toStyles)),i.push(le);return}if(ee.isFallbackTransition){le.onStart(()=>Rp(de,et.fromStyles)),le.onDestroy(()=>ac(de,et.toStyles)),i.push(le);return}let ye=[];et.timelines.forEach(Re=>{Re.stretchStartingKeyframe=!0,this.disabledNodes.has(Re.element)||ye.push(Re)}),et.timelines=ye,t.append(de,et.timelines);let Y={instruction:et,player:le,element:de};s.push(Y),et.queriedElements.forEach(Re=>Ss(d,Re,[]).push(le)),et.preStyleProps.forEach((Re,Ze)=>{if(Re.size){let ct=m.get(Ze);ct||m.set(Ze,ct=new Set),Re.forEach((Tt,ri)=>ct.add(ri))}}),et.postStyleProps.forEach((Re,Ze)=>{let ct=u.get(Ze);ct||u.set(Ze,ct=new Set),Re.forEach((Tt,ri)=>ct.add(ri))})});if(I.length){let ue=[];I.forEach(H=>{ue.push(G8(H.triggerName,H.errors))}),F.forEach(H=>H.destroy()),this.reportError(ue)}let V=new Map,ae=new Map;s.forEach(ue=>{let H=ue.element;t.has(H)&&(ae.set(H,H),this._beforeAnimationBuild(ue.player.namespaceId,ue.instruction,V))}),i.forEach(ue=>{let H=ue.element;this._getPreviousPlayers(H,!1,ue.namespaceId,ue.triggerName,null).forEach(le=>{Ss(V,H,[]).push(le),le.destroy()})});let oe=k.filter(ue=>sR(ue,m,u)),W=new Map;rR(W,this.driver,T,u,rc).forEach(ue=>{sR(ue,m,u)&&oe.push(ue)});let Ce=new Map;y.forEach((ue,H)=>{rR(Ce,this.driver,new Set(ue),m,H0)}),oe.forEach(ue=>{let H=W.get(ue),ee=Ce.get(ue);W.set(ue,new Map([...H?.entries()??[],...ee?.entries()??[]]))});let we=[],me=[],Ie={};s.forEach(ue=>{let{element:H,player:ee,instruction:le}=ue;if(t.has(H)){if(h.has(H)){ee.onDestroy(()=>ac(H,le.toStyles)),ee.disabled=!0,ee.overrideTotalTime(le.totalTime),i.push(ee);return}let de=Ie;if(ae.size>1){let je=H,tt=[];for(;je=je.parentNode;){let et=ae.get(je);if(et){de=et;break}tt.push(je)}tt.forEach(et=>ae.set(et,de))}let De=this._buildAnimation(ee.namespaceId,le,V,r,Ce,W);if(ee.setRealPlayer(De),de===Ie)we.push(ee);else{let je=this.playersByElement.get(de);je&&je.length&&(ee.parentPlayer=Ld(je)),i.push(ee)}}else Rp(H,le.fromStyles),ee.onDestroy(()=>ac(H,le.toStyles)),me.push(ee),h.has(H)&&i.push(ee)}),me.forEach(ue=>{let H=r.get(ue.element);if(H&&H.length){let ee=Ld(H);ue.setRealPlayer(ee)}}),i.forEach(ue=>{ue.parentPlayer?ue.syncPlayerEvents(ue.parentPlayer):ue.destroy()});for(let ue=0;ue!De.destroyed);de.length?fte(this,H,de):this.processLeaveNode(H)}return k.length=0,we.forEach(ue=>{this.players.push(ue),ue.onDone(()=>{ue.destroy();let H=this.players.indexOf(ue);this.players.splice(H,1)}),ue.play()}),we}afterFlush(o){this._flushFns.push(o)}afterFlushAnimationsDone(o){this._whenQuietFns.push(o)}_getPreviousPlayers(o,e,t,i,r){let s=[];if(e){let d=this.playersByQueriedElement.get(o);d&&(s=d)}else{let d=this.playersByElement.get(o);if(d){let m=!r||r==q0;d.forEach(u=>{u.queued||!m&&u.triggerName!=i||s.push(u)})}}return(t||i)&&(s=s.filter(d=>!(t&&t!=d.namespaceId||i&&i!=d.triggerName))),s}_beforeAnimationBuild(o,e,t){let i=e.triggerName,r=e.element,s=e.isRemovalTransition?void 0:o,d=e.isRemovalTransition?void 0:i;for(let m of e.timelines){let u=m.element,h=u!==r,v=Ss(t,u,[]);this._getPreviousPlayers(u,h,s,d,e.toState).forEach(y=>{let x=y.getRealPlayer();x.beforeDestroy&&x.beforeDestroy(),y.destroy(),v.push(y)})}Rp(r,e.fromStyles)}_buildAnimation(o,e,t,i,r,s){let d=e.triggerName,m=e.element,u=[],h=new Set,v=new Set,b=e.timelines.map(x=>{let P=x.element;h.add(P);let k=P[sc];if(k&&k.removedBeforeQueried)return new Rd(x.duration,x.delay);let S=P!==m,T=Cte((t.get(P)||mte).map(V=>V.getRealPlayer())).filter(V=>{let ae=V;return ae.element?ae.element===P:!1}),A=r.get(P),D=s.get(P),F=TE(this._normalizer,x.keyframes,A,D),I=this._buildPlayer(x,F,T);if(x.subTimeline&&i&&v.add(P),S){let V=new X0(o,d,P);V.setRealPlayer(I),u.push(V)}return I});u.forEach(x=>{Ss(this.playersByQueriedElement,x.element,[]).push(x),x.onDone(()=>gte(this.playersByQueriedElement,x.element,x))}),h.forEach(x=>Ml(x,NE));let y=Ld(b);return y.onDestroy(()=>{h.forEach(x=>lh(x,NE)),ac(m,e.toStyles)}),v.forEach(x=>{Ss(i,x,[]).push(y)}),y}_buildPlayer(o,e,t){return e.length>0?this.driver.animate(o.element,e,o.duration,o.delay,o.easing,t):new Rd(o.duration,o.delay)}},X0=class{namespaceId;triggerName;element;_player=new Rd;_containsRealPlayer=!1;_queuedCallbacks=new Map;destroyed=!1;parentPlayer=null;markedForDestroy=!1;disabled=!1;queued=!0;totalTime=0;constructor(o,e,t){this.namespaceId=o,this.triggerName=e,this.element=t}setRealPlayer(o){this._containsRealPlayer||(this._player=o,this._queuedCallbacks.forEach((e,t)=>{e.forEach(i=>Y1(o,t,void 0,i))}),this._queuedCallbacks.clear(),this._containsRealPlayer=!0,this.overrideTotalTime(o.totalTime),this.queued=!1)}getRealPlayer(){return this._player}overrideTotalTime(o){this.totalTime=o}syncPlayerEvents(o){let e=this._player;e.triggerCallback&&o.onStart(()=>e.triggerCallback("start")),o.onDone(()=>this.finish()),o.onDestroy(()=>this.destroy())}_queueEvent(o,e){Ss(this._queuedCallbacks,o,[]).push(e)}onDone(o){this.queued&&this._queueEvent("done",o),this._player.onDone(o)}onStart(o){this.queued&&this._queueEvent("start",o),this._player.onStart(o)}onDestroy(o){this.queued&&this._queueEvent("destroy",o),this._player.onDestroy(o)}init(){this._player.init()}hasStarted(){return this.queued?!1:this._player.hasStarted()}play(){!this.queued&&this._player.play()}pause(){!this.queued&&this._player.pause()}restart(){!this.queued&&this._player.restart()}finish(){this._player.finish()}destroy(){this.destroyed=!0,this._player.destroy()}reset(){!this.queued&&this._player.reset()}setPosition(o){this.queued||this._player.setPosition(o)}getPosition(){return this.queued?0:this._player.getPosition()}triggerCallback(o){let e=this._player;e.triggerCallback&&e.triggerCallback(o)}};function gte(n,o,e){let t=n.get(o);if(t){if(t.length){let i=t.indexOf(e);t.splice(i,1)}t.length==0&&n.delete(o)}return t}function hte(n){return n??null}function ay(n){return n&&n.nodeType===1}function _te(n){return n=="start"||n=="done"}function oR(n,o){let e=n.style.display;return n.style.display=o??"none",e}function rR(n,o,e,t,i){let r=[];e.forEach(m=>r.push(oR(m)));let s=[];t.forEach((m,u)=>{let h=new Map;m.forEach(v=>{let b=o.computeStyle(u,v,i);h.set(v,b),(!b||b.length==0)&&(u[sc]=ute,s.push(u))}),n.set(u,h)});let d=0;return e.forEach(m=>oR(m,r[d++])),s}function aR(n,o){let e=new Map;if(n.forEach(d=>e.set(d,[])),o.length==0)return e;let t=1,i=new Set(o),r=new Map;function s(d){if(!d)return t;let m=r.get(d);if(m)return m;let u=d.parentNode;return e.has(u)?m=u:i.has(u)?m=t:m=s(u),r.set(d,m),m}return o.forEach(d=>{let m=s(d);m!==t&&e.get(m).push(d)}),e}function Ml(n,o){n.classList?.add(o)}function lh(n,o){n.classList?.remove(o)}function fte(n,o,e){Ld(e).onDone(()=>n.processLeaveNode(o))}function Cte(n){let o=[];return uR(n,o),o}function uR(n,o){for(let e=0;ei.add(r)):o.set(n,t),e.delete(n),!0}var ch=class{_driver;_normalizer;_transitionEngine;_timelineEngine;_triggerCache={};onRemovalComplete=(o,e)=>{};constructor(o,e,t){this._driver=e,this._normalizer=t,this._transitionEngine=new XE(o.body,e,t),this._timelineEngine=new QE(o.body,e,t),this._transitionEngine.onRemovalComplete=(i,r)=>this.onRemovalComplete(i,r)}registerTrigger(o,e,t,i,r){let s=o+"-"+i,d=this._triggerCache[s];if(!d){let m=[],u=[],h=cR(this._driver,r,m,u);if(m.length)throw R8(i,m);d=rte(i,h,this._normalizer),this._triggerCache[s]=d}this._transitionEngine.registerTrigger(e,i,d)}register(o,e){this._transitionEngine.register(o,e)}destroy(o,e){this._transitionEngine.destroy(o,e)}onInsert(o,e,t,i){this._transitionEngine.insertNode(o,e,t,i)}onRemove(o,e,t){this._transitionEngine.removeNode(o,e,t)}disableAnimations(o,e){this._transitionEngine.markElementAsDisabled(o,e)}process(o,e,t,i){if(t.charAt(0)=="@"){let[r,s]=EE(t),d=i;this._timelineEngine.command(r,e,s,d)}else this._transitionEngine.trigger(o,e,t,i)}listen(o,e,t,i,r){if(t.charAt(0)=="@"){let[s,d]=EE(t);return this._timelineEngine.listen(s,e,d,r)}return this._transitionEngine.listen(o,e,t,i,r)}flush(o=-1){this._transitionEngine.flush(o)}get players(){return[...this._transitionEngine.players,...this._timelineEngine.players]}whenRenderingDone(){return this._transitionEngine.whenRenderingDone()}afterFlushAnimationsDone(o){this._transitionEngine.afterFlushAnimationsDone(o)}};function xte(n,o){let e=null,t=null;return Array.isArray(o)&&o.length?(e=BE(o[0]),o.length>1&&(t=BE(o[o.length-1]))):o instanceof Map&&(e=BE(o)),e||t?new yte(n,e,t):null}var yte=(()=>{class n{_element;_startStyles;_endStyles;static initialStylesByElement=new WeakMap;_state=0;_initialStyles;constructor(e,t,i){this._element=e,this._startStyles=t,this._endStyles=i;let r=n.initialStylesByElement.get(e);r||n.initialStylesByElement.set(e,r=new Map),this._initialStyles=r}start(){this._state<1&&(this._startStyles&&ac(this._element,this._startStyles,this._initialStyles),this._state=1)}finish(){this.start(),this._state<2&&(ac(this._element,this._initialStyles),this._endStyles&&(ac(this._element,this._endStyles),this._endStyles=null),this._state=1)}destroy(){this.finish(),this._state<3&&(n.initialStylesByElement.delete(this._element),this._startStyles&&(Rp(this._element,this._startStyles),this._endStyles=null),this._endStyles&&(Rp(this._element,this._endStyles),this._endStyles=null),ac(this._element,this._initialStyles),this._state=3)}}return n})();function BE(n){let o=null;return n.forEach((e,t)=>{bte(t)&&(o=o||new Map,o.set(t,e))}),o}function bte(n){return n==="display"||n==="position"}var uy=class{element;keyframes;options;_specialStyles;_onDoneFns=[];_onStartFns=[];_onDestroyFns=[];_duration;_delay;_initialized=!1;_finished=!1;_started=!1;_destroyed=!1;_finalKeyframe;_originalOnDoneFns=[];_originalOnStartFns=[];domPlayer=null;time=0;parentPlayer=null;currentSnapshot=new Map;constructor(o,e,t,i){this.element=o,this.keyframes=e,this.options=t,this._specialStyles=i,this._duration=t.duration,this._delay=t.delay||0,this.time=this._duration+this._delay}_onFinish(){this._finished||(this._finished=!0,this._onDoneFns.forEach(o=>o()),this._onDoneFns=[])}init(){this._buildPlayer()&&this._preparePlayerBeforeStart()}_buildPlayer(){if(this._initialized)return this.domPlayer;this._initialized=!0;let o=this.keyframes,e=this._triggerWebAnimation(this.element,o,this.options);if(!e)return this._onFinish(),null;this.domPlayer=e,this._finalKeyframe=o.length?o[o.length-1]:new Map;let t=()=>this._onFinish();return e.addEventListener("finish",t),this.onDestroy(()=>{e.removeEventListener("finish",t)}),e}_preparePlayerBeforeStart(){this._delay?this._resetDomPlayerState():this.domPlayer?.pause()}_convertKeyframesToObject(o){let e=[];return o.forEach(t=>{e.push(Object.fromEntries(t))}),e}_triggerWebAnimation(o,e,t){let i=this._convertKeyframesToObject(e);try{return o.animate(i,t)}catch{return null}}onStart(o){this._originalOnStartFns.push(o),this._onStartFns.push(o)}onDone(o){this._originalOnDoneFns.push(o),this._onDoneFns.push(o)}onDestroy(o){this._onDestroyFns.push(o)}play(){let o=this._buildPlayer();o&&(this.hasStarted()||(this._onStartFns.forEach(e=>e()),this._onStartFns=[],this._started=!0,this._specialStyles&&this._specialStyles.start()),o.play())}pause(){this.init(),this.domPlayer?.pause()}finish(){this.init(),this.domPlayer&&(this._specialStyles&&this._specialStyles.finish(),this._onFinish(),this.domPlayer.finish())}reset(){this._resetDomPlayerState(),this._destroyed=!1,this._finished=!1,this._started=!1,this._onStartFns=this._originalOnStartFns,this._onDoneFns=this._originalOnDoneFns}_resetDomPlayerState(){this.domPlayer?.cancel()}restart(){this.reset(),this.play()}hasStarted(){return this._started}destroy(){this._destroyed||(this._destroyed=!0,this._resetDomPlayerState(),this._onFinish(),this._specialStyles&&this._specialStyles.destroy(),this._onDestroyFns.forEach(o=>o()),this._onDestroyFns=[])}setPosition(o){this.domPlayer||this.init(),this.domPlayer&&(this.domPlayer.currentTime=o*this.time)}getPosition(){return this.domPlayer?+(this.domPlayer.currentTime??0)/this.time:this._initialized?1:0}get totalTime(){return this._delay+this._duration}beforeDestroy(){let o=new Map;this.hasStarted()&&this._finalKeyframe.forEach((t,i)=>{i!=="offset"&&o.set(i,this._finished?t:iy(this.element,i))}),this.currentSnapshot=o}triggerCallback(o){let e=o==="start"?this._onStartFns:this._onDoneFns;e.forEach(t=>t()),e.length=0}},gy=class{validateStyleProperty(o){return!0}validateAnimatableStyleProperty(o){return!0}containsElement(o,e){return OE(o,e)}getParentElement(o){return J1(o)}query(o,e,t){return DE(o,e,t)}computeStyle(o,e,t){return iy(o,e)}animate(o,e,t,i,r,s=[]){let d=i==0?"both":"forwards",m={duration:t,delay:i,fill:d};r&&(m.easing=r);let u=new Map,h=s.filter(y=>y instanceof uy);X8(t,i)&&h.forEach(y=>{y.currentSnapshot.forEach((x,P)=>u.set(P,x))});let v=Q8(e).map(y=>new Map(y));v=Y8(o,v,u);let b=xte(o,v);return new uy(o,v,m,b)}};var sy="@",gR="@.disabled",hy=class{namespaceId;delegate;engine;_onDestroy;\u0275type=0;constructor(o,e,t,i){this.namespaceId=o,this.delegate=e,this.engine=t,this._onDestroy=i}get data(){return this.delegate.data}destroyNode(o){this.delegate.destroyNode?.(o)}destroy(){this.engine.destroy(this.namespaceId,this.delegate),this.engine.afterFlushAnimationsDone(()=>{queueMicrotask(()=>{this.delegate.destroy()})}),this._onDestroy?.()}createElement(o,e){return this.delegate.createElement(o,e)}createComment(o){return this.delegate.createComment(o)}createText(o){return this.delegate.createText(o)}appendChild(o,e){this.delegate.appendChild(o,e),this.engine.onInsert(this.namespaceId,e,o,!1)}insertBefore(o,e,t,i=!0){this.delegate.insertBefore(o,e,t),this.engine.onInsert(this.namespaceId,e,o,i)}removeChild(o,e,t,i){if(i){this.delegate.removeChild(o,e,t,i);return}this.parentNode(e)&&this.engine.onRemove(this.namespaceId,e,this.delegate)}selectRootElement(o,e){return this.delegate.selectRootElement(o,e)}parentNode(o){return this.delegate.parentNode(o)}nextSibling(o){return this.delegate.nextSibling(o)}setAttribute(o,e,t,i){this.delegate.setAttribute(o,e,t,i)}removeAttribute(o,e,t){this.delegate.removeAttribute(o,e,t)}addClass(o,e){this.delegate.addClass(o,e)}removeClass(o,e){this.delegate.removeClass(o,e)}setStyle(o,e,t,i){this.delegate.setStyle(o,e,t,i)}removeStyle(o,e,t){this.delegate.removeStyle(o,e,t)}setProperty(o,e,t){e.charAt(0)==sy&&e==gR?this.disableAnimations(o,!!t):this.delegate.setProperty(o,e,t)}setValue(o,e){this.delegate.setValue(o,e)}listen(o,e,t,i){return this.delegate.listen(o,e,t,i)}disableAnimations(o,e){this.engine.disableAnimations(o,e)}},YE=class extends hy{factory;constructor(o,e,t,i,r){super(e,t,i,r),this.factory=o,this.namespaceId=e}setProperty(o,e,t){e.charAt(0)==sy?e.charAt(1)=="."&&e==gR?(t=t===void 0?!0:!!t,this.disableAnimations(o,t)):this.engine.process(this.namespaceId,o,e.slice(1),t):this.delegate.setProperty(o,e,t)}listen(o,e,t,i){if(e.charAt(0)==sy){let r=wte(o),s=e.slice(1),d="";return s.charAt(0)!=sy&&([s,d]=Mte(s)),this.engine.listen(this.namespaceId,r,s,d,m=>{let u=m._data||-1;this.factory.scheduleListenerCallback(u,t,m)})}return this.delegate.listen(o,e,t,i)}};function wte(n){switch(n){case"body":return document.body;case"document":return document;case"window":return window;default:return n}}function Mte(n){let o=n.indexOf("."),e=n.substring(0,o),t=n.slice(o+1);return[e,t]}var _y=class{delegate;engine;_zone;_currentId=0;_microtaskId=1;_animationCallbacksBuffer=[];_rendererCache=new Map;_cdRecurDepth=0;constructor(o,e,t){this.delegate=o,this.engine=e,this._zone=t,e.onRemovalComplete=(i,r)=>{r?.removeChild(null,i)}}createRenderer(o,e){let i=this.delegate.createRenderer(o,e);if(!o||!e?.data?.animation){let u=this._rendererCache,h=u.get(i);if(!h){let v=()=>u.delete(i);h=new hy("",i,this.engine,v),u.set(i,h)}return h}let r=e.id,s=e.id+"-"+this._currentId;this._currentId++,this.engine.register(s,o);let d=u=>{Array.isArray(u)?u.forEach(d):this.engine.registerTrigger(r,s,o,u.name,u)};return e.data.animation.forEach(d),new YE(this,s,i,this.engine)}begin(){this._cdRecurDepth++,this.delegate.begin&&this.delegate.begin()}_scheduleCountTask(){queueMicrotask(()=>{this._microtaskId++})}scheduleListenerCallback(o,e,t){if(o>=0&&oe(t));return}let i=this._animationCallbacksBuffer;i.length==0&&queueMicrotask(()=>{this._zone.run(()=>{i.forEach(r=>{let[s,d]=r;s(d)}),this._animationCallbacksBuffer=[]})}),i.push([e,t])}end(){this._cdRecurDepth--,this._cdRecurDepth==0&&this._zone.runOutsideAngular(()=>{this._scheduleCountTask(),this.engine.flush(this._microtaskId)}),this.delegate.end&&this.delegate.end()}whenRenderingDone(){return this.engine.whenRenderingDone()}componentReplaced(o){this.engine.flush(),this.delegate.componentReplaced?.(o)}};var kte=(()=>{class n extends ch{constructor(e,t,i){super(e,t,i)}ngOnDestroy(){this.flush()}static \u0275fac=function(t){return new(t||n)(he(Eo),he(vu),he(xu))};static \u0275prov=J({token:n,factory:n.\u0275fac})}return n})();function Pte(){return new ly}function Tte(){return new _y(f(sA),f(ch),f(Ki))}var _R=[{provide:xu,useFactory:Pte},{provide:ch,useClass:kte},{provide:Dm,useFactory:Tte}],Ete=[{provide:vu,useClass:ZE},{provide:Ok,useValue:"NoopAnimations"},..._R],hR=[{provide:vu,useFactory:()=>new gy},{provide:Ok,useFactory:()=>"BrowserAnimations"},..._R],fR=(()=>{class n{static withConfig(e){return{ngModule:n,providers:e.disableAnimations?Ete:hR}}static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({providers:hR,imports:[Nv]})}return n})();var CR=void 0;typeof Object.create!="function"?CR=(function(n){var o=function(){};return function(e,t){if(e!==Object(e)&&e!==null)throw TypeError("Argument must be an object, or null");o.prototype=e||{};var i=new o;return o.prototype=null,t!==n&&Object.defineProperties(i,t),e===null&&(i.__proto__=null),i}})():CR=Object.create;var Y0=["webkit","moz","ms","o"],dHe=(function(){if(typeof window>"u")return function(){};for(var n=0,o=Y0.length;n"u")return function(){};for(var n=0,o=Y0.length;n{class n{static{this.\u0275fac=function(t){return new(t||n)}}static{this.\u0275mod=nn({type:n})}static{this.\u0275inj=tn({})}}return n})();var Z0=class{constructor(o){this.rawFile=o;let e=o instanceof HTMLInputElement?o.value:o;this[`_createFrom${typeof e=="string"?"FakePath":"Object"}`](e)}_createFromFakePath(o){this.lastModifiedDate=void 0,this.size=void 0,this.type=`like/${o.slice(o.lastIndexOf(".")+1).toLowerCase()}`,this.name=o.slice(o.lastIndexOf("/")+o.lastIndexOf("\\")+2)}_createFromObject(o){this.size=o.size,this.type=o.type,this.name=o.name}},e3=class{constructor(o,e,t){this.url="/",this.headers=[],this.withCredentials=!0,this.formData=[],this.isReady=!1,this.isUploading=!1,this.isUploaded=!1,this.isSuccess=!1,this.isCancel=!1,this.isError=!1,this.progress=0,this.uploader=o,this.some=e,this.options=t,this.file=new Z0(e),this._file=e,o.options&&(this.method=o.options.method||"POST",this.alias=o.options.itemAlias||"file"),this.url=o.options.url}upload(){try{this.uploader.uploadItem(this)}catch{this.uploader._onCompleteItem(this,"",0,{}),this.uploader._onErrorItem(this,"",0,{})}}cancel(){this.uploader.cancelItem(this)}remove(){this.uploader.removeFromQueue(this)}onBeforeUpload(){}onBuildForm(o){return{form:o}}onProgress(o){return{progress:o}}onSuccess(o,e,t){return{response:o,status:e,headers:t}}onError(o,e,t){return{response:o,status:e,headers:t}}onCancel(o,e,t){return{response:o,status:e,headers:t}}onComplete(o,e,t){return{response:o,status:e,headers:t}}_onBeforeUpload(){this.isReady=!0,this.isUploading=!0,this.isUploaded=!1,this.isSuccess=!1,this.isCancel=!1,this.isError=!1,this.progress=0,this.onBeforeUpload()}_onBuildForm(o){this.onBuildForm(o)}_onProgress(o){this.progress=o,this.onProgress(o)}_onSuccess(o,e,t){this.isReady=!1,this.isUploading=!1,this.isUploaded=!0,this.isSuccess=!0,this.isCancel=!1,this.isError=!1,this.progress=100,this.index=void 0,this.onSuccess(o,e,t)}_onError(o,e,t){this.isReady=!1,this.isUploading=!1,this.isUploaded=!0,this.isSuccess=!1,this.isCancel=!1,this.isError=!0,this.progress=0,this.index=void 0,this.onError(o,e,t)}_onCancel(o,e,t){this.isReady=!1,this.isUploading=!1,this.isUploaded=!1,this.isSuccess=!1,this.isCancel=!0,this.isError=!1,this.progress=0,this.index=void 0,this.onCancel(o,e,t)}_onComplete(o,e,t){this.onComplete(o,e,t),this.uploader.options.removeAfterUpload&&this.remove()}_prepareToUploading(){this.index=this.index||++this.uploader._nextIndex,this.isReady=!0}},Ote=(()=>{class n{static getMimeClass(e){let t="application";return e?.type&&this.mime_psd.indexOf(e.type)!==-1||e?.type?.match("image.*")?t="image":e?.type?.match("video.*")?t="video":e?.type?.match("audio.*")?t="audio":e?.type==="application/pdf"?t="pdf":e?.type&&this.mime_compress.indexOf(e.type)!==-1?t="compress":e?.type&&this.mime_doc.indexOf(e.type)!==-1?t="doc":e?.type&&this.mime_xsl.indexOf(e.type)!==-1?t="xls":e?.type&&this.mime_ppt.indexOf(e.type)!==-1&&(t="ppt"),t==="application"&&e?.name&&(t=this.fileTypeDetection(e.name)),t}static fileTypeDetection(e){let t={jpg:"image",jpeg:"image",tif:"image",psd:"image",bmp:"image",png:"image",nef:"image",tiff:"image",cr2:"image",dwg:"image",cdr:"image",ai:"image",indd:"image",pin:"image",cdp:"image",skp:"image",stp:"image","3dm":"image",mp3:"audio",wav:"audio",wma:"audio",mod:"audio",m4a:"audio",compress:"compress",zip:"compress",rar:"compress","7z":"compress",lz:"compress",z01:"compress",bz2:"compress",gz:"compress",pdf:"pdf",xls:"xls",xlsx:"xls",ods:"xls",mp4:"video",avi:"video",wmv:"video",mpg:"video",mts:"video",flv:"video","3gp":"video",vob:"video",m4v:"video",mpeg:"video",m2ts:"video",mov:"video",doc:"doc",docx:"doc",eps:"doc",txt:"doc",odt:"doc",rtf:"doc",ppt:"ppt",pptx:"ppt",pps:"ppt",ppsx:"ppt",odp:"ppt"},i=e.split(".");if(i.length<2)return"application";let r=i[i.length-1].toLowerCase();return t[r]===void 0?"application":t[r]}}return n.mime_doc=["application/msword","application/msword","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.openxmlformats-officedocument.wordprocessingml.template","application/vnd.ms-word.document.macroEnabled.12","application/vnd.ms-word.template.macroEnabled.12"],n.mime_xsl=["application/vnd.ms-excel","application/vnd.ms-excel","application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","application/vnd.openxmlformats-officedocument.spreadsheetml.template","application/vnd.ms-excel.sheet.macroEnabled.12","application/vnd.ms-excel.template.macroEnabled.12","application/vnd.ms-excel.addin.macroEnabled.12","application/vnd.ms-excel.sheet.binary.macroEnabled.12"],n.mime_ppt=["application/vnd.ms-powerpoint","application/vnd.ms-powerpoint","application/vnd.ms-powerpoint","application/vnd.ms-powerpoint","application/vnd.openxmlformats-officedocument.presentationml.presentation","application/vnd.openxmlformats-officedocument.presentationml.template","application/vnd.openxmlformats-officedocument.presentationml.slideshow","application/vnd.ms-powerpoint.addin.macroEnabled.12","application/vnd.ms-powerpoint.presentation.macroEnabled.12","application/vnd.ms-powerpoint.presentation.macroEnabled.12","application/vnd.ms-powerpoint.slideshow.macroEnabled.12"],n.mime_psd=["image/photoshop","image/x-photoshop","image/psd","application/photoshop","application/psd","zz-application/zz-winassoc-psd"],n.mime_compress=["application/x-gtar","application/x-gcompress","application/compress","application/x-tar","application/x-rar-compressed","application/octet-stream","application/x-zip-compressed","application/zip-compressed","application/x-7z-compressed","application/gzip","application/x-bzip2"],n})();function Dte(n){return File&&n instanceof File}var Sl=class{constructor(o){this.isUploading=!1,this.queue=[],this.progress=0,this._nextIndex=0,this.options={autoUpload:!1,isHTML5:!0,filters:[],removeAfterUpload:!1,disableMultipart:!1,formatDataFunction:e=>e._file,formatDataFunctionIsAsync:!1,url:""},this.setOptions(o),this.response=new ve}setOptions(o){this.options=Object.assign(this.options,o),this.authToken=this.options.authToken,this.authTokenHeader=this.options.authTokenHeader||"Authorization",this.autoUpload=this.options.autoUpload,this.options.filters?.unshift({name:"queueLimit",fn:this._queueLimitFilter}),this.options.maxFileSize&&this.options.filters?.unshift({name:"fileSize",fn:this._fileSizeFilter}),this.options.allowedFileType&&this.options.filters?.unshift({name:"fileType",fn:this._fileTypeFilter}),this.options.allowedMimeType&&this.options.filters?.unshift({name:"mimeType",fn:this._mimeTypeFilter});for(let e=0;e{i||(i=this.options);let h=new Z0(u);if(this._isValidFile(h,s,i)){let v=new e3(this,u,i);m.push(v),this.queue.push(v),this._onAfterAddingFile(v)}else if(this._failFilterIndex){let v=s[this._failFilterIndex];this._onWhenAddingFileFailed(h,v,i)}}),this.queue.length!==d&&(this._onAfterAddingAll(m),this.progress=this._getTotalProgress()),this._render(),this.options.autoUpload&&this.uploadAll()}removeFromQueue(o){let e=this.getIndexOfItem(o),t=this.queue[e];t.isUploading&&t.cancel(),this.queue.splice(e,1),this.progress=this._getTotalProgress()}clearQueue(){for(;this.queue.length;)this.queue[0].remove();this.progress=0}uploadItem(o){let e=this.getIndexOfItem(o),t=this.queue[e],i=this.options.isHTML5?"_xhrTransport":"_iframeTransport";t._prepareToUploading(),!this.isUploading&&(this.isUploading=!0,this[i](t))}cancelItem(o){let e=this.getIndexOfItem(o),t=this.queue[e],i=this.options.isHTML5?t._xhr:t._form;t&&t.isUploading&&i.abort()}uploadAll(){let o=this.getNotUploadedItems().filter(e=>!e.isUploading);o.length&&(o.map(e=>e._prepareToUploading()),o[0].upload())}cancelAll(){this.getNotUploadedItems().map(e=>e.cancel())}isFile(o){return Dte(o)}isFileLikeObject(o){return o instanceof Z0}getIndexOfItem(o){return typeof o=="number"?o:this.queue.indexOf(o)}getNotUploadedItems(){return this.queue.filter(o=>!o.isUploaded)}getReadyItems(){return this.queue.filter(o=>o.isReady&&!o.isUploading).sort((o,e)=>o.index-e.index)}onAfterAddingAll(o){return{fileItems:o}}onBuildItemForm(o,e){return{fileItem:o,form:e}}onAfterAddingFile(o){return{fileItem:o}}onWhenAddingFileFailed(o,e,t){return{item:o,filter:e,options:t}}onBeforeUploadItem(o){return{fileItem:o}}onProgressItem(o,e){return{fileItem:o,progress:e}}onProgressAll(o){return{progress:o}}onSuccessItem(o,e,t,i){return{item:o,response:e,status:t,headers:i}}onErrorItem(o,e,t,i){return{item:o,response:e,status:t,headers:i}}onCancelItem(o,e,t,i){return{item:o,response:e,status:t,headers:i}}onCompleteItem(o,e,t,i){return{item:o,response:e,status:t,headers:i}}onCompleteAll(){}_mimeTypeFilter(o){return!(o?.type&&this.options.allowedMimeType&&this.options.allowedMimeType?.indexOf(o.type)===-1)}_fileSizeFilter(o){return!(this.options.maxFileSize&&o.size>this.options.maxFileSize)}_fileTypeFilter(o){return!(this.options.allowedFileType&&this.options.allowedFileType.indexOf(Ote.getMimeClass(o))===-1)}_onErrorItem(o,e,t,i){o._onError(e,t,i),this.onErrorItem(o,e,t,i)}_onCompleteItem(o,e,t,i){o._onComplete(e,t,i),this.onCompleteItem(o,e,t,i);let r=this.getReadyItems()[0];if(this.isUploading=!1,r){r.upload();return}this.onCompleteAll(),this.progress=this._getTotalProgress(),this._render()}_headersGetter(o){return e=>e?o[e.toLowerCase()]||void 0:o}_xhrTransport(o){let e=this,t=o._xhr=new XMLHttpRequest,i;if(this._onBeforeUploadItem(o),typeof o._file.size!="number")throw new TypeError("The file specified is no longer valid");if(this.options.disableMultipart)this.options.formatDataFunction&&(i=this.options.formatDataFunction(o));else{i=new FormData,this._onBuildItemForm(o,i);let r=()=>i.append(o.alias,o._file,o.file.name);this.options.parametersBeforeFiles||r(),this.options.additionalParameter!==void 0&&Object.keys(this.options.additionalParameter).forEach(s=>{let d=this.options.additionalParameter?.[s];typeof d=="string"&&d.indexOf("{{file_name}}")>=0&&o.file?.name&&(d=d.replace("{{file_name}}",o.file.name)),i.append(s,d)}),r&&this.options.parametersBeforeFiles&&r()}if(t.upload.onprogress=r=>{let s=Math.round(r.lengthComputable?r.loaded*100/r.total:0);this._onProgressItem(o,s)},t.onload=()=>{let r=this._parseHeaders(t.getAllResponseHeaders()),s=this._transformResponse(t.response,r),m=`_on${this._isSuccessCode(t.status)?"Success":"Error"}Item`;this[m](o,s,t.status,r),this._onCompleteItem(o,s,t.status,r)},t.onerror=()=>{let r=this._parseHeaders(t.getAllResponseHeaders()),s=this._transformResponse(t.response,r);this._onErrorItem(o,s,t.status,r),this._onCompleteItem(o,s,t.status,r)},t.onabort=()=>{let r=this._parseHeaders(t.getAllResponseHeaders()),s=this._transformResponse(t.response,r);this._onCancelItem(o,s,t.status,r),this._onCompleteItem(o,s,t.status,r)},o.method&&o.url&&t.open(o.method,o.url,!0),t.withCredentials=o.withCredentials,this.options.headers)for(let r of this.options.headers)t.setRequestHeader(r.name,r.value);if(o.headers.length)for(let r of o.headers)t.setRequestHeader(r.name,r.value);this.authToken&&this.authTokenHeader&&t.setRequestHeader(this.authTokenHeader,this.authToken),t.onreadystatechange=function(){t.readyState==XMLHttpRequest.DONE&&e.response.emit(t.responseText)},this.options.formatDataFunctionIsAsync?i.then(r=>t.send(JSON.stringify(r))):t.send(i),this._render()}_getTotalProgress(o=0){if(this.options.removeAfterUpload)return o;let e=this.getNotUploadedItems().length,t=e?this.queue.length-e:this.queue.length,i=100/this.queue.length,r=o*i/100;return Math.round(t*i+r)}_getFilters(o){if(!o)return this.options?.filters||[];if(Array.isArray(o))return o;if(typeof o=="string"){let e=o.match(/[^\s,]+/g);return this.options?.filters||[].filter(t=>e?.indexOf(t.name)!==-1)}return this.options?.filters||[]}_render(){}_queueLimitFilter(){return this.options.queueLimit===void 0||this.queue.length(this._failFilterIndex&&this._failFilterIndex++,i.fn.call(this,o,t))):!0}_isSuccessCode(o){return o>=200&&o<300||o===304}_transformResponse(o,e){return o}_parseHeaders(o){let e={},t,i,r;return o&&o.split(` +`).map(s=>{r=s.indexOf(":"),t=s.slice(0,r).trim().toLowerCase(),i=s.slice(r+1).trim(),t&&(e[t]=e[t]?e[t]+", "+i:i)}),e}_onWhenAddingFileFailed(o,e,t){this.onWhenAddingFileFailed(o,e,t)}_onAfterAddingFile(o){this.onAfterAddingFile(o)}_onAfterAddingAll(o){this.onAfterAddingAll(o)}_onBeforeUploadItem(o){o._onBeforeUpload(),this.onBeforeUploadItem(o)}_onBuildItemForm(o,e){o._onBuildForm(e),this.onBuildItemForm(o,e)}_onProgressItem(o,e){let t=this._getTotalProgress(e);this.progress=t,o._onProgress(e),this.onProgressItem(o,e),this.onProgressAll(t),this._render()}_onSuccessItem(o,e,t,i){o._onSuccess(e,t,i),this.onSuccessItem(o,e,t,i)}_onCancelItem(o,e,t,i){o._onCancel(e,t,i),this.onCancelItem(o,e,t,i)}};var Wc=(()=>{class n{constructor(e){this.onFileSelected=new ve,this.element=e}getOptions(){return this.uploader?.options}getFilters(){return""}isEmptyAfterSelection(){return!!this.element.nativeElement.attributes.multiple}onChange(){let e=this.element.nativeElement.files,t=this.getOptions(),i=this.getFilters();this.uploader?.addToQueue(e,t,i),this.onFileSelected.emit(e),this.isEmptyAfterSelection()&&(this.element.nativeElement.value="")}}return n.\u0275fac=function(e){return new(e||n)(wt(Qt))},n.\u0275dir=yt({type:n,selectors:[["","ng2FileSelect",""]],hostBindings:function(e,t){e&1&&_("change",function(){return t.onChange()})},inputs:{uploader:"uploader"},outputs:{onFileSelected:"onFileSelected"},standalone:!1}),n})(),kl=(()=>{class n{}return n.\u0275fac=function(e){return new(e||n)},n.\u0275mod=nn({type:n}),n.\u0275inj=tn({imports:[fe]}),n})();var yu=class{visible;error;clear;constructor(o,e,t=!1){this.visible=o,this.error=e,this.clear=t}},lo=(()=>{class n{state=new sn(new yu(!1));constructor(){}setError(e){this.state.next(new yu(!1,e.error))}clear(){this.state.next(new yu(!1,null,!0))}activate(){this.state.next(new yu(!0))}deactivate(){this.state.next(new yu(!1))}static \u0275fac=function(t){return new(t||n)};static \u0275prov=J({token:n,factory:n.\u0275fac})}return n})();function Ite(n,o){n&1&&(a(0,"div",1),z(1,"mat-spinner",3),l())}function Ate(n,o){if(n&1){let e=L();a(0,"div",2)(1,"div",4)(2,"mat-icon"),c(3,"error_outline"),l()(),a(4,"div"),c(5),l(),a(6,"div")(7,"button",5),_("click",function(){w(e);let i=g(2);return M(i.refresh())}),a(8,"mat-icon"),c(9,"refresh"),l()(),a(10,"button",6)(11,"mat-icon"),c(12,"home"),l()()()()}if(n&2){let e,t=g(2);p(5),Z("Error occurred: ",(e=t.error())==null?null:e.message)}}function Nte(n,o){if(n&1&&(a(0,"div",0),E(1,Ite,2,0,"div",1),E(2,Ate,13,1,"div",2),l()),n&2){let e=g();p(),O(e.visible()&&!e.error()?1:-1),p(),O(e.error()?2:-1)}}var Lp=(()=>{class n{progressService=f(lo);router=f(It);visible=R(!1);error=R(null);routerSubscription;ngOnInit(){this.progressService.state.subscribe(e=>{this.visible.set(e.visible),e.error&&!this.error()&&this.error.set(e.error),e.clear&&this.error.set(null)}),this.routerSubscription=this.router.events.subscribe(()=>{this.progressService.clear()})}refresh(){this.router.navigateByUrl(this.router.url)}ngOnDestroy(){this.routerSubscription.unsubscribe()}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["app-progress"]],decls:1,vars:1,consts:[[1,"overlay"],[1,"loading-spinner"],[1,"error-state"],["color","primary"],[1,"error-icon"],["mat-button","","matTooltip","Refresh page","matTooltipClass","custom-tooltip",3,"click"],["mat-button","","routerLink","/","matTooltip","Go to home","matTooltipClass","custom-tooltip"]],template:function(t,i){t&1&&E(0,Nte,3,2,"div",0),t&2&&O(i.visible()||i.error()?0:-1)},dependencies:[fe,ot,dt,ne,pe,X,_e,at,pt,fo],styles:[".overlay[_ngcontent-%COMP%]{position:fixed;width:100%;height:100%;inset:0;background-color:color-mix(in srgb,var(--mat-sys-shadow) 50%,transparent);z-index:2000}.loading-spinner[_ngcontent-%COMP%], .error-state[_ngcontent-%COMP%]{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}.error-state[_ngcontent-%COMP%] div[_ngcontent-%COMP%]{text-align:center}.error-icon[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{font-size:64px;width:64px;height:64px}"],changeDetection:0})}return n})();var fy=(()=>{class n{document;router=f(It);controllerService=f(_t);progressService=f(lo);constructor(e){this.document=e}ngOnInit(){this.progressService.activate(),setTimeout(()=>{let e;parseInt(this.document.location.port,10)?e=parseInt(this.document.location.port,10):this.document.location.protocol=="https:"?e=443:e=80,this.controllerService.getLocalController(this.document.location.hostname,e).then(t=>{this.router.navigate(["/controller",t.id,"projects"]),this.progressService.deactivate()})},100)}static \u0275fac=function(t){return new(t||n)(wt(Eo))};static \u0275cmp=B({type:n,selectors:[["app-bundled-controller-finder"]],decls:1,vars:0,template:function(t,i){t&1&&z(0,"app-progress")},dependencies:[Lp],encapsulation:2,changeDetection:0})}return n})();var Fte=[[["caption"]],[["colgroup"],["col"]],"*"],Rte=["caption","colgroup, col","*"];function Lte(n,o){n&1&&Zt(0,2)}function zte(n,o){n&1&&(a(0,"thead",0),Ci(1,1),l(),a(2,"tbody",2),Ci(3,3)(4,4),l(),a(5,"tfoot",0),Ci(6,5),l())}function Vte(n,o){n&1&&Ci(0,1)(1,3)(2,4)(3,5)}var vn=(()=>{class n extends yE{stickyCssClass="mat-mdc-table-sticky";needsPositionStickyOnElement=!1;static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275cmp=B({type:n,selectors:[["mat-table"],["table","mat-table",""]],hostAttrs:[1,"mat-mdc-table","mdc-data-table__table"],hostVars:2,hostBindings:function(t,i){t&2&&se("mat-table-fixed-layout",i.fixedLayout)},exportAs:["matTable"],features:[un([{provide:yE,useExisting:n},{provide:oc,useExisting:n},{provide:B0,useValue:null}]),li],ngContentSelectors:Rte,decls:5,vars:2,consts:[["role","rowgroup"],["headerRowOutlet",""],["role","rowgroup",1,"mdc-data-table__content"],["rowOutlet",""],["noDataRowOutlet",""],["footerRowOutlet",""]],template:function(t,i){t&1&&(ci(Fte),Zt(0),Zt(1,1),E(2,Lte,1,0),E(3,zte,7,0)(4,Vte,4,0)),t&2&&(p(2),O(i._isServer?2:-1),p(),O(i._isNativeHtmlTable?3:4))},dependencies:[CE,fE,xE,vE],styles:[`.mat-mdc-table-sticky { + position: sticky !important; +} + +mat-table { + display: block; +} + +mat-header-row { + min-height: var(--mat-table-header-container-height, 56px); +} + +mat-row { + min-height: var(--mat-table-row-item-container-height, 52px); +} + +mat-footer-row { + min-height: var(--mat-table-footer-container-height, 52px); +} + +mat-row, mat-header-row, mat-footer-row { + display: flex; + border-width: 0; + border-bottom-width: 1px; + border-style: solid; + align-items: center; + box-sizing: border-box; +} + +mat-cell:first-of-type, mat-header-cell:first-of-type, mat-footer-cell:first-of-type { + padding-left: 24px; +} +[dir=rtl] mat-cell:first-of-type:not(:only-of-type), [dir=rtl] mat-header-cell:first-of-type:not(:only-of-type), [dir=rtl] mat-footer-cell:first-of-type:not(:only-of-type) { + padding-left: 0; + padding-right: 24px; +} +mat-cell:last-of-type, mat-header-cell:last-of-type, mat-footer-cell:last-of-type { + padding-right: 24px; +} +[dir=rtl] mat-cell:last-of-type:not(:only-of-type), [dir=rtl] mat-header-cell:last-of-type:not(:only-of-type), [dir=rtl] mat-footer-cell:last-of-type:not(:only-of-type) { + padding-right: 0; + padding-left: 24px; +} + +mat-cell, mat-header-cell, mat-footer-cell { + flex: 1; + display: flex; + align-items: center; + overflow: hidden; + word-wrap: break-word; + min-height: inherit; +} + +.mat-mdc-table { + min-width: 100%; + border: 0; + border-spacing: 0; + table-layout: auto; + white-space: normal; + background-color: var(--mat-table-background-color, var(--mat-sys-surface)); +} + +.mat-table-fixed-layout { + table-layout: fixed; +} + +.mdc-data-table__cell { + box-sizing: border-box; + overflow: hidden; + text-align: start; + text-overflow: ellipsis; +} + +.mdc-data-table__cell, +.mdc-data-table__header-cell { + padding: 0 16px; +} + +.mat-mdc-header-row { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + height: var(--mat-table-header-container-height, 56px); + color: var(--mat-table-header-headline-color, var(--mat-sys-on-surface, rgba(0, 0, 0, 0.87))); + font-family: var(--mat-table-header-headline-font, var(--mat-sys-title-small-font, Roboto, sans-serif)); + line-height: var(--mat-table-header-headline-line-height, var(--mat-sys-title-small-line-height)); + font-size: var(--mat-table-header-headline-size, var(--mat-sys-title-small-size, 14px)); + font-weight: var(--mat-table-header-headline-weight, var(--mat-sys-title-small-weight, 500)); +} + +.mat-mdc-row { + height: var(--mat-table-row-item-container-height, 52px); + color: var(--mat-table-row-item-label-text-color, var(--mat-sys-on-surface, rgba(0, 0, 0, 0.87))); +} + +.mat-mdc-row, +.mdc-data-table__content { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-family: var(--mat-table-row-item-label-text-font, var(--mat-sys-body-medium-font, Roboto, sans-serif)); + line-height: var(--mat-table-row-item-label-text-line-height, var(--mat-sys-body-medium-line-height)); + font-size: var(--mat-table-row-item-label-text-size, var(--mat-sys-body-medium-size, 14px)); + font-weight: var(--mat-table-row-item-label-text-weight, var(--mat-sys-body-medium-weight)); +} + +.mat-mdc-footer-row { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + height: var(--mat-table-footer-container-height, 52px); + color: var(--mat-table-row-item-label-text-color, var(--mat-sys-on-surface, rgba(0, 0, 0, 0.87))); + font-family: var(--mat-table-footer-supporting-text-font, var(--mat-sys-body-medium-font, Roboto, sans-serif)); + line-height: var(--mat-table-footer-supporting-text-line-height, var(--mat-sys-body-medium-line-height)); + font-size: var(--mat-table-footer-supporting-text-size, var(--mat-sys-body-medium-size, 14px)); + font-weight: var(--mat-table-footer-supporting-text-weight, var(--mat-sys-body-medium-weight)); + letter-spacing: var(--mat-table-footer-supporting-text-tracking, var(--mat-sys-body-medium-tracking)); +} + +.mat-mdc-header-cell { + border-bottom-color: var(--mat-table-row-item-outline-color, var(--mat-sys-outline, rgba(0, 0, 0, 0.12))); + border-bottom-width: var(--mat-table-row-item-outline-width, 1px); + border-bottom-style: solid; + letter-spacing: var(--mat-table-header-headline-tracking, var(--mat-sys-title-small-tracking)); + font-weight: inherit; + line-height: inherit; + box-sizing: border-box; + text-overflow: ellipsis; + overflow: hidden; + outline: none; + text-align: start; +} +.mdc-data-table__row:last-child > .mat-mdc-header-cell { + border-bottom: none; +} + +.mat-mdc-cell { + border-bottom-color: var(--mat-table-row-item-outline-color, var(--mat-sys-outline, rgba(0, 0, 0, 0.12))); + border-bottom-width: var(--mat-table-row-item-outline-width, 1px); + border-bottom-style: solid; + letter-spacing: var(--mat-table-row-item-label-text-tracking, var(--mat-sys-body-medium-tracking)); + line-height: inherit; +} +.mdc-data-table__row:last-child > .mat-mdc-cell { + border-bottom: none; +} + +.mat-mdc-footer-cell { + letter-spacing: var(--mat-table-row-item-label-text-tracking, var(--mat-sys-body-medium-tracking)); +} + +mat-row.mat-mdc-row, +mat-header-row.mat-mdc-header-row, +mat-footer-row.mat-mdc-footer-row { + border-bottom: none; +} + +.mat-mdc-table tbody, +.mat-mdc-table tfoot, +.mat-mdc-table thead, +.mat-mdc-cell, +.mat-mdc-footer-cell, +.mat-mdc-header-row, +.mat-mdc-row, +.mat-mdc-footer-row, +.mat-mdc-table .mat-mdc-header-cell { + background: inherit; +} + +.mat-mdc-table mat-header-row.mat-mdc-header-row, +.mat-mdc-table mat-row.mat-mdc-row, +.mat-mdc-table mat-footer-row.mat-mdc-footer-cell { + height: unset; +} + +mat-header-cell.mat-mdc-header-cell, +mat-cell.mat-mdc-cell, +mat-footer-cell.mat-mdc-footer-cell { + align-self: stretch; +} +`],encapsulation:2})}return n})(),xn=(()=>{class n extends W1{static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275dir=yt({type:n,selectors:[["","matCellDef",""]],features:[un([{provide:W1,useExisting:n}]),li]})}return n})(),yn=(()=>{class n extends G1{static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275dir=yt({type:n,selectors:[["","matHeaderCellDef",""]],features:[un([{provide:G1,useExisting:n}]),li]})}return n})();var bn=(()=>{class n extends Fp{get name(){return this._name}set name(e){this._setNameInput(e)}_updateColumnCssClassName(){super._updateColumnCssClassName(),this._columnCssClassName.push(`mat-column-${this.cssClassFriendlyName}`)}static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275dir=yt({type:n,selectors:[["","matColumnDef",""]],inputs:{name:[0,"matColumnDef","name"]},features:[un([{provide:Fp,useExisting:n}]),li]})}return n})(),wn=(()=>{class n extends g8{static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275dir=yt({type:n,selectors:[["mat-header-cell"],["th","mat-header-cell",""]],hostAttrs:["role","columnheader",1,"mat-mdc-header-cell","mdc-data-table__header-cell"],features:[li]})}return n})();var Mn=(()=>{class n extends h8{static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275dir=yt({type:n,selectors:[["mat-cell"],["td","mat-cell",""]],hostAttrs:[1,"mat-mdc-cell","mdc-data-table__cell"],features:[li]})}return n})();var Sn=(()=>{class n extends j0{static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275dir=yt({type:n,selectors:[["","matHeaderRowDef",""]],inputs:{columns:[0,"matHeaderRowDef","columns"],sticky:[2,"matHeaderRowDefSticky","sticky",At]},features:[un([{provide:j0,useExisting:n}]),li]})}return n})();var kn=(()=>{class n extends q1{static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275dir=yt({type:n,selectors:[["","matRowDef",""]],inputs:{columns:[0,"matRowDefColumns","columns"],when:[0,"matRowDefWhen","when"]},features:[un([{provide:q1,useExisting:n}]),li]})}return n})(),Pn=(()=>{class n extends hE{static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275cmp=B({type:n,selectors:[["mat-header-row"],["tr","mat-header-row",""]],hostAttrs:["role","row",1,"mat-mdc-header-row","mdc-data-table__header-row"],exportAs:["matHeaderRow"],features:[un([{provide:hE,useExisting:n}]),li],decls:1,vars:0,consts:[["cdkCellOutlet",""]],template:function(t,i){t&1&&Ci(0,0)},dependencies:[_u],encapsulation:2})}return n})();var Tn=(()=>{class n extends _E{static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275cmp=B({type:n,selectors:[["mat-row"],["tr","mat-row",""]],hostAttrs:["role","row",1,"mat-mdc-row","mdc-data-table__row"],exportAs:["matRow"],features:[un([{provide:_E,useExisting:n}]),li],decls:1,vars:0,consts:[["cdkCellOutlet",""]],template:function(t,i){t&1&&Ci(0,0)},dependencies:[_u],encapsulation:2})}return n})();var _n=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({imports:[Q1,zi]})}return n})(),Bte=9007199254740991,Da=class extends Am{_data;_renderData=new sn([]);_filter=new sn("");_internalPageChanges=new Ye;_renderChangesSubscription=null;filteredData;get data(){return this._data.value}set data(o){o=Array.isArray(o)?o:[],this._data.next(o),this._renderChangesSubscription||this._filterData(o)}get filter(){return this._filter.value}set filter(o){this._filter.next(o),this._renderChangesSubscription||this._filterData(this.data)}get sort(){return this._sort}set sort(o){this._sort=o,this._updateChangeSubscription()}_sort;get paginator(){return this._paginator}set paginator(o){this._paginator=o,this._updateChangeSubscription()}_paginator;sortingDataAccessor=(o,e)=>{let t=o[e];if(fA(t)){let i=Number(t);return i{let t=e.active,i=e.direction;return!t||i==""?o:o.sort((r,s)=>{let d=this.sortingDataAccessor(r,t),m=this.sortingDataAccessor(s,t),u=typeof d,h=typeof m;u!==h&&(u==="number"&&(d+=""),h==="number"&&(m+=""));let v=0;return d!=null&&m!=null?d>m?v=1:d{let t=e.trim().toLowerCase();return Object.values(o).some(i=>`${i}`.toLowerCase().includes(t))};constructor(o=[]){super(),this._data=new sn(o),this._updateChangeSubscription()}_updateChangeSubscription(){let o=this._sort?io(this._sort.sortChange,this._sort.initialized):Ut(null),e=this._paginator?io(this._paginator.page,this._internalPageChanges,this._paginator.initialized):Ut(null),t=this._data,i=da([t,this._filter]).pipe(Pt(([d])=>this._filterData(d))),r=da([i,o]).pipe(Pt(([d])=>this._orderData(d))),s=da([r,e]).pipe(Pt(([d])=>this._pageData(d)));this._renderChangesSubscription?.unsubscribe(),this._renderChangesSubscription=s.subscribe(d=>this._renderData.next(d))}_filterData(o){return this.filteredData=this.filter==null||this.filter===""?o:o.filter(e=>this.filterPredicate(e,this.filter)),this.paginator&&this._updatePaginator(this.filteredData.length),this.filteredData}_orderData(o){return this.sort?this.sortData(o.slice(),this.sort):o}_pageData(o){if(!this.paginator)return o;let e=this.paginator.pageIndex*this.paginator.pageSize;return o.slice(e,e+this.paginator.pageSize)}_updatePaginator(o){Promise.resolve().then(()=>{let e=this.paginator;if(e&&(e.length=o,e.pageIndex>0)){let t=Math.ceil(e.length/e.pageSize)-1||0,i=Math.min(e.pageIndex,t);i!==e.pageIndex&&(e.pageIndex=i,this._internalPageChanges.next())}})}connect(){return this._renderChangesSubscription||this._updateChangeSubscription(),this._renderData}disconnect(){this._renderChangesSubscription?.unsubscribe(),this._renderChangesSubscription=null}};var xR=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["ng-component"]],hostAttrs:["cdk-text-field-style-loader",""],decls:0,vars:0,template:function(t,i){},styles:[`textarea.cdk-textarea-autosize { + resize: none; +} + +textarea.cdk-textarea-autosize-measuring { + padding: 2px 0 !important; + box-sizing: content-box !important; + height: auto !important; + overflow: hidden !important; +} + +textarea.cdk-textarea-autosize-measuring-firefox { + padding: 2px 0 !important; + box-sizing: content-box !important; + height: 0 !important; +} + +@keyframes cdk-text-field-autofill-start { /*!*/ } +@keyframes cdk-text-field-autofill-end { /*!*/ } +.cdk-text-field-autofill-monitored:-webkit-autofill { + animation: cdk-text-field-autofill-start 0s 1ms; +} + +.cdk-text-field-autofill-monitored:not(:-webkit-autofill) { + animation: cdk-text-field-autofill-end 0s 1ms; +} +`],encapsulation:2,changeDetection:0})}return n})(),jte={passive:!0},yR=(()=>{class n{_platform=f(Vr);_ngZone=f(Ki);_renderer=f(Dm).createRenderer(null,null);_styleLoader=f(kr);_monitoredElements=new Map;constructor(){}monitor(e){if(!this._platform.isBrowser)return Yu;this._styleLoader.load(xR);let t=sl(e),i=this._monitoredElements.get(t);if(i)return i.subject;let r=new Ye,s="cdk-text-field-autofilled",d=u=>{u.animationName==="cdk-text-field-autofill-start"&&!t.classList.contains(s)?(t.classList.add(s),this._ngZone.run(()=>r.next({target:u.target,isAutofilled:!0}))):u.animationName==="cdk-text-field-autofill-end"&&t.classList.contains(s)&&(t.classList.remove(s),this._ngZone.run(()=>r.next({target:u.target,isAutofilled:!1})))},m=this._ngZone.runOutsideAngular(()=>(t.classList.add("cdk-text-field-autofill-monitored"),this._renderer.listen(t,"animationstart",d,jte)));return this._monitoredElements.set(t,{subject:r,unlisten:m}),r}stopMonitoring(e){let t=sl(e),i=this._monitoredElements.get(t);i&&(i.unlisten(),i.subject.complete(),t.classList.remove("cdk-text-field-autofill-monitored"),t.classList.remove("cdk-text-field-autofilled"),this._monitoredElements.delete(t))}ngOnDestroy(){this._monitoredElements.forEach((e,t)=>this.stopMonitoring(t))}static \u0275fac=function(t){return new(t||n)};static \u0275prov=J({token:n,factory:n.\u0275fac,providedIn:"root"})}return n})();var Gc=(()=>{class n{_elementRef=f(Qt);_platform=f(Vr);_ngZone=f(Ki);_renderer=f(Ui);_resizeEvents=new Ye;_previousValue;_initialHeight;_destroyed=new Ye;_listenerCleanups;_minRows;_maxRows;_enabled=!0;_previousMinRows=-1;_textareaElement;get minRows(){return this._minRows}set minRows(e){this._minRows=ap(e),this._setMinHeight()}get maxRows(){return this._maxRows}set maxRows(e){this._maxRows=ap(e),this._setMaxHeight()}get enabled(){return this._enabled}set enabled(e){this._enabled!==e&&((this._enabled=e)?this.resizeToFitContent(!0):this.reset())}get placeholder(){return this._textareaElement.placeholder}set placeholder(e){this._cachedPlaceholderHeight=void 0,e?this._textareaElement.setAttribute("placeholder",e):this._textareaElement.removeAttribute("placeholder"),this._cacheTextareaPlaceholderHeight()}_cachedLineHeight;_cachedPlaceholderHeight;_document=f(Eo);_hasFocus=!1;_isViewInited=!1;constructor(){f(kr).load(xR),this._textareaElement=this._elementRef.nativeElement}_setMinHeight(){let e=this.minRows&&this._cachedLineHeight?`${this.minRows*this._cachedLineHeight}px`:null;e&&(this._textareaElement.style.minHeight=e)}_setMaxHeight(){let e=this.maxRows&&this._cachedLineHeight?`${this.maxRows*this._cachedLineHeight}px`:null;e&&(this._textareaElement.style.maxHeight=e)}ngAfterViewInit(){this._platform.isBrowser&&(this._initialHeight=this._textareaElement.style.height,this.resizeToFitContent(),this._ngZone.runOutsideAngular(()=>{this._listenerCleanups=[this._renderer.listen("window","resize",()=>this._resizeEvents.next()),this._renderer.listen(this._textareaElement,"focus",this._handleFocusEvent),this._renderer.listen(this._textareaElement,"blur",this._handleFocusEvent)],this._resizeEvents.pipe(rl(16)).subscribe(()=>{this._cachedLineHeight=this._cachedPlaceholderHeight=void 0,this.resizeToFitContent(!0)})}),this._isViewInited=!0,this.resizeToFitContent(!0))}ngOnDestroy(){this._listenerCleanups?.forEach(e=>e()),this._resizeEvents.complete(),this._destroyed.next(),this._destroyed.complete()}_cacheTextareaLineHeight(){if(this._cachedLineHeight)return;let e=this._textareaElement.cloneNode(!1),t=e.style;e.rows=1,t.position="absolute",t.visibility="hidden",t.border="none",t.padding="0",t.height="",t.minHeight="",t.maxHeight="",t.top=t.bottom=t.left=t.right="auto",t.overflow="hidden",this._textareaElement.parentNode.appendChild(e),this._cachedLineHeight=e.clientHeight,e.remove(),this._setMinHeight(),this._setMaxHeight()}_measureScrollHeight(){let e=this._textareaElement,t=e.style.marginBottom||"",i=this._platform.FIREFOX,r=this._hasFocus,s=i?"cdk-textarea-autosize-measuring-firefox":"cdk-textarea-autosize-measuring";r&&(e.style.marginBottom=`${e.clientHeight}px`),e.classList.add(s);let d=e.scrollHeight-4;return e.classList.remove(s),r&&(e.style.marginBottom=t),d}_cacheTextareaPlaceholderHeight(){if(!this._isViewInited||this._cachedPlaceholderHeight!=null)return;if(!this.placeholder){this._cachedPlaceholderHeight=0;return}let e=this._textareaElement.value;this._textareaElement.value=this._textareaElement.placeholder,this._cachedPlaceholderHeight=this._measureScrollHeight(),this._textareaElement.value=e}_handleFocusEvent=e=>{this._hasFocus=e.type==="focus"};ngDoCheck(){this._platform.isBrowser&&this.resizeToFitContent()}resizeToFitContent(e=!1){if(!this._enabled||(this._cacheTextareaLineHeight(),this._cacheTextareaPlaceholderHeight(),!this._cachedLineHeight))return;let t=this._elementRef.nativeElement,i=t.value;if(!e&&this._minRows===this._previousMinRows&&i===this._previousValue)return;let r=this._measureScrollHeight(),s=Math.max(r,this._cachedPlaceholderHeight||0);t.style.height=`${s}px`,this._ngZone.runOutsideAngular(()=>{typeof requestAnimationFrame<"u"?requestAnimationFrame(()=>this._scrollToCaretPosition(t)):setTimeout(()=>this._scrollToCaretPosition(t))}),this._previousValue=i,this._previousMinRows=this._minRows}reset(){this._initialHeight!==void 0&&(this._textareaElement.style.height=this._initialHeight)}_noopInputHandler(){}_scrollToCaretPosition(e){let{selectionStart:t,selectionEnd:i}=e;!this._destroyed.isStopped&&this._hasFocus&&e.setSelectionRange(t,i)}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["textarea","cdkTextareaAutosize",""]],hostAttrs:["rows","1",1,"cdk-textarea-autosize"],hostBindings:function(t,i){t&1&&_("input",function(){return i._noopInputHandler()})},inputs:{minRows:[0,"cdkAutosizeMinRows","minRows"],maxRows:[0,"cdkAutosizeMaxRows","maxRows"],enabled:[2,"cdkTextareaAutosize","enabled",At],placeholder:"placeholder"},exportAs:["cdkTextareaAutosize"]})}return n})(),bR=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({})}return n})();var wR=new mn("MAT_INPUT_VALUE_ACCESSOR");var $te=["button","checkbox","file","hidden","image","radio","range","reset","submit"],Hte=new mn("MAT_INPUT_CONFIG"),Pe=(()=>{class n{_elementRef=f(Qt);_platform=f(Vr);ngControl=f($v,{optional:!0,self:!0});_autofillMonitor=f(yR);_ngZone=f(Ki);_formField=f(mg,{optional:!0});_renderer=f(Ui);_uid=f(cr).getId("mat-input-");_previousNativeValue;_inputValueAccessor;_signalBasedValueAccessor;_previousPlaceholder=null;_errorStateTracker;_config=f(Hte,{optional:!0});_cleanupIosKeyup;_cleanupWebkitWheel;_isServer=!1;_isNativeSelect=!1;_isTextarea=!1;_isInFormField=!1;focused=!1;stateChanges=new Ye;controlType="mat-input";autofilled=!1;get disabled(){return this._disabled}set disabled(e){this._disabled=Pa(e),this.focused&&(this.focused=!1,this.stateChanges.next())}_disabled=!1;get id(){return this._id}set id(e){this._id=e||this._uid}_id;placeholder;name;get required(){return this._required??this.ngControl?.control?.hasValidator(We.required)??!1}set required(e){this._required=Pa(e)}_required;get type(){return this._type}set type(e){this._type=e||"text",this._validateType(),!this._isTextarea&&Ak().has(this._type)&&(this._elementRef.nativeElement.type=this._type)}_type="text";get errorStateMatcher(){return this._errorStateTracker.matcher}set errorStateMatcher(e){this._errorStateTracker.matcher=e}userAriaDescribedBy;get value(){return this._signalBasedValueAccessor?this._signalBasedValueAccessor.value():this._inputValueAccessor.value}set value(e){e!==this.value&&(this._signalBasedValueAccessor?this._signalBasedValueAccessor.value.set(e):this._inputValueAccessor.value=e,this.stateChanges.next())}get readonly(){return this._readonly}set readonly(e){this._readonly=Pa(e)}_readonly=!1;disabledInteractive;get errorState(){return this._errorStateTracker.errorState}set errorState(e){this._errorStateTracker.errorState=e}_neverEmptyInputTypes=["date","datetime","datetime-local","month","time","week"].filter(e=>Ak().has(e));constructor(){let e=f(Yi,{optional:!0}),t=f(Ht,{optional:!0}),i=f(Sd),r=f(wR,{optional:!0,self:!0}),s=this._elementRef.nativeElement,d=s.nodeName.toLowerCase();r?K4(r.value)?this._signalBasedValueAccessor=r:this._inputValueAccessor=r:this._inputValueAccessor=s,this._previousNativeValue=this.value,this.id=this.id,this._platform.IOS&&this._ngZone.runOutsideAngular(()=>{this._cleanupIosKeyup=this._renderer.listen(s,"keyup",this._iOSKeyupListener)}),this._errorStateTracker=new Zv(i,this.ngControl,t,e,this.stateChanges),this._isServer=!this._platform.isBrowser,this._isNativeSelect=d==="select",this._isTextarea=d==="textarea",this._isInFormField=!!this._formField,this.disabledInteractive=this._config?.disabledInteractive||!1,this._isNativeSelect&&(this.controlType=s.multiple?"mat-native-select-multiple":"mat-native-select"),this._signalBasedValueAccessor&&zn(()=>{this._signalBasedValueAccessor.value(),this.stateChanges.next()})}ngAfterViewInit(){this._platform.isBrowser&&this._autofillMonitor.monitor(this._elementRef.nativeElement).subscribe(e=>{this.autofilled=e.isAutofilled,this.stateChanges.next()})}ngOnChanges(){this.stateChanges.next()}ngOnDestroy(){this.stateChanges.complete(),this._platform.isBrowser&&this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement),this._cleanupIosKeyup?.(),this._cleanupWebkitWheel?.()}ngDoCheck(){this.ngControl&&(this.updateErrorState(),this.ngControl.disabled!==null&&this.ngControl.disabled!==this.disabled&&(this.disabled=this.ngControl.disabled,this.stateChanges.next())),this._dirtyCheckNativeValue(),this._dirtyCheckPlaceholder()}focus(e){this._elementRef.nativeElement.focus(e)}updateErrorState(){this._errorStateTracker.updateErrorState()}_focusChanged(e){if(e!==this.focused){if(!this._isNativeSelect&&e&&this.disabled&&this.disabledInteractive){let t=this._elementRef.nativeElement;t.type==="number"?(t.type="text",t.setSelectionRange(0,0),t.type="number"):t.setSelectionRange(0,0)}this.focused=e,this.stateChanges.next()}}_onInput(){}_dirtyCheckNativeValue(){let e=this._elementRef.nativeElement.value;this._previousNativeValue!==e&&(this._previousNativeValue=e,this.stateChanges.next())}_dirtyCheckPlaceholder(){let e=this._getPlaceholder();if(e!==this._previousPlaceholder){let t=this._elementRef.nativeElement;this._previousPlaceholder=e,e?t.setAttribute("placeholder",e):t.removeAttribute("placeholder")}}_getPlaceholder(){return this.placeholder||null}_validateType(){$te.indexOf(this._type)>-1}_isNeverEmpty(){return this._neverEmptyInputTypes.indexOf(this._type)>-1}_isBadInput(){let e=this._elementRef.nativeElement.validity;return e&&e.badInput}get empty(){return!this._isNeverEmpty()&&!this._elementRef.nativeElement.value&&!this._isBadInput()&&!this.autofilled}get shouldLabelFloat(){if(this._isNativeSelect){let e=this._elementRef.nativeElement,t=e.options[0];return this.focused||e.multiple||!this.empty||!!(e.selectedIndex>-1&&t&&t.label)}else return this.focused&&!this.disabled||!this.empty}get describedByIds(){return this._elementRef.nativeElement.getAttribute("aria-describedby")?.split(" ")||[]}setDescribedByIds(e){let t=this._elementRef.nativeElement;e.length?t.setAttribute("aria-describedby",e.join(" ")):t.removeAttribute("aria-describedby")}onContainerClick(){this.focused||this.focus()}_isInlineSelect(){let e=this._elementRef.nativeElement;return this._isNativeSelect&&(e.multiple||e.size>1)}_iOSKeyupListener=e=>{let t=e.target;!t.value&&t.selectionStart===0&&t.selectionEnd===0&&(t.setSelectionRange(1,1),t.setSelectionRange(0,0))};_getReadonlyAttribute(){return this._isNativeSelect?null:this.readonly||this.disabled&&this.disabledInteractive?"true":null}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["input","matInput",""],["textarea","matInput",""],["select","matNativeControl",""],["input","matNativeControl",""],["textarea","matNativeControl",""]],hostAttrs:[1,"mat-mdc-input-element"],hostVars:21,hostBindings:function(t,i){t&1&&_("focus",function(){return i._focusChanged(!0)})("blur",function(){return i._focusChanged(!1)})("input",function(){return i._onInput()}),t&2&&(ma("id",i.id)("disabled",i.disabled&&!i.disabledInteractive)("required",i.required),Ve("name",i.name||null)("readonly",i._getReadonlyAttribute())("aria-disabled",i.disabled&&i.disabledInteractive?"true":null)("aria-invalid",i.empty&&i.required?null:i.errorState)("aria-required",i.required)("id",i.id),se("mat-input-server",i._isServer)("mat-mdc-form-field-textarea-control",i._isInFormField&&i._isTextarea)("mat-mdc-form-field-input-control",i._isInFormField)("mat-mdc-input-disabled-interactive",i.disabledInteractive)("mdc-text-field__input",i._isInFormField)("mat-mdc-native-select-inline",i._isInlineSelect()))},inputs:{disabled:"disabled",id:"id",placeholder:"placeholder",name:"name",required:"required",type:"type",errorStateMatcher:"errorStateMatcher",userAriaDescribedBy:[0,"aria-describedby","userAriaDescribedBy"],value:"value",readonly:"readonly",disabledInteractive:[2,"disabledInteractive","disabledInteractive",At]},exportAs:["matInput"],features:[un([{provide:Yv,useExisting:n}]),In]})}return n})(),be=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({imports:[Me,Me,bR,zi]})}return n})();function Ute(n,o){if(n&1&&(a(0,"li"),c(1),l()),n&2){let e=o.$implicit;p(),N(e)}}function Wte(n,o){if(n&1&&(a(0,"ul",4),Q(1,Ute,2,1,"li",null,Ee),l()),n&2){let e=g();p(),K(e.data.details)}}function Gte(n,o){if(n&1&&(a(0,"p",5),c(1),l()),n&2){let e=g();p(),N(e.data.note)}}function qte(n,o){if(n&1){let e=L();a(0,"button",9),_("click",function(){w(e);let i=g();return M(i.onYesClick())}),c(1),l()}if(n&2){let e=g();se("destructive-button",e.tone==="danger"),p(),Z(" ",e.confirmButtonText," ")}}var Bt=(()=>{class n{dialogRef;data;constructor(e,t){this.dialogRef=e,this.data=t}get title(){return this.data.title||"Confirm action"}get confirmButtonText(){return this.data.confirmButtonText||"Confirm"}get cancelButtonText(){return this.data.cancelButtonText||"Cancel"}get tone(){return this.data.tone||"danger"}get icon(){return this.data.icon?this.data.icon:this.tone==="danger"?"delete_forever":this.tone==="warning"?"warning":"help"}onNoClick(){this.dialogRef.close(!1)}onYesClick(){this.dialogRef.close(!0)}static \u0275fac=function(t){return new(t||n)(wt(He),wt(Wt))};static \u0275cmp=B({type:n,selectors:[["app-confirmation-dialog"]],decls:16,vars:11,consts:[[1,"confirmation-dialog"],["mat-dialog-title","",1,"confirmation-dialog__header"],["aria-hidden","true",1,"confirmation-dialog__icon"],[1,"confirmation-dialog__content"],[1,"confirmation-dialog__details"],[1,"confirmation-dialog__note"],["align","end",1,"confirmation-dialog__actions"],["mat-stroked-button","","type","button","cdkFocusInitial","",1,"cancel-button",3,"click"],["mat-flat-button","","type","button",1,"confirm-button",3,"destructive-button"],["mat-flat-button","","type","button",1,"confirm-button",3,"click"]],template:function(t,i){t&1&&(a(0,"div",0)(1,"div",1)(2,"span",2)(3,"mat-icon"),c(4),l()(),a(5,"h2"),c(6),l()(),a(7,"mat-dialog-content",3)(8,"p"),c(9),l(),E(10,Wte,3,0,"ul",4),E(11,Gte,2,1,"p",5),l(),a(12,"mat-dialog-actions",6)(13,"button",7),_("click",function(){return i.onNoClick()}),c(14),l(),E(15,qte,2,3,"button",8),l()()),t&2&&(se("confirmation-dialog--danger",i.tone==="danger")("confirmation-dialog--warning",i.tone==="warning"),p(4),N(i.icon),p(2),N(i.title),p(3),N(i.data.message),p(),O(i.data.details!=null&&i.data.details.length?10:-1),p(),O(i.data.note?11:-1),p(3),Z(" ",i.cancelButtonText," "),p(),O(i.data.hideConfirm?-1:15))},dependencies:[fe,Se,Ge,qe,xt,ne,pe,X,_e],styles:[".confirmation-dialog[_ngcontent-%COMP%]{--confirmation-accent: var(--mat-sys-primary);color:var(--mat-sys-on-surface)}.confirmation-dialog--danger[_ngcontent-%COMP%]{--confirmation-accent: var(--mat-sys-error)}.confirmation-dialog--warning[_ngcontent-%COMP%]{--confirmation-accent: var(--mat-sys-tertiary)}.confirmation-dialog__header[_ngcontent-%COMP%]{min-height:var(--gns3-density-section-header-height);margin:0;padding:var(--gns3-density-section-header-padding);display:flex;align-items:center;gap:var(--gns3-density-space-3);box-sizing:border-box;color:var(--mat-sys-on-primary-container);background:var(--mat-sys-primary-container);border-bottom:1px solid var(--mat-sys-outline-variant)}.confirmation-dialog__header[_ngcontent-%COMP%] h2[_ngcontent-%COMP%]{margin:0;color:var(--mat-sys-on-primary-container);font-family:var(--gns3-density-section-header-font);font-size:var(--gns3-density-section-header-title-size);font-weight:650;line-height:1.2;letter-spacing:-.01em}.confirmation-dialog__icon[_ngcontent-%COMP%]{width:var(--gns3-density-dialog-icon-container);height:var(--gns3-density-dialog-icon-container);display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;color:var(--confirmation-accent);background:color-mix(in srgb,var(--confirmation-accent) 12%,transparent);border-radius:var(--gns3-density-radius-sm)}.confirmation-dialog__icon[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{width:var(--gns3-density-dialog-icon-size);height:var(--gns3-density-dialog-icon-size);font-size:var(--gns3-density-dialog-icon-size)}.confirmation-dialog__content[_ngcontent-%COMP%]{padding:var(--gns3-density-space-4);color:var(--mat-sys-on-surface-variant);font:var(--mat-sys-body-medium);font-size:var(--gns3-density-dialog-body-size);line-height:1.45}.confirmation-dialog__content[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:0;white-space:pre-line}.confirmation-dialog__details[_ngcontent-%COMP%]{max-height:180px;margin:var(--gns3-density-space-3) 0 0;padding:var(--gns3-density-space-2) var(--gns3-density-space-3) var(--gns3-density-space-2) calc(var(--gns3-density-space-5) + var(--gns3-density-space-2));overflow-y:auto;background:var(--mat-sys-surface-container);border:1px solid var(--mat-sys-outline-variant);border-radius:var(--gns3-density-radius-sm)}.confirmation-dialog__content[_ngcontent-%COMP%] .confirmation-dialog__note[_ngcontent-%COMP%]{margin-top:var(--gns3-density-space-3);color:var(--confirmation-accent);font-weight:600}.confirmation-dialog__actions[_ngcontent-%COMP%]{min-height:var(--gns3-density-section-header-height);padding:var(--gns3-density-space-2) var(--gns3-density-space-4);gap:var(--gns3-density-space-2);border-top:1px solid var(--mat-sys-outline-variant)}.confirmation-dialog__actions[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{min-width:96px;height:var(--gns3-density-button-height);padding-inline:var(--gns3-density-space-4);border-radius:var(--gns3-density-radius-sm);font:var(--mat-sys-label-large);font-size:var(--gns3-density-dialog-action-size)}.confirmation-dialog__actions[_ngcontent-%COMP%] .confirm-button[_ngcontent-%COMP%]:not(.destructive-button){color:var(--mat-sys-on-primary);background:var(--mat-sys-primary)}html[data-density=compact][_nghost-%COMP%] .confirmation-dialog__header[_ngcontent-%COMP%], html[data-density=compact] [_nghost-%COMP%] .confirmation-dialog__header[_ngcontent-%COMP%]{gap:var(--gns3-density-space-2)}html[data-density=compact][_nghost-%COMP%] .confirmation-dialog__content[_ngcontent-%COMP%], html[data-density=compact] [_nghost-%COMP%] .confirmation-dialog__content[_ngcontent-%COMP%]{padding:var(--gns3-density-space-3) var(--gns3-density-space-4)}html[data-density=compact][_nghost-%COMP%] .confirmation-dialog__actions[_ngcontent-%COMP%], html[data-density=compact] [_nghost-%COMP%] .confirmation-dialog__actions[_ngcontent-%COMP%]{min-height:var(--gns3-density-section-header-height);padding:var(--gns3-density-space-1) var(--gns3-density-space-4)}html[data-density=compact][_nghost-%COMP%] .confirmation-dialog__actions[_ngcontent-%COMP%] button[_ngcontent-%COMP%], html[data-density=compact] [_nghost-%COMP%] .confirmation-dialog__actions[_ngcontent-%COMP%] button[_ngcontent-%COMP%]{min-width:80px;padding-inline:var(--gns3-density-space-3)}"],changeDetection:0})}return n})();function Qte(n,o){n&1&&(a(0,"div",21),z(1,"mat-progress-spinner",24),a(2,"span"),c(3,"Loading computes\u2026"),l()())}function Kte(n,o){n&1&&(a(0,"mat-header-cell"),c(1,"Name"),l())}function Xte(n,o){if(n&1&&(a(0,"mat-cell")(1,"div",35)(2,"span",36)(3,"mat-icon"),c(4),l()(),a(5,"span",37)(6,"strong"),c(7),l(),a(8,"small"),c(9),l()()()()),n&2){let e=o.$implicit;p(4),N(e.compute_id==="local"?"computer":"dns"),p(3),N(e.name||e.compute_id),p(2),N(e.compute_id==="local"?"Local compute":e.compute_id)}}function Yte(n,o){n&1&&(a(0,"mat-header-cell"),c(1,"Status"),l())}function Zte(n,o){if(n&1&&(a(0,"mat-cell")(1,"span",38),z(2,"span"),c(3),l()()),n&2){let e=o.$implicit;p(),se("computes-page__status--connected",e.connected),C("matTooltip",e.last_error||""),p(2),Z(" ",e.connected?"Connected":"Disconnected"," ")}}function Jte(n,o){n&1&&(a(0,"mat-header-cell"),c(1,"Host"),l())}function ene(n,o){if(n&1&&(a(0,"mat-cell")(1,"span",39),c(2),l()()),n&2){let e=o.$implicit,t=g(2);p(),C("matTooltip",e.protocol+"//"+t.formatHost(e)),p(),Z(" ",t.formatHost(e)," ")}}function tne(n,o){n&1&&(a(0,"mat-header-cell"),c(1,"Platform"),l())}function nne(n,o){if(n&1&&(a(0,"mat-cell")(1,"span",40)(2,"mat-icon"),c(3,"memory"),l(),c(4),l()()),n&2){let e=o.$implicit,t=g(2);p(4),Z(" ",t.getPlatform(e)," ")}}function ine(n,o){n&1&&(a(0,"mat-header-cell"),c(1,"Resources"),l())}function one(n,o){if(n&1&&(a(0,"div",41)(1,"span",43)(2,"mat-icon"),c(3,"developer_board"),l(),c(4),l(),a(5,"span",44)(6,"mat-icon"),c(7,"memory"),l(),c(8),l(),a(9,"span",45)(10,"mat-icon"),c(11,"storage"),l(),c(12),l()()),n&2){let e=g().$implicit,t=g(2);p(4),N(t.formatPercent(e.cpu_usage_percent)),p(4),N(t.formatPercent(e.memory_usage_percent)),p(4),N(t.formatPercent(e.disk_usage_percent))}}function rne(n,o){n&1&&(a(0,"span",42),c(1,"Unavailable"),l())}function ane(n,o){if(n&1&&(a(0,"mat-cell"),E(1,one,13,3,"div",41)(2,rne,2,0,"span",42),l()),n&2){let e=o.$implicit;p(),O(e.connected?1:2)}}function sne(n,o){n&1&&(a(0,"mat-header-cell"),c(1,"Actions"),l())}function lne(n,o){if(n&1){let e=L();a(0,"div",46)(1,"button",48),_("click",function(){w(e);let i=g().$implicit,r=g(2);return M(r.connectCompute(i))}),a(2,"mat-icon"),c(3,"link"),l()(),a(4,"button",49),_("click",function(){w(e);let i=g().$implicit,r=g(2);return M(r.openEditDialog(i))}),a(5,"mat-icon"),c(6,"edit"),l()(),a(7,"button",50),_("click",function(){w(e);let i=g().$implicit,r=g(2);return M(r.deleteCompute(i))}),a(8,"mat-icon"),c(9,"delete"),l()()()}if(n&2){let e=g().$implicit;p(),Ve("aria-label","Connect "+(e.name||e.compute_id)),p(3),Ve("aria-label","Edit "+(e.name||e.compute_id)),p(3),Ve("aria-label","Delete "+(e.name||e.compute_id))}}function cne(n,o){n&1&&(a(0,"span",47),c(1," Controller managed "),l())}function dne(n,o){if(n&1&&(a(0,"mat-cell"),E(1,lne,10,3,"div",46)(2,cne,2,0,"span",47),l()),n&2){let e=o.$implicit;p(),O(e.compute_id!=="local"?1:2)}}function pne(n,o){n&1&&z(0,"mat-header-row")}function mne(n,o){if(n&1&&z(0,"mat-row"),n&2){let e=o.$implicit;se("computes-page__row--connected",e.connected)}}function une(n,o){if(n&1&&(a(0,"mat-table",22),Ae(1,25),ce(2,Kte,2,0,"mat-header-cell",26)(3,Xte,10,3,"mat-cell",27),Ne(),Ae(4,28),ce(5,Yte,2,0,"mat-header-cell",26)(6,Zte,4,4,"mat-cell",27),Ne(),Ae(7,29),ce(8,Jte,2,0,"mat-header-cell",26)(9,ene,3,2,"mat-cell",27),Ne(),Ae(10,30),ce(11,tne,2,0,"mat-header-cell",26)(12,nne,5,1,"mat-cell",27),Ne(),Ae(13,31),ce(14,ine,2,0,"mat-header-cell",26)(15,ane,3,1,"mat-cell",27),Ne(),Ae(16,32),ce(17,sne,2,0,"mat-header-cell",26)(18,dne,3,1,"mat-cell",27),Ne(),ce(19,pne,1,0,"mat-header-row",33)(20,mne,1,2,"mat-row",34),l()),n&2){let e=g();C("dataSource",e.filteredComputes())("trackBy",e.trackCompute),p(19),C("matHeaderRowDef",e.displayedColumns)("matHeaderRowDefSticky",!0),p(),C("matRowDefColumns",e.displayedColumns)}}function gne(n,o){n&1&&c(0," Try changing the search or status filter. ")}function hne(n,o){n&1&&c(0," Add a compute server to extend the controller's resources. ")}function _ne(n,o){if(n&1){let e=L();a(0,"button",52),_("click",function(){w(e);let i=g(2);return M(i.openAddDialog())}),a(1,"mat-icon"),c(2,"add"),l(),c(3," Add Compute "),l()}}function fne(n,o){if(n&1&&(a(0,"div",23)(1,"mat-icon"),c(2,"dns"),l(),a(3,"h3"),c(4,"No computes found"),l(),a(5,"p"),E(6,gne,1,0)(7,hne,1,0),l(),E(8,_ne,4,0,"button",51),l()),n&2){let e=g();p(6),O(e.searchText()||e.statusFilter()!=="all"?6:7),p(2),O(!e.searchText()&&e.statusFilter()==="all"?8:-1)}}var Cne=(n,o)=>o.key;function vne(n,o){if(n&1&&(a(0,"mat-option",6),c(1),l()),n&2){let e=o.$implicit;C("value",e.key),p(),N(e.name)}}function xne(n,o){n&1&&(a(0,"mat-error"),c(1,"You must select a protocol"),l())}function yne(n,o){n&1&&(a(0,"mat-error"),c(1,"You must enter a host"),l())}function bne(n,o){n&1&&(a(0,"mat-error"),c(1,"You must enter a port"),l())}function wne(n,o){n&1&&(a(0,"mat-error"),c(1,"Port must be between 1 and 65535"),l())}var SR=(()=>{class n{route=f(Lt);controllerService=f(_t);computeService=f(dl);notificationService=f(us);toasterService=f(re);dialog=f(Xe);cd=f(ie);controller;_computes=R([]);computes=Fe(()=>[...this._computes()].sort((t,i)=>t.compute_id==="local"?-1:i.compute_id==="local"?1:(t.name||"").localeCompare(i.name||"")));searchText=R("");statusFilter=R("all");filteredComputes=Fe(()=>{let e=this.searchText().trim().toLowerCase(),t=this.statusFilter();return this.computes().filter(i=>{let r=t==="all"||t==="connected"&&i.connected||t==="disconnected"&&!i.connected,s=[i.name,i.compute_id,i.host,i.port,i.protocol,i.capabilities?.platform,...i.capabilities?.node_types||[]].filter(d=>d!=null).join(" ").toLowerCase();return r&&(!e||s.includes(e))})});connectedCount=Fe(()=>this.computes().filter(e=>e.connected).length);displayedColumns=["name","status","host","platform","resources","actions"];loading=R(!0);subscription=new To;ngOnInit(){this.loadControllerAndComputes()}ngOnDestroy(){this.subscription.unsubscribe()}loadControllerAndComputes(){let e=this.route.snapshot.paramMap.get("controller_id");this.controllerService.get(parseInt(e,10)).then(t=>{if(this.controller=t,this.cd.markForCheck(),this.notificationService.hasCachedData()){let i=this.notificationService.getCachedComputes();this._computes.set(i),this.loading.set(!1),this.cd.markForCheck()}else this.loadComputes();this.subscription.add(this.notificationService.computeNotificationEmitter.subscribe(i=>{this.handleComputeNotification(i)})),this.subscription.add(this.notificationService.computeCacheUpdated.subscribe(i=>{this._computes.set(i),this.loading.set(!1),this.cd.markForCheck()}))},t=>{let i=t.error?.message||t.message||"Failed to load controller";this.toasterService.error(i),this.loading.set(!1),this.cd.markForCheck()})}handleComputeNotification(e){switch(e.action){case"compute.created":this._computes.update(t=>[...t,e.event]),this.toasterService.success(`Compute "${e.event.name}" added`);break;case"compute.updated":this._computes.update(t=>t.map(i=>i.compute_id===e.event.compute_id?e.event:i));break;case"compute.deleted":this._computes.update(t=>t.filter(i=>i.compute_id!==e.event.compute_id)),this.toasterService.success(`Compute "${e.event.name}" deleted`);break}this.cd.markForCheck()}loadComputes(){this.loading.set(!0),this.computeService.getComputes(this.controller).subscribe({next:e=>{this.notificationService.setInitialComputes(e),this._computes.set(e),this.loading.set(!1),this.cd.markForCheck()},error:e=>{let t=e.error?.message||e.message||"Failed to load computes";this.loading.set(!1),this.toasterService.error(t),this.cd.markForCheck()}})}openAddDialog(){this.dialog.open(MR,{panelClass:["base-dialog-panel","simple-dialog-panel","dialog-medium-panel"],autoFocus:!1,disableClose:!0,data:{controller:this.controller}}).afterClosed().subscribe(t=>{t&&this.computeService.createCompute(this.controller,t).subscribe({next:()=>{this.toasterService.success("Compute added successfully"),this.loadComputes()},error:i=>{let r=i.error?.message||i.message||"Failed to add compute";this.toasterService.error(r),this.cd.markForCheck()}})})}openEditDialog(e){this.computeService.getCompute(this.controller,e.compute_id).subscribe({next:t=>{this.dialog.open(MR,{panelClass:["base-dialog-panel","simple-dialog-panel","dialog-medium-panel"],autoFocus:!1,disableClose:!0,data:{controller:this.controller,compute:t}}).afterClosed().subscribe(r=>{r&&this.computeService.updateCompute(this.controller,e.compute_id,r).subscribe({next:()=>{this.toasterService.success("Compute updated successfully"),this.loadComputes()},error:s=>{let d=s.error?.message||s.message||"Failed to update compute";this.toasterService.error(d),this.cd.markForCheck()}})})},error:t=>{let i=t.error?.message||t.message||"Failed to load compute details";this.toasterService.error(i),this.cd.markForCheck()}})}deleteCompute(e){this.dialog.open(Bt,{panelClass:["base-confirmation-dialog-panel","confirmation-danger-panel","dialog-small-panel"],autoFocus:".cancel-button",disableClose:!0,data:{title:"Delete compute?",message:`Compute "${e.name||e.compute_id}" will be permanently deleted.`,note:"This action cannot be undone.",confirmButtonText:"Delete compute",tone:"danger"}}).afterClosed().subscribe(i=>{i&&this.computeService.deleteCompute(this.controller,e.compute_id).subscribe({next:()=>{this.toasterService.success("Compute deleted successfully"),this.loadComputes()},error:r=>{let s=r.error?.message||r.message||"Failed to delete compute";this.toasterService.error(s),this.cd.markForCheck()}})})}connectCompute(e){this.computeService.connectCompute(this.controller,e.compute_id).subscribe({next:()=>{this.toasterService.success("Connection request sent"),this.computeService.getCompute(this.controller,e.compute_id).subscribe({next:t=>{let i=this.computes().map(r=>r.compute_id===t.compute_id?t:r);this._computes.set(i),this.cd.markForCheck()},error:()=>{this.loadComputes()}})},error:t=>{let i=t.error?.message||t.message||"Failed to connect compute";this.toasterService.error(i),this.cd.markForCheck()}})}getStatusIcon(e){return e.connected?"check_circle":"cancel"}getStatusColor(e){return e.connected?"var(--mat-sys-primary)":"var(--mat-sys-error)"}formatPercent(e){return e!=null?`${e.toFixed(1)}%`:"--"}formatHost(e){return`${e.host}:${e.port}`}onSearchChange(e){this.searchText.set(e)}onStatusFilterChange(e){this.statusFilter.set(e)}trackCompute(e,t){return t.compute_id}getPlatform(e){return e.capabilities?.platform||"Unknown"}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["app-computes"]],decls:56,vars:10,consts:[[1,"computes-page"],[1,"computes-page__header"],[1,"computes-page__heading"],[1,"computes-page__title"],[1,"computes-page__header-actions"],["mat-raised-button","","color","primary",1,"computes-page__button",3,"click","disabled"],[1,"computes-page__toolbar"],["subscriptSizing","dynamic",1,"computes-page__search"],["matPrefix",""],["matInput","","placeholder","Search by name, host, platform, or node type",3,"input","value"],["subscriptSizing","dynamic",1,"computes-page__filter"],[3,"valueChange","value"],["value","all"],["value","connected"],["value","disconnected"],[1,"computes-page__main"],["aria-label","Compute summary",1,"computes-page__summary"],[1,"computes-page__summary-item"],[1,"computes-page__summary-item","computes-page__summary-item--connected"],[1,"computes-page__summary-item","computes-page__summary-item--disconnected"],[1,"computes-page__table-container"],[1,"computes-page__loading"],[3,"dataSource","trackBy"],[1,"computes-page__empty"],["diameter","42","mode","indeterminate"],["matColumnDef","name"],[4,"matHeaderCellDef"],[4,"matCellDef"],["matColumnDef","status"],["matColumnDef","host"],["matColumnDef","platform"],["matColumnDef","resources"],["matColumnDef","actions"],[4,"matHeaderRowDef","matHeaderRowDefSticky"],[3,"computes-page__row--connected",4,"matRowDef","matRowDefColumns"],[1,"computes-page__name-cell"],[1,"computes-page__compute-icon"],[1,"computes-page__name-copy"],[1,"computes-page__status",3,"matTooltip"],[1,"computes-page__host",3,"matTooltip"],[1,"computes-page__platform"],[1,"computes-page__resources"],[1,"computes-page__unavailable"],["matTooltip","CPU usage"],["matTooltip","Memory usage"],["matTooltip","Disk usage"],[1,"computes-page__actions-cell"],["matTooltip","The local compute is managed by the controller",1,"computes-page__managed-label"],["mat-icon-button","","matTooltip","Connect compute",1,"computes-page__action","computes-page__action--connect",3,"click"],["mat-icon-button","","matTooltip","Edit compute",1,"computes-page__action","computes-page__action--edit",3,"click"],["mat-icon-button","","matTooltip","Delete compute",1,"computes-page__action","computes-page__action--delete",3,"click"],["mat-raised-button","","color","primary",1,"computes-page__button"],["mat-raised-button","","color","primary",1,"computes-page__button",3,"click"]],template:function(t,i){t&1&&(a(0,"div",0)(1,"header",1)(2,"div",2)(3,"h1",3),c(4,"Computes"),l()(),a(5,"div",4)(6,"button",5),_("click",function(){return i.loadComputes()}),a(7,"mat-icon"),c(8,"refresh"),l(),c(9," Refresh "),l(),a(10,"button",5),_("click",function(){return i.openAddDialog()}),a(11,"mat-icon"),c(12,"add"),l(),c(13," Add Compute "),l()()(),a(14,"section",6)(15,"mat-form-field",7)(16,"mat-icon",8),c(17,"search"),l(),a(18,"input",9),_("input",function(s){return i.onSearchChange(s.target.value)}),l()(),a(19,"mat-form-field",10)(20,"mat-label"),c(21,"Status"),l(),a(22,"mat-select",11),_("valueChange",function(s){return i.onStatusFilterChange(s)}),a(23,"mat-option",12),c(24,"All statuses"),l(),a(25,"mat-option",13),c(26,"Connected"),l(),a(27,"mat-option",14),c(28,"Disconnected"),l()()()(),a(29,"main",15)(30,"section",16)(31,"div",17)(32,"mat-icon"),c(33,"dns"),l(),a(34,"span")(35,"strong"),c(36),l(),c(37," Total "),l()(),a(38,"div",18)(39,"mat-icon"),c(40,"check_circle"),l(),a(41,"span")(42,"strong"),c(43),l(),c(44," Connected "),l()(),a(45,"div",19)(46,"mat-icon"),c(47,"error_outline"),l(),a(48,"span")(49,"strong"),c(50),l(),c(51," Disconnected "),l()()(),a(52,"div",20),E(53,Qte,4,0,"div",21)(54,une,21,5,"mat-table",22)(55,fne,9,2,"div",23),l()()()),t&2&&(p(6),C("disabled",i.loading()),p(4),C("disabled",!i.controller),p(8),C("value",i.searchText()),p(4),C("value",i.statusFilter()),p(14),N(i.computes().length),p(7),N(i.connectedCount()),p(7),N(i.computes().length-i.connectedCount()),p(2),se("computes-page__table-container--loading",i.loading()),p(),O(i.loading()?53:i.filteredComputes().length?54:55))},dependencies:[fe,fn,X,_e,Be,ne,pe,rt,_n,vn,yn,Sn,bn,xn,kn,wn,Mn,Pn,Tn,at,pt,Se,Me,ke,ze,Go,be,Pe,st,mt,nt,ot,dt,Ft],styles:["[_nghost-%COMP%]{display:block;width:100%;background:transparent}.computes-page[_ngcontent-%COMP%]{animation:_ngcontent-%COMP%_computes-fade-in .4s ease-out}.computes-page__header[_ngcontent-%COMP%], .computes-page__toolbar[_ngcontent-%COMP%], .computes-page__main[_ngcontent-%COMP%]{max-width:1600px;margin-inline:auto}.computes-page__header[_ngcontent-%COMP%]{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:24px 20px 16px}.computes-page__heading[_ngcontent-%COMP%]{min-width:0}.computes-page__title[_ngcontent-%COMP%]{margin:0;color:var(--mat-sys-on-surface);font-size:32px;font-weight:500}.computes-page__header-actions[_ngcontent-%COMP%], .computes-page__toolbar[_ngcontent-%COMP%], .computes-page__resources[_ngcontent-%COMP%], .computes-page__actions-cell[_ngcontent-%COMP%]{display:flex;align-items:center}.computes-page__header-actions[_ngcontent-%COMP%]{flex:0 0 auto;gap:var(--gns3-density-space-3)}button.computes-page__button[_ngcontent-%COMP%]{height:var(--gns3-density-control-height);padding-inline:18px;border-radius:12px;font-size:15px;font-weight:500;letter-spacing:.3px;text-transform:none;transition:transform .2s ease,box-shadow .2s ease}button.computes-page__button[_ngcontent-%COMP%]:hover:not([disabled]){transform:translateY(-2px);box-shadow:0 4px 12px color-mix(in srgb,var(--mat-sys-primary) 25%,transparent)}.computes-page__toolbar[_ngcontent-%COMP%]{gap:var(--gns3-density-card-gap);padding:0 20px 8px}.computes-page__search[_ngcontent-%COMP%]{flex:1;min-width:260px}.computes-page__filter[_ngcontent-%COMP%]{flex:0 0 190px}.computes-page__main[_ngcontent-%COMP%]{padding:0 20px 24px}.computes-page__summary[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gns3-density-space-3);margin-bottom:var(--gns3-density-card-gap)}.computes-page__summary-item[_ngcontent-%COMP%]{display:flex;align-items:center;min-width:0;gap:var(--gns3-density-space-3);padding:14px 16px;border:1px solid var(--mat-sys-outline-variant);border-radius:14px;color:var(--mat-sys-on-surface-variant);background:var(--mat-sys-surface-container)}.computes-page__summary-item[_ngcontent-%COMP%] > mat-icon[_ngcontent-%COMP%]{display:grid;flex:0 0 38px;width:38px;height:38px;place-items:center;border-radius:10px;color:var(--mat-sys-on-primary-container);background:var(--mat-sys-primary-container)}.computes-page__summary-item[_ngcontent-%COMP%] > span[_ngcontent-%COMP%]{display:flex;min-width:0;flex-direction:column;font-size:12px}.computes-page__summary-item[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface);font-size:20px;line-height:1.15}.computes-page__summary-item--connected[_ngcontent-%COMP%] > mat-icon[_ngcontent-%COMP%]{color:var(--mat-sys-on-primary-container);background:var(--mat-sys-primary-container)}.computes-page__summary-item--disconnected[_ngcontent-%COMP%] > mat-icon[_ngcontent-%COMP%]{color:var(--mat-sys-on-error-container);background:var(--mat-sys-error-container)}.computes-page__table-container[_ngcontent-%COMP%]{position:relative;min-width:0;overflow:hidden;border-radius:16px;background:var(--mat-sys-surface);box-shadow:0 8px 32px color-mix(in srgb,var(--mat-sys-shadow) 20%,transparent),0 2px 8px color-mix(in srgb,var(--mat-sys-shadow) 10%,transparent)}.computes-page__table-container--loading[_ngcontent-%COMP%]{min-height:320px}.computes-page__loading[_ngcontent-%COMP%]{position:absolute;z-index:2;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:320px;gap:var(--gns3-density-card-gap);color:var(--mat-sys-on-surface-variant);background:var(--mat-sys-surface)}mat-table[_ngcontent-%COMP%]{width:100%;min-width:980px}.mat-mdc-row[_ngcontent-%COMP%], .mat-row[_ngcontent-%COMP%]{height:var(--mat-table-row-item-container-height);min-height:var(--mat-table-row-item-container-height)}.mat-header-row[_ngcontent-%COMP%], .mat-mdc-header-row[_ngcontent-%COMP%]{position:sticky;z-index:10;top:0;background:color-mix(in srgb,var(--mat-sys-primary-container) 72%,var(--mat-sys-surface))}.mat-header-row[_ngcontent-%COMP%] mat-header-cell[_ngcontent-%COMP%], .mat-mdc-header-row[_ngcontent-%COMP%] mat-header-cell[_ngcontent-%COMP%]{color:var(--mat-sys-on-primary-container);font-size:13px;font-weight:650;letter-spacing:.5px;text-transform:uppercase}mat-row[_ngcontent-%COMP%]{border-left:3px solid var(--mat-sys-error);transition:background-color .18s ease}mat-row[_ngcontent-%COMP%]:nth-child(2n){background:color-mix(in srgb,var(--mat-sys-on-surface) 1%,transparent)}mat-row[_ngcontent-%COMP%]:hover{background:color-mix(in srgb,var(--mat-sys-primary) 5%,transparent)}mat-row.computes-page__row--connected[_ngcontent-%COMP%]{border-left-color:var(--mat-sys-primary)}mat-cell[_ngcontent-%COMP%], mat-header-cell[_ngcontent-%COMP%]{display:flex;align-items:center;min-height:inherit;overflow:hidden;color:var(--mat-sys-on-surface);text-overflow:ellipsis;white-space:nowrap}.mat-column-name[_ngcontent-%COMP%]{flex:0 0 24%;min-width:235px}.mat-column-status[_ngcontent-%COMP%]{flex:0 0 14%;min-width:135px;justify-content:center}.mat-column-host[_ngcontent-%COMP%]{flex:0 0 17%;min-width:150px;justify-content:center}.mat-column-platform[_ngcontent-%COMP%]{flex:0 0 14%;min-width:130px;justify-content:center}.mat-column-resources[_ngcontent-%COMP%]{flex:1 0 20%;min-width:225px;justify-content:center}.mat-column-actions[_ngcontent-%COMP%]{flex:0 0 14%;min-width:155px;justify-content:center}.computes-page__name-cell[_ngcontent-%COMP%], .computes-page__platform[_ngcontent-%COMP%]{display:flex;align-items:center}.computes-page__name-cell[_ngcontent-%COMP%]{min-width:0;gap:var(--gns3-density-space-3)}.computes-page__compute-icon[_ngcontent-%COMP%]{display:grid;flex:0 0 38px;width:38px;height:38px;place-items:center;border-radius:10px;color:var(--mat-sys-on-primary-container);background:color-mix(in srgb,var(--mat-sys-primary-container) 82%,var(--mat-sys-surface))}.computes-page__compute-icon[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{width:21px;height:21px;font-size:21px}.computes-page__name-copy[_ngcontent-%COMP%]{display:flex;min-width:0;flex-direction:column}.computes-page__name-copy[_ngcontent-%COMP%] strong[_ngcontent-%COMP%], .computes-page__name-copy[_ngcontent-%COMP%] small[_ngcontent-%COMP%]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.computes-page__name-copy[_ngcontent-%COMP%] strong[_ngcontent-%COMP%]{font-size:14px;font-weight:600}.computes-page__name-copy[_ngcontent-%COMP%] small[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface-variant);font-size:11px}.computes-page__status[_ngcontent-%COMP%]{display:inline-flex;align-items:center;width:fit-content;gap:var(--gns3-density-space-2);padding:4px 10px;border:1px solid var(--mat-sys-error);border-radius:18px;color:var(--mat-sys-on-error-container);background:var(--mat-sys-error-container);font-size:12px;font-weight:600}.computes-page__status[_ngcontent-%COMP%] > span[_ngcontent-%COMP%]{width:7px;height:7px;border-radius:50%;background:currentColor}.computes-page__status--connected[_ngcontent-%COMP%]{border-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary-container);background:var(--mat-sys-primary-container)}.computes-page__host[_ngcontent-%COMP%]{overflow:hidden;text-overflow:ellipsis}.computes-page__platform[_ngcontent-%COMP%]{gap:var(--gns3-density-space-2);color:var(--mat-sys-on-surface-variant);font-size:13px}.computes-page__platform[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{width:17px;height:17px;font-size:17px}.computes-page__resources[_ngcontent-%COMP%]{gap:var(--gns3-density-space-3)}.computes-page__resources[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{display:inline-flex;align-items:center;gap:4px;font-size:12px}.computes-page__resources[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{width:16px;height:16px;color:var(--mat-sys-on-surface-variant);font-size:16px}.computes-page__unavailable[_ngcontent-%COMP%], .computes-page__managed-label[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface-variant);font-size:12px}.computes-page__actions-cell[_ngcontent-%COMP%]{gap:4px}button.computes-page__action[_ngcontent-%COMP%]{--compute-action-color: var(--mat-sys-on-surface-variant);width:32px;height:32px;color:var(--compute-action-color);--mat-icon-button-state-layer-size: 32px;--mat-icon-button-icon-size: 20px}button.computes-page__action[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{color:var(--compute-action-color);transition:transform .15s ease}button.computes-page__action[_ngcontent-%COMP%]:hover mat-icon[_ngcontent-%COMP%]{transform:scale(1.15)}button.computes-page__action--connect[_ngcontent-%COMP%]{--compute-action-color: var(--mat-sys-secondary)}button.computes-page__action--edit[_ngcontent-%COMP%]{--compute-action-color: var(--mat-sys-primary)}button.computes-page__action--delete[_ngcontent-%COMP%]{--compute-action-color: var(--mat-sys-error)}.computes-page__empty[_ngcontent-%COMP%]{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:260px;padding:32px;color:var(--mat-sys-on-surface-variant);text-align:center}.computes-page__empty[_ngcontent-%COMP%] > mat-icon[_ngcontent-%COMP%]{width:48px;height:48px;font-size:48px;opacity:.55}.computes-page__empty[_ngcontent-%COMP%] h3[_ngcontent-%COMP%]{margin:12px 0 0;color:var(--mat-sys-on-surface);font-size:17px}.computes-page__empty[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:5px 0 16px;font-size:14px}[data-density=compact][_nghost-%COMP%] button.computes-page__button[_ngcontent-%COMP%], [data-density=compact] [_nghost-%COMP%] button.computes-page__button[_ngcontent-%COMP%]{min-height:40px;height:40px}@keyframes _ngcontent-%COMP%_computes-fade-in{0%{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}@media(max-width:900px){.computes-page__header[_ngcontent-%COMP%], .computes-page__toolbar[_ngcontent-%COMP%]{align-items:stretch;flex-direction:column}.computes-page__header-actions[_ngcontent-%COMP%]{width:100%}.computes-page__filter[_ngcontent-%COMP%]{flex:1}}@media(max-width:600px){.computes-page__header[_ngcontent-%COMP%]{padding:20px 16px 12px}.computes-page__toolbar[_ngcontent-%COMP%], .computes-page__main[_ngcontent-%COMP%]{padding-inline:16px}.computes-page__title[_ngcontent-%COMP%]{font-size:25px}.computes-page__header-actions[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}button.computes-page__button[_ngcontent-%COMP%]{width:100%}.computes-page__summary[_ngcontent-%COMP%]{grid-template-columns:1fr}}"],changeDetection:0})}return n})(),MR=(()=>{class n{dialogRef=f(He);data=f(Wt);protocols=[{key:"http",name:"HTTP"},{key:"https",name:"HTTPS"}];computeForm=new Uo({name:new Qe(""),protocol:new Qe("http",[We.required]),host:new Qe("",[We.required]),port:new Qe(3080,[We.required,We.min(1),We.max(65535)]),user:new Qe("gns3"),password:new Qe("gns3")});isEditMode=!1;constructor(){this.data.compute&&(this.isEditMode=!0,this.computeForm.patchValue({name:this.data.compute.name,protocol:this.data.compute.protocol,host:this.data.compute.host,port:this.data.compute.port,user:this.data.compute.user,password:""}))}onSaveClick(){if(!this.computeForm.valid)return;let e=this.computeForm.value,t={protocol:e.protocol,host:e.host,port:e.port,user:e.user||void 0,name:e.name||void 0};e.password&&e.password.trim()!==""&&(t.password=e.password),this.dialogRef.close(t)}onCancelClick(){this.dialogRef.close()}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["app-add-compute-dialog"]],decls:39,vars:8,consts:[["mat-dialog-title",""],[3,"formGroup"],["mat-dialog-content",""],["appearance","fill",1,"full-width-field"],["matInput","","tabindex","1","formControlName","name","placeholder","My Compute"],["formControlName","protocol"],[3,"value"],["matInput","","tabindex","1","formControlName","host","placeholder","192.168.1.100"],["matInput","","type","number","tabindex","1","formControlName","port","placeholder","3080"],["matInput","","tabindex","1","formControlName","user","placeholder","gns3"],["matInput","","type","password","tabindex","1","formControlName","password","placeholder","gns3"],["mat-dialog-actions","","align","end"],["mat-button","","tabindex","-1","color","accent",3,"click"],["mat-button","","tabindex","2","mat-raised-button","","color","primary",3,"click","disabled"]],template:function(t,i){t&1&&(a(0,"h2",0),c(1),l(),a(2,"form",1)(3,"div",2)(4,"mat-form-field",3)(5,"mat-label"),c(6,"Name (optional)"),l(),z(7,"input",4),l(),a(8,"mat-form-field",3)(9,"mat-label"),c(10,"Protocol"),l(),a(11,"mat-select",5),Q(12,vne,2,2,"mat-option",6,Cne),l(),E(14,xne,2,0,"mat-error"),l(),a(15,"mat-form-field",3)(16,"mat-label"),c(17,"Host"),l(),z(18,"input",7),E(19,yne,2,0,"mat-error"),l(),a(20,"mat-form-field",3)(21,"mat-label"),c(22,"Port"),l(),z(23,"input",8),E(24,bne,2,0,"mat-error"),E(25,wne,2,0,"mat-error"),l(),a(26,"mat-form-field",3)(27,"mat-label"),c(28,"User"),l(),z(29,"input",9),l(),a(30,"mat-form-field",3)(31,"mat-label"),c(32,"Password"),l(),z(33,"input",10),l()(),a(34,"div",11)(35,"button",12),_("click",function(){return i.onCancelClick()}),c(36,"Cancel"),l(),a(37,"button",13),_("click",function(){return i.onSaveClick()}),c(38),l()()()),t&2&&(p(),N(i.isEditMode?"Edit Compute":"Add Compute"),p(),C("formGroup",i.computeForm),p(10),K(i.protocols),p(2),O(i.computeForm.get("protocol").hasError("required")?14:-1),p(5),O(i.computeForm.get("host").hasError("required")?19:-1),p(5),O(i.computeForm.get("port").hasError("required")?24:-1),p(),O(i.computeForm.get("port").hasError("min")||i.computeForm.get("port").hasError("max")?25:-1),p(12),C("disabled",i.computeForm.invalid),p(),Z(" ",i.isEditMode?"Update":"Add"," "))},dependencies:[fe,Ft,vt,jt,Wo,$t,Ct,Ht,Kt,Se,Ge,qe,xt,X,_e,Me,ke,ze,ni,be,Pe,st,mt,nt],encapsulation:2,changeDetection:0})}return n})();var Mne=["*"];var Sne=[[["","mat-card-avatar",""],["","matCardAvatar",""]],[["mat-card-title"],["mat-card-subtitle"],["","mat-card-title",""],["","mat-card-subtitle",""],["","matCardTitle",""],["","matCardSubtitle",""]],"*"],kne=["[mat-card-avatar], [matCardAvatar]",`mat-card-title, mat-card-subtitle, + [mat-card-title], [mat-card-subtitle], + [matCardTitle], [matCardSubtitle]`,"*"],Pne=new mn("MAT_CARD_CONFIG"),Kn=(()=>{class n{appearance;constructor(){let e=f(Pne,{optional:!0});this.appearance=e?.appearance||"raised"}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["mat-card"]],hostAttrs:[1,"mat-mdc-card","mdc-card"],hostVars:8,hostBindings:function(t,i){t&2&&se("mat-mdc-card-outlined",i.appearance==="outlined")("mdc-card--outlined",i.appearance==="outlined")("mat-mdc-card-filled",i.appearance==="filled")("mdc-card--filled",i.appearance==="filled")},inputs:{appearance:"appearance"},exportAs:["matCard"],ngContentSelectors:Mne,decls:1,vars:0,template:function(t,i){t&1&&(ci(),Zt(0))},styles:[`.mat-mdc-card { + display: flex; + flex-direction: column; + box-sizing: border-box; + position: relative; + border-style: solid; + border-width: 0; + background-color: var(--mat-card-elevated-container-color, var(--mat-sys-surface-container-low)); + border-color: var(--mat-card-elevated-container-color, var(--mat-sys-surface-container-low)); + border-radius: var(--mat-card-elevated-container-shape, var(--mat-sys-corner-medium)); + box-shadow: var(--mat-card-elevated-container-elevation, var(--mat-sys-level1)); +} +.mat-mdc-card::after { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: solid 1px transparent; + content: ""; + display: block; + pointer-events: none; + box-sizing: border-box; + border-radius: var(--mat-card-elevated-container-shape, var(--mat-sys-corner-medium)); +} + +.mat-mdc-card-outlined { + background-color: var(--mat-card-outlined-container-color, var(--mat-sys-surface)); + border-radius: var(--mat-card-outlined-container-shape, var(--mat-sys-corner-medium)); + border-width: var(--mat-card-outlined-outline-width, 1px); + border-color: var(--mat-card-outlined-outline-color, var(--mat-sys-outline-variant)); + box-shadow: var(--mat-card-outlined-container-elevation, var(--mat-sys-level0)); +} +.mat-mdc-card-outlined::after { + border: none; +} + +.mat-mdc-card-filled { + background-color: var(--mat-card-filled-container-color, var(--mat-sys-surface-container-highest)); + border-radius: var(--mat-card-filled-container-shape, var(--mat-sys-corner-medium)); + box-shadow: var(--mat-card-filled-container-elevation, var(--mat-sys-level0)); +} + +.mdc-card__media { + position: relative; + box-sizing: border-box; + background-repeat: no-repeat; + background-position: center; + background-size: cover; +} +.mdc-card__media::before { + display: block; + content: ""; +} +.mdc-card__media:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} +.mdc-card__media:last-child { + border-bottom-left-radius: inherit; + border-bottom-right-radius: inherit; +} + +.mat-mdc-card-actions { + display: flex; + flex-direction: row; + align-items: center; + box-sizing: border-box; + min-height: 52px; + padding: 8px; +} + +.mat-mdc-card-title { + font-family: var(--mat-card-title-text-font, var(--mat-sys-title-large-font)); + line-height: var(--mat-card-title-text-line-height, var(--mat-sys-title-large-line-height)); + font-size: var(--mat-card-title-text-size, var(--mat-sys-title-large-size)); + letter-spacing: var(--mat-card-title-text-tracking, var(--mat-sys-title-large-tracking)); + font-weight: var(--mat-card-title-text-weight, var(--mat-sys-title-large-weight)); +} + +.mat-mdc-card-subtitle { + color: var(--mat-card-subtitle-text-color, var(--mat-sys-on-surface)); + font-family: var(--mat-card-subtitle-text-font, var(--mat-sys-title-medium-font)); + line-height: var(--mat-card-subtitle-text-line-height, var(--mat-sys-title-medium-line-height)); + font-size: var(--mat-card-subtitle-text-size, var(--mat-sys-title-medium-size)); + letter-spacing: var(--mat-card-subtitle-text-tracking, var(--mat-sys-title-medium-tracking)); + font-weight: var(--mat-card-subtitle-text-weight, var(--mat-sys-title-medium-weight)); +} + +.mat-mdc-card-title, +.mat-mdc-card-subtitle { + display: block; + margin: 0; +} +.mat-mdc-card-avatar ~ .mat-mdc-card-header-text .mat-mdc-card-title, +.mat-mdc-card-avatar ~ .mat-mdc-card-header-text .mat-mdc-card-subtitle { + padding: 16px 16px 0; +} + +.mat-mdc-card-header { + display: flex; + padding: 16px 16px 0; +} + +.mat-mdc-card-content { + display: block; + padding: 0 16px; +} +.mat-mdc-card-content:first-child { + padding-top: 16px; +} +.mat-mdc-card-content:last-child { + padding-bottom: 16px; +} + +.mat-mdc-card-title-group { + display: flex; + justify-content: space-between; + width: 100%; +} + +.mat-mdc-card-avatar { + height: 40px; + width: 40px; + border-radius: 50%; + flex-shrink: 0; + margin-bottom: 16px; + object-fit: cover; +} +.mat-mdc-card-avatar ~ .mat-mdc-card-header-text .mat-mdc-card-subtitle, +.mat-mdc-card-avatar ~ .mat-mdc-card-header-text .mat-mdc-card-title { + line-height: normal; +} + +.mat-mdc-card-sm-image { + width: 80px; + height: 80px; +} + +.mat-mdc-card-md-image { + width: 112px; + height: 112px; +} + +.mat-mdc-card-lg-image { + width: 152px; + height: 152px; +} + +.mat-mdc-card-xl-image { + width: 240px; + height: 240px; +} + +.mat-mdc-card-subtitle ~ .mat-mdc-card-title, +.mat-mdc-card-title ~ .mat-mdc-card-subtitle, +.mat-mdc-card-header .mat-mdc-card-header-text .mat-mdc-card-title, +.mat-mdc-card-header .mat-mdc-card-header-text .mat-mdc-card-subtitle, +.mat-mdc-card-title-group .mat-mdc-card-title, +.mat-mdc-card-title-group .mat-mdc-card-subtitle { + padding-top: 0; +} + +.mat-mdc-card-content > :last-child:not(.mat-mdc-card-footer) { + margin-bottom: 0; +} + +.mat-mdc-card-actions-align-end { + justify-content: flex-end; +} +`],encapsulation:2,changeDetection:0})}return n})(),qc=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["mat-card-title"],["","mat-card-title",""],["","matCardTitle",""]],hostAttrs:[1,"mat-mdc-card-title"]})}return n})();var Pl=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["mat-card-content"]],hostAttrs:[1,"mat-mdc-card-content"]})}return n})();var Cy=(()=>{class n{align="start";static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["mat-card-actions"]],hostAttrs:[1,"mat-mdc-card-actions","mdc-card__actions"],hostVars:2,hostBindings:function(t,i){t&2&&se("mat-mdc-card-actions-align-end",i.align==="end")},inputs:{align:"align"},exportAs:["matCardActions"]})}return n})(),Qc=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["mat-card-header"]],hostAttrs:[1,"mat-mdc-card-header"],ngContentSelectors:kne,decls:4,vars:0,consts:[[1,"mat-mdc-card-header-text"]],template:function(t,i){t&1&&(ci(Sne),Zt(0),ti(1,"div",0),Zt(2,1),wi(),Zt(3,2))},encapsulation:2,changeDetection:0})}return n})();var gn=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({imports:[zi]})}return n})();var dh=(()=>{class n{dataChange=new sn([]);constructor(){}get data(){return this.dataChange.value}addController(e){let t=this.data.slice();t.push(e),this.dataChange.next(t)}addControllers(e){this.dataChange.next(e)}remove(e){let t=this.data.indexOf(e);t>=0&&(this.data.splice(t,1),this.dataChange.next(this.data.slice()))}find(e){return this.data.find(t=>t.name===e)}findById(e){return this.data.find(t=>t.id===e)}findIndex(e){return this.data.findIndex(t=>t.name===e)}findIndexById(e){return this.data.findIndex(t=>t.id===e)}update(e){let t=this.findIndexById(e.id);t>=0&&(this.data[t]=e,this.dataChange.next(this.data.slice()))}static \u0275fac=function(t){return new(t||n)};static \u0275prov=J({token:n,factory:n.\u0275fac})}return n})();var kR=(n,o)=>o.key;function Tne(n,o){n&1&&(a(0,"mat-error"),c(1,"You must enter a value"),l())}function Ene(n,o){if(n&1&&(a(0,"mat-option",9),c(1),l()),n&2){let e=o.$implicit;C("value",e.key),p(),Z(" ",e.name," ")}}function One(n,o){if(n&1&&(a(0,"mat-option",9),c(1),l()),n&2){let e=o.$implicit;C("value",e.key),p(),Z(" ",e.name," ")}}var vy=(()=>{class n{controllerService=f(_t);controllerDatabase=f(dh);route=f(Lt);router=f(It);toasterService=f(re);cdr=f(ie);controllerOptionsVisibility=R(!1);controllerIp;controllerPort;projectId;protocols=[{key:"http:",name:"HTTP"},{key:"https:",name:"HTTPS"}];locations=[{key:"local",name:"Local"},{key:"remote",name:"Remote"}];controllerForm=new Uo({name:new Qe("",[We.required]),location:new Qe(""),protocol:new Qe("http:")});constructor(){}async ngOnInit(){this.controllerService.isServiceInitialized&&this.getControllers(),this.controllerService.serviceInitialized.subscribe(async e=>{e&&this.getControllers()})}async getControllers(){this.controllerIp=this.route.snapshot.paramMap.get("controller_ip"),this.controllerPort=+this.route.snapshot.paramMap.get("controller_port"),this.projectId=this.route.snapshot.paramMap.get("project_id");try{let t=(await this.controllerService.findAll()).filter(i=>i.host===this.controllerIp&&i.port===this.controllerPort)[0];t?this.router.navigate(["/controller",t.id,"project",this.projectId]):(this.controllerOptionsVisibility.set(!0),this.cdr.markForCheck())}catch(e){let t=e.error?.message||e.message||"Failed to load controllers";this.toasterService.error(t),this.cdr.markForCheck()}}createController(){if(!this.controllerForm.get("name").hasError&&!this.controllerForm.get("location").hasError&&!this.controllerForm.get("protocol").hasError){this.toasterService.error("Please use correct values");return}let e=new KA;e.host=this.controllerIp,e.port=this.controllerPort,e.name=this.controllerForm.get("name").value,e.location=this.controllerForm.get("location").value,e.protocol=this.controllerForm.get("protocol").value,this.controllerService.create(e).then(t=>{this.toasterService.success(`Controller "${t.name}" added.`),this.router.navigate(["/controller",t.id,"project",this.projectId])},t=>{let i=t.error?.message||t.message||"Failed to create controller";this.toasterService.error(i),this.cdr.markForCheck()})}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["app-direct-link"]],decls:22,vars:3,consts:[[1,"content",3,"hidden"],[1,"default-header"],[1,"row"],[1,"col"],[1,"default-content"],[1,"matCard"],[3,"formGroup"],["matInput","","tabindex","1","formControlName","name","placeholder","Name"],["placeholder","Location","formControlName","location"],[3,"value"],["placeholder","Protocol","formControlName","protocol"],[1,"buttons-bar"],["mat-raised-button","","color","primary",3,"click"]],template:function(t,i){t&1&&(a(0,"div",0)(1,"div",1)(2,"div",2)(3,"h1",3),c(4,"Add new controller"),l()()(),a(5,"div",4)(6,"mat-card",5)(7,"form",6)(8,"mat-form-field"),z(9,"input",7),E(10,Tne,2,0,"mat-error"),l(),a(11,"mat-form-field")(12,"mat-select",8),Q(13,Ene,2,2,"mat-option",9,kR),l()(),a(15,"mat-form-field")(16,"mat-select",10),Q(17,One,2,2,"mat-option",9,kR),l()()()(),a(19,"div",11)(20,"button",12),_("click",function(){return i.createController()}),c(21,"Add controller"),l()()()()),t&2&&(C("hidden",!i.controllerOptionsVisibility()),p(7),C("formGroup",i.controllerForm),p(3),O(i.controllerForm.get("name").hasError("required")?10:-1),p(3),K(i.locations),p(4),K(i.protocols))},dependencies:[fe,Ft,vt,jt,$t,Ct,Ht,Kt,fn,gn,Kn,Me,ke,ni,be,Pe,st,mt,nt,Vo,X,_e],styles:["mat-form-field[_ngcontent-%COMP%]{width:100%}"],changeDetection:0})}return n})();var t3=new mn("CdkAccordion"),PR=(()=>{class n{_stateChanges=new Ye;_openCloseAllActions=new Ye;id=f(cr).getId("cdk-accordion-");multi=!1;openAll(){this.multi&&this._openCloseAllActions.next(!0)}closeAll(){this._openCloseAllActions.next(!1)}ngOnChanges(e){this._stateChanges.next(e)}ngOnDestroy(){this._stateChanges.complete(),this._openCloseAllActions.complete()}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["cdk-accordion"],["","cdkAccordion",""]],inputs:{multi:[2,"multi","multi",At]},exportAs:["cdkAccordion"],features:[un([{provide:t3,useExisting:n}]),In]})}return n})(),TR=(()=>{class n{accordion=f(t3,{optional:!0,skipSelf:!0});_changeDetectorRef=f(ie);_expansionDispatcher=f(pg);_openCloseAllSubscription=To.EMPTY;closed=new ve;opened=new ve;destroyed=new ve;expandedChange=new ve;id=f(cr).getId("cdk-accordion-child-");get expanded(){return this._expanded}set expanded(e){if(this._expanded!==e){if(this._expanded=e,this.expandedChange.emit(e),e){this.opened.emit();let t=this.accordion?this.accordion.id:this.id;this._expansionDispatcher.notify(this.id,t)}else this.closed.emit();this._changeDetectorRef.markForCheck()}}_expanded=!1;get disabled(){return this._disabled()}set disabled(e){this._disabled.set(e)}_disabled=R(!1);_removeUniqueSelectionListener=()=>{};constructor(){}ngOnInit(){this._removeUniqueSelectionListener=this._expansionDispatcher.listen((e,t)=>{this.accordion&&!this.accordion.multi&&this.accordion.id===t&&this.id!==e&&(this.expanded=!1)}),this.accordion&&(this._openCloseAllSubscription=this._subscribeToOpenCloseAllActions())}ngOnDestroy(){this.opened.complete(),this.closed.complete(),this.destroyed.emit(),this.destroyed.complete(),this._removeUniqueSelectionListener(),this._openCloseAllSubscription.unsubscribe()}toggle(){this.disabled||(this.expanded=!this.expanded)}close(){this.disabled||(this.expanded=!1)}open(){this.disabled||(this.expanded=!0)}_subscribeToOpenCloseAllActions(){return this.accordion._openCloseAllActions.subscribe(e=>{this.disabled||(this.expanded=e)})}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["cdk-accordion-item"],["","cdkAccordionItem",""]],inputs:{expanded:[2,"expanded","expanded",At],disabled:[2,"disabled","disabled",At]},outputs:{closed:"closed",opened:"opened",destroyed:"destroyed",expandedChange:"expandedChange"},exportAs:["cdkAccordionItem"],features:[un([{provide:t3,useValue:void 0}])]})}return n})(),xy=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({})}return n})();var Dne=["body"],Ine=["bodyWrapper"],Ane=[[["mat-expansion-panel-header"]],"*",[["mat-action-row"]]],Nne=["mat-expansion-panel-header","*","mat-action-row"];function Fne(n,o){}var Rne=[[["mat-panel-title"]],[["mat-panel-description"]],"*"],Lne=["mat-panel-title","mat-panel-description","*"];function zne(n,o){n&1&&(ti(0,"span",1),gi(),ti(1,"svg",2),Sa(2,"path",3),wi()())}var n3=new mn("MAT_ACCORDION"),ER=new mn("MAT_EXPANSION_PANEL"),Vne=(()=>{class n{_template=f(lr);_expansionPanel=f(ER,{optional:!0});constructor(){}static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["ng-template","matExpansionPanelContent",""]]})}return n})(),OR=new mn("MAT_EXPANSION_PANEL_DEFAULT_OPTIONS"),zd=(()=>{class n extends TR{_viewContainerRef=f($o);_animationsDisabled=Br();_document=f(Eo);_ngZone=f(Ki);_elementRef=f(Qt);_renderer=f(Ui);_cleanupTransitionEnd;get hideToggle(){return this._hideToggle||this.accordion&&this.accordion.hideToggle}set hideToggle(e){this._hideToggle=e}_hideToggle=!1;get togglePosition(){return this._togglePosition||this.accordion&&this.accordion.togglePosition}set togglePosition(e){this._togglePosition=e}_togglePosition;afterExpand=new ve;afterCollapse=new ve;_inputChanges=new Ye;accordion=f(n3,{optional:!0,skipSelf:!0});_lazyContent;_body;_bodyWrapper;_portal;_headerId=f(cr).getId("mat-expansion-panel-header-");constructor(){super();let e=f(OR,{optional:!0});this._expansionDispatcher=f(pg),e&&(this.hideToggle=e.hideToggle)}_hasSpacing(){return this.accordion?this.expanded&&this.accordion.displayMode==="default":!1}_getExpandedState(){return this.expanded?"expanded":"collapsed"}toggle(){this.expanded=!this.expanded}close(){this.expanded=!1}open(){this.expanded=!0}ngAfterContentInit(){this._lazyContent&&this._lazyContent._expansionPanel===this&&this.opened.pipe(Oi(null),oo(()=>this.expanded&&!this._portal),$a(1)).subscribe(()=>{this._portal=new rg(this._lazyContent._template,this._viewContainerRef)}),this._setupAnimationEvents()}ngOnChanges(e){this._inputChanges.next(e)}ngOnDestroy(){super.ngOnDestroy(),this._cleanupTransitionEnd?.(),this._inputChanges.complete()}_containsFocus(){if(this._body){let e=this._document.activeElement,t=this._body.nativeElement;return e===t||t.contains(e)}return!1}_transitionEndListener=({target:e,propertyName:t})=>{e===this._bodyWrapper?.nativeElement&&t==="grid-template-rows"&&this._ngZone.run(()=>{this.expanded?this.afterExpand.emit():this.afterCollapse.emit()})};_setupAnimationEvents(){this._ngZone.runOutsideAngular(()=>{this._animationsDisabled?(this.opened.subscribe(()=>this._ngZone.run(()=>this.afterExpand.emit())),this.closed.subscribe(()=>this._ngZone.run(()=>this.afterCollapse.emit()))):setTimeout(()=>{let e=this._elementRef.nativeElement;this._cleanupTransitionEnd=this._renderer.listen(e,"transitionend",this._transitionEndListener),e.classList.add("mat-expansion-panel-animations-enabled")},200)})}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["mat-expansion-panel"]],contentQueries:function(t,i,r){if(t&1&&Xi(r,Vne,5),t&2){let s;gt(s=ht())&&(i._lazyContent=s.first)}},viewQuery:function(t,i){if(t&1&&Gn(Dne,5)(Ine,5),t&2){let r;gt(r=ht())&&(i._body=r.first),gt(r=ht())&&(i._bodyWrapper=r.first)}},hostAttrs:[1,"mat-expansion-panel"],hostVars:4,hostBindings:function(t,i){t&2&&se("mat-expanded",i.expanded)("mat-expansion-panel-spacing",i._hasSpacing())},inputs:{hideToggle:[2,"hideToggle","hideToggle",At],togglePosition:"togglePosition"},outputs:{afterExpand:"afterExpand",afterCollapse:"afterCollapse"},exportAs:["matExpansionPanel"],features:[un([{provide:n3,useValue:void 0},{provide:ER,useExisting:n}]),li,In],ngContentSelectors:Nne,decls:9,vars:4,consts:[["bodyWrapper",""],["body",""],[1,"mat-expansion-panel-content-wrapper"],["role","region",1,"mat-expansion-panel-content",3,"id"],[1,"mat-expansion-panel-body"],[3,"cdkPortalOutlet"]],template:function(t,i){t&1&&(ci(Ane),Zt(0),a(1,"div",2,0)(3,"div",3,1)(5,"div",4),Zt(6,1),ce(7,Fne,0,0,"ng-template",5),l(),Zt(8,2),l()()),t&2&&(p(),Ve("inert",i.expanded?null:""),p(2),C("id",i.id),Ve("aria-labelledby",i._headerId),p(4),C("cdkPortalOutlet",i._portal))},dependencies:[zv],styles:[`.mat-expansion-panel { + box-sizing: content-box; + display: block; + margin: 0; + overflow: hidden; +} +.mat-expansion-panel.mat-expansion-panel-animations-enabled { + transition: margin 225ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-expansion-panel { + position: relative; + background: var(--mat-expansion-container-background-color, var(--mat-sys-surface)); + color: var(--mat-expansion-container-text-color, var(--mat-sys-on-surface)); + border-radius: var(--mat-expansion-container-shape, 12px); +} +.mat-expansion-panel:not([class*=mat-elevation-z]) { + box-shadow: var(--mat-expansion-container-elevation-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12)); +} +.mat-accordion .mat-expansion-panel:not(.mat-expanded), .mat-accordion .mat-expansion-panel:not(.mat-expansion-panel-spacing) { + border-radius: 0; +} +.mat-accordion .mat-expansion-panel:first-of-type { + border-top-right-radius: var(--mat-expansion-container-shape, 12px); + border-top-left-radius: var(--mat-expansion-container-shape, 12px); +} +.mat-accordion .mat-expansion-panel:last-of-type { + border-bottom-right-radius: var(--mat-expansion-container-shape, 12px); + border-bottom-left-radius: var(--mat-expansion-container-shape, 12px); +} +@media (forced-colors: active) { + .mat-expansion-panel { + outline: solid 1px; + } +} + +.mat-expansion-panel-content-wrapper { + display: grid; + grid-template-rows: 0fr; + grid-template-columns: 100%; +} +.mat-expansion-panel-animations-enabled .mat-expansion-panel-content-wrapper { + transition: grid-template-rows 225ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-expansion-panel.mat-expanded > .mat-expansion-panel-content-wrapper { + grid-template-rows: 1fr; +} +@supports not (grid-template-rows: 0fr) { + .mat-expansion-panel-content-wrapper { + height: 0; + } + .mat-expansion-panel.mat-expanded > .mat-expansion-panel-content-wrapper { + height: auto; + } +} +@media print { + .mat-expansion-panel-content-wrapper { + height: 0; + } + .mat-expansion-panel.mat-expanded > .mat-expansion-panel-content-wrapper { + height: auto; + } +} + +.mat-expansion-panel-content { + display: flex; + flex-direction: column; + overflow: visible; + min-height: 0; + visibility: hidden; +} +.mat-expansion-panel-animations-enabled .mat-expansion-panel-content { + transition: visibility 190ms linear; +} +.mat-expansion-panel.mat-expanded > .mat-expansion-panel-content-wrapper > .mat-expansion-panel-content { + visibility: visible; +} +.mat-expansion-panel-content { + font-family: var(--mat-expansion-container-text-font, var(--mat-sys-body-large-font)); + font-size: var(--mat-expansion-container-text-size, var(--mat-sys-body-large-size)); + font-weight: var(--mat-expansion-container-text-weight, var(--mat-sys-body-large-weight)); + line-height: var(--mat-expansion-container-text-line-height, var(--mat-sys-body-large-line-height)); + letter-spacing: var(--mat-expansion-container-text-tracking, var(--mat-sys-body-large-tracking)); +} + +.mat-expansion-panel-body { + padding: 0 24px 16px; +} + +.mat-expansion-panel-spacing { + margin: 16px 0; +} +.mat-accordion > .mat-expansion-panel-spacing:first-child, .mat-accordion > *:first-child:not(.mat-expansion-panel) .mat-expansion-panel-spacing { + margin-top: 0; +} +.mat-accordion > .mat-expansion-panel-spacing:last-child, .mat-accordion > *:last-child:not(.mat-expansion-panel) .mat-expansion-panel-spacing { + margin-bottom: 0; +} + +.mat-action-row { + border-top-style: solid; + border-top-width: 1px; + display: flex; + flex-direction: row; + justify-content: flex-end; + padding: 16px 8px 16px 24px; + border-top-color: var(--mat-expansion-actions-divider-color, var(--mat-sys-outline)); +} +.mat-action-row .mat-button-base, +.mat-action-row .mat-mdc-button-base { + margin-left: 8px; +} +[dir=rtl] .mat-action-row .mat-button-base, +[dir=rtl] .mat-action-row .mat-mdc-button-base { + margin-left: 0; + margin-right: 8px; +} +`],encapsulation:2,changeDetection:0})}return n})();var Vd=(()=>{class n{panel=f(zd,{host:!0});_element=f(Qt);_focusMonitor=f(ms);_changeDetectorRef=f(ie);_parentChangeSubscription=To.EMPTY;constructor(){f(kr).load(Wa);let e=this.panel,t=f(OR,{optional:!0}),i=f(new op("tabindex"),{optional:!0}),r=e.accordion?e.accordion._stateChanges.pipe(oo(s=>!!(s.hideToggle||s.togglePosition))):Yu;this.tabIndex=parseInt(i||"")||0,this._parentChangeSubscription=io(e.opened,e.closed,r,e._inputChanges.pipe(oo(s=>!!(s.hideToggle||s.disabled||s.togglePosition)))).subscribe(()=>this._changeDetectorRef.markForCheck()),e.closed.pipe(oo(()=>e._containsFocus())).subscribe(()=>this._focusMonitor.focusVia(this._element,"program")),t&&(this.expandedHeight=t.expandedHeight,this.collapsedHeight=t.collapsedHeight)}expandedHeight;collapsedHeight;tabIndex=0;get disabled(){return this.panel.disabled}_toggle(){this.disabled||this.panel.toggle()}_isExpanded(){return this.panel.expanded}_getExpandedState(){return this.panel._getExpandedState()}_getPanelId(){return this.panel.id}_getTogglePosition(){return this.panel.togglePosition}_showToggle(){return!this.panel.hideToggle&&!this.panel.disabled}_getHeaderHeight(){let e=this._isExpanded();return e&&this.expandedHeight?this.expandedHeight:!e&&this.collapsedHeight?this.collapsedHeight:null}_keydown(e){switch(e.keyCode){case 32:case 13:ds(e)||(e.preventDefault(),this._toggle());break;default:this.panel.accordion&&this.panel.accordion._handleHeaderKeydown(e);return}}focus(e,t){e?this._focusMonitor.focusVia(this._element,e,t):this._element.nativeElement.focus(t)}ngAfterViewInit(){this._focusMonitor.monitor(this._element).subscribe(e=>{e&&this.panel.accordion&&this.panel.accordion._handleHeaderFocus(this)})}ngOnDestroy(){this._parentChangeSubscription.unsubscribe(),this._focusMonitor.stopMonitoring(this._element)}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["mat-expansion-panel-header"]],hostAttrs:["role","button",1,"mat-expansion-panel-header","mat-focus-indicator"],hostVars:13,hostBindings:function(t,i){t&1&&_("click",function(){return i._toggle()})("keydown",function(s){return i._keydown(s)}),t&2&&(Ve("id",i.panel._headerId)("tabindex",i.disabled?-1:i.tabIndex)("aria-controls",i._getPanelId())("aria-expanded",i._isExpanded())("aria-disabled",i.panel.disabled),Ot("height",i._getHeaderHeight()),se("mat-expanded",i._isExpanded())("mat-expansion-toggle-indicator-after",i._getTogglePosition()==="after")("mat-expansion-toggle-indicator-before",i._getTogglePosition()==="before"))},inputs:{expandedHeight:"expandedHeight",collapsedHeight:"collapsedHeight",tabIndex:[2,"tabIndex","tabIndex",e=>e==null?0:Ho(e)]},ngContentSelectors:Lne,decls:5,vars:3,consts:[[1,"mat-content"],[1,"mat-expansion-indicator"],["xmlns","http://www.w3.org/2000/svg","viewBox","0 -960 960 960","aria-hidden","true","focusable","false"],["d","M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z"]],template:function(t,i){t&1&&(ci(Rne),ti(0,"span",0),Zt(1),Zt(2,1),Zt(3,2),wi(),E(4,zne,3,0,"span",1)),t&2&&(se("mat-content-hide-toggle",!i._showToggle()),p(4),O(i._showToggle()?4:-1))},styles:[`.mat-expansion-panel-header { + display: flex; + flex-direction: row; + align-items: center; + padding: 0 24px; + border-radius: inherit; +} +.mat-expansion-panel-animations-enabled .mat-expansion-panel-header { + transition: height 225ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-expansion-panel-header::before { + border-radius: inherit; +} +.mat-expansion-panel-header { + height: var(--mat-expansion-header-collapsed-state-height, 48px); + font-family: var(--mat-expansion-header-text-font, var(--mat-sys-title-medium-font)); + font-size: var(--mat-expansion-header-text-size, var(--mat-sys-title-medium-size)); + font-weight: var(--mat-expansion-header-text-weight, var(--mat-sys-title-medium-weight)); + line-height: var(--mat-expansion-header-text-line-height, var(--mat-sys-title-medium-line-height)); + letter-spacing: var(--mat-expansion-header-text-tracking, var(--mat-sys-title-medium-tracking)); +} +.mat-expansion-panel-header.mat-expanded { + height: var(--mat-expansion-header-expanded-state-height, 64px); +} +.mat-expansion-panel-header[aria-disabled=true] { + color: var(--mat-expansion-header-disabled-state-text-color, color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)); +} +.mat-expansion-panel-header:not([aria-disabled=true]) { + cursor: pointer; +} +.mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled=true]):hover { + background: var(--mat-expansion-header-hover-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), transparent)); +} +@media (hover: none) { + .mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:not([aria-disabled=true]):hover { + background: var(--mat-expansion-container-background-color, var(--mat-sys-surface)); + } +} +.mat-expansion-panel .mat-expansion-panel-header:not([aria-disabled=true]).cdk-keyboard-focused, .mat-expansion-panel .mat-expansion-panel-header:not([aria-disabled=true]).cdk-program-focused { + background: var(--mat-expansion-header-focus-state-layer-color, color-mix(in srgb, var(--mat-sys-on-surface) calc(var(--mat-sys-focus-state-layer-opacity) * 100%), transparent)); +} +.mat-expansion-panel-header._mat-animation-noopable { + transition: none; +} +.mat-expansion-panel-header:focus, .mat-expansion-panel-header:hover { + outline: none; +} +.mat-expansion-panel-header.mat-expanded:focus, .mat-expansion-panel-header.mat-expanded:hover { + background: inherit; +} +.mat-expansion-panel-header.mat-expansion-toggle-indicator-before { + flex-direction: row-reverse; +} +.mat-expansion-panel-header.mat-expansion-toggle-indicator-before .mat-expansion-indicator { + margin: 0 16px 0 0; +} +[dir=rtl] .mat-expansion-panel-header.mat-expansion-toggle-indicator-before .mat-expansion-indicator { + margin: 0 0 0 16px; +} + +.mat-content { + display: flex; + flex: 1; + flex-direction: row; + overflow: hidden; +} +.mat-content.mat-content-hide-toggle { + margin-right: 8px; +} +[dir=rtl] .mat-content.mat-content-hide-toggle { + margin-right: 0; + margin-left: 8px; +} +.mat-expansion-toggle-indicator-before .mat-content.mat-content-hide-toggle { + margin-left: 24px; + margin-right: 0; +} +[dir=rtl] .mat-expansion-toggle-indicator-before .mat-content.mat-content-hide-toggle { + margin-right: 24px; + margin-left: 0; +} + +.mat-expansion-panel-header-title { + color: var(--mat-expansion-header-text-color, var(--mat-sys-on-surface)); +} + +.mat-expansion-panel-header-title, +.mat-expansion-panel-header-description { + display: flex; + flex-grow: 1; + flex-basis: 0; + margin-right: 16px; + align-items: center; +} +[dir=rtl] .mat-expansion-panel-header-title, +[dir=rtl] .mat-expansion-panel-header-description { + margin-right: 0; + margin-left: 16px; +} +.mat-expansion-panel-header[aria-disabled=true] .mat-expansion-panel-header-title, +.mat-expansion-panel-header[aria-disabled=true] .mat-expansion-panel-header-description { + color: inherit; +} + +.mat-expansion-panel-header-description { + flex-grow: 2; + color: var(--mat-expansion-header-description-color, var(--mat-sys-on-surface-variant)); +} + +.mat-expansion-panel-animations-enabled .mat-expansion-indicator { + transition: transform 225ms cubic-bezier(0.4, 0, 0.2, 1); +} +.mat-expansion-panel-header.mat-expanded .mat-expansion-indicator { + transform: rotate(180deg); +} +.mat-expansion-indicator::after { + border-style: solid; + border-width: 0 2px 2px 0; + content: ""; + padding: 3px; + transform: rotate(45deg); + vertical-align: middle; + color: var(--mat-expansion-header-indicator-color, var(--mat-sys-on-surface-variant)); + display: var(--mat-expansion-legacy-header-indicator-display, none); +} +.mat-expansion-indicator svg { + width: 24px; + height: 24px; + margin: 0 -8px; + vertical-align: middle; + fill: var(--mat-expansion-header-indicator-color, var(--mat-sys-on-surface-variant)); + display: var(--mat-expansion-header-indicator-display, inline-block); +} + +@media (forced-colors: active) { + .mat-expansion-panel-content { + border-top: 1px solid; + border-top-left-radius: 0; + border-top-right-radius: 0; + } +} +`],encapsulation:2,changeDetection:0})}return n})(),DR=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["mat-panel-description"]],hostAttrs:[1,"mat-expansion-panel-header-description"]})}return n})(),zp=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275dir=yt({type:n,selectors:[["mat-panel-title"]],hostAttrs:[1,"mat-expansion-panel-header-title"]})}return n})(),mh=(()=>{class n extends PR{_keyManager;_ownHeaders=new Zl;_headers;hideToggle=!1;displayMode="default";togglePosition="after";ngAfterContentInit(){this._headers.changes.pipe(Oi(this._headers)).subscribe(e=>{this._ownHeaders.reset(e.filter(t=>t.panel.accordion===this)),this._ownHeaders.notifyOnChanges()}),this._keyManager=new cg(this._ownHeaders).withWrap().withHomeAndEnd()}_handleHeaderKeydown(e){this._keyManager.onKeydown(e)}_handleHeaderFocus(e){this._keyManager.updateActiveItem(e)}ngOnDestroy(){super.ngOnDestroy(),this._keyManager?.destroy(),this._ownHeaders.destroy()}static \u0275fac=(()=>{let e;return function(i){return(e||(e=yi(n)))(i||n)}})();static \u0275dir=yt({type:n,selectors:[["mat-accordion"]],contentQueries:function(t,i,r){if(t&1&&Xi(r,Vd,5),t&2){let s;gt(s=ht())&&(i._headers=s)}},hostAttrs:[1,"mat-accordion"],hostVars:2,hostBindings:function(t,i){t&2&&se("mat-accordion-multi",i.multi)},inputs:{hideToggle:[2,"hideToggle","hideToggle",At],displayMode:"displayMode",togglePosition:"togglePosition"},exportAs:["matAccordion"],features:[un([{provide:n3,useExisting:n}]),li]})}return n})(),Tl=(()=>{class n{static \u0275fac=function(t){return new(t||n)};static \u0275mod=nn({type:n});static \u0275inj=tn({imports:[xy,Vv,zi]})}return n})();var by=(()=>{class n{httpClient=f(cs);sanitizer=f(ka);toasterService=f(re);cd=f(ie);thirdpartylicenses=R("");releasenotes=R("");ngOnInit(){this.httpClient.get(window.location.href+"/3rdpartylicenses.txt",{responseType:"text"}).subscribe({next:e=>{let t=e.replace(new RegExp(` +`,"g"),"
");this.thirdpartylicenses.set(this.sanitizer.bypassSecurityTrustHtml(t))},error:e=>{if(e.status===404)this.thirdpartylicenses.set("Download Solar-PuTTY");else{let t=e.error?.message||e.message||"Failed to load third party licenses";this.toasterService.error(t)}this.cd.markForCheck()}}),this.httpClient.get("ReleaseNotes.txt",{responseType:"text"}).subscribe({next:e=>{let t=e.replace(new RegExp(` +`,"g"),"
");this.releasenotes.set(this.sanitizer.bypassSecurityTrustHtml(t))},error:e=>{let t=e.error?.message||e.message||"Failed to load release notes";this.toasterService.error(t),this.cd.markForCheck()}})}goToDocumentation(){window.location.href="https://docs.gns3.com/docs/"}static \u0275fac=function(t){return new(t||n)};static \u0275cmp=B({type:n,selectors:[["app-help"]],decls:107,vars:2,consts:[[1,"help-page"],[1,"help-page__header"],[1,"help-page__title"],["mat-raised-button","","color","primary",1,"help-page__button",3,"click"],[1,"help-page__main"],[1,"help-page__overview"],[1,"help-page__overview-icon"],[1,"help-page__panel"],["multi",""],[1,"help-page__panel-title"],[1,"help-page__shortcuts"],[1,"help-page__shortcut"],["href","https://downloads.solarwinds.com/solarwinds/GNS3/Solar-PuTTY/Solar-PuTTY-Optional.exe",1,"help-page__link"],[3,"innerHTML"],[1,"help-page__release-notes",3,"innerHTML"]],template:function(t,i){t&1&&(a(0,"div",0)(1,"header",1)(2,"h1",2),c(3,"Help"),l(),a(4,"button",3),_("click",function(){return i.goToDocumentation()}),a(5,"mat-icon"),c(6,"open_in_new"),l(),c(7," Documentation "),l()(),a(8,"main",4)(9,"section",5)(10,"span",6)(11,"mat-icon"),c(12,"help_outline"),l()(),a(13,"div")(14,"h2"),c(15,"GNS3 Web UI help"),l(),a(16,"p"),c(17,"Find keyboard shortcuts, third-party component information, and release notes."),l()()(),a(18,"section",7)(19,"mat-accordion",8)(20,"mat-expansion-panel")(21,"mat-expansion-panel-header")(22,"mat-panel-title")(23,"span",9)(24,"mat-icon"),c(25,"keyboard"),l(),c(26," Useful shortcuts "),l()()(),a(27,"div",10)(28,"div",11)(29,"span")(30,"kbd"),c(31,"Ctrl"),l(),a(32,"kbd"),c(33,"+"),l()(),a(34,"span"),c(35,"Zoom in"),l()(),a(36,"div",11)(37,"span")(38,"kbd"),c(39,"Ctrl"),l(),a(40,"kbd"),c(41,"-"),l()(),a(42,"span"),c(43,"Zoom out"),l()(),a(44,"div",11)(45,"span")(46,"kbd"),c(47,"Ctrl"),l(),a(48,"kbd"),c(49,"0"),l()(),a(50,"span"),c(51,"Reset zoom"),l()(),a(52,"div",11)(53,"span")(54,"kbd"),c(55,"Ctrl"),l(),a(56,"kbd"),c(57,"H"),l()(),a(58,"span"),c(59,"Hide toolbar"),l()(),a(60,"div",11)(61,"span")(62,"kbd"),c(63,"Ctrl"),l(),a(64,"kbd"),c(65,"A"),l()(),a(66,"span"),c(67,"Select all map items"),l()(),a(68,"div",11)(69,"span")(70,"kbd"),c(71,"Ctrl"),l(),a(72,"kbd"),c(73,"Shift"),l(),a(74,"kbd"),c(75,"A"),l()(),a(76,"span"),c(77,"Deselect all map items"),l()(),a(78,"div",11)(79,"span")(80,"kbd"),c(81,"Ctrl"),l(),a(82,"kbd"),c(83,"Shift"),l(),a(84,"kbd"),c(85,"S"),l()(),a(86,"span"),c(87,"Open preferences"),l()()()(),a(88,"mat-expansion-panel")(89,"mat-expansion-panel-header")(90,"mat-panel-title")(91,"span",9)(92,"mat-icon"),c(93,"extension"),l(),c(94," Third-party components "),l()()(),a(95,"a",12)(96,"mat-icon"),c(97,"download"),l(),z(98,"span",13),l()(),a(99,"mat-expansion-panel")(100,"mat-expansion-panel-header")(101,"mat-panel-title")(102,"span",9)(103,"mat-icon"),c(104,"description"),l(),c(105," Release notes "),l()()(),z(106,"div",14),l()()()()()),t&2&&(p(98),C("innerHTML",i.thirdpartylicenses(),rf),p(8),C("innerHTML",i.releasenotes(),rf))},dependencies:[X,_e,Tl,mh,zd,Vd,zp,ne,pe],styles:["[_nghost-%COMP%]{display:block;width:100%;background:transparent}.help-page[_ngcontent-%COMP%]{animation:_ngcontent-%COMP%_help-fade-in .4s ease-out}.help-page__header[_ngcontent-%COMP%], .help-page__main[_ngcontent-%COMP%]{max-width:1600px;margin-inline:auto}.help-page__header[_ngcontent-%COMP%]{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:24px 20px 16px}.help-page__title[_ngcontent-%COMP%]{margin:0;color:var(--mat-sys-on-surface);font-size:32px;font-weight:500;white-space:nowrap}button.help-page__button[_ngcontent-%COMP%]{height:var(--gns3-density-control-height);padding-inline:18px;border-radius:12px;font-size:15px;font-weight:500;letter-spacing:.3px;text-transform:none;transition:transform .2s ease,box-shadow .2s ease}button.help-page__button[_ngcontent-%COMP%]:hover{transform:translateY(-2px);box-shadow:0 4px 12px color-mix(in srgb,var(--mat-sys-primary) 25%,transparent)}.help-page__main[_ngcontent-%COMP%]{padding:0 20px 24px}.help-page__overview[_ngcontent-%COMP%]{display:flex;align-items:center;gap:var(--gns3-density-card-gap);margin-bottom:var(--gns3-density-card-gap);padding:16px 18px;border:1px solid var(--mat-sys-outline-variant);border-radius:14px;background:var(--mat-sys-surface-container)}.help-page__overview[_ngcontent-%COMP%] h2[_ngcontent-%COMP%]{margin:0;color:var(--mat-sys-on-surface);font-size:17px;font-weight:600}.help-page__overview[_ngcontent-%COMP%] p[_ngcontent-%COMP%]{margin:3px 0 0;color:var(--mat-sys-on-surface-variant);font-size:14px}.help-page__overview-icon[_ngcontent-%COMP%]{display:grid;flex:0 0 42px;width:42px;height:42px;place-items:center;border-radius:11px;color:var(--mat-sys-on-primary-container);background:var(--mat-sys-primary-container)}.help-page__panel[_ngcontent-%COMP%]{overflow:hidden;border-radius:16px;background:var(--mat-sys-surface);box-shadow:0 8px 32px color-mix(in srgb,var(--mat-sys-shadow) 20%,transparent),0 2px 8px color-mix(in srgb,var(--mat-sys-shadow) 10%,transparent)}.mat-expansion-panel[_ngcontent-%COMP%]{border-bottom:1px solid var(--mat-sys-outline-variant);border-radius:0;background:var(--mat-sys-surface);box-shadow:none}.mat-expansion-panel[_ngcontent-%COMP%]:last-child{border-bottom:0}.mat-expansion-panel-header[_ngcontent-%COMP%]{min-height:var(--gns3-density-section-header-height);padding-inline:20px;color:var(--mat-sys-on-surface);background:color-mix(in srgb,var(--mat-sys-primary-container) 28%,var(--mat-sys-surface))}.mat-expansion-panel[_ngcontent-%COMP%] .mat-expansion-panel-header[_ngcontent-%COMP%]:hover{background:color-mix(in srgb,var(--mat-sys-primary-container) 45%,var(--mat-sys-surface))}.mat-expansion-panel-header-title[_ngcontent-%COMP%]{color:var(--mat-sys-on-surface)}.help-page__panel-title[_ngcontent-%COMP%]{display:inline-flex;align-items:center;gap:var(--gns3-density-space-3);font-size:15px;font-weight:600}.help-page__panel-title[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{color:var(--mat-sys-primary)}.help-page__shortcuts[_ngcontent-%COMP%]{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;overflow:hidden;margin:4px 0 8px;border:1px solid var(--mat-sys-outline-variant);border-radius:12px;background:var(--mat-sys-outline-variant)}.help-page__shortcut[_ngcontent-%COMP%]{display:flex;align-items:center;justify-content:space-between;min-height:54px;gap:var(--gns3-density-card-gap);padding:10px 14px;color:var(--mat-sys-on-surface);background:var(--mat-sys-surface-container-low);font-size:14px}.help-page__shortcut[_ngcontent-%COMP%] > span[_ngcontent-%COMP%]:first-child{display:flex;flex:0 0 auto;align-items:center;gap:5px}kbd[_ngcontent-%COMP%]{min-width:25px;padding:4px 7px;border:1px solid var(--mat-sys-outline-variant);border-bottom-width:2px;border-radius:6px;color:var(--mat-sys-on-surface);background:var(--mat-sys-surface-container-highest);font:inherit;font-size:12px;font-weight:600;line-height:1;text-align:center}.help-page__link[_ngcontent-%COMP%]{display:inline-flex;align-items:flex-start;max-width:100%;gap:8px;padding-block:8px;color:var(--mat-sys-primary);font-size:14px;text-decoration:none}.help-page__link[_ngcontent-%COMP%] mat-icon[_ngcontent-%COMP%]{flex:0 0 auto;width:19px;height:19px;font-size:19px}.help-page__link[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{min-width:0;overflow-wrap:anywhere}.help-page__link[_ngcontent-%COMP%]:hover{text-decoration:underline}.help-page__release-notes[_ngcontent-%COMP%]{max-height:min(60vh,640px);overflow:auto;padding:8px 4px 12px;color:var(--mat-sys-on-surface);font-size:14px;line-height:1.6;overflow-wrap:anywhere}[data-density=compact][_nghost-%COMP%] button.help-page__button[_ngcontent-%COMP%], [data-density=compact] [_nghost-%COMP%] button.help-page__button[_ngcontent-%COMP%]{min-height:40px;height:40px}[data-density=compact][_nghost-%COMP%] .mat-expansion-panel-header[_ngcontent-%COMP%], [data-density=compact] [_nghost-%COMP%] .mat-expansion-panel-header[_ngcontent-%COMP%]{min-height:52px}@keyframes _ngcontent-%COMP%_help-fade-in{0%{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}@media(max-width:800px){.help-page__shortcuts[_ngcontent-%COMP%]{grid-template-columns:1fr}}@media(max-width:600px){.help-page__header[_ngcontent-%COMP%]{align-items:stretch;flex-direction:column;padding:20px 16px 12px}.help-page__title[_ngcontent-%COMP%]{font-size:25px}.help-page__main[_ngcontent-%COMP%]{padding-inline:16px}button.help-page__button[_ngcontent-%COMP%]{width:100%}.help-page__overview[_ngcontent-%COMP%]{align-items:flex-start}.help-page__shortcut[_ngcontent-%COMP%]{align-items:flex-start;flex-direction:column;gap:8px}}"],changeDetection:0})}return n})();var wy=(()=>{class n{constructor(){}isWindows(){return navigator.platform.indexOf("Win")>-1}isLinux(){return navigator.platform.indexOf("Linux")>-1}isDarwin(){return navigator.platform.indexOf("Mac")>-1}static \u0275fac=function(t){return new(t||n)};static \u0275prov=J({token:n,factory:n.\u0275fac})}return n})();var My=(()=>{class n{platformService;constructor(e){this.platformService=e}get(){return this.platformService.isWindows()?this.getForWindows():this.platformService.isDarwin()?this.getForDarwin():this.getForLinux()}getForWindows(){let e=[{name:"Wireshark",locations:["C:\\Program Files\\Wireshark\\Wireshark.exe"],type:"web",resource:"https://1.na.dl.wireshark.org/win64/all-versions/Wireshark-win64-2.6.3.exe",binary:"Wireshark.exe",sudo:!0,installation_arguments:[],installed:!1,installer:!0}],t={name:"SolarPuTTY",locations:["SolarPuTTY.exe","external\\SolarPuTTY.exe"],type:"web",resource:"",binary:"SolarPuTTY.exe",sudo:!1,installation_arguments:["--only-ask"],installed:!1,installer:!1};return Li.solarputty_download_url&&(t.resource=Li.solarputty_download_url,e.push(t)),e}getForLinux(){return[]}getForDarwin(){return[]}static \u0275fac=function(t){return new(t||n)(he(wy))};static \u0275prov=J({token:n,factory:n.\u0275fac})}return n})();var Sy=(()=>{class n{externalSoftwareDefinition;constructor(e){this.externalSoftwareDefinition=e}list(){return this.externalSoftwareDefinition.get().map(t=>(t.installed=!1,t))}static \u0275fac=function(t){return new(t||n)(he(My))};static \u0275prov=J({token:n,factory:n.\u0275fac})}return n})();var $ne=(n,o)=>({hidden:n,lightTheme:o}),Hne=/(.*)<\/a>(.*)\s*