diff --git a/DOSPORTAL/migrations/0036_alter_record_data_file_alter_record_log_file_and_more.py b/DOSPORTAL/migrations/0036_alter_record_data_file_alter_record_log_file_and_more.py new file mode 100644 index 0000000..d93fbed --- /dev/null +++ b/DOSPORTAL/migrations/0036_alter_record_data_file_alter_record_log_file_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 6.0 on 2026-01-05 17:07 + +import DOSPORTAL.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('DOSPORTAL', '0035_record_time_internal_start'), + ] + + operations = [ + migrations.AlterField( + model_name='record', + name='data_file', + field=models.FileField(blank=True, help_text='Processed spectral file', null=True, upload_to=DOSPORTAL.models.Record.user_directory_path_data, validators=[DOSPORTAL.models._validate_data_file], verbose_name='Log file'), + ), + migrations.AlterField( + model_name='record', + name='log_file', + field=models.FileField(blank=True, help_text='Upload recorded data file form your detector', upload_to=DOSPORTAL.models.Record.user_directory_path, validators=[DOSPORTAL.models._validate_log_file], verbose_name='File log'), + ), + migrations.AlterField( + model_name='record', + name='metadata_file', + field=models.FileField(blank=True, help_text='Processed metadata file', null=True, upload_to=DOSPORTAL.models.Record.user_directory_path_data, validators=[DOSPORTAL.models._validate_metadata_file], verbose_name='Metadata file'), + ), + ] diff --git a/DOSPORTAL/migrations/0037_alter_detectorlogbook_options_and_more.py b/DOSPORTAL/migrations/0037_alter_detectorlogbook_options_and_more.py new file mode 100644 index 0000000..576843f --- /dev/null +++ b/DOSPORTAL/migrations/0037_alter_detectorlogbook_options_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 6.0 on 2026-01-05 17:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('DOSPORTAL', '0036_alter_record_data_file_alter_record_log_file_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='detectorlogbook', + options={'ordering': ['-created']}, + ), + migrations.RemoveField( + model_name='record', + name='data_policy', + ), + migrations.AddField( + model_name='detectorlogbook', + name='entry_type', + field=models.CharField(choices=[('reset', 'Reset'), ('sync', 'Sync'), ('maintenance', 'Maintenance'), ('note', 'Note'), ('location_update', 'Location update'), ('calibration', 'Calibration'), ('other', 'Other')], default='note', help_text='Category of the logbook entry.', max_length=30), + ), + migrations.AddField( + model_name='detectorlogbook', + name='source', + field=models.CharField(choices=[('web', 'Web'), ('api', 'API'), ('qr', 'QR'), ('auto', 'Automatic'), ('other', 'Other')], default='web', help_text='Origin of the logbook entry.', max_length=20), + ), + migrations.AlterField( + model_name='record', + name='belongs', + field=models.ForeignKey(choices=[('PR', 'Private'), ('PU', 'Public'), ('NV', 'Non-public')], default='PU', help_text='Data policy of this record. Field can be overridden depending by setting of the organisation, that owns this record.', max_length=2, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='records_owning', to='DOSPORTAL.organization'), + ), + ] diff --git a/DOSPORTAL/migrations/0038_detectorlogbook_altitude_detectorlogbook_latitude_and_more.py b/DOSPORTAL/migrations/0038_detectorlogbook_altitude_detectorlogbook_latitude_and_more.py new file mode 100644 index 0000000..857e899 --- /dev/null +++ b/DOSPORTAL/migrations/0038_detectorlogbook_altitude_detectorlogbook_latitude_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0 on 2026-01-06 21:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('DOSPORTAL', '0037_alter_detectorlogbook_options_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='detectorlogbook', + name='altitude', + field=models.FloatField(blank=True, help_text='Altitude of the location in meters', null=True, verbose_name='Altitude'), + ), + migrations.AddField( + model_name='detectorlogbook', + name='latitude', + field=models.FloatField(blank=True, help_text='GPS latitude of the location', null=True, verbose_name='Latitude'), + ), + migrations.AddField( + model_name='detectorlogbook', + name='longitude', + field=models.FloatField(blank=True, help_text='GPS longitude of the location', null=True, verbose_name='Longitude'), + ), + ] diff --git a/DOSPORTAL/models.py b/DOSPORTAL/models.py index f3b89f2..a3467de 100644 --- a/DOSPORTAL/models.py +++ b/DOSPORTAL/models.py @@ -8,7 +8,7 @@ from matplotlib.colors import LightSource from martor.models import MartorField from django_q.tasks import async_task -from django.utils.text import slugify +from django.utils.text import slugify from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType @@ -18,11 +18,13 @@ from django.contrib.gis.measure import Distance from django import forms +from DOSPORTAL.services.file_validation import validate_uploaded_file + from markdownx.models import MarkdownxField import json from markdownx.utils import markdownify -#from .tasks import process_flight_entry, process_record_entry +# from .tasks import process_flight_entry, process_record_entry def get_enum_dsc(enum, t): @@ -32,43 +34,88 @@ def get_enum_dsc(enum, t): return t +def _validate_log_file(uploaded_file): + return validate_uploaded_file( + uploaded_file, + allowed_extensions=[ + ".log", + ".txt", + ".csv", + ".json", + ], # TODO adjust values as neeeded + max_size_mb=50, + ) + + +def _validate_data_file(uploaded_file): + return validate_uploaded_file( + uploaded_file, + allowed_extensions=[ + ".csv", + ".json", + ".txt", + ".bin", + ], # TODO adjust values as neeeded + max_size_mb=200, + ) + + +def _validate_metadata_file(uploaded_file): + return validate_uploaded_file( + uploaded_file, + allowed_extensions=[ + ".json", + ".yaml", + ".yml", + ".csv", + ], # TODO adjust values as neeeded + max_size_mb=20, + ) + + class UUIDMixin(models.Model): id = models.UUIDField( - primary_key = True, - default = uuid.uuid4, - editable = False, - unique = True + primary_key=True, default=uuid.uuid4, editable=False, unique=True ) def get_admin_url(self): - return reverse("admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name), args=(self.id,)) + return reverse( + "admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name), + args=(self.id,), + ) class Meta: - abstract = True + abstract = True class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) - image = models.ImageField(default='default_user_profileimage.jpg', upload_to='profile_pics') + image = models.ImageField( + default="default_user_profileimage.jpg", upload_to="profile_pics" + ) web = models.URLField(max_length=200, null=True, blank=True) - - def __str__(self): - return f'{self.user.username} Profile' #show how we want it to be displayed + return f"{self.user.username} Profile" # show how we want it to be displayed class Organization(UUIDMixin): DATA_POLICY_CHOICES = [ - ('PR', 'Private'), - ('PU', 'Public'), - ('NV', 'Non-public'), + ("PR", "Private"), + ("PU", "Public"), + ("NV", "Non-public"), ] name = models.CharField(max_length=200) - users = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='organizations', through='OrganizationUser') + users = models.ManyToManyField( + settings.AUTH_USER_MODEL, + related_name="organizations", + through="OrganizationUser", + ) slug = models.SlugField(max_length=255, unique=True, blank=True) - data_policy = models.CharField(max_length=2, choices=DATA_POLICY_CHOICES, default='PU') + data_policy = models.CharField( + max_length=2, choices=DATA_POLICY_CHOICES, default="PU" + ) can_users_change_policy = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -76,7 +123,6 @@ class Organization(UUIDMixin): contact_email = models.EmailField(max_length=200, null=True, blank=True) description = models.TextField(null=True, blank=True) - def save(self, *args, **kwargs): # Aktualizace slug pole na základě názvu, pokud není zadáno if not self.slug: @@ -90,39 +136,44 @@ def get_members(self): return ", ".join([str(user) for user in self.users.all()]) def get_admin_url(self): - return reverse("admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name), args=(self.id,)) - + return reverse( + "admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name), + args=(self.id,), + ) def get_absolute_url(self): - return reverse('organization-detail', args=[str(self.id)]) - + return reverse("organization-detail", args=[str(self.id)]) class OrganizationUser(models.Model): USER_TYPE_CHOICES = [ - ('ME', 'Member'), - ('AD', 'Admin'), - ('OW', 'Owner'), + ("ME", "Member"), + ("AD", "Admin"), + ("OW", "Owner"), ] - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='organization_users') - organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='user_organizations') - user_type = models.CharField(max_length=2, choices=USER_TYPE_CHOICES, default='ME') + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="organization_users", + ) + organization = models.ForeignKey( + Organization, on_delete=models.CASCADE, related_name="user_organizations" + ) + user_type = models.CharField(max_length=2, choices=USER_TYPE_CHOICES, default="ME") class Meta: - unique_together = ('user', 'organization') + unique_together = ("user", "organization") def __str__(self): - return f'{self.user.username}: {self.get_user_type_display()} of {self.organization.name}' - + return f"{self.user.username}: {self.get_user_type_display()} of {self.organization.name}" class CARImodel(UUIDMixin): data = models.JSONField() - class Airports(UUIDMixin): name = models.CharField() code_iata = models.CharField(null=True, unique=True) @@ -138,117 +189,109 @@ class Airports(UUIDMixin): def __str__(self) -> str: return "Airport {} ({})".format(self.code_iata, self.name) + class Flight(UUIDMixin): flight_number = models.CharField() takeoff = models.ForeignKey( - Airports, - on_delete=models.CASCADE, - related_name="takeoff") - + Airports, on_delete=models.CASCADE, related_name="takeoff" + ) departure_time = models.DateTimeField( - verbose_name = _("Scheduled departure time"), + verbose_name=_("Scheduled departure time"), null=True, ) - - land = models.ForeignKey( - Airports, - on_delete=models.CASCADE, - related_name="landing") - + land = models.ForeignKey(Airports, on_delete=models.CASCADE, related_name="landing") def user_directory_path(instance, filename): - return "data/flights/{0}/{1}/path.txt".format(instance.flight_number.rstrip(), instance.departure_time.strftime("%Y-%m-%d %H:%M")) + return "data/flights/{0}/{1}/path.txt".format( + instance.flight_number.rstrip(), + instance.departure_time.strftime("%Y-%m-%d %H:%M"), + ) trajectory_file = models.FileField( verbose_name=_("Trajectory log"), upload_to=user_directory_path, - ) - cari = models.ForeignKey( - CARImodel, - on_delete=models.CASCADE, - null=True, - blank=True - ) + cari = models.ForeignKey(CARImodel, on_delete=models.CASCADE, null=True, blank=True) def get_absolute_url(self): - return reverse('flight-detail', args=[str(self.id)]) - + return reverse("flight-detail", args=[str(self.id)]) + def __str__(self) -> str: - return "Flight {} ({}->{}) @ {}".format(self.flight_number, self.takeoff.code_iata, self.land.code_iata, self.departure_time.strftime("%Y-%m-%d %H:%M")) + return "Flight {} ({}->{}) @ {}".format( + self.flight_number, + self.takeoff.code_iata, + self.land.code_iata, + self.departure_time.strftime("%Y-%m-%d %H:%M"), + ) def save(self, *args, **kwargs): print("ASYNYC..", self) - #async_task(process_flight_entry, self) + # async_task(process_flight_entry, self) super(Flight, self).save(*args, **kwargs) class Meta: - unique_together = ('flight_number', 'departure_time') + unique_together = ("flight_number", "departure_time") class MeasurementDataFlight(UUIDMixin): flight = models.ForeignKey( - Flight, - on_delete=models.CASCADE, - related_name="measurements") + Flight, on_delete=models.CASCADE, related_name="measurements" + ) + class DetectorManufacturer(UUIDMixin): - + name = models.CharField( - max_length = 80, + max_length=80, ) - + url = models.URLField(max_length=200) def __str__(self) -> str: return "Detector manufacturer: {}".format(self.url, self.name) + class DetectorType(UUIDMixin): name = models.CharField( - max_length = 80, + max_length=80, ) - manufacturer = models.ForeignKey( - DetectorManufacturer, - on_delete=models.CASCADE - ) + manufacturer = models.ForeignKey(DetectorManufacturer, on_delete=models.CASCADE) image = models.ImageField( name=_("Detector image"), help_text=_("Detector image"), - upload_to='detector_images', + upload_to="detector_images", null=True, - blank=True + blank=True, ) - url = models.URLField( - max_length=200, - null=True, - blank=True - ) + url = models.URLField(max_length=200, null=True, blank=True) description = MarkdownxField( verbose_name=_("Detector description"), help_text=_("Detector description"), - blank=True + blank=True, ) def get_absolute_url(self): - return reverse('detector-type-view', args=[str(self.id)]) + return reverse("detector-type-view", args=[str(self.id)]) def get_admin_url(self): - return reverse("admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name), args=(self.id,)) + return reverse( + "admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name), + args=(self.id,), + ) def __str__(self) -> str: return "Detector type {} ({})".format(self.name, self.manufacturer.name) - @property def description_formatted(self): return markdownify(self.description) @@ -258,12 +301,9 @@ def formatted_label(self): return f""" {self.name}""" - class DetectorCalib(UUIDMixin): - name = models.CharField( - _("Calibration name") - ) + name = models.CharField(_("Calibration name")) created = models.DateTimeField(auto_now_add=True) @@ -272,29 +312,18 @@ class DetectorCalib(UUIDMixin): on_delete=models.DO_NOTHING, related_name="calibrations", null=True, - default=None + default=None, ) - description = models.TextField( - _("Description of calibration status") - ) + description = models.TextField(_("Description of calibration status")) created = models.DateTimeField(auto_now_add=True) - coef0 = models.FloatField( - _("Coefficient 0 (offset)"), - default=0.0 - ) + coef0 = models.FloatField(_("Coefficient 0 (offset)"), default=0.0) - coef1 = models.FloatField( - _("Coefficient 1, (linear)"), - default=1 - ) + coef1 = models.FloatField(_("Coefficient 1, (linear)"), default=1) - coef2 = models.FloatField( - _("Coefficient 2, (quadratic)"), - default=0.0 - ) + coef2 = models.FloatField(_("Coefficient 2, (quadratic)"), default=0.0) # author = models.ForeignKey( # settings.AUTH_USER_MODEL, @@ -304,12 +333,11 @@ class DetectorCalib(UUIDMixin): def __str__(self) -> str: return f"Calibration '{self.name}' ({self.coef0/1000:.2f}+x*{self.coef1/1000:.2f} KeV), {self.created}, {self.description}" - class Detector(UUIDMixin): sn = models.CharField( - max_length = 80, + max_length=80, ) name = models.CharField( @@ -317,31 +345,29 @@ class Detector(UUIDMixin): max_length=150, ) - type = models.ForeignKey( - DetectorType, - on_delete=models.CASCADE - ) + type = models.ForeignKey(DetectorType, on_delete=models.CASCADE) calib = models.ManyToManyField( DetectorCalib, blank=True, - #name=_("Detector calibration"), + # name=_("Detector calibration"), related_name="detectors", help_text=_("Detector calibration"), - #limit_choices_to=, + # limit_choices_to=, ) manufactured_date = models.DateField( _("Manufactured date"), help_text=_("Date when detector was manufactured"), - null=True, blank=True + null=True, + blank=True, ) data = models.JSONField( _("Detector metadata"), help_text="Detector metadata, used for advanced data processing and maintaining", default=dict, - blank=True + blank=True, ) owner = models.ForeignKey( @@ -349,22 +375,21 @@ class Detector(UUIDMixin): on_delete=models.DO_NOTHING, related_name="detectors", blank=True, - null=True + null=True, ) access = models.ManyToManyField( - Organization, - related_name="detector_access", - blank=True - + Organization, related_name="detector_access", blank=True ) def get_absolute_url(self): - return reverse('detector-view', args=[str(self.id)]) + return reverse("detector-view", args=[str(self.id)]) def __str__(self) -> str: - return "Detector {} ({}), SN:{}".format(self.name, self.type.manufacturer.name, self.sn) - + return "Detector {} ({}), SN:{}".format( + self.name, self.type.manufacturer.name, self.sn + ) + @property def formatted_label(self): return f""" {self.type.name} @@ -374,9 +399,7 @@ def formatted_label(self): class DetectorLogbook(UUIDMixin): detector = models.ForeignKey( - Detector, - on_delete=models.CASCADE, - related_name='logbook' + Detector, on_delete=models.CASCADE, related_name="logbook" ) author = models.ForeignKey( @@ -388,56 +411,113 @@ class DetectorLogbook(UUIDMixin): text = models.TextField( _("Logbook text"), - help_text="Detailed description of activity made on the detector." + help_text="Detailed description of activity made on the detector.", ) + public = models.BooleanField( _("Wish to be visible to everyone?"), - help_text=_("Private logbook will be visible for maintainers of detector and for dosportal admins."), - default=True) + help_text=_( + "Private logbook will be visible for maintainers of detector and for dosportal admins." + ), + default=True, + ) + + ENTRY_TYPE_CHOICES = [ + ("reset", "Reset"), + ("sync", "Sync"), + ("maintenance", "Maintenance"), + ("note", "Note"), + ("location_update", "Location update"), + ("calibration", "Calibration"), + ("other", "Other"), + ] + + SOURCE_CHOICES = [ + ("web", "Web"), + ("api", "API"), + ("qr", "QR"), + ("auto", "Automatic"), + ("other", "Other"), + ] + + entry_type = models.CharField( + max_length=30, + choices=ENTRY_TYPE_CHOICES, + default="note", + help_text=_("Category of the logbook entry."), + ) + + source = models.CharField( + max_length=20, + choices=SOURCE_CHOICES, + default="web", + help_text=_("Origin of the logbook entry."), + ) + + latitude = models.FloatField( + verbose_name=_("Latitude"), + help_text=_("GPS latitude of the location"), + null=True, + blank=True, + ) + + longitude = models.FloatField( + verbose_name=_("Longitude"), + help_text=_("GPS longitude of the location"), + null=True, + blank=True, + ) + + altitude = models.FloatField( + verbose_name=_("Altitude"), + help_text=_("Altitude of the location in meters"), + null=True, + blank=True, + ) + + class Meta: + ordering = ["-created"] + class measurement_campaign(UUIDMixin): name = models.CharField( - _("measurement name"), - max_length=150, - null=True, blank=True + _("measurement name"), max_length=150, null=True, blank=True ) - def __str__(self) -> str: return "Campaign: {}".format(self.name) - class measurement(UUIDMixin): """ Měřením se rozumí sada měření, které analyzují jednu a tu samou věc a jsou změřeny jedním detektorem. Pokud jsou v latedle dva detektory, tak to jsou dvě měření. Pokud je ale měření z jednoho detektoru - přerušeno a navázáno novým záznamem, tak to je celé jedno měření. - + přerušeno a navázáno novým záznamem, tak to je celé jedno měření. + """ - + time_start = models.DateTimeField( - verbose_name = _("Measurement beginning time"), - null=True, blank=True, + verbose_name=_("Measurement beginning time"), + null=True, + blank=True, ) time_end = models.DateTimeField( - verbose_name = _("Measurement beginning time"), - null=True, blank=True, + verbose_name=_("Measurement beginning time"), + null=True, + blank=True, ) time_created = models.DateTimeField( - verbose_name = _("Time of creation"), + verbose_name=_("Time of creation"), null=False, editable=False, - auto_now_add=True + auto_now_add=True, ) - + author = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name = "measurements" + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="measurements" ) name = models.CharField( @@ -445,74 +525,68 @@ class measurement(UUIDMixin): max_length=150, ) - description = MartorField( - _("Measurement description"), - blank = True - ) + description = MartorField(_("Measurement description"), blank=True) public = models.BooleanField( - verbose_name=_("Will be data publicly available"), - default = True + verbose_name=_("Will be data publicly available"), default=True ) # Tohle pole by melo obsahovat nasledujici typy: MEASUREMENT_TYPES = ( - ('D', 'Debug measurement'), - ('S', 'Static measurement'), - ('M', 'Mobile measurement (ground)'), - ('C', 'Civil airborne measurement'), - ('A', 'Special airborne measurement') + ("D", "Debug measurement"), + ("S", "Static measurement"), + ("M", "Mobile measurement (ground)"), + ("C", "Civil airborne measurement"), + ("A", "Special airborne measurement"), ) measurement_type = models.CharField( verbose_name=_("Certain measurement type, enum"), choices=MEASUREMENT_TYPES, default="S", - help_text=_("Type of measurement") + help_text=_("Type of measurement"), ) base_location_lat = models.FloatField(null=True, default=None, blank=True) base_location_lon = models.FloatField(null=True, default=None, blank=True) base_location_alt = models.FloatField(null=True, default=None, blank=True) - def user_directory_path(instance, filename): return "data/user_records/{0}/{1}".format(instance.user.id, filename) location_file = models.FileField( - verbose_name=_("File log"), - upload_to=user_directory_path, - blank = True + verbose_name=_("File log"), upload_to=user_directory_path, blank=True ) def get_absolute_url(self): - return reverse('measurement-detail', args=[str(self.id)]) + return reverse("measurement-detail", args=[str(self.id)]) def __str__(self): - return f'Mereni: {self.name}, Typ: {self.measurement_type}' - + return f"Mereni: {self.name}, Typ: {self.measurement_type}" + flight = models.ForeignKey( Flight, on_delete=models.CASCADE, - related_name = "measurement", - null = True, + related_name="measurement", + null=True, verbose_name=_("Reference na objekt s informacemi o letu"), - blank = True + blank=True, ) - campaings = models.ManyToManyField(measurement_campaign, related_name="Campaigns", blank=True) - + campaings = models.ManyToManyField( + measurement_campaign, related_name="Campaigns", blank=True + ) class Record(UUIDMixin): name = models.CharField( - max_length = 80, + max_length=80, verbose_name=_("Record name"), help_text=_("Name of this record. Short and simple description of record."), null=True, blank=False, - default="Record" + default="Record", ) detector = models.ForeignKey( @@ -520,14 +594,14 @@ class Record(UUIDMixin): on_delete=models.CASCADE, null=True, blank=True, - related_name='records' + related_name="records", ) def user_directory_path(instance, filename): print("USER FILENAME", filename) return "user_records/record_{0}".format(instance.pk) - - def user_directory_path_data(instance, extension='pk'): + + def user_directory_path_data(instance, extension="pk"): print("USER FILENAME", extension) return "user_records/record_{0}.{1}".format(instance.pk, extension) @@ -535,20 +609,21 @@ def user_directory_path_data(instance, extension='pk'): verbose_name=_("File log"), help_text=_("Upload recorded data file form your detector"), upload_to=user_directory_path, - blank=True + blank=True, + validators=[_validate_log_file], ) description = MarkdownxField( verbose_name=_("Description"), help_text=_("Description of the record"), - blank=True + blank=True, ) log_original_filename = models.CharField( - verbose_name = _("Original filename of log file"), + verbose_name=_("Original filename of log file"), null=True, blank=True, - max_length=150 + max_length=150, ) data_file = models.FileField( @@ -556,7 +631,8 @@ def user_directory_path_data(instance, extension='pk'): help_text=_("Processed spectral file"), upload_to=user_directory_path_data, null=True, - blank=True + blank=True, + validators=[_validate_data_file], ) metadata_file = models.FileField( @@ -564,88 +640,85 @@ def user_directory_path_data(instance, extension='pk'): help_text=_("Processed metadata file"), upload_to=user_directory_path_data, null=True, - blank=True + blank=True, + validators=[_validate_metadata_file], ) time_tracked = models.BooleanField( - verbose_name = _("Is time tracked?"), - default = False, - help_text=_("Tick this box if the record is dependent on absolute time. When you need align record to real time.") + verbose_name=_("Is time tracked?"), + default=False, + help_text=_( + "Tick this box if the record is dependent on absolute time. When you need align record to real time." + ), ) time_internal_start = models.FloatField( - verbose_name = _("Internal time start"), + verbose_name=_("Internal time start"), help_text=_("System time of record start"), null=True, blank=True, - default=0 + default=0, ) time_start = models.DateTimeField( - verbose_name = _("Measurement beginning time"), - help_text=("When 'time is tracked', you can set start time of the record beginning. "), + verbose_name=_("Measurement beginning time"), + help_text=( + "When 'time is tracked', you can set start time of the record beginning. " + ), null=True, blank=True, - default=datetime.datetime(2000, 1, 1, 0, 0, 0) + default=datetime.datetime(2000, 1, 1, 0, 0, 0), ) time_of_interest_start = models.FloatField( - verbose_name = _("Time of interest start"), - null=True, - blank=True, - default=None + verbose_name=_("Time of interest start"), null=True, blank=True, default=None ) time_of_interest_end = models.FloatField( - verbose_name = _("Time of interest end"), - null=True, - blank=True, - default=None + verbose_name=_("Time of interest end"), null=True, blank=True, default=None ) created = models.DateTimeField( - verbose_name = _("Time of creation"), + verbose_name=_("Time of creation"), null=False, editable=False, - auto_now_add=True + auto_now_add=True, ) record_duration = models.DurationField( - verbose_name = _("Record duration"), - help_text=_("Duration of record"), - null=True + verbose_name=_("Record duration"), help_text=_("Duration of record"), null=True ) - # Tohle pole by melo obsahovat nasledujici typy: RECORD_TYPES = ( - ('U', 'Unknown'), - ('S', 'Spectral measurements'), - ('E', 'Event measurements'), - ('L', 'Location') + ("U", "Unknown"), + ("S", "Spectral measurements"), + ("E", "Event measurements"), + ("L", "Location"), ) record_type = models.CharField( verbose_name=_("Certain record type, enum"), choices=RECORD_TYPES, default="U", - help_text=_("Type of log file") + help_text=_("Type of log file"), ) metadata = models.JSONField( _("record_metadata"), - help_text=_("record metadata, used for advanced data processing and maintaining"), + help_text=_( + "record metadata, used for advanced data processing and maintaining" + ), default=dict, - blank=True + blank=True, ) - calib = models.ForeignKey( DetectorCalib, on_delete=models.CASCADE, null=True, blank=True, - related_name='records' + related_name="records", ) belongs = models.ForeignKey( @@ -653,75 +726,68 @@ def user_directory_path_data(instance, extension='pk'): on_delete=models.DO_NOTHING, null=True, related_name="records_owning", - help_text=_("Organization, which owns this record. If you are the only owner, please leave this field empty.") - ) - - data_policy = models.CharField( max_length=2, - choices= Organization.DATA_POLICY_CHOICES, - default='PU', - help_text=_("Data policy of this record. Field can be overridden depending by setting of the organisation, that owns this record."), - ) + choices=Organization.DATA_POLICY_CHOICES, + default="PU", + help_text=_( + "Data policy of this record. Field can be overridden depending by setting of the organisation, that owns this record." + ), + ) author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, - null=True, - blank = True, + null=True, + blank=True, ) def save(self, *args, **kwargs): super(Record, self).save(*args, **kwargs) def __str__(self) -> str: - return "record ({}, {}, start {}, {})".format(self.belongs, self.log_original_filename, self.time_start, 0) + return "record ({}, {}, start {}, {})".format( + self.belongs, self.log_original_filename, self.time_start, 0 + ) # def description(self) -> str: # return "Record ({}, {})".format(get_enum_dsc(self.RECORD_TYPES, self.record_type), self.time_start.strftime("%Y-%m-%d_%H:%M")) - def calibration_select_form(self): class selectCalibForm(forms.ModelForm): class Meta: model = Record - #fields = ['calib'] + # fields = ['calib'] exclude = [] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) detector = self.instance.detector if detector: - self.fields['calib'].queryset = detector.calib.all() + self.fields["calib"].queryset = detector.calib.all() else: - self.fields['calib'].queryset = DetectorCalib.objects.none() + self.fields["calib"].queryset = DetectorCalib.objects.none() + return selectCalibForm - + @property def formatted_markdown(self): return markdownify(self.description) - class Trajectory(UUIDMixin): - name = models.CharField( - max_length = 80 - ) + name = models.CharField(max_length=80) - description = models.TextField( - null=True, - blank=True - ) + description = models.TextField(null=True, blank=True) def __str__(self) -> str: return "Trajectory: {}".format(self.name) - class TrajectoryPoint(models.Model): datetime = models.DateTimeField( null=True, blank=True, - verbose_name = _("Point timestamp"), + verbose_name=_("Point timestamp"), ) location = geomodels.PointField( @@ -731,25 +797,26 @@ class TrajectoryPoint(models.Model): ) trajectory = models.ForeignKey( - Trajectory, - on_delete=models.CASCADE, - related_name="points" + Trajectory, on_delete=models.CASCADE, related_name="points" ) def __str__(self) -> str: return "Trajectory point: {}".format(self.trajectory) + from django.contrib.postgres.fields import ArrayField, HStoreField + class SpectrumData(UUIDMixin): """ Model to store energy spectrum data """ + record = models.ForeignKey( - 'record', + "record", on_delete=models.CASCADE, - related_name='spectrum_data', - verbose_name=_("Record") + related_name="spectrum_data", + verbose_name=_("Record"), ) # spectrum = models.JSONField( @@ -762,9 +829,9 @@ class SpectrumData(UUIDMixin): integration = models.FloatField( _("Integration time"), - help_text = _("Duration of last exposition"), - null = True, - blank = True + help_text=_("Duration of last exposition"), + null=True, + blank=True, ) particles = models.IntegerField( @@ -773,27 +840,26 @@ class SpectrumData(UUIDMixin): null=True, blank=True, ) - + location = models.ForeignKey( TrajectoryPoint, on_delete=models.CASCADE, - related_name='spectrum_data', + related_name="spectrum_data", null=True, - blank=True + blank=True, ) time = models.DurationField( verbose_name=_("Time difference"), help_text=_("Time difference from the start of the measurement"), - null=True + null=True, ) def __str__(self) -> str: return f"Spectrum data {self.record.id}" - + def save(self, *args, **kwargs): - #self.particles = sum(self.spectrum) + # self.particles = sum(self.spectrum) - #self.metadata['particles'] = self.particles + # self.metadata['particles'] = self.particles super(SpectrumData, self).save(*args, **kwargs) - diff --git a/DOSPORTAL/services/file_validation.py b/DOSPORTAL/services/file_validation.py new file mode 100644 index 0000000..46016f5 --- /dev/null +++ b/DOSPORTAL/services/file_validation.py @@ -0,0 +1,29 @@ +import os +from django.core.exceptions import ValidationError + +""" +This file will (TODO) contain specific hook implementations for file data validation and parsing logic +""" + + +def validate_uploaded_file(uploaded_file, allowed_extensions=None, max_size_mb=None): + """guard: extension and size""" + ext = os.path.splitext(uploaded_file.name)[1].lower() + if allowed_extensions: + if ext not in allowed_extensions: + allowed = ", ".join(allowed_extensions) + raise ValidationError( + f"Unsupported file extension '{ext}'. Allowed: {allowed}" + ) + + if max_size_mb is not None: + limit_bytes = max_size_mb * 1024 * 1024 + if uploaded_file.size and uploaded_file.size > limit_bytes: + raise ValidationError( + f"File too large. Max size of filetype {ext} is {max_size_mb} MB." + ) + + +def parse_record_file_placeholder(uploaded_file): + # TODO: implement specific parsing/validation ? + return None diff --git a/DOSPORTAL/settings.py b/DOSPORTAL/settings.py index 2fab706..d3ec6ad 100644 --- a/DOSPORTAL/settings.py +++ b/DOSPORTAL/settings.py @@ -21,87 +21,93 @@ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-)&9q7=6szljptu&2a11rq1k-ofhz1s$nxk&t+f=3xk74(vq4jq') +SECRET_KEY = os.getenv( + "SECRET_KEY", "django-insecure-)&9q7=6szljptu&2a11rq1k-ofhz1s$nxk&t+f=3xk74(vq4jq" +) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['*','localhost', '127.0.0.1', '0.0.0.0'] +ALLOWED_HOSTS = ["*", "localhost", "127.0.0.1", "0.0.0.0"] -CSRF_TRUSTED_ORIGINS = ['https://portal.dos.ust.cz', 'https://eurados-demo.dos.ust.cz'] +CSRF_TRUSTED_ORIGINS = [ + "https://portal.dos.ust.cz", + "https://eurados-demo.dos.ust.cz", + # Local dev (Vite) + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://frontend:5173", +] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.postgres', - 'django.contrib.gis', - 'bootstrap5', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.postgres", + "django.contrib.gis", + # 'bootstrap5', + "django_bootstrap5", #'background_task', #'django_json_widget', - 'rest_framework', - 'corsheaders', - 'jquery', - 'import_export', - 'django_select2', - 'martor', - 'django_tables2', - - 'DOSPORTAL', - 'django_q', - 'crispy_forms', - 'crispy_bootstrap5', - 'django_gravatar', - 'markdownx', - 'guardian', - 'prettyjson', - 'storages', - # 'organizations', + "rest_framework", + "corsheaders", + "jquery", + "import_export", + "django_select2", + "martor", + "django_tables2", + "DOSPORTAL", + "django_q", + "crispy_forms", + "crispy_bootstrap5", + "django_gravatar", + "markdownx", + "guardian", + "prettyjson", + "storages", + # 'organizations', ] -MARKDOWNX_MARKDOWN_EXTENSIONS = [ - 'markdown.extensions.extra' -] +MARKDOWNX_MARKDOWN_EXTENSIONS = ["markdown.extensions.extra"] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', + "django.middleware.security.SecurityMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" -CRISPY_TEMPLATE_PACK = 'bootstrap5' +CRISPY_TEMPLATE_PACK = "bootstrap5" -ROOT_URLCONF = 'DOSPORTAL.urls' +ROOT_URLCONF = "DOSPORTAL.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / "templates"], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'DOSPORTAL.wsgi.application' +WSGI_APPLICATION = "DOSPORTAL.wsgi.application" # Database @@ -113,7 +119,7 @@ # 'NAME': BASE_DIR / 'db.sqlite3', # } "default": { - #"ENGINE": "django.db.backends.postgresql", + # "ENGINE": "django.db.backends.postgresql", "ENGINE": "django.contrib.gis.db.backends.postgis", "NAME": os.getenv("POSTGRES_DB", "dosportal"), "USER": os.getenv("POSTGRES_USER", "dosportal_user"), @@ -125,8 +131,8 @@ AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', # this is default - 'guardian.backends.ObjectPermissionBackend', + "django.contrib.auth.backends.ModelBackend", # this is default + "guardian.backends.ObjectPermissionBackend", ) # Password validation @@ -134,16 +140,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -153,98 +159,120 @@ # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -# CORS_ORIGIN_ALLOW_ALL = True +# CORS Configuration - React frontend +CORS_ORIGIN_ALLOW_ALL = False +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", + "http://127.0.0.1:3000", + "http://frontend:3000", + "http://0.0.0.0:3000", + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://frontend:5173", + "http://0.0.0.0:5173", +] + +CORS_ALLOW_CREDENTIALS = True STATICFILES_DIRS = [ Path(__file__).resolve().parent / "static", ] -STATIC_ROOT = BASE_DIR / 'static' -STATIC_URL = 'static/' +STATIC_ROOT = BASE_DIR / "static" +STATIC_URL = "static/" # S3 Configuration -AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID', 'minioadmin') -AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY', 'minioadmin') -AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME', 'dosportal-media') -AWS_S3_ENDPOINT_URL = os.getenv('AWS_S3_ENDPOINT_URL', 'http://minio:9000') -AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME', 'us-east-1') -AWS_S3_CUSTOM_DOMAIN = os.getenv('AWS_S3_CUSTOM_DOMAIN') -AWS_DEFAULT_ACL = os.getenv('AWS_DEFAULT_ACL', 'public-read') -AWS_S3_SIGNATURE_VERSION = os.getenv('AWS_S3_SIGNATURE_VERSION', 's3v4') +AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "minioadmin") +AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "minioadmin") +AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME", "dosportal-media") +AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL", "http://minio:9000") +AWS_S3_REGION_NAME = os.getenv("AWS_S3_REGION_NAME", "us-east-1") +AWS_S3_CUSTOM_DOMAIN = os.getenv("AWS_S3_CUSTOM_DOMAIN") +AWS_DEFAULT_ACL = os.getenv("AWS_DEFAULT_ACL", "public-read") +AWS_S3_SIGNATURE_VERSION = os.getenv("AWS_S3_SIGNATURE_VERSION", "s3v4") # Use S3 for media files -DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' -MEDIA_URL = f'{AWS_S3_ENDPOINT_URL}/{AWS_STORAGE_BUCKET_NAME}/' +DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" +MEDIA_URL = f"{AWS_S3_ENDPOINT_URL}/{AWS_STORAGE_BUCKET_NAME}/" REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" ] } - - DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5-responsive.html" DJANGO_TABLES2_TABLE_ATTRS = { - 'class': 'table table-hover', - 'thead': { - 'class': 'table-light', + "class": "table table-hover", + "thead": { + "class": "table-light", }, } -MARTOR_THEME = 'bootstrap' +MARTOR_THEME = "bootstrap" MARTOR_ENABLE_CONFIGS = { - 'emoji': 'true', # to enable/disable emoji icons. - 'imgur': 'false', # to enable/disable imgur/custom uploader. - 'mention': 'false', # to enable/disable mention - 'jquery': 'true', # to include/revoke jquery (require for admin default django) - 'living': 'false', # to enable/disable live updates in preview - 'spellcheck': 'false', # to enable/disable spellcheck in form textareas - 'hljs': 'true', # to enable/disable hljs highlighting in preview + "emoji": "true", # to enable/disable emoji icons. + "imgur": "false", # to enable/disable imgur/custom uploader. + "mention": "false", # to enable/disable mention + "jquery": "true", # to include/revoke jquery (require for admin default django) + "living": "false", # to enable/disable live updates in preview + "spellcheck": "false", # to enable/disable spellcheck in form textareas + "hljs": "true", # to enable/disable hljs highlighting in preview } MARTOR_TOOLBAR_BUTTONS = [ - 'bold', 'italic', 'horizontal', 'heading', 'pre-code', - 'blockquote', 'unordered-list', 'ordered-list', - 'link', 'image-link', 'image-upload', 'emoji', - 'direct-mention', 'toggle-maximize', 'help' + "bold", + "italic", + "horizontal", + "heading", + "pre-code", + "blockquote", + "unordered-list", + "ordered-list", + "link", + "image-link", + "image-upload", + "emoji", + "direct-mention", + "toggle-maximize", + "help", ] MARTOR_ENABLE_LABEL = False Q_CLUSTER = { - 'name': 'Worker', - 'retry': 5, - 'workers': 4, - 'recycle': 500, - 'timeout': 60, - 'compress': True, + "name": "Worker", + "workers": 4, + "timeout": 60, + "retry": 90, + "recycle": 500, + "compress": True, #'save_limit': 250, #'queue_limit': 500, - 'cpu_affinity': 1, - 'label': 'Async dosportal worker', + "cpu_affinity": 1, + "label": "Async dosportal worker", #'orm': 'default', - 'redis': { - 'host': '10.5.0.7', - 'port': 6379, - 'db': 0, - } + "redis": { + "host": "10.5.0.7", + "port": 6379, + "db": 0, + }, } diff --git a/DOSPORTAL/templates/base.html b/DOSPORTAL/templates/base.html index c5fc0f8..6aa4ac1 100644 --- a/DOSPORTAL/templates/base.html +++ b/DOSPORTAL/templates/base.html @@ -6,7 +6,7 @@ {% block title %} DOSPORTAL {% endblock %} - {% load bootstrap5 %} + {% load django_bootstrap5 %} {% bootstrap_css %} {% bootstrap_javascript %} diff --git a/DOSPORTAL/templates/measurements/measurement_detail.html b/DOSPORTAL/templates/measurements/measurement_detail.html index 1bc1594..c942ec2 100644 --- a/DOSPORTAL/templates/measurements/measurement_detail.html +++ b/DOSPORTAL/templates/measurements/measurement_detail.html @@ -59,7 +59,7 @@
diff --git a/DOSPORTAL/templates/registration/signup.html b/DOSPORTAL/templates/registration/signup.html new file mode 100644 index 0000000..86e9e8f --- /dev/null +++ b/DOSPORTAL/templates/registration/signup.html @@ -0,0 +1,111 @@ +{% load crispy_forms_tags %} +{% load static %} + + + + + + + + + + DOSPORTAL: Sign Up + + {% load django_bootstrap5 %} + {% bootstrap_css %} + {% bootstrap_javascript %} + + + + + + + + +
+
+
{% csrf_token %} + +

Create your account

+ + {% if form.non_field_errors %} + + {% endif %} + + {{ form | crispy }} + + +

Already have an account? Log in here

+

Universal Scientific Technologies s.r.o.
© 2023–2024

+
+
+
+ + + + diff --git a/DOSPORTAL/templates/user/login.html b/DOSPORTAL/templates/user/login.html index 64f7316..d2bad9a 100644 --- a/DOSPORTAL/templates/user/login.html +++ b/DOSPORTAL/templates/user/login.html @@ -12,7 +12,7 @@ DOSPORTAL: Login - {% load bootstrap5 %} + {% load django_bootstrap5 %} {% bootstrap_css %} {% bootstrap_javascript %} @@ -113,6 +113,7 @@

Please sign in ...

{{ form | crispy }} +

Don't have an account? Sign up here

Universal Scientific Technologies s.r.o.
© 2023–2024

diff --git a/DOSPORTAL/urls.py b/DOSPORTAL/urls.py index 0db7690..acfd909 100644 --- a/DOSPORTAL/urls.py +++ b/DOSPORTAL/urls.py @@ -7,77 +7,108 @@ import uuid -from .users.views_users import user_profile, login_view +from .users.views_users import user_profile, login_view, signup_view, logout_view from .users import urls as user_urls -from .views import MeasurementsListView, MeasurementDetailView, MeasurementNewView, MeasurementNewView, MeasurementDataView, measuredDataGet, measuredSpectraGet, MeasurementRecordNewView -from .views_detectors import DetectorView, DetectorEditView,DetectorOverview, DetectorNewLogbookRecord, DetectorTypeView, DetectorCalibDetailView +from .views import ( + MeasurementsListView, + MeasurementDetailView, + MeasurementNewView, + MeasurementNewView, + MeasurementDataView, + measuredDataGet, + measuredSpectraGet, + MeasurementRecordNewView, +) +from .views_detectors import ( + DetectorView, + DetectorEditView, + DetectorOverview, + DetectorNewLogbookRecord, + DetectorTypeView, + DetectorCalibDetailView, +) from .views_flights import FlightView -from .views_record import RecordsListView, RecordView, RecordNewView, GetSpectrum, GetEvolution, GetHistogram, GetTelemetry, CalcDSI +from .views_record import ( + RecordsListView, + RecordView, + RecordNewView, + GetSpectrum, + GetEvolution, + GetHistogram, + GetTelemetry, + CalcDSI, +) -#from organizations.backends import invitation_backend +# from organizations.backends import invitation_backend urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), path("accounts/", include("django.contrib.auth.urls")), - - path('login/', login_view, name='login'), - path('markdownx/', include('markdownx.urls')), - - #path(r'accounts/', include('organizations.urls')), - #path(r'invitations/', include(invitation_backend().get_urls())), - - #path('user/', user_profile, name='profile'), - #path('user/', user_profile, name='user_profile'), - - path('user/', include('DOSPORTAL.users.urls')), - path('organization/', include('DOSPORTAL.PART_organizations.urls')), - #path('account/', include('DOSPORTAL.users.urls')), - + path("login/", login_view, name="login"), + path("signup/", signup_view, name="signup"), + path("logout/", logout_view, name="logout"), + path("markdownx/", include("markdownx.urls")), + # path(r'accounts/', include('organizations.urls')), + # path(r'invitations/', include(invitation_backend().get_urls())), + # path('user/', user_profile, name='profile'), + # path('user/', user_profile, name='user_profile'), + path("user/", include("DOSPORTAL.users.urls")), + path("organization/", include("DOSPORTAL.PART_organizations.urls")), + # path('account/', include('DOSPORTAL.users.urls')), path("measurements/", MeasurementsListView.as_view(), name="measurements"), - path("measurement/new/", MeasurementNewView, name='measurement-new'), - path('measurement//new/', MeasurementRecordNewView, name="record-upload"), - path('measurement//visualizate/', MeasurementDataView, name="measurement-data-view"), + path("measurement/new/", MeasurementNewView, name="measurement-new"), + path("measurement//new/", MeasurementRecordNewView, name="record-upload"), + path( + "measurement//visualizate/", + MeasurementDataView, + name="measurement-data-view", + ), # path('measurement//metadata/', MeasurementDataView, name="measurement-data-view"), - path('measurement//measured_data/', measuredDataGet, name="measurement-data-get"), - path('measurement//measured_evolution/', measuredDataGet, name="measurement-evolution-get"), - path('measurement//measured_spectra/', measuredSpectraGet, name="measurement-spectra-get"), - path('measurement//', MeasurementDetailView, name='measurement-detail'), - - path('calibration//', DetectorCalibDetailView.as_view(), name='calibration-detail'), - - path('records/', RecordsListView, name='records'), - - path('record/new/', RecordNewView, name='record-new'), - path('record//', RecordView, name='record-view'), - path('record//get_spectrum/', GetSpectrum, name='record-GetSpectrum'), - path('record//get_evolution/', GetEvolution, name='record-GetEvolution'), - path('record//get_histogram/', GetHistogram, name='record-GetHistogram'), - path('record//get_telemetry/', GetTelemetry, name='record-GetTelemetry'), - path('record//calc_dsi/', CalcDSI, name='record-CalcDSI'), - - - path('flight//', FlightView, name='flight-detail'), - - path('detector//new_logbook_record', DetectorNewLogbookRecord), - path('detector/new/', DetectorEditView, name="detector-new"), - path('detectors/', DetectorOverview.as_view(), name="detector-overview"), - path('detector//edit/', DetectorEditView, name="detector-edit"), - path('detector//', DetectorView, name="detector-view"), - - path('detector_type//', DetectorTypeView, name="detector-type-view"), - + path( + "measurement//measured_data/", + measuredDataGet, + name="measurement-data-get", + ), + path( + "measurement//measured_evolution/", + measuredDataGet, + name="measurement-evolution-get", + ), + path( + "measurement//measured_spectra/", + measuredSpectraGet, + name="measurement-spectra-get", + ), + path("measurement//", MeasurementDetailView, name="measurement-detail"), + path( + "calibration//", + DetectorCalibDetailView.as_view(), + name="calibration-detail", + ), + path("records/", RecordsListView, name="records"), + path("record/new/", RecordNewView, name="record-new"), + path("record//", RecordView, name="record-view"), + path("record//get_spectrum/", GetSpectrum, name="record-GetSpectrum"), + path("record//get_evolution/", GetEvolution, name="record-GetEvolution"), + path("record//get_histogram/", GetHistogram, name="record-GetHistogram"), + path("record//get_telemetry/", GetTelemetry, name="record-GetTelemetry"), + path("record//calc_dsi/", CalcDSI, name="record-CalcDSI"), + path("flight//", FlightView, name="flight-detail"), + path("detector//new_logbook_record", DetectorNewLogbookRecord), + path("detector/new/", DetectorEditView, name="detector-new"), + path("detectors/", DetectorOverview.as_view(), name="detector-overview"), + path("detector//edit/", DetectorEditView, name="detector-edit"), + path("detector//", DetectorView, name="detector-view"), + path("detector_type//", DetectorTypeView, name="detector-type-view"), path("select2/", include("django_select2.urls")), - path('martor/', include('martor.urls')), - - path('analysis/', TemplateView.as_view(template_name='home.html'), name='analysis'), - - #path("record/"), - - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), - path('api/', include('api.urls')), - path('', TemplateView.as_view(template_name='home.html'), name='home'), + path("martor/", include("martor.urls")), + path("analysis/", TemplateView.as_view(template_name="home.html"), name="analysis"), + # path("record/"), + path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), + path("api/", include("api.urls")), + path("", TemplateView.as_view(template_name="home.html"), name="home"), ] -urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/DOSPORTAL/users/views_users.py b/DOSPORTAL/users/views_users.py index bdd40f2..1473209 100644 --- a/DOSPORTAL/users/views_users.py +++ b/DOSPORTAL/users/views_users.py @@ -3,43 +3,64 @@ from django import forms from django.http import HttpResponse, JsonResponse from django.views import generic -from ..models import (DetectorManufacturer, measurement, - Record, Detector, DetectorType) +from ..models import DetectorManufacturer, measurement, Record, Detector, DetectorType from django.shortcuts import get_object_or_404, redirect, render from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User - -from django.contrib.auth import authenticate, login +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth import authenticate, login, logout from ..forms import LoginForm + @login_required -def user_profile(request, username = None): +def user_profile(request, username=None): if username is None: - return redirect('user_profile', username=request.user.username) - - user = get_object_or_404(User, username = username) - #user = request.user - context = { - 'user': user - } - - return render(request, 'user/user_profile.html', context) + return redirect("user_profile", username=request.user.username) + user = get_object_or_404(User, username=username) + # user = request.user + context = {"user": user} + return render(request, "user/user_profile.html", context) def login_view(request): - if request.method == 'POST': + if request.method == "POST": form = LoginForm(request.POST) if form.is_valid(): - user = authenticate(request, username=form.cleaned_data['username'], password=form.cleaned_data['password']) + user = authenticate( + request, + username=form.cleaned_data["username"], + password=form.cleaned_data["password"], + ) if user is not None: login(request, user) - return redirect('home') + return redirect("home") else: - return render(request, 'user/login.html', {'form': form, 'error': 'Invalid username or password'}) + return render( + request, + "user/login.html", + {"form": form, "error": "Invalid username or password"}, + ) else: form = LoginForm() - return render(request, 'user/login.html', {'form': form}) \ No newline at end of file + return render(request, "user/login.html", {"form": form}) + + +def signup_view(request): + if request.method == "POST": + form = UserCreationForm(request.POST) + if form.is_valid(): + user = form.save() + login(request, user, backend="django.contrib.auth.backends.ModelBackend") + return redirect("home") + else: + form = UserCreationForm() + return render(request, "registration/signup.html", {"form": form}) + + +def logout_view(request): + logout(request) + return redirect("home") diff --git a/api/serializers.py b/api/serializers.py index b1e2dc1..6b4f004 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,26 +1,74 @@ from rest_framework import serializers -from DOSPORTAL.models import measurement, Record, Detector +from DOSPORTAL.models import ( + measurement, + Record, + Detector, + DetectorLogbook, + DetectorType, + DetectorManufacturer, + Organization, + User, +) +class DetectorManufacturerSerializer(serializers.ModelSerializer): + class Meta: + model = DetectorManufacturer + fields = ("id", "name", "url") + + +class DetectorTypeSerializer(serializers.ModelSerializer): + manufacturer = DetectorManufacturerSerializer(read_only=True) + + class Meta: + model = DetectorType + fields = ("id", "name", "manufacturer", "url", "description") + + +class OrganizationSummarySerializer(serializers.ModelSerializer): + class Meta: + model = Organization + fields = ("id", "name", "slug") + + +class UserSummarySerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ("id", "username", "first_name", "last_name") class DetectorSerializer(serializers.ModelSerializer): + type = DetectorTypeSerializer(read_only=True) + owner = OrganizationSummarySerializer(read_only=True) + class Meta: model = Detector - fields = '__all__' + fields = "__all__" + class RecordSerializer(serializers.ModelSerializer): - #detector = DetectorSerializer(read_only = True, many=True) + # detector = DetectorSerializer(read_only = True, many=True) class Meta: model = Record - #fields = ['id', 'name', 'description', ] - fields = '__all__' + # fields = ['id', 'name', 'description', ] + fields = "__all__" + + +class DetectorLogbookSerializer(serializers.ModelSerializer): + author = UserSummarySerializer(read_only=True) + + class Meta: + model = DetectorLogbook + fields = "__all__" + read_only_fields = ["id", "author", "created"] + class MeasurementsSerializer(serializers.ModelSerializer): - records = RecordSerializer(read_only = True, many=True) + records = RecordSerializer(read_only=True, many=True) + class Meta: model = measurement - fields = '__all__' - #fields = ('id', 'name') - #exclude = () \ No newline at end of file + fields = "__all__" + # fields = ('id', 'name') + # exclude = () diff --git a/api/urls.py b/api/urls.py index c2fcb34..47b88a0 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,8 +1,11 @@ from django.urls import path -from . import views +from . import views urlpatterns = [ - path('measurement/', views.MeasurementsGet), - path('measurement/add/', views.MeasurementsPost), - path('record/', views.RecordGet), -] \ No newline at end of file + path("measurement/", views.MeasurementsGet), + path("measurement/add/", views.MeasurementsPost), + path("record/", views.RecordGet), + path("detector/", views.DetectorGet), + path("logbook/", views.DetectorLogbookGet), + path("logbook/add/", views.DetectorLogbookPost), +] diff --git a/api/views.py b/api/views.py index 2ea3c4a..e9bed78 100644 --- a/api/views.py +++ b/api/views.py @@ -1,28 +1,108 @@ from rest_framework.response import Response -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.decorators import api_view, permission_classes +from rest_framework import status -from DOSPORTAL.models import measurement, Record -from .serializers import MeasurementsSerializer, RecordSerializer +from django.utils.dateparse import parse_datetime +from DOSPORTAL.models import measurement, Record, DetectorLogbook, Detector +from .serializers import ( + MeasurementsSerializer, + RecordSerializer, + DetectorLogbookSerializer, + DetectorSerializer, +) -@api_view(['GET']) -@permission_classes((AllowAny, )) + +@api_view(["GET"]) +@permission_classes((AllowAny,)) def MeasurementsGet(request): items = measurement.objects.all() serializer = MeasurementsSerializer(items, many=True) return Response(serializer.data) -@api_view(['POST']) -@permission_classes((AllowAny, )) + +@api_view(["POST"]) +@permission_classes((AllowAny,)) def MeasurementsPost(request): serializer = MeasurementsSerializer(data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) -@api_view(['GET']) -@permission_classes((AllowAny, )) + +@api_view(["GET"]) +@permission_classes((AllowAny,)) def RecordGet(request): items = Record.objects.all() serializer = RecordSerializer(items, many=True) - return Response(serializer.data) \ No newline at end of file + return Response(serializer.data) + + +@api_view(["GET"]) +@permission_classes((IsAuthenticated,)) +def DetectorGet(request): + items = Detector.objects.select_related("type__manufacturer", "owner").all() + serializer = DetectorSerializer(items, many=True) + return Response(serializer.data) + + +@api_view(["GET"]) +@permission_classes((IsAuthenticated,)) +def DetectorLogbookGet(request): + items = DetectorLogbook.objects.select_related("detector", "author").all() + + detector_id = request.query_params.get("detector") + if detector_id: + items = items.filter(detector_id=detector_id) + + entry_type = request.query_params.get("entry_type") + if entry_type: + items = items.filter(entry_type=entry_type) + + date_from = request.query_params.get("date_from") + date_to = request.query_params.get("date_to") + + if date_from: + parsed_from = parse_datetime(date_from) + if parsed_from: + items = items.filter(created__gte=parsed_from) + + if date_to: + parsed_to = parse_datetime(date_to) + if parsed_to: + items = items.filter(created__lte=parsed_to) + + serializer = DetectorLogbookSerializer(items, many=True) + return Response(serializer.data) + + +@api_view(["POST"]) +@permission_classes((IsAuthenticated,)) +def DetectorLogbookPost(request): + + data = dict(request.data) + data["author"] = request.user.id + + detector_id = data.get("detector") + if detector_id: + try: + detector = Detector.objects.get(id=detector_id) + user_has_access = ( + detector.owner and request.user in detector.owner.users.all() + ) or detector.access.filter(users=request.user).exists() + + if not user_has_access: + return Response( + {"detail": "Access to the detector denied."}, + status=status.HTTP_403_FORBIDDEN, + ) + except Detector.DoesNotExist: + return Response( + {"detail": "Detektor not found."}, status=status.HTTP_404_NOT_FOUND + ) + + serializer = DetectorLogbookSerializer(data=data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/Dockerfile b/backend.Dockerfile similarity index 89% rename from Dockerfile rename to backend.Dockerfile index 3b325d1..beaab30 100644 --- a/Dockerfile +++ b/backend.Dockerfile @@ -1,3 +1,5 @@ +# Django backend + FROM python:3.13-alpine ENV PYTHONDONTWRITEBYTECODE=1 @@ -13,7 +15,9 @@ RUN apk add --no-cache \ binutils \ proj-dev \ gdal-dev \ - gdal + gdal \ + geos \ + geos-dev WORKDIR /DOSPORTAL COPY requirements.txt /DOSPORTAL/ diff --git a/docker-compose.yml b/docker-compose.yml index e24fca5..58e5c39 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,9 +37,10 @@ services: inet: ipv4_address: 10.5.0.5 - web: + backend: build: context: . + dockerfile: backend.Dockerfile network: host # platform: linux/amd64 # command: python manage.py runserver 0.0.0.0:8000 @@ -60,12 +61,14 @@ services: # default: worker: - build: . + build: + context: . + dockerfile: backend.Dockerfile entrypoint: python3 manage.py qcluster volumes: - .:/DOSPORTAL depends_on: - - web + - backend - db_dosportal - redis networks: @@ -98,3 +101,25 @@ services: networks: inet: ipv4_address: 10.5.0.9 + + frontend: + build: + context: . + dockerfile: frontend.Dockerfile + ports: + - "5173:5173" + volumes: + - ./frontend:/app/frontend + - /app/frontend/node_modules + environment: + - NODE_ENV=development + depends_on: + - backend + networks: + inet: + ipv4_address: 10.5.0.10 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5173"] + interval: 10s + timeout: 5s + retries: 5 diff --git a/frontend.Dockerfile b/frontend.Dockerfile new file mode 100644 index 0000000..300f21a --- /dev/null +++ b/frontend.Dockerfile @@ -0,0 +1,18 @@ +# Django frontend + +FROM node:20-alpine + +WORKDIR /app/frontend + +COPY frontend/package*.json ./ + +RUN npm install + +COPY frontend/ ./ + +COPY frontend/entrypoint.sh /usr/local/bin/frontend-entrypoint.sh +RUN chmod +x /usr/local/bin/frontend-entrypoint.sh + +EXPOSE 5173 + +ENTRYPOINT ["/usr/local/bin/frontend-entrypoint.sh"] diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..33b4012 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:8100/api \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/entrypoint.sh b/frontend/entrypoint.sh new file mode 100644 index 0000000..95b4dae --- /dev/null +++ b/frontend/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Etrypoint for frontend dockerfile + +set -e + +cd /app/frontend + +if [ ! -d node_modules ] || [ -z "$(ls -A node_modules 2>/dev/null)" ]; then + echo "[frontend] Installing dependencies..." + npm install +fi + +echo "[frontend] Starting dev server..." +exec npm run dev -- --host 0.0.0.0 --port 5173 diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..072a57e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..d7edece --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3091 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "npm:rolldown-vite@7.2.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "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" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.97.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.97.0.tgz", + "integrity": "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.97.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.97.0.tgz", + "integrity": "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.50.tgz", + "integrity": "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.50.tgz", + "integrity": "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.50.tgz", + "integrity": "sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.50.tgz", + "integrity": "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.50.tgz", + "integrity": "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.50.tgz", + "integrity": "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.50.tgz", + "integrity": "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.50.tgz", + "integrity": "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.50.tgz", + "integrity": "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.50.tgz", + "integrity": "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.50.tgz", + "integrity": "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.50.tgz", + "integrity": "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-ia32-msvc": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.50.tgz", + "integrity": "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.50.tgz", + "integrity": "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", + "integrity": "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/type-utils": "8.52.0", + "@typescript-eslint/utils": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.52.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz", + "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.52.0.tgz", + "integrity": "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.52.0", + "@typescript-eslint/types": "^8.52.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.52.0.tgz", + "integrity": "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.52.0.tgz", + "integrity": "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.52.0.tgz", + "integrity": "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/utils": "8.52.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.52.0.tgz", + "integrity": "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.52.0.tgz", + "integrity": "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.52.0", + "@typescript-eslint/tsconfig-utils": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "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" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.52.0.tgz", + "integrity": "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.52.0.tgz", + "integrity": "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.52.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "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" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "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, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "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", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "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" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "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" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", + "integrity": "sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.1.tgz", + "integrity": "sha512-vSrQHWlJ5DCfyrhgo0k6zViOe9ToK8uT5XGSmnuC2R3/g261IdIMpZVqfjD6vWSXdnf5Czs4VA/V60oVR6/jnA==", + "license": "MIT", + "dependencies": { + "react-router": "7.1.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.50.tgz", + "integrity": "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.97.0", + "@rolldown/pluginutils": "1.0.0-beta.50" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.50", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", + "@rolldown/binding-darwin-x64": "1.0.0-beta.50", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.50", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz", + "integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "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" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.52.0.tgz", + "integrity": "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.52.0", + "@typescript-eslint/parser": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/utils": "8.52.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "name": "rolldown-vite", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.2.5.tgz", + "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.97.0", + "fdir": "^6.5.0", + "lightningcss": "^1.30.2", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rolldown": "1.0.0-beta.50", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "esbuild": "^0.25.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "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" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b98ac80 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "npm:rolldown-vite@7.2.5" + }, + "overrides": { + "vite": "npm:rolldown-vite@7.2.5" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..de76197 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,315 @@ +:root { + --bg: #ffffff; + --card: #ffffff; + --text: #111827; + --muted: #6b7280; + --primary: #0d6efd; /* bootstrap primary */ + --green: #198754; /* bootstrap success */ + --red: #dc3545; /* bootstrap danger */ + --border: #e5e7eb; +} + +* { + box-sizing: border-box; +} + +html, body, #root { + height: 100%; +} + +body { + margin: 0; + color: var(--text); + background: var(--bg); + font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, + Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; +} + +.page { + min-height: 100vh; + position: relative; + overflow-x: hidden; +} + +.bg { display: none; } + +.auth-layout { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: 1fr 1.2fr; + gap: 2rem; + max-width: 1100px; + margin: 0 auto; + padding: 3rem 1.25rem; +} + +.auth-layout.single { + max-width: 520px; + grid-template-columns: 1fr; +} + +.content { + position: relative; + z-index: 1; + max-width: 1100px; + margin: 0 auto; + padding: 2.5rem 1.25rem 3rem; + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.hero { + background: var(--card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1.5rem; + display: flex; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.hero h1 { + margin: 0.15rem 0 0.35rem 0; +} + +.hero-actions { + display: flex; + gap: 0.5rem; + align-items: center; + flex-wrap: wrap; +} + +.eyebrow { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.8rem; + color: var(--muted); + margin: 0; +} + +@media (max-width: 880px) { + .auth-layout { + grid-template-columns: 1fr; + padding: 2rem 1rem; + } +} + +.login-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 2rem; + box-shadow: 0 6px 18px rgba(0,0,0,0.08); +} + +.brand { + margin: 0 0 0.25rem 0; + letter-spacing: 0.04em; +} + +.subtitle { + margin-top: 0; + color: var(--muted); + font-size: 0.95rem; +} + +.login-form { + margin-top: 1.5rem; + display: grid; + gap: 1rem; +} + +.field label { + display: block; + margin-bottom: 0.5rem; + color: var(--muted); + font-size: 0.9rem; + text-align: right; +} + +.field input { + width: 100%; + padding: 0.75rem 0.9rem; + border: 1px solid #ced4da; + border-radius: 8px; + background: #ffffff; + color: var(--text); + outline: none; +} + +.field input:focus { + border-color: #86b7fe; + box-shadow: 0 0 0 0.2rem rgba(13,110,253,0.25); +} + +button.primary { + padding: 0.75rem 1rem; + border-radius: 8px; + border: 1px solid var(--green); + background: var(--green); + color: #ffffff; + cursor: pointer; +} + +button.primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.help { + margin-top: 0.75rem; + color: var(--muted); +} + +.error { + margin-top: 0.75rem; + color: var(--red); +} + +.success { + margin-top: 0.75rem; + color: var(--green); +} + +.panel { + background: var(--card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1.25rem; + min-height: 340px; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.75rem; +} + +.panel-header button { + padding: 0.5rem 0.8rem; + border-radius: 8px; + border: 1px solid var(--primary); + background: var(--primary); + color: #ffffff; + cursor: pointer; +} + +.panel-body { + text-align: left; +} + +.logbook-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 0.75rem; +} + +.logbook-item { + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.9rem 1rem; + background: #ffffff; +} + +.logbook-item .meta { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; + color: var(--muted); +} + +.badge { + display: inline-block; + padding: 0.15rem 0.5rem; + border: 1px solid #d1d5db; + border-radius: 9999px; + font-size: 0.75rem; + color: var(--text); +} + +.text { + margin: 0.2rem 0 0.5rem 0; +} + +.maplink { + font-size: 0.85rem; + color: var(--primary); +} + +.muted { + color: var(--muted); + margin: 0; +} + +/* Navbar */ +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.25rem; + background: #ffffff; + border-bottom: 1px solid var(--border); + z-index: 20; +} + +.nav-left, .nav-right { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.nav-brand { + font-weight: 700; + letter-spacing: 0.03em; + color: var(--text); +} + +.nav-link { + color: var(--muted); + text-decoration: none; + padding: 0.35rem 0.65rem; + border-radius: 8px; + transition: background 0.15s ease, color 0.15s ease; +} + +.nav-link:hover { + background: rgba(13,110,253,0.08); + color: var(--text); +} + +.pill { + color: #ffffff; + text-decoration: none; + padding: 0.35rem 0.75rem; + border-radius: 999px; + border: 1px solid var(--primary); + background: var(--primary); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.95rem; +} + +.pill.ghost { + background: transparent; + color: var(--text); + border-color: #d1d5db; +} + +.pill:hover { + filter: brightness(1.05); +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..e7d143d --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,30 @@ +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' +import './App.css' +import { Navbar } from './components/Navbar' +import { useAuth } from './hooks/useAuth' +import { HomePage } from './pages/HomePage' +import { LoginPage } from './pages/LoginPage' +import { LogbooksPage } from './pages/LogbooksPage' +import { DetectorLogbookPage } from './pages/DetectorLogbookPage' + +function App() { + const { API_BASE, ORIGIN_BASE, isAuthed, login, logout } = useAuth() + + return ( + + + + : } + /> + } /> + } /> + } /> + } /> + + + ) +} + +export default App diff --git a/frontend/src/assets/img/SPACEDOS01.jpg b/frontend/src/assets/img/SPACEDOS01.jpg new file mode 100644 index 0000000..50db389 Binary files /dev/null and b/frontend/src/assets/img/SPACEDOS01.jpg differ diff --git a/frontend/src/assets/img/login_background.jpg b/frontend/src/assets/img/login_background.jpg new file mode 100644 index 0000000..759fda0 Binary files /dev/null and b/frontend/src/assets/img/login_background.jpg differ diff --git a/frontend/src/assets/img/login_background.png b/frontend/src/assets/img/login_background.png new file mode 100644 index 0000000..0126dc6 Binary files /dev/null and b/frontend/src/assets/img/login_background.png differ diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx new file mode 100644 index 0000000..416b7fa --- /dev/null +++ b/frontend/src/components/Navbar.tsx @@ -0,0 +1,41 @@ +import { Link } from 'react-router-dom' + +export const Navbar = ({ + isAuthed, + onLogout, +}: { + isAuthed: boolean + onLogout: () => Promise +}) => { + return ( + + ) +} diff --git a/frontend/src/components/PageLayout.tsx b/frontend/src/components/PageLayout.tsx new file mode 100644 index 0000000..f31c96c --- /dev/null +++ b/frontend/src/components/PageLayout.tsx @@ -0,0 +1,32 @@ +interface PageLayoutProps { + children: React.ReactNode + backgroundImage?: string +} + +export const PageLayout = ({ children, backgroundImage }: PageLayoutProps) => { + return ( +
+
+ {children} +
+
+ ) +} diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts new file mode 100644 index 0000000..19adb1d --- /dev/null +++ b/frontend/src/hooks/useAuth.ts @@ -0,0 +1,73 @@ +import { useMemo, useState } from 'react' + +const getCookie = (name: string) => { + const value = `; ${document.cookie}` + const parts = value.split(`; ${name}=`) + if (parts.length === 2) return parts.pop()!.split(';').shift() || '' + return '' +} + +const ensureCsrfCookie = async (originBase: string) => { + await fetch(`${originBase}/login/`, { method: 'GET', credentials: 'include' }) +} + +export const useAuth = () => { + const { API_BASE, ORIGIN_BASE } = useMemo(() => { + const api = (import.meta as any).env.VITE_API_URL || 'http://web:8000/api' + return { + API_BASE: api, + ORIGIN_BASE: api.replace(/\/?api\/?$/, ''), + } + }, []) + + const [isAuthed, setIsAuthed] = useState(false) + + const login = async (username: string, password: string) => { + await ensureCsrfCookie(ORIGIN_BASE) + const csrftoken = getCookie('csrftoken') + + const form = new URLSearchParams() + form.set('username', username) + form.set('password', password) + + const res = await fetch(`${ORIGIN_BASE}/login/`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRFToken': csrftoken || '', + }, + body: form.toString(), + redirect: 'follow', + }) + + if (res.ok || res.status === 302) { + setIsAuthed(true) + return + } + + const txt = await res.text() + throw new Error(`Login failed (HTTP ${res.status}): ${txt.slice(0, 200)}`) + } + + const logout = async () => { + await ensureCsrfCookie(ORIGIN_BASE) + const csrftoken = getCookie('csrftoken') + await fetch(`${ORIGIN_BASE}/logout/`, { + method: 'GET', + credentials: 'include', + headers: { + 'X-CSRFToken': csrftoken || '', + }, + }) + setIsAuthed(false) + } + + return { + API_BASE, + ORIGIN_BASE, + isAuthed, + login, + logout, + } +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..5fa83c0 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,78 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #e5e7eb; + color: #111827; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +link { + padding: 0.75rem 1rem; + border-radius: 8px; + border: 1px solid var(--green); + background: var(--green); + color: #ffffff; + cursor: pointer; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/pages/DetectorLogbookPage.tsx b/frontend/src/pages/DetectorLogbookPage.tsx new file mode 100644 index 0000000..3456d10 --- /dev/null +++ b/frontend/src/pages/DetectorLogbookPage.tsx @@ -0,0 +1,214 @@ +import { useState, useEffect } from 'react' +import { useParams, Link } from 'react-router-dom' +import { PageLayout } from '../components/PageLayout' +import type { LogbookItem } from '../types' +import logbookBg from '../assets/img/SPACEDOS01.jpg' + +interface Detector { + id: string + name: string + sn: string + type: { + name: string + manufacturer: { + name: string + url?: string + } + url?: string + description?: string + } + owner?: { + id: string + name: string + slug: string + } + manufactured_date?: string + data?: any +} + +export const DetectorLogbookPage = ({ + apiBase, + isAuthed, +}: { + apiBase: string + isAuthed: boolean +}) => { + const { id } = useParams<{ id: string }>() + const [detector, setDetector] = useState(null) + const [logbook, setLogbook] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (!id || !isAuthed) return + + const fetchDetectorAndLogbook = async () => { + setLoading(true) + setError(null) + try { + // Fetch detector details + const detectorRes = await fetch(`${apiBase}/detector/`, { + method: 'GET', + credentials: 'include', + }) + if (!detectorRes.ok) throw new Error(`HTTP ${detectorRes.status}`) + const detectors = await detectorRes.json() + const foundDetector = detectors.find((d: Detector) => d.id === id) + + if (!foundDetector) { + throw new Error('Detector not found') + } + setDetector(foundDetector) + + // Fetch logbook entries + const logbookRes = await fetch(`${apiBase}/logbook/?detector=${id}`, { + method: 'GET', + credentials: 'include', + }) + if (!logbookRes.ok) throw new Error(`HTTP ${logbookRes.status}`) + const logbookData = await logbookRes.json() + setLogbook(logbookData) + } catch (e: any) { + setError(`Failed to load detector logbook: ${e.message}`) + } finally { + setLoading(false) + } + } + + fetchDetectorAndLogbook() + }, [id, apiBase, isAuthed]) + + if (!isAuthed) { + return ( + +
+
+ Login required to view logbook. +
+
+
+ ) + } + + return ( + +
+
+
+ + ← Back to Detectors + + {detector ? ( + <> +

{detector.name}

+

+ {detector.type?.name} +

+ + ) : ( +

Detector Logbook

+ )} +
+
+ + {error &&
{error}
} + {loading &&

Loading logbook...

} + + {detector && ( +
+

+ Detector Information +

+
+
+ Serial Number: +
{detector.sn}
+
+
+ Type: +
{detector.type?.name}
+
+
+ Manufacturer: +
+ {detector.type?.manufacturer?.url ? ( + + {detector.type.manufacturer.name} + + ) : ( + detector.type?.manufacturer?.name + )} +
+
+ {detector.owner && ( +
+ Owner: +
{detector.owner.name}
+
+ )} + {detector.manufactured_date && ( +
+ Manufactured: +
+ {new Date(detector.manufactured_date).toLocaleDateString()} +
+
+ )} +
+
+ )} + +
+

+ Logbook Entries +

+ {logbook.length === 0 && !loading ? ( +

No logbook entries yet.

+ ) : ( +
    + {logbook.map((item) => ( +
  • +
    + {item.entry_type} + + {item.author?.username && ( + + by @{item.author.username} + + )} +
    +

    {item.text}

    + {item.latitude && item.longitude ? ( + + View on map + + ) : null} +
  • + ))} +
+ )} +
+
+
+ ) +} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx new file mode 100644 index 0000000..e0b1141 --- /dev/null +++ b/frontend/src/pages/HomePage.tsx @@ -0,0 +1,62 @@ +import { PageLayout } from '../components/PageLayout' +import homeBg from '../assets/img/SPACEDOS01.jpg' + +export const HomePage = () => { + return ( + + {/* Welcome Hero Section */} +
+
+

+ Welcome to DOSPORTAL +

+

+ Page, where you can visualize, store, share or compare your dosimetry data. +

+
+
+ + {/* About Section */} +
+
+

About DOSPORTAL

+ +

+ This web application has been developed and maintained by UST (Universal Scientific Technologies s.r.o.), a company specializing in the manufacturing and development of both semiconductor and scintillation detectors. Our extensive range of detectors includes SPACEDOS, AIRDOS, and LABDOS, which are designed to meet the diverse needs of scientific applications. +

+ +

+ Semiconductor detectors, based on solid-state physics principles, utilize semiconductor materials to detect radiation. These detectors offer excellent energy resolution and are widely used in fields such as nuclear physics, materials science, and environmental monitoring. +

+ +

+ Scintillation detectors, on the other hand, employ scintillating materials that emit light when interacting with radiation. These detectors are highly efficient and find applications in areas such as medical imaging, homeland security, and nuclear power plants. +

+ +

+ UST collaborates closely with the Nuclear Physics Institute of the Czech Academy of Sciences to develop advanced detectors. This collaboration ensures that our detectors meet the rigorous standards required for scientific research and personal dosimetry. +

+ +

+ This website serves as a comprehensive portal for accessing fundamental information about measurements conducted with our detectors. Users can browse through individual records or contribute their own measurements to further scientific understanding. +

+ +

+ For more in-depth information about our detectors, please visit the official UST website or refer to the documentation pages, where you will find detailed specifications and technical details about our semiconductor and scintillation detectors. +

+
+
+
+ ) + } diff --git a/frontend/src/pages/LogbooksPage.tsx b/frontend/src/pages/LogbooksPage.tsx new file mode 100644 index 0000000..4eccdd8 --- /dev/null +++ b/frontend/src/pages/LogbooksPage.tsx @@ -0,0 +1,138 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { PageLayout } from '../components/PageLayout' +import logbookBg from '../assets/img/SPACEDOS01.jpg' + +interface Detector { + id: string + name: string + sn: string + type: { + name: string + manufacturer: { + name: string + } + } +} + +export const LogbooksPage = ({ + apiBase, + isAuthed, +}: { + apiBase: string + isAuthed: boolean +}) => { + const navigate = useNavigate() + const [detectors, setDetectors] = useState([]) + const [error, setError] = useState(null) + + useEffect(() => { + if (!isAuthed) return + const fetchDetectors = async () => { + try { + const res = await fetch(`${apiBase}/detector/`, { + method: 'GET', + credentials: 'include', + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setDetectors(data) + } catch (e: any) { + setError(`Failed to load detectors: ${e.message}`) + } + } + fetchDetectors() + }, [apiBase, isAuthed]) + + if (!isAuthed) { + return ( + +
+
+ Login required to view logbooks. +
+
+
+ ) + } + + return ( + +
+
+

Detector Logbooks

+ +
+ + {error &&
{error}
} + +
+ {detectors.length === 0 ? ( +

No detectors available.

+ ) : ( +
+ {detectors.map((d) => ( +
navigate(`/logbook/${d.id}`)} + style={{ + padding: '1.5rem', + background: '#ffffff', + border: '2px solid #e5e7eb', + borderRadius: '12px', + cursor: 'pointer', + transition: 'all 0.2s ease', + boxShadow: '0 1px 3px rgba(0,0,0,0.05)' + }} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = '#0d6efd' + e.currentTarget.style.boxShadow = '0 4px 12px rgba(13,110,253,0.15)' + e.currentTarget.style.transform = 'translateY(-2px)' + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = '#e5e7eb' + e.currentTarget.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)' + e.currentTarget.style.transform = 'translateY(0)' + }} + > +

+ {d.name} +

+
+
+ Type: {d.type?.name || 'N/A'} +
+
+ Serial: {d.sn} +
+
+
+ View Logbook → +
+
+ ))} +
+ )} +
+
+
+ ) +} diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..62f5c3b --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,104 @@ +import { useState } from 'react' +import loginBg from '../assets/img/login_background.jpg' + +export const LoginPage = ({ + originBase, + onLogin, +}: { + originBase: string + onLogin: (username: string, password: string) => Promise +}) => { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [status, setStatus] = useState<'idle' | 'loading' | 'error' | 'success'>('idle') + const [error, setError] = useState(null) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setStatus('loading') + setError(null) + try { + await onLogin(username, password) + setStatus('success') + } catch (err: any) { + setStatus('error') + setError(err.message) + } + } + + return ( +
+
+
+
+

+ Please sign in +

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setUsername(e.target.value)} + placeholder="jdoe" + required + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + required + /> +
+ + + +
+ +
+ + Not registered yet? + +
+
+
+
+
+ ) +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..2cbf566 --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,16 @@ +export type LogbookItem = { + id: string + detector: string + text: string + entry_type: string + source: string + created: string + author?: { + id: string + username: string + first_name: string + last_name: string + } + latitude?: number + longitude?: number +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/requirements.txt b/requirements.txt index 8784661..f7fcded 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,10 +12,11 @@ djangorestframework django-json-widget django-tables2 django-filter +django-bootstrap5 martor markdown django-filter -django-q +django-q2 django-storages[s3] boto3 @@ -36,4 +37,5 @@ django-prettyjson django-markdownx markdown -djangorestframework \ No newline at end of file +djangorestframework +setuptools \ No newline at end of file