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 %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 }}
Sign in
+
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 (
+
+
+
+ DOSPORTAL
+
+ {isAuthed && (
+
+ LogBooks
+
+ )}
+
+
+ {isAuthed ? (
+ <>
+
+ 👤 Profile
+
+
+ Logout
+
+
+ >
+ ) : (
+
+ Login
+
+ )}
+
+
+ )
+}
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 (
+
+ )
+}
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 (
+
+
+
+
+ {error && {error}
}
+ {loading && Loading logbook...
}
+
+ {detector && (
+
+
+ Detector Information
+
+
+
+
Serial Number:
+
{detector.sn}
+
+
+
Type:
+
{detector.type?.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.
+ ) : (
+
+ )}
+
+
+
+ )
+}
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
+ alert('QR code scanning for new logbook entry coming soon!')}
+ style={{ background: '#198754', border: '1px solid #198754' }}
+ >
+ + Add Entry (QR)
+
+
+
+ {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}
+
+ )}
+
+
+
+
+
+ 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