diff --git a/.github/workflows/auth_test.yml b/.github/workflows/auth_test.yml
index c76d7d84..6b3982c5 100644
--- a/.github/workflows/auth_test.yml
+++ b/.github/workflows/auth_test.yml
@@ -16,7 +16,7 @@ jobs:
- name: Installing dependencies
run: |
sudo apt-get update
- pip install -r front/requirements.txt
+ pip install -r front/requirements.lock
- name: Running tests
run: |
diff --git a/.github/workflows/dependency_review.yml b/.github/workflows/dependency_review.yml
index 8b3b4fb2..5fb816de 100644
--- a/.github/workflows/dependency_review.yml
+++ b/.github/workflows/dependency_review.yml
@@ -12,3 +12,5 @@ jobs:
uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
+ with:
+ fail-on-severity: critical
diff --git a/.github/workflows/full_test.yml b/.github/workflows/full_test.yml
index 8e6ff1f3..b2125c93 100644
--- a/.github/workflows/full_test.yml
+++ b/.github/workflows/full_test.yml
@@ -33,8 +33,8 @@ jobs:
- name: Install dependencies
run: |
- sudo python -m pip install pip pytest selenium --ignore-installed urllib3
- sudo python -m pip install -r front/requirements.txt --ignore-installed
+ sudo python -m pip install pip pytest pytest-mock selenium --ignore-installed urllib3
+ sudo python -m pip install -r front/requirements.lock --ignore-installed
- name: Check availability
run: |
diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
index 0b57736f..b45de221 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/linter.yml
@@ -18,7 +18,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- cache-dependency-path: ${{matrix.node}}/requirements.txt
+ cache-dependency-path: ${{matrix.node}}/${{ matrix.node == 'front' && 'requirements.lock' || 'requirements.txt' }}
- name: Install dependencies
run: |
@@ -30,7 +30,7 @@ jobs:
sudo apt-get install -y --no-install-recommends mininet
mkdir -p /opt/mininet_dependencies
fi
- pip install -r ${{matrix.node}}/requirements.txt
+ pip install -r ${{matrix.node}}/${{ matrix.node == 'front' && 'requirements.lock' || 'requirements.txt' }}
- name: Lint with mypy
run: |
diff --git a/.gitignore b/.gitignore
index 0f6a4998..1987e778 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,6 @@ back/rabbitmq/rabbitmq/
.env
*.log
*.ini
-front/rabbitmq/rabbitmq
\ No newline at end of file
+front/rabbitmq/rabbitmq
+front/src/static/dist/
+front/web/node_modules/
\ No newline at end of file
diff --git a/front/.dockerignore b/front/.dockerignore
index d03d2623..8f6c15f4 100644
--- a/front/.dockerignore
+++ b/front/.dockerignore
@@ -5,4 +5,6 @@ src/static/video
src/static/pcaps
src/static/assets
src/static/avatar
-src/static/quiz_images
\ No newline at end of file
+src/static/quiz_images
+src/static/dist
+web/node_modules
\ No newline at end of file
diff --git a/front/.python-version b/front/.python-version
new file mode 100644
index 00000000..2c073331
--- /dev/null
+++ b/front/.python-version
@@ -0,0 +1 @@
+3.11
diff --git a/front/Dockerfile b/front/Dockerfile
index 14fec740..df3e4b2a 100644
--- a/front/Dockerfile
+++ b/front/Dockerfile
@@ -1,17 +1,29 @@
+FROM node:22-slim AS web-build
+
+WORKDIR /front
+COPY ./web/ /front/web/
+COPY ./src/static/netfront/ /front/src/static/netfront/
+COPY ./src/static/config_forms/ /front/src/static/config_forms/
+
+WORKDIR /front/web
+RUN npm install --no-audit --no-fund --prefer-offline
+RUN npm run build
+
FROM python:3.11
WORKDIR /app
-RUN pip install setuptools --upgrade
-RUN pip install wheel uwsgi
-ADD ./requirements.txt /app/requirements.txt
-RUN pip install -r requirements.txt
+COPY requirements.lock /app/requirements.lock
+RUN pip install --no-cache-dir -r /app/requirements.lock
+
RUN mkdir -p /app/.postgresql && \
wget "https://storage.yandexcloud.net/cloud-certs/CA.pem" -O /app/.postgresql/root.crt && \
chmod 0600 /app/.postgresql/root.crt
ADD ./src /app
+COPY --from=web-build /front/src/static/dist/ /app/static/dist/
+
ADD run_app.sh /app/run_app.sh
RUN chmod +x /app/run_app.sh
diff --git a/front/pyproject.toml b/front/pyproject.toml
new file mode 100644
index 00000000..03072126
--- /dev/null
+++ b/front/pyproject.toml
@@ -0,0 +1,70 @@
+[project]
+name = "miminet-front"
+version = "0.1.0"
+description = "Miminet front-end Flask application."
+requires-python = ">=3.11,<3.13"
+dependencies = [
+ "Flask==3.1.2",
+ "Flask-Admin==2.0.1",
+ "Flask-Cors==6.0.2",
+ "Flask-JWT-Extended==4.7.1",
+ "Flask-Login==0.6.3",
+ "Flask-Migrate==4.1.0",
+ "Flask-SQLAlchemy==3.1.1",
+ "Jinja2==3.1.6",
+ "Mako>=1.3.11",
+ "MarkupSafe==3.0.2",
+ "Pillow==11.3.0",
+ "PySocks==1.7.1",
+ "SQLAlchemy==2.0.43",
+ "Werkzeug==3.1.3",
+ "alembic==1.16.5",
+ "beautifulsoup4==4.13.5",
+ "cachecontrol>=0.14",
+ "celery==5.5.3",
+ "certifi==2025.10.5",
+ "charset-normalizer==3.4.3",
+ "click==8.1.8",
+ "colorama==0.4.6",
+ "dpkt==1.9.8",
+ "google==3.0.0",
+ "google-auth==2.40.3",
+ "google-auth-oauthlib==1.2.2",
+ "greenlet==3.2.4",
+ "h11==0.16.0",
+ "idna==3.10",
+ "importlib-metadata==8.7.0",
+ "importlib-resources==6.5.2",
+ "itsdangerous==2.2.0",
+ "jsonschema==4.25.1",
+ "kombu==5.5.4",
+ "oauthlib==3.3.1",
+ "psycopg2-binary==2.9.10",
+ "pyasn1==0.6.1",
+ "pyasn1-modules==0.4.2",
+ "python-dateutil==2.9.0.post0",
+ "python-dotenv==1.1.1",
+ "requests==2.32.5",
+ "requests-oauthlib==2.0.0",
+ "rsa==4.9.1",
+ "six==1.17.0",
+ "soupsieve==2.8",
+ "urllib3==2.5.0",
+ "uWSGI>=2.0",
+ "vine==5.1.0",
+ "wcwidth==0.2.13",
+ "websocket-client==1.8.0",
+ "wsproto==1.2.0",
+ "wtforms==3.2.1",
+ "zipp==3.23.0",
+]
+
+[project.optional-dependencies]
+test = [
+ "pytest==8.4.2",
+ "pytest-mock==3.15.1",
+ "selenium",
+]
+
+[tool.uv]
+package = false
diff --git a/front/requirements.lock b/front/requirements.lock
new file mode 100644
index 00000000..061582c8
--- /dev/null
+++ b/front/requirements.lock
@@ -0,0 +1,242 @@
+# This file was autogenerated by uv via the following command:
+# uv export --frozen --no-dev --no-hashes --no-emit-project --format requirements.txt -o requirements.lock
+alembic==1.16.5
+ # via
+ # flask-migrate
+ # miminet-front
+amqp==5.3.1
+ # via kombu
+attrs==26.1.0
+ # via
+ # jsonschema
+ # referencing
+beautifulsoup4==4.13.5
+ # via
+ # google
+ # miminet-front
+billiard==4.2.4
+ # via celery
+blinker==1.9.0
+ # via flask
+cachecontrol==0.14.4
+ # via miminet-front
+cachetools==5.5.2
+ # via google-auth
+celery==5.5.3
+ # via miminet-front
+certifi==2025.10.5
+ # via
+ # miminet-front
+ # requests
+charset-normalizer==3.4.3
+ # via
+ # miminet-front
+ # requests
+click==8.1.8
+ # via
+ # celery
+ # click-didyoumean
+ # click-plugins
+ # click-repl
+ # flask
+ # miminet-front
+click-didyoumean==0.3.1
+ # via celery
+click-plugins==1.1.1.2
+ # via celery
+click-repl==0.3.0
+ # via celery
+colorama==0.4.6
+ # via
+ # click
+ # miminet-front
+dpkt==1.9.8
+ # via miminet-front
+flask==3.1.2
+ # via
+ # flask-admin
+ # flask-cors
+ # flask-jwt-extended
+ # flask-login
+ # flask-migrate
+ # flask-sqlalchemy
+ # miminet-front
+flask-admin==2.0.1
+ # via miminet-front
+flask-cors==6.0.2
+ # via miminet-front
+flask-jwt-extended==4.7.1
+ # via miminet-front
+flask-login==0.6.3
+ # via miminet-front
+flask-migrate==4.1.0
+ # via miminet-front
+flask-sqlalchemy==3.1.1
+ # via
+ # flask-migrate
+ # miminet-front
+google==3.0.0
+ # via miminet-front
+google-auth==2.40.3
+ # via
+ # google-auth-oauthlib
+ # miminet-front
+google-auth-oauthlib==1.2.2
+ # via miminet-front
+greenlet==3.2.4
+ # via
+ # miminet-front
+ # sqlalchemy
+h11==0.16.0
+ # via
+ # miminet-front
+ # wsproto
+idna==3.10
+ # via
+ # miminet-front
+ # requests
+importlib-metadata==8.7.0
+ # via miminet-front
+importlib-resources==6.5.2
+ # via miminet-front
+itsdangerous==2.2.0
+ # via
+ # flask
+ # miminet-front
+jinja2==3.1.6
+ # via
+ # flask
+ # flask-admin
+ # miminet-front
+jsonschema==4.25.1
+ # via miminet-front
+jsonschema-specifications==2025.9.1
+ # via jsonschema
+kombu==5.5.4
+ # via
+ # celery
+ # miminet-front
+mako==1.3.10
+ # via
+ # alembic
+ # miminet-front
+markupsafe==3.0.2
+ # via
+ # flask
+ # flask-admin
+ # jinja2
+ # mako
+ # miminet-front
+ # werkzeug
+ # wtforms
+msgpack==1.1.2
+ # via cachecontrol
+oauthlib==3.3.1
+ # via
+ # miminet-front
+ # requests-oauthlib
+packaging==26.2
+ # via kombu
+pillow==11.3.0
+ # via miminet-front
+prompt-toolkit==3.0.52
+ # via click-repl
+psycopg2-binary==2.9.10
+ # via miminet-front
+pyasn1==0.6.1
+ # via
+ # miminet-front
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.4.2
+ # via
+ # google-auth
+ # miminet-front
+pyjwt==2.12.1
+ # via flask-jwt-extended
+pysocks==1.7.1
+ # via miminet-front
+python-dateutil==2.9.0.post0
+ # via
+ # celery
+ # miminet-front
+python-dotenv==1.1.1
+ # via miminet-front
+referencing==0.37.0
+ # via
+ # jsonschema
+ # jsonschema-specifications
+requests==2.32.5
+ # via
+ # cachecontrol
+ # miminet-front
+ # requests-oauthlib
+requests-oauthlib==2.0.0
+ # via
+ # google-auth-oauthlib
+ # miminet-front
+rpds-py==0.30.0
+ # via
+ # jsonschema
+ # referencing
+rsa==4.9.1
+ # via
+ # google-auth
+ # miminet-front
+six==1.17.0
+ # via
+ # miminet-front
+ # python-dateutil
+soupsieve==2.8
+ # via
+ # beautifulsoup4
+ # miminet-front
+sqlalchemy==2.0.43
+ # via
+ # alembic
+ # flask-sqlalchemy
+ # miminet-front
+typing-extensions==4.15.0
+ # via
+ # alembic
+ # beautifulsoup4
+ # referencing
+ # sqlalchemy
+tzdata==2026.2
+ # via kombu
+urllib3==2.5.0
+ # via
+ # miminet-front
+ # requests
+uwsgi==2.0.31
+ # via miminet-front
+vine==5.1.0
+ # via
+ # amqp
+ # celery
+ # kombu
+ # miminet-front
+wcwidth==0.2.13
+ # via
+ # miminet-front
+ # prompt-toolkit
+websocket-client==1.8.0
+ # via miminet-front
+werkzeug==3.1.3
+ # via
+ # flask
+ # flask-admin
+ # flask-cors
+ # flask-jwt-extended
+ # flask-login
+ # miminet-front
+wsproto==1.2.0
+ # via miminet-front
+wtforms==3.2.1
+ # via
+ # flask-admin
+ # miminet-front
+zipp==3.23.0
+ # via
+ # importlib-metadata
+ # miminet-front
diff --git a/front/requirements.txt b/front/requirements.txt
deleted file mode 100644
index 4ad25f44..00000000
--- a/front/requirements.txt
+++ /dev/null
@@ -1,57 +0,0 @@
-alembic==1.16.5
-beautifulsoup4==4.13.5
-certifi==2025.10.5
-charset-normalizer==3.4.3
-click==8.1.8
-colorama==0.4.6
-dpkt==1.9.8
-Flask==3.1.2
-Flask-Login==0.6.3
-Flask-Migrate==4.1.0
-Flask-SQLAlchemy==3.1.1
-google==3.0.0
-google-auth==2.40.3
-google-auth-oauthlib==1.2.2
-greenlet==3.2.4
-h11==0.16.0
-idna==3.10
-importlib-metadata==8.7.0
-importlib-resources==6.5.2
-ipaddress==1.0.23
-itsdangerous==2.2.0
-kombu==5.5.4
-Jinja2==3.1.6
-Mako==1.3.10
-MarkupSafe==3.0.2
-oauthlib==3.3.1
-Pillow==11.3.0
-pyasn1==0.6.1
-pyasn1_modules==0.4.2
-PySocks==1.7.1
-pytest==8.4.2
-pytest-mock==3.15.1
-python-dateutil==2.9.0.post0
-python-dotenv==1.1.1
-requests==2.32.5
-requests-oauthlib==2.0.0
-rsa==4.9.1
-six==1.17.0
-soupsieve==2.8
-SQLAlchemy==2.0.43
-urllib3==2.5.0
-uuid==1.30
-vine==5.1.0
-wcwidth==0.2.13
-websocket-client==1.8.0
-wsproto==1.2.0
-Werkzeug==3.1.3
-zipp==3.23.0
-celery==5.5.3
-python-dotenv==1.1.1
-wtforms==3.2.1
-jsonschema==4.25.1
-Flask-Admin==2.0.1
-psycopg2-binary==2.9.10
-flask_jwt_extended==4.7.1
-flask_cors==6.0.2
-openai>=1.0.0
diff --git a/front/src/miminet_auth.py b/front/src/miminet_auth.py
index 83269cb4..5a38e4be 100644
--- a/front/src/miminet_auth.py
+++ b/front/src/miminet_auth.py
@@ -38,7 +38,7 @@
from miminet_config import make_example_net_switch_and_hub
from miminet_model import Network, User, db
from oauthlib.oauth2 import TokenExpiredError
-from pip._vendor import cachecontrol
+import cachecontrol
from requests_oauthlib import OAuth2Session
from sqlalchemy.exc import SQLAlchemyError
from werkzeug.security import check_password_hash, generate_password_hash
diff --git a/front/src/static/config_devices.js b/front/src/static/config_devices.js
deleted file mode 100644
index 25a3d274..00000000
--- a/front/src/static/config_devices.js
+++ /dev/null
@@ -1,1702 +0,0 @@
-$('#config_host').load(ExternalUrlFor("/config_host.html"));
-$('#config_hub').load(ExternalUrlFor("/config_hub.html"));
-$('#config_switch').load(ExternalUrlFor("/config_switch.html"));
-$('#config_edge').load(ExternalUrlFor("/config_edge.html"));
-$('#config_router').load(ExternalUrlFor("/config_router.html"));
-$('#config_server').load(ExternalUrlFor("/config_server.html"));
-$('#config_vlan').load(ExternalUrlFor("/config_vlan.html"));
-$('#config_vxlan').load(ExternalUrlFor("/config_vxlan.html"));
-$("#config_textbox").load(ExternalUrlFor("/config_textbox.html"))
-
-const config_content_id = "#config_content";
-const config_main_form_id = "#config_main_form";
-const config_router_main_form_id = "#config_router_main_form";
-const config_server_main_form_id = "#config_server_main_form";
-const config_hub_main_form_id = "#config_hub_main_form";
-const config_switch_main_form_id = "#config_switch_main_form";
-const config_edge_main_form_id = "#config_edge_main_form";
-const config_content_save_tag = "#config_content_save";
-const config_content_save_id = "config_content_save";
-
-const ClearConfigForm = function (text) {
-
- let txt = ''
-
- if (!text) {
- txt = 'Тут будут настройки устройств. Выделите любое на схеме.';
- }
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
- $(config_content_id).append('' + txt + '');
- document.getElementById(config_content_save_id).style.display='none';
-
- // Update grid to reclaim full width
- if (typeof updateGridForConfigPanel === 'function') {
- updateGridForConfigPanel();
- }
-}
-
-const HostWarningMsg = function (msg) {
-
- let warning_msg = '
' +
- msg + '
';
-
- $(config_content_id).prepend(warning_msg);
-}
-const SwitchWarningMsg = function (msg) {
-
- let warning_msg = '' +
- msg + '
';
-
- $(config_content_id).prepend(warning_msg);
-}
-
-const ServerWarningMsg = function (msg) {
-
- let warning_msg = '' +
- msg + '
';
-
- $(config_content_id).prepend(warning_msg);
-}
-
-const HostErrorMsg = function (msg) {
-
- $(config_content_id).find('.alert-info, .alert-danger').remove();
-
- let error_msg = '' +
- msg + '
';
-
- $(config_content_id).prepend(error_msg);
-
- $("#config_main_form :input").prop("disabled", false);
- $("#config_router_main_form :input").prop("disabled", false);
- $("#config_server_main_form :input").prop("disabled", false);
- $("config_switch_main_form :input").prop("disabled", false);
-
- $('#config_host_main_form_submit_button').text('Сохранить').removeClass('disabled');
- $('#config_router_main_form_submit_button').text('Сохранить').removeClass('disabled');
- $('#config_server_main_form_submit_button').text('Сохранить').removeClass('disabled');
- $('#config_switch_main_form_submit_button').text('Сохранить').removeClass('disabled');
-}
-
-const UpdateJobCounter = function (counterId, deviceId = null) {
- const counter = document.getElementById(counterId);
- if (!counter) {
- return;
- }
-
- counter.style.display = 'none';
-}
-
-const UpdateHostConfigurationForm = function(host_id) {
- let data = $('#config_main_form').serialize();
-
- // Disable all input fields
- $("#config_main_form :input").prop("disabled", true);
-
- // Set loading spinner
- $('#config_host_main_form_submit_button').text('');
- $('#config_host_main_form_submit_button').append('Сохранение...');
-
- // Use unified delete and save function
- DeleteAndSaveJob('host', UpdateHostConfiguration, data, host_id);
-};
-
-const UpdateTextboxConfigurationForm = function (textbox_id) {
- let data = $("#config_textbox_main_form").serialize();
-
- // Disable all input fields
- $("#config_textbox_main_form :input").prop("disabled", true);
-
- // Set loading spinner
- $("#config_textbox_main_form_submit_button").text("");
- $("#config_textbox_main_form_submit_button").append(
- 'Сохранение...',
- );
-
- UpdateTextboxConfiguration(data, textbox_id);
-};
-
-const ConfigTextboxForm = function (textbox_id) {
-
- var form = document.getElementById(
- "config_textbox_main_form_script",
- ).innerHTML;
- var button = document.getElementById(
- "config_textbox_save_script",
- ).innerHTML;
-
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
-
- document.getElementById(config_content_save_id).style.display = "block";
-
- $(config_content_id).append(form);
- $(config_content_save_tag).append(button);
-
- $("#textbox_id").val(textbox_id);
- $("#net_guid").val(network_guid);
-
- function handleTextboxClick(event) {
- event.preventDefault();
- UpdateTextboxConfigurationForm(textbox_id);
- }
-
- $("#config_textbox_main_form_submit_button, #config_textbox_end_form").on(
- "click",
- handleTextboxClick,
- );
-};
-
-const ConfigHostForm = function (host_id) {
- var form = document.getElementById(
- "config_host_main_form_script",
- ).innerHTML;
- var button = document.getElementById("config_host_save_script").innerHTML;
- var banner = document.getElementById(
- "config_host_edit_banner_script",
- ).innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
-
- document.getElementById(config_content_save_id).style.display='block';
-
- // Add new form
- $(config_content_id).append(form);
- $(config_content_id).append(banner);
- $(config_content_save_tag).append(button);
-
- addIpFieldHandlers();
-
- // Set host_id
- $('#host_id').val(host_id);
- $('#net_guid').val(network_guid);
-
- function handleHostClick(event) {
- event.preventDefault();
- UpdateHostConfigurationForm(host_id);
- }
-
- $('#config_host_main_form_submit_button, #config_host_end_form').on('click', handleHostClick);
-
- // Update grid to exclude config panel area
- if (typeof updateGridForConfigPanel === 'function') {
- updateGridForConfigPanel();
- }
-}
-
-const ConfigRouterForm = function (router_id) {
- var form = document.getElementById('config_router_main_form_script').innerHTML;
- var button = document.getElementById('config_router_save_script').innerHTML;
- var banner = document.getElementById('config_router_edit_banner_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
-
- document.getElementById(config_content_save_id).style.display='block';
-
- // Add new form
- $(config_content_id).append(form);
- $(config_content_id).append(banner);
- $(config_content_save_tag).append(button);
-
- addIpFieldHandlers();
-
- // Set host_id
- $('#router_id').val(router_id);
- $('#net_guid').val(network_guid);
-
- function handleRouterClick(event) {
- event.preventDefault();
- let data = $('#config_main_form').serialize();
-
- // Disable all input fields
- $("#config_main_form :input").prop("disabled", true);
-
- // Set loading spinner
- $('#config_router_main_form_submit_button').text('');
- $('#config_router_main_form_submit_button').append('Сохранение...');
-
- // Use unified delete and save function
- DeleteAndSaveJob('router', UpdateRouterConfiguration, data, router_id);
- }
-
- $('#config_router_main_form_submit_button, #config_router_end_form').on('click', handleRouterClick);
-
- // Update grid to exclude config panel area
- if (typeof updateGridForConfigPanel === 'function') {
- updateGridForConfigPanel();
- }
-}
-
-const ConfigServerForm = function (server_id) {
- var form = document.getElementById('config_server_main_form_script').innerHTML;
- var button = document.getElementById('config_server_save_script').innerHTML;
- var banner = document.getElementById('config_server_edit_banner_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
-
- document.getElementById(config_content_save_id).style.display='block';
-
- // Add new form
- $(config_content_id).append(form);
- $(config_content_id).append(banner);
- $(config_content_save_tag).append(button);
-
- addIpFieldHandlers();
-
- // Set host_id
- $('#server_id').val(server_id);
- $('#net_guid').val(network_guid);
-
- function handleServerClick(event) {
- event.preventDefault();
- let data = $('#config_main_form').serialize();
-
- // Disable all input fields
- $("#config_main_form :input").prop("disabled", true);
-
- // Set loading spinner
- $('#config_server_main_form_submit_button').text('');
- $('#config_server_main_form_submit_button').append('Сохранение...');
-
- // Use unified delete and save function
- DeleteAndSaveJob('server', UpdateServerConfiguration, data, server_id);
- }
-
- $('#config_server_main_form_submit_button, #config_server_end_form').on('click', handleServerClick);
-
- // Update grid to exclude config panel area
- if (typeof updateGridForConfigPanel === 'function') {
- updateGridForConfigPanel();
- }
-}
-
-const ConfigHubForm = function (hub_id) {
- var form = document.getElementById('config_hub_main_form_script').innerHTML;
- var button = document.getElementById('config_hub_save_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
-
- document.getElementById(config_content_save_id).style.display='block';
-
- // Add new form
- $(config_content_id).append(form);
- $(config_content_save_tag).append(button);
-
- addIpFieldHandlers();
-
- // Set host_id
- $('#hub_id').val(hub_id);
- $('#net_guid').val(network_guid);
-
- function handleHubClick(event) {
- event.preventDefault();
- let data = $('#config_hub_main_form').serialize();
-
- // Disable all input fields
- $("#config_hub_main_form :input").prop("disabled", true);
-
- // Set loading spinner
- $('#config_hub_main_form_submit_button').text('');
- $('#config_hub_main_form_submit_button').append('Сохранение...');
-
- UpdateHubConfiguration(data, hub_id);
- }
-
- $('#config_hub_main_form_submit_button, #config_hub_end_form').on('click', handleHubClick);
-
- // Update grid to exclude config panel area
- if (typeof updateGridForConfigPanel === 'function') {
- updateGridForConfigPanel();
- }
-}
-
-const ConfigSwitchForm = function (switch_id) {
- var form = document.getElementById('config_switch_main_form_script').innerHTML;
- var button = document.getElementById('config_switch_save_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
-
- document.getElementById(config_content_save_id).style.display='block';
-
- // Add new form
- $(config_content_id).append(form);
- $(config_content_save_tag).append(button);
-
- addIpFieldHandlers();
-
- // Add href for mimishark
- // var url = "/MimiShark?guid="+network_guid
- // $(needhref).attr('href',url)
-
- // Set host_id
- $('#switch_id').val(switch_id);
- $('#net_guid').val(network_guid);
-
- function handleSwitchClick(event) {
- $("#config_switch_main_form [name='config_rstp_stp']").val($('#config_button_rstp').val());
- event.preventDefault();
- let data = $('#config_switch_main_form').serialize();
-
- // Disable all input fields
- $("#config_switch_main_form :input").prop("disabled", true);
-
- // Set loading spinner
- $('#config_switch_main_form_submit_button').text('');
- $('#config_switch_main_form_submit_button').append('Сохранение...');
-
- DeleteAndSaveJob('switch', UpdateSwitchConfiguration, data, switch_id);
- }
-
- $('#config_switch_main_form_submit_button, #config_switch_end_form').on('click', handleSwitchClick);
-
- // Update grid to exclude config panel area
- if (typeof updateGridForConfigPanel === 'function') {
- updateGridForConfigPanel();
- }
-}
-
-const ConfigEdgeForm = function (edge_id) {
- let edgeSaveXHR = null;
- var form = document.getElementById('config_edge_main_form_script').innerHTML;
- var button = document.getElementById('config_edge_save_script').innerHTML;
-
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
-
- document.getElementById(config_content_save_id).style.display='block';
-
- // Add new form
- $(config_content_id).append(form);
- $(config_content_save_tag).append(button);
-
- // Set host_id
- $('#edge_id').val(edge_id);
- $('#net_guid').val(network_guid);
-
- function handleEdgeClick(event) {
- event.preventDefault();
-
- if (edgeSaveXHR) {
- edgeSaveXHR.abort();
- }
-
- let data = $('#config_edge_main_form').serialize();
- const edge = edges.find(e => e.data.id === edge_id);
- console.log(edge);
- const lossValue = $("#edge_loss").val();
- const duplicateValue = $("#edge_duplicate").val();
-
- if (edge) {
- edge.data.loss_percentage = lossValue;
- edge.data.duplicate_percentage = duplicateValue;
- }
- const inputsToDisable = $('#edge_loss, #edge_duplicate, #config_edge_main_form_submit_button');
- inputsToDisable.prop("disabled", true);
-
- $('#config_edge_main_form_submit_button').html(
- ' Сохранение...'
- );
-
- edgeSaveXHR = UpdateEdgeConfiguration(data);
- inputsToDisable.prop("disabled", false);
- }
-
- $('#config_edge_main_form_submit_button, #config_edge_end_form').off('click').on('click', handleEdgeClick);
-
- // Update grid to exclude config panel area
- if (typeof updateGridForConfigPanel === 'function') {
- updateGridForConfigPanel();
- }
-}
-
-const ConfigHubName = function (hostname) {
-
- var text = document.getElementById('config_hub_name_script').innerHTML;
-
- $(config_hub_main_form_id).prepend(text);
- $("#config_hub_name").val(hostname);
-};
-
-const ConfigTextboxContent = function(textbox_name) {
- var text = document.getElementById("config_textbox_content_script").innerHTML;
-
- $("#config_textbox_main_form").prepend(text);
- $("#config_textbox_content").val(textbox_name)
-}
-
-const ConfigTextboxFontColor = function(textbox_font_color) {
- var colorScript = document.getElementById("config_textbox_font_color_script").innerHTML;
- $("#config_textbox_main_form").prepend(colorScript);
-
- var input = $("#config_textbox_font_color");
-
- var currentColor = textbox_font_color || "#000000";
- input.val(currentColor);
-
- const highlightColor = (selectedColor) => {
- if (!selectedColor) return;
- selectedColor = selectedColor.toLowerCase();
-
- var $form = $("#config_textbox_main_form");
- var $presets = $form.find(".color-preset");
- var $customWrapper = $form.find(".custom-color-wrapper");
-
- $presets.add($customWrapper).css({
- "outline": "none"
- });
-
- let foundPreset = false;
- $presets.each(function() {
- var $el = $(this);
- var presetColor = $el.data("color");
- if (presetColor && presetColor.toLowerCase() === selectedColor) {
- $el.css({
- "outline": "2px solid #0d6efd",
- "outline-offset": "2px"
- });
- foundPreset = true;
- return false;
- }
- });
-
- if (!foundPreset) {
- $customWrapper.css({
- "outline": "2px solid #0d6efd",
- "outline-offset": "2px"
- });
- }
- };
-
- highlightColor(currentColor);
-
- $("#config_textbox_main_form").on("click", ".color-preset", function() {
- var selectedColor = $(this).data("color");
- input.val(selectedColor);
- highlightColor(selectedColor);
- });
-
- input.on("input change", function() {
- highlightColor($(this).val());
- });
-}
-
-const ConfigTextboxFontControls = function(size, style, weight) {
- if ($("#config_textbox_font_size").length === 0) {
- var controls = document.getElementById("config_textbox_font_controls_script").innerHTML;
- $("#config_textbox_main_form").prepend(controls);
- }
-
- $("#config_textbox_font_size").val(size);
-
- var inputStyle = $("#config_textbox_font_style");
- var btnStyle = $("#btn_toggle_style");
- var inputWeight = $("#config_textbox_font_weight");
- var btnWeight = $("#btn_toggle_weight");
-
- inputStyle.val(style || 'normal');
- if (style === 'italic') {
- btnStyle.removeClass('btn-outline-secondary').addClass('btn-primary active');
- } else {
- btnStyle.removeClass('btn-primary active').addClass('btn-outline-secondary');
- }
-
- inputWeight.val(weight || 'normal');
- if (weight === 'bold') {
- btnWeight.removeClass('btn-outline-secondary').addClass('btn-primary active');
- } else {
- btnWeight.removeClass('btn-primary active').addClass('btn-outline-secondary');
- }
-
- $("#config_textbox_main_form").off('click', '#btn_toggle_style').on('click', '#btn_toggle_style', function() {
- var btn = $(this);
- var input = $("#config_textbox_font_style");
- if (input.val() === 'italic') {
- input.val('normal');
- btn.removeClass('btn-primary active').addClass('btn-outline-secondary');
- } else {
- input.val('italic');
- btn.removeClass('btn-outline-secondary').addClass('btn-primary active');
- }
- });
-
- $("#config_textbox_main_form").off('click', '#btn_toggle_weight').on('click', '#btn_toggle_weight', function() {
- var btn = $(this);
- var input = $("#config_textbox_font_weight");
- if (input.val() === 'bold') {
- input.val('normal');
- btn.removeClass('btn-primary active').addClass('btn-outline-secondary');
- } else {
- input.val('bold');
- btn.removeClass('btn-outline-secondary').addClass('btn-primary active');
- }
- });
-}
-
-const ConfigEdgeNetworkIssues = function (edge_loss, edge_duplicate) {
- var text = document.getElementById('config_edge_set_network_issues_script').innerHTML;
- $(config_edge_main_form_id).prepend(text);
- $('#edge_loss').val(edge_loss);
- $('#edge_duplicate').val(edge_duplicate);
-};
-
-const ConfigEdgeEndpoints = function (edge_source, edge_target) {
-
- var text = document.getElementById('config_edge_edpoint_script').innerHTML;
-
- $(config_edge_main_form_id).prepend((text));
- $('#edge_source').val(edge_source);
- $('#edge_target').val(edge_target);
-}
-
-const ConfigSwitchName = function (hostname) {
-
- var text = document.getElementById('config_switch_name_script').innerHTML;
-
- $(config_switch_main_form_id).prepend((text));
- $('#switch_name').val(hostname);
-}
-
-const ConfigSwtichSTP = function (stp) {
- var elem = document.getElementById('config_switch_checkbox_stp_script');
-
- $(elem.innerHTML).insertBefore('#config_switch_end_form');
-
- if (stp === 1) {
- $('#config_switch_stp').attr('checked', 'checked');
- }
-
- var warning_text = document.getElementById('config_switch_warning_stp_script').innerHTML;
- $('#config_switch_stp').on('click', function () {
- if ($(this).is(':checked')) {
- $(warning_text).insertBefore('#config_switch_end_form');
- } else {
- $('#config_warning_stp').remove();
- }
- });
-}
-
-const ConfigSwtichRSTP = function (rstp) {
- var elem = document.getElementById('config_switch_checkbox_rstp_script');
-
- $(elem.innerHTML).insertBefore('#config_switch_end_form');
-
- if (rstp === 1) {
- $('#config_switch_rstp').attr('checked', 'checked');
- }
-
- var warning_text = document.getElementById('config_switch_warning_rstp_script').innerHTML;
- $('#config_switch_rstp').on('click', function () {
- if ($(this).is(':checked')) {
- $(warning_text).insertBefore('#config_switch_end_form');
- } else {
- $('#config_warning_rstp').remove();
- }
- });
-}
-
-const SharedConfigHostForm = function(host_id){
- var form = document.getElementById('config_host_main_form_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
- document.getElementById(config_content_save_id).style.display='none';
-
- // Add new form
- $(config_content_id).append(form);
-
- // Set host_id
- $('#host_id').val( host_id );
- $('#net_guid').val( network_guid );
- $('#config_host_main_form_submit_button').prop('disabled', true);
-}
-
-const SharedConfigRouterForm = function (router_id) {
- var form = document.getElementById('config_router_main_form_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
- document.getElementById(config_content_save_id).style.display='none';
-
- // Add new form
- $(config_content_id).append(form);
-
- // Set host_id
- $('#router_id').val(router_id);
- $('#net_guid').val(network_guid);
-
- $('#config_router_main_form_submit_button').prop('disabled', true);
-}
-
-const SharedConfigServerForm = function (router_id) {
- var form = document.getElementById('config_server_main_form_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
- document.getElementById(config_content_save_id).style.display='none';
-
- // Add new form
- $(config_content_id).append(form);
-
- // Set host_id
- $('#router_id').val(router_id);
- $('#net_guid').val(network_guid);
-
- $('#config_server_main_form_submit_button').prop('disabled', true);
-}
-
-const SharedConfigHubForm = function (hub_id) {
- var form = document.getElementById('config_hub_main_form_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
- document.getElementById(config_content_save_id).style.display='none';
-
- // Add new form
- $(config_content_id).append(form);
- $('#config_hub_main_form_submit_button').prop('disabled', true);
-}
-
-const SharedConfigSwitchForm = function (switch_id) {
- var form = document.getElementById('config_switch_main_form_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
- document.getElementById(config_content_save_id).style.display='none';
-
- // Add new form
- $(config_content_id).append(form);
- $('#config_switch_main_form_submit_button').prop('disabled', true);
-}
-
-const SharedConfigEdgeForm = function (edge_id) {
- var form = document.getElementById('config_edge_main_form_script').innerHTML;
-
- // Clear all child
- $(config_content_id).empty();
- $(config_content_save_tag).empty();
- document.getElementById(config_content_save_id).style.display='block';
-
- // Add new form
- $(config_content_id).append(form);
- $('#config_edge_main_form_submit_button').prop('disabled', true);
-}
-
-
-const ConfigHostName = function (hostname) {
-
- var text = document.getElementById('config_host_name_script').innerHTML;
-
- $(config_main_form_id).prepend((text));
- $('#config_host_name').val(hostname);
-}
-
-const ConfigRouterName = function (hostname) {
-
- var text = document.getElementById('config_router_name_script').innerHTML;
-
- $(config_main_form_id).prepend((text));
- $('#config_router_name').val(hostname);
-}
-
-const ConfigServerName = function (hostname) {
-
- var text = document.getElementById('config_server_name_script').innerHTML;
-
- $(config_main_form_id).prepend((text));
- $('#config_server_name').val(hostname);
-}
-
-const ConfigItemInterface = function (name, ip, netmask, connected_to, item) {
-
- let conf_item = 'config_' + item;
- let elem = document.getElementById(conf_item + '_interface_script');
- let eth = jQuery.extend({}, elem);
-
- if (!name) {
- return;
- }
-
- let ids = ["_iface_name_label_", "_iface_name_", "_ip_", "_mask_"];
- ids.forEach(function (id) {
- eth.innerHTML = eth.innerHTML.replace(RegExp(conf_item + id + 'example', "g"), conf_item + id + name);
- });
-
- let tag = '#' + conf_item;
- let text = eth.innerHTML;
- $(text).insertBefore(tag + '_end_form');
-
- $('').insertBefore(tag + ids[1] + name);
- $(tag + ids[1] + name).attr("placeholder", connected_to);
- $(tag + ids[2] + name).val(ip);
- $(tag + ids[3] + name).val(netmask);
-
- if (Array.isArray(pcaps) && pcaps.includes(name)) {
- $(tag + '_iface_name_label_' + name).html('Линк к (pcap)');
- } else {
- console.warn('pcaps не определен или не является массивом:', pcaps);
- }
-}
-
-const ConfigHostInterface = function (name, ip, netmask, connected_to) {
- ConfigItemInterface(name, ip, netmask, connected_to, "host");
-}
-
-const ConfigRouterInterface = function (name, ip, netmask, connected_to) {
- ConfigItemInterface(name, ip, netmask, connected_to, "router");
-}
-
-const ConfigServerInterface = function (name, ip, netmask, connected_to) {
- ConfigItemInterface(name, ip, netmask, connected_to, "server");
-}
-
-const ConfigHubInterface = function (name, ip, netmask, connected_to) {
- ConfigItemInterface(name, ip, netmask, connected_to, "hub");
-}
-
-const ConfigSwitchInterface = function (name, ip, netmask, connected_to) {
- ConfigItemInterface(name, ip, netmask, connected_to, "switch");
-}
-
-const ConfigItemIndent = function (item) {
- let conf_item = 'config_' + item
- let text = document.getElementById(conf_item + '_indent_script').innerHTML;
- $(text).insertBefore('#' + conf_item + '_end_form');
-}
-
-const ConfigHubIndent = function () {
- ConfigItemIndent("hub");
-}
-
-const ConfigSwitchIndent = function () {
- ConfigItemIndent("switch");
-}
-
-const addIpFieldHandlers = function () {
- document.addEventListener('input', function (e) {
- const input = e.target;
-
- if (!input.matches('input[type="text"][id*="ip"], input[type="text"][name*="ip"], input[type="text"][id*="gw"], input[type="text"][name*="gw"]')) {
- return;
- }
-
- const newValue = input.value.replace(/,/g, '.').replace(/ю/g, '.');
-
- input.value = newValue;
- });
-};
-
-const UpdateHostForm = function(name) {
- elem = document.getElementById(name).innerHTML;
- host_job_list = document.getElementById('config_host_job_list');
-
- if (!elem || !host_job_list) {
- return;
- }
-
- $('div[name="config_host_select_input"]').remove();
- $(elem).insertBefore(host_job_list);
-};
-
-const ConfigHostJobOnChange = function (evnt) {
-
- let elem = null;
- let host_job_list = null;
-
- switch (evnt.target.value) {
- case '1':
- UpdateHostForm('config_host_ping_c_1_script');
- break;
-
- case '2':
- UpdateHostForm('config_host_ping_with_options_script');
- break;
-
- case '3':
- UpdateHostForm('config_host_send_udp_data_script');
- break;
-
- case '4':
- UpdateHostForm('config_host_send_tcp_data_script');
- break;
-
- case '5':
- UpdateHostForm('config_host_traceroute_with_options_script');
- break;
-
- case '102':
- UpdateHostForm('config_host_add_route_script');
- break;
-
- case '103':
- UpdateHostForm('config_host_add_arp_cache_script');
- break;
-
- case '108':
- UpdateHostForm('config_host_add_dhclient');
- FillDeviceSelectIntf('#config_host_add_dhclient_interface_select_iface_field', '#host_id', "Выберите линк", false)
- break;
-
- case '0':
- $('div[name="config_host_select_input"]').remove();
- break;
-
- default:
- console.log("Unknown target.value");
- }
-
-}
-
-const ConfigHostJob = function (host_jobs, shared = 0) {
-
- let elem = document.getElementById('config_host_job_script').innerHTML;
- let host_id = document.getElementById('host_id');
-
- if (!elem || !host_id) {
- return;
- }
-
- $(elem).insertBefore(host_id);
-
- // Set onchange
- document.getElementById('config_host_job_select_field').addEventListener('change', ConfigHostJobOnChange);
-
- // Update job counter with device ID
- UpdateJobCounter('config_host_job_counter', host_id.value);
-
- elem = document.getElementById('config_host_job_list_script').innerHTML;
- if (!elem) {
- return;
- }
-
- $(elem).insertBefore(host_id);
-
- // Print jobs if we have
- if (!host_jobs) {
- return;
- }
-
- $.each(host_jobs, function (i) {
- let jid = host_jobs[i].id;
-
- if (i == 0) {
- $('#config_host_job_list').append('');
- }
-
- elem = document.getElementById('config_host_job_list_elem_script');
-
- if (!elem) {
- return;
- }
-
- let job_elem = jQuery.extend({}, elem);
- job_elem.innerHTML = job_elem.innerHTML.replace(/config_host_job_delete/g, 'config_host_job_delete_' + jid);
- job_elem.innerHTML = job_elem.innerHTML.replace(/config_host_job_edit/g, 'config_host_job_edit_' + jid);
- job_elem.innerHTML = job_elem.innerHTML.replace(/justify-content-between align-items-center\">/, 'justify-content-between align-items-center\">' + host_jobs[i].print_cmd + '');
-
- let text = job_elem.innerHTML;
- //$(text).insertBefore(host_id);
- $('#config_host_job_list').append(text);
-
- $('#config_host_job_delete_' + jid).click(function (event) {
- event.preventDefault();
- if (!shared) {
- DeleteJobFromHost(host_id.value, jid, network_guid);
- }
- });
-
- $('#config_host_job_edit_' + jid).click(function (event) {
- event.preventDefault();
- if (!shared) {
- EditJobInHost(host_id.value, jid, network_guid);
- }
- });
- });
-}
-
-const ConfigHostGateway = function (gw) {
-
- var text = document.getElementById('config_host_default_gw_script').innerHTML;
-
- $(text).insertBefore('#config_host_end_form');
- $('#config_host_default_gw').val(gw);
-}
-
-const ConfigRouterGateway = function (gw) {
-
- var text = document.getElementById('config_router_default_gw_script').innerHTML;
-
- $(text).insertBefore('#config_router_end_form');
- $('#config_router_default_gw').val(gw);
-}
-
-const ConfigServerGateway = function (gw) {
-
- var text = document.getElementById('config_server_default_gw_script').innerHTML;
-
- $(text).insertBefore('#config_server_end_form');
- $('#config_server_default_gw').val(gw);
-}
-
-const UpdateSwitchForm = function(name) {
- elem = document.getElementById(name).innerHTML;
- switch_job_list = document.getElementById('config_switch_job_list');
-
- if (!elem || !switch_job_list) {
- return;
- }
-
- $('div[name="config_switch_select_input"]').remove();
- $(elem).insertBefore(switch_job_list);
-};
-
-const ConfigSwitchJobOnChange = function(evnt) {
- switch (evnt.target.value) {
- case '0':
- $('div[name="config_switch_select_input"]').remove();
-
- break;
- case '6':
- UpdateSwitchForm('config_switch_link_down_script');
- FillDeviceSelectIntf("#config_switch_link_down_iface_select_field", '#switch_id', "Выберите линк", false);
- break;
- case '7':
- UpdateSwitchForm('config_switch_sleep_script');
- }
-}
-const ConfigSwitchJob = function (switch_jobs, shared = 0) {
-
- let elem = document.getElementById('config_switch_job_script').innerHTML;
- let switch_id = document.getElementById('switch_id');
-
- if (!elem || !switch_id) {
- return;
- }
-
- $(elem).insertBefore(switch_id);
-
- // Set onchange
- document.getElementById('config_switch_job_select_field').addEventListener('change', ConfigSwitchJobOnChange);
-
- // Update job counter with device ID
- UpdateJobCounter('config_switch_job_counter', switch_id.value);
-
- elem = document.getElementById('config_switch_job_list_script').innerHTML;
- if (!elem) {
- return;
- }
-
- $(elem).insertBefore(switch_id);
-
- // Print jobs if we have
- if (!switch_jobs) {
- return;
- }
-
- $.each(switch_jobs, function (i) {
- let jid = switch_jobs[i].id;
-
- if (i == 0) {
- $('#config_switch_job_list').append('');
- }
-
- elem = document.getElementById('config_switch_job_list_elem_script');
-
- if (!elem) {
- return;
- }
-
- let job_elem = jQuery.extend({}, elem);
- job_elem.innerHTML = job_elem.innerHTML.replace(/config_switch_job_delete/g, 'config_switch_job_delete_' + jid);
- job_elem.innerHTML = job_elem.innerHTML.replace(/config_switch_job_edit/g, 'config_switch_job_edit_' + jid);
- job_elem.innerHTML = job_elem.innerHTML.replace(/justify-content-between align-items-center\">/, 'justify-content-between align-items-center\">' + switch_jobs[i].print_cmd + '');
-
- let text = job_elem.innerHTML;
- //$(text).insertBefore(host_id);
- $('#config_switch_job_list').append(text);
-
- $('#config_switch_job_delete_' + jid).click(function (event) {
- event.preventDefault();
- if (!shared) {
- DeleteJobFromSwitch(switch_id.value, jid, network_guid);
- }
- });
-
- $('#config_switch_job_edit_' + jid).click(function (event) {
- event.preventDefault();
- if (!shared) {
- EditJobInSwitch(switch_id.value, jid, network_guid);
- }
- });
- });
-}
-
-const ConfigRouterJobOnChange = function(evnt) {
-
- switch (evnt.target.value) {
- case '0':
- $('div[name="config_router_select_input"]').remove();
-
- break;
- case '1':
- UpdateRouterForm('config_router_ping_c_1_script');
-
- break;
- case '100':
- UpdateRouterForm('config_router_add_ip_mask_script');
- FillDeviceSelectIntf("#config_router_add_ip_mask_iface_select_field", '#router_id', "Выберите линк", false);
-
- break;
- case '101':
- UpdateRouterForm('config_router_add_nat_masquerade_script');
- FillDeviceSelectIntf("#config_router_add_nat_masquerade_iface_select_field", '#router_id', "Выберите линк", false);
-
- break;
- case '102':
- UpdateRouterForm('config_router_add_route_script');
-
- break;
- case '104':
- UpdateRouterForm('config_router_add_subinterface_script');
- FillDeviceSelectIntf("#config_router_add_subinterface_iface_select_field", '#router_id', "Выберите линк" ,false);
-
- break;
- case '105':
- UpdateRouterForm('config_router_add_ipip_tunnel_script');
- FillDeviceSelectIntf("#config_router_add_ipip_tunnel_iface_select_ip_field", '#router_id');
-
- break;
- case '106':
- UpdateRouterForm('config_router_add_gre_interface_script');
- FillDeviceSelectIntf("#config_router_add_gre_interface_select_ip_field", '#router_id');
-
- break;
- case '107':
- UpdateRouterForm('config_router_add_arp_proxy_script');
- FillDeviceSelectIntf("#config_router_add_arp_proxy_iface_select_field", '#router_id', "Выберите линк", false);
- case '109':
- UpdateRouterForm('config_router_add_port_forwarding_tcp_script');
- FillDeviceSelectIntf("#config_router_add_port_forwarding_tcp_iface_select_field", "#router_id", "Выберите линк", false)
- break;
- case '110':
- UpdateRouterForm('config_router_add_port_forwarding_udp_script');
- FillDeviceSelectIntf("#config_router_add_port_forwarding_udp_iface_select_field", "#router_id", "Выберите линк", false)
- break;
- default:
- console.log("Unknown target.value");
- }
-}
-
-const ConfigRouterJob = function (router_jobs, shared = 0) {
-
- let elem = document.getElementById('config_router_job_script').innerHTML;
- let router_id = document.getElementById('router_id');
-
- if (!elem || !router_id) {
- return;
- }
-
- $(elem).insertBefore(router_id);
-
- // Set onchange
- document.getElementById('config_router_job_select_field').addEventListener('change', ConfigRouterJobOnChange);
-
- // Update job counter with device ID
- UpdateJobCounter('config_router_job_counter', router_id.value);
-
- elem = document.getElementById('config_router_job_list_script').innerHTML;
- if (!elem) {
- return;
- }
-
- $(elem).insertBefore(router_id);
-
- // Print jobs if we have
- if (!router_jobs) {
- return;
- }
-
- $.each(router_jobs, function (i) {
- let jid = router_jobs[i].id;
-
- if (i == 0) {
- $('#config_router_job_list').append('');
- }
-
- elem = document.getElementById('config_router_job_list_elem_script');
-
- if (!elem) {
- return;
- }
-
- let job_elem = jQuery.extend({}, elem);
- job_elem.innerHTML = job_elem.innerHTML.replace(/config_router_job_delete/g, 'config_router_job_delete_' + jid);
- job_elem.innerHTML = job_elem.innerHTML.replace(/config_router_job_edit/g, 'config_router_job_edit_' + jid);
- job_elem.innerHTML = job_elem.innerHTML.replace(/justify-content-between align-items-center\">/, 'justify-content-between align-items-center\">' + router_jobs[i].print_cmd + '');
-
- let text = job_elem.innerHTML;
- //$(text).insertBefore(host_id);
- $('#config_router_job_list').append(text);
-
- $('#config_router_job_delete_' + jid).click(function (event) {
- event.preventDefault();
- if (!shared) {
- DeleteJobFromRouter(router_id.value, jid, network_guid);
- }
- });
-
- $('#config_router_job_edit_' + jid).click(function (event) {
- event.preventDefault();
- if (!shared) {
- EditJobInRouter(router_id.value, jid, network_guid);
- }
- });
- });
-}
-
-const ConfigServerJob = function (server_jobs, shared = 0) {
-
- let elem = document.getElementById('config_server_job_script').innerHTML;
- let server_id = document.getElementById('server_id');
-
- if (!elem || !server_id) {
- return;
- }
-
- $(elem).insertBefore(server_id);
-
- // Set onchange
- document.getElementById('config_server_job_select_field').addEventListener('change', ConfigServerJobOnChange);
-
- // Update job counter with device ID
- UpdateJobCounter('config_server_job_counter', server_id.value);
-
- elem = document.getElementById('config_server_job_list_script').innerHTML;
- if (!elem) {
- return;
- }
-
- $(elem).insertBefore(server_id);
-
- // Print jobs if we have
- if (!server_jobs) {
- return;
- }
-
- $.each(server_jobs, function (i) {
- let jid = server_jobs[i].id;
-
- if (i == 0) {
- $('#config_server_job_list').append('');
- }
-
- elem = document.getElementById('config_server_job_list_elem_script');
-
- if (!elem) {
- return;
- }
-
- let job_elem = jQuery.extend({}, elem);
- job_elem.innerHTML = job_elem.innerHTML.replace(/config_server_job_delete/g, 'config_server_job_delete_' + jid);
- job_elem.innerHTML = job_elem.innerHTML.replace(/config_server_job_edit/g, 'config_server_job_edit_' + jid);
- job_elem.innerHTML = job_elem.innerHTML.replace(/justify-content-between align-items-center\">/, 'justify-content-between align-items-center\">' + server_jobs[i].print_cmd + '');
-
- let text = job_elem.innerHTML;
- //$(text).insertBefore(host_id);
- $('#config_server_job_list').append(text);
-
- $('#config_server_job_delete_' + jid).click(function (event) {
- event.preventDefault();
-
- if (!shared) {
- DeleteJobFromServer(server_id.value, jid, network_guid);
- }
-
- });
-
- $('#config_server_job_edit_' + jid).click(function (event) {
- event.preventDefault();
- if (!shared) {
- EditJobInServer(server_id.value, jid, network_guid);
- }
- });
- });
-}
-
-const UpdateServerForm = function(name) {
- elem = document.getElementById(name).innerHTML;
- server_job_list = document.getElementById('config_server_job_list');
-
- if (!elem || !server_job_list) {
- return;
- }
-
- $('div[name="config_server_select_input"]').remove();
- $(elem).insertBefore(server_job_list);
-}
-
-const ConfigServerJobOnChange = function (evnt) {
-
- let elem = null;
- let server_job_list = null;
- let n = null;
- let server_id = null;
-
- switch (evnt.target.value) {
- case '0':
- $('div[name="config_server_select_input"]').remove();
- break;
-
- case '1':
- UpdateServerForm('config_server_ping_c_1_script');
- break;
-
- case '200':
- UpdateServerForm('config_server_start_udp_server_script');
- break;
-
- case '201':
- UpdateServerForm('config_server_start_tcp_server_script');
- break;
-
- case '202':
- UpdateServerForm('config_server_block_tcp_udp_port_script');
- break;
-
- case '203':
- UpdateServerForm('config_server_add_dhcp_server_script');
- FillDeviceSelectIntf('#config_server_add_dhcp_interface_select_iface_field', '#server_id', "Выберите линк", false)
- break;
-
- default:
- console.log("Unknown target.value");
- }
-
-}
-
-const DisableFormInputs = function () {
- let s = config_content_id + ' :input';
- $(s).prop("disabled", true);
- $(config_content_save_tag + ' :input').prop("disabled", true);
-}
-
-const DisableVLANInputs = function (n) {
- var modalId = 'VlanModal_' + n.data.id;
-
- $(document).ready(function () {
- $('#config_button_vlan').prop('disabled', false);
- $('#' + modalId + ' :input').not('.btn-close').prop('disabled', true);
- $('#' + modalId + ' .form-check-input, ' + modalId + ' .form-switch input').prop('disabled', true);
- });
-};
-
-const UpdateRouterForm = function(name) {
- /**
- * Replace old form with new one
- */
- elem = document.getElementById(name).innerHTML;
- router_job_list = document.getElementById('config_router_job_list');
-
- if (!elem || !router_job_list){
- return;
- }
-
- $('div[name="config_router_select_input"]').remove();
- $(elem).insertBefore(router_job_list);
-}
-
-const FillDeviceSelectIntf = function(select_id, device, field_msg = 'Интерфейс начальной точки', return_ip = true) {
- /**
- * Fill select element with network hosts.
- * @param {String} select_id ID(name) of the element to which you need to add data.
- * @param {String} field_msg Message that will be displayed in the select list by default.
- * @param {Boolean} return_ip True if replace user's input with ip and False if replace it with element's id.
- */
-
- // configured router id
- device_id = $(device)[0].value;
-
- if (!device_id) {
- console.log("Не нашел device_id");
- return
- }
-
- device_node = nodes.find(n => n.data.id === device_id);
- device_type = device.slice(1, -3 ) //example : #router_id -> router
-
- if (!device_node) {
- console.log("Не нашел device_node");
- return;
- }
-
- if (!device_node.interface.length) {
- $(select_id).append('');
- return;
- } else {
- $(select_id).append(``);
- }
- $(select_id).on('change', function () {
- let selectedOption = $(this).find('option:selected'); // Получаем выбранный элемент
- let selectedLabel = selectedOption.text(); // Получаем текст выбранного элемента
- document.getElementById(device_type + '_connection_host_label_hidden').value = selectedLabel; // Записываем его в скрытое поле
- });
-
- device_node.interface.forEach(function(iface) {
- // iterating over the router interfaces
-
- let iface_id = iface.id;
- let iface_ip = iface.ip;
-
- if (!iface_id || (return_ip && !iface_ip)) {
- console.log("Не нашел ip/id у интерфейса");
- return;
- }
-
- let connect_id = iface.connect;
- if (!connect_id) {
- console.log("Не нашел подключение у интерфейса");
- return;
- }
-
- let edge = edges.find(e => e.data.id === connect_id);
-
- if (!edge) {
- console.log("Не нашел ребро по подключению интерфейса");
- return;
- }
-
- let edge_source = edge.data.source;
- let edge_target = edge.data.target;
-
- if (!edge_source || !edge_target) {
- console.log("Не получилось найти target и source у ребра");
- return;
- }
-
- let device_connection = (device_node.data.id === edge_target) ? edge_source : edge_target;
-
- let device_connection_host_node = nodes.find(n => n.data.id === device_connection);
- let device_connection_host_label = (device_connection_host_node) ? device_connection_host_node.data.label : "Unknown";
-
- $(select_id).append('');
-
- });
-}
-
-
-const DisableVXLANInputs = function (n) {
- var modalId = 'VxlanConfigModal' + n.data.id;
-
-
- $(document).ready(function () {
- $('#config_button_vxlan').prop('disabled', false);
- $('#' + modalId + ' :input').not('.btn-close').prop('disabled', true);
- $('#' + modalId + ' .form-check-input, #' + modalId + ' .form-switch input').prop('disabled', true);
- $('
- `);
- }
-
- let activeNodeId = null;
-
- const hideResizeFrame = () => {
- $('#resize-frame').remove();
- activeNodeId = null;
- };
-
- $(document).off('mousedown.resizeHide').on('mousedown.resizeHide', function(e) {
- if ($(e.target).closest('#network_scheme').length > 0) {
- return;
- }
-
- if ($(e.target).closest('.resize-frame').length > 0 || $(e.target).hasClass('resize-handle')) {
- return;
- }
-
- hideResizeFrame();
- });
-
- const updateResizeFrame = () => {
- if (!activeNodeId) return;
- const node = cy.getElementById(activeNodeId);
- if (node.empty() || node.removed()) { hideResizeFrame(); return; }
-
- const bb = node.renderedBoundingBox({ includeLabels: false });
- const containerOffset = $(cy.container()).offset();
-
- let frame = $('#resize-frame');
- if (frame.length === 0) return;
-
- frame.css({
- top: containerOffset.top + bb.y1,
- left: containerOffset.left + bb.x1,
- width: bb.w,
- height: bb.h
- });
- };
-
- const initResizeFrame = (node) => {
- hideResizeFrame();
- activeNodeId = node.id();
-
- let n = nodes.find(n => n.data.id === node.id());
- if (!n || !n.config) return;
-
- const frame = $('');
- $('body').append(frame);
-
- const handles = [
- { d: 'nw', x: 0, y: 0, c: 'nw-resize' }, { d: 'n', x: 50, y: 0, c: 'n-resize' },
- { d: 'ne', x: 100, y: 0, c: 'ne-resize' }, { d: 'e', x: 100, y: 50, c: 'e-resize' },
- { d: 'se', x: 100, y: 100, c: 'se-resize' }, { d: 's', x: 50, y: 100, c: 's-resize' },
- { d: 'sw', x: 0, y: 100, c: 'sw-resize' }, { d: 'w', x: 0, y: 50, c: 'w-resize' }
- ];
-
- handles.forEach(h => {
- const handle = $('');
- handle.css({ left: h.x + '%', top: h.y + '%', cursor: h.c });
-
- handle.on('mousedown', function(e) {
- e.stopPropagation();
- e.preventDefault();
-
- const startX = e.pageX;
- const startY = e.pageY;
- const startW = n.config.width || 100;
- const startH = n.config.height || 50;
- const startPos = {x: node.position().x, y: node.position().y};
- const zoom = cy.zoom();
-
- node.ungrabify();
-
- $(document).on('mousemove.resizing', function(ev) {
- const dx = (ev.pageX - startX) / zoom;
- const dy = (ev.pageY - startY) / zoom;
-
- let newW = startW;
- let newH = startH;
- let newX = startPos.x;
- let newY = startPos.y;
-
- // Horizontal Resize
- if (h.d.includes('e')) {
- newW = Math.max(30, startW + dx);
- newX += (newW - startW) / 2; // Shift center right
- }
- if (h.d.includes('w')) {
- newW = Math.max(30, startW - dx);
- newX -= (newW - startW) / 2; // Shift center left
- }
-
- // Vertical Resize
- if (h.d.includes('s')) {
- newH = Math.max(30, startH + dy);
- newY += (newH - startH) / 2; // Shift center down
- }
- if (h.d.includes('n')) {
- newH = Math.max(30, startH - dy);
- newY -= (newH - startH) / 2; // Shift center up
- }
-
- n.config.width = newW;
- n.config.height = newH;
-
- node.style('width', newW);
- node.style('height', newH);
- node.style('text-max-width', newW);
-
- node.position({x: newX, y: newY});
-
- updateResizeFrame();
- });
-
- $(document).on('mouseup.resizing', function() {
- $(document).off('.resizing');
- node.grabify();
- TakeGraphPictureAndUpdate();
- MoveNodes();
- });
- });
-
- frame.append(handle);
- });
-
- updateResizeFrame();
- };
-
- cy.on('tap', '.textbox', (e) => initResizeFrame(e.target));
- cy.on('tap', (e) => { if (e.target === cy) hideResizeFrame(); });
- cy.on('zoom pan position', updateResizeFrame);
-
- let allowEdges = function(src, tgt) {
- const sNode = nodes.find(n => n.data.id === src.id());
- const tNode = nodes.find(n => n.data.id === tgt.id());
-
- if (sNode && sNode.config && sNode.config.type === 'textbox') return false;
- if (tNode && tNode.config && tNode.config.type === 'textbox') return false;
-
- return !src.same(tgt);
- }
-
- let customDefaults = {
- handleNodes: '.host, .l2_switch, .l1_hub, .l3_router, .server',
- canConnect: (src, tgt) => allowEdges(src, tgt),
-
- edgeParams: (src, tgt) => allowEdges(src, tgt) ? {} : null,
-
- hoverDelay: 150,
- snap: false,
- snapThreshold: 50,
- snapFrequency: 15,
- noEdgeEventsInDraw: true,
- disableBrowserGestures: true,
- };
-
- global_eh = cy.edgehandles(customDefaults);
-
- cy.minZoom(0.5);
- cy.maxZoom(2);
-
- cy.add(nodes);
- cy.add(edges);
-
- // Mark edges that have a link-down job configured
- MarkLinkDownEdges(cy);
-
- // Auto-snap existing network nodes on load
- SnapNodesToGrid(cy);
-
- // Changing zoom
- cy.on('zoom', function(evt){
-
- if (NetworkUpdateTimeoutId >= 0){
- clearTimeout(NetworkUpdateTimeoutId);
- NetworkUpdateTimeoutId = -1;
- }
-
- NetworkUpdateTimeoutId = setTimeout(UpdateNetworkConfig, 2000);
-
- // Update grid zoom and redraw
- if (gridCanvasLayer) {
- currentGridZoom = cy.zoom();
- drawGrid();
- }
- });
-
- // Changing the pan
- cy.on('pan', function(evt){
-
- if (NetworkUpdateTimeoutId >= 0){
- clearTimeout(NetworkUpdateTimeoutId);
- NetworkUpdateTimeoutId = -1;
- }
-
- NetworkUpdateTimeoutId = setTimeout(UpdateNetworkConfig, 2000);
-
- // Update grid when panning to keep it aligned with nodes
- if (gridCanvasLayer) {
- drawGrid();
- }
- });
-
- // Looking for a position changing
- cy.on('dragfree', 'node', function(evt){
-
- //let node_id = evt.target.id();
- let n = nodes.find(n => n.data.id === this.id());
-
- if (!n) {
- return;
- }
-
- // Get current position
- let posX = this.position().x;
- let posY = this.position().y;
-
- // Snap to grid (like draw.io)
- const baseGridSize = 25;
-
- // Snap position to nearest grid intersection
- posX = Math.round(posX / baseGridSize) * baseGridSize;
- posY = Math.round(posY / baseGridSize) * baseGridSize;
-
- // Apply snapped position back to node
- this.position({
- x: posX,
- y: posY
- });
-
- n.position.x = posX;
- n.position.y = posY;
-
- MoveNodes(); // --- RESIZE UI LOGIC END ---
-
- TakeGraphPictureAndUpdate();
- });
-
- // Click on object
- cy.on('click', function (evt) {
-
- let evtTarget = evt.target;
-
- // Is this cy ?
- if (evtTarget === cy) {
- ClearConfigForm('');
- selecteed_node_id = 0;
- selected_edge_id = 0;
- return;
- }
-
- // Is this edge ?
- if (evtTarget.group() === 'edges'){
- selected_edge_id = evtTarget.data().id;
- ShowEdgeConfig(selected_edge_id);
- selecteed_node_id = 0;
- return;
- }
-
- // Maybe host ?
- var target_id = evt.target.id();
- let n = nodes.find(n => n.data.id === target_id);
-
- if (!n) {
- return;
- }
-
- selecteed_node_id = n.data.id;
- selected_edge_id = 0;
-
- if (n.config.type === 'host'){
- ShowHostConfig(n);
- } else if (n.config.type === 'l1_hub'){
- ShowHubConfig(n);
- } else if (n.config.type === 'l2_switch'){
- ShowSwitchConfig(n);
- } else if (n.config.type === 'router'){
- ShowRouterConfig(n);
- } else if (n.config.type === 'server'){
- ShowServerConfig(n);
- } else if (n.config.type === 'textbox'){
- ShowTextboxConfig(n);
- }
- });
-
- // Add edge to the edges[] and then save it to the server.
- cy.on('ehcomplete', (event, sourceNode, targetNode, addedEdge) => {
- AddEdge(sourceNode._private.data.id, targetNode._private.data.id);
- DrawGraph();
- PostNodesEdges();
- TakeGraphPictureAndUpdate();
-
- SetNetworkPlayerState(-1);
- });
-
- $(document).on('keyup', function(e){
-
- const evtTarget = e.target;
- if (evtTarget && evtTarget.form) {
- return;
- }
-
- if (e.keyCode == 46 && selecteed_node_id) {
- if (activeNodeId === selecteed_node_id) {
- hideResizeFrame();
- }
-
- // Save the network state.
- SaveNetworkObject();
-
- DeleteNode(selecteed_node_id);
- DeleteJob(selecteed_node_id);
-
- ClearConfigForm('');
- selecteed_node_id = 0;
- selected_edge_id = 0;
-
- PostNodesEdges(); // Update network on server
- cy.elements().remove();
- cy.add(nodes);
- cy.add(edges);
-
- TakeGraphPictureAndUpdate();
-
- // Reset network state
- SetNetworkPlayerState(-1);
- }
- if (e.keyCode == 46 && selected_edge_id) {
-
- // Save the network state.
- SaveNetworkObject();
-
- // If the source or target is a switch, delete the jobs.
- let ed = edges.find(ed => ed.data.id === selected_edge_id);
- if (ed){
- if (ed.data.source.startsWith("l2sw")){
- DeleteJob(ed.data.source)
- }
- if (ed.data.target.startsWith("l2sw")){
- DeleteJob(ed.data.target)
- }
- }
- DeleteEdge(selected_edge_id);
-
- ClearConfigForm('');
- selected_edge_id = 0;
-
- PostNodesEdges(); // Update network on server
- cy.elements().remove();
- cy.add(nodes);
- cy.add(edges);
-
- TakeGraphPictureAndUpdate();
-
- // Reset network state
- SetNetworkPlayerState(-1);
- }
-
- if (e.keyCode == 90 && e.ctrlKey){
-
- ClearConfigForm('');
- selecteed_node_id = 0;
- selected_edge_id = 0;
-
- RestoreNetworkObject();
-
- PostNodesEdges(); // Update network on server
- cy.elements().remove();
- cy.add(nodes);
- cy.add(edges);
-
- TakeGraphPictureAndUpdate();
-
- // Reset network state
- SetNetworkPlayerState(-1);
- }
-
- });
-
-
- cy.on('tap', '.textbox', (e) => {
- e.originalEvent.stopPropagation();
- initResizeFrame(e.target);
- });
-
- cy.on('tap', (e) => {
- if (e.target === cy) {
- hideResizeFrame();
- return;
- }
-
- if (e.target.isNode && e.target.isNode()) {
- let n = nodes.find(n => n.data.id === e.target.id());
- if (n && n.config && n.config.type !== 'textbox') {
- hideResizeFrame();
- }
- }
- });
-
- cy.on('zoom pan', updateResizeFrame);
-
- cy.on('dragstart', (e) => {
- if (e.target.id() !== activeNodeId) {
- hideResizeFrame();
- }
- });
-
- // Initialize grid
- initGrid(cy);
-}
-
-const DrawGraphStatic = function(nodes, edges, shared=0) {
-
- // Do we already have one?
- let cy = undefined;
-
- let network_scheme_id = "network_scheme";
-
- if (shared){
- network_scheme_id = "network_scheme_shared";
- }
-
- if (global_cy)
- {
- cy = global_cy;
- cy.elements().remove();
- } else {
- cy = cytoscape({
- container: document.getElementById(network_scheme_id),
- boxSelectionEnabled: true,
- autounselectify: false,
- style: prepareStylesheet(),
- elements: [],
- layout: 'preset',
- zoom: network_zoom,
- pan: { x: network_pan_x, y: network_pan_y },
- fit: true,
- });
-
- global_cy = cy;
- }
-
- // Turn off edges creation.
- if (global_eh){
- global_eh.disable();
- }
-
- cy.autounselectify(false);
- cy.add(nodes);
- cy.add(edges);
- MarkLinkDownEdges(cy);
- cy.nodes().ungrabify();
-
- // Initialize grid
- initGrid(cy);
-
- return;
-}
-
-const DrawSharedGraph = function(nodes, edges) {
-
- // Do we already have one?
- let cy = undefined;
-
- if (global_cy)
- {
- cy = global_cy;
- cy.elements().remove();
- } else {
- cy = cytoscape({
- container: document.getElementById("network_scheme_shared"),
- boxSelectionEnabled: true,
- autounselectify: true,
- style: prepareStylesheet(),
- elements: [],
- layout: 'preset',
- zoom: network_zoom,
- pan: { x: network_pan_x, y: network_pan_y },
- fit: true,
- });
-
- global_cy = cy;
- }
-
- cy.autounselectify(true);
-
- cy.minZoom(0.5);
- cy.maxZoom(2);
-
- cy.add(nodes);
- cy.add(edges);
- MarkLinkDownEdges(cy);
-
- // Click on object
- cy.on('click', function (evt) {
-
- let evtTarget = evt.target;
- if (evtTarget === cy) {
- ClearConfigForm('');
- selecteed_node_id = 0;
- selected_edge_id = 0;
- return;
- }
-
- // Is this edge ?
- if (evtTarget.group() === 'edges'){
- selected_edge_id = evtTarget.data().id;
- ShowEdgeConfig(selected_edge_id, 1);
- selecteed_node_id = 0;
- return;
- }
-
- var target_id = evt.target.id();
- let n = nodes.find(n => n.data.id === target_id);
-
- if (!n) {
- return;
- }
-
- selecteed_node_id = n.data.id;
- selected_edge_id = 0;
-
- if (n.config.type === 'host'){
- ShowHostConfig(n, 1);
- } else if (n.config.type === 'l1_hub'){
- ShowHubConfig(n, 1);
- } else if (n.config.type === 'l2_switch'){
- ShowSwitchConfig(n, 1);
- } else if (n.config.type === 'router'){
- ShowRouterConfig(n, 1);
- } else if (n.config.type === 'server'){
- ShowServerConfig(n, 1);
- }
- });
-
- // Initialize grid
- initGrid(cy);
-}
-
-const DrawIndexGraphStatic = function(nodes, edges, container_id, graph_network_zoom,
- graph_network_pan_x, graph_network_pan_y)
-{
-
- let index_cy = cytoscape({
- container: document.getElementById(container_id),
- boxSelectionEnabled: true,
- autounselectify: false,
- style: prepareStylesheet(),
- elements: [],
- layout: 'preset',
- zoom: graph_network_zoom,
- pan: { x: graph_network_pan_x, y: graph_network_pan_y },
- fit: true,
- });
-
- index_cy.autounselectify(false);
-
- index_cy.add(nodes);
- index_cy.add(edges);
- index_cy.panningEnabled(false);
-
- index_cy.nodes().ungrabify();
- return index_cy;
-}
-
-// Check whether simulation is over and we can run packets
-const CheckSimulation = function (simulation_id)
-{
- ajaxWithAuth({
- type: 'GET',
- url: ExternalUrlFor('/check_simulation?simulation_id=' + simulation_id + '&network_guid=' + network_guid),
- data: '',
- success: function(data, textStatus, xhr) {
- // If we got 210 (processing) wait 2 sec and call themself again
- if (xhr.status === 210)
- {
- setTimeout(CheckSimulation, 2000, simulation_id);
- }
-
- // Simulation is ended up and we can grab the packets
- if (xhr.status === 200)
- {
- packets = JSON.parse(data.packets);
- pcaps = data.pcaps;
-
- // Set filters
- packetsNotFiltered = null;
- SetPacketFilter();
-
- const answerButton = document.querySelector('button[name="answerQuestion"]');
- if (answerButton) {
- answerButton.disabled = false;
- }
- }
- },
- error: function(xhr) {
- console.log('Cannot check simulation id = ' + simulation_id);
- if (lastSimulationId == simulation_id){
- SetNetworkPlayerState(-1);
- }
- },
- contentType: "application/json",
- dataType: 'json'
- });
-}
-
-// Update edge configuration
-const UpdateEdgeConfiguration = (data) => {
- SetNetworkPlayerState(-1);
-
- return ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/edge/save_config'),
- data: data,
- complete: function() {
- DrawGraph();
- $('#config_edge_main_form_submit_button').html('Сохранить');
- },
- error: function(xhr) {
- console.log('Не удалось обновить конфигурацию ребра');
- console.log(xhr);
- },
- dataType: 'json'
- });
-};
-
-
-const InsertWaitingTime = function ()
-{
- // Get last emulation task time
- // and send request to get count of emulating networks before this time
- ajaxWithAuth({
- type: 'GET',
- url: ExternalUrlFor('/emulation_queue/time'),
- data: '',
- success: function(data) {
- // Run helper function with time param
- InsertWaitingTimeHelper(data.time)
- },
- error: function(err) {
- console.error("Failed to fetch queue time:", err);
- },
- contentType: "application/json",
- dataType: 'json'
- });
-}
-
-const InsertWaitingTimeHelper = function(time_filter) {
- // Insert field with queue size
- ajaxWithAuth({
- type: 'GET',
- url: ExternalUrlFor('/emulation_queue/size?time-filter=' + time_filter.toString()),
- data: '',
- success: function(data) {
- const queue_size = parseInt(data.size);
- if (!$('#NetworkPlayer button:first').prop('disabled')) {
- console.log($('#NetworkPlayer button:first').prop('disabled'))
- return;
- } else if (queue_size <= 1) {
- $('#NetworkPlayerLabel').text("Ожидание 10-15 сек.");
- } else {
- $('#NetworkPlayerLabel').text(`Место в очереди ${queue_size}`);
-
- // Update waiting time
- setTimeout(() => InsertWaitingTimeHelper(time_filter), 500);
- }
-
- },
- error: function(err) {
- console.error("Failed to fetch queue size:", err);
- },
- contentType: "application/json",
- dataType: 'json'
- });
-}
-
-// Update host configuration
-const UpdateHostConfiguration = function (data, host_id)
-{
- // Reset network player
- SetNetworkPlayerState(-1);
-
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/host/save_config'),
- data: data,
- success: function(data, textStatus, xhr) {
-
- if (xhr.status === 200)
- {
- // Exit edit mode on successful save
- if (editingJobId && editingDeviceType === 'host') {
- ExitEditMode('host');
- }
- if (!data.warning){
- // Update nodes
- nodes = data.nodes;
- // Update jobs
- jobs = data.jobs;
-
- // Update graph
- DrawGraph();
- }
-
- // Ok, let's try to update host config form
- let n = nodes.find(n => n.data.id === host_id);
-
- if (!n) {
- ClearConfigForm('Нет такого хоста');
- return;
- }
-
- if (n.config.type === 'host'){
- ShowHostConfig(n);
- } else {
- ClearConfigForm('Узел есть, но это не хост');
- return;
- }
-
- if (data.warning){
- HostWarningMsg(data.warning);
- }
-
- // Update job counter after successful configuration
- UpdateJobCounter('config_host_job_counter', host_id);
- }
- },
- error: function(xhr) {
- console.log('Не удалось обновить конфигурацию хоста');
- console.log(xhr);
-
- // Show error message to user
- let errorMsg = 'Ошибка при сохранении конфигурации';
- if (xhr.responseJSON && xhr.responseJSON.message) {
- errorMsg = xhr.responseJSON.message;
- }
- HostErrorMsg(errorMsg);
-
- // Exit edit mode on error to allow retry
- if (editingJobId && editingDeviceType === 'host') {
- ExitEditMode('host');
- }
- },
- dataType: 'json'
- });
-}
-
-// Delete job from host
-const DeleteJobFromHost = function (host_id, job_id, network_guid)
-{
- // Reset network player
- SetNetworkPlayerState(-1);
-
- let data = {
- id: job_id,
- guid: network_guid,
- };
-
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/host/delete_job'),
- data: data,
- encode: true,
- success: function(data, textStatus, xhr) {
-
- if (xhr.status === 200)
- {
- // Update jobs
- jobs = data.jobs;
-
- // Update graph
- DrawGraph();
-
- // Ok, let's try to update host config form
- let n = nodes.find(n => n.data.id === host_id);
-
- if (!n) {
- ClearConfigForm('Нет такого хоста');
- return;
- }
-
- if (n.config.type === 'host'){
- ShowHostConfig(n);
- } else {
- ClearConfigForm('Узел есть, но это не хост');
- }
-
- // Update job counter after deletion
- UpdateJobCounter('config_host_job_counter', host_id);
-
- }
- },
- error: function(xhr) {
- console.log('Не удалось удалить команду');
- console.log(xhr);
- },
- dataType: 'json'
- });
-}
-
-// Delete job from router
-const DeleteJobFromRouter = function (router_id, job_id, network_guid)
-{
- // Reset network player
- SetNetworkPlayerState(-1);
-
- let data = {
- id: job_id,
- guid: network_guid,
- };
-
- $.ajax({
- type: 'POST',
- url: '/host/delete_job',
- data: data,
- encode: true,
- success: function(data, textStatus, xhr) {
-
- if (xhr.status === 200)
- {
- // Update jobs
- jobs = data.jobs;
-
- // Update graph
- DrawGraph();
-
- // Ok, let's try to update host config form
- let n = nodes.find(n => n.data.id === router_id);
-
- if (!n) {
- ClearConfigForm('Нет такого хоста');
- return;
- }
-
- if (n.config.type === 'router'){
- ShowRouterConfig(n);
- } else {
- ClearConfigForm('Узел есть, но это не раутер');
- }
-
- // Update job counter after deletion
- UpdateJobCounter('config_router_job_counter', router_id);
- }
- },
- error: function(xhr) {
- console.log('Не удалось удалить команду');
- console.log(xhr);
- },
- dataType: 'json'
- });
-}
-
-const DeleteJobFromSwitch = function (switch_id, job_id, network_guid)
-{
- // Reset network player
- SetNetworkPlayerState(-1);
-
- let data = {
- id: job_id,
- guid: network_guid,
- };
-
- $.ajax({
- type: 'POST',
- url: '/host/delete_job',
- data: data,
- encode: true,
- success: function(data, textStatus, xhr) {
-
- if (xhr.status === 200)
- {
- // Update jobs
- jobs = data.jobs;
-
- // Update graph
- DrawGraph();
-
- // Ok, let's try to update host config form
- let n = nodes.find(n => n.data.id === switch_id);
-
- if (!n) {
- ClearConfigForm('Нет такого хоста');
- return;
- }
-
- if (n.config.type === 'l2_switch'){
- ShowSwitchConfig(n);
- } else {
- ClearConfigForm('Узел есть, но это не свитч');
- }
- UpdateJobCounter('config_switch_job_counter', switch_id);
- }
- },
- error: function(xhr) {
- console.log('Не удалось удалить команду');
- console.log(xhr);
- },
- dataType: 'json'
- });
-}
-
-// Delete job from server
-const DeleteJobFromServer = function (server_id, job_id, network_guid)
-{
- // Reset network player
- SetNetworkPlayerState(-1);
-
- let data = {
- id: job_id,
- guid: network_guid,
- };
-
- $.ajax({
- type: 'POST',
- url: '/host/delete_job',
- data: data,
- encode: true,
- success: function(data, textStatus, xhr) {
-
- if (xhr.status === 200)
- {
- // Update jobs
- jobs = data.jobs;
-
- // Update graph
- DrawGraph();
-
- // Ok, let's try to update host config form
- let n = nodes.find(n => n.data.id === server_id);
-
- if (!n) {
- ClearConfigForm('Нет такого хоста');
- return;
- }
-
- if (n.config.type === 'server'){
- ShowServerConfig(n);
- } else {
- ClearConfigForm('Узел есть, но это не сервер');
- }
-
- // Update job counter after deletion
- UpdateJobCounter('config_server_job_counter', server_id);
- }
- },
- error: function(xhr) {
- console.log('Не удалось удалить команду');
- console.log(xhr);
- },
- dataType: 'json'
- });
-}
-
-// Update router configuration
-const UpdateRouterConfiguration = function (data, router_id)
-{
- // Reset network player
- SetNetworkPlayerState(-1);
-
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/host/router_save_config'),
- data: data,
- success: function(data, textStatus, xhr) {
-
- if (xhr.status === 200)
- {
-
- // Exit edit mode on successful save
- if (editingJobId && editingDeviceType === 'router') {
- ExitEditMode('router');
- }
-
- // Update nodes
- if (data.nodes)
- {
- nodes = data.nodes;
- }
-
- // Update jobs
- if (data.jobs)
- {
- jobs = data.jobs;
- }
-
- // Update graph
- DrawGraph();
-
- // Ok, let's try to update router config form
- let n = nodes.find(n => n.data.id === router_id);
-
- if (!n) {
- ClearConfigForm('Нет такого раутера');
- return;
- }
-
- if (n.config.type === 'router'){
- ShowRouterConfig(n);
- } else {
- ClearConfigForm('Узел есть, но это не раутер');
- return;
- }
-
- if (data.warning)
- {
- HostWarningMsg(data.warning);
- }
-
- // Update job counter after successful configuration
- UpdateJobCounter('config_router_job_counter', router_id);
- }
-
- },
- error: function(xhr) {
- console.log('Не удалось обновить конфигурацию хоста');
- console.log(xhr);
-
- // Show error message to user
- let errorMsg = 'Ошибка при сохранении конфигурации роутера';
- if (xhr.responseJSON && xhr.responseJSON.message) {
- errorMsg = xhr.responseJSON.message;
- }
- HostErrorMsg(errorMsg);
-
- // Exit edit mode on error to allow retry
- if (editingJobId && editingDeviceType === 'router') {
- ExitEditMode('router');
- }
- },
- dataType: 'json'
- });
-}
-
-// Update server configuration
-const UpdateServerConfiguration = function (data, router_id)
-{
- // Reset network player
- SetNetworkPlayerState(-1);
-
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/host/server_save_config'),
- data: data,
- success: function(data, textStatus, xhr) {
-
- if (xhr.status === 200)
- {
-
- // Exit edit mode on successful save
- if (editingJobId && editingDeviceType === 'server') {
- ExitEditMode('server');
- }
-
- if (!data.warning){
-
- if (data.nodes){
- nodes = data.nodes;
- }
-
- if (data.jobs){
- jobs = data.jobs;
- }
-
- // Update graph
- DrawGraph();
- }
-
- // Ok, let's try to update router config form
- let n = nodes.find(n => n.data.id === router_id);
-
- if (!n) {
- ClearConfigForm('Нет такого сервера');
- return;
- }
-
- if (n.config.type === 'server'){
- ShowServerConfig(n);
- } else {
- ClearConfigForm('Узел есть, но это не сервер');
- return;
- }
-
- if (data.warning)
- {
- ServerWarningMsg(data.warning);
- }
-
- // Update job counter after successful configuration
- UpdateJobCounter('config_server_job_counter', router_id);
- }
-
- },
- error: function(xhr) {
- console.log('Не удалось обновить конфигурацию сервера');
- console.log(xhr);
-
- // Show error message to user
- let errorMsg = 'Ошибка при сохранении конфигурации сервера';
- if (xhr.responseJSON && xhr.responseJSON.message) {
- errorMsg = xhr.responseJSON.message;
- }
- HostErrorMsg(errorMsg);
-
- // Exit edit mode on error to allow retry
- if (editingJobId && editingDeviceType === 'server') {
- ExitEditMode('server');
- }
- },
- dataType: 'json'
- });
-}
-
-// Update hub configuration
-const UpdateHubConfiguration = function (data, hub_id)
-{
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/host/hub_save_config'),
- data: data,
- success: function(data, textStatus, xhr) {
-
- if (xhr.status === 200)
- {
- // Update nodes
- nodes = data.nodes;
-
- // We don't clear packets and RunButtonState.
- // Hub can change only names
-
- // Update graph
- DrawGraph();
-
- // Ok, let's try to update host config form
- let n = nodes.find(n => n.data.id === hub_id);
-
- if (!n) {
- ClearConfigForm('Нет такого узла');
- return;
- }
-
- if (n.config.type === 'l1_hub'){
- ShowHubConfig(n);
- } else {
- ClearConfigForm('Нет такого хаба');
- }
- }
- },
- error: function(xhr) {
- console.log('Cannot update host config');
- console.log(xhr);
- },
- dataType: 'json'
- });
-}
-
-const UpdateTextboxConfiguration = function (data, textbox_id) {
- SetNetworkPlayerState(-1);
-
- $.ajax({
- type: "POST",
- url: "/host/textbox_save_config",
- data: data,
- success: function (data, textStatus, xhr) {
- if (xhr.status === 200) {
-
- nodes = data.nodes;
- DrawGraph();
-
- let n = nodes.find((n) => n.data.id === textbox_id);
-
- if (!n) {
- ClearConfigForm("Нет такого текстового блока");
- return;
- }
-
- if (n.config.type === "textbox") {
- ShowTextboxConfig(n);
- } else {
- ClearConfigForm("Нет такого текстового блока");
- return;
- }
-
- }
- },
- error: function(xhr) {
- console.log("Cannot update textbox config");
- console.log(xhr);
- },
- dataType: 'json'
-
- });
-};
-
-// Update Switch configuration
-const UpdateSwitchConfiguration = function (data, switch_id)
-{
- // Reset network player
- SetNetworkPlayerState(-1);
-
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/host/switch_save_config'),
- data: data,
- success: function(data, textStatus, xhr) {
-
- if (xhr.status === 200)
- {
- if (editingJobId && editingDeviceType === 'switch') {
- ExitEditMode('switch');
- }
- if (!data.warning){
-
- // Update nodes
- nodes = data.nodes;
-
- // Update jobs
- jobs = data.jobs;
-
- // Update graph
- DrawGraph();
- }
-
- // We don't clear packets and RunButtonState.
- // Hub can change only names
-
- // Ok, let's try to update host config form
- let n = nodes.find(n => n.data.id === switch_id);
-
- if (!n) {
- ClearConfigForm('Нет такого узла');
- return;
- }
-
- if (n.config.type === 'l2_switch'){
- ShowSwitchConfig(n);
- } else {
- ClearConfigForm('Нет такого свитча');
- }
- if (data.warning){
- SwitchWarningMsg(data.warning)
- }
- UpdateJobCounter('config_switch_job_counter', switch_id);
- }
- },
- error: function(xhr) {
- console.log('Cannot update switch config');
- console.log(xhr);
- // Show error message to user
- let errorMsg = 'Ошибка при сохранении конфигурации свитча';
- if (xhr.responseJSON && xhr.responseJSON.message) {
- errorMsg = xhr.responseJSON.message;
- }
- HostErrorMsg(errorMsg);
-
- // Exit edit mode on error to allow retry
- if (editingJobId && editingDeviceType === 'switch') {
- ExitEditMode('switch');
- }
- },
- dataType: 'json'
- });
-}
-
-const RunSimulation = function (network_guid)
-{
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/run_simulation?guid=' + network_guid),
- data: '',
- success: function(data, textStatus, xhr) {
- if (xhr.status === 201)
- {
- lastSimulationId = data.simulation_id
- console.log("Simulation is running!");
- // Ok, run CheckSimulation
- if (data.simulation_id)
- {
- CheckSimulation(data.simulation_id);
- }
- }
- },
- error: function(err) {
- console.log('Cannot run simulation guid = ' + network_guid);
- SetNetworkPlayerState(-1);
- },
- contentType: "application/json",
- dataType: 'json'
- });
-}
-
-const FilterPackets = function () {
- const tcpRegex = /TCP \((ACK|SYN|FIN)/;
- packets = packets
- .map((step) =>
- step.filter(
- (pkt) =>
- !(
- (packetFilterState.hideARP &&
- pkt.data.label.startsWith("ARP")) ||
- (packetFilterState.hideSTP &&
- (pkt.data.label.startsWith("STP") ||
- pkt.data.label.startsWith("RSTP"))) ||
- (packetFilterState.hideSYN &&
- tcpRegex.test(pkt.data.label))
- )
- )
- )
- .filter((step) => step.length > 0);
-};
-
-const UpdateFilterStates = function (settings) {
- if (!settings) {
- return;
- }
-
- Object.assign(packetFilterState, settings);
- $("#ARPFilterCheckbox").prop("checked", packetFilterState.hideARP);
- $("#STPFilterCheckbox").prop("checked", packetFilterState.hideSTP);
- $("#SYNFilterCheckbox").prop("checked", packetFilterState.hideSYN);
-};
-
-const SaveAnimationFilters = function () {
- if (!window.isAuthenticated) {
- return;
- }
-
- const payload = {
- hideARP: Boolean(packetFilterState.hideARP),
- hideSTP: Boolean(packetFilterState.hideSTP),
- hideSYN: Boolean(packetFilterState.hideSYN),
- };
-
- $.ajax({
- type: "POST",
- url: "/user/animation_filters",
- data: JSON.stringify(payload),
- contentType: "application/json; charset=utf-8",
- dataType: "json",
- success: function (data) {
- if (!data) {
- return;
- }
-
- const saved = {
- hideARP: Boolean(data.hideARP),
- hideSTP: Boolean(data.hideSTP),
- hideSYN: Boolean(data.hideSYN),
- };
-
- UpdateFilterStates(saved);
- },
- error: function (xhr) {
- console.log("Cannot save animation filters");
- console.log(xhr);
- },
- });
-};
-
-const SetPacketFilter = function (shared = 0) {
- // If network player UI is absent (e.g., not on network page), skip.
- if (!document.getElementById("NetworkPlayer") || !document.getElementById("PacketSliderInput")) {
- return;
- }
-
- console.log("Packet filter call");
- // SetPacketFilter first call on emulated network
- if (packets && !packetsNotFiltered) {
- packetsNotFiltered = JSON.parse(JSON.stringify(packets)); // Array deep copy
- }
- // Numerous filter call, we grab our packets copy to filter it
- else if (packetsNotFiltered) {
- packets = JSON.parse(JSON.stringify(packetsNotFiltered));
- }
-
- packetFilterState.hideARP = $("#ARPFilterCheckbox").is(":checked");
- packetFilterState.hideSTP = $("#STPFilterCheckbox").is(":checked");
- packetFilterState.hideSYN = $("#SYNFilterCheckbox").is(":checked");
-
- if (packets) {
- FilterPackets();
- if (shared) {
- SetSharedNetworkPlayerState();
- } else {
- SetNetworkPlayerState(0);
- }
- }
-};
-
-// 2 states:
-// Do we need emulation
-// We have a packets and ready to play packets
-const SetNetworkPlayerState = function (simulation_id) {
-
- // Reset?
- if (simulation_id === -1) {
- packetsNotFiltered = null;
- packets = null;
- pcaps = [];
- SetNetworkPlayerState(0);
- return;
- }
-
- // If we have packets, then we're ready to run
- if (packets)
- {
- $('#NetworkPlayer').empty();
- $('#NetworkPlayer').append('');
- $('#NetworkPlayer').append('');
-
- // Init player
- PacketPlayer.getInstance().InitPlayer(packets);
-
- // Configure the slider
- if (!$('#PacketSliderInput')[0] || !$('#PacketSliderInput')[0].noUiSlider) {
- return;
- }
-
- $('#PacketSliderInput')[0].noUiSlider.updateOptions({
- start: [1],
- range: {
- 'min': 1,
- 'max': packets.length,
- },
- format: {
- to: function (val){return '' + val},
- from: function (val){return '' + val},
- },
- tooltips: false,
- });
-
- // Show Slider on
- $('#PacketSliderInput').show();
-
- const pkt_count = packets.reduce((currentCount, row) => currentCount + row.length, 0);
- $('#NetworkPlayerLabel').text(packets.length + ' ' + NumWord(packets.length, ['шаг', 'шага', 'шагов']) + ' / ' + pkt_count + ' ' + NumWord(pkt_count, ['пакет', 'пакета', 'пакетов']));
-
- $('#PacketSliderInput')[0].noUiSlider.on('slide', function (e) {
- if (!e) return;
- let x = Math.round(e[0]);
- PacketPlayer.getInstance().setAnimationTrafficStep(x-1);
- });
-
- $('#PacketSliderInput')[0].noUiSlider.on('update', function (e) {
- if (!e) return;
- let x = Math.round(e[0]);
- if (packets.length === 0){
- $('#NetworkPlayerLabel').text('0 пакетов');
- return;
- }
- $('#NetworkPlayerLabel').text('Шаг: ' + x + '/' + packets.length + ' (' + packets[x-1].length + ' ' + NumWord(packets[x-1].length, ['пакет', 'пакета', 'пакетов']) + ')');
- });
-
- // Set click handlers
- $('#NetworkPlayPauseButton').click(function() {
-
- // If btn-success then start to play
- if ($(this).hasClass("btn-success")){
- $(this).removeClass('btn-success');
- $(this).addClass('btn-warning');
-
- $(this).empty();
- $(this).append('');
-
- // If not in pause. Draw a new layout and go.
- if (!PacketPlayer.getInstance().getPlayerPause())
- {
- DrawGraphStatic(nodes, edges);
- }
-
- PacketPlayer.getInstance().setAnimationTrafficStepCallback(function() {
- $('#PacketSliderInput')[0].noUiSlider.set(PacketPlayer.getInstance().getAnimationTrafficStep());
- });
-
- PacketPlayer.getInstance().StartPlayer(global_cy);
- return;
- } else {
-
- $(this).removeClass('btn-warning');
- $(this).addClass('btn-success');
- $(this).empty();
- $(this).append('');
-
- PacketPlayer.getInstance().PausePlayer();
- return;
- }
- });
-
- $('#NetworkStopButton').click(function() {
-
- PacketPlayer.getInstance().resetAnimationTrafficStepCallback();
- PacketPlayer.getInstance().StopPlayer();
-
- // Reset slider.
- $('#PacketSliderInput')[0].noUiSlider.set(0);
-
- DrawGraph(nodes, edges);
-
- $('#NetworkPlayPauseButton').removeClass('btn-success');
- $('#NetworkPlayPauseButton').removeClass('btn-warning');
- $('#NetworkPlayPauseButton').empty();
- $('#NetworkPlayPauseButton').addClass('btn-success');
- $('#NetworkPlayPauseButton').append('');
- return;
- });
-
- return;
- }
-
- // No packets.
- // The network is simulating?
- if (simulation_id) {
- $('#NetworkPlayer').empty();
- $('#PacketSliderInput').hide();
- $('#NetworkPlayer').append('');
- InsertWaitingTime()
- CheckSimulation(simulation_id);
- return;
- }
-
- // No packets and no simulation.
- // Add emulation button.
- $('#NetworkPlayer').empty();
- $('#PacketSliderInput').hide();
- $('#NetworkPlayer').append('');
- $('#NetworkPlayerLabel').empty();
-
- $('#NetworkEmulateButton').click(function() {
-
- // Check for job. If no job - show modal and exit.
- if (!jobs.length)
- {
- $('#noJobsModal').modal('toggle');
- return;
- }
-
- if (nodes.length > 80)
- {
- $('#tooManyHostModal').modal('toggle');
- return;
- }
-
- if (typeof window.ym != 'undefined')
- {
- ym(92293993,'reachGoal','NetworkEmulate');
- }
-
- RunSimulation(network_guid);
-
- $('#NetworkPlayer').empty();
- $('#NetworkPlayer').append('');
- InsertWaitingTime();
- return;
- });
-
- return;
-
-}
-
-// 2 states:
-// No packets - disable button.
-// We have a packets and ready to play packets
-const SetSharedNetworkPlayerState = function()
-{
-
- // If we have packets, then we're ready to run
- if (packets)
- {
- $('#NetworkPlayer').empty();
- $('#NetworkPlayer').append('');
- $('#NetworkPlayer').append('');
-
- // Init player
- PacketPlayer.getInstance().InitPlayer(packets);
-
- // Configure the slider
- $('#PacketSliderInput')[0].noUiSlider.updateOptions({
- start: [1],
- range: {
- 'min': 1,
- 'max': packets.length,
- },
- format: {
- to: function (val){return '' + val},
- from: function (val){return '' + val},
- },
- tooltips: false,
- });
-
- // Show Slider on
- $('#PacketSliderInput').show();
-
- const pkt_count = packets.reduce((currentCount, row) => currentCount + row.length, 0);
- $('#NetworkPlayerLabel').text(packets.length + ' ' + NumWord(packets.length, ['шаг', 'шага', 'шагов']) + ' / ' + pkt_count + ' ' + NumWord(pkt_count, ['пакет', 'пакета', 'пакетов']));
-
- $('#PacketSliderInput')[0].noUiSlider.on('slide', function (e) {
- if (!e) return;
- let x = Math.round(e[0]);
- PacketPlayer.getInstance().setAnimationTrafficStep(x-1);
- });
-
- $('#PacketSliderInput')[0].noUiSlider.on('update', function (e) {
- if (!e) return;
- let x = Math.round(e[0]);
- if (packets.length === 0){
- $('#NetworkPlayerLabel').text('0 пакетов');
- return;
- }
- $('#NetworkPlayerLabel').text('Шаг: ' + x + '/' + packets.length + ' (' + packets[x-1].length + ' ' + NumWord(packets[x-1].length, ['пакет', 'пакета', 'пакетов']) + ')');
- });
-
- // Set click handlers
- $('#NetworkPlayPauseButton').click(function() {
-
- // If btn-success then start to play
- if ($(this).hasClass("btn-success")){
- $(this).removeClass('btn-success');
- $(this).addClass('btn-warning');
-
- $(this).empty();
- $(this).append('');
-
- // If not in pause. Draw a new layout and go.
- if (!PacketPlayer.getInstance().getPlayerPause())
- {
- DrawGraphStatic(nodes, edges);
- }
-
- PacketPlayer.getInstance().setAnimationTrafficStepCallback(function() {
- $('#PacketSliderInput')[0].noUiSlider.set(PacketPlayer.getInstance().getAnimationTrafficStep());
- });
-
- PacketPlayer.getInstance().StartPlayer(global_cy);
- } else {
- $(this).removeClass('btn-warning');
- $(this).addClass('btn-success');
- $(this).empty();
- $(this).append('');
-
- PacketPlayer.getInstance().PausePlayer();
- return;
- }
- });
-
- $('#NetworkStopButton').click(function() {
-
- PacketPlayer.getInstance().resetAnimationTrafficStepCallback();
- PacketPlayer.getInstance().StopPlayer();
-
- // Reset slider.
- $('#PacketSliderInput')[0].noUiSlider.set(0);
-
- DrawSharedGraph(nodes, edges);
-
- $('#NetworkPlayPauseButton').removeClass('btn-success');
- $('#NetworkPlayPauseButton').removeClass('btn-warning');
- $('#NetworkPlayPauseButton').empty();
- $('#NetworkPlayPauseButton').addClass('btn-success');
- $('#NetworkPlayPauseButton').append('');
- return;
- });
-
- return;
- }
-
- // No packets
- // Add info button
- $('#NetworkPlayer').empty();
- $('#PacketSliderInput').hide();
- $('#NetworkPlayerLabel').empty();
- $('#NetworkPlayer').append('');
- return;
-}
-
-// Take a picture and update it.
-const TakeGraphPictureAndUpdate = function()
-{
- if (!global_cy)
- {
- return;
- }
-
- let png_blob = global_cy.png({output: 'blob', maxWidth: 512, maxHeight: 512});
-
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/network/upload_network_picture?guid=' + network_guid),
- data: png_blob,
- processData: false,
- error: function(xhr) {
-
- if (xhr.status != 200){
- console.log('Cannot upload graph picture');
- }
-
- },
- dataType: 'image/png'
- });
-}
-
-// Calculate drop offsets
-const CalculateDropOffset = function(elem_x, elem_y)
-{
- const network_scheme = document.getElementById("network_scheme");
- let offset_left = 0;
- let offset_top = 0;
- let ret = {'x' : 0, 'y' : 0};
-
- console.log(elem_x + ", " + elem_y);
-
- if (network_scheme){
- ret.x += network_scheme.offsetLeft - 25;
- ret.y += network_scheme.offsetTop - 15;
- }
-
- if (global_cy)
- {
- ret.x = ret.x + global_cy.pan().x;
- ret.y = ret.y + global_cy.pan().y;
-
- ret.x = (elem_x - ret.x) / global_cy.zoom();
- ret.y = (elem_y - ret.y) / global_cy.zoom();
-
- // Apply snap-to-grid
- const baseGridSize = 25;
- ret.x = Math.round(ret.x / baseGridSize) * baseGridSize;
- ret.y = Math.round(ret.y / baseGridSize) * baseGridSize;
- }
-
- return ret;
-}
-
-const UpdateNetworkConfig = function()
-{
- if (!global_cy){
- return;
- }
-
- let data = {'network_title' : network_title, 'network_description' : network_description,
- 'zoom' : global_cy.zoom(),'pan_x' : global_cy.pan().x, 'pan_y' : global_cy.pan().y};
-
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/network/update_network_config?guid=' + network_guid),
- data: JSON.stringify(data),
- contentType: "application/json; charset=utf-8",
- success: function(data, textStatus, xhr) {
- },
- error: function(xhr) {
- console.log('Cannot update network config');
- console.log(xhr);
- },
- dataType: 'json'
- });
-
-}
-
-const CopyNetwork = function ()
-{
- ajaxWithAuth({
- type: 'POST',
- url: ExternalUrlFor('/network/copy_network?guid=' + network_guid),
- data: '',
- success: function(data, textStatus, xhr) {
- if (xhr.status === 200)
- {
- console.log("Copy network is made.");
- $('#ModalCopy').modal('show');
- $('.modal-option').click(function() {
- var selectedOption = $(this).attr('data-option');
- if (selectedOption === 'edit') {
- var newUrl = data.new_url;
- window.location.href = newUrl;
- console.log('Go to editing');
- } else if (selectedOption === 'continue') {
- console.log('Continue here');
- }
- $('#ModalCopy').modal('hide');
- });
- }
- },
- error: function(err) {
- console.log('Copy has not been made.');
- },
- contentType: "application/json",
- dataType: 'json'
- });
-}
-
-
-const NumWord = function (value, words){
- value = Math.abs(value) % 100;
- var num = value % 10;
- if(value > 10 && value < 20) return words[2];
- if(num > 1 && num < 5) return words[1];
- if(num == 1) return words[0];
- return words[2];
-}
-
-const SaveNetworkObject = function (){
- let n = JSON.parse(JSON.stringify(nodes));
- let e = JSON.parse(JSON.stringify(edges));
-
- NetworkCache.push({
- nodes: n,
- edges: e,
- });
-
- return 0;
-}
-
-const RestoreNetworkObject = function (){
- let x = NetworkCache.pop();
-
- if (!x){
- return;
- }
-
- nodes=x.nodes;
- edges=x.edges;
-
- return 0;
-}
-
-// ========== COMMAND EDITING UTILITIES ==========
-// Global variables to track editing state
-let editingJobId = null;
-let editingDeviceType = null;
-
-// Function to enter edit mode
-const EnterEditMode = function(deviceType, jobId, jobTypeId) {
- editingJobId = jobId;
- editingDeviceType = deviceType;
-
- // Change submit button text
- const submitButton = document.getElementById(`config_${deviceType}_main_form_submit_button`);
- if (submitButton) {
- submitButton.textContent = 'Сохранить изменения';
- }
-
- // Change label text from "Выполнить команду" to "Редактировать команду"
- const selectLabel = $(`label[for="config_${deviceType}_job_select_field"]`);
- if (selectLabel.length) {
- selectLabel.text('Редактировать команду');
- }
-
- // Hide the select dropdown and show command name
- const selectField = document.getElementById(`config_${deviceType}_job_select_field`);
- if (selectField) {
- selectField.style.display = 'none';
-
- // Remove old command display if exists
- const existingDisplay = document.getElementById(`config_${deviceType}_edit_command_display`);
- if (existingDisplay) {
- existingDisplay.remove();
- }
-
- // Get command name from the selected option in HTML
- const selectedOption = selectField.querySelector(`option[value="${jobTypeId}"]`);
- const commandName = selectedOption ? selectedOption.textContent : 'Команда';
-
- // Create and insert command name display
- const commandDisplay = document.createElement('input');
- commandDisplay.type = 'text';
- commandDisplay.id = `config_${deviceType}_edit_command_display`;
- commandDisplay.className = 'form-control form-control-sm';
- commandDisplay.value = commandName;
- commandDisplay.disabled = true;
- selectField.parentNode.insertBefore(commandDisplay, selectField.nextSibling);
- }
-
- // Highlight the editing command
- $(`#config_${deviceType}_job_list li`).removeClass('editing-command');
- const listItem = $(`#config_${deviceType}_job_delete_${jobId}`).closest('li');
- listItem.addClass('editing-command');
-
- // Highlight only the input fields area after it's inserted into DOM
- setTimeout(() => {
- const jobList = document.getElementById(`config_${deviceType}_job_list`);
- if (jobList) {
- const inputDiv = $(jobList).prev(`div[name="config_${deviceType}_select_input"]`);
- if (inputDiv.length) {
- inputDiv.addClass('editing-form-area');
- }
- }
-
- // Scroll to the "Редактировать команду" label (select field)
- // This helps when user clicks edit on a command at the bottom of the list
- const selectLabel = $(`label[for="config_${deviceType}_job_select_field"]`);
- if (selectLabel.length) {
- selectLabel[0].scrollIntoView({
- behavior: 'smooth',
- block: 'start',
- inline: 'nearest'
- });
- }
- }, 50);
-};
-
-// Function to exit edit mode
-const ExitEditMode = function(deviceType) {
- editingJobId = null;
- editingDeviceType = null;
-
- // Reset submit button text
- const submitButton = document.getElementById(`config_${deviceType}_main_form_submit_button`);
- if (submitButton) {
- submitButton.textContent = 'Сохранить';
- }
-
- // Reset label text back to "Выполнить команду"
- const selectLabel = $(`label[for="config_${deviceType}_job_select_field"]`);
- if (selectLabel.length) {
- selectLabel.text('Выполнить команду');
- }
-
- // Remove command text display
- const commandDisplay = document.getElementById(`config_${deviceType}_edit_command_display`);
- if (commandDisplay) {
- commandDisplay.remove();
- }
-
- // Show the select dropdown again
- const selectField = document.getElementById(`config_${deviceType}_job_select_field`);
- if (selectField) {
- selectField.style.display = 'block';
- selectField.value = '0';
- }
-
- // Remove highlight from command and input areas
- $(`#config_${deviceType}_job_list li`).removeClass('editing-command');
- $(`div[name="config_${deviceType}_select_input"]`).removeClass('editing-form-area');
-
- // Clear form inputs
- $('div[name="config_' + deviceType + '_select_input"]').remove();
-};
-
-// Function to delete old job and save new configuration
-const DeleteAndSaveJob = function(deviceType, updateFunction, formData, deviceId) {
- if (!editingJobId || editingDeviceType !== deviceType) {
- // Not in edit mode, just save
- updateFunction(formData, deviceId);
- return;
- }
-
- // In edit mode: pass editing_job_id to server
- // Server will validate first, then delete old and add new atomically
- formData += '&editing_job_id=' + encodeURIComponent(editingJobId);
-
- updateFunction(formData, deviceId);
-};
-
-// Grid drawing functions
-const initGrid = function(cy) {
- if (!cy) return;
-
- // Clean up previous listener
- if (typeof gridCanvasLayer !== 'undefined' && gridCanvasLayer && gridCanvasLayer.resizeAndDrawCanvas) {
- window.removeEventListener('resize', gridCanvasLayer.resizeAndDrawCanvas);
- }
-
- // Remove old grid canvas if exists
- const oldCanvas = document.getElementById('grid-canvas-static');
- if (oldCanvas) {
- oldCanvas.remove();
- }
-
- // Create canvas with absolute positioning to overlay on top of cytoscape container
- const canvas = document.createElement('canvas');
- canvas.id = 'grid-canvas-static';
- canvas.style.position = 'absolute';
- canvas.style.top = '0';
- canvas.style.left = '0';
- canvas.style.width = '100%';
- canvas.style.height = '100%';
- canvas.style.pointerEvents = 'none';
-
- const container = cy.container();
- container.insertBefore(canvas, container.firstChild);
-
- const ctx = canvas.getContext('2d');
-
- const resizeAndDrawCanvas = function() {
- const pixelRatio = window.devicePixelRatio || 1;
-
- // Use container dimensions instead of window dimensions to prevent distortion
- // when container is not full screen
- canvas.width = container.clientWidth * pixelRatio;
- canvas.height = container.clientHeight * pixelRatio;
-
- // Always redraw when resizing
- if (gridCanvasLayer) {
- drawGrid();
- }
- };
-
- gridCanvasLayer = {
- canvas: canvas,
- ctx: ctx,
- resizeAndDrawCanvas: resizeAndDrawCanvas
- };
-
- resizeAndDrawCanvas();
-
- // Add event listener for resize
- window.addEventListener('resize', resizeAndDrawCanvas);
-
- // Add cy resize listener to handle container resizing specifically
- if (cy) {
- cy.on('resize', resizeAndDrawCanvas);
- cy.on('viewport', function() {
- currentGridZoom = cy.zoom();
- drawGrid();
- });
- }
-
- // Initialize current zoom from cytoscape
- if (cy && cy.zoom) {
- currentGridZoom = cy.zoom();
- }
-
- // Draw grid
- drawGrid();
-};
-
-const drawGrid = function() {
- if (!gridCanvasLayer) {
- return;
- }
-
- const canvas = gridCanvasLayer.canvas;
- const ctx = gridCanvasLayer.ctx;
-
- if (!canvas || !ctx) {
- return;
- }
-
- // Scale grid with zoom: at max zoom (2.0) = 50px like before, at min zoom (0.5) = small cells
- const gridSize = 25 * currentGridZoom; // 25 * 2.0 = 50px (max zoom), 25 * 0.5 = 12.5px (min zoom)
- const pixelRatio = window.devicePixelRatio || 1;
-
- ctx.clearRect(0, 0, canvas.width, canvas.height);
-
- const screenWidth = canvas.width / pixelRatio;
- const screenHeight = canvas.height / pixelRatio;
-
- // Get pan offset to align grid with cytoscape coordinate system
- let panX = 0;
- let panY = 0;
- if (global_cy && global_cy.pan) {
- const pan = global_cy.pan();
- panX = pan.x;
- panY = pan.y;
- }
-
- ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
-
- // Draw grid lines across entire viewport
- ctx.strokeStyle = 'rgba(200, 200, 200, 0.4)';
- ctx.lineWidth = 1;
-
- ctx.beginPath();
-
- // Calculate grid origin with pan offset
- // Grid should be offset by pan to stay aligned with nodes
- const gridOriginX = panX % gridSize;
- const gridOriginY = panY % gridSize;
-
- // Vertical lines across entire viewport
- let verticalCount = 0;
- const startX = Math.floor(-gridOriginX / gridSize) * gridSize + gridOriginX;
- for (let x = startX; x <= screenWidth; x += gridSize) {
- ctx.moveTo(x, 0);
- ctx.lineTo(x, screenHeight);
- verticalCount++;
- }
-
- // Horizontal lines across entire viewport
- let horizontalCount = 0;
- const startY = Math.floor(-gridOriginY / gridSize) * gridSize + gridOriginY;
- for (let y = startY; y <= screenHeight; y += gridSize) {
- ctx.moveTo(0, y);
- ctx.lineTo(screenWidth, y);
- horizontalCount++;
- }
-
- ctx.stroke();
-};
-
-
-// Update grid when config panel opens/closes
-const updateGridForConfigPanel = function() {
- if (gridCanvasLayer && gridCanvasLayer.resizeAndDrawCanvas) {
- // Small delay to let DOM update
- setTimeout(function() {
- gridCanvasLayer.resizeAndDrawCanvas();
- }, 50);
- }
-}
diff --git a/front/src/templates/base.html b/front/src/templates/base.html
index ed785be5..919e1d4c 100644
--- a/front/src/templates/base.html
+++ b/front/src/templates/base.html
@@ -323,7 +323,8 @@ Наше компьюнити
-
+
+
-
diff --git a/front/src/templates/quiz/networkBase.html b/front/src/templates/quiz/networkBase.html
index 6e3c2c68..cfe6d07a 100644
--- a/front/src/templates/quiz/networkBase.html
+++ b/front/src/templates/quiz/networkBase.html
@@ -163,7 +163,8 @@ Что делать?
-
+
+
-
diff --git a/front/uv.lock b/front/uv.lock
new file mode 100644
index 00000000..9b50e3d7
--- /dev/null
+++ b/front/uv.lock
@@ -0,0 +1,1261 @@
+version = 1
+revision = 3
+requires-python = ">=3.11, <3.13"
+
+[[package]]
+name = "alembic"
+version = "1.16.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mako" },
+ { name = "sqlalchemy" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9a/ca/4dc52902cf3491892d464f5265a81e9dff094692c8a049a3ed6a05fe7ee8/alembic-1.16.5.tar.gz", hash = "sha256:a88bb7f6e513bd4301ecf4c7f2206fe93f9913f9b48dac3b78babde2d6fe765e", size = 1969868, upload-time = "2025-08-27T18:02:05.668Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/4a/4c61d4c84cfd9befb6fa08a702535b27b21fff08c946bc2f6139decbf7f7/alembic-1.16.5-py3-none-any.whl", hash = "sha256:e845dfe090c5ffa7b92593ae6687c5cb1a101e91fa53868497dbd79847f9dbe3", size = 247355, upload-time = "2025-08-27T18:02:07.37Z" },
+]
+
+[[package]]
+name = "amqp"
+version = "5.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "vine" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.13.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "soupsieve" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" },
+]
+
+[[package]]
+name = "billiard"
+version = "4.2.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" },
+]
+
+[[package]]
+name = "blinker"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
+]
+
+[[package]]
+name = "cachecontrol"
+version = "0.14.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "msgpack" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" },
+]
+
+[[package]]
+name = "cachetools"
+version = "5.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" },
+]
+
+[[package]]
+name = "celery"
+version = "5.5.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "billiard" },
+ { name = "click" },
+ { name = "click-didyoumean" },
+ { name = "click-plugins" },
+ { name = "click-repl" },
+ { name = "kombu" },
+ { name = "python-dateutil" },
+ { name = "vine" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.10.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" },
+ { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" },
+ { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" },
+ { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
+ { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
+ { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
+ { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
+]
+
+[[package]]
+name = "click-didyoumean"
+version = "0.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" },
+]
+
+[[package]]
+name = "click-plugins"
+version = "1.1.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" },
+]
+
+[[package]]
+name = "click-repl"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "prompt-toolkit" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "dpkt"
+version = "1.9.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/7d/52f17a794db52a66e46ebb0c7549bf2f035ed61d5a920ba4aaa127dd038e/dpkt-1.9.8.tar.gz", hash = "sha256:43f8686e455da5052835fd1eda2689d51de3670aac9799b1b00cfd203927ee45", size = 180073, upload-time = "2022-08-18T05:54:13.582Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/79/479e2194c9096b92aecdf33634ae948d2be306c6011673e98ee1917f32c2/dpkt-1.9.8-py3-none-any.whl", hash = "sha256:4da4d111d7bf67575b571f5c678c71bddd2d8a01a3d57d489faf0a92c748fbfd", size = 194973, upload-time = "2022-08-18T05:54:10.793Z" },
+]
+
+[[package]]
+name = "flask"
+version = "3.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "blinker" },
+ { name = "click" },
+ { name = "itsdangerous" },
+ { name = "jinja2" },
+ { name = "markupsafe" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
+]
+
+[[package]]
+name = "flask-admin"
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "jinja2" },
+ { name = "markupsafe" },
+ { name = "werkzeug" },
+ { name = "wtforms" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/5f/318ac7beac7e8511e0aa1dd1781dc112e44061c3dfcfeebfacdef2862508/flask_admin-2.0.1.tar.gz", hash = "sha256:ab83a435942c2a6af83e80f306b18786e459887564a18cb8fb45c31af8a4929b", size = 5528560, upload-time = "2025-11-02T13:00:24.307Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/fc/69007b74724738f8a39c5acebafef0e97b22260e0e5621b65e2883270e8a/flask_admin-2.0.1-py3-none-any.whl", hash = "sha256:3bebd383322d680f46793bc59325e991d99f5b5e0ddb6db05e759c824f372f1c", size = 6458579, upload-time = "2025-11-02T13:00:21.83Z" },
+]
+
+[[package]]
+name = "flask-cors"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" },
+]
+
+[[package]]
+name = "flask-jwt-extended"
+version = "4.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "pyjwt" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/16/96b101f18cba17ecce3225ab07bc4c8f23e6befd8552dbbed87482e7c7fb/flask_jwt_extended-4.7.1.tar.gz", hash = "sha256:8085d6757505b6f3291a2638c84d207e8f0ad0de662d1f46aa2f77e658a0c976", size = 34411, upload-time = "2024-11-20T23:44:41.044Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/67/34/9a91da47b1565811ab4aa5fb134632c8d1757960bfa7d457f486947c4d75/Flask_JWT_Extended-4.7.1-py2.py3-none-any.whl", hash = "sha256:52f35bf0985354d7fb7b876e2eb0e0b141aaff865a22ff6cc33d9a18aa987978", size = 22588, upload-time = "2024-11-20T23:44:39.435Z" },
+]
+
+[[package]]
+name = "flask-login"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834, upload-time = "2023-10-30T14:53:21.151Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303, upload-time = "2023-10-30T14:53:19.636Z" },
+]
+
+[[package]]
+name = "flask-migrate"
+version = "4.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "alembic" },
+ { name = "flask" },
+ { name = "flask-sqlalchemy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/47c7b3c93855ceffc2eabfa271782332942443321a07de193e4198f920cf/flask_migrate-4.1.0.tar.gz", hash = "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", size = 21965, upload-time = "2025-01-10T18:51:11.848Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/c4/3f329b23d769fe7628a5fc57ad36956f1fb7132cf8837be6da762b197327/Flask_Migrate-4.1.0-py3-none-any.whl", hash = "sha256:24d8051af161782e0743af1b04a152d007bad9772b2bca67b7ec1e8ceeb3910d", size = 21237, upload-time = "2025-01-10T18:51:09.527Z" },
+]
+
+[[package]]
+name = "flask-sqlalchemy"
+version = "3.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "sqlalchemy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312", size = 81899, upload-time = "2023-09-11T21:42:36.147Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125, upload-time = "2023-09-11T21:42:34.514Z" },
+]
+
+[[package]]
+name = "google"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beautifulsoup4" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/89/97/b49c69893cddea912c7a660a4b6102c6b02cd268f8c7162dd70b7c16f753/google-3.0.0.tar.gz", hash = "sha256:143530122ee5130509ad5e989f0512f7cb218b2d4eddbafbad40fd10e8d8ccbe", size = 44978, upload-time = "2020-07-11T14:50:45.678Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/35/17c9141c4ae21e9a29a43acdfd848e3e468a810517f862cad07977bf8fe9/google-3.0.0-py2.py3-none-any.whl", hash = "sha256:889cf695f84e4ae2c55fbc0cfdaf4c1e729417fa52ab1db0485202ba173e4935", size = 45258, upload-time = "2020-07-11T14:49:58.287Z" },
+]
+
+[[package]]
+name = "google-auth"
+version = "2.40.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cachetools" },
+ { name = "pyasn1-modules" },
+ { name = "rsa" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" },
+]
+
+[[package]]
+name = "google-auth-oauthlib"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "google-auth" },
+ { name = "requests-oauthlib" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/87/e10bf24f7bcffc1421b84d6f9c3377c30ec305d082cd737ddaa6d8f77f7c/google_auth_oauthlib-1.2.2.tar.gz", hash = "sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684", size = 20955, upload-time = "2025-04-22T16:40:29.172Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/84/40ee070be95771acd2f4418981edb834979424565c3eec3cd88b6aa09d24/google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2", size = 19072, upload-time = "2025-04-22T16:40:28.174Z" },
+]
+
+[[package]]
+name = "greenlet"
+version = "3.2.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" },
+ { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" },
+ { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" },
+ { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" },
+ { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" },
+ { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" },
+ { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" },
+ { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
+ { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" },
+ { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
+]
+
+[[package]]
+name = "importlib-resources"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "itsdangerous"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "kombu"
+version = "5.5.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "amqp" },
+ { name = "packaging" },
+ { name = "tzdata" },
+ { name = "vine" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" },
+]
+
+[[package]]
+name = "mako"
+version = "1.3.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
+ { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
+ { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
+]
+
+[[package]]
+name = "miminet-front"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "alembic" },
+ { name = "beautifulsoup4" },
+ { name = "cachecontrol" },
+ { name = "celery" },
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "click" },
+ { name = "colorama" },
+ { name = "dpkt" },
+ { name = "flask" },
+ { name = "flask-admin" },
+ { name = "flask-cors" },
+ { name = "flask-jwt-extended" },
+ { name = "flask-login" },
+ { name = "flask-migrate" },
+ { name = "flask-sqlalchemy" },
+ { name = "google" },
+ { name = "google-auth" },
+ { name = "google-auth-oauthlib" },
+ { name = "greenlet" },
+ { name = "h11" },
+ { name = "idna" },
+ { name = "importlib-metadata" },
+ { name = "importlib-resources" },
+ { name = "itsdangerous" },
+ { name = "jinja2" },
+ { name = "jsonschema" },
+ { name = "kombu" },
+ { name = "mako" },
+ { name = "markupsafe" },
+ { name = "oauthlib" },
+ { name = "pillow" },
+ { name = "psycopg2-binary" },
+ { name = "pyasn1" },
+ { name = "pyasn1-modules" },
+ { name = "pysocks" },
+ { name = "python-dateutil" },
+ { name = "python-dotenv" },
+ { name = "requests" },
+ { name = "requests-oauthlib" },
+ { name = "rsa" },
+ { name = "six" },
+ { name = "soupsieve" },
+ { name = "sqlalchemy" },
+ { name = "urllib3" },
+ { name = "uwsgi" },
+ { name = "vine" },
+ { name = "wcwidth" },
+ { name = "websocket-client" },
+ { name = "werkzeug" },
+ { name = "wsproto" },
+ { name = "wtforms" },
+ { name = "zipp" },
+]
+
+[package.optional-dependencies]
+test = [
+ { name = "pytest" },
+ { name = "pytest-mock" },
+ { name = "selenium" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "alembic", specifier = "==1.16.5" },
+ { name = "beautifulsoup4", specifier = "==4.13.5" },
+ { name = "cachecontrol", specifier = ">=0.14" },
+ { name = "celery", specifier = "==5.5.3" },
+ { name = "certifi", specifier = "==2025.10.5" },
+ { name = "charset-normalizer", specifier = "==3.4.3" },
+ { name = "click", specifier = "==8.1.8" },
+ { name = "colorama", specifier = "==0.4.6" },
+ { name = "dpkt", specifier = "==1.9.8" },
+ { name = "flask", specifier = "==3.1.2" },
+ { name = "flask-admin", specifier = "==2.0.1" },
+ { name = "flask-cors", specifier = "==6.0.2" },
+ { name = "flask-jwt-extended", specifier = "==4.7.1" },
+ { name = "flask-login", specifier = "==0.6.3" },
+ { name = "flask-migrate", specifier = "==4.1.0" },
+ { name = "flask-sqlalchemy", specifier = "==3.1.1" },
+ { name = "google", specifier = "==3.0.0" },
+ { name = "google-auth", specifier = "==2.40.3" },
+ { name = "google-auth-oauthlib", specifier = "==1.2.2" },
+ { name = "greenlet", specifier = "==3.2.4" },
+ { name = "h11", specifier = "==0.16.0" },
+ { name = "idna", specifier = "==3.10" },
+ { name = "importlib-metadata", specifier = "==8.7.0" },
+ { name = "importlib-resources", specifier = "==6.5.2" },
+ { name = "itsdangerous", specifier = "==2.2.0" },
+ { name = "jinja2", specifier = "==3.1.6" },
+ { name = "jsonschema", specifier = "==4.25.1" },
+ { name = "kombu", specifier = "==5.5.4" },
+ { name = "mako", specifier = "==1.3.10" },
+ { name = "markupsafe", specifier = "==3.0.2" },
+ { name = "oauthlib", specifier = "==3.3.1" },
+ { name = "pillow", specifier = "==11.3.0" },
+ { name = "psycopg2-binary", specifier = "==2.9.10" },
+ { name = "pyasn1", specifier = "==0.6.1" },
+ { name = "pyasn1-modules", specifier = "==0.4.2" },
+ { name = "pysocks", specifier = "==1.7.1" },
+ { name = "pytest", marker = "extra == 'test'", specifier = "==8.4.2" },
+ { name = "pytest-mock", marker = "extra == 'test'", specifier = "==3.15.1" },
+ { name = "python-dateutil", specifier = "==2.9.0.post0" },
+ { name = "python-dotenv", specifier = "==1.1.1" },
+ { name = "requests", specifier = "==2.32.5" },
+ { name = "requests-oauthlib", specifier = "==2.0.0" },
+ { name = "rsa", specifier = "==4.9.1" },
+ { name = "selenium", marker = "extra == 'test'" },
+ { name = "six", specifier = "==1.17.0" },
+ { name = "soupsieve", specifier = "==2.8" },
+ { name = "sqlalchemy", specifier = "==2.0.43" },
+ { name = "urllib3", specifier = "==2.5.0" },
+ { name = "uwsgi", specifier = ">=2.0" },
+ { name = "vine", specifier = "==5.1.0" },
+ { name = "wcwidth", specifier = "==0.2.13" },
+ { name = "websocket-client", specifier = "==1.8.0" },
+ { name = "werkzeug", specifier = "==3.1.3" },
+ { name = "wsproto", specifier = "==1.2.0" },
+ { name = "wtforms", specifier = "==3.2.1" },
+ { name = "zipp", specifier = "==3.23.0" },
+]
+provides-extras = ["test"]
+
+[[package]]
+name = "msgpack"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
+ { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
+ { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
+ { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
+]
+
+[[package]]
+name = "oauthlib"
+version = "3.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" },
+]
+
+[[package]]
+name = "outcome"
+version = "1.3.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "11.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" },
+ { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" },
+ { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" },
+ { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" },
+ { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" },
+ { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" },
+ { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" },
+ { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" },
+ { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" },
+ { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" },
+ { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.52"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
+]
+
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/8f/9feb01291d0d7a0a4c6a6bab24094135c2b59c6a81943752f632c75896d6/psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", size = 3043397, upload-time = "2024-10-16T11:19:40.033Z" },
+ { url = "https://files.pythonhosted.org/packages/15/30/346e4683532011561cd9c8dfeac6a8153dd96452fee0b12666058ab7893c/psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", size = 3274806, upload-time = "2024-10-16T11:19:43.5Z" },
+ { url = "https://files.pythonhosted.org/packages/66/6e/4efebe76f76aee7ec99166b6c023ff8abdc4e183f7b70913d7c047701b79/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", size = 2851370, upload-time = "2024-10-16T11:19:46.986Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/fd/ff83313f86b50f7ca089b161b8e0a22bb3c319974096093cd50680433fdb/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", size = 3080780, upload-time = "2024-10-16T11:19:50.242Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/c4/bfadd202dcda8333a7ccafdc51c541dbdfce7c2c7cda89fa2374455d795f/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", size = 3264583, upload-time = "2024-10-16T11:19:54.424Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/f1/09f45ac25e704ac954862581f9f9ae21303cc5ded3d0b775532b407f0e90/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", size = 3019831, upload-time = "2024-10-16T11:19:57.762Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/2e/9beaea078095cc558f215e38f647c7114987d9febfc25cb2beed7c3582a5/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", size = 2871822, upload-time = "2024-10-16T11:20:04.693Z" },
+ { url = "https://files.pythonhosted.org/packages/01/9e/ef93c5d93f3dc9fc92786ffab39e323b9aed066ba59fdc34cf85e2722271/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", size = 2820975, upload-time = "2024-10-16T11:20:11.401Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/f0/049e9631e3268fe4c5a387f6fc27e267ebe199acf1bc1bc9cbde4bd6916c/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", size = 2919320, upload-time = "2024-10-16T11:20:17.959Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/9a/bcb8773b88e45fb5a5ea8339e2104d82c863a3b8558fbb2aadfe66df86b3/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", size = 2957617, upload-time = "2024-10-16T11:20:24.711Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/6b/144336a9bf08a67d217b3af3246abb1d027095dab726f0687f01f43e8c03/psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", size = 1024618, upload-time = "2024-10-16T11:20:27.718Z" },
+ { url = "https://files.pythonhosted.org/packages/61/69/3b3d7bd583c6d3cbe5100802efa5beacaacc86e37b653fc708bf3d6853b8/psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", size = 1163816, upload-time = "2024-10-16T11:20:30.777Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771, upload-time = "2024-10-16T11:20:35.234Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336, upload-time = "2024-10-16T11:20:38.742Z" },
+ { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637, upload-time = "2024-10-16T11:20:42.145Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097, upload-time = "2024-10-16T11:20:46.185Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776, upload-time = "2024-10-16T11:20:50.879Z" },
+ { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968, upload-time = "2024-10-16T11:20:56.819Z" },
+ { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334, upload-time = "2024-10-16T11:21:02.411Z" },
+ { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722, upload-time = "2024-10-16T11:21:09.01Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132, upload-time = "2024-10-16T11:21:16.339Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312, upload-time = "2024-10-16T11:21:25.584Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191, upload-time = "2024-10-16T11:21:29.912Z" },
+ { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031, upload-time = "2024-10-16T11:21:34.211Z" },
+]
+
+[[package]]
+name = "pyasn1"
+version = "0.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
+]
+
+[[package]]
+name = "pyasn1-modules"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
+]
+
+[[package]]
+name = "pysocks"
+version = "1.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
+]
+
+[[package]]
+name = "pytest-mock"
+version = "3.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "requests-oauthlib"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "oauthlib" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
+[[package]]
+name = "rsa"
+version = "4.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
+]
+
+[[package]]
+name = "selenium"
+version = "4.39.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "trio" },
+ { name = "trio-websocket" },
+ { name = "typing-extensions" },
+ { name = "urllib3", extra = ["socks"] },
+ { name = "websocket-client" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/af/19/27c1bf9eb1f7025632d35a956b50746efb4b10aa87f961b263fa7081f4c5/selenium-4.39.0.tar.gz", hash = "sha256:12f3325f02d43b6c24030fc9602b34a3c6865abbb1db9406641d13d108aa1889", size = 928575, upload-time = "2025-12-06T23:12:34.896Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/d0/55a6b7c6f35aad4c8a54be0eb7a52c1ff29a59542fc3e655f0ecbb14456d/selenium-4.39.0-py3-none-any.whl", hash = "sha256:c85f65d5610642ca0f47dae9d5cc117cd9e831f74038bc09fe1af126288200f9", size = 9655249, upload-time = "2025-12-06T23:12:33.085Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "sortedcontainers"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
+]
+
+[[package]]
+name = "sqlalchemy"
+version = "2.0.43"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/77/fa7189fe44114658002566c6fe443d3ed0ec1fa782feb72af6ef7fbe98e7/sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", size = 2136472, upload-time = "2025-08-11T15:52:21.789Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ea/92ac27f2fbc2e6c1766bb807084ca455265707e041ba027c09c17d697867/sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", size = 2126535, upload-time = "2025-08-11T15:52:23.109Z" },
+ { url = "https://files.pythonhosted.org/packages/94/12/536ede80163e295dc57fff69724caf68f91bb40578b6ac6583a293534849/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", size = 3297521, upload-time = "2025-08-11T15:50:33.536Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b5/cacf432e6f1fc9d156eca0560ac61d4355d2181e751ba8c0cd9cb232c8c1/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca", size = 3297343, upload-time = "2025-08-11T15:57:51.186Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ba/d4c9b526f18457667de4c024ffbc3a0920c34237b9e9dd298e44c7c00ee5/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d", size = 3232113, upload-time = "2025-08-11T15:50:34.949Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/79/c0121b12b1b114e2c8a10ea297a8a6d5367bc59081b2be896815154b1163/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3", size = 3258240, upload-time = "2025-08-11T15:57:52.983Z" },
+ { url = "https://files.pythonhosted.org/packages/79/99/a2f9be96fb382f3ba027ad42f00dbe30fdb6ba28cda5f11412eee346bec5/sqlalchemy-2.0.43-cp311-cp311-win32.whl", hash = "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921", size = 2101248, upload-time = "2025-08-11T15:55:01.855Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/13/744a32ebe3b4a7a9c7ea4e57babae7aa22070d47acf330d8e5a1359607f1/sqlalchemy-2.0.43-cp311-cp311-win_amd64.whl", hash = "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8", size = 2126109, upload-time = "2025-08-11T15:55:04.092Z" },
+ { url = "https://files.pythonhosted.org/packages/61/db/20c78f1081446095450bdc6ee6cc10045fce67a8e003a5876b6eaafc5cc4/sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", size = 2134891, upload-time = "2025-08-11T15:51:13.019Z" },
+ { url = "https://files.pythonhosted.org/packages/45/0a/3d89034ae62b200b4396f0f95319f7d86e9945ee64d2343dcad857150fa2/sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", size = 2123061, upload-time = "2025-08-11T15:51:14.319Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/10/2711f7ff1805919221ad5bee205971254845c069ee2e7036847103ca1e4c/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", size = 3320384, upload-time = "2025-08-11T15:52:35.088Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0e/3d155e264d2ed2778484006ef04647bc63f55b3e2d12e6a4f787747b5900/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", size = 3329648, upload-time = "2025-08-11T15:56:34.153Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/81/635100fb19725c931622c673900da5efb1595c96ff5b441e07e3dd61f2be/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", size = 3258030, upload-time = "2025-08-11T15:52:36.933Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ed/a99302716d62b4965fded12520c1cbb189f99b17a6d8cf77611d21442e47/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", size = 3294469, upload-time = "2025-08-11T15:56:35.553Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/a2/3a11b06715149bf3310b55a98b5c1e84a42cfb949a7b800bc75cb4e33abc/sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", size = 2098906, upload-time = "2025-08-11T15:55:00.645Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/09/405c915a974814b90aa591280623adc6ad6b322f61fd5cff80aeaef216c9/sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", size = 2126260, upload-time = "2025-08-11T15:55:02.965Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" },
+]
+
+[[package]]
+name = "trio"
+version = "0.33.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
+ { name = "idna" },
+ { name = "outcome" },
+ { name = "sniffio" },
+ { name = "sortedcontainers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/b6/c744031c6f89b18b3f5f4f7338603ab381d740a7f45938c4607b2302481f/trio-0.33.0.tar.gz", hash = "sha256:a29b92b73f09d4b48ed249acd91073281a7f1063f09caba5dc70465b5c7aa970", size = 605109, upload-time = "2026-02-14T18:40:55.386Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/93/dab25dc87ac48da0fe0f6419e07d0bfd98799bed4e05e7b9e0f85a1a4b4b/trio-0.33.0-py3-none-any.whl", hash = "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b", size = 510294, upload-time = "2026-02-14T18:40:53.313Z" },
+]
+
+[[package]]
+name = "trio-websocket"
+version = "0.12.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "outcome" },
+ { name = "trio" },
+ { name = "wsproto" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2026.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[package.optional-dependencies]
+socks = [
+ { name = "pysocks" },
+]
+
+[[package]]
+name = "uwsgi"
+version = "2.0.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9f/49/2f57640e889ba509fd1fae10cccec1b58972a07c2724486efba94c5ea448/uwsgi-2.0.31.tar.gz", hash = "sha256:e8f8b350ccc106ff93a65247b9136f529c14bf96b936ac5b264c6ff9d0c76257", size = 822796, upload-time = "2025-10-11T19:17:28.794Z" }
+
+[[package]]
+name = "vine"
+version = "5.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
+]
+
+[[package]]
+name = "websocket-client"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
+]
+
+[[package]]
+name = "wsproto"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" },
+]
+
+[[package]]
+name = "wtforms"
+version = "3.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/01/e4/633d080897e769ed5712dcfad626e55dbd6cf45db0ff4d9884315c6a82da/wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682", size = 137801, upload-time = "2024-10-21T11:34:00.108Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454, upload-time = "2024-10-21T11:33:58.44Z" },
+]
+
+[[package]]
+name = "zipp"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
+]
diff --git a/front/web/package-lock.json b/front/web/package-lock.json
new file mode 100644
index 00000000..9b1f22ba
--- /dev/null
+++ b/front/web/package-lock.json
@@ -0,0 +1,1026 @@
+{
+ "name": "miminet-front-web",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "miminet-front-web",
+ "version": "0.1.0",
+ "devDependencies": {
+ "vite": "^5.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
+ "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
+ "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
+ "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
+ "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
+ "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
+ "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
+ "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
+ "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
+ "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
+ "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
+ "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
+ "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
+ "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
+ "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
+ "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
+ "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
+ "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
+ "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
+ "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
+ "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
+ "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
+ "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
+ "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
+ "@rollup/rollup-android-arm64": "4.60.4",
+ "@rollup/rollup-darwin-arm64": "4.60.4",
+ "@rollup/rollup-darwin-x64": "4.60.4",
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
+ "@rollup/rollup-freebsd-x64": "4.60.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
+ "@rollup/rollup-openbsd-x64": "4.60.4",
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/front/web/package.json b/front/web/package.json
new file mode 100644
index 00000000..513070f7
--- /dev/null
+++ b/front/web/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "miminet-front-web",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Vite build pipeline for miminet front-end JS.",
+ "scripts": {
+ "build": "vite build",
+ "dev": "vite build --watch"
+ },
+ "devDependencies": {
+ "vite": "^5.4.0"
+ }
+}
diff --git a/front/web/src/main.js b/front/web/src/main.js
new file mode 100644
index 00000000..b49b4c25
--- /dev/null
+++ b/front/web/src/main.js
@@ -0,0 +1 @@
+window.miminetEntryLoaded = true;
diff --git a/front/web/vite.config.js b/front/web/vite.config.js
new file mode 100644
index 00000000..28958d18
--- /dev/null
+++ b/front/web/vite.config.js
@@ -0,0 +1,56 @@
+import { defineConfig } from 'vite';
+import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'fs';
+import { dirname, resolve } from 'path';
+
+const STATIC_ROOT = resolve(__dirname, '../src/static');
+const DIST_ROOT = resolve(STATIC_ROOT, 'dist');
+
+const CLASSIC_BUNDLE = [
+ 'netfront/state.js',
+ 'netfront/show_config.js',
+ 'netfront/network_ops.js',
+ 'netfront/draw.js',
+ 'netfront/simulation.js',
+ 'netfront/update_config.js',
+ 'netfront/runtime.js',
+ 'config_forms/common.js',
+ 'config_forms/device.js',
+ 'config_forms/shared.js',
+ 'config_forms/helpers.js',
+ 'config_forms/jobs.js',
+ 'config_forms/edit_jobs.js',
+];
+
+const concatClassicScripts = () => ({
+ name: 'miminet-concat-classic-scripts',
+ apply: 'build',
+ closeBundle() {
+ const parts = [];
+ for (const rel of CLASSIC_BUNDLE) {
+ const abs = resolve(STATIC_ROOT, rel);
+ if (!existsSync(abs)) {
+ throw new Error(`Bundle source missing: ${abs}`);
+ }
+ parts.push(readFileSync(abs, 'utf8'));
+ }
+ const outFile = resolve(DIST_ROOT, 'miminet.classic.js');
+ mkdirSync(dirname(outFile), { recursive: true });
+ writeFileSync(outFile, parts.join('\n'));
+ },
+});
+
+export default defineConfig({
+ build: {
+ outDir: DIST_ROOT,
+ emptyOutDir: true,
+ rollupOptions: {
+ input: resolve(__dirname, 'src/main.js'),
+ output: {
+ entryFileNames: 'miminet.entry.js',
+ format: 'iife',
+ name: 'MiminetEntry',
+ },
+ },
+ },
+ plugins: [concatClassicScripts()],
+});