diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 8687890..16cc9c7 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -24,7 +24,10 @@
"Vue.volar"
]
}
- }
+ },
+
+ // Install pip requirements
+ "postCreateCommand": "pip install -r requirements.txt"
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
@@ -35,9 +38,6 @@
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
// "shutdownAction": "none",
- // Uncomment the next line to run commands after the container is created.
- // "postCreateCommand": "cat /etc/os-release",
-
// Configure tool-specific properties.
// "customizations": {},
}
diff --git a/.gitignore b/.gitignore
index e1bddf0..3732e54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,17 @@ nginx/ssl/keys/*
!**/.gitkeep
**/node_modules
**/junit.xml
+
+# Screenshots
+logs/screenshots/*
+!logs/screenshots/.gitkeep
+
+# Fish shell
+$HOME
+
+# JetBrains IDEs
+.idea
+
# Created by https://www.toptal.com/developers/gitignore/api/intellij,python,flask,macos
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij,python,flask,macos
diff --git a/Dockerfile b/Dockerfile
index b7bd102..9439b3d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -54,7 +54,11 @@ RUN cp /app/scripts/daemon/xvfb /etc/init.d/xvfb
# Install python packages
COPY requirements.txt /app/requirements.txt
RUN pip install --user -r /app/requirements.txt
+RUN apt-get install python3-tk python3-xlib gnome-screenshot -y
+# Initiate PyAutoGUI
+RUN touch /root/.Xauthority && \
+ xauth add ${HOST}:0 . $(xxd -l 16 -p /dev/urandom)
FROM base AS core
# Copy rest of source code
diff --git a/bci/analysis/plot_factory.py b/bci/analysis/plot_factory.py
index 84a0f04..7fa5173 100644
--- a/bci/analysis/plot_factory.py
+++ b/bci/analysis/plot_factory.py
@@ -5,14 +5,14 @@
class PlotFactory:
@staticmethod
- def get_plot_revision_data(params: PlotParameters, db: MongoDB) -> dict:
- revision_docs = db.get_documents_for_plotting(params)
+ def get_plot_revision_data(params: PlotParameters) -> dict:
+ revision_docs = MongoDB().get_documents_for_plotting(params)
revision_results = PlotFactory.__add_outcome_info(params, revision_docs)
return revision_results
@staticmethod
- def get_plot_version_data(params: PlotParameters, db: MongoDB) -> dict:
- version_docs = db.get_documents_for_plotting(params, releases=True)
+ def get_plot_version_data(params: PlotParameters) -> dict:
+ version_docs = MongoDB().get_documents_for_plotting(params, releases=True)
version_results = PlotFactory.__add_outcome_info(params, version_docs)
return version_results
diff --git a/bci/app.py b/bci/app.py
index 9e504cb..5f037be 100644
--- a/bci/app.py
+++ b/bci/app.py
@@ -3,12 +3,20 @@
from flask import Flask
from flask_sock import Sock
-from bci.main import Main as bci_api
+from bci.configuration import Global, Loggers
+from bci.main import Main
sock = Sock()
+
def create_app():
- bci_api.initialize()
+ Loggers.configure_loggers()
+
+ if not Global.check_required_env_parameters():
+ raise Exception('Not all required environment variables are available')
+
+ # Instantiate main object and add to global flask context
+ main = Main()
# Blueprint modules are only imported after loggers are configured
from bci.web.blueprints.api import api
@@ -16,6 +24,7 @@ def create_app():
app = Flask(__name__)
# We don't store anything sensitive in the session, so we can use a simple secret key
+ app.config['main'] = main
app.secret_key = 'secret_key'
app.register_blueprint(api)
@@ -23,8 +32,8 @@ def create_app():
sock.init_app(app)
# Configure signal handlers
- signal.signal(signal.SIGTERM, bci_api.sigint_handler)
- signal.signal(signal.SIGINT, bci_api.sigint_handler)
+ signal.signal(signal.SIGTERM, main.sigint_handler)
+ signal.signal(signal.SIGINT, main.sigint_handler)
return app
diff --git a/bci/browser/automation/terminal.py b/bci/browser/automation/terminal.py
index d1fe6f7..ddc08b8 100644
--- a/bci/browser/automation/terminal.py
+++ b/bci/browser/automation/terminal.py
@@ -7,22 +7,26 @@
class TerminalAutomation:
-
@staticmethod
- def run(url: str, args: list[str], seconds_per_visit: int):
- logger.debug("Starting browser process...")
+ def visit_url(url: str, args: list[str], seconds_per_visit: int):
args.append(url)
+ proc = TerminalAutomation.open_browser(args)
+ logger.debug(f'Visiting the page for {seconds_per_visit}s')
+ time.sleep(seconds_per_visit)
+ TerminalAutomation.terminate_browser(proc, args)
+
+ @staticmethod
+ def open_browser(args: list[str]) -> subprocess.Popen:
+ logger.debug('Starting browser process...')
logger.debug(f'Command string: \'{" ".join(args)}\'')
- with open('/tmp/browser.log', 'a') as file:
- proc = subprocess.Popen(
- args,
- stdout=file,
- stderr=file
- )
+ with open('/tmp/browser.log', 'a+') as file:
+ proc = subprocess.Popen(args, stdout=file, stderr=file)
+ return proc
- time.sleep(seconds_per_visit)
+ @staticmethod
+ def terminate_browser(proc: subprocess.Popen, args: list[str]) -> None:
+ logger.debug('Terminating browser process using SIGINT...')
- logger.debug(f'Terminating browser process after {seconds_per_visit}s using SIGINT...')
# Use SIGINT and SIGTERM to end process such that cookies remain saved.
proc.send_signal(signal.SIGINT)
proc.send_signal(signal.SIGTERM)
@@ -30,8 +34,8 @@ def run(url: str, args: list[str], seconds_per_visit: int):
try:
stdout, stderr = proc.communicate(timeout=5)
except subprocess.TimeoutExpired:
- logger.info("Browser process did not terminate after 5s. Killing process through pkill...")
+ logger.info('Browser process did not terminate after 5s. Killing process through pkill...')
subprocess.run(['pkill', '-2', args[0].split('/')[-1]])
proc.wait()
- logger.debug("Browser process terminated.")
+ logger.debug('Browser process terminated.')
diff --git a/bci/browser/configuration/browser.py b/bci/browser/configuration/browser.py
index 1178ece..6b0c86d 100644
--- a/bci/browser/configuration/browser.py
+++ b/bci/browser/configuration/browser.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
+import subprocess
from abc import abstractmethod
import bci.browser.binary.factory as binary_factory
@@ -15,9 +16,13 @@
class Browser:
+ process: subprocess.Popen | None
- def __init__(self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary) -> None:
+ def __init__(
+ self, browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, binary: Binary
+ ) -> None:
self.browser_config = browser_config
+ self.process = None
self.eval_config = eval_config
self.binary = binary
self.state = binary.state
@@ -34,10 +39,22 @@ def visit(self, url: str):
match self.eval_config.automation:
case 'terminal':
args = self._get_terminal_args()
- TerminalAutomation.run(url, args, self.eval_config.seconds_per_visit)
+ TerminalAutomation.visit_url(url, args, self.eval_config.seconds_per_visit)
case _:
raise AttributeError('Not implemented')
+ def open(self, url: str) -> None:
+ args = self._get_terminal_args()
+ args.append(url)
+ self.process = TerminalAutomation.open_browser(args)
+
+ def terminate(self):
+ if self.process is None:
+ return
+
+ TerminalAutomation.terminate_browser(self.process, self._get_terminal_args())
+ self.process = None
+
def pre_evaluation_setup(self):
self.__fetch_binary()
@@ -46,11 +63,16 @@ def post_evaluation_cleanup(self):
def pre_test_setup(self):
self.__prepare_execution_folder()
- self._prepare_profile_folder()
def post_test_cleanup(self):
self.__remove_execution_folder()
+
+ def pre_try_setup(self):
+ self._prepare_profile_folder()
+
+ def post_try_cleanup(self):
self.__remove_profile_folder()
+ self.__empty_downloads_folder()
def __fetch_binary(self):
self.binary.fetch_binary()
@@ -70,9 +92,15 @@ def __remove_execution_folder(self):
util.rmtree(self.__get_execution_folder_path())
def __remove_profile_folder(self):
+ if self._profile_path is None:
+ return
remove_profile_execution_folder(self._profile_path)
self._profile_path = None
+ def __empty_downloads_folder(self):
+ download_folder = '/root/Downloads'
+ util.remove_all_in_folder(download_folder)
+
def __get_execution_folder_path(self) -> str:
return os.path.join(EXECUTION_PARENT_FOLDER, str(self.state.name))
@@ -80,11 +108,17 @@ def _get_executable_file_path(self) -> str:
return os.path.join(self.__get_execution_folder_path(), self.binary.executable_name)
@abstractmethod
- def _get_terminal_args(self):
+ def _get_terminal_args(self) -> list[str]:
+ pass
+
+ @abstractmethod
+ def get_navigation_sleep_duration(self) -> int:
pass
@staticmethod
- def get_browser(browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State) -> Browser:
+ def get_browser(
+ browser_config: BrowserConfiguration, eval_config: EvaluationConfiguration, state: State
+ ) -> Browser:
from bci.browser.configuration.chromium import Chromium
from bci.browser.configuration.firefox import Firefox
diff --git a/bci/browser/configuration/chromium.py b/bci/browser/configuration/chromium.py
index 6108328..f2cd16c 100644
--- a/bci/browser/configuration/chromium.py
+++ b/bci/browser/configuration/chromium.py
@@ -32,6 +32,9 @@
class Chromium(Browser):
+ def get_navigation_sleep_duration(self) -> int:
+ return 1
+
def _get_terminal_args(self) -> list[str]:
assert self._profile_path is not None
diff --git a/bci/browser/configuration/firefox.py b/bci/browser/configuration/firefox.py
index 643ae57..239934a 100644
--- a/bci/browser/configuration/firefox.py
+++ b/bci/browser/configuration/firefox.py
@@ -2,10 +2,9 @@
from bci import cli
from bci.browser.configuration.browser import Browser
-from bci.browser.configuration.options import Default, BlockThirdPartyCookies, PrivateBrowsing, TrackingProtection
+from bci.browser.configuration.options import BlockThirdPartyCookies, Default, PrivateBrowsing, TrackingProtection
from bci.browser.configuration.profile import prepare_firefox_profile
-
SUPPORTED_OPTIONS = [
Default(),
BlockThirdPartyCookies(),
@@ -21,11 +20,15 @@
class Firefox(Browser):
+ def get_navigation_sleep_duration(self) -> int:
+ return 2
+
def _get_terminal_args(self) -> list[str]:
assert self._profile_path is not None
args = [self._get_executable_file_path()]
args.extend(['-profile', self._profile_path])
+ args.append('-setDefaultBrowser')
user_prefs = []
def add_user_pref(key: str, value: str | int | bool):
@@ -45,6 +48,7 @@ def add_user_pref(key: str, value: str | int | bool):
# add_user_pref('network.proxy.type', 1)
add_user_pref('app.update.enabled', False)
+ add_user_pref('browser.shell.checkDefaultBrowser', False)
if 'default' in self.browser_config.browser_setting:
pass
elif 'btpc' in self.browser_config.browser_setting:
@@ -92,7 +96,7 @@ def _prepare_profile_folder(self):
if 'tp' in self.browser_config.browser_setting:
self._profile_path = prepare_firefox_profile('tp-67')
else:
- self._profile_path = prepare_firefox_profile('default-67')
+ self._profile_path = prepare_firefox_profile()
# Make Firefox trust the bughog CA
diff --git a/bci/browser/configuration/profile.py b/bci/browser/configuration/profile.py
index 520b612..79e03d1 100644
--- a/bci/browser/configuration/profile.py
+++ b/bci/browser/configuration/profile.py
@@ -1,4 +1,5 @@
import os
+from typing import Optional
from bci import cli
@@ -6,7 +7,7 @@
PROFILE_EXECUTION_FOLDER = '/tmp/profiles'
-def prepare_chromium_profile(profile_name: str = None) -> str:
+def prepare_chromium_profile(profile_name: Optional[str] = None) -> str:
# Create new execution profile folder
profile_execution_path = os.path.join(PROFILE_EXECUTION_FOLDER, 'new_profile')
profile_execution_path = __create_folder(profile_execution_path)
@@ -14,21 +15,26 @@ def prepare_chromium_profile(profile_name: str = None) -> str:
# Copy profile from storage to execution folder if profile_name is given
if profile_name:
if not os.path.exists(os.path.join(PROFILE_STORAGE_FOLDER, 'chromium', profile_name)):
- raise Exception(f'Profile \'{profile_name}\' does not exist')
+ raise Exception(f"Profile '{profile_name}' does not exist")
profile_storage_path = os.path.join(PROFILE_STORAGE_FOLDER, profile_name, 'Default')
cli.execute(f'cp -r {profile_storage_path} {profile_execution_path}')
return profile_execution_path
-def prepare_firefox_profile(profile_name: str = None) -> str:
+def prepare_firefox_profile(profile_name: Optional[str] = None) -> str:
+ """"
+ Create a temporary profile folder, based on the provided name of the premade profile.
+
+ :param profile_name: Reference to the premade profile folder used for creating the temporary profile.
+ """
# Create new execution profile folder
profile_execution_path = os.path.join(PROFILE_EXECUTION_FOLDER, 'new_profile')
profile_execution_path = __create_folder(profile_execution_path)
# Copy profile from storage to execution folder if profile_name is given
- if profile_name is None:
+ if profile_name is not None:
if not os.path.exists(os.path.join(PROFILE_STORAGE_FOLDER, 'firefox', profile_name)):
- raise Exception(f'Profile \'{profile_name}\' does not exist')
+ raise Exception(f"Profile '{profile_name}' does not exist")
profile_storage_path = os.path.join(PROFILE_STORAGE_FOLDER, profile_name)
cli.execute(f'cp -r {profile_storage_path} {profile_execution_path}')
return profile_execution_path
diff --git a/bci/browser/interaction/__init__.py b/bci/browser/interaction/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/bci/browser/interaction/elements/five.png b/bci/browser/interaction/elements/five.png
new file mode 100644
index 0000000..92dfde6
Binary files /dev/null and b/bci/browser/interaction/elements/five.png differ
diff --git a/bci/browser/interaction/elements/four.png b/bci/browser/interaction/elements/four.png
new file mode 100644
index 0000000..055ece5
Binary files /dev/null and b/bci/browser/interaction/elements/four.png differ
diff --git a/bci/browser/interaction/elements/one.png b/bci/browser/interaction/elements/one.png
new file mode 100644
index 0000000..c87a2d0
Binary files /dev/null and b/bci/browser/interaction/elements/one.png differ
diff --git a/bci/browser/interaction/elements/six.png b/bci/browser/interaction/elements/six.png
new file mode 100644
index 0000000..4adc77b
Binary files /dev/null and b/bci/browser/interaction/elements/six.png differ
diff --git a/bci/browser/interaction/elements/three.png b/bci/browser/interaction/elements/three.png
new file mode 100644
index 0000000..4368269
Binary files /dev/null and b/bci/browser/interaction/elements/three.png differ
diff --git a/bci/browser/interaction/elements/two.png b/bci/browser/interaction/elements/two.png
new file mode 100644
index 0000000..7f3c24a
Binary files /dev/null and b/bci/browser/interaction/elements/two.png differ
diff --git a/bci/browser/interaction/interaction.py b/bci/browser/interaction/interaction.py
new file mode 100644
index 0000000..14fb8d1
--- /dev/null
+++ b/bci/browser/interaction/interaction.py
@@ -0,0 +1,57 @@
+import logging
+from inspect import signature
+
+from bci.browser.configuration.browser import Browser as BrowserConfig
+from bci.browser.interaction.simulation import Simulation
+from bci.evaluations.logic import TestParameters
+
+logger = logging.getLogger(__name__)
+
+
+class Interaction:
+ browser: BrowserConfig
+ script: list[str]
+ params: TestParameters
+
+ def __init__(self, browser: BrowserConfig, script: list[str], params: TestParameters) -> None:
+ self.browser = browser
+ self.script = script
+ self.params = params
+
+ def execute(self) -> None:
+ simulation = Simulation(self.browser, self.params)
+
+ if self._interpret(simulation):
+ simulation.sleep(str(self.browser.get_navigation_sleep_duration()))
+ simulation.navigate('https://a.test/report/?bughog_sanity_check=OK')
+
+ def _interpret(self, simulation: Simulation) -> bool:
+ for statement in self.script:
+ if statement.strip() == '' or statement[0] == '#':
+ continue
+
+ cmd, *args = statement.split()
+ method_name = cmd.lower()
+
+ if method_name not in Simulation.public_methods:
+ raise Exception(
+ f'Invalid command `{cmd}`. Expected one of {", ".join(map(lambda m: m.upper(), Simulation.public_methods))}.'
+ )
+
+ method = getattr(simulation, method_name)
+ method_params = list(signature(method).parameters.values())
+
+ # Allow different number of arguments only for variable argument number (*)
+ if len(method_params) != len(args) and (len(method_params) < 1 or str(method_params[0])[0] != '*'):
+ raise Exception(
+ f'Invalid number of arguments for command `{cmd}`. Expected {len(method_params)}, got {len(args)}.'
+ )
+
+ logger.debug(f'Executing interaction method `{method_name}` with the arguments {args}')
+
+ try:
+ method(*args)
+ except:
+ return False
+
+ return True
diff --git a/bci/browser/interaction/simulation.py b/bci/browser/interaction/simulation.py
new file mode 100644
index 0000000..d4ba899
--- /dev/null
+++ b/bci/browser/interaction/simulation.py
@@ -0,0 +1,85 @@
+import os
+from time import sleep
+
+import pyautogui as gui
+import Xlib.display
+from pyvirtualdisplay.display import Display
+
+from bci.browser.configuration.browser import Browser as BrowserConfig
+from bci.evaluations.logic import TestParameters
+
+
+class Simulation:
+ browser_config: BrowserConfig
+ params: TestParameters
+
+ public_methods: list[str] = [
+ 'navigate',
+ 'click_position',
+ 'click',
+ 'write',
+ 'press',
+ 'hold',
+ 'release',
+ 'hotkey',
+ 'sleep',
+ 'screenshot',
+ ]
+
+ def __init__(self, browser_config: BrowserConfig, params: TestParameters):
+ self.browser_config = browser_config
+ self.params = params
+ disp = Display(visible=True, size=(1920, 1080), backend='xvfb', use_xauth=True)
+ disp.start()
+ gui._pyautogui_x11._display = Xlib.display.Display(os.environ['DISPLAY'])
+
+ def __del__(self):
+ self.browser_config.terminate()
+
+ def parse_position(self, position: str, max_value: int) -> int:
+ # Screen percentage
+ if position[-1] == '%':
+ return round(max_value * (int(position[:-1]) / 100))
+
+ # Absolute value in pixels
+ return int(position)
+
+ # --- PUBLIC METHODS ---
+ def navigate(self, url: str):
+ self.browser_config.terminate()
+ self.browser_config.open(url)
+ self.sleep(str(self.browser_config.get_navigation_sleep_duration()))
+
+ def click_position(self, x: str, y: str):
+ max_x, max_y = gui.size()
+
+ gui.moveTo(self.parse_position(x, max_x), self.parse_position(y, max_y))
+ gui.click()
+
+ def click(self, el_id: str):
+ el_image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'elements/{el_id}.png')
+ x, y = gui.locateCenterOnScreen(el_image_path)
+ self.click_position(str(x), str(y))
+
+ def write(self, text: str):
+ gui.write(text, interval=0.1)
+
+ def press(self, key: str):
+ gui.press(key)
+
+ def hold(self, key: str):
+ gui.keyDown(key)
+
+ def release(self, key: str):
+ gui.keyUp(key)
+
+ def hotkey(self, *keys: str):
+ gui.hotkey(*keys)
+
+ def sleep(self, duration: str):
+ sleep(float(duration))
+
+ def screenshot(self, filename: str):
+ filename = f'{self.params.evaluation_configuration.project}-{self.params.mech_group}-{filename}-{type(self.browser_config).__name__}-{self.browser_config.version}.jpg'
+ filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../../logs/screenshots', filename)
+ gui.screenshot(filepath)
diff --git a/bci/configuration.py b/bci/configuration.py
index c8472c0..166e0e0 100644
--- a/bci/configuration.py
+++ b/bci/configuration.py
@@ -170,5 +170,14 @@ def handle_exception(exc_type, exc_value, exc_traceback):
bci_logger.debug('Loggers initialized')
@staticmethod
- def get_formatted_buffer_logs() -> list[str]:
- return [Loggers.formatter.format(record) for record in Loggers.memory_handler.buffer]
+ def get_logs() -> list[str]:
+ return list(
+ map(
+ lambda x: Loggers.format_to_user_log(x.__dict__),
+ Loggers.memory_handler.buffer,
+ )
+ )
+
+ @staticmethod
+ def format_to_user_log(log: dict) -> str:
+ return f'[{log["asctime"]}] [{log["levelname"]}] {log["name"]}: {log["msg"]}'
diff --git a/bci/database/mongo/mongodb.py b/bci/database/mongo/mongodb.py
index 6b87583..df8c567 100644
--- a/bci/database/mongo/mongodb.py
+++ b/bci/database/mongo/mongodb.py
@@ -18,7 +18,6 @@
StateResult,
TestParameters,
TestResult,
- WorkerParameters,
)
from bci.evaluations.outcome_checker import OutcomeChecker
from bci.version_control.states.state import State, StateCondition
@@ -178,7 +177,7 @@ def has_result(self, params: TestParameters) -> bool:
def get_evaluated_states(
self, params: EvaluationParameters, boundary_states: tuple[State, State], outcome_checker: OutcomeChecker
) -> list[State]:
- collection = self.get_collection(params.database_collection)
+ collection = self.get_collection(params.database_collection, create_if_not_found=True)
query = {
'browser_config': params.browser_configuration.browser_setting,
'mech_group': params.evaluation_range.mech_group,
@@ -308,7 +307,7 @@ def get_build_id_firefox(self, state: State):
return result['build_id']
def get_documents_for_plotting(self, params: PlotParameters, releases: bool = False):
- collection = self.get_collection(params.database_collection)
+ collection = self.get_collection(params.database_collection, create_if_not_found=True)
query = {
'mech_group': params.mech_group,
'browser_config': params.browser_config,
@@ -340,6 +339,11 @@ def get_documents_for_plotting(self, params: PlotParameters, releases: bool = Fa
)
return list(docs)
+ def remove_datapoint(self, params: TestParameters) -> None:
+ collection = self.get_collection(params.database_collection)
+ query = self.__to_query(params)
+ collection.delete_one(query)
+
def get_info(self) -> dict:
if self.client and self.client.address:
return {'type': 'mongo', 'host': self.client.address[0], 'connected': True}
diff --git a/bci/database/mongo/revision_cache.py b/bci/database/mongo/revision_cache.py
index 08cb5d4..6a06efd 100644
--- a/bci/database/mongo/revision_cache.py
+++ b/bci/database/mongo/revision_cache.py
@@ -44,7 +44,7 @@ def firefox_has_binary_for(revision_nb: Optional[int], revision_id: Optional[str
@staticmethod
def firefox_get_binary_info(revision_id: str) -> Optional[dict]:
collection = MongoDB().get_collection('firefox_binary_availability')
- return collection.find_one({'revision_id': revision_id}, {'files_url': 1, 'app_version': 1})
+ return collection.find_one({'node': revision_id}, {'files_url': 1, 'app_version': 1})
@staticmethod
def firefox_get_previous_and_next_revision_nb_with_binary(revision_nb: int) -> tuple[Optional[int], Optional[int]]:
diff --git a/bci/evaluations/collectors/base.py b/bci/evaluations/collectors/base.py
index 903ed5d..b85ee6f 100644
--- a/bci/evaluations/collectors/base.py
+++ b/bci/evaluations/collectors/base.py
@@ -3,23 +3,22 @@
class BaseCollector:
-
def __init__(self) -> None:
self.data = {}
@abstractmethod
- def start():
+ def start(self):
pass
@abstractmethod
- def stop():
+ def stop(self):
pass
@staticmethod
def _parse_bughog_variables(raw_log_lines: list[str], regex) -> list[tuple[str, str]]:
- '''
+ """
Parses the given `raw_log_lines` for matches against the given `regex`.
- '''
+ """
data = []
regex_match_lists = [re.findall(regex, line) for line in raw_log_lines if re.search(regex, line)]
# Flatten list
@@ -27,15 +26,5 @@ def _parse_bughog_variables(raw_log_lines: list[str], regex) -> list[tuple[str,
for match in regex_matches:
var = match[0]
val = match[1]
- BaseCollector._add_val_var_pair(var, val, data)
+ data.append({'var': var, 'val': val})
return data
-
- @staticmethod
- def _add_val_var_pair(var: str, val: str, data: list) -> list:
- for entry in data:
- if entry['var'] == var and entry['val'] == val:
- return data
- data.append({
- 'var': var,
- 'val': val
- })
diff --git a/bci/evaluations/collectors/requests.py b/bci/evaluations/collectors/requests.py
index d5b9efe..db99803 100644
--- a/bci/evaluations/collectors/requests.py
+++ b/bci/evaluations/collectors/requests.py
@@ -1,6 +1,7 @@
import http.server
import json
import logging
+import socket
import socketserver
from threading import Thread
@@ -12,32 +13,49 @@
class RequestHandler(http.server.BaseHTTPRequestHandler):
+ """
+ Handles requests sent to the collector.
+ """
def __init__(self, collector, request, client_address, server) -> None:
self.collector = collector
self.request_body = None
super().__init__(request, client_address, server)
- def log_message(self, *_):
+ def log_message(self, format: str, *args) -> None:
+ """
+ Handle and store the received body.
+ """
if not self.request_body:
logger.debug('Received request without body')
return
- logger.debug(f'Received request with body: {self.request_body}')
request_body = json.loads(self.request_body)
+ logger.debug(f'Received request information with {len(request_body.keys())} attributes.')
self.collector.data['requests'].append(request_body)
def do_POST(self):
- content_length = int(self.headers['Content-Length'])
- body = self.rfile.read(content_length)
- self.request_body = body.decode('utf-8')
- self.send_response(200)
- self.end_headers()
- self.wfile.write(b'Post request received')
+ """
+ This function is called upon receiving a POST request.
+ It sets `self.request_body`, which will be parsed later by `self.log_message`.
+ """
+ # We have to read the body before allowing it to be thrashed when connection clusure is confirmed.
+ if self.headers['Content-Length'] is not None:
+ content_length = int(self.headers['Content-Length'])
+ body = self.rfile.read(content_length)
+ self.request_body = body.decode('utf-8')
+ # Because of our hacky NGINX methodology, we have to allow premature socket closings.
+ try:
+ self.send_response(200)
+ self.send_header('Content-Type', 'text/plain; charset=utf-8')
+ self.end_headers()
+ self.wfile.write('Post request received!\n'.encode('utf-8'))
+ except socket.error:
+ logger.debug('Socket closed by NGINX (expected)')
-class RequestCollector(BaseCollector):
+class RequestCollector(BaseCollector):
def __init__(self):
super().__init__()
self.__httpd = None
@@ -48,17 +66,20 @@ def __init__(self):
def start(self):
logger.debug('Starting collector...')
socketserver.TCPServer.allow_reuse_address = True
- self.__httpd = socketserver.TCPServer(("", PORT), lambda *args, **kwargs: RequestHandler(self, *args, **kwargs))
+ self.__httpd = socketserver.TCPServer(('', PORT), lambda *args, **kwargs: RequestHandler(self, *args, **kwargs))
# self.__httpd.allow_reuse_address = True
self.__thread = Thread(target=self.__httpd.serve_forever)
self.__thread.start()
def stop(self):
data = []
- regex = r'bughog_(.+)=(.+)'
+ # Important: we only consider requests to the /report/ endpoint where the bughog parameter immediately follows.
+ # Otherwise conditional endpoints (e.g., /report/if/Referer/) cause false positives.
+ regex = r'/report/\?bughog_(.+)=(.+)'
if self.__httpd:
self.__httpd.shutdown()
- self.__thread.join()
+ if self.__thread:
+ self.__thread.join()
self.__httpd.server_close()
request_urls = [request['url'] for request in self.data['requests'] if 'url' in request]
data = self._parse_bughog_variables(request_urls, regex)
diff --git a/bci/evaluations/custom/custom_evaluation.py b/bci/evaluations/custom/custom_evaluation.py
index b359c03..96a441b 100644
--- a/bci/evaluations/custom/custom_evaluation.py
+++ b/bci/evaluations/custom/custom_evaluation.py
@@ -1,8 +1,9 @@
import logging
import os
-import textwrap
+from typing import Optional
from bci.browser.configuration.browser import Browser
+from bci.browser.interaction.interaction import Interaction
from bci.configuration import Global
from bci.evaluations.collectors.collector import Collector, Type
from bci.evaluations.evaluation_framework import EvaluationFramework
@@ -16,26 +17,36 @@ class CustomEvaluationFramework(EvaluationFramework):
def __init__(self):
super().__init__()
self.dir_tree = self.initialize_dir_tree()
- self.tests_per_project = self.initialize_tests_and_url_queues(self.dir_tree)
+ self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree)
@staticmethod
def initialize_dir_tree() -> dict:
+ """
+ Initializes directory tree of experiments.
+ """
path = Global.custom_page_folder
+ dir_tree = {}
- def path_to_dict(path):
- if os.path.isdir(path):
- return {
- sub_folder: path_to_dict(os.path.join(path, sub_folder))
- for sub_folder in os.listdir(path)
- if sub_folder != 'url_queue.txt'
- }
+ def set_nested_value(d: dict, keys: list[str], value: dict):
+ nested_dict = d
+ for key in keys[:-1]:
+ nested_dict = nested_dict[key]
+ nested_dict[keys[-1]] = value
+
+ for root, dirs, files in os.walk(path):
+ # Remove base path from root
+ root = root[len(path):]
+ keys = root.split('/')[1:]
+ subdir_tree = {dir: {} for dir in dirs} | {file: None for file in files}
+ if root:
+ set_nested_value(dir_tree, keys, subdir_tree)
else:
- return os.path.basename(path)
+ dir_tree = subdir_tree
- return path_to_dict(path)
+ return dir_tree
@staticmethod
- def initialize_tests_and_url_queues(dir_tree: dict) -> dict:
+ def initialize_tests_and_interactions(dir_tree: dict) -> dict:
experiments_per_project = {}
page_folder_path = Global.custom_page_folder
for project, experiments in dir_tree.items():
@@ -43,15 +54,29 @@ def initialize_tests_and_url_queues(dir_tree: dict) -> dict:
project_path = os.path.join(page_folder_path, project)
experiments_per_project[project] = {}
for experiment in experiments:
- url_queue = CustomEvaluationFramework.__get_url_queue(project, project_path, experiment)
- experiments_per_project[project][experiment] = {
- 'url_queue': url_queue,
- 'runnable': CustomEvaluationFramework.is_runnable_experiment(project, experiment, dir_tree),
- }
+ data = {}
+
+ if interaction_script := CustomEvaluationFramework.__get_interaction_script(project_path, experiment):
+ data['script'] = interaction_script
+ elif url_queue := CustomEvaluationFramework.__get_url_queue(project, project_path, experiment):
+ data['url_queue'] = url_queue
+
+ data['runnable'] = CustomEvaluationFramework.is_runnable_experiment(project, experiment, dir_tree, data)
+
+ experiments_per_project[project][experiment] = data
return experiments_per_project
@staticmethod
- def __get_url_queue(project: str, project_path: str, experiment: str) -> list[str]:
+ def __get_interaction_script(project_path: str, experiment: str) -> list[str] | None:
+ interaction_file_path = os.path.join(project_path, experiment, 'script.cmd')
+ if os.path.isfile(interaction_file_path):
+ # If an interaction script is specified, it is parsed and used
+ with open(interaction_file_path) as file:
+ return file.readlines()
+ return None
+
+ @staticmethod
+ def __get_url_queue(project: str, project_path: str, experiment: str) -> Optional[list[str]]:
url_queue_file_path = os.path.join(project_path, experiment, 'url_queue.txt')
if os.path.isfile(url_queue_file_path):
# If an URL queue is specified, it is parsed and used
@@ -67,14 +92,21 @@ def __get_url_queue(project: str, project_path: str, experiment: str) -> list[st
f'https://{domain}/{project}/{experiment}/main',
'https://a.test/report/?bughog_sanity_check=OK',
]
- raise AttributeError(f"Could not infer url queue for experiment '{experiment}' in project '{project}'")
+ return None
@staticmethod
- def is_runnable_experiment(project: str, poc: str, dir_tree: dict) -> bool:
+ def is_runnable_experiment(project: str, poc: str, dir_tree: dict[str,dict], data: dict[str,str]) -> bool:
+ # Always runnable if there is either an interaction script or url_queue present
+ if 'script' in data or 'url_queue' in data:
+ return True
+
+ # Should have exactly one main folder otherwise
domains = dir_tree[project][poc]
- if not (poc_main_path := [paths for domain, paths in domains.items() if 'main' in paths]):
+ main_paths = [paths for paths in domains.values() if paths is not None and 'main' in paths.keys()]
+ if len(main_paths) != 1:
return False
- if 'index.html' not in poc_main_path[0]['main'].keys():
+ # Main should have index.html
+ if 'index.html' not in main_paths[0]['main'].keys():
return False
return True
@@ -88,17 +120,25 @@ def perform_specific_evaluation(self, browser: Browser, params: TestParameters)
is_dirty = False
try:
- url_queue = self.tests_per_project[params.evaluation_configuration.project][params.mech_group]['url_queue']
- for url in url_queue:
- tries = 0
- while tries < 3:
- tries += 1
- browser.visit(url)
+ experiment = self.tests_per_project[params.evaluation_configuration.project][params.mech_group]
+
+ max_tries = 3
+ for _ in range(max_tries):
+ browser.pre_try_setup()
+ if 'script' in experiment:
+ interaction = Interaction(browser, experiment['script'], params)
+ interaction.execute()
+ else:
+ url_queue = experiment['url_queue']
+ for url in url_queue:
+ browser.visit(url)
+ browser.post_try_cleanup()
except Exception as e:
logger.error(f'Error during test: {e}', exc_info=True)
is_dirty = True
finally:
collector.stop()
+
results = collector.collect_results()
if not is_dirty:
# New way to perform sanity check
@@ -124,18 +164,39 @@ def get_mech_groups(self, project: str) -> list[tuple[str, bool]]:
def get_projects(self) -> list[str]:
return sorted(list(self.tests_per_project.keys()))
+ def create_empty_project(self, project_name: str) -> bool:
+ if project_name is None or project_name == '':
+ logger.error('Given project name is invalid')
+ return False
+
+ if project_name in self.dir_tree:
+ logger.error(f"Given project name '{project_name}' already exists")
+ return False
+
+ new_project_path = os.path.join(Global.custom_page_folder, project_name)
+ os.mkdir(new_project_path)
+ self.sync_with_folders()
+ return True
+
def get_poc_structure(self, project: str, poc: str) -> dict:
return self.dir_tree[project][poc]
+ def _get_poc_file_path(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str:
+ # Top-level config file
+ if domain == 'Config' and path == '_':
+ return os.path.join(Global.custom_page_folder, project, poc, file_name)
+
+ return os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name)
+
def get_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str) -> str:
- file_path = os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name)
+ file_path = self._get_poc_file_path(project, poc, domain, path, file_name)
if os.path.isfile(file_path):
with open(file_path) as file:
return file.read()
raise AttributeError(f"Could not find PoC file at expected path '{file_path}'")
def update_poc_file(self, project: str, poc: str, domain: str, path: str, file_name: str, content: str) -> bool:
- file_path = os.path.join(Global.custom_page_folder, project, poc, domain, path, file_name)
+ file_path = self._get_poc_file_path(project, poc, domain, path, file_name)
if os.path.isfile(file_path):
if content == '':
logger.warning('Attempt to save empty file ignored')
@@ -158,36 +219,63 @@ def add_page(self, project: str, poc: str, domain: str, path: str, file_type: st
domain_path = os.path.join(Global.custom_page_folder, project, poc, domain)
if not os.path.exists(domain_path):
os.makedirs(domain_path)
- page_path = os.path.join(domain_path, path)
- if not os.path.exists(page_path):
- os.makedirs(page_path)
- new_file_name = f'index.{file_type}'
- file_path = os.path.join(page_path, new_file_name)
+
+ if file_type == 'py':
+ file_name = path if path.endswith('.py') else path + '.py'
+ file_path = os.path.join(domain_path, file_name)
+ else:
+ page_path = os.path.join(domain_path, path)
+ if not os.path.exists(page_path):
+ os.makedirs(page_path)
+ new_file_name = f'index.{file_type}'
+ file_path = os.path.join(page_path, new_file_name)
+
if os.path.exists(file_path):
return False
with open(file_path, 'w') as file:
- file.write('')
- headers_file_path = os.path.join(page_path, 'headers.json')
- if not os.path.exists(headers_file_path):
- with open(headers_file_path, 'w') as file:
- file.write(
- textwrap.dedent(
- """\
- [
- {
- "key": "Header-Name",
- "value": "Header-Value"
- }
- ]
- """
- )
- )
+ file.write(self.get_default_file_content(file_type))
+
+ if self.include_file_headers(file_type):
+ headers_file_path = os.path.join(page_path, 'headers.json')
+ if not os.path.exists(headers_file_path):
+ with open(headers_file_path, 'w') as file:
+ file.write(self.get_default_file_content('headers.json'))
self.sync_with_folders()
# Notify clients of change (an experiment might now be runnable)
Clients.push_experiments_to_all()
return True
+ def add_config(self, project: str, poc: str, type: str) -> bool:
+ content = self.get_default_file_content(type)
+
+ if (content == ''):
+ return False
+
+ file_path = os.path.join(Global.custom_page_folder, project, poc, type)
+ with open(file_path, 'w') as file:
+ file.write(content)
+
+ self.sync_with_folders()
+ # Notify clients of change (an experiment might now be runnable)
+ Clients.push_experiments_to_all()
+
+ return True
+
+ @staticmethod
+ def get_default_file_content(file_type: str) -> str:
+ path = os.path.join(os.path.dirname(os.path.realpath(__file__)), f'default_files/{file_type}')
+
+ if not os.path.exists(path):
+ return ''
+
+ with open(path, 'r') as file:
+ return file.read()
+
+ @staticmethod
+ def include_file_headers(file_type: str) -> bool:
+ return file_type != 'py'
+
def sync_with_folders(self):
self.dir_tree = self.initialize_dir_tree()
- self.tests_per_project = self.initialize_tests_and_url_queues(self.dir_tree)
+ self.tests_per_project = self.initialize_tests_and_interactions(self.dir_tree)
logger.info('Framework is synced with folders')
diff --git a/bci/evaluations/custom/default_files/headers.json b/bci/evaluations/custom/default_files/headers.json
new file mode 100644
index 0000000..81975b3
--- /dev/null
+++ b/bci/evaluations/custom/default_files/headers.json
@@ -0,0 +1,6 @@
+[
+ {
+ "key": "Header-Name",
+ "value": "Header-Value"
+ }
+]
\ No newline at end of file
diff --git a/bci/evaluations/custom/default_files/html b/bci/evaluations/custom/default_files/html
new file mode 100644
index 0000000..2af8232
--- /dev/null
+++ b/bci/evaluations/custom/default_files/html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bci/evaluations/custom/default_files/js b/bci/evaluations/custom/default_files/js
new file mode 100644
index 0000000..664997c
--- /dev/null
+++ b/bci/evaluations/custom/default_files/js
@@ -0,0 +1 @@
+// TODO - implement your PoC
\ No newline at end of file
diff --git a/bci/evaluations/custom/default_files/py b/bci/evaluations/custom/default_files/py
new file mode 100644
index 0000000..91e823b
--- /dev/null
+++ b/bci/evaluations/custom/default_files/py
@@ -0,0 +1,15 @@
+from flask import Request
+
+# Make sure that your page directory starts with 'py-'
+
+def main(req: Request):
+ # TODO - implement your functionality and return a Flask response
+
+ return {
+ "agent": req.headers.get("User-Agent"),
+ "cookies": req.cookies,
+ "host": req.host,
+ "path": req.path,
+ "scheme": req.scheme,
+ "url": req.url
+ }
\ No newline at end of file
diff --git a/bci/evaluations/custom/default_files/script.cmd b/bci/evaluations/custom/default_files/script.cmd
new file mode 100644
index 0000000..749e7a2
--- /dev/null
+++ b/bci/evaluations/custom/default_files/script.cmd
@@ -0,0 +1,12 @@
+# TODO - add your interaction script using the commands
+
+# NAVIGATE url
+# CLICK_POSITION x y where x and y are absolute numbers or screen percentages
+# CLICK element_id where element_id is one of one, two, three, four, five, six
+# WRITE text
+# PRESS key
+# HOLD key
+# RELEASE key
+# HOTKEY key1 key2 ...
+# SLEEP seconds where seconds is a float or an int
+# SCREENSHOT file_name
diff --git a/bci/evaluations/custom/default_files/url_queue.txt b/bci/evaluations/custom/default_files/url_queue.txt
new file mode 100644
index 0000000..8918d8d
--- /dev/null
+++ b/bci/evaluations/custom/default_files/url_queue.txt
@@ -0,0 +1,2 @@
+TODO - add your URLs to visit
+https://a.test/report/?bughog_sanity_check=OK
\ No newline at end of file
diff --git a/bci/evaluations/logic.py b/bci/evaluations/logic.py
index ad6f0e8..3097c59 100644
--- a/bci/evaluations/logic.py
+++ b/bci/evaluations/logic.py
@@ -190,6 +190,16 @@ class TestParameters:
def create_test_result_with(self, browser_version: str, binary_origin: str, data: dict, dirty: bool) -> TestResult:
return TestResult(self, browser_version, binary_origin, data, dirty)
+ @staticmethod
+ def from_dict(data) -> Optional[TestParameters]:
+ if data is None:
+ return None
+ browser_configuration = BrowserConfiguration.from_dict(data)
+ evaluation_configuration = EvaluationConfiguration.from_dict(data)
+ state = State.from_dict(data)
+ mech_group = data['mech_group']
+ database_collection = data['db_collection']
+ return TestParameters(browser_configuration, evaluation_configuration, state, mech_group, database_collection)
@dataclass(frozen=True)
class TestResult:
@@ -228,6 +238,35 @@ class PlotParameters:
dirty_allowed: bool = True
target_cookie_name: Optional[str] = None
+ @staticmethod
+ def from_dict(data: dict) -> PlotParameters:
+ if data.get("lower_version", None) and data.get("upper_version", None):
+ major_version_range = (data["lower_version"], data["upper_version"])
+ else:
+ major_version_range = None
+ if data.get("lower_revision_nb", None) and data.get("upper_revision_nb", None):
+ revision_number_range = (
+ data["lower_revision_nb"],
+ data["upper_revision_nb"],
+ )
+ else:
+ revision_number_range = None
+ return PlotParameters(
+ data.get("plot_mech_group"),
+ data.get("target_mech_id"),
+ data.get("browser_name"),
+ data.get("db_collection"),
+ major_version_range=major_version_range,
+ revision_number_range=revision_number_range,
+ browser_config=data.get("browser_setting", "default"),
+ extensions=data.get("extensions", []),
+ cli_options=data.get("cli_options", []),
+ dirty_allowed=data.get("dirty_allowed", True),
+ target_cookie_name=None
+ if data.get("check_for") == "request"
+ else data.get("target_cookie_name", "generic"),
+ )
+
@staticmethod
def evaluation_factory(kwargs: ImmutableMultiDict) -> list[EvaluationParameters]:
diff --git a/bci/main.py b/bci/main.py
index 127c1c2..a377f18 100644
--- a/bci/main.py
+++ b/bci/main.py
@@ -1,161 +1,193 @@
import logging
-import bci.browser.binary.factory as binary_factory
-from bci.analysis.plot_factory import PlotFactory
-from bci.browser.support import get_chromium_support, get_firefox_support
+import bci.database.mongo.container as mongodb_container
from bci.configuration import Global, Loggers
-from bci.database.mongo.mongodb import MongoDB
-from bci.evaluations.logic import EvaluationParameters, PlotParameters
-from bci.master import Master
+from bci.database.mongo.mongodb import MongoDB, ServerException
+from bci.database.mongo.revision_cache import RevisionCache
+from bci.distribution.worker_manager import WorkerManager
+from bci.evaluations.custom.custom_evaluation import CustomEvaluationFramework
+from bci.evaluations.logic import (
+ DatabaseParameters,
+ EvaluationParameters,
+ TestParameters,
+)
+from bci.evaluations.outcome_checker import OutcomeChecker
+from bci.search_strategy.bgb_search import BiggestGapBisectionSearch
+from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence
+from bci.search_strategy.composite_search import CompositeSearch
+from bci.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy
+from bci.version_control.factory import StateFactory
+from bci.version_control.states.revisions.firefox import BINARY_AVAILABILITY_MAPPING
+from bci.web.clients import Clients
logger = logging.getLogger(__name__)
class Main:
- loggers = None
- master = None
-
- @staticmethod
- def initialize():
- Main.loggers = Loggers()
- Main.loggers.configure_loggers()
- if Global.check_required_env_parameters():
- Main.master = Master()
-
- @staticmethod
- def is_ready() -> bool:
- return Main.master is not None
-
- @staticmethod
- def run(params: EvaluationParameters):
- Main.master.run(params)
-
- @staticmethod
- def stop_gracefully():
- Main.master.activate_stop_gracefully()
-
- @staticmethod
- def stop_forcefully():
- Main.master.activate_stop_forcefully()
-
- @staticmethod
- def get_state() -> str:
- return Main.master.state
-
- @staticmethod
- def connect_to_database():
- return Main.master.connect_to_database()
-
- @staticmethod
- def get_logs() -> list[str]:
- return list(
- map(
- lambda x: Main.format_to_user_log(x.__dict__),
- Loggers.memory_handler.buffer,
- )
- )
-
- @staticmethod
- def format_to_user_log(log: dict) -> str:
- return f'[{log["asctime"]}] [{log["levelname"]}] {log["name"]}: {log["msg"]}'
-
- @staticmethod
- def get_database_info() -> dict:
- return MongoDB().get_info()
-
- @staticmethod
- def get_browser_support() -> list[dict]:
- return [get_chromium_support(), get_firefox_support()]
-
- @staticmethod
- def list_downloaded_binaries(browser):
- return binary_factory.list_downloaded_binaries(browser)
-
- @staticmethod
- def list_artisanal_binaries(browser):
- return binary_factory.list_artisanal_binaries(browser)
-
- @staticmethod
- def update_artisanal_binaries(browser):
- return binary_factory.update_artisanal_binaries(browser)
-
- @staticmethod
- def download_online_binary(browser, rev_number):
- binary_factory.download_online_binary(browser, rev_number)
-
- @staticmethod
- def get_mech_groups_of_evaluation_framework(evaluation_name: str, project) -> list[tuple[str, bool]]:
- return Main.master.evaluation_framework.get_mech_groups(project)
-
- @staticmethod
- def get_projects_of_custom_framework() -> list[str]:
- return Main.master.evaluation_framework.get_projects()
-
- @staticmethod
- def convert_to_plotparams(data: dict) -> PlotParameters:
- if data.get("lower_version", None) and data.get("upper_version", None):
- major_version_range = (data["lower_version"], data["upper_version"])
+ def __init__(self) -> None:
+ self.state = {'is_running': False, 'reason': 'init', 'status': 'idle'}
+
+ self.stop_gracefully = False
+ self.stop_forcefully = False
+
+ self.firefox_build = None
+ self.chromium_build = None
+
+ self.eval_queue = []
+
+ Global.initialize_folders()
+ self.db_connection_params = Global.get_database_params()
+ self.connect_to_database(self.db_connection_params)
+ RevisionCache.store_firefox_binary_availability(BINARY_AVAILABILITY_MAPPING) # TODO: find better place
+ self.evaluation_framework = CustomEvaluationFramework()
+ logger.info('BugHog is ready!')
+
+ def connect_to_database(self, db_connection_params: DatabaseParameters) -> None:
+ try:
+ MongoDB().connect(db_connection_params)
+ except ServerException:
+ logger.error('Could not connect to database.', exc_info=True)
+
+ def run(self, eval_params_list: list[EvaluationParameters]) -> None:
+ # Sequence_configuration settings are the same over evaluation parameters (quick fix)
+ worker_manager = WorkerManager(eval_params_list[0].sequence_configuration.nb_of_containers)
+ self.stop_gracefully = False
+ self.stop_forcefully = False
+ try:
+ self.__init_eval_queue(eval_params_list)
+ for eval_params in eval_params_list:
+ if self.stop_gracefully or self.stop_forcefully:
+ break
+ self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'active')
+ self.__update_state(is_running=True, reason='user', status='running', queue=self.eval_queue)
+ self.run_single_evaluation(eval_params, worker_manager)
+
+ except Exception as e:
+ logger.critical('A critical error occurred', exc_info=True)
+ raise e
+ finally:
+ # Gracefully exit
+ if self.stop_gracefully:
+ logger.info('Gracefully stopping experiment queue due to user end signal...')
+ self.state['reason'] = 'user'
+ if self.stop_forcefully:
+ logger.info('Forcefully stopping experiment queue due to user end signal...')
+ self.state['reason'] = 'user'
+ worker_manager.forcefully_stop_all_running_containers()
+ else:
+ logger.info('Gracefully stopping experiment queue since last experiment started.')
+ # MongoDB.disconnect()
+ logger.info('Waiting for remaining experiments to stop...')
+ worker_manager.wait_until_all_evaluations_are_done()
+ logger.info('BugHog has finished the evaluation!')
+ self.__update_state(is_running=False, status='idle', queue=self.eval_queue)
+
+ def run_single_evaluation(self, eval_params: EvaluationParameters, worker_manager: WorkerManager) -> None:
+ browser_name = eval_params.browser_configuration.browser_name
+ experiment_name = eval_params.evaluation_range.mech_group
+
+ logger.info(f"Starting evaluation for experiment '{experiment_name}' with browser '{browser_name}'")
+
+ search_strategy = self.create_sequence_strategy(eval_params)
+
+ try:
+ while (self.stop_gracefully or self.stop_forcefully) is False:
+ # Update search strategy with new potentially new results
+ current_state = search_strategy.next()
+
+ # Prepare worker parameters
+ worker_params = eval_params.create_worker_params_for(current_state, self.db_connection_params)
+
+ # Start worker to perform evaluation
+ worker_manager.start_test(worker_params)
+
+ except SequenceFinished:
+ logger.debug('Last experiment has started')
+ self.state['reason'] = 'finished'
+ self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'done')
+
+ @staticmethod
+ def create_sequence_strategy(eval_params: EvaluationParameters) -> SequenceStrategy:
+ sequence_config = eval_params.sequence_configuration
+ search_strategy = sequence_config.search_strategy
+ sequence_limit = sequence_config.sequence_limit
+ outcome_checker = OutcomeChecker(sequence_config)
+ state_factory = StateFactory(eval_params, outcome_checker)
+
+ if search_strategy == 'bgb_sequence':
+ strategy = BiggestGapBisectionSequence(state_factory, sequence_limit)
+ elif search_strategy == 'bgb_search':
+ strategy = BiggestGapBisectionSearch(state_factory)
+ elif search_strategy == 'comp_search':
+ strategy = CompositeSearch(state_factory, sequence_limit)
else:
- major_version_range = None
- if data.get("lower_revision_nb", None) and data.get("upper_revision_nb", None):
- revision_number_range = (
- data["lower_revision_nb"],
- data["upper_revision_nb"],
- )
+ raise AttributeError("Unknown search strategy option '%s'" % search_strategy)
+ return strategy
+
+ def activate_stop_gracefully(self):
+ if self.evaluation_framework:
+ self.stop_gracefully = True
+ self.__update_state(is_running=True, reason='user', status='waiting_to_stop')
+ self.evaluation_framework.stop_gracefully()
+ logger.info('Received user signal to gracefully stop.')
else:
- revision_number_range = None
- return PlotParameters(
- data.get("plot_mech_group"),
- data.get("target_mech_id"),
- data.get("browser_name"),
- data.get("db_collection"),
- major_version_range=major_version_range,
- revision_number_range=revision_number_range,
- browser_config=data.get("browser_setting", "default"),
- extensions=data.get("extensions", []),
- cli_options=data.get("cli_options", []),
- dirty_allowed=data.get("dirty_allowed", True),
- target_cookie_name=None
- if data.get("check_for") == "request"
- else data.get("target_cookie_name", "generic"),
- )
-
- @staticmethod
- def get_data_sources(data: dict):
- params = Main.convert_to_plotparams(data)
-
- if PlotFactory.validate_params(params):
- return None, None
-
- return \
- PlotFactory.get_plot_revision_data(params, MongoDB()), \
- PlotFactory.get_plot_version_data(params, MongoDB())
-
- @staticmethod
- def get_poc(project: str, poc: str) -> dict:
- return Main.master.evaluation_framework.get_poc_structure(project, poc)
-
- @staticmethod
- def get_poc_file(project: str, poc: str, domain: str, path: str, file: str) -> str:
- return Main.master.evaluation_framework.get_poc_file(project, poc, domain, path, file)
-
- @staticmethod
- def update_poc_file(project: str, poc: str, domain: str, path: str, file: str, content: str) -> bool:
- logger.debug(f'Updating file {file} of project {project} and poc {poc}')
- return Main.master.evaluation_framework.update_poc_file(project, poc, domain, path, file, content)
-
- @staticmethod
- def create_empty_poc(project: str, poc_name: str) -> bool:
- return Main.master.evaluation_framework.create_empty_poc(project, poc_name)
-
- @staticmethod
- def get_available_domains() -> list[str]:
- return Global.get_available_domains()
-
- @staticmethod
- def add_page(project: str, poc: str, domain: str, path: str, file_type: str) -> bool:
- return Main.master.evaluation_framework.add_page(project, poc, domain, path, file_type)
-
- @staticmethod
- def sigint_handler(signum, frame):
- return Main.master.stop_bughog()
+ logger.info('Received user signal to gracefully stop, but no evaluation is running.')
+
+ def activate_stop_forcefully(self) -> None:
+ if self.evaluation_framework:
+ self.stop_forcefully = True
+ self.__update_state(is_running=True, reason='user', status='waiting_to_stop')
+ self.evaluation_framework.stop_gracefully()
+ WorkerManager.forcefully_stop_all_running_containers()
+ logger.info('Received user signal to forcefully stop.')
+ else:
+ logger.info('Received user signal to forcefully stop, but no evaluation is running.')
+
+ def quit_bughog(self) -> None:
+ """
+ Quits the bughog application, stopping all associated containers.
+ """
+ logger.info('Stopping all running BugHog containers...')
+ self.activate_stop_forcefully()
+ mongodb_container.stop()
+ logger.info('Stopping BugHog core...')
+ exit(0)
+
+ def sigint_handler(self, sig_number, stack_frame) -> None:
+ logger.debug(f'Sigint received with number {sig_number} for stack frame {stack_frame}')
+ self.quit_bughog()
+
+ def push_info(self, ws, *args) -> None:
+ update = {}
+ all = 'all' in args
+ for arg in args:
+ if arg == 'db_info' or all:
+ update['db_info'] = MongoDB().get_info()
+ if arg == 'logs' or all:
+ update['logs'] = Loggers.get_logs()
+ if arg == 'state' or all:
+ update['state'] = self.state
+ Clients.push_info(ws, update)
+
+ def remove_datapoint(self, params: TestParameters) -> None:
+ MongoDB().remove_datapoint(params)
+ Clients.push_results_to_all()
+
+ def __update_state(self, **kwargs) -> None:
+ for key, value in kwargs.items():
+ self.state[key] = value
+ Clients.push_info_to_all({'state': self.state})
+
+ def __init_eval_queue(self, eval_params_list: list[EvaluationParameters]) -> None:
+ self.eval_queue = []
+ for eval_params in eval_params_list:
+ self.eval_queue.append({
+ 'experiment': eval_params.evaluation_range.mech_group,
+ 'state': 'pending'
+ })
+
+ def __update_eval_queue(self, experiment: str, state: str) -> None:
+ for eval in self.eval_queue:
+ if eval['experiment'] == experiment:
+ eval['state'] = state
+ return
diff --git a/bci/master.py b/bci/master.py
deleted file mode 100644
index 60b36ce..0000000
--- a/bci/master.py
+++ /dev/null
@@ -1,169 +0,0 @@
-import logging
-
-import bci.database.mongo.container as mongodb_container
-from bci.configuration import Global
-from bci.database.mongo.mongodb import MongoDB, ServerException
-from bci.database.mongo.revision_cache import RevisionCache
-from bci.distribution.worker_manager import WorkerManager
-from bci.evaluations.custom.custom_evaluation import CustomEvaluationFramework
-from bci.evaluations.logic import (
- DatabaseParameters,
- EvaluationParameters,
-)
-from bci.evaluations.outcome_checker import OutcomeChecker
-from bci.search_strategy.bgb_search import BiggestGapBisectionSearch
-from bci.search_strategy.bgb_sequence import BiggestGapBisectionSequence
-from bci.search_strategy.composite_search import CompositeSearch
-from bci.search_strategy.sequence_strategy import SequenceFinished, SequenceStrategy
-from bci.version_control.factory import StateFactory
-from bci.version_control.states.revisions.firefox import BINARY_AVAILABILITY_MAPPING
-from bci.web.clients import Clients
-
-logger = logging.getLogger(__name__)
-
-
-class Master:
- def __init__(self) -> None:
- self.state = {'is_running': False, 'reason': 'init', 'status': 'idle'}
-
- self.stop_gracefully = False
- self.stop_forcefully = False
-
- self.firefox_build = None
- self.chromium_build = None
-
- self.eval_queue = []
-
- Global.initialize_folders()
- self.db_connection_params = Global.get_database_params()
- self.connect_to_database(self.db_connection_params)
- RevisionCache.store_firefox_binary_availability(BINARY_AVAILABILITY_MAPPING) # TODO: find better place
- self.evaluation_framework = CustomEvaluationFramework()
- logger.info('BugHog is ready!')
-
- def connect_to_database(self, db_connection_params: DatabaseParameters) -> None:
- try:
- MongoDB().connect(db_connection_params)
- except ServerException:
- logger.error('Could not connect to database.', exc_info=True)
-
- def run(self, eval_params_list: list[EvaluationParameters]) -> None:
- # Sequence_configuration settings are the same over evaluation parameters (quick fix)
- worker_manager = WorkerManager(eval_params_list[0].sequence_configuration.nb_of_containers)
- self.stop_gracefully = False
- self.stop_forcefully = False
- try:
- self.__init_eval_queue(eval_params_list)
- for eval_params in eval_params_list:
- if self.stop_gracefully or self.stop_forcefully:
- break
- self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'active')
- self.__update_state(is_running=True,reason='user', status='running', queue=self.eval_queue)
- self.run_single_evaluation(eval_params, worker_manager)
-
- except Exception as e:
- logger.critical('A critical error occurred', exc_info=True)
- raise e
- finally:
- # Gracefully exit
- if self.stop_gracefully:
- logger.info('Gracefully stopping experiment queue due to user end signal...')
- self.state['reason'] = 'user'
- if self.stop_forcefully:
- logger.info('Forcefully stopping experiment queue due to user end signal...')
- self.state['reason'] = 'user'
- worker_manager.forcefully_stop_all_running_containers()
- else:
- logger.info('Gracefully stopping experiment queue since last experiment started.')
- # MongoDB.disconnect()
- logger.info('Waiting for remaining experiments to stop...')
- worker_manager.wait_until_all_evaluations_are_done()
- logger.info('BugHog has finished the evaluation!')
- self.__update_state(is_running=False, status='idle', queue=self.eval_queue)
-
- def run_single_evaluation(self, eval_params: EvaluationParameters, worker_manager: WorkerManager) -> None:
- browser_name = eval_params.browser_configuration.browser_name
- experiment_name = eval_params.evaluation_range.mech_group
-
- logger.info(f"Starting evaluation for experiment '{experiment_name}' with browser '{browser_name}'")
-
- search_strategy = self.create_sequence_strategy(eval_params)
-
- try:
- while (self.stop_gracefully or self.stop_forcefully) is False:
- # Update search strategy with new potentially new results
- current_state = search_strategy.next()
-
- # Prepare worker parameters
- worker_params = eval_params.create_worker_params_for(current_state, self.db_connection_params)
-
- # Start worker to perform evaluation
- worker_manager.start_test(worker_params)
-
- except SequenceFinished:
- logger.debug('Last experiment has started')
- self.state['reason'] = 'finished'
- self.__update_eval_queue(eval_params.evaluation_range.mech_group, 'done')
-
- @staticmethod
- def create_sequence_strategy(eval_params: EvaluationParameters) -> SequenceStrategy:
- sequence_config = eval_params.sequence_configuration
- search_strategy = sequence_config.search_strategy
- sequence_limit = sequence_config.sequence_limit
- outcome_checker = OutcomeChecker(sequence_config)
- state_factory = StateFactory(eval_params, outcome_checker)
-
- if search_strategy == 'bgb_sequence':
- strategy = BiggestGapBisectionSequence(state_factory, sequence_limit)
- elif search_strategy == 'bgb_search':
- strategy = BiggestGapBisectionSearch(state_factory)
- elif search_strategy == 'comp_search':
- strategy = CompositeSearch(state_factory, sequence_limit)
- else:
- raise AttributeError("Unknown search strategy option '%s'" % search_strategy)
- return strategy
-
- def activate_stop_gracefully(self):
- if self.evaluation_framework:
- self.stop_gracefully = True
- self.__update_state(is_running=True, reason='user', status='waiting_to_stop')
- self.evaluation_framework.stop_gracefully()
- logger.info('Received user signal to gracefully stop.')
- else:
- logger.info('Received user signal to gracefully stop, but no evaluation is running.')
-
- def activate_stop_forcefully(self) -> None:
- if self.evaluation_framework:
- self.stop_forcefully = True
- self.__update_state(is_running=True, reason='user', status='waiting_to_stop')
- self.evaluation_framework.stop_gracefully()
- WorkerManager.forcefully_stop_all_running_containers()
- logger.info('Received user signal to forcefully stop.')
- else:
- logger.info('Received user signal to forcefully stop, but no evaluation is running.')
-
- def stop_bughog(self) -> None:
- logger.info('Stopping all running BugHog containers...')
- self.activate_stop_forcefully()
- mongodb_container.stop()
- logger.info('Stopping BugHog core...')
- exit(0)
-
- def __update_state(self, **kwargs) -> None:
- for key, value in kwargs.items():
- self.state[key] = value
- Clients.push_info_to_all('state')
-
- def __init_eval_queue(self, eval_params_list: list[EvaluationParameters]) -> None:
- self.eval_queue = []
- for eval_params in eval_params_list:
- self.eval_queue.append({
- 'experiment': eval_params.evaluation_range.mech_group,
- 'state': 'pending'
- })
-
- def __update_eval_queue(self, experiment: str, state: str) -> None:
- for eval in self.eval_queue:
- if eval['experiment'] == experiment:
- eval['state'] = state
- return
diff --git a/bci/search_strategy/bgb_search.py b/bci/search_strategy/bgb_search.py
index a9f6a22..34fcf67 100644
--- a/bci/search_strategy/bgb_search.py
+++ b/bci/search_strategy/bgb_search.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import logging
from typing import Optional
@@ -16,13 +18,14 @@ class BiggestGapBisectionSearch(BiggestGapBisectionSequence):
It stops when there are no more states to evaluate between two states with different outcomes.
"""
- def __init__(self, state_factory: StateFactory) -> None:
+ def __init__(self, state_factory: StateFactory, completed_states: Optional[list[State]]=None) -> None:
"""
Initializes the search strategy.
:param state_factory: The factory to create new states.
+ :param completed_states: States that have already been returned.
"""
- super().__init__(state_factory, 0)
+ super().__init__(state_factory, 0, completed_states=completed_states)
def next(self) -> State:
"""
@@ -86,3 +89,12 @@ def __get_next_pair_to_split(self) -> Optional[tuple[State, State]]:
# splitter of the second gap with having to wait for the first gap to be fully evaluated.
pairs.sort(key=lambda pair: pair[1].index - pair[0].index, reverse=True)
return pairs[0]
+
+ @staticmethod
+ def create_from_bgb_sequence(bgb_sequence: BiggestGapBisectionSequence) -> BiggestGapBisectionSearch:
+ """
+ Returns a BGB search object, which continues on state of the given BGB sequence object.
+
+ :param bgb_sequence: The BGB sequence object from which the state will be used to create the BGB search object.
+ """
+ return BiggestGapBisectionSearch(bgb_sequence._state_factory, completed_states=bgb_sequence._completed_states)
diff --git a/bci/search_strategy/bgb_sequence.py b/bci/search_strategy/bgb_sequence.py
index 92a9b77..f59efde 100644
--- a/bci/search_strategy/bgb_sequence.py
+++ b/bci/search_strategy/bgb_sequence.py
@@ -13,14 +13,15 @@ class BiggestGapBisectionSequence(SequenceStrategy):
This sequence strategy will split the biggest gap between two states in half and return the state in the middle.
"""
- def __init__(self, state_factory: StateFactory, limit: int) -> None:
+ def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]]=None) -> None:
"""
Initializes the sequence strategy.
:param state_factory: The factory to create new states.
:param limit: The maximum number of states to evaluate. 0 means no limit.
+ :param completed_states: States that have already been returned.
"""
- super().__init__(state_factory, limit)
+ super().__init__(state_factory, limit, completed_states=completed_states)
self._unavailability_gap_pairs: set[tuple[State, State]] = set()
"""Tuples in this list are **strict** boundaries of ranges without any available binaries."""
diff --git a/bci/search_strategy/composite_search.py b/bci/search_strategy/composite_search.py
index f59a7c1..90625e6 100644
--- a/bci/search_strategy/composite_search.py
+++ b/bci/search_strategy/composite_search.py
@@ -8,16 +8,19 @@
class CompositeSearch():
def __init__(self, state_factory: StateFactory, sequence_limit: int) -> None:
self.sequence_strategy = BiggestGapBisectionSequence(state_factory, limit=sequence_limit)
- self.search_strategy = BiggestGapBisectionSearch(state_factory)
- self.sequence_strategy_finished = False
+ self.search_strategy = None
def next(self) -> State:
- # First we use the sequence strategy to select the next state
- if not self.sequence_strategy_finished:
+ """
+ Returns the next state, based on a sequence strategy and search strategy.
+ First, the sequence strategy decides which state to return until it returns the SequenceFinished exception.
+ From then on, the search strategy decides which state to return.
+ """
+ if self.search_strategy is None:
try:
return self.sequence_strategy.next()
except SequenceFinished:
- self.sequence_strategy_finished = True
+ self.search_strategy = BiggestGapBisectionSearch.create_from_bgb_sequence(self.sequence_strategy)
return self.search_strategy.next()
else:
return self.search_strategy.next()
diff --git a/bci/search_strategy/sequence_strategy.py b/bci/search_strategy/sequence_strategy.py
index 3a6eaff..c93585f 100644
--- a/bci/search_strategy/sequence_strategy.py
+++ b/bci/search_strategy/sequence_strategy.py
@@ -10,17 +10,18 @@
class SequenceStrategy:
- def __init__(self, state_factory: StateFactory, limit) -> None:
+ def __init__(self, state_factory: StateFactory, limit: int, completed_states: Optional[list[State]]=None) -> None:
"""
Initializes the sequence strategy.
:param state_factory: The factory to create new states.
:param limit: The maximum number of states to evaluate. 0 means no limit.
+ :param completed_states: States that have already been returned.
"""
self._state_factory = state_factory
self._limit = limit
self._lower_state, self._upper_state = self.__create_available_boundary_states()
- self._completed_states = []
+ self._completed_states = completed_states if completed_states else []
@abstractmethod
def next(self) -> State:
diff --git a/bci/util.py b/bci/util.py
index ffaf965..3175ca1 100644
--- a/bci/util.py
+++ b/bci/util.py
@@ -42,6 +42,16 @@ def copy_folder(src_path, dst_path):
shutil.copytree(src_path, dst_path, dirs_exist_ok=True)
+def remove_all_in_folder(folder_path: str) -> None:
+ for root, dirs, files in os.walk(folder_path):
+ for file_name in files:
+ file_path = os.path.join(root, file_name)
+ os.remove(file_path)
+ for dir_name in dirs:
+ dir_path = os.path.join(root, dir_name)
+ shutil.rmtree(dir_path)
+
+
def rmtree(src_path):
"""
Removes folder at given src_path.
diff --git a/bci/version_control/states/revisions/firefox.py b/bci/version_control/states/revisions/firefox.py
index f9eb831..928864b 100644
--- a/bci/version_control/states/revisions/firefox.py
+++ b/bci/version_control/states/revisions/firefox.py
@@ -28,6 +28,8 @@ def has_online_binary(self) -> bool:
def get_online_binary_url(self) -> str:
result = RevisionCache.firefox_get_binary_info(self._revision_id)
+ if result is None:
+ raise AttributeError(f"Could not find binary url for '{self._revision_id}")
binary_base_url = result['files_url']
app_version = result['app_version']
binary_url = f'{binary_base_url}firefox-{app_version}.en-US.linux-x86_64.tar.bz2'
diff --git a/bci/web/blueprints/api.py b/bci/web/blueprints/api.py
index 592fbca..607edcf 100644
--- a/bci/web/blueprints/api.py
+++ b/bci/web/blueprints/api.py
@@ -3,11 +3,15 @@
import os
import threading
-from flask import Blueprint, request
+from flask import Blueprint, current_app, request
+import bci.browser.support as browser_support
+import bci.evaluations.logic as application_logic
+from bci.analysis.plot_factory import PlotFactory
from bci.app import sock
-from bci.evaluations.logic import evaluation_factory
-from bci.main import Main as bci_api
+from bci.configuration import Global, Loggers
+from bci.evaluations.logic import PlotParameters
+from bci.main import Main
from bci.web.clients import Clients
logger = logging.getLogger(__name__)
@@ -16,8 +20,10 @@
THREAD = None
-def start_thread(func, args=None) -> bool:
+def __start_thread(func, args=None) -> bool:
global THREAD
+ if args is None:
+ args = []
if THREAD and THREAD.is_alive():
return False
else:
@@ -26,15 +32,24 @@ def start_thread(func, args=None) -> bool:
return True
+def __get_main() -> Main:
+ if main := current_app.config['main']:
+ return main
+ raise Exception('Main object is not instantiated')
+
@api.before_request
def check_readiness():
- if not bci_api.is_ready():
+ try:
+ pass
+ # _ = ____get_main()
+ except Exception as e:
+ logger.critical(e)
return {
'status': 'NOK',
'msg': 'BugHog is not ready',
'info': {
- 'log': bci_api.get_logs(),
+ 'log': Loggers.get_logs()
}
}
@@ -55,9 +70,15 @@ def add_headers(response):
@api.route('/evaluation/start/', methods=['POST'])
def start_evaluation():
+ if request.json is None:
+ return {
+ 'status': 'NOK',
+ 'msg': "No evaluation parameters found"
+ }
+
data = request.json.copy()
- params = evaluation_factory(data)
- if start_thread(bci_api.run, args=[params]):
+ params = application_logic.evaluation_factory(data)
+ if __start_thread(__get_main().run, args=[params]):
return {
'status': 'OK'
}
@@ -69,12 +90,18 @@ def start_evaluation():
@api.route('/evaluation/stop/', methods=['POST'])
def stop_evaluation():
+ if request.json is None:
+ return {
+ 'status': 'NOK',
+ 'msg': "No stop parameters found"
+ }
+
data = request.json.copy()
forcefully = data.get('forcefully', False)
if forcefully:
- bci_api.stop_forcefully()
+ __get_main().activate_stop_forcefully()
else:
- bci_api.stop_gracefully()
+ __get_main().activate_stop_gracefully()
return {
'status': 'OK'
}
@@ -100,7 +127,7 @@ def init_websocket(ws):
if params := message.get('select_project', None):
Clients.associate_project(ws, params)
if requested_variables := message.get('get', []):
- Clients.push_info(ws, *requested_variables)
+ __get_main().push_info(ws, *requested_variables)
except ValueError:
logger.warning('Ignoring invalid message from client.')
ws.send('Connected to BugHog')
@@ -110,7 +137,7 @@ def init_websocket(ws):
def get_browsers():
return {
'status': 'OK',
- 'browsers': bci_api.get_browser_support()
+ 'browsers': [browser_support.get_chromium_support(), browser_support.get_firefox_support()]
}
@@ -118,7 +145,21 @@ def get_browsers():
def get_projects():
return {
'status': 'OK',
- 'projects': bci_api.get_projects_of_custom_framework()
+ 'projects': __get_main().evaluation_framework.get_projects()
+ }
+
+
+@api.route('/projects/', methods=['POST'])
+def create_project():
+ if request.json is None:
+ return {
+ 'status': 'NOK',
+ 'msg': "No parameters found"
+ }
+ project_name = request.json.get('project_name')
+ return {
+ 'status': 'OK',
+ 'projects': __get_main().evaluation_framework.create_empty_project(project_name)
}
@@ -140,24 +181,28 @@ def log():
@api.route('/data/', methods=['PUT'])
def data_source():
- params = request.json.copy()
- revision_data, version_data = bci_api.get_data_sources(params)
- if revision_data or version_data:
+ if request.json is None:
return {
- 'status': 'OK',
- 'revision': revision_data,
- 'version': version_data
+ 'status': 'NOK',
+ 'msg': "No data parameters found"
}
- else:
+
+ params = request.json.copy()
+ plot_params = PlotParameters.from_dict(params)
+ if missing_params := PlotFactory.validate_params(plot_params):
return {
'status': 'NOK',
- 'msg': 'Invalid type'
+ 'msg': f'Missing plot parameters: {missing_params}'
+ }
+ return {
+ 'status': 'OK',
+ 'revision': PlotFactory.get_plot_revision_data(params),
+ 'version': PlotFactory.get_plot_version_data(params)
}
-
@api.route('/poc//', methods=['GET'])
def get_experiments(project: str):
- experiments = bci_api.get_mech_groups_of_evaluation_framework('custom', project)
+ experiments = __get_main().evaluation_framework.get_mech_groups(project)
return {
'status': 'OK',
'experiments': experiments
@@ -168,36 +213,71 @@ def get_experiments(project: str):
def poc(project: str, poc: str):
return {
'status': 'OK',
- 'tree': bci_api.get_poc(project, poc)
+ 'tree': __get_main().evaluation_framework.get_poc_structure(project, poc)
}
-@api.route('/poc//////', methods=['GET'])
-def poc_file(project: str, poc: str, domain: str, path: str, file: str):
- return {
- 'status': 'OK',
- 'content': bci_api.get_poc_file(project, poc, domain, path, file)
- }
+@api.route('/poc////', methods=['GET', 'POST'])
+def get_poc_file_content(project: str, poc: str, file: str):
+ domain = request.args.get('domain', '')
+ path = request.args.get('path', '')
+ if request.method == 'GET':
+ return {
+ 'status': 'OK',
+ 'content': __get_main().evaluation_framework.get_poc_file(project, poc, domain, path, file)
+ }
+ else:
+ if not request.json:
+ return {
+ 'status': 'NOK',
+ 'msg': 'No content to update file with'
+ }
+ data = request.json.copy()
+ content = data['content']
+ success = __get_main().evaluation_framework.update_poc_file(project, poc, domain, path, file, content)
+ if success:
+ return {
+ 'status': 'OK'
+ }
+ else :
+ return {
+ 'status': 'NOK'
+ }
-@api.route('/poc//////', methods=['POST'])
-def update_poc_file(project: str, poc: str, domain: str, path: str, file: str):
+@api.route('/poc///', methods=['POST'])
+def add_page(project: str, poc: str):
+ if request.json is None:
+ return {
+ 'status': 'NOK',
+ 'msg': "No page parameters found"
+ }
+
data = request.json.copy()
- success = bci_api.update_poc_file(project, poc, domain, path, file, data['content'])
+ domain = data['domain']
+ path = data['page']
+ file_type = data['file_type']
+ success = __get_main().evaluation_framework.add_page(project, poc, domain, path, file_type)
if success:
return {
'status': 'OK'
}
- else :
+ else:
return {
'status': 'NOK'
}
-@api.route('/poc///', methods=['POST'])
-def add_page(project: str, poc: str):
+@api.route('/poc///config', methods=['POST'])
+def add_config(project: str, poc: str):
+ if request.json is None:
+ return {
+ 'status': 'NOK',
+ 'msg': "No parameters found"
+ }
data = request.json.copy()
- success = bci_api.add_page(project, poc, data['domain'], data['page'], data['file_type'])
+ type = data['type']
+ success = __get_main().evaluation_framework.add_config(project, poc, type)
if success:
return {
'status': 'OK'
@@ -212,19 +292,26 @@ def add_page(project: str, poc: str):
def get_available_domains():
return {
'status': 'OK',
- 'domains': bci_api.get_available_domains()
+ 'domains': Global.get_available_domains()
}
@api.route('/poc//', methods=['POST'])
def create_experiment(project: str):
+ if request.json is None:
+ return {
+ 'status': 'NOK',
+ 'msg': "No experiment parameters found"
+ }
+
data = request.json.copy()
if 'poc_name' not in data.keys():
return {
'status': 'NOK',
'msg': 'Missing experiment name'
}
- if bci_api.create_empty_poc(project, data['poc_name']):
+ poc_name = data['poc_name']
+ if __get_main().evaluation_framework.create_empty_poc(project, poc_name):
return {
'status': 'OK'
}
@@ -233,3 +320,17 @@ def create_experiment(project: str):
'status': 'NOK',
'msg': 'Could not create experiment'
}
+
+
+@api.route('/data/remove/', methods=['POST'])
+def remove_datapoint():
+ if (params := application_logic.TestParameters.from_dict(request.json)) is None:
+ return {
+ 'status': 'NOK',
+ 'msg': "No parameters found"
+ }
+ __get_main().remove_datapoint(params)
+ return {
+ 'status': 'OK'
+ }
+
diff --git a/bci/web/blueprints/experiments.py b/bci/web/blueprints/experiments.py
index 96297cc..a594240 100644
--- a/bci/web/blueprints/experiments.py
+++ b/bci/web/blueprints/experiments.py
@@ -1,9 +1,11 @@
import datetime
import logging
import threading
+import importlib.util
+import sys
import requests
-from flask import Blueprint, make_response, render_template, request, url_for
+from flask import Blueprint, Request, make_response, render_template, request, url_for
from bci.web.page_parser import load_experiment_pages
@@ -24,6 +26,7 @@
@exp.before_request
def before_request():
+ __report(request)
host = request.host.lower()
if host not in ALLOWED_DOMAINS:
logger.error(
@@ -32,36 +35,13 @@ def before_request():
return f"Host '{host}' is not supported by this framework."
-@exp.route("/")
-def index():
- return f"This page is visited over {request.scheme}."
-
-
-@exp.route("/report/", methods=["GET", "POST"])
-def report():
- leak = request.args.get("leak")
- if leak is not None:
- resp = make_response(
- render_template("cookies.html", title="Report", to_report=leak)
- )
- else:
- resp = make_response(
- render_template(
- "cookies.html", title="Report", to_report="Nothing to report"
- )
- )
-
- cookie_exp_date = datetime.datetime.now() + datetime.timedelta(weeks=4)
- resp.set_cookie("generic", "1", expires=cookie_exp_date)
- resp.set_cookie("secure", "1", expires=cookie_exp_date, secure=True)
- resp.set_cookie("httpOnly", "1", expires=cookie_exp_date, httponly=True)
- resp.set_cookie("lax", "1", expires=cookie_exp_date, samesite="lax")
- resp.set_cookie("strict", "1", expires=cookie_exp_date, samesite="strict")
-
+def __report(request: Request) -> None:
+ """
+ Submit report to BugHog
+ """
# Respond to collector on same IP
# remote_ip = request.remote_addr
remote_ip = request.headers.get("X-Real-IP")
-
response_data = {
"url": request.url,
"method": request.method,
@@ -77,6 +57,29 @@ def send_report_to_collector():
threading.Thread(target=send_report_to_collector).start()
+
+def __get_all_GET_parameters(request) -> dict[str,str]:
+ return {k: v for k, v in request.args.items()}
+
+
+@exp.route("/")
+def index():
+ return f"This page is visited over {request.scheme}."
+
+
+@exp.route("/report/", methods=["GET", "POST"])
+def report_endpoint():
+ get_params = [item for item in __get_all_GET_parameters(request).items()]
+ resp = make_response(
+ render_template("cookies.html", title="Report", get_params=get_params)
+ )
+
+ cookie_exp_date = datetime.datetime.now() + datetime.timedelta(weeks=4)
+ resp.set_cookie("generic", "1", expires=cookie_exp_date)
+ resp.set_cookie("secure", "1", expires=cookie_exp_date, secure=True)
+ resp.set_cookie("httpOnly", "1", expires=cookie_exp_date, httponly=True)
+ resp.set_cookie("lax", "1", expires=cookie_exp_date, samesite="lax")
+ resp.set_cookie("strict", "1", expires=cookie_exp_date, samesite="strict")
return resp
@@ -86,9 +89,9 @@ def report_leak_if_using_http(target_scheme):
Triggers request to /report/ if a request was received over the specified `scheme`.
"""
used_scheme = request.headers.get("X-Forwarded-Proto")
- params = get_all_bughog_GET_parameters(request)
+ params = __get_all_GET_parameters(request)
if used_scheme == target_scheme:
- return "Redirect", 307, {"Location": url_for("experiments.report", **params)}
+ return "Redirect", 307, {"Location": url_for("experiments.report_endpoint", **params)}
else:
return f"Request was received over {used_scheme}, instead of {target_scheme}", 200, {}
@@ -101,12 +104,12 @@ def report_leak_if_present(expected_header_name: str):
if expected_header_name not in request.headers:
return f"Header {expected_header_name} not found", 200, {"Allow-CSP-From": "*"}
- params = get_all_bughog_GET_parameters(request)
+ params = __get_all_GET_parameters(request)
return (
"Redirect",
307,
{
- "Location": url_for("experiments.report", **params),
+ "Location": url_for("experiments.report_endpoint", **params),
"Allow-CSP-From": "*",
},
)
@@ -126,16 +129,32 @@ def report_leak_if_contains(expected_header_name: str, expected_header_value: st
{"Allow-CSP-From": "*"},
)
- params = get_all_bughog_GET_parameters(request)
+ params = __get_all_GET_parameters(request)
return (
"Redirect",
307,
{
- "Location": url_for("experiments.report", **params),
+ "Location": url_for("experiments.report_endpoint", **params),
"Allow-CSP-From": "*",
},
)
-def get_all_bughog_GET_parameters(request):
- return {k: v for k, v in request.args.items() if k.startswith("bughog_")}
+@exp.route("///.py")
+def python_evaluation(project: str, experiment: str, file_name: str):
+ """
+ Evaluates the python script and returns its result.
+ """
+ host = request.host.lower()
+
+ module_name = f"{host}/{project}/{experiment}"
+ path = f"experiments/pages/{project}/{experiment}/{host}/{file_name}.py"
+
+ # Dynamically import the file
+ sys.dont_write_bytecode = True
+ spec = importlib.util.spec_from_file_location(module_name, path)
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = module
+ spec.loader.exec_module(module)
+
+ return module.main(request)
diff --git a/bci/web/clients.py b/bci/web/clients.py
index eee5d24..acb2045 100644
--- a/bci/web/clients.py
+++ b/bci/web/clients.py
@@ -1,8 +1,13 @@
import json
import threading
+from venv import logger
+from flask import current_app
from simple_websocket import Server
+from bci.analysis.plot_factory import PlotFactory
+from bci.evaluations.logic import PlotParameters
+
class Clients:
__semaphore = threading.Semaphore()
@@ -39,10 +44,17 @@ def associate_project(ws_client: Server, project: str):
@staticmethod
def push_results(ws_client: Server):
- from bci.main import Main as bci_api
-
if params := Clients.__clients.get(ws_client, None):
- revision_data, version_data = bci_api.get_data_sources(params)
+ plot_params = PlotParameters.from_dict(params)
+
+ if PlotFactory.validate_params(plot_params):
+ revision_data = None
+ version_data = None
+ else:
+ revision_data = PlotFactory.get_plot_revision_data(plot_params)
+ version_data = PlotFactory.get_plot_version_data(plot_params)
+
+
ws_client.send(
json.dumps(
{
@@ -63,32 +75,27 @@ def push_results_to_all():
Clients.push_results(ws_client)
@staticmethod
- def push_info(ws_client: Server, *requested_vars: str):
- from bci.main import Main as bci_api
-
- update = {}
- all = not requested_vars or 'all' in requested_vars
- if 'db_info' in requested_vars or all:
- update['db_info'] = bci_api.get_database_info()
- if 'logs' in requested_vars or all:
- update['logs'] = bci_api.get_logs()
- if 'state' in requested_vars or all:
- update['state'] = bci_api.get_state()
+ def push_info(ws_client: Server, update: dict):
ws_client.send(json.dumps({'update': update}))
@staticmethod
- def push_info_to_all(*requested_vars: str):
+ def push_info_to_all(update: dict):
Clients.__remove_disconnected_clients()
for ws_client in Clients.__clients.keys():
- Clients.push_info(ws_client, *requested_vars)
+ Clients.push_info(ws_client, update)
@staticmethod
def push_experiments(ws_client: Server):
- from bci.main import Main as bci_api
+ client_info = Clients.__clients[ws_client]
+ if client_info is None:
+ logger.error('Could not find any associated info for this client')
+ return
- project = Clients.__clients[ws_client].get('project', None)
+ project = client_info.get('project', None)
if project:
- experiments = bci_api.get_mech_groups_of_evaluation_framework('custom', project)
+ from bci.main import Main
+ main: Main = current_app.config['main']
+ experiments = main.evaluation_framework.get_mech_groups(project)
ws_client.send(json.dumps({'update': {'experiments': experiments}}))
@staticmethod
diff --git a/bci/web/page_parser.py b/bci/web/page_parser.py
index 1cff781..57dc618 100644
--- a/bci/web/page_parser.py
+++ b/bci/web/page_parser.py
@@ -68,6 +68,10 @@ def get_content(subdir_folder_path: str):
{
"file_name": "index.js",
"content_type": "text/javascript"
+ },
+ {
+ "file_name": "index.py",
+ "content_type": "text/x-python"
}
]
content = None
diff --git a/bci/web/templates/cookies.html b/bci/web/templates/cookies.html
index 320a390..2e1b0b0 100644
--- a/bci/web/templates/cookies.html
+++ b/bci/web/templates/cookies.html
@@ -2,10 +2,15 @@
{% block content %}
-{% if to_report %}
+{% if get_params %}
Reported:
-
-{{ to_report }}
+
+{% for get_param in get_params %}
+ - {{ get_param[0] }}: {{ get_param[1] }}
+{% endfor %}
+
+{% else %}
+No GET parameters.
{% endif %}
Cookies
@@ -20,4 +25,4 @@ Cookies
elem.appendChild(cookie);
}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/bci/web/vue/index.html b/bci/web/vue/index.html
index a427ba2..6bf8ba4 100644
--- a/bci/web/vue/index.html
+++ b/bci/web/vue/index.html
@@ -4,8 +4,8 @@
-
-
+
+
BugHog
diff --git a/bci/web/vue/package-lock.json b/bci/web/vue/package-lock.json
index 1a1d035..e393c6a 100644
--- a/bci/web/vue/package-lock.json
+++ b/bci/web/vue/package-lock.json
@@ -29,6 +29,7 @@
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=10"
},
@@ -40,6 +41,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -48,16 +50,18 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz",
- "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==",
+ "version": "7.26.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
+ "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.25.9"
+ "@babel/types": "^7.26.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -67,9 +71,10 @@
}
},
"node_modules/@babel/types": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz",
- "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==",
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
+ "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+ "license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
@@ -86,6 +91,7 @@
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
@@ -102,6 +108,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
@@ -118,6 +125,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
@@ -134,6 +142,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -150,6 +159,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -166,6 +176,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"freebsd"
@@ -182,6 +193,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"freebsd"
@@ -198,6 +210,7 @@
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -214,6 +227,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -230,6 +244,7 @@
"ia32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -246,6 +261,7 @@
"loong64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -262,6 +278,7 @@
"mips64el"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -278,6 +295,7 @@
"ppc64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -294,6 +312,7 @@
"riscv64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -310,6 +329,7 @@
"s390x"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -326,6 +346,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -342,6 +363,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"netbsd"
@@ -358,6 +380,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"openbsd"
@@ -374,6 +397,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"sunos"
@@ -390,6 +414,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -406,6 +431,7 @@
"ia32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -422,6 +448,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -435,6 +462,7 @@
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
@@ -452,6 +480,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@@ -466,6 +495,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6.0.0"
}
@@ -475,6 +505,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6.0.0"
}
@@ -482,13 +513,15 @@
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -499,6 +532,7 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
@@ -512,6 +546,7 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 8"
}
@@ -521,6 +556,7 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
@@ -534,6 +570,7 @@
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
+ "license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
@@ -543,6 +580,7 @@
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -553,6 +591,7 @@
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
@@ -562,111 +601,123 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz",
- "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
+ "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
+ "license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
- "@vue/shared": "3.5.12",
+ "@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz",
- "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
+ "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
+ "license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.5.12",
- "@vue/shared": "3.5.12"
+ "@vue/compiler-core": "3.5.13",
+ "@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz",
- "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
+ "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
+ "license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
- "@vue/compiler-core": "3.5.12",
- "@vue/compiler-dom": "3.5.12",
- "@vue/compiler-ssr": "3.5.12",
- "@vue/shared": "3.5.12",
+ "@vue/compiler-core": "3.5.13",
+ "@vue/compiler-dom": "3.5.13",
+ "@vue/compiler-ssr": "3.5.13",
+ "@vue/shared": "3.5.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
- "postcss": "^8.4.47",
+ "postcss": "^8.4.48",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz",
- "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
+ "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
+ "license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.12",
- "@vue/shared": "3.5.12"
+ "@vue/compiler-dom": "3.5.13",
+ "@vue/shared": "3.5.13"
}
},
"node_modules/@vue/reactivity": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz",
- "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
+ "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
+ "license": "MIT",
"dependencies": {
- "@vue/shared": "3.5.12"
+ "@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz",
- "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
+ "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
+ "license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.12",
- "@vue/shared": "3.5.12"
+ "@vue/reactivity": "3.5.13",
+ "@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz",
- "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==",
- "dependencies": {
- "@vue/reactivity": "3.5.12",
- "@vue/runtime-core": "3.5.12",
- "@vue/shared": "3.5.12",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
+ "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.13",
+ "@vue/runtime-core": "3.5.13",
+ "@vue/shared": "3.5.13",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz",
- "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
+ "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
+ "license": "MIT",
"dependencies": {
- "@vue/compiler-ssr": "3.5.12",
- "@vue/shared": "3.5.12"
+ "@vue/compiler-ssr": "3.5.13",
+ "@vue/shared": "3.5.13"
},
"peerDependencies": {
- "vue": "3.5.12"
+ "vue": "3.5.13"
}
},
"node_modules/@vue/shared": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz",
- "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg=="
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
+ "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
+ "license": "MIT"
},
"node_modules/@vueform/slider": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@vueform/slider/-/slider-2.1.10.tgz",
- "integrity": "sha512-L2G3Ju51Yq6yWF2wzYYsicUUaH56kL1QKGVtimUVHT1K1ADcRT94xVyIeJpS0klliVEeF6iMZFbdXtHq8AsDHw=="
+ "integrity": "sha512-L2G3Ju51Yq6yWF2wzYYsicUUaH56kL1QKGVtimUVHT1K1ADcRT94xVyIeJpS0klliVEeF6iMZFbdXtHq8AsDHw==",
+ "license": "MIT"
},
"node_modules/ace-builds": {
- "version": "1.36.3",
- "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.36.3.tgz",
- "integrity": "sha512-YcdwV2IIaJSfjkWAR1NEYN5IxBiXefTgwXsJ//UlaFrjXDX5hQpvPFvEePHz2ZBUfvO54RjHeRUQGX8MS5HaMQ=="
+ "version": "1.36.5",
+ "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.36.5.tgz",
+ "integrity": "sha512-mZ5KVanRT6nLRDLqtG/1YQQLX/gZVC/v526cm1Ru/MTSlrbweSmqv2ZT0d2GaHpJq035MwCMIrj+LgDAUnDXrg==",
+ "license": "BSD-3-Clause"
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=12"
},
@@ -679,6 +730,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=12"
},
@@ -690,13 +742,15 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -709,12 +763,14 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.20",
@@ -735,6 +791,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646",
@@ -754,9 +811,10 @@
}
},
"node_modules/axios": {
- "version": "1.7.7",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
- "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+ "version": "1.7.8",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
+ "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
+ "license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -767,13 +825,15 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
},
@@ -786,6 +846,7 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -795,6 +856,7 @@
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
@@ -821,6 +883,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41",
@@ -839,14 +902,15 @@
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001669",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz",
- "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==",
+ "version": "1.0.30001684",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz",
+ "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==",
"dev": true,
"funding": [
{
@@ -861,13 +925,15 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -892,6 +958,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -904,6 +971,7 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
@@ -915,12 +983,14 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -933,15 +1003,17 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/cross-spawn": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
- "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -956,6 +1028,7 @@
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
+ "license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
},
@@ -966,12 +1039,14 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
"engines": {
"node": ">=0.4.0"
}
@@ -980,36 +1055,42 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "dev": true
+ "dev": true,
+ "license": "Apache-2.0"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.43",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.43.tgz",
- "integrity": "sha512-NxnmFBHDl5Sachd2P46O7UJiMaMHMLSofoIWVJq3mj8NJgG0umiSeljAVP9lGzjI0UDLJJ5jjoGjcrB8RSbjLQ==",
- "dev": true
+ "version": "1.5.66",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.66.tgz",
+ "integrity": "sha512-pI2QF6+i+zjPbqRzJwkMvtvkdI7MjVbSh2g8dlMguDJIXEPw+kwasS1Jl+YGPEBfGVxsVgGUratAKymPdPo2vQ==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
@@ -1023,6 +1104,7 @@
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
@@ -1059,6 +1141,7 @@
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -1066,13 +1149,15 @@
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -1089,6 +1174,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -1101,6 +1187,7 @@
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
@@ -1110,6 +1197,7 @@
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -1121,6 +1209,7 @@
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-1.8.1.tgz",
"integrity": "sha512-lXTcO8a6dRTPFpINyOLcATCN/pK1Of/jY4PryklPllAiqH64tSDUsOdQpar3TO59ZXWwugm2e92oaqwH6X90Xg==",
+ "license": "MIT",
"dependencies": {
"@popperjs/core": "^2.9.3",
"mini-svg-data-uri": "^1.4.3"
@@ -1136,6 +1225,7 @@
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
+ "license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -1150,6 +1240,7 @@
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
@@ -1165,6 +1256,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -1179,6 +1271,7 @@
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "*"
},
@@ -1193,6 +1286,7 @@
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -1206,6 +1300,7 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
+ "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -1215,6 +1310,7 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
@@ -1235,6 +1331,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -1247,6 +1344,7 @@
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -1259,6 +1357,7 @@
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
@@ -1271,6 +1370,7 @@
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
@@ -1286,6 +1386,7 @@
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -1295,6 +1396,7 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1304,6 +1406,7 @@
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -1316,6 +1419,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.12.0"
}
@@ -1324,13 +1428,15 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true
+ "dev": true,
+ "license": "ISC"
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
+ "license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
@@ -1346,6 +1452,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
"dev": true,
+ "license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
}
@@ -1355,6 +1462,7 @@
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=10"
}
@@ -1363,18 +1471,21 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true
+ "dev": true,
+ "license": "ISC"
},
"node_modules/magic-string": {
- "version": "0.30.12",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
- "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
+ "version": "0.30.14",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
+ "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==",
+ "license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
@@ -1384,6 +1495,7 @@
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 8"
}
@@ -1393,6 +1505,7 @@
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -1405,6 +1518,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -1413,6 +1527,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
@@ -1424,6 +1539,7 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
+ "license": "MIT",
"bin": {
"mini-svg-data-uri": "cli.js"
}
@@ -1433,6 +1549,7 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -1448,6 +1565,7 @@
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
+ "license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -1457,6 +1575,7 @@
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
@@ -1464,15 +1583,16 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -1484,13 +1604,15 @@
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -1500,6 +1622,7 @@
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -1509,6 +1632,7 @@
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -1518,6 +1642,7 @@
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 6"
}
@@ -1526,6 +1651,7 @@
"version": "1.0.0-rc3",
"resolved": "https://registry.npmjs.org/oh-vue-icons/-/oh-vue-icons-1.0.0-rc3.tgz",
"integrity": "sha512-+k2YC6piK7sEZnwbkQF3UokFPMmgqpiLP6f/H0ovQFLl20QA5V4U8EcI6EclD2Lt5NMQ3k6ilLGo8XyXqdVSvg==",
+ "license": "MIT",
"dependencies": {
"vue-demi": "^0.12.5"
},
@@ -1544,6 +1670,7 @@
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.5.tgz",
"integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==",
"hasInstallScript": true,
+ "license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
@@ -1568,13 +1695,15 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "dev": true
+ "dev": true,
+ "license": "BlueOak-1.0.0"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1583,13 +1712,15 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
+ "license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -1604,13 +1735,15 @@
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8.6"
},
@@ -1623,6 +1756,7 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -1632,14 +1766,15 @@
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/postcss": {
- "version": "8.4.47",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
- "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [
{
"type": "opencollective",
@@ -1654,9 +1789,10 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
+ "picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -1668,6 +1804,7 @@
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
@@ -1685,6 +1822,7 @@
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
},
@@ -1714,6 +1852,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"lilconfig": "^3.0.0",
"yaml": "^2.3.4"
@@ -1739,6 +1878,7 @@
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=14"
},
@@ -1761,6 +1901,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"postcss-selector-parser": "^6.1.1"
},
@@ -1776,6 +1917,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -1788,12 +1930,14 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
@@ -1813,13 +1957,15 @@
"type": "consulting",
"url": "https://feross.org/support"
}
- ]
+ ],
+ "license": "MIT"
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"pify": "^2.3.0"
}
@@ -1829,6 +1975,7 @@
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -1841,6 +1988,7 @@
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
@@ -1858,6 +2006,7 @@
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true,
+ "license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
@@ -1868,6 +2017,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
"dev": true,
+ "license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -1898,6 +2048,7 @@
"url": "https://feross.org/support"
}
],
+ "license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
@@ -1907,6 +2058,7 @@
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -1919,6 +2071,7 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1928,6 +2081,7 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
+ "license": "ISC",
"engines": {
"node": ">=14"
},
@@ -1939,6 +2093,7 @@
"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==",
+ "license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -1948,6 +2103,7 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
@@ -1966,6 +2122,7 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -1980,6 +2137,7 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1988,13 +2146,15 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -2007,6 +2167,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -2023,6 +2184,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -2035,6 +2197,7 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -2044,6 +2207,7 @@
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
@@ -2066,6 +2230,7 @@
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -2074,33 +2239,34 @@
}
},
"node_modules/tailwindcss": {
- "version": "3.4.14",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
- "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==",
+ "version": "3.4.15",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz",
+ "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
- "chokidar": "^3.5.3",
+ "chokidar": "^3.6.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
- "fast-glob": "^3.3.0",
+ "fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
- "jiti": "^1.21.0",
+ "jiti": "^1.21.6",
"lilconfig": "^2.1.0",
- "micromatch": "^4.0.5",
+ "micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
- "picocolors": "^1.0.0",
- "postcss": "^8.4.23",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
- "postcss-load-config": "^4.0.1",
- "postcss-nested": "^6.0.1",
- "postcss-selector-parser": "^6.0.11",
- "resolve": "^1.22.2",
- "sucrase": "^3.32.0"
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
},
"bin": {
"tailwind": "lib/cli.js",
@@ -2115,6 +2281,7 @@
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
}
@@ -2124,6 +2291,7 @@
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
@@ -2136,6 +2304,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
@@ -2147,7 +2316,8 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true
+ "dev": true,
+ "license": "Apache-2.0"
},
"node_modules/update-browserslist-db": {
"version": "1.1.1",
@@ -2168,6 +2338,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
@@ -2183,13 +2354,15 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/vite": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz",
"integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
@@ -2241,15 +2414,16 @@
}
},
"node_modules/vue": {
- "version": "3.5.12",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz",
- "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==",
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
+ "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
+ "license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.12",
- "@vue/compiler-sfc": "3.5.12",
- "@vue/runtime-dom": "3.5.12",
- "@vue/server-renderer": "3.5.12",
- "@vue/shared": "3.5.12"
+ "@vue/compiler-dom": "3.5.13",
+ "@vue/compiler-sfc": "3.5.13",
+ "@vue/runtime-dom": "3.5.13",
+ "@vue/server-renderer": "3.5.13",
+ "@vue/shared": "3.5.13"
},
"peerDependencies": {
"typescript": "*"
@@ -2264,6 +2438,7 @@
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.9.tgz",
"integrity": "sha512-nGEppmzhQQT2iDz4cl+ZCX3BpeNhygK50zWFTIRS+r7K7i61uWXJWSioMuf+V/161EPQjexI8NaEBdUlF3dp+g==",
+ "license": "MIT",
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
@@ -2274,6 +2449,7 @@
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
@@ -2289,6 +2465,7 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
@@ -2307,6 +2484,7 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -2324,6 +2502,7 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -2333,6 +2512,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -2347,13 +2527,15 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -2368,6 +2550,7 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -2376,10 +2559,11 @@
}
},
"node_modules/yaml": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
- "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
+ "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
"dev": true,
+ "license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
diff --git a/bci/web/vue/public/.gitkeep b/bci/web/vue/public/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/bci/web/vue/src/App.vue b/bci/web/vue/src/App.vue
index cac9425..4d316bf 100644
--- a/bci/web/vue/src/App.vue
+++ b/bci/web/vue/src/App.vue
@@ -78,7 +78,8 @@ export default {
project: null,
},
dialog: {
- new_experiment_name: null
+ new_experiment_name: null,
+ new_project_name: null
},
darkmode: null,
darkmode_toggle: null,
@@ -131,9 +132,6 @@ export default {
},
},
watch: {
- "selected.project": function (val) {
- this.set_curr_project(val);
- },
"selected.experiment": function (val) {
if (val !== null) {
this.hide_poc_editor = false;
@@ -185,7 +183,8 @@ export default {
created: function () {
this.websocket = this.create_socket();
this.get_projects();
- this.get_browser_support();const path = `/api/poc/domain/`;
+ this.get_browser_support();
+ const path = `/api/poc/domain/`;
axios.get(path)
.then((res) => {
if (res.data.status === "OK") {
@@ -235,10 +234,13 @@ export default {
"get": ["all"],
});
// This might be a re-open after connection loss, which means we might have to propagate our params again
- this.propagate_new_params()
+ if (this.selected.project !== null) {
+ this.set_curr_project(this.selected.project);
+ }
+ this.propagate_new_params();
});
- websocket.addEventListener("message", () => {
- const data = JSON.parse(event.data);
+ websocket.addEventListener("message", (e) => {
+ const data = JSON.parse(e.data);
if (data.hasOwnProperty("update")) {
if (data.update.hasOwnProperty("plot_data")) {
const revision_data = data.update.plot_data.revision_data;
@@ -288,12 +290,15 @@ export default {
localStorage.setItem('theme', 'light');
}
},
- get_projects() {
+ get_projects(cb) {
const path = `/api/projects/`;
axios.get(path)
.then((res) => {
if (res.data.status == "OK") {
this.projects = res.data.projects;
+ if (cb !== undefined) {
+ cb();
+ }
}
})
.catch((error) => {
@@ -336,11 +341,20 @@ export default {
console.log("Missing plot parameters: ", this.missing_plot_params);
}
},
+ project_dropdown_change(event) {
+ const option = event.target.value;
+ if (option == "Create new project...") {
+ create_project_dialog.showModal();
+ } else {
+ this.set_curr_project(option)
+ }
+ },
set_curr_project(project) {
this.eval_params.project = project;
this.send_with_socket({
"select_project": project
})
+ this.selected.project = project;
this.eval_params.tests = [];
},
set_curr_browser(browser) {
@@ -389,7 +403,21 @@ export default {
this.dialog.new_experiment_name = null;
})
.catch((error) => {
- console.log('Could not create new experiment');
+ console.error('Could not create new experiment');
+ });
+ },
+ create_new_project() {
+ const url = `/api/projects/`;
+ const new_project_name = this.dialog.new_project_name;
+ axios.post(url, {'project_name': new_project_name})
+ .then((res) => {
+ this.dialog.new_project_name = null;
+ this.get_projects(() => {
+ this.set_curr_project(new_project_name);
+ });
+ })
+ .catch((error) => {
+ console.error('Could not create new project', error);
});
},
},
@@ -465,9 +493,10 @@ export default {
-
+
@@ -605,21 +634,21 @@ export default {
+ value="bgb_sequence">
+ value="bgb_search">
+ value="comp_search">
@@ -629,7 +658,7 @@ export default {
-
+
@@ -683,5 +712,23 @@ export default {
+
+
+
diff --git a/bci/web/vue/src/components/gantt.vue b/bci/web/vue/src/components/gantt.vue
index a23bf78..fcbad9d 100644
--- a/bci/web/vue/src/components/gantt.vue
+++ b/bci/web/vue/src/components/gantt.vue
@@ -1,8 +1,14 @@
diff --git a/bci/web/vue/src/components/poc-editor.vue b/bci/web/vue/src/components/poc-editor.vue
index 7bac6b8..98385d7 100644
--- a/bci/web/vue/src/components/poc-editor.vue
+++ b/bci/web/vue/src/components/poc-editor.vue
@@ -1,5 +1,10 @@
@@ -187,35 +303,66 @@