From 7eb1c399cd5bf1fa7e44ebb4b11a32e4210ae619 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Fri, 19 Apr 2024 20:17:30 +0200 Subject: [PATCH 01/50] misc improvements, fixes and wip --- django/Dockerfile | 2 +- django/attachments/models.py | 13 +-- django/finances/models.py | 5 +- django/labellabor/urls.py | 11 ++ django/labellabor/views.py | 102 ++++++++++++++++++ django/requirements.txt | 2 +- django/templates/_base.html | 12 ++- .../templates/label_edit_output_details.html | 2 +- nginx/nginx.conf | 3 + 9 files changed, 134 insertions(+), 18 deletions(-) diff --git a/django/Dockerfile b/django/Dockerfile index 2e9f23c..cb3226f 100644 --- a/django/Dockerfile +++ b/django/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 +FROM python:3.11 ENV PYTHONUNBUFFERED 1 diff --git a/django/attachments/models.py b/django/attachments/models.py index 54c1d06..01da2d4 100644 --- a/django/attachments/models.py +++ b/django/attachments/models.py @@ -15,13 +15,7 @@ import logging logger = logging.getLogger('labelbase') def attachment_upload(instance, filename): - """Stores the attachment in a "per module/appname/primary key" folder""" - return "attachments/{app}_{model}/{pk}/{filename}".format( - app=instance.content_object._meta.app_label, - model=instance.content_object._meta.object_name.lower(), - pk=instance.content_object.pk, - filename=filename, - ) + pass # used for compatibility / migrations only. class AttachmentManager(models.Manager): @@ -35,6 +29,7 @@ class AttachmentManager(models.Manager): class Attachment(models.Model): objects = AttachmentManager() + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.CharField(db_index=True, max_length=64) content_object = GenericForeignKey("content_type", "object_id") @@ -45,7 +40,7 @@ class Attachment(models.Model): on_delete=models.CASCADE, ) attachment_file = models.FileField( - _("attachment"), upload_to=upload_to # attachment_upload + _("attachment"), upload_to=upload_to ) created = models.DateTimeField(_("created"), auto_now_add=True, db_index=True) modified = models.DateTimeField(_("modified"), auto_now=True, db_index=True) @@ -83,7 +78,7 @@ class Attachment(models.Model): if update_path: old_path = self.attachment_file.path - self.attachment_file.name = upload_to # attachment_upload(self, self.filename) + self.attachment_file.name = upload_to self.attachment_file.save() os.makedirs( diff --git a/django/finances/models.py b/django/finances/models.py index 9928aea..5ba8e2c 100644 --- a/django/finances/models.py +++ b/django/finances/models.py @@ -14,7 +14,6 @@ import logging logger = logging.getLogger('labelbase') - class OutputStat(models.Model): """ These fields are unencrypted. Why? @@ -193,7 +192,7 @@ class OutputStat(models.Model): network=network).last() if cached_data: - print("found cached data {} for type_ref_hash {}".format(cached_data, type_ref_hash)) + print("found cached data {} for type_ref_hash {}, created {}".format(cached_data, type_ref_hash, created)) return cached_data, False def get_value_and_spent(txid, vout): @@ -264,7 +263,7 @@ class HistoricalPrice(models.Model): class Meta: ordering = ['-timestamp'] - + @classmethod def get_or_create_from_api(cls, timestamp=-1): print("running get_or_create_from_api @ timestamp {}".format(timestamp)) diff --git a/django/labellabor/urls.py b/django/labellabor/urls.py index 6c51b55..0092559 100644 --- a/django/labellabor/urls.py +++ b/django/labellabor/urls.py @@ -18,6 +18,8 @@ from hashtags.views import HashtagListView, HashtagUpdateView, LabelbaseProxyVie from .views import ( + UTXOsHealthView, + LabelbaseHealthDatatableView, LabelbaseView, LabelbaseViewActionView, LabelbaseMergeView, @@ -99,6 +101,15 @@ urlpatterns = [ login_required(ProfileView.as_view()), name="userprofile", ), + path( + "labelbase//health/", + login_required(UTXOsHealthView.as_view()), + name="labelbase_health" + ), + path( + "labelbase//health/data/", + login_required(LabelbaseHealthDatatableView.as_view()), + name="labelbase_health_label_data"), path( "labelbase//delete/", login_required(LabelbaseDeleteView.as_view()), diff --git a/django/labellabor/views.py b/django/labellabor/views.py index 285195a..018dfe2 100644 --- a/django/labellabor/views.py +++ b/django/labellabor/views.py @@ -172,6 +172,82 @@ class LabelbaseDatatableView(BaseDatatableView): return qs + +class LabelbaseHealthDatatableView(BaseDatatableView): + columns = ["id", "ref", "label", "value"] + + order_columns = ["id", "ref", "label"] + + def get_initial_queryset(self): + labelbase_id = self.kwargs["labelbase_id"] + + search_tag = self.request.GET.get('tag', None) + + qs = Label.objects.filter(type=Label.TYPE_OUTPUT, + labelbase__user_id=self.request.user.id, + labelbase_id=labelbase_id).order_by("id") + + if search_tag: + # Due to encryption, we need to use a super slow process here... + res_ids = [] + search = f'#{search_tag}' + + for record in qs: + if record.label and search in record.label: + res_ids.append(record.id) + continue + qs = qs.filter(id__in=res_ids) + return qs + + def render_column(self, row, column): + if column == 'id': + return f'{row.id}' + elif column == 'type': + return "{}".format(row.get_type_display()) + elif column == 'ref': + ctx = { + 'row': row, + 'mempool_url': row.get_mempool_url(), + } + return render_to_string('labelbase_dt_ref.html', context=ctx, request=self.request) + elif column == 'label': + if row.label is None: + return "" + if row.labelbase.user.profile.use_hashtags: + return hashtag_to_badge(f'{row.label}') + else: + return f'{row.label}' + elif column == 'value': + try: + return row.get_finance_output_metrics_dict().get('value') + except Exception as ex: + return "{}".format(ex) + else: + return super(LabelbaseHealthDatatableView, self).render_column(row, column) + + def filter_queryset(self, qs): + search = self.request.GET.get('search[value]', None) + if search: + # Due to encryption, we need to use a super slow process here... + res_ids = [] + search = search.lower() + for record in qs: + if record.type and search in record.type.lower(): + res_ids.append(record.id) + continue + if record.ref and search in record.ref.lower(): + res_ids.append(record.id) + continue + if record.label and search in record.label.lower(): + res_ids.append(record.id) + continue + if search in "{}".format(record.get_finance_output_metrics_dict().get('value')): + res_ids.append(record.id) + continue + return qs.filter(id__in=res_ids) + return qs + + class LabelbaseViewActionView(View): def get(self, request, *args, **kwargs): labelbase = get_object_or_404( @@ -214,6 +290,32 @@ class LabelbaseView(ListView): return HttpResponseRedirect(label.labelbase.get_absolute_url()) + +class UTXOsHealthView(ListView): + template_name = "utxos_health.html" + context_object_name = "label_list" + + def get_queryset(self): + qs = Label.objects.filter( + type=Label.TYPE_OUTPUT, + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["labelbase_id"], + ) + return qs.order_by("id") + + def get_context_data(self, **kwargs): + labelbase_id = self.kwargs["labelbase_id"] + context = super(UTXOsHealthView, self).get_context_data(**kwargs) + context["labelbase"] = get_object_or_404( + Labelbase, id=labelbase_id, user_id=self.request.user.id + ) + context["active_labelbase_id"] = labelbase_id + context["labelform"] = LabelForm( + request=self.request, labelbase_id=labelbase_id + ) + context["api_token"] = Token.objects.get(user_id=self.request.user.id) + return context + class LabelbaseMergeView(LabelbaseView): template_name = "labelbase_merge.html" diff --git a/django/requirements.txt b/django/requirements.txt index 7ffd1a4..808b053 100644 --- a/django/requirements.txt +++ b/django/requirements.txt @@ -36,7 +36,7 @@ qrcode==7.3.1 requests==2.31.0 sentry-sdk==1.43.0 six==1.16.0 -sqlparse==0.4.4 +sqlparse==0.5.0 typing_extensions==4.1.1 uritemplate==4.1.1 urllib3==1.26.18 diff --git a/django/templates/_base.html b/django/templates/_base.html index ff0e0d1..19137f4 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -19,8 +19,14 @@ - - {% block title %}{% endblock %} | Labelbase + {% if user.is_authenticated %} + {% for labelbase in user.profile.labelbases %} + {% if labelbase.id == active_labelbase_id %} + {% if labelbase.name %}{{ labelbase.name }}{% endif %} + {% endif %} + {% endfor %} + {% endif %} + {% block title %}{% endblock %} | Labelbase
  • Labels
  • Fix & Manage
  • - {#
  • UTXOs Health
  • #} +
  • Tree Map
  • {% comment %}
  • Stats & KPIs
  • {% endcomment %} {% if request.user.profile.use_fiatfinances %} diff --git a/django/templates/label_edit_output_details.html b/django/templates/label_edit_output_details.html index 36e0273..a7d1413 100644 --- a/django/templates/label_edit_output_details.html +++ b/django/templates/label_edit_output_details.html @@ -7,7 +7,7 @@ -{% if action == "label-attachments" %} +{% if action == "output-details" %} {% if form.instance.type == "output" %}
    diff --git a/nginx/nginx.conf b/nginx/nginx.conf
    index 347a52e..532b65f 100644
    --- a/nginx/nginx.conf
    +++ b/nginx/nginx.conf
    @@ -31,6 +31,9 @@ http {
                 proxy_set_header X-Real-IP $remote_addr;
                 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                 proxy_set_header X-Forwarded-Proto $scheme;
    +            proxy_buffers   4 32k;
    +            proxy_buffer_size   32k;
    +
             }
     
             location /static {
    
    From 1af919bed4d4bab64b58f829367cd8669296b747 Mon Sep 17 00:00:00 2001
    From: Xavier Fiechter 
    Date: Sat, 20 Apr 2024 00:08:07 +0200
    Subject: [PATCH 02/50] removed unused perms system from attachement app
    
    ---
     .../attachments/django-attachments-1.11.zip   | Bin 59937 -> 0 bytes
     .../templatetags/attachments_tags.py          |  30 +++++-------------
     django/attachments/views.py                   |  10 +-----
     django/labellabor/views.py                    |   5 +++
     4 files changed, 14 insertions(+), 31 deletions(-)
     delete mode 100644 django/attachments/django-attachments-1.11.zip
    
    diff --git a/django/attachments/django-attachments-1.11.zip b/django/attachments/django-attachments-1.11.zip
    deleted file mode 100644
    index ea2e0b95e64df6ab3685ff2d4782ed2d399bd2b9..0000000000000000000000000000000000000000
    GIT binary patch
    literal 0
    HcmV?d00001
    
    literal 59937
    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

    ^jycv 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 diff --git a/django/attachments/templatetags/attachments_tags.py b/django/attachments/templatetags/attachments_tags.py index e5a8633..6580fef 100644 --- a/django/attachments/templatetags/attachments_tags.py +++ b/django/attachments/templatetags/attachments_tags.py @@ -22,38 +22,24 @@ def attachment_form(context, obj, **kwargs): The user must own ``attachments.add_attachment permission`` to add attachments. """ - if context["user"].has_perm("attachments.add_attachment"): - return { - "form": AttachmentForm(), - "form_url": add_url_for_obj(obj), - "next": context.request.path, - } - else: - return {"form": None} + + return { + "form": AttachmentForm(), + "form_url": add_url_for_obj(obj), + "next": context.request.path, + } @register.inclusion_tag("attachments/delete_link.html", takes_context=True) def attachment_delete_link(context, attachment, **kwargs): - """ - Renders a html link to the delete view of the given attachment. Returns - no content if the request-user has no permission to delete attachments. - - The user must own either the ``attachments.delete_attachment`` permission - and is the creator of the attachment, that he can delete it or he has - ``attachments.delete_foreign_attachments`` which allows him to delete all - attachments. - """ - if context["user"].has_perm("attachments.delete_foreign_attachments") or ( - context["user"] == attachment.creator - and context["user"].has_perm("attachments.delete_attachment") - ): + if context["user"] == attachment.creator: return { "next": context.request.path, "delete_url": reverse( "attachments:delete", kwargs={"attachment_pk": attachment.pk} ), } - return {"delete_url": None} + @register.simple_tag diff --git a/django/attachments/views.py b/django/attachments/views.py index 77f7ddb..c6938eb 100644 --- a/django/attachments/views.py +++ b/django/attachments/views.py @@ -49,10 +49,6 @@ def add_attachment( extra_context=None, ): next_ = request.POST.get("next", "/") - - if not request.user.has_perm("attachments.add_attachment"): - return HttpResponseRedirect(next_) - model = apps.get_model(app_label, model_name) obj = get_object_or_404(model, pk=pk) obj = obj.get_label_attachment() # our label to attachment proxy @@ -71,17 +67,13 @@ def add_attachment( "next": next_, } template_context.update(extra_context or {}) - return render(request, template_name, template_context) @login_required def delete_attachment(request, attachment_pk): g = get_object_or_404(Attachment, pk=attachment_pk) - if ( - request.user.has_perm("attachments.delete_attachment") - and request.user == g.creator - ) or request.user.has_perm("attachments.delete_foreign_attachments"): + if request.user == g.creator: remove_file_from_disk(g.attachment_file) g.delete() messages.success(request, gettext("Your attachment was deleted.")) diff --git a/django/labellabor/views.py b/django/labellabor/views.py index 018dfe2..8727716 100644 --- a/django/labellabor/views.py +++ b/django/labellabor/views.py @@ -669,6 +669,11 @@ class LabelUpdateView(UpdateView): model = Label fields = ["type", "ref", "label", "origin", "spendable"] + def get_object(self): + user_id = self.request.user.id + pk = self.kwargs["pk"] + return get_object_or_404(Label, labelbase__user_id=self.request.user.id, pk=pk) + def get_template_names(self): if 'action' in self.kwargs: action = self.kwargs['action'] From ee812c4e69f80108e352c9ba662a516b30af1aed Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Sat, 20 Apr 2024 01:03:44 +0200 Subject: [PATCH 03/50] . --- install.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.md b/install.md index 5bae049..946b4ec 100644 --- a/install.md +++ b/install.md @@ -102,6 +102,12 @@ location / { proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } + +``` +Add `client_max_body_size` just below the `location / { }` block to enable attachment uploads up to 100M. + +```bash +client_max_body_size 100M; ``` Restart `nginx`, then access your domain (or IP address) in your browser. From af2aaeeb83b9a74fb21e74e050ec76b973d39594 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Sun, 2 Jun 2024 17:02:45 +0200 Subject: [PATCH 04/50] Address Derivation for xpub --- django/labelbase/models.py | 9 +- .../labelbase/templatetags/labelbase_tags.py | 1 + django/labellabor/settings.py | 11 +- django/labellabor/urls.py | 8 + django/labellabor/views.py | 139 +++++++++++++++++- django/templates/_base.html | 5 + .../_labelbase_header_info_menu.html | 6 + django/templates/attachments/add.html | 3 +- django/templates/attachments/add_form.html | 2 - django/templates/home.html | 5 +- django/templates/label_edit.html | 71 ++++++--- django/templates/labelbase.html | 2 +- 12 files changed, 226 insertions(+), 36 deletions(-) diff --git a/django/labelbase/models.py b/django/labelbase/models.py index 4020547..6ba61fc 100644 --- a/django/labelbase/models.py +++ b/django/labelbase/models.py @@ -112,6 +112,13 @@ class Labelbase(models.Model): def get_hashtags_url(self): return reverse('labelbase_hashtags', kwargs={'labelbase_id': self.id}) + def get_xpub_url(self): + for label in self.label_set.all(): + if label.type == "xpub": # and is_valid_xpub() ... + # returns the first xpub, works for single signature only at the moment. + return reverse('edit_label', kwargs={'pk': label.id}) + return None + class Label(models.Model): """ @@ -201,7 +208,7 @@ class Label(models.Model): def get_absolute_url(self): """ Is used by "edit label" functionality. - This brings us back to the labelbase once the lable was saved. + This brings us back to the labelbase once the label was saved. """ return self.labelbase.get_absolute_url() diff --git a/django/labelbase/templatetags/labelbase_tags.py b/django/labelbase/templatetags/labelbase_tags.py index df786cb..ca9845f 100644 --- a/django/labelbase/templatetags/labelbase_tags.py +++ b/django/labelbase/templatetags/labelbase_tags.py @@ -8,6 +8,7 @@ from labelbase.forms import ExportLabelsForm register = template.Library() + @register.simple_tag def is_self_hosted(): return settings.SELF_HOSTED diff --git a/django/labellabor/settings.py b/django/labellabor/settings.py index e813def..680f4ee 100644 --- a/django/labellabor/settings.py +++ b/django/labellabor/settings.py @@ -33,14 +33,13 @@ except AssertionError: SECRET_KEY = proj_config.get("internal", "secret_key") CRYPTOGRAPHY_SALT = proj_config.get("internal", "crypto_salt") -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True # proj_config.getboolean("internal", "debug") +DEBUG = proj_config.getboolean("internal", "debug") SELF_HOSTED = proj_config.getboolean("internal", "self_hosted", fallback=True) -#if DEBUG: -ALLOWED_HOSTS = ["*"] # we don't know your host config, keep like that at the moment. -#else: -# ALLOWED_HOSTS = [proj_config.get("internal", "allowed_host")] +if DEBUG: + ALLOWED_HOSTS = ["*"] # we don't know your host config, keep like that at the moment. +else: + ALLOWED_HOSTS = [proj_config.get("internal", "allowed_host")] #SENTRY_DSN = "https://3b833ae08ccc4ff68793e961fff4921c@o4504646963232768.ingest.sentry.io/4504646967361536" diff --git a/django/labellabor/urls.py b/django/labellabor/urls.py index 0092559..5c6bf8a 100644 --- a/django/labellabor/urls.py +++ b/django/labellabor/urls.py @@ -44,6 +44,7 @@ from .views import ( LabelbaseDatatableView, #LabelbasePortfolioView, OutputStatUpdateRedirectView, + BitcoinAddressDatatableView, ) from importer.views import upload_labels @@ -266,6 +267,13 @@ urlpatterns = [ login_required(OutputStatUpdateRedirectView.as_view()), name='outputstat_update_redirect' ), + + path( + "label-derived-addresses//", + login_required(BitcoinAddressDatatableView.as_view()), + name='label_derived_addresses' + ), + path( "", HomeView.as_view(), diff --git a/django/labellabor/views.py b/django/labellabor/views.py index 8727716..e83cd0f 100644 --- a/django/labellabor/views.py +++ b/django/labellabor/views.py @@ -32,6 +32,133 @@ from .utils import hashtag_to_badge, extract_fiat_value + +from embit import bip32, script +from embit.networks import NETWORKS + +from django.http import JsonResponse +from django_datatables_view.base_datatable_view import BaseDatatableView +from embit import bip32, script +from embit.networks import NETWORKS + +DEFAULT_DERIVE_ADDRESS_COUNT = 100 + +class BitcoinAddressDatatableView(BaseDatatableView): + label_id = None # Variable to store label ID and verify if all is okay. + + + def get(self, request, *args, **kwargs): + self.label_id = kwargs.get('label_id') + return super().get(request, *args, **kwargs) + + def get_initial_queryset(self): + return self.initialize_addresses() + + def initialize_addresses(self): + addresses = [] + if self.label_id is not None: + # Verify if the label belongs to the current user and get xpub + try: + label = Label.objects.get(id=self.label_id, + labelbase__user_id=self.request.user.id) + except Label.DoesNotExist: + return [] + if label.type == "xpub": + xpub = label.ref + #policy_type = self.request.GET.get('policy_type', 'Single Signature') + derivation = self.request.GET.get('derivation', 'm/84') + offset = int(self.request.GET.get('offset', 0)) + addresses = [] + + supported_policy_types = ['Single Signature'] + supported_derivations = ['m/44', 'm/49', 'm/84'] + + #if policy_type not in supported_policy_types: + # return [] + if derivation not in supported_derivations: + return [] + + if not xpub: + return [] + + key = bip32.HDKey.from_base58(xpub) + + for i in range(int(self.request.GET.get('address_count', DEFAULT_DERIVE_ADDRESS_COUNT))): + idx = i + offset + if derivation == 'm/44' and xpub.startswith("xpub"): + # BIP 44 - Legacy Addresses (P2PKH) + pub = key.derive(f"m/0/{idx}").key + sc = script.p2pkh(pub) + address = sc.address(NETWORKS["main"]) + elif derivation == 'm/49' and xpub.startswith("ypub"): + # BIP 49 - SegWit Addresses (P2SH-P2WPKH) + pub = key.derive(f"m/0/{idx}").key + witness_script = script.p2wpkh(pub) + sc = script.p2sh(witness_script) + address = sc.address(NETWORKS["main"]) + elif derivation == 'm/84' and xpub.startswith("zpub"): + # BIP 84 - Native SegWit Addresses (P2WPKH) + pub = key.derive(f"m/0/{idx}").key + sc = script.p2wpkh(pub) + address = sc.address(NETWORKS["main"]) + else: + continue + + addresses.append({ + 'index': idx, + 'path': f"{derivation}'/0'/0'/0/{idx}", + 'address': address + }) + + + return addresses + + + def filter_queryset(self, qs): + query = self.request.GET.get('search[value]', '').lower() + q_qs = [] + if query: + for item in qs: + if query in item['address'].lower(): + q_qs.append(item) + return q_qs + return qs + + + def ordering(self, qs): + # not supported right now + return qs + + def paging(self, qs): + start = int(self.request.GET.get('start', 0)) + length = int(self.request.GET.get('length', 10)) + return qs[start:start + length] + + def prepare_results(self, qs): + return qs + + def count_total_records(self, qs=None): + return int(self.request.GET.get('address_count', DEFAULT_DERIVE_ADDRESS_COUNT)) + + def count_records(self, qs): + return len(qs) + + def count_filtered_records(self, qs=None): + return len(qs) + + def render_to_response(self, context, **response_kwargs): + qs = self.get_initial_queryset() + filtered_qs = self.filter_queryset(qs) + ordered_qs = self.ordering(filtered_qs) + page_qs = self.paging(ordered_qs) + return JsonResponse({ + "draw": int(self.request.GET.get('draw', 1)), + "recordsTotal": self.count_total_records(qs), + "recordsFiltered": self.count_filtered_records(filtered_qs), + "data": page_qs + }) + + class AboutView(TemplateView): template_name = "about.html" @@ -679,9 +806,11 @@ class LabelUpdateView(UpdateView): action = self.kwargs['action'] if action == 'labeling': return "label_edit_labeling.html" - if action == 'attachments' and settings.SELF_HOSTED and \ + elif action == 'attachments' and settings.SELF_HOSTED and \ self.object.labelbase.user.profile.use_attachments: return "label_edit_attachments.html" + elif action == "derive-addresses": + return "label_derive_addresses.html" elif action == 'output-details': return "label_edit_output_details.html" @@ -692,14 +821,20 @@ class LabelUpdateView(UpdateView): context["active_labelbase_id"] = self.object.labelbase.id context["labelbase"] = self.object.labelbase context["action"] = self.kwargs.get('action', 'update') - if context["action"] == "labeling": + if context["action"] in ["labeling", "derive-addresses"]: context["labelform"] = LabelForm( request=self.request, labelbase_id=self.object.labelbase.id ) if self.object.type == "tx": + # used by "labeling" mempool_api = self.object.labelbase.get_mempool_api() context["res_tx"] = mempool_api.get_transaction(self.object.ref) + + if self.object.type == "xpub": + context["address_count"] = self.request.GET.get("address_count", DEFAULT_DERIVE_ADDRESS_COUNT) + context["offset"] = int(self.request.GET.get("offset", 0)) + if self.object.type == "output": context["output"] = OutputStat.objects.filter( user=self.object.labelbase.user, diff --git a/django/templates/_base.html b/django/templates/_base.html index 19137f4..9d178f1 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -136,6 +136,11 @@ {% if labelbase.id == active_labelbase_id %}

    • Labels
    • + {% with xpub_url=labelbase.get_xpub_url %} + {% if xpub_url %} +
    • Addresses
    • + {% endif %} + {% endwith %}
    • Fix & Manage
    • Tree Map
    • diff --git a/django/templates/_labelbase_header_info_menu.html b/django/templates/_labelbase_header_info_menu.html index 7f65bbe..cb66fc5 100644 --- a/django/templates/_labelbase_header_info_menu.html +++ b/django/templates/_labelbase_header_info_menu.html @@ -38,6 +38,12 @@ Update spent Outputs + +
    • + + Derived addresses from xpub + +
    diff --git a/django/templates/attachments/add.html b/django/templates/attachments/add.html index 79667f3..13f507b 100644 --- a/django/templates/attachments/add.html +++ b/django/templates/attachments/add.html @@ -1,5 +1,4 @@ {% extends "attachments/base.html" %} - {% block content %} {% include "attachments/add_form.html" %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/django/templates/attachments/add_form.html b/django/templates/attachments/add_form.html index 96a43a9..9414688 100644 --- a/django/templates/attachments/add_form.html +++ b/django/templates/attachments/add_form.html @@ -8,7 +8,5 @@ {{ form|bootstrap }}
    - - {% endif %} diff --git a/django/templates/home.html b/django/templates/home.html index b69639f..c1731e7 100644 --- a/django/templates/home.html +++ b/django/templates/home.html @@ -268,10 +268,11 @@
    -

    Cloud-Hosted

    -

    Experience the flexibility and accessibility of Labelbase's cloud-hosted solution, providing users with a reliable and efficient label management service hosted on secure and scalable cloud infrastructure.

    +

    Attachments

    +

    Enhance your labels with attachments. Attach images and files directly to your labels, streamlining your documentation workflow and enriching the data associated with each label.

    + {% comment %}
    diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html index ae6f434..734db27 100644 --- a/django/templates/label_edit.html +++ b/django/templates/label_edit.html @@ -11,29 +11,15 @@ {% block content %} {% get_attachments_for object.get_label_attachment as my_attachments %} +
    +
    + +
    + + +
    + + +
    {% if output.last_error %} diff --git a/django/templates/labelbase.html b/django/templates/labelbase.html index 227fc3d..be0cf4d 100644 --- a/django/templates/labelbase.html +++ b/django/templates/labelbase.html @@ -25,7 +25,7 @@ {% endif %} -
    +
    {% if label_list %} From 0687e9c0baa6da831613499557740784db3425c3 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Sun, 2 Jun 2024 17:04:10 +0200 Subject: [PATCH 05/50] Address Derivation for xpub, added template --- django/templates/label_derive_addresses.html | 171 +++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 django/templates/label_derive_addresses.html diff --git a/django/templates/label_derive_addresses.html b/django/templates/label_derive_addresses.html new file mode 100644 index 0000000..76be5ef --- /dev/null +++ b/django/templates/label_derive_addresses.html @@ -0,0 +1,171 @@ +{% extends "label_edit.html" %} +{% load bootstrap %} +{% load sekizai_tags %} +{% load i18n %} +{% load labelbase_tags %} + + +{% block label_edit_content %} + +{% include "_modal_add_label.html" %} + + + + + +
    + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    + + + +
    +
    + + {{ form.instance.ref }} + +
    + + + +
    +
    + + + + + + + + + + + +
    + +
    + +
    + + + + + + + +{% endblock %} From f4c43f7cd1dc9e2cc3a8b3f655dced765c886403 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Sun, 2 Jun 2024 17:18:04 +0200 Subject: [PATCH 06/50] Fix for build script --- build-and-run-labelbase.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/build-and-run-labelbase.sh b/build-and-run-labelbase.sh index bfa4ec3..b00af70 100755 --- a/build-and-run-labelbase.sh +++ b/build-and-run-labelbase.sh @@ -7,17 +7,19 @@ docker-compose down # make sure Labelbase is terminated. if [[ -z "${MYSQL_ROOT_PASSWORD}" ]]; then echo "Error: MYSQL_ROOT_PASSWORD environment variable is not set" exit 1 -else - export $MYSQL_ROOT_PASSWORD fi if [[ -z "${MYSQL_PASSWORD}" ]]; then echo "Error: MYSQL_PASSWORD environment variable is not set" exit 1 -else - export $MYSQL_PASSWORD fi + +export MYSQL_ROOT_PASSWORD +export MYSQL_PASSWORD + + + # Check git next LAST_GIT_COMMIT_FILE=".last_git_commit" From c46c44f6e0691b67d046a7c3d341762e0b5f252c Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Sun, 2 Jun 2024 18:04:40 +0200 Subject: [PATCH 07/50] template refactoring --- django/templates/label_edit.html | 116 ++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 31 deletions(-) diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html index 734db27..038df78 100644 --- a/django/templates/label_edit.html +++ b/django/templates/label_edit.html @@ -8,11 +8,41 @@ {% block title %}{{ object.type }} {{ object.ref }}{% endblock %} +{% comment %}{% endcomment %} + {% block content %} {% get_attachments_for object.get_label_attachment as my_attachments %} + +{% comment %}
    -
    +{% endcomment %} +
    +
    From 54a442de4bc0b0376f17c5e9b596441f6efb1743 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Sun, 2 Jun 2024 22:01:25 +0200 Subject: [PATCH 08/50] . --- django/templates/label_edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html index 038df78..9031c07 100644 --- a/django/templates/label_edit.html +++ b/django/templates/label_edit.html @@ -139,7 +139,7 @@ - + {% switch object.type %} {% case "addr" %} {% case "tx" %} From c5950a5caab0160da993264c345364f9b6ddbdc0 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Sun, 2 Jun 2024 22:11:39 +0200 Subject: [PATCH 09/50] . --- django/templates/label_edit.html | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html index 9031c07..a8f83f9 100644 --- a/django/templates/label_edit.html +++ b/django/templates/label_edit.html @@ -73,31 +73,27 @@ {% case "xpub" %} - - - + {% switch object.ref|slice:":4" %} + {% case "xpub" %} + Derive Addresses + {% case "ypub" %} + Derive Addresses + {% case "zpub" %} + Derive Addresses + {% else %} + Derive Addresses + {% endswitch %} + {% endswitch %} @@ -142,15 +138,19 @@ {% switch object.type %} {% case "addr" %} + {% case "tx" %} + {% case "output" %} + {% case "xpub" %} +
    {% endcomment %} - +{% comment %}
    +{% endcomment %} +
    + +
    From b420c121fd7ef413603832ce679d62f1cf42c667 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Sun, 2 Jun 2024 22:38:12 +0200 Subject: [PATCH 11/50] . --- django/templates/label_edit.html | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html index ca89f77..a794732 100644 --- a/django/templates/label_edit.html +++ b/django/templates/label_edit.html @@ -202,6 +202,27 @@ {% endif %} + {% if object.type == "output" %} + + {% endif %} + + {% if object.type == "xpub" %} + + {% endif %} + {% if object.labelbase.user.profile.use_attachments %}
    diff --git a/django/templates/_intro.html b/django/templates/_intro.html index 8332de6..e536e8e 100644 --- a/django/templates/_intro.html +++ b/django/templates/_intro.html @@ -95,11 +95,6 @@ - -
    diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html index a794732..7e4542a 100644 --- a/django/templates/label_edit.html +++ b/django/templates/label_edit.html @@ -41,154 +41,6 @@ {% get_attachments_for object.get_label_attachment as my_attachments %} -{% comment %} -
    - - - -
    -{% endcomment %} -{% comment %} -
    - -
    -{% endcomment %}
    +
    diff --git a/django/templates/encryption.html b/django/templates/encryption.html index be512f6..c925c51 100644 --- a/django/templates/encryption.html +++ b/django/templates/encryption.html @@ -5,7 +5,7 @@ {% block nav_home %}active{% endblock %} {% block content %} - +

    Lbelbase

    All your labels in one place.

    @@ -18,11 +18,8 @@
    -
    - ... +
    +

    Your labels are encrypted.
     

    From aa207cc403bef8415ae5a09e2f41f5b87cc93a25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:49:42 +0000 Subject: [PATCH 14/50] Bump jinja2 from 3.1.3 to 3.1.4 in /django Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- django/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/requirements.txt b/django/requirements.txt index 808b053..1079bc5 100644 --- a/django/requirements.txt +++ b/django/requirements.txt @@ -26,7 +26,7 @@ djangorestframework==3.14.0 idna==3.4 importlib-metadata==4.8.3 itypes==1.2.0 -Jinja2==3.1.3 +Jinja2==3.1.4 Markdown==3.3.7 MarkupSafe==2.0.1 mysqlclient>=2.0,<3 From c0e12e748380c5385a7f2c02aac7b03c773a2e25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:50:39 +0000 Subject: [PATCH 15/50] Bump idna from 3.4 to 3.7 in /django Bumps [idna](https://github.com/kjd/idna) from 3.4 to 3.7. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst) - [Commits](https://github.com/kjd/idna/compare/v3.4...v3.7) --- updated-dependencies: - dependency-name: idna dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- django/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/requirements.txt b/django/requirements.txt index 808b053..e0e86a9 100644 --- a/django/requirements.txt +++ b/django/requirements.txt @@ -23,7 +23,7 @@ django-phonenumber-field==6.0.0 django-sekizai==2.0.0 django-user-sessions==2.0.0 djangorestframework==3.14.0 -idna==3.4 +idna==3.7 importlib-metadata==4.8.3 itypes==1.2.0 Jinja2==3.1.3 From 9471c2c2aed0f9281a0bc78c0449a5a475867c1d Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Thu, 6 Jun 2024 16:56:07 +0200 Subject: [PATCH 16/50] . --- django/labellabor/settings.py | 2 +- django/templates/_dropdown_profile.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/django/labellabor/settings.py b/django/labellabor/settings.py index 2a8dad6..1ba13e4 100644 --- a/django/labellabor/settings.py +++ b/django/labellabor/settings.py @@ -54,7 +54,7 @@ sentry_sdk.init( traces_sample_rate=1.0, send_default_pii=True, # must be "True" here, will skip or omit in `before_send` callback ) -sentry_sdk.set_tag("version", "2.2.0") +sentry_sdk.set_tag("version", "2.2.1") LOGGING = { diff --git a/django/templates/_dropdown_profile.html b/django/templates/_dropdown_profile.html index ac3b5a8..cc61e49 100644 --- a/django/templates/_dropdown_profile.html +++ b/django/templates/_dropdown_profile.html @@ -55,7 +55,7 @@
    From 7f13c702b051a8e066098c24a9ef18dd797eef6f Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Thu, 6 Jun 2024 17:01:38 +0200 Subject: [PATCH 17/50] . --- django/labellabor/make_config.py | 3 --- docker-compose.yml | 2 -- 2 files changed, 5 deletions(-) diff --git a/django/labellabor/make_config.py b/django/labellabor/make_config.py index db2a7c0..bb392e5 100644 --- a/django/labellabor/make_config.py +++ b/django/labellabor/make_config.py @@ -23,7 +23,6 @@ def generate_config_file(config_file_path="config.ini"): database_password = os.getenv("MYSQL_PASSWORD") crypto_salt = 'labelbase_{}_'.format(generate_random_string(32)) - # Set the values in the configuration file config['internal'] = { 'secret_key': '{}'.format(dj_secret_key), 'proj_name': 'labelbase', @@ -41,13 +40,11 @@ def generate_config_file(config_file_path="config.ini"): # 'host': '127.0.0.1' } - # Create the configuration file and write the values with open(config_file_path, 'w') as configfile: config.write(configfile) if __name__ == "__main__": - # Check if the config.ini file exists if not os.path.isfile(config_file_path): generate_config_file() print("Config file {} created.".format(config_file_path)) diff --git a/docker-compose.yml b/docker-compose.yml index 871d4ce..92d5aa2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.7" - services: labelbase_mysql: build: From bca2be7c3b22277c123743089963832b0c047982 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Tue, 18 Jun 2024 08:56:33 +0200 Subject: [PATCH 18/50] Added QR Scanner --- django/finances/models.py | 2 +- django/templates/_base.html | 77 ++++++++++++++++++++++++- django/templates/_modal_add_label.html | 79 +++++++++++++++++++++++++- 3 files changed, 154 insertions(+), 4 deletions(-) diff --git a/django/finances/models.py b/django/finances/models.py index 5ba8e2c..c184486 100644 --- a/django/finances/models.py +++ b/django/finances/models.py @@ -263,7 +263,7 @@ class HistoricalPrice(models.Model): class Meta: ordering = ['-timestamp'] - + @classmethod def get_or_create_from_api(cls, timestamp=-1): print("running get_or_create_from_api @ timestamp {}".format(timestamp)) diff --git a/django/templates/_base.html b/django/templates/_base.html index 9d178f1..1c1a6da 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -41,6 +41,26 @@ {% render_block "css" %} + + @@ -70,7 +90,6 @@ - From 06ef65f1838ab029eb3d0d41169b8030e1d1237b Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Tue, 30 Jul 2024 22:43:13 +0200 Subject: [PATCH 20/50] added new files --- django/finances/admin.py | 11 +- django/finances/electrum.py | 6 +- .../migrations/0012_auto_20240701_0932.py | 22 + django/finances/models.py | 16 +- django/finances/signals.py | 12 +- django/finances/tasks.py | 2 - django/importer/forms.py | 5 + django/importer/models.py | 9 +- django/importer/pocket.py | 4 +- django/importer/samourai.py | 153 ++++++ django/importer/tasks.py | 258 ++++++----- django/importer/templatetags/importer_tags.py | 25 +- django/importer/views.py | 2 +- django/labelbase/api.py | 5 +- django/labelbase/models.py | 4 +- django/labelbase/static/Cloud.png | Bin 0 -> 91345 bytes django/labellabor/settings.py | 3 + django/labellabor/urls.py | 6 + django/labellabor/views.py | 20 + django/requirements.txt | 3 +- django/templates/_base.html | 2 +- .../_labelbase_header_info_menu.html | 37 +- django/templates/_modal_connect_api_key.html | 35 ++ django/templates/_modal_delete_labelbase.html | 26 ++ django/templates/_modal_edit_labelbase.html | 24 + .../templates/_modal_importWizzardModal.html | 43 +- django/templates/cloud.html | 51 ++ django/templates/donate.html | 112 ++++- django/templates/label_edit.html | 3 + django/templates/labelbase.html | 435 +++++++++++------- 30 files changed, 961 insertions(+), 373 deletions(-) create mode 100644 django/finances/migrations/0012_auto_20240701_0932.py create mode 100644 django/importer/samourai.py create mode 100644 django/labelbase/static/Cloud.png create mode 100644 django/templates/_modal_connect_api_key.html create mode 100644 django/templates/_modal_delete_labelbase.html create mode 100644 django/templates/_modal_edit_labelbase.html create mode 100644 django/templates/cloud.html diff --git a/django/finances/admin.py b/django/finances/admin.py index 766fba6..ba6f402 100644 --- a/django/finances/admin.py +++ b/django/finances/admin.py @@ -2,7 +2,16 @@ from django.contrib import admin from .models import OutputStat, HistoricalPrice class OutputStatAdmin(admin.ModelAdmin): - list_display = ('type_ref_hash', 'value', 'confirmed_at_block_height', 'confirmed_at_block_time', 'get_spent_status', 'spent', 'network', 'user') + list_display = ('type_ref_hash', + 'value', + 'confirmed_at_block_height', + 'confirmed_at_block_time', + 'get_spent_status', + 'spent', + 'network', + 'user', + 'next_enc_input_attrs', + 'last_error') list_filter = ('network', 'spent') search_fields = ('type_ref_hash',) ordering = ('-confirmed_at_block_time',) diff --git a/django/finances/electrum.py b/django/finances/electrum.py index bb8459b..10f57dd 100644 --- a/django/finances/electrum.py +++ b/django/finances/electrum.py @@ -88,7 +88,8 @@ def checkup_label(label_id, loop): if utxo_resp: txid, index, address, value, blocktime, utxo_data = utxo_resp if utxo_data: - output.next_input_attributes = utxo_data + #output.next_input_attributes = utxo_data + output.set_next_input_attributes(utxo_data) if blocktime: HistoricalPrice.get_or_create_from_api(timestamp=blocktime) @@ -98,9 +99,6 @@ def checkup_label(label_id, loop): conn.last_error = None # reset error if needed unspents = loop.run_until_complete(interact_addr(conn, server_info, "blockchain.scripthash.listunspent", address)) - - - utxo_value = 0 utxo_height = 0 diff --git a/django/finances/migrations/0012_auto_20240701_0932.py b/django/finances/migrations/0012_auto_20240701_0932.py new file mode 100644 index 0000000..f357d8c --- /dev/null +++ b/django/finances/migrations/0012_auto_20240701_0932.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.25 on 2024-07-01 09:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('finances', '0011_alter_outputstat_network'), + ] + + operations = [ + migrations.RemoveField( + model_name='outputstat', + name='next_input_attributes', + ), + migrations.AddField( + model_name='outputstat', + name='next_enc_input_attrs', + field=models.TextField(default=None, null=True), + ), + ] diff --git a/django/finances/models.py b/django/finances/models.py index c184486..fb51d48 100644 --- a/django/finances/models.py +++ b/django/finances/models.py @@ -7,7 +7,10 @@ from pymempool import MempoolAPI from labelbase.receivers import compute_type_ref_hash from django.conf import settings from jsonfield import JSONField +import json from django.contrib.auth.models import User +from shared.encryption import get_fernet_key, cipher_suite + import logging @@ -42,7 +45,18 @@ class OutputStat(models.Model): confirmed_at_block_time = models.IntegerField(default=0) last_error = JSONField(default={}) - next_input_attributes = JSONField(default={}) # will be used for fee estimation + next_enc_input_attrs = models.TextField(default=None, null=True) # will be used for fee estimation + + def set_next_input_attributes(self, data): + json_data = json.dumps(data).encode('utf-8') + encrypted_data = cipher_suite.encrypt(json_data) + self.next_enc_input_attrs = encrypted_data.decode('utf-8') + + def next_input_attributes(self): + encrypted_data = self.next_enc_input_attrs.encode('utf-8') + decrypted_data = cipher_suite.decrypt(encrypted_data) + return json.loads(decrypted_data.decode('utf-8')) + MAINNET = 'mainnet' TESTNET = 'testnet' diff --git a/django/finances/signals.py b/django/finances/signals.py index 40fdbe6..22cc9a2 100644 --- a/django/finances/signals.py +++ b/django/finances/signals.py @@ -1,15 +1,10 @@ from django.contrib.auth.signals import user_logged_in +from django.contrib import messages from django.dispatch import receiver from finances.models import HistoricalPrice -from django.contrib import messages - - - - @receiver(user_logged_in) def perform_tasks_on_login(sender, user, request, **kwargs): - """ """ if user.profile.update_utxo_on_login: from finances.tasks import check_all_outputs from labelbase.models import Label @@ -17,4 +12,7 @@ def perform_tasks_on_login(sender, user, request, **kwargs): if Label.objects.filter(labelbase__user_id=user.id).exists(): messages.info(request, "Sync in progress: We are checking your unspent transaction outputs now.") # Store nearest price information. - HistoricalPrice.get_or_create_from_api(-1) + try: + HistoricalPrice.get_or_create_from_api(-1) + except Exception as ex: + logger.error(ex, exc_info=True) diff --git a/django/finances/tasks.py b/django/finances/tasks.py index fb43e22..7816cbb 100644 --- a/django/finances/tasks.py +++ b/django/finances/tasks.py @@ -1,8 +1,6 @@ import logging from background_task import background from background_task.management.commands.remove_completed import _remove_completed_task - - from labelbase.models import Label from finances.electrum import checkup_label diff --git a/django/importer/forms.py b/django/importer/forms.py index d3ae1ca..e14fe52 100644 --- a/django/importer/forms.py +++ b/django/importer/forms.py @@ -8,3 +8,8 @@ class UploadFileForm(forms.Form): choices=IMPORTER_CHOICES ) file = forms.FileField() + passphrase = forms.CharField( + widget=forms.PasswordInput(), + required=False, + max_length=100 + ) diff --git a/django/importer/models.py b/django/importer/models.py index 82cafed..11c8f31 100644 --- a/django/importer/models.py +++ b/django/importer/models.py @@ -2,15 +2,18 @@ from django.db import models from django.contrib.auth.models import User from labelbase.models import Labelbase from uuid_upload_path import upload_to +from django.conf import settings -IMPORTER_CHOICES = ( +IMPORTER_CHOICES = [ ("BIP-0329", "BIP-329 .jsonl"), # TODO: ("BIP-0329-7z-enc" , "BIP-329 (encrypted) .7z"), ("csv-bluewallet", "BlueWallet .csv"), ("csv-bitbox", "BitBox .csv"), - ("pocket-accointing", "Pocket Accointing .csv") -) + ("pocket-accointing", "Pocket Accointing .csv"), +] +if settings.SELF_HOSTED: + IMPORTER_CHOICES.append(("samourai", "Samourai .txt, (v2)")) class UploadedData(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/django/importer/pocket.py b/django/importer/pocket.py index bbc88ca..9d8af30 100644 --- a/django/importer/pocket.py +++ b/django/importer/pocket.py @@ -1,7 +1,5 @@ import csv -import json -from labelbase.models import Label -from labelbase.models import Labelbase +import json def validate_csv_format(csv_file_path): diff --git a/django/importer/samourai.py b/django/importer/samourai.py new file mode 100644 index 0000000..f4a1cae --- /dev/null +++ b/django/importer/samourai.py @@ -0,0 +1,153 @@ +import json +from Crypto.Cipher import AES +from Crypto.Hash import SHA256 +from Crypto.Protocol.KDF import PBKDF2 +import hashlib +import re +import base64 + +from labelbase.serializers import LabelSerializer +from labelbase.models import Label + +import logging +logger = logging.getLogger('labelbase') + +DefaultPBKDF2Iterations = 5000 +DefaultPBKDF2HMACSHA256Iterations = 15000 +DefaultSamouraiImportLabel = "Imported form samourai.txt" + + + + + +def decrypt_v1(payload, password, iterations=DefaultPBKDF2Iterations): + # V1 uses PBKDF2 for key derivation and AES for decryption + AESBlockSize = 16 + cipherdata = base64.b64decode(payload) + iv = cipherdata[:AESBlockSize] + input_data = cipherdata[AESBlockSize:] + key = PBKDF2(password, iv, dkLen=32, count=iterations) + cipher = AES.new(key, AES.MODE_CBC, iv) + decrypted = cipher.decrypt(input_data) + return decrypted.rstrip(b"\x00").decode('utf-8') + + +def decrypt_v2(payload, password, iterations=DefaultPBKDF2HMACSHA256Iterations): + # V2 uses SHA256 for key derivation and AES for decryption + encrypted_bytes = base64.b64decode(payload.replace("\n", "")) + salt = encrypted_bytes[8:16] + cipher_text = encrypted_bytes[16:] + key_iv = PBKDF2(password, salt, dkLen=48, count=iterations, hmac_hash_module=SHA256) + key = key_iv[:32] + iv = key_iv[32:] + cipher = AES.new(key, AES.MODE_CBC, iv) + decrypted = cipher.decrypt(cipher_text) + pad_len = decrypted[-1] + decrypted = decrypted[:-pad_len] + return decrypted.decode('utf-8') + + +def import_samourai_labels(labelbase, content, passphrase): + content = content.decode('utf-8') + pattern = re.compile(r'\{.*?\}') + match = pattern.search(content) + imported_lables = 0 + payload = None + if match: + json_content = match.group(0) + try: + logger.info(f"json_content {json_content}") + data = json.loads(json_content) + logger.info(f"data: {data}") + version = data.get("version", 1) + payload = data.get("payload", "") + logger.info(f"version: {version}, payload {payload}, passphrase {passphrase}") + + if payload: + if version in [1, "1"]: + decrypted_data = decrypt_v1(payload, passphrase) + elif version in [2, "2"]: + decrypted_data = decrypt_v2(payload, passphrase) + else: + logger.error(f"Unsupported backup version: {version}") + raise ValueError(f"Unsupported backup version: {version}") + logger.info(decrypted_data) + samourai_data = json.loads(decrypted_data) + logger.info(samourai_data) + + """ + DOC/KB: If the labelbase where you import your samourai.txt into, labelbase will set the fingerprint, + """ + labels = Label.objects.filter(labelbase__id=labelbase.id) + if labels.count() == 0: + if not labelbase.fingerprint: + labelbase.fingerprint = samourai_data.get('wallet').get('fingerprint') + if samourai_data.get('wallet').get('testnet'): + labelbase.network == labelbase.TESTNET + else: + labelbase.network == labelbase.MAINNET + labelbase.save() + + xpub = samourai_data.get('wallet', {}).get('accounts')[0].get('xpub') + ypub = samourai_data.get('wallet', {}).get('bip49_accounts')[0].get('ypub') + zpub = samourai_data.get('wallet', {}).get('bip84_accounts')[0].get('zpub') + + for pub in [xpub, ypub, zpub]: + if pub: + _data = { + "type": Label.TYPE_XPUB, + "ref": pub, + "label": DefaultSamouraiImportLabel, + } + _data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=_data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + + utxo_notes = samourai_data.get('meta', {}).get('utxo_notes') + logger.info(utxo_notes) + for note in utxo_notes: + _data = { + "type": Label.TYPE_TX, + "ref": note[0], + "label": note[1], + } + _data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=_data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + blocked_utxos = samourai_data.get('meta', {}).get('blocked_utxos',{}).get('blocked') + logger.info(blocked_utxos) + for utxo in blocked_utxos: + _data = { + "type": Label.TYPE_OUTPUT, + "ref": utxo.get('id','').replace("-", ":"), + "label": DefaultSamouraiImportLabel, + "spendable": False + } + _data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=_data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + return imported_lables + + else: + print("No payload found in the JSON content.") + logger.error("No payload found in the JSON content.") + return imported_lables + except json.JSONDecodeError as e: + print(f"JSONDecodeError: {e}") + logger.error(f"JSONDecodeError: {e}") + return imported_lables + except Exception as ex: + print(f"An error occurred: {ex}") + logger.error(f"An error occurred: {ex}") + logger.error(ex, exc_info=True) + return imported_lables + else: + print("No JSON found in file.") + logger.error("No JSON found in file.") + return imported_lables diff --git a/django/importer/tasks.py b/django/importer/tasks.py index a29b72b..00e30aa 100644 --- a/django/importer/tasks.py +++ b/django/importer/tasks.py @@ -2,9 +2,10 @@ from background_task import background import json import decimal -from labelbase.models import Labelbase from labelbase.serializers import LabelSerializer +import logging +logger = logging.getLogger('labelbase') from .models import UploadedData @@ -12,93 +13,132 @@ EOLSTOP = [b"", "", None, "\n"] @background(schedule=1) -def process_uploaded_data(uploaded_data_id, loop=None): - imported_lables = 0 - uploaded_data = UploadedData.objects.get(pk=uploaded_data_id) - labelbase = uploaded_data.labelbase - fp = uploaded_data.file.open() +def process_uploaded_data(uploaded_data_id, passphrase=None, loop=None): + try: + uploaded_data = UploadedData.objects.get(pk=uploaded_data_id) + imported_lables = 0 + labelbase = uploaded_data.labelbase + fp = uploaded_data.file.open() - # BIP-0329 - if uploaded_data.import_type == "BIP-0329": - while True: - buf = fp.readline() - if buf in EOLSTOP: - break - data = json.loads(buf) - data["labelbase"] = labelbase.id - serializer = LabelSerializer(data=data) - if serializer.is_valid(): - serializer.save() - imported_lables += 1 - elif uploaded_data.import_type == "BIP-0329-7z-enc": - # TODO: Implementation needed. - pass - # Bitbox App - elif uploaded_data.import_type == "csv-bitbox": - while True: - buf = fp.readline() - if buf in EOLSTOP: - break - try: - buf = str(buf.decode("utf-8")) - sbuf = buf.split(",") - # Time,Type,Amount,Unit,Fee,Address,Transaction ID,Note - for elem in [("tx", 6), ("addr", 5)]: - data = { - "type": elem[0], - "ref": sbuf[elem[1]], - "label": " ".join(sbuf[7:]), - } - data["labelbase"] = labelbase.id - serializer = LabelSerializer(data=data) - if serializer.is_valid(): - serializer.save() - imported_lables += 1 - else: - messages.add_message( - request, - messages.ERROR, - 'Could not process line "{}".'.format(buf), - ) - except Exception as ex: - messages.add_message( - request, - messages.ERROR, - 'Could not process line "{}", {}.'.format(buf, ex), - ) - # Pocket Accointing - elif uploaded_data.import_type == "pocket-accointing": - fp.close() - csv_file_path = fp.name - mempool_api = labelbase.get_mempool_api() - from .pocket import validate_csv_format, parse_csv_to_json - if validate_csv_format(csv_file_path): - for item in parse_csv_to_json(csv_file_path): - label = "Got {} {} for {:.2f} {} with reference: {} #Pocket".format(item[0].get('outSellAmount'), item[1].get( - 'inBuyAsset'), decimal.Decimal(item[2].get('inBuyAmount')), item[2].get('inBuyAsset'), item[1].get('operationId')) - txid = item[0].get('operationId') - tx = mempool_api.get_transaction(txid) - potential_utxos = [] - vouts = tx.get("vout", []) - for i in range(len(vouts)): - if vouts[i].get('value', 0) == decimal.Decimal(item[0].get('outSellAmount'))*100000000: - potential_utxos.append("{}:{}".format(txid, i )) - data = {} - if len(potential_utxos) == 1: - # label UTXO/output of tx - data = { - "type": "output", - "ref": potential_utxos[0], - "label": label, - } - if len(potential_utxos) > 1: - # mark tx, add warning tag + # BIP-0329 + if uploaded_data.import_type == "BIP-0329": + while True: + buf = fp.readline() + if buf in EOLSTOP: + break + data = json.loads(buf) + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + elif uploaded_data.import_type == "BIP-0329-7z-enc": + # TODO: Implementation needed. + pass + elif uploaded_data.import_type == "samourai": + buf = fp.read() + logger.info(buf) + print(buf) + from .samourai import import_samourai_labels + import_samourai_labels(labelbase, buf, passphrase) + # Bitbox App + elif uploaded_data.import_type == "csv-bitbox": + while True: + buf = fp.readline() + if buf in EOLSTOP: + break + try: + buf = str(buf.decode("utf-8")) + sbuf = buf.split(",") + # Time,Type,Amount,Unit,Fee,Address,Transaction ID,Note + for elem in [("tx", 6), ("addr", 5)]: + data = { + "type": elem[0], + "ref": sbuf[elem[1]], + "label": " ".join(sbuf[7:]), + } + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + else: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}".'.format(buf), + ) + except Exception as ex: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}", {}.'.format(buf, ex), + ) + # Pocket Accointing + elif uploaded_data.import_type == "pocket-accointing": + fp.close() + csv_file_path = fp.name + mempool_api = labelbase.get_mempool_api() + from .pocket import validate_csv_format, parse_csv_to_json + if validate_csv_format(csv_file_path): + for item in parse_csv_to_json(csv_file_path): + label = "Got {} {} for {:.2f} {} with reference: {} #Pocket".format(item[0].get('outSellAmount'), item[1].get( + 'inBuyAsset'), decimal.Decimal(item[2].get('inBuyAmount')), item[2].get('inBuyAsset'), item[1].get('operationId')) + txid = item[0].get('operationId') + tx = mempool_api.get_transaction(txid) + potential_utxos = [] + vouts = tx.get("vout", []) + for i in range(len(vouts)): + if vouts[i].get('value', 0) == decimal.Decimal(item[0].get('outSellAmount'))*100000000: + potential_utxos.append("{}:{}".format(txid, i )) + data = {} + if len(potential_utxos) == 1: + # label UTXO/output of tx + data = { + "type": "output", + "ref": potential_utxos[0], + "label": label, + } + if len(potential_utxos) > 1: + # mark tx, add warning tag + data = { + "type": "tx", + "ref": txid, + "label": "{} {}".format(label, "#W001_UTXO_NOT_FOUND"), + } + if data: + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + else: + # messages.add_message( + # request, + # messages.ERROR, + # 'Could not process record "{}".'.format(item), + # ) + pass + else: + print("ERROR") # TODO + # BlueWallet + elif uploaded_data.import_type == "csv-bluewallet": + header_row = True + while True: + buf = fp.readline() + if buf in EOLSTOP: + break + if header_row: + header_row = False + continue + try: + buf = str(buf.decode("utf-8")) + sbuf = buf.split(",") data = { "type": "tx", - "ref": txid, - "label": "{} {}".format(label, "#W001_UTXO_NOT_FOUND"), + "ref": sbuf[1], + "label": " ".join(sbuf[3:]), } - if data: data["labelbase"] = labelbase.id serializer = LabelSerializer(data=data) if serializer.is_valid(): @@ -108,50 +148,20 @@ def process_uploaded_data(uploaded_data_id, loop=None): # messages.add_message( # request, # messages.ERROR, - # 'Could not process record "{}".'.format(item), + # 'Could not process line "{}".'.format(buf), # ) pass - else: - print("ERROR") # TODO - # BlueWallet - elif uploaded_data.import_type == "csv-bluewallet": - header_row = True - while True: - buf = fp.readline() - if buf in EOLSTOP: - break - if header_row: - header_row = False - continue - try: - buf = str(buf.decode("utf-8")) - sbuf = buf.split(",") - data = { - "type": "tx", - "ref": sbuf[1], - "label": " ".join(sbuf[3:]), - } - data["labelbase"] = labelbase.id - serializer = LabelSerializer(data=data) - if serializer.is_valid(): - serializer.save() - imported_lables += 1 - else: + except Exception as ex: # messages.add_message( # request, # messages.ERROR, - # 'Could not process line "{}".'.format(buf), + # 'Could not process line "{}", {}.'.format(buf, ex), # ) pass - except Exception as ex: - # messages.add_message( - # request, - # messages.ERROR, - # 'Could not process line "{}", {}.'.format(buf, ex), - # ) - pass - # Clean up – Note: Currently we delete the upload from the file system, - # later we can store the messages.add_message messages, the state and the - # amount of importet labels in it to propagate the messages to the - # frontend/user interface. - uploaded_data.delete() + # Clean up – Note: Currently we delete the upload from the file system, + # later we can store the messages.add_message messages, the state and the + # amount of importet labels in it to propagate the messages to the + # frontend/user interface. + uploaded_data.delete() + except Exception as ex: + logger.error(ex, exc_info=True) diff --git a/django/importer/templatetags/importer_tags.py b/django/importer/templatetags/importer_tags.py index dc43461..a10cfb0 100644 --- a/django/importer/templatetags/importer_tags.py +++ b/django/importer/templatetags/importer_tags.py @@ -10,27 +10,4 @@ def genericlabeluploadform(labelbase_id): form.fields["labelbase_id"].initial = labelbase_id form.fields["import_type"].initial = "BIP-0329" return form - - -@register.simple_tag -def bip0329labeluploadform(labelbase_id): - form = UploadFileForm() - form.fields["labelbase_id"].initial = labelbase_id - form.fields["import_type"].initial = "BIP-0329" - return form - - -@register.simple_tag -def csvBlueWalletlabeluploadform(labelbase_id): - form = UploadFileForm() - form.fields["labelbase_id"].initial = labelbase_id - form.fields["import_type"].initial = "csv-bluewallet" - return form - - -@register.simple_tag -def csvBitBoxLabeluploadform(labelbase_id): - form = UploadFileForm() - form.fields["labelbase_id"].initial = labelbase_id - form.fields["import_type"].initial = "csv-bitbox" - return form + diff --git a/django/importer/views.py b/django/importer/views.py index 62e1dd1..7b72d57 100644 --- a/django/importer/views.py +++ b/django/importer/views.py @@ -30,7 +30,7 @@ def upload_labels(request): file=request.FILES["file"], ) # Schedule the background task to process the uploaded data - process_uploaded_data(uploaded_data.id) + process_uploaded_data(uploaded_data.id, passphrase=form.cleaned_data.get("passphrase", None)) messages.add_message( request, messages.INFO, diff --git a/django/labelbase/api.py b/django/labelbase/api.py index 2ec8a2d..5781795 100644 --- a/django/labelbase/api.py +++ b/django/labelbase/api.py @@ -10,6 +10,9 @@ from labelbase.models import Labelbase, Label from labelbase.serializers import LabelbaseSerializer, LabelSerializer +import logging +logger = logging.getLogger('labelbase') + class LabelbaseAPIView(APIView): """ Labelbase @@ -90,7 +93,7 @@ class LabelAPIView(APIView): "spendable": request.data.get("spendable", "null"), } - + #logger.debug(f"data: {data}") serializer = LabelSerializer(data=data) if serializer.is_valid(): serializer.save() diff --git a/django/labelbase/models.py b/django/labelbase/models.py index 6ba61fc..f719a63 100644 --- a/django/labelbase/models.py +++ b/django/labelbase/models.py @@ -130,7 +130,7 @@ class Label(models.Model): TYPE_PUBKEY = "pubkey" TYPE_INPUT = "input" TYPE_OUTPUT = "output" - TYPE_XPUT = "xpub" + TYPE_XPUB = "xpub" TYPE_CHOICES = [ (TYPE_TX, "tx"), @@ -138,7 +138,7 @@ class Label(models.Model): (TYPE_PUBKEY, "pubkey"), (TYPE_INPUT, "input"), (TYPE_OUTPUT, "output"), - (TYPE_XPUT, "xpub"), + (TYPE_XPUB, "xpub"), ] type = models.CharField( diff --git a/django/labelbase/static/Cloud.png b/django/labelbase/static/Cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..5882b4a43f2d94f6346bbf1265cdff5b7d69c8f2 GIT binary patch literal 91345 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 KQY@0U2>u^A/update//", login_required(OutputStatUpdateRedirectView.as_view()), diff --git a/django/labellabor/views.py b/django/labellabor/views.py index e83cd0f..0072354 100644 --- a/django/labellabor/views.py +++ b/django/labellabor/views.py @@ -101,6 +101,22 @@ class BitcoinAddressDatatableView(BaseDatatableView): pub = key.derive(f"m/0/{idx}").key sc = script.p2wpkh(pub) address = sc.address(NETWORKS["main"]) + elif derivation == 'm/44' and xpub.startswith("tpub"): + # BIP 44 - Legacy Addresses (P2PKH) + pub = key.derive(f"m/0/{idx}").key + sc = script.p2pkh(pub) + address = sc.address(NETWORKS["test"]) + elif derivation == 'm/49' and xpub.startswith("upub"): + # BIP 49 - SegWit Addresses (P2SH-P2WPKH) + pub = key.derive(f"m/0/{idx}").key + witness_script = script.p2wpkh(pub) + sc = script.p2sh(witness_script) + address = sc.address(NETWORKS["test"]) + elif derivation == 'm/84' and xpub.startswith("vpub"): + # BIP 84 - Native SegWit Addresses (P2WPKH) + pub = key.derive(f"m/0/{idx}").key + sc = script.p2wpkh(pub) + address = sc.address(NETWORKS["test"]) else: continue @@ -171,6 +187,10 @@ class InteroperationalView(TemplateView): template_name = "interoperational.html" +class CloudView(TemplateView): + template_name = "cloud.html" + + class HomeView(TemplateView): template_name = "home.html" diff --git a/django/requirements.txt b/django/requirements.txt index 34f0759..4f9e659 100644 --- a/django/requirements.txt +++ b/django/requirements.txt @@ -1,11 +1,12 @@ gunicorn asgiref==3.4.1 -certifi==2023.07.22 +certifi==2024.07.04 cffi==1.15.1 charset-normalizer==2.0.12 coreapi==2.3.3 coreschema==0.0.4 cryptography==42.0.4 +pycryptodome Django==3.2.25 django-appconf==1.0.5 django-bootstrap-form==3.4 diff --git a/django/templates/_base.html b/django/templates/_base.html index 1c1a6da..fe1e9f7 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -184,7 +184,7 @@ {% endif %} {% endwith %} - + {% comment %}
  • UTXOs Health
  • {% endcomment %}
  • Tree Map
  • {% comment %}
  • Stats & KPIs
  • {% endcomment %} {% if request.user.profile.use_fiatfinances %} diff --git a/django/templates/_labelbase_header_info_menu.html b/django/templates/_labelbase_header_info_menu.html index f16d081..77b9b88 100644 --- a/django/templates/_labelbase_header_info_menu.html +++ b/django/templates/_labelbase_header_info_menu.html @@ -8,30 +8,35 @@ {% endif %}
  • Fix & Manage
  • +
    - - - {% comment %} - - Export - {% endcomment %} +--> + + + + + +
    - + + +
    + {% addtoblock "js" %} + $(document).ready(function () { - var form = $('#importLabelbaseModal form'); - form.on('submit', function () { - // Disable the OK button on form submission - $('#importLabelbaseModal button[type="submit"]').prop('disabled', true); + var form = $('#importLabelbaseModal form'); + var importTypeField = $('#id_import_type'); + var passphraseFieldGroup = $('#id_passphrase').closest('.form-group'); + + function togglePassphraseField() { + if (importTypeField.val() === 'samourai') { + passphraseFieldGroup.show(); + $('#id_passphrase').prop('disabled', false); + $('#samourai-warning').show(); + } else { + passphraseFieldGroup.hide(); + $('#id_passphrase').prop('disabled', true); + $('#id_passphrase').val('') + $('#samourai-warning').hide(); + } + } + + importTypeField.on('change', togglePassphraseField); + + // Initial call to set the correct state on page load + togglePassphraseField(); + + form.on('submit', function () { + // Disable the OK button on form submission + $('#importLabelbaseModal button[type="submit"]').prop('disabled', true); + }); }); -}); + {% endaddtoblock %} diff --git a/django/templates/cloud.html b/django/templates/cloud.html new file mode 100644 index 0000000..347533c --- /dev/null +++ b/django/templates/cloud.html @@ -0,0 +1,51 @@ +{% extends "_base.html" %} +{% load i18n %} + +{% block title %}Interoperability{% endblock %} +{% block nav_home %}active{% endblock %} + +{% block content %} +
    +

    Lbelbase

    +

    All your labels in one place.

    +
    + + + +
    + + +
    +
    +
    + ... +
    +

    Cloud

    + +

    "Use your labels here, use your labels there – Dave doesn't care."

    +

    + Be like Dave with Labelbase, your ultimate cloud-based platform for managing, merging, and synchronizing wallet labels across all your devices and wallet applications. + +Labelbase seamlessly synchronizes your labels across various wallets and systems, ensuring you have consistent and organized data wherever you go. Simplify your financial management and stay in sync with Labelbase. +

    + + +

    +

    + Get started +

    +
    +
    +
    +
    + +
    + +{% endblock %} diff --git a/django/templates/donate.html b/django/templates/donate.html index 3056ac5..dc6a32d 100644 --- a/django/templates/donate.html +++ b/django/templates/donate.html @@ -7,22 +7,106 @@ {% block content %} -
    -

    Support Labelbase: Make a difference with your donation

    -
    -

    Every contribution helps us build a better Bitcoin labeling experience.

    -

    -Choose the amount you'd like to donate to Labelbase. -

    -Your generosity ensures that our project continues to thrive and evolve, directly supporting our mission to make Bitcoin transactions more organized and transparent. -

    -Thank you for your support! + +

    +

    Support Labelbase: Keep it Going

    + +

    + Every contribution helps us continue building a better Bitcoin labeling experience. +

    +

    + Choose the amount you'd like to donate to Labelbase. +

    +

    + As a free and open-source software, your generosity is crucial to our mission. Your support ensures our project keeps thriving and evolving, directly enhancing Bitcoin label management. +

    + + + + +
    + + + + + + +
    + + + +
    + +
    + + +


    + Thank you for keeping Labelbase going.

    - - - - +
    + + Seedor powers our BTCPay Server instance, providing secure and private donations. + +
    diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html index 7e4542a..e6ff81b 100644 --- a/django/templates/label_edit.html +++ b/django/templates/label_edit.html @@ -64,10 +64,13 @@
    diff --git a/django/templates/registration/logged_out.html b/django/templates/registration/logged_out.html index 49d12b8..a512367 100644 --- a/django/templates/registration/logged_out.html +++ b/django/templates/registration/logged_out.html @@ -2,6 +2,6 @@ {% load i18n %} {% block content %} -

    {% block title %}{% trans "Logged Out" %}{% endblock %}

    +

    {% block title %}{% trans "Logged Out" %}{% endblock %}

    {% trans "See you around!" %}

    {% endblock %} From 37b2a1b988b42e9c2a25290a444343f6e2c49e42 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Wed, 9 Oct 2024 14:46:54 +0200 Subject: [PATCH 24/50] added threadlocals --- django/finances/models.py | 13 +++-- django/labellabor/settings.py | 2 + django/templates/_base.html | 4 +- django/threadlocals/__init__.py | 6 ++ django/threadlocals/middleware.py | 35 ++++++++++++ django/threadlocals/threadlocals.py | 88 +++++++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 django/threadlocals/__init__.py create mode 100644 django/threadlocals/middleware.py create mode 100644 django/threadlocals/threadlocals.py diff --git a/django/finances/models.py b/django/finances/models.py index b9a2201..89cd0ab 100644 --- a/django/finances/models.py +++ b/django/finances/models.py @@ -278,7 +278,7 @@ class HistoricalPrice(models.Model): class Meta: ordering = ['-timestamp'] - @classmethod + @classmethod def get_or_create_from_api(cls, user=None, timestamp=-1): if timestamp == -1: current_datetime = datetime.datetime.now() @@ -295,10 +295,13 @@ class HistoricalPrice(models.Model): response = requests.get(url) api_response = response.json() except Exception as ex: - #T ODO: - #if request: - # messages.error(request, "Connection Error: Could not connect to Mempool to retrieve historical price.") - logger.error(ex, exc_info=True) + try: + from threadlocals.threadlocals import get_current_request + request = get_current_request() + if request: + messages.error(request, "Connection Error: Could not connect to Mempool to retrieve historical price.") + except Exception as ex2: + logger.error(ex, exc_info=True) return None, None try: obj, created = cls.objects.get_or_create(timestamp=timestamp, defaults={ diff --git a/django/labellabor/settings.py b/django/labellabor/settings.py index 4104b77..4366510 100644 --- a/django/labellabor/settings.py +++ b/django/labellabor/settings.py @@ -137,6 +137,8 @@ MIDDLEWARE = [ # "django.middleware.cache.FetchFromCacheMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "threadlocals.middleware.ThreadLocalMiddleware", + ] diff --git a/django/templates/_base.html b/django/templates/_base.html index fe1e9f7..d3d02e8 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -252,12 +252,10 @@ Community - {% comment %}
  • - {% endcomment %} - +
  • diff --git a/django/threadlocals/__init__.py b/django/threadlocals/__init__.py new file mode 100644 index 0000000..413f5a5 --- /dev/null +++ b/django/threadlocals/__init__.py @@ -0,0 +1,6 @@ +# User: Troy Evans +# Date: 1/24/13 +# Time: 8:06 PM +# +# Copyright 2012, Nutrislice Inc. +VERSION = '0.10' \ No newline at end of file diff --git a/django/threadlocals/middleware.py b/django/threadlocals/middleware.py new file mode 100644 index 0000000..4372b6b --- /dev/null +++ b/django/threadlocals/middleware.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +threadlocals Middleware, provides a better, faster way to get at request and user. + +:Authors: + - Ben Roberts (Nutrislice, Inc.) + - Troy Evans (Nutrislice, Inc.) + - Herbert Poul http://sct.sphene.net + - Bruce Kroeze + +Branched from [http://code.djangoproject.com/wiki/CookBookthreadlocalsAndUser CookBookThreadLocalsAndUser] +as modified by [http://sct.sphene.net Sphene Community tools]. + +(see license.txt) +""" + +from .threadlocals import set_thread_variable, del_thread_variables +try: + from django.utils.deprecation import MiddlewareMixin +except ImportError: + MiddlewareMixin = object + +class ThreadLocalMiddleware(MiddlewareMixin): + """Middleware that puts the request object in thread local storage.""" + + def process_request(self, request): + set_thread_variable('request', request) + # set_current_user(request.user) # not going to store user in TL's for now, since we can get it from the request if we need it, and I read somewhere that accessing reqeust.user can potentially prevent view caching from functioning correctly + + def process_response(self, request, response): + del_thread_variables() + return response + + def process_exception(self, request, exception): + del_thread_variables() diff --git a/django/threadlocals/threadlocals.py b/django/threadlocals/threadlocals.py new file mode 100644 index 0000000..7c17316 --- /dev/null +++ b/django/threadlocals/threadlocals.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +""" +__init__ module for the threadlocals package + +Derived from django-threaded-multihost (see license.txt) +""" +__docformat__ = "restructuredtext" + +import logging + +log = logging.getLogger('threadlocals.middleware') + +from threading import local + +_threadlocals = local() +_threadvariables = set() + + +def set_thread_variable(key, val): + _threadvariables.add(key) + setattr(_threadlocals, key, val) + + +def get_thread_variable(key, default=None): + return getattr(_threadlocals, key, default) + + +def del_thread_variable(key): + if hasattr(_threadlocals, key): + delattr(_threadlocals, key) + + +def del_thread_variables(): + for key in _threadvariables: + del_thread_variable(key) + + +def get_current_request(): + return get_thread_variable('request', None) + + +def get_current_session(): + req = get_current_request() + if req is None: + return None + return req.session + + +def get_current_user(): + user = get_thread_variable('user', None) + if user is None: + req = get_current_request() + if req == None or not hasattr(req, 'user'): + return None + user = req.user + return user + + +def set_current_user(user): + set_thread_variable('user', user) + + +def set_request_variable(key, val, use_threadlocal_if_no_request=True): + request = get_current_request() + if not request: + if not use_threadlocal_if_no_request: + raise RuntimeError( + "Unable to set request variable. No request available in threadlocals. Is ThreadLocalMiddleware installed?") + set_thread_variable(key, val) + else: + try: + var_store = getattr(request, '_variables') + except AttributeError: + setattr(request, '_variables', {}) + var_store = getattr(request, '_variables') + var_store[key] = val + + +def get_request_variable(key, default=None, use_threadlocal_if_no_request=True): + request = get_current_request() + if not request: + if not use_threadlocal_if_no_request: + raise RuntimeError( + "Unable to get request variable. No threadlocal request available. Is ThreadLocalMiddleware installed?") + else: + return get_thread_variable(key, default) + return request._variables.get(key, default) if hasattr(request, '_variables') else default + From b0b88a156aaaeb1a8c792da7dccdb8fc18336b3c Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Fri, 29 Nov 2024 15:24:45 +0100 Subject: [PATCH 25/50] . --- django/background_task/tasks.py | 3 - django/finances/electrum.py | 196 ++++++------------ django/finances/models.py | 18 +- django/labellabor/views.py | 1 - django/notifications/utils.py | 14 -- django/templates/_base.html | 9 +- .../electrum_server_info_update.html | 1 + .../templates/label_edit_output_details.html | 4 - .../labelbase_tree_maps_unspent_outputs.html | 21 -- django/templates/two_factor/core/login.html | 6 - 10 files changed, 69 insertions(+), 204 deletions(-) diff --git a/django/background_task/tasks.py b/django/background_task/tasks.py index 80a1248..409cea6 100644 --- a/django/background_task/tasks.py +++ b/django/background_task/tasks.py @@ -38,7 +38,6 @@ def bg_runner(proxy_task, task=None, loop=None, *args, **kwargs): task = task_qs[0] if func is None: raise BackgroundTaskError("Function is None, can't execute!") - print("bg_runner, loop {}".format(loop)) kwargs['loop'] = loop func(*args, **kwargs) @@ -96,8 +95,6 @@ class Tasks(object): return _decorator def run_task(self, task_name, loop, args=None, kwargs=None): - print("run_task loop {}".format(loop)) - # task_name can be either the name of a task or a Task instance. if isinstance(task_name, Task): task = task_name task_name = task.task_name diff --git a/django/finances/electrum.py b/django/finances/electrum.py index 454b007..ea417fc 100644 --- a/django/finances/electrum.py +++ b/django/finances/electrum.py @@ -9,11 +9,10 @@ from finances.models import OutputStat, HistoricalPrice import logging logger = logging.getLogger('labelbase') - async def interact(conn, server_info, method, utxo): try: await conn.connect(server_info, "s", use_tor=server_info.is_onion, - disable_cert_verify=True, short_term=True) + disable_cert_verify=True, short_term=True) txid, index = utxo.split(":") try: txn = await conn.RPC(method, txid, True) @@ -43,10 +42,8 @@ async def interact_addr(conn, server_info, method, addr): hextx = await conn.RPC(method, addr) if hextx is not None: return hextx - else: - print("Failed to fetch transaction.") except ElectrumErrorResponse as ex: - print(ex) + logger.error(ex) finally: conn.close() @@ -58,167 +55,92 @@ def is_valid_output_ref(ref): return False -def checkup_label(label_id, loop): +def checkup_label(label_id, loop): if label_id and loop: try: elem = Label.objects.get(id=label_id) - output = OutputStat.objects.filter(user=elem.labelbase.user, - type_ref_hash=elem.type_ref_hash, - network=elem.labelbase.network).last() - if not output: - output = OutputStat(user=elem.labelbase.user, - type_ref_hash=elem.type_ref_hash, - network=elem.labelbase.network, value=0) + output = OutputStat.objects.filter( + user=elem.labelbase.user, + type_ref_hash=elem.type_ref_hash, + network=elem.labelbase.network + ).last() - if elem.type == "output" and is_valid_output_ref(elem.ref) and \ - (output.spent is not True or output.confirmed_at_block_time == 0): + if not output: + output = OutputStat( + user=elem.labelbase.user, + type_ref_hash=elem.type_ref_hash, + network=elem.labelbase.network, + value=0, + spent=None, + confirmed_at_block_height=0, + confirmed_at_block_time=0 + ) + logger.debug(f"Output before processing: {output.output_metrics_dict()}") + + if elem.type == "output" and is_valid_output_ref(elem.ref) and ( + output.spent is not True or output.confirmed_at_block_time == 0 + ): + # Determine server info based on network if elem.labelbase.is_mainnet: - electrum_hostname = elem.labelbase.user.profile.electrum_hostname or "electrum.emzy.de" + electrum_hostname = elem.labelbase.user.profile.electrum_hostname or "fulcrum.sethforprivacy.com" electrum_ports = elem.labelbase.user.profile.electrum_ports or "s50002" elif elem.labelbase.is_testnet: electrum_hostname = elem.labelbase.user.profile.electrum_hostname_test or "testnet.qtornado.com" electrum_ports = elem.labelbase.user.profile.electrum_ports_test or "s51002" - server_info = ServerInfo(electrum_hostname, electrum_hostname, ports=(electrum_ports)) + else: + raise ValueError("Unknown network type.") + server_info = ServerInfo(electrum_hostname, electrum_hostname, ports=(electrum_ports)) conn = StratumClient() utxo = elem.ref - utxo_data = {} + + # Fetch transaction details utxo_resp = loop.run_until_complete(interact(conn, server_info, "blockchain.transaction.get", utxo)) if utxo_resp: txid, index, address, value, blocktime, utxo_data = utxo_resp - if utxo_data: - #output.next_input_attributes = utxo_data - output.set_next_input_attributes(utxo_data) + logger.debug(f"Transaction {txid} fetched with blocktime {blocktime}") if blocktime: - HistoricalPrice.get_or_create_from_api(None, timestamp=blocktime) + output.confirmed_at_block_time = blocktime + output.confirmed_at_block_height = txn.get('height', 0) + # Fetch all unspents for the address try: unspents = loop.run_until_complete(interact_addr(conn, server_info, "blockchain.address.listunspent", address)) except: - conn.last_error = None # reset error if needed + conn.last_error = None unspents = loop.run_until_complete(interact_addr(conn, server_info, "blockchain.scripthash.listunspent", address)) - utxo_value = 0 - utxo_height = 0 + logger.debug(f"Unspents for address {address}: {unspents}") - if unspents: - for unspent in unspents: - if unspent.get('tx_hash') == txid and \ - unspent.get('tx_pos') == int(index) and \ - unspent.get('height') > 0 and \ - unspent.get('value') > 0: # Output is confirmed, but not spent yet - output.spent = False - utxo_value = unspent.get('value') - utxo_height = unspent.get('height') + utxo_found = False + for unspent in unspents: + if unspent.get('tx_hash') == txid and unspent.get('tx_pos') == int(index): + output.spent = False + output.value = unspent.get('value', 0) + output.confirmed_at_block_height = unspent.get('height', 0) + utxo_found = True + break + + if not utxo_found: + output.spent = True + logger.warning(f"UTXO {txid}:{index} not found in unspent outputs.") + else: + logger.error(f"Failed to fetch transaction details for {utxo}") - output.network = elem.labelbase.network - if utxo_height: - output.confirmed_at_block_height = utxo_height - if blocktime: - output.confirmed_at_block_time = blocktime - if utxo_value: - output.value = utxo_value - elif value: - output.value = value - break - # elif conn.last_error: output.last_error = conn.last_error else: - output.last_error = {} + logger.warning(f"Unknown error occurred for UTXO {utxo}") + output.last_error = {"error": "Unknown issue"} + + logger.debug(f"Output after processing (before save): {output.output_metrics_dict()}") output.save() - try: - conn.close() - except: - pass + output.refresh_from_db() + logger.debug(f"Output after saving: {output.output_metrics_dict()}") + conn.close() except Exception as e: - logger.error("Error processing label {}: {}".format(label_id, e)) + logger.error(f"Error processing label {label_id}: {e}") else: - if not label_id: - logger.error("Can't get label_id! {}".format(label_id)) - if not loop: - logger.error("Can't get loop!") - - -def checkup_label_buggy(label_id, loop): - if label_id and loop: - elem = Label.objects.get(id=label_id) - output = OutputStat.objects.filter(user=elem.labelbase.user, - type_ref_hash=elem.type_ref_hash, - network=elem.labelbase.network).last() - if not output: - output = OutputStat(user=elem.labelbase.user, - type_ref_hash=elem.type_ref_hash, - network=elem.labelbase.network, value=0) - #print("Using OutputStat id {}".format(output)) - #print("elem.type {} {} {} {}".format(elem.type, is_valid_output_ref(elem.ref), elem.ref, output.spent)) - if elem.type == "output" and is_valid_output_ref(elem.ref) and \ - (output.spent is not True or output.confirmed_at_block_time == 0): - electrum_hostname = elem.labelbase.user.profile.electrum_hostname - if not electrum_hostname: - electrum_hostname = "electrum.emzy.de" - electrum_ports = elem.labelbase.user.profile.electrum_ports - if not electrum_ports: - electrum_ports = "s50002" - server_info = ServerInfo(electrum_hostname, electrum_hostname, ports=((electrum_ports))) - conn = StratumClient() - assert elem.type_ref_hash - utxo = elem.ref - tx_hash, tx_pos = elem.ref.split(":") - utxo_resp = loop.run_until_complete(interact(conn, server_info, "blockchain.transaction.get", utxo)) - blocktime = 0 - if utxo_resp: - txid, index, address, value, blocktime = utxo_resp - if blocktime: - #print("Found blocktime {} for label id {}.".format(blocktime, label_id)) - HistoricalPrice.get_or_create_from_api(None, timestamp=blocktime) - try: - unspents = loop.run_until_complete(interact_addr(conn, server_info, "blockchain.address.listunspent", address)) - except: - conn.last_error = None # reset error if needed - unspents = loop.run_until_complete(interact_addr(conn, server_info, "blockchain.scripthash.listunspent", address)) - unspent_utxo = False - utxo_value = 0 - utxo_height = 0 - if unspents: - for unspent in unspents: - if unspent.get('tx_hash') == tx_hash and \ - unspent.get('tx_pos') == int(tx_pos) and \ - unspent.get('height') > 0 and \ - unspent.get('value') > 0: - unspent_utxo = True - utxo_value = unspent.get('value') - utxo_height = unspent.get('height') - break - if output: - output.network = elem.labelbase.network - if utxo_height: - output.confirmed_at_block_height = utxo_height - if blocktime: - output.confirmed_at_block_time = blocktime - if utxo_value: - output.value = utxo_value - elif value: # take value from TX - output.value = value - if unspent_utxo: - output.spent = False - else: - output.spent = True - output.last_error = {} - elif conn.last_error: - # Damn... - output.last_error = conn.last_error - else: - output.last_error = {} - output.save() - try: - conn.close() - except: - pass - else: - if not label_id: - logger.error("Can't get label_id! {}".format(label_id)) - if not loop: - logger.error("Can't get loop!") + logger.error(f"Invalid input: label_id={label_id}, loop={loop}") diff --git a/django/finances/models.py b/django/finances/models.py index 89cd0ab..9c89365 100644 --- a/django/finances/models.py +++ b/django/finances/models.py @@ -11,9 +11,6 @@ from jsonfield import JSONField import json from django.contrib.auth.models import User from shared.encryption import get_fernet_key, cipher_suite - - - import logging logger = logging.getLogger('labelbase') @@ -54,9 +51,11 @@ class OutputStat(models.Model): self.next_enc_input_attrs = encrypted_data.decode('utf-8') def next_input_attributes(self): - encrypted_data = self.next_enc_input_attrs.encode('utf-8') - decrypted_data = cipher_suite.decrypt(encrypted_data) - return json.loads(decrypted_data.decode('utf-8')) + if self.next_enc_input_attrs: + encrypted_data = self.next_enc_input_attrs.encode('utf-8') + decrypted_data = cipher_suite.decrypt(encrypted_data) + return json.loads(decrypted_data.decode('utf-8')) + return json.loads("{}") MAINNET = 'mainnet' TESTNET = 'testnet' @@ -207,13 +206,11 @@ class OutputStat(models.Model): network=network).last() if cached_data: - print("found cached data {} for type_ref_hash {}, created {}".format(cached_data, type_ref_hash, created)) return cached_data, False def get_value_and_spent(txid, vout): mempool_api = MempoolAPI() res0 = mempool_api.get_transaction(txid) - print("res0 {}".format(res0)) vouts = res0.get("vout", []) if vouts: value = vouts[int(vout)].get("value", 0) @@ -291,17 +288,18 @@ class HistoricalPrice(models.Model): mempool_endpoint = user.profile.mempool_endpoint else: mempool_endpoint = "https://mempool.space" - url = f"{mempool_endpoints}/api/v1/historical-price?timestamp={timestamp}" + url = f"{mempool_endpoint}/api/v1/historical-price?timestamp={timestamp}" response = requests.get(url) api_response = response.json() except Exception as ex: + logger.error(ex, exc_info=True) try: from threadlocals.threadlocals import get_current_request request = get_current_request() if request: messages.error(request, "Connection Error: Could not connect to Mempool to retrieve historical price.") except Exception as ex2: - logger.error(ex, exc_info=True) + logger.error(ex2, exc_info=True) return None, None try: obj, created = cls.objects.get_or_create(timestamp=timestamp, defaults={ diff --git a/django/labellabor/views.py b/django/labellabor/views.py index 441b877..3c4ced7 100644 --- a/django/labellabor/views.py +++ b/django/labellabor/views.py @@ -833,7 +833,6 @@ class LabelUpdateView(UpdateView): return "label_derive_addresses.html" elif action == 'output-details': return "label_edit_output_details.html" - return "label_edit_update.html" def get_context_data(self, **kwargs): diff --git a/django/notifications/utils.py b/django/notifications/utils.py index 2f4c698..066b34d 100644 --- a/django/notifications/utils.py +++ b/django/notifications/utils.py @@ -1,8 +1,5 @@ from django.contrib.auth.models import User - from messages_extends.models import Message - - from messages_extends import constants """ DEBUG = 10 @@ -40,14 +37,3 @@ def notify_error_persistent(username, message): def notify_error(username, message): notify_user(username, message, level=constants.ERROR) - - -""" -{% for message in messages %} -
    - {# close-href is used because href is used by bootstrap to closing other divs #} - × - {{ message }} -
    -{% endfor %} -""" diff --git a/django/templates/_base.html b/django/templates/_base.html index d3d02e8..206739a 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -255,7 +255,7 @@
  • - +
  • @@ -608,13 +608,6 @@ loadChat(false); {% endif %} }); - - - - - - - diff --git a/django/templates/electrum_server_info_update.html b/django/templates/electrum_server_info_update.html index 1388885..f9ddf90 100644 --- a/django/templates/electrum_server_info_update.html +++ b/django/templates/electrum_server_info_update.html @@ -22,6 +22,7 @@

    Mainnet:

      +
    • fulcrum.sethforprivacy.com / s50002
    • electrum.emzy.de / s50002
    • electrum.blockstream.info / s50002
    diff --git a/django/templates/label_edit_output_details.html b/django/templates/label_edit_output_details.html index a7d1413..4b7c02b 100644 --- a/django/templates/label_edit_output_details.html +++ b/django/templates/label_edit_output_details.html @@ -2,7 +2,6 @@ {% load bootstrap %} {% load i18n %} - {% block label_edit_content %}
    -    
    +
         Output, ref:  {{ label.ref }}
         Output value, in sats: {{ output.value }}
         Confirmed at block height: {{ output.confirmed_at_block_height }}
    
    From 086bee5745c0a9bd468a9e1506572e636dbf7035 Mon Sep 17 00:00:00 2001
    From: Xavier Fiechter 
    Date: Fri, 6 Dec 2024 16:46:08 +0100
    Subject: [PATCH 33/50] .
    
    ---
     django/finances/models.py                       | 6 +++---
     django/templates/label_edit_output_details.html | 2 +-
     2 files changed, 4 insertions(+), 4 deletions(-)
    
    diff --git a/django/finances/models.py b/django/finances/models.py
    index 96b1c6c..b123cb2 100644
    --- a/django/finances/models.py
    +++ b/django/finances/models.py
    @@ -79,11 +79,11 @@ class OutputStat(models.Model):
         def get_spent_status(self):
             if self.spent:
                 return "spent"
    -        if not self.spent:
    +        elif not self.spent:
                 return "unspent"
    -        if self.confirmed_at_block_time == 0:
    +        elif self.confirmed_at_block_time == 0:
                 return "unconfirmed"
    -            
    +
         def output_metrics_dict(self, tracked_fiat_value=0, fiat_currency="USD"):
             """
             Get output metrics dictionary for the current instance.
    diff --git a/django/templates/label_edit_output_details.html b/django/templates/label_edit_output_details.html
    index 9e62261..0025a8e 100644
    --- a/django/templates/label_edit_output_details.html
    +++ b/django/templates/label_edit_output_details.html
    @@ -17,7 +17,7 @@
         Confirmed at block height: {{ output.confirmed_at_block_height }}
         Confirmed at block time: {{ output.confirmed_at_block_time }}
         Network: {{ output.get_network_display }}
    -    Spent: {{ output.spent }}
    +    Spent: {{ output.get_spent_status }} 
         {% if output.next_input_attributes %}
         Fee estimation will be made based on:
     
    
    From cb5a782c13a779501577259361dc5c03fe389d65 Mon Sep 17 00:00:00 2001
    From: Xavier Fiechter 
    Date: Fri, 6 Dec 2024 16:58:36 +0100
    Subject: [PATCH 34/50] .
    
    ---
     django/templates/label_edit.html | 60 +++++++++++++++-----------------
     1 file changed, 28 insertions(+), 32 deletions(-)
    
    diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html
    index e6ff81b..e56d94a 100644
    --- a/django/templates/label_edit.html
    +++ b/django/templates/label_edit.html
    @@ -136,39 +136,35 @@
         Output in queue! This output is currently in the processing queue. It will be checked shortly.
       
  • {% else %} + {% if object.type == "output" %} + {% switch output.get_spent_status %} + {% case "spent" %} +
    + Output spent! Blockchain records indicate that this output has been spent in another transaction. +
    + {% case "unspent" %} +
    + Output unspent! Blockchain records indicate that this output has not been spent yet. +
    + {% case "unconfirmed" %} +
    +
    + + +
    + Output unconfirmed! Blockchain records indicate that this output has not been confirmed yet. +
    + {% endswitch %} + {% endif %} +{% endif %} - - {% if object.type == "output" %} - {% switch output.get_spent_status %} - {% case "spent" %} -
    - Output spent! Blockchain records indicate that this output has been spent in another transaction. -
    - {% case "unspent" %} -
    - Output unspent! Blockchain records indicate that this output has not been spent yet. -
    - {% case "unconfirmed" %} -
    -
    - - -
    - Output unconfirmed! Blockchain records indicate that this output has not been confirmed yet. -
    - {% endswitch %} - {% endif %} - - - - {% endif %} - - + + Output in queue! This output is currently in the processing queue. It will be checked shortly.
    - {% else %} +{% else %} {% if object.type == "output" %} {% switch output.get_spent_status %} {% case "spent" %} @@ -162,9 +163,48 @@ {% endswitch %} {% endif %} {% endif %} +{% endcomment %} +{% is_label_id_in_queue object.id as is_in_queue %} +{% if is_in_queue %} +
    + Output in queue! This output is currently in the processing queue. It will be checked shortly. +
    +{% else %} + {% if object.type == "output" %} + {% if output.get_spent_status == "spent" %} +
    + Output spent! Blockchain records indicate that this output has been spent in another transaction. +
    + {% elif output.get_spent_status == "unspent" %} +
    + Output unspent! Blockchain records indicate that this output has not been spent yet. +
    + {% elif output.get_spent_status == "unconfirmed" %} +
    +
    + + +
    + Output unconfirmed! Blockchain records indicate that this output has not been confirmed yet. +
    + {% else %} +
    + Unknown status: The status of this output could not be determined. +
    + {% endif %} + {% endif %} +{% endif %} - + - - - From 2f517a3ef8a7ee9ea17f53c661e544a94e770b50 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Tue, 18 Nov 2025 22:39:20 +0100 Subject: [PATCH 37/50] . --- django/Dockerfile | 2 +- django/importer/samourai.py | 2 -- django/labellabor/make_config.py | 3 ++- django/labellabor/settings.py | 4 ++-- django/requirements.txt | 2 +- docker-compose.yml | 3 +++ nginx/Dockerfile | 12 ++++++++++-- 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/django/Dockerfile b/django/Dockerfile index cb3226f..5b90b98 100644 --- a/django/Dockerfile +++ b/django/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update && \ default-libmysqlclient-dev \ build-essential \ cron vim logrotate \ - libpcre3-dev \ + libpcre2-dev \ default-mysql-client \ && rm -rf /var/lib/apt/lists/* \ && pip install --upgrade pip \ diff --git a/django/importer/samourai.py b/django/importer/samourai.py index f4a1cae..91fb3b1 100644 --- a/django/importer/samourai.py +++ b/django/importer/samourai.py @@ -61,8 +61,6 @@ def import_samourai_labels(labelbase, content, passphrase): logger.info(f"data: {data}") version = data.get("version", 1) payload = data.get("payload", "") - logger.info(f"version: {version}, payload {payload}, passphrase {passphrase}") - if payload: if version in [1, "1"]: decrypted_data = decrypt_v1(payload, passphrase) diff --git a/django/labellabor/make_config.py b/django/labellabor/make_config.py index bb392e5..8c78a57 100644 --- a/django/labellabor/make_config.py +++ b/django/labellabor/make_config.py @@ -37,7 +37,8 @@ def generate_config_file(config_file_path="config.ini"): 'name': 'labelbase', 'user': 'ulabelbase', 'password': database_password, - # 'host': '127.0.0.1' + 'host': os.getenv("MYSQL_HOST", "127.0.0.1"), + 'port': os.getenv("MYSQL_PORT", "3306"), } with open(config_file_path, 'w') as configfile: diff --git a/django/labellabor/settings.py b/django/labellabor/settings.py index 4366510..1a46048 100644 --- a/django/labellabor/settings.py +++ b/django/labellabor/settings.py @@ -185,8 +185,8 @@ DATABASES = { "USER": proj_config.get("database", "user"), "OPTIONS": {"charset": "utf8mb4"}, "PASSWORD": proj_config.get("database", "password"), - 'HOST': 'localhost', - 'PORT': 3306, + 'HOST': proj_config.get("database", "host", fallback="labelbase_mysql"), + 'PORT': int(proj_config.get("database", "port", fallback="3306")), 'OPTIONS': { 'charset': 'utf8mb4', }, diff --git a/django/requirements.txt b/django/requirements.txt index 4f9e659..801d041 100644 --- a/django/requirements.txt +++ b/django/requirements.txt @@ -42,7 +42,7 @@ typing_extensions==4.1.1 uritemplate==4.1.1 urllib3==1.26.18 zipp==3.6.0 -bip329==0.0.3 +bip329==1.0.0 pymempool==0.0.5 django-datatables-view==1.20.0 django-money==3.3 diff --git a/docker-compose.yml b/docker-compose.yml index 92d5aa2..6e3235d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - MYSQL_USER=ulabelbase - MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD - MYSQL_PASSWORD=$MYSQL_PASSWORD + - MYSQL_HOST=labelbase_mysql ports: - "3306:3306" volumes: @@ -39,6 +40,8 @@ services: - MYSQL_DATABASE=labelbase - MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD - MYSQL_PASSWORD=$MYSQL_PASSWORD + - MYSQL_HOST=labelbase_mysql + - MYSQL_PORT=3306 command: /app/run.sh labelbase_nginx: build: diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 3becd29..4352666 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,3 +1,11 @@ -FROM nginx:latest +# Use official nginx image with explicit platform support +FROM --platform=$BUILDPLATFORM nginx:alpine -COPY nginx.conf /etc/nginx/ +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port 8080 +EXPOSE 8080 + +# Use the default nginx entrypoint +CMD ["nginx", "-g", "daemon off;"] From f8008fd6f49c353d478120f8bb91e95c0a4ac380 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Tue, 18 Nov 2025 23:54:24 +0100 Subject: [PATCH 38/50] fixes #91 --- django/labelbase/static/js/bootstrap.bundle.min.js | 7 +++++++ django/labelbase/static/js/feather.min.js | 13 +++++++++++++ django/labelbase/static/js/jquery-3.5.1.min.js | 2 ++ .../labelbase/static/js/jquery.dataTables.min.js | 4 ++++ django/labelbase/static/js/qrcode.min.js | 1 + django/requirements.txt | 9 +++++---- django/templates/_base.html | 14 +++++--------- 7 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 django/labelbase/static/js/bootstrap.bundle.min.js create mode 100644 django/labelbase/static/js/feather.min.js create mode 100644 django/labelbase/static/js/jquery-3.5.1.min.js create mode 100644 django/labelbase/static/js/jquery.dataTables.min.js create mode 100644 django/labelbase/static/js/qrcode.min.js diff --git a/django/labelbase/static/js/bootstrap.bundle.min.js b/django/labelbase/static/js/bootstrap.bundle.min.js new file mode 100644 index 0000000..da8d852 --- /dev/null +++ b/django/labelbase/static/js/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),i=e=>{e.dispatchEvent(new Event(t))},n=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),s=t=>n(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(e(t)):null,o=t=>{if(!n(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},r=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),a=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?a(t.parentNode):null},l=()=>{},c=t=>{t.offsetHeight},h=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,d=[],u=()=>"rtl"===document.documentElement.dir,f=t=>{var e;e=()=>{const e=h();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(d.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of d)t()})),d.push(e)):e()},p=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,m=(e,n,s=!0)=>{if(!s)return void p(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(n)+5;let r=!1;const a=({target:i})=>{i===n&&(r=!0,n.removeEventListener(t,a),p(e))};n.addEventListener(t,a),setTimeout((()=>{r||i(n)}),o)},g=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},_=/[^.]*(?=\..*)\.|.*/,b=/\..*/,v=/::\d+$/,y={};let w=1;const A={mouseenter:"mouseover",mouseleave:"mouseout"},E=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function T(t,e){return e&&`${e}::${w++}`||t.uidEvent||w++}function C(t){const e=T(t);return t.uidEvent=e,y[e]=y[e]||{},y[e]}function O(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function x(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=D(t);return E.has(o)||(o=t),[n,s,o]}function k(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=x(e,i,n);if(e in A){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=C(t),c=l[a]||(l[a]={}),h=O(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=T(r,e.replace(_,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return N(s,{delegateTarget:r}),n.oneOff&&I.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return N(n,{delegateTarget:t}),i.oneOff&&I.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function L(t,e,i,n,s){const o=O(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function S(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&L(t,e,i,r.callable,r.delegationSelector)}function D(t){return t=t.replace(b,""),A[t]||t}const I={on(t,e,i,n){k(t,e,i,n,!1)},one(t,e,i,n){k(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=x(e,i,n),a=r!==e,l=C(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))S(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(v,"");a&&!e.includes(s)||L(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;L(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=h();let s=null,o=!0,r=!0,a=!1;e!==D(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());let l=new Event(e,{bubbles:o,cancelable:!0});return l=N(l,i),a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function N(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}const P=new Map,j={set(t,e,i){P.has(t)||P.set(t,new Map);const n=P.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>P.has(t)&&P.get(t).get(e)||null,remove(t,e){if(!P.has(t))return;const i=P.get(t);i.delete(e),0===i.size&&P.delete(t)}};function M(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const H={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=M(t.dataset[n])}return e},getDataAttribute:(t,e)=>M(t.getAttribute(`data-bs-${F(e)}`))};class ${static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=n(e)?H.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...n(e)?H.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[s,o]of Object.entries(e)){const e=t[s],r=n(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(o).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${s}" provided type "${r}" but expected type "${o}".`)}var i}}class W extends ${constructor(t,e){super(),(t=s(t))&&(this._element=t,this._config=this._getConfig(e),j.set(this._element,this.constructor.DATA_KEY,this))}dispose(){j.remove(this._element,this.constructor.DATA_KEY),I.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){m(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return j.get(s(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.0-alpha1"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let i=t.getAttribute("data-bs-target");if(!i||"#"===i){let e=t.getAttribute("href");if(!e||!e.includes("#")&&!e.startsWith("."))return null;e.includes("#")&&!e.startsWith("#")&&(e=`#${e.split("#")[1]}`),i=e&&"#"!==e?e.trim():null}return e(i)},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!r(t)&&o(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;I.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),r(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))};class q extends W{static get NAME(){return"alert"}close(){if(I.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),I.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(q,"close"),f(q);const V='[data-bs-toggle="button"]';class K extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=K.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}I.on(document,"click.bs.button.data-api",V,(t=>{t.preventDefault();const e=t.target.closest(V);K.getOrCreateInstance(e).toggle()})),f(K);const Q={endCallback:null,leftCallback:null,rightCallback:null},X={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class Y extends ${constructor(t,e){super(),this._element=t,t&&Y.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Q}static get DefaultType(){return X}static get NAME(){return"swipe"}dispose(){I.off(this._element,".bs.swipe")}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),p(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&p(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(I.on(this._element,"pointerdown.bs.swipe",(t=>this._start(t))),I.on(this._element,"pointerup.bs.swipe",(t=>this._end(t))),this._element.classList.add("pointer-event")):(I.on(this._element,"touchstart.bs.swipe",(t=>this._start(t))),I.on(this._element,"touchmove.bs.swipe",(t=>this._move(t))),I.on(this._element,"touchend.bs.swipe",(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const U="next",G="prev",J="left",Z="right",tt="slid.bs.carousel",et="carousel",it="active",nt={ArrowLeft:Z,ArrowRight:J},st={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class rt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===et&&this.cycle()}static get Default(){return st}static get DefaultType(){return ot}static get NAME(){return"carousel"}next(){this._slide(U)}nextWhenVisible(){!document.hidden&&o(this._element)&&this.next()}prev(){this._slide(G)}pause(){this._isSliding&&i(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?I.one(this._element,tt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void I.one(this._element,tt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?U:G;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&I.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(I.on(this._element,"mouseenter.bs.carousel",(()=>this.pause())),I.on(this._element,"mouseleave.bs.carousel",(()=>this._maybeEnableCycle()))),this._config.touch&&Y.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))I.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(J)),rightCallback:()=>this._slide(this._directionToOrder(Z)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new Y(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=nt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(it),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===U,s=e||g(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>I.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r("slide.bs.carousel").defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",h=n?"carousel-item-next":"carousel-item-prev";s.classList.add(h),c(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,h),s.classList.add(it),i.classList.remove(it,h,l),this._isSliding=!1,r(tt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(".active.carousel-item",this._element)}_getItems(){return z.find(".carousel-item",this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return u()?t===J?G:U:t===J?U:G}_orderToDirection(t){return u()?t===G?J:Z:t===G?Z:J}static jQueryInterface(t){return this.each((function(){const e=rt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}I.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(et))return;t.preventDefault();const i=rt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===H.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),I.on(window,"load.bs.carousel.data-api",(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)rt.getOrCreateInstance(e)})),f(rt);const at="show",lt="collapse",ct="collapsing",ht='[data-bs-toggle="collapse"]',dt={parent:null,toggle:!0},ut={parent:"(null|element)",toggle:"boolean"};class ft extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(ht);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return dt}static get DefaultType(){return ut}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>ft.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(I.trigger(this._element,"show.bs.collapse").defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(lt),this._element.classList.add(ct),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ct),this._element.classList.add(lt,at),this._element.style[e]="",I.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(I.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,c(this._element),this._element.classList.add(ct),this._element.classList.remove(lt,at);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ct),this._element.classList.add(lt),I.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(at)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=s(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(ht);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(":scope .collapse .collapse",this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=ft.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}I.on(document,"click.bs.collapse.data-api",ht,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))ft.getOrCreateInstance(t,{toggle:!1}).toggle()})),f(ft);var pt="top",mt="bottom",gt="right",_t="left",bt="auto",vt=[pt,mt,gt,_t],yt="start",wt="end",At="clippingParents",Et="viewport",Tt="popper",Ct="reference",Ot=vt.reduce((function(t,e){return t.concat([e+"-"+yt,e+"-"+wt])}),[]),xt=[].concat(vt,[bt]).reduce((function(t,e){return t.concat([e,e+"-"+yt,e+"-"+wt])}),[]),kt="beforeRead",Lt="read",St="afterRead",Dt="beforeMain",It="main",Nt="afterMain",Pt="beforeWrite",jt="write",Mt="afterWrite",Ft=[kt,Lt,St,Dt,It,Nt,Pt,jt,Mt];function Ht(t){return t?(t.nodeName||"").toLowerCase():null}function $t(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function Wt(t){return t instanceof $t(t).Element||t instanceof Element}function Bt(t){return t instanceof $t(t).HTMLElement||t instanceof HTMLElement}function zt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof $t(t).ShadowRoot||t instanceof ShadowRoot)}const Rt={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];Bt(s)&&Ht(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});Bt(n)&&Ht(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function qt(t){return t.split("-")[0]}var Vt=Math.max,Kt=Math.min,Qt=Math.round;function Xt(){var t=navigator.userAgentData;return null!=t&&t.brands?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Yt(){return!/^((?!chrome|android).)*safari/i.test(Xt())}function Ut(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&Bt(t)&&(s=t.offsetWidth>0&&Qt(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&Qt(n.height)/t.offsetHeight||1);var r=(Wt(t)?$t(t):window).visualViewport,a=!Yt()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Gt(t){var e=Ut(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Jt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&zt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Zt(t){return $t(t).getComputedStyle(t)}function te(t){return["table","td","th"].indexOf(Ht(t))>=0}function ee(t){return((Wt(t)?t.ownerDocument:t.document)||window.document).documentElement}function ie(t){return"html"===Ht(t)?t:t.assignedSlot||t.parentNode||(zt(t)?t.host:null)||ee(t)}function ne(t){return Bt(t)&&"fixed"!==Zt(t).position?t.offsetParent:null}function se(t){for(var e=$t(t),i=ne(t);i&&te(i)&&"static"===Zt(i).position;)i=ne(i);return i&&("html"===Ht(i)||"body"===Ht(i)&&"static"===Zt(i).position)?e:i||function(t){var e=/firefox/i.test(Xt());if(/Trident/i.test(Xt())&&Bt(t)&&"fixed"===Zt(t).position)return null;var i=ie(t);for(zt(i)&&(i=i.host);Bt(i)&&["html","body"].indexOf(Ht(i))<0;){var n=Zt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function oe(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function re(t,e,i){return Vt(t,Kt(e,i))}function ae(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function le(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const ce={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=qt(i.placement),l=oe(a),c=[_t,gt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return ae("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:le(t,vt))}(s.padding,i),d=Gt(o),u="y"===l?pt:_t,f="y"===l?mt:gt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=se(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=re(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Jt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function he(t){return t.split("-")[1]}var de={top:"auto",right:"auto",bottom:"auto",left:"auto"};function ue(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=_t,y=pt,w=window;if(c){var A=se(i),E="clientHeight",T="clientWidth";A===$t(i)&&"static"!==Zt(A=ee(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===pt||(s===_t||s===gt)&&o===wt)&&(y=mt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==_t&&(s!==pt&&s!==mt||o!==wt)||(v=gt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&de),x=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:Qt(e*n)/n||0,y:Qt(i*n)/n||0}}({x:f,y:m}):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const fe={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:qt(e.placement),variation:he(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,ue(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,ue(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var pe={passive:!0};const me={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=$t(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,pe)})),a&&l.addEventListener("resize",i.update,pe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,pe)})),a&&l.removeEventListener("resize",i.update,pe)}},data:{}};var ge={left:"right",right:"left",bottom:"top",top:"bottom"};function _e(t){return t.replace(/left|right|bottom|top/g,(function(t){return ge[t]}))}var be={start:"end",end:"start"};function ve(t){return t.replace(/start|end/g,(function(t){return be[t]}))}function ye(t){var e=$t(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function we(t){return Ut(ee(t)).left+ye(t).scrollLeft}function Ae(t){var e=Zt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Ht(t))>=0?t.ownerDocument.body:Bt(t)&&Ae(t)?t:Ee(ie(t))}function Te(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=$t(n),r=s?[o].concat(o.visualViewport||[],Ae(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Te(ie(r)))}function Ce(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e,i){return e===Et?Ce(function(t,e){var i=$t(t),n=ee(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Yt();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+we(t),y:l}}(t,i)):Wt(e)?function(t,e){var i=Ut(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ce(function(t){var e,i=ee(t),n=ye(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=Vt(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=Vt(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+we(t),l=-n.scrollTop;return"rtl"===Zt(s||i).direction&&(a+=Vt(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(ee(t)))}function xe(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?qt(s):null,r=s?he(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case pt:e={x:a,y:i.y-n.height};break;case mt:e={x:a,y:i.y+i.height};break;case gt:e={x:i.x+i.width,y:l};break;case _t:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?oe(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case yt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case wt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?At:a,c=i.rootBoundary,h=void 0===c?Et:c,d=i.elementContext,u=void 0===d?Tt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=ae("number"!=typeof g?g:le(g,vt)),b=u===Tt?Ct:Tt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Te(ie(t)),i=["absolute","fixed"].indexOf(Zt(t).position)>=0&&Bt(t)?se(t):t;return Wt(i)?e.filter((function(t){return Wt(t)&&Jt(t,i)&&"body"!==Ht(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=Oe(t,i,n);return e.top=Vt(s.top,e.top),e.right=Kt(s.right,e.right),e.bottom=Kt(s.bottom,e.bottom),e.left=Vt(s.left,e.left),e}),Oe(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(Wt(y)?y:y.contextElement||ee(t.elements.popper),l,h,r),A=Ut(t.elements.reference),E=xe({reference:A,element:v,strategy:"absolute",placement:s}),T=Ce(Object.assign({},v,E)),C=u===Tt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Tt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[gt,mt].indexOf(t)>=0?1:-1,i=[pt,mt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?xt:l,h=he(n),d=h?a?Ot:Ot.filter((function(t){return he(t)===h})):vt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[qt(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const Se={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=qt(g),b=l||(_!==g&&p?function(t){if(qt(t)===bt)return[];var e=_e(t);return[ve(t),e,ve(e)]}(g):[_e(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(qt(i)===bt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ke(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),I=L?k?gt:_t:k?mt:pt;y[S]>w[S]&&(I=_e(I));var N=_e(I),P=[];if(o&&P.push(D[x]<=0),a&&P.push(D[I]<=0,D[N]<=0),P.every((function(t){return t}))){T=O,E=!1;break}A.set(O,P)}if(E)for(var j=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Ie(t){return[pt,gt,mt,_t].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Ie(l),d=Ie(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Pe={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=xt.reduce((function(t,i){return t[i]=function(t,e,i){var n=qt(t),s=[_t,pt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[_t,gt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},je={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=xe({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Me={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=qt(e.placement),b=he(e.placement),v=!b,y=oe(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?pt:_t,D="y"===y?mt:gt,I="y"===y?"height":"width",N=A[y],P=N+g[S],j=N-g[D],M=f?-T[I]/2:0,F=b===yt?E[I]:T[I],H=b===yt?-T[I]:-E[I],$=e.elements.arrow,W=f&&$?Gt($):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=re(0,E[I],W[I]),V=v?E[I]/2-M-q-z-O.mainAxis:F-q-z-O.mainAxis,K=v?-E[I]/2+M+q+R+O.mainAxis:H+q+R+O.mainAxis,Q=e.elements.arrow&&se(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=N+K-Y,G=re(f?Kt(P,N+V-Y-X):P,N,f?Vt(j,U):j);A[y]=G,k[y]=G-N}if(a){var J,Z="x"===y?pt:_t,tt="x"===y?mt:gt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[pt,_t].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=re(t,e,i);return n>i?i:n}(at,et,lt):re(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function Fe(t,e,i){void 0===i&&(i=!1);var n,s,o=Bt(e),r=Bt(e)&&function(t){var e=t.getBoundingClientRect(),i=Qt(e.width)/t.offsetWidth||1,n=Qt(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=ee(e),l=Ut(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==Ht(e)||Ae(a))&&(c=(n=e)!==$t(n)&&Bt(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ye(n)),Bt(e)?((h=Ut(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=we(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var $e={placement:"bottom",modifiers:[],strategy:"absolute"};function We(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(H.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...p(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>o(t)));i.length&&g(i,e,t===Xe,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=ci.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ze);for(const i of e){const e=ci.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Qe,Xe].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Je)?this:z.prev(this,Je)[0]||z.next(this,Je)[0]||z.findOne(Je,t.delegateTarget.parentNode),o=ci.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}I.on(document,Ue,Je,ci.dataApiKeydownHandler),I.on(document,Ue,ti,ci.dataApiKeydownHandler),I.on(document,Ye,ci.clearMenus),I.on(document,"keyup.bs.dropdown.data-api",ci.clearMenus),I.on(document,Ye,Je,(function(t){t.preventDefault(),ci.getOrCreateInstance(this).toggle()})),f(ci);const hi=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",di=".sticky-top",ui="padding-right",fi="margin-right";class pi{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,ui,(e=>e+t)),this._setElementAttributes(hi,ui,(e=>e+t)),this._setElementAttributes(di,fi,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,ui),this._resetElementAttributes(hi,ui),this._resetElementAttributes(di,fi)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&H.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=H.getDataAttribute(t,e);null!==i?(H.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(n(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const mi="show",gi="mousedown.bs.backdrop",_i={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},bi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class vi extends ${constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return _i}static get DefaultType(){return bi}static get NAME(){return"backdrop"}show(t){if(!this._config.isVisible)return void p(t);this._append();const e=this._getElement();this._config.isAnimated&&c(e),e.classList.add(mi),this._emulateAnimation((()=>{p(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(mi),this._emulateAnimation((()=>{this.dispose(),p(t)}))):p(t)}dispose(){this._isAppended&&(I.off(this._element,gi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=s(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),I.on(t,gi,(()=>{p(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){m(t,this._getElement(),this._config.isAnimated)}}const yi=".bs.focustrap",wi="backward",Ai={autofocus:!0,trapElement:null},Ei={autofocus:"boolean",trapElement:"element"};class Ti extends ${constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Ai}static get DefaultType(){return Ei}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),I.off(document,yi),I.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),I.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,I.off(document,yi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===wi?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?wi:"forward")}}const Ci="hidden.bs.modal",Oi="show.bs.modal",xi="modal-open",ki="show",Li="modal-static",Si={backdrop:!0,focus:!0,keyboard:!0},Di={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ii extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new pi,this._addEventListeners()}static get Default(){return Si}static get DefaultType(){return Di}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||I.trigger(this._element,Oi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(xi),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(I.trigger(this._element,"hide.bs.modal").defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(ki),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){for(const t of[window,this._dialog])I.off(t,".bs.modal");this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new vi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ti({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),c(this._element),this._element.classList.add(ki),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,I.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){I.on(this._element,"keydown.dismiss.bs.modal",(t=>{if("Escape"===t.key)return this._config.keyboard?(t.preventDefault(),void this.hide()):void this._triggerBackdropTransition()})),I.on(window,"resize.bs.modal",(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),I.on(this._element,"mousedown.dismiss.bs.modal",(t=>{I.one(this._element,"click.dismiss.bs.modal",(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(xi),this._resetAdjustments(),this._scrollBar.reset(),I.trigger(this._element,Ci)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(I.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Li)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Li),this._queueCallback((()=>{this._element.classList.remove(Li),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=u()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=u()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Ii.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}I.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),I.one(e,Oi,(t=>{t.defaultPrevented||I.one(e,Ci,(()=>{o(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&Ii.getInstance(i).hide(),Ii.getOrCreateInstance(e).toggle(this)})),R(Ii),f(Ii);const Ni="show",Pi="showing",ji="hiding",Mi=".offcanvas.show",Fi="hidePrevented.bs.offcanvas",Hi="hidden.bs.offcanvas",$i={backdrop:!0,keyboard:!0,scroll:!1},Wi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Bi extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return $i}static get DefaultType(){return Wi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||I.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new pi).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Pi),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Ni),this._element.classList.remove(Pi),I.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(I.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(ji),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Ni,ji),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new pi).reset(),I.trigger(this._element,Hi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new vi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():I.trigger(this._element,Fi)}:null})}_initializeFocusTrap(){return new Ti({trapElement:this._element})}_addEventListeners(){I.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():I.trigger(this._element,Fi))}))}static jQueryInterface(t){return this.each((function(){const e=Bi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}I.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),r(this))return;I.one(e,Hi,(()=>{o(this)&&this.focus()}));const i=z.findOne(Mi);i&&i!==e&&Bi.getInstance(i).hide(),Bi.getOrCreateInstance(e).toggle(this)})),I.on(window,"load.bs.offcanvas.data-api",(()=>{for(const t of z.find(Mi))Bi.getOrCreateInstance(t).show()})),I.on(window,"resize.bs.offcanvas",(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Bi.getOrCreateInstance(t).hide()})),R(Bi),f(Bi);const zi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Ri=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,qi=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Vi=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!zi.has(i)||Boolean(Ri.test(t.nodeValue)||qi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Ki={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Qi={allowList:Ki,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
    "},Xi={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Yi={entry:"(string|element|function|null)",selector:"(string|element)"};class Ui extends ${constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Qi}static get DefaultType(){return Xi}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Yi)}_setContent(t,e,i){const o=z.findOne(i,t);o&&((e=this._resolvePossibleFunction(e))?n(e)?this._putElementInTemplate(s(e),o):this._config.html?o.innerHTML=this._maybeSanitize(e):o.textContent=e:o.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Vi(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return p(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Gi=new Set(["sanitize","allowList","sanitizeFn"]),Ji="fade",Zi="show",tn=".modal",en="hide.bs.modal",nn="hover",sn="focus",on={AUTO:"auto",TOP:"top",RIGHT:u()?"left":"right",BOTTOM:"bottom",LEFT:u()?"right":"left"},rn={allowList:Ki,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,0],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},an={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class ln extends W{constructor(t,e){if(void 0===Ve)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return rn}static get DefaultType(){return an}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),I.off(this._element.closest(tn),en,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=I.trigger(this._element,this.constructor.eventName("show")),e=(a(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),I.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(Zi),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))I.on(t,"mouseover",l);this._queueCallback((()=>{I.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!I.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(Zi),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))I.off(t,"mouseover",l);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),I.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(Ji,Zi),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(Ji),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ui({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Ji)}_isShown(){return this.tip&&this.tip.classList.contains(Zi)}_createPopper(t){const e=p(this._config.placement,[this,t,this._element]),i=on[e.toUpperCase()];return qe(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return p(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...p(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)I.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===nn?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===nn?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");I.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?sn:nn]=!0,e._enter()})),I.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?sn:nn]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},I.on(this._element.closest(tn),en,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=H.getDataAttributes(this._element);for(const t of Object.keys(e))Gi.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:s(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=ln.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}f(ln);const cn={...ln.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},hn={...ln.DefaultType,content:"(null|string|element|function)"};class dn extends ln{static get Default(){return cn}static get DefaultType(){return hn}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=dn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}f(dn);const un="click.bs.scrollspy",fn="active",pn="[href]",mn={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},gn={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class _n extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return mn}static get DefaultType(){return gn}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=s(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(I.off(this._config.target,un),I.on(this._config.target,un,pn,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(pn,this._config.target);for(const e of t){if(!e.hash||r(e))continue;const t=z.findOne(e.hash,this._element);o(t)&&(this._targetLinks.set(e.hash,e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(fn),this._activateParents(t),I.trigger(this._element,"activate.bs.scrollspy",{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(fn);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,".nav-link, .nav-item > .nav-link, .list-group-item"))t.classList.add(fn)}_clearActiveClass(t){t.classList.remove(fn);const e=z.find("[href].active",t);for(const t of e)t.classList.remove(fn)}static jQueryInterface(t){return this.each((function(){const e=_n.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}I.on(window,"load.bs.scrollspy.data-api",(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))_n.getOrCreateInstance(t)})),f(_n);const bn="ArrowLeft",vn="ArrowRight",yn="ArrowUp",wn="ArrowDown",An="active",En="fade",Tn="show",Cn='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',On=`.nav-link:not(.dropdown-toggle), .list-group-item:not(.dropdown-toggle), [role="tab"]:not(.dropdown-toggle), ${Cn}`;class xn extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),I.on(this._element,"keydown.bs.tab",(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?I.trigger(e,"hide.bs.tab",{relatedTarget:t}):null;I.trigger(t,"show.bs.tab",{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(An),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),I.trigger(t,"shown.bs.tab",{relatedTarget:e})):t.classList.add(Tn)}),t,t.classList.contains(En)))}_deactivate(t,e){t&&(t.classList.remove(An),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),I.trigger(t,"hidden.bs.tab",{relatedTarget:e})):t.classList.remove(Tn)}),t,t.classList.contains(En)))}_keydown(t){if(![bn,vn,yn,wn].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=[vn,wn].includes(t.key),i=g(this._getChildren().filter((t=>!r(t))),t.target,e,!0);i&&(i.focus({preventScroll:!0}),xn.getOrCreateInstance(i).show())}_getChildren(){return z.find(On,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`#${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(".dropdown-toggle",An),n(".dropdown-menu",Tn),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(An)}_getInnerElement(t){return t.matches(On)?t:z.findOne(On,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}I.on(document,"click.bs.tab",Cn,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),r(this)||xn.getOrCreateInstance(this).show()})),I.on(window,"load.bs.tab",(()=>{for(const t of z.find('.active[data-bs-toggle="tab"], .active[data-bs-toggle="pill"], .active[data-bs-toggle="list"]'))xn.getOrCreateInstance(t)})),f(xn);const kn="hide",Ln="show",Sn="showing",Dn={animation:"boolean",autohide:"boolean",delay:"number"},In={animation:!0,autohide:!0,delay:5e3};class Nn extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return In}static get DefaultType(){return Dn}static get NAME(){return"toast"}show(){I.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(kn),c(this._element),this._element.classList.add(Ln,Sn),this._queueCallback((()=>{this._element.classList.remove(Sn),I.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(I.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(Sn),this._queueCallback((()=>{this._element.classList.add(kn),this._element.classList.remove(Sn,Ln),I.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Ln),super.dispose()}isShown(){return this._element.classList.contains(Ln)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){I.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),I.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),I.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),I.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Nn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Nn),f(Nn),{Alert:q,Button:K,Carousel:rt,Collapse:ft,Dropdown:ci,Modal:Ii,Offcanvas:Bi,Popover:dn,ScrollSpy:_n,Tab:xn,Toast:Nn,Tooltip:ln}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/django/labelbase/static/js/feather.min.js b/django/labelbase/static/js/feather.min.js new file mode 100644 index 0000000..156cd61 --- /dev/null +++ b/django/labelbase/static/js/feather.min.js @@ -0,0 +1,13 @@ +!function(e,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.feather=n():e.feather=n()}("undefined"!=typeof self?self:this,function(){return function(e){var n={};function i(t){if(n[t])return n[t].exports;var l=n[t]={i:t,l:!1,exports:{}};return e[t].call(l.exports,l,l.exports,i),l.l=!0,l.exports}return i.m=e,i.c=n,i.d=function(e,n,t){i.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},i.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},i.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(n,"a",n),n},i.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},i.p="",i(i.s=80)}([function(e,n,i){(function(n){var i="object",t=function(e){return e&&e.Math==Math&&e};e.exports=t(typeof globalThis==i&&globalThis)||t(typeof window==i&&window)||t(typeof self==i&&self)||t(typeof n==i&&n)||Function("return this")()}).call(this,i(75))},function(e,n){var i={}.hasOwnProperty;e.exports=function(e,n){return i.call(e,n)}},function(e,n,i){var t=i(0),l=i(11),r=i(33),o=i(62),a=t.Symbol,c=l("wks");e.exports=function(e){return c[e]||(c[e]=o&&a[e]||(o?a:r)("Symbol."+e))}},function(e,n,i){var t=i(6);e.exports=function(e){if(!t(e))throw TypeError(String(e)+" is not an object");return e}},function(e,n){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,n,i){var t=i(8),l=i(7),r=i(10);e.exports=t?function(e,n,i){return l.f(e,n,r(1,i))}:function(e,n,i){return e[n]=i,e}},function(e,n){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,n,i){var t=i(8),l=i(35),r=i(3),o=i(18),a=Object.defineProperty;n.f=t?a:function(e,n,i){if(r(e),n=o(n,!0),r(i),l)try{return a(e,n,i)}catch(e){}if("get"in i||"set"in i)throw TypeError("Accessors not supported");return"value"in i&&(e[n]=i.value),e}},function(e,n,i){var t=i(4);e.exports=!t(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,n){e.exports={}},function(e,n){e.exports=function(e,n){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:n}}},function(e,n,i){var t=i(0),l=i(19),r=i(17),o=t["__core-js_shared__"]||l("__core-js_shared__",{});(e.exports=function(e,n){return o[e]||(o[e]=void 0!==n?n:{})})("versions",[]).push({version:"3.1.3",mode:r?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var t=o(i(43)),l=o(i(41)),r=o(i(40));function o(e){return e&&e.__esModule?e:{default:e}}n.default=Object.keys(l.default).map(function(e){return new t.default(e,l.default[e],r.default[e])}).reduce(function(e,n){return e[n.name]=n,e},{})},function(e,n){e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},function(e,n,i){var t=i(72),l=i(20);e.exports=function(e){return t(l(e))}},function(e,n){e.exports={}},function(e,n,i){var t=i(11),l=i(33),r=t("keys");e.exports=function(e){return r[e]||(r[e]=l(e))}},function(e,n){e.exports=!1},function(e,n,i){var t=i(6);e.exports=function(e,n){if(!t(e))return e;var i,l;if(n&&"function"==typeof(i=e.toString)&&!t(l=i.call(e)))return l;if("function"==typeof(i=e.valueOf)&&!t(l=i.call(e)))return l;if(!n&&"function"==typeof(i=e.toString)&&!t(l=i.call(e)))return l;throw TypeError("Can't convert object to primitive value")}},function(e,n,i){var t=i(0),l=i(5);e.exports=function(e,n){try{l(t,e,n)}catch(i){t[e]=n}return n}},function(e,n){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,n){var i=Math.ceil,t=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?t:i)(e)}},function(e,n,i){var t; +/*! + Copyright (c) 2016 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames +*/ +/*! + Copyright (c) 2016 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames +*/ +!function(){"use strict";var i=function(){function e(){}function n(e,n){for(var i=n.length,t=0;t0?l(t(e),9007199254740991):0}},function(e,n,i){var t=i(1),l=i(14),r=i(68),o=i(15),a=r(!1);e.exports=function(e,n){var i,r=l(e),c=0,p=[];for(i in r)!t(o,i)&&t(r,i)&&p.push(i);for(;n.length>c;)t(r,i=n[c++])&&(~a(p,i)||p.push(i));return p}},function(e,n,i){var t=i(0),l=i(11),r=i(5),o=i(1),a=i(19),c=i(36),p=i(37),y=p.get,h=p.enforce,x=String(c).split("toString");l("inspectSource",function(e){return c.call(e)}),(e.exports=function(e,n,i,l){var c=!!l&&!!l.unsafe,p=!!l&&!!l.enumerable,y=!!l&&!!l.noTargetGet;"function"==typeof i&&("string"!=typeof n||o(i,"name")||r(i,"name",n),h(i).source=x.join("string"==typeof n?n:"")),e!==t?(c?!y&&e[n]&&(p=!0):delete e[n],p?e[n]=i:r(e,n,i)):p?e[n]=i:a(n,i)})(Function.prototype,"toString",function(){return"function"==typeof this&&y(this).source||c.call(this)})},function(e,n){var i={}.toString;e.exports=function(e){return i.call(e).slice(8,-1)}},function(e,n,i){var t=i(8),l=i(73),r=i(10),o=i(14),a=i(18),c=i(1),p=i(35),y=Object.getOwnPropertyDescriptor;n.f=t?y:function(e,n){if(e=o(e),n=a(n,!0),p)try{return y(e,n)}catch(e){}if(c(e,n))return r(!l.f.call(e,n),e[n])}},function(e,n,i){var t=i(0),l=i(31).f,r=i(5),o=i(29),a=i(19),c=i(71),p=i(65);e.exports=function(e,n){var i,y,h,x,s,u=e.target,d=e.global,f=e.stat;if(i=d?t:f?t[u]||a(u,{}):(t[u]||{}).prototype)for(y in n){if(x=n[y],h=e.noTargetGet?(s=l(i,y))&&s.value:i[y],!p(d?y:u+(f?".":"#")+y,e.forced)&&void 0!==h){if(typeof x==typeof h)continue;c(x,h)}(e.sham||h&&h.sham)&&r(x,"sham",!0),o(i,y,x,e)}}},function(e,n){var i=0,t=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++i+t).toString(36))}},function(e,n,i){var t=i(0),l=i(6),r=t.document,o=l(r)&&l(r.createElement);e.exports=function(e){return o?r.createElement(e):{}}},function(e,n,i){var t=i(8),l=i(4),r=i(34);e.exports=!t&&!l(function(){return 7!=Object.defineProperty(r("div"),"a",{get:function(){return 7}}).a})},function(e,n,i){var t=i(11);e.exports=t("native-function-to-string",Function.toString)},function(e,n,i){var t,l,r,o=i(76),a=i(0),c=i(6),p=i(5),y=i(1),h=i(16),x=i(15),s=a.WeakMap;if(o){var u=new s,d=u.get,f=u.has,g=u.set;t=function(e,n){return g.call(u,e,n),n},l=function(e){return d.call(u,e)||{}},r=function(e){return f.call(u,e)}}else{var v=h("state");x[v]=!0,t=function(e,n){return p(e,v,n),n},l=function(e){return y(e,v)?e[v]:{}},r=function(e){return y(e,v)}}e.exports={set:t,get:l,has:r,enforce:function(e){return r(e)?l(e):t(e,{})},getterFor:function(e){return function(n){var i;if(!c(n)||(i=l(n)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return i}}}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var t=Object.assign||function(e){for(var n=1;n0&&void 0!==arguments[0]?arguments[0]:{};if("undefined"==typeof document)throw new Error("`feather.replace()` only works in a browser environment.");var n=document.querySelectorAll("[data-feather]");Array.from(n).forEach(function(n){return function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=function(e){return Array.from(e.attributes).reduce(function(e,n){return e[n.name]=n.value,e},{})}(e),o=i["data-feather"];delete i["data-feather"];var a=r.default[o].toSvg(t({},n,i,{class:(0,l.default)(n.class,i.class)})),c=(new DOMParser).parseFromString(a,"image/svg+xml").querySelector("svg");e.parentNode.replaceChild(c,e)}(n,e)})}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var t,l=i(12),r=(t=l)&&t.__esModule?t:{default:t};n.default=function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(console.warn("feather.toSvg() is deprecated. Please use feather.icons[name].toSvg() instead."),!e)throw new Error("The required `key` (icon name) parameter is missing.");if(!r.default[e])throw new Error("No icon matching '"+e+"'. See the complete list of icons at https://feathericons.com");return r.default[e].toSvg(n)}},function(e){e.exports={activity:["pulse","health","action","motion"],airplay:["stream","cast","mirroring"],"alert-circle":["warning","alert","danger"],"alert-octagon":["warning","alert","danger"],"alert-triangle":["warning","alert","danger"],"align-center":["text alignment","center"],"align-justify":["text alignment","justified"],"align-left":["text alignment","left"],"align-right":["text alignment","right"],anchor:[],archive:["index","box"],"at-sign":["mention","at","email","message"],award:["achievement","badge"],aperture:["camera","photo"],"bar-chart":["statistics","diagram","graph"],"bar-chart-2":["statistics","diagram","graph"],battery:["power","electricity"],"battery-charging":["power","electricity"],bell:["alarm","notification","sound"],"bell-off":["alarm","notification","silent"],bluetooth:["wireless"],"book-open":["read","library"],book:["read","dictionary","booklet","magazine","library"],bookmark:["read","clip","marker","tag"],box:["cube"],briefcase:["work","bag","baggage","folder"],calendar:["date"],camera:["photo"],cast:["chromecast","airplay"],circle:["off","zero","record"],clipboard:["copy"],clock:["time","watch","alarm"],"cloud-drizzle":["weather","shower"],"cloud-lightning":["weather","bolt"],"cloud-rain":["weather"],"cloud-snow":["weather","blizzard"],cloud:["weather"],codepen:["logo"],codesandbox:["logo"],code:["source","programming"],coffee:["drink","cup","mug","tea","cafe","hot","beverage"],columns:["layout"],command:["keyboard","cmd","terminal","prompt"],compass:["navigation","safari","travel","direction"],copy:["clone","duplicate"],"corner-down-left":["arrow","return"],"corner-down-right":["arrow"],"corner-left-down":["arrow"],"corner-left-up":["arrow"],"corner-right-down":["arrow"],"corner-right-up":["arrow"],"corner-up-left":["arrow"],"corner-up-right":["arrow"],cpu:["processor","technology"],"credit-card":["purchase","payment","cc"],crop:["photo","image"],crosshair:["aim","target"],database:["storage","memory"],delete:["remove"],disc:["album","cd","dvd","music"],"dollar-sign":["currency","money","payment"],droplet:["water"],edit:["pencil","change"],"edit-2":["pencil","change"],"edit-3":["pencil","change"],eye:["view","watch"],"eye-off":["view","watch","hide","hidden"],"external-link":["outbound"],facebook:["logo","social"],"fast-forward":["music"],figma:["logo","design","tool"],"file-minus":["delete","remove","erase"],"file-plus":["add","create","new"],"file-text":["data","txt","pdf"],film:["movie","video"],filter:["funnel","hopper"],flag:["report"],"folder-minus":["directory"],"folder-plus":["directory"],folder:["directory"],framer:["logo","design","tool"],frown:["emoji","face","bad","sad","emotion"],gift:["present","box","birthday","party"],"git-branch":["code","version control"],"git-commit":["code","version control"],"git-merge":["code","version control"],"git-pull-request":["code","version control"],github:["logo","version control"],gitlab:["logo","version control"],globe:["world","browser","language","translate"],"hard-drive":["computer","server","memory","data"],hash:["hashtag","number","pound"],headphones:["music","audio","sound"],heart:["like","love","emotion"],"help-circle":["question mark"],hexagon:["shape","node.js","logo"],home:["house","living"],image:["picture"],inbox:["email"],instagram:["logo","camera"],key:["password","login","authentication","secure"],layers:["stack"],layout:["window","webpage"],"life-bouy":["help","life ring","support"],link:["chain","url"],"link-2":["chain","url"],linkedin:["logo","social media"],list:["options"],lock:["security","password","secure"],"log-in":["sign in","arrow","enter"],"log-out":["sign out","arrow","exit"],mail:["email","message"],"map-pin":["location","navigation","travel","marker"],map:["location","navigation","travel"],maximize:["fullscreen"],"maximize-2":["fullscreen","arrows","expand"],meh:["emoji","face","neutral","emotion"],menu:["bars","navigation","hamburger"],"message-circle":["comment","chat"],"message-square":["comment","chat"],"mic-off":["record","sound","mute"],mic:["record","sound","listen"],minimize:["exit fullscreen","close"],"minimize-2":["exit fullscreen","arrows","close"],minus:["subtract"],monitor:["tv","screen","display"],moon:["dark","night"],"more-horizontal":["ellipsis"],"more-vertical":["ellipsis"],"mouse-pointer":["arrow","cursor"],move:["arrows"],music:["note"],navigation:["location","travel"],"navigation-2":["location","travel"],octagon:["stop"],package:["box","container"],paperclip:["attachment"],pause:["music","stop"],"pause-circle":["music","audio","stop"],"pen-tool":["vector","drawing"],percent:["discount"],"phone-call":["ring"],"phone-forwarded":["call"],"phone-incoming":["call"],"phone-missed":["call"],"phone-off":["call","mute"],"phone-outgoing":["call"],phone:["call"],play:["music","start"],"pie-chart":["statistics","diagram"],"play-circle":["music","start"],plus:["add","new"],"plus-circle":["add","new"],"plus-square":["add","new"],pocket:["logo","save"],power:["on","off"],printer:["fax","office","device"],radio:["signal"],"refresh-cw":["synchronise","arrows"],"refresh-ccw":["arrows"],repeat:["loop","arrows"],rewind:["music"],"rotate-ccw":["arrow"],"rotate-cw":["arrow"],rss:["feed","subscribe"],save:["floppy disk"],scissors:["cut"],search:["find","magnifier","magnifying glass"],send:["message","mail","email","paper airplane","paper aeroplane"],settings:["cog","edit","gear","preferences"],"share-2":["network","connections"],shield:["security","secure"],"shield-off":["security","insecure"],"shopping-bag":["ecommerce","cart","purchase","store"],"shopping-cart":["ecommerce","cart","purchase","store"],shuffle:["music"],"skip-back":["music"],"skip-forward":["music"],slack:["logo"],slash:["ban","no"],sliders:["settings","controls"],smartphone:["cellphone","device"],smile:["emoji","face","happy","good","emotion"],speaker:["audio","music"],star:["bookmark","favorite","like"],"stop-circle":["media","music"],sun:["brightness","weather","light"],sunrise:["weather","time","morning","day"],sunset:["weather","time","evening","night"],tablet:["device"],tag:["label"],target:["logo","bullseye"],terminal:["code","command line","prompt"],thermometer:["temperature","celsius","fahrenheit","weather"],"thumbs-down":["dislike","bad","emotion"],"thumbs-up":["like","good","emotion"],"toggle-left":["on","off","switch"],"toggle-right":["on","off","switch"],tool:["settings","spanner"],trash:["garbage","delete","remove","bin"],"trash-2":["garbage","delete","remove","bin"],triangle:["delta"],truck:["delivery","van","shipping","transport","lorry"],tv:["television","stream"],twitch:["logo"],twitter:["logo","social"],type:["text"],umbrella:["rain","weather"],unlock:["security"],"user-check":["followed","subscribed"],"user-minus":["delete","remove","unfollow","unsubscribe"],"user-plus":["new","add","create","follow","subscribe"],"user-x":["delete","remove","unfollow","unsubscribe","unavailable"],user:["person","account"],users:["group"],"video-off":["camera","movie","film"],video:["camera","movie","film"],voicemail:["phone"],volume:["music","sound","mute"],"volume-1":["music","sound"],"volume-2":["music","sound"],"volume-x":["music","sound","mute"],watch:["clock","time"],"wifi-off":["disabled"],wifi:["connection","signal","wireless"],wind:["weather","air"],"x-circle":["cancel","close","delete","remove","times","clear"],"x-octagon":["delete","stop","alert","warning","times","clear"],"x-square":["cancel","close","delete","remove","times","clear"],x:["cancel","close","delete","remove","times","clear"],youtube:["logo","video","play"],"zap-off":["flash","camera","lightning"],zap:["flash","camera","lightning"],"zoom-in":["magnifying glass"],"zoom-out":["magnifying glass"]}},function(e){e.exports={activity:'',airplay:'',"alert-circle":'',"alert-octagon":'',"alert-triangle":'',"align-center":'',"align-justify":'',"align-left":'',"align-right":'',anchor:'',aperture:'',archive:'',"arrow-down-circle":'',"arrow-down-left":'',"arrow-down-right":'',"arrow-down":'',"arrow-left-circle":'',"arrow-left":'',"arrow-right-circle":'',"arrow-right":'',"arrow-up-circle":'',"arrow-up-left":'',"arrow-up-right":'',"arrow-up":'',"at-sign":'',award:'',"bar-chart-2":'',"bar-chart":'',"battery-charging":'',battery:'',"bell-off":'',bell:'',bluetooth:'',bold:'',"book-open":'',book:'',bookmark:'',box:'',briefcase:'',calendar:'',"camera-off":'',camera:'',cast:'',"check-circle":'',"check-square":'',check:'',"chevron-down":'',"chevron-left":'',"chevron-right":'',"chevron-up":'',"chevrons-down":'',"chevrons-left":'',"chevrons-right":'',"chevrons-up":'',chrome:'',circle:'',clipboard:'',clock:'',"cloud-drizzle":'',"cloud-lightning":'',"cloud-off":'',"cloud-rain":'',"cloud-snow":'',cloud:'',code:'',codepen:'',codesandbox:'',coffee:'',columns:'',command:'',compass:'',copy:'',"corner-down-left":'',"corner-down-right":'',"corner-left-down":'',"corner-left-up":'',"corner-right-down":'',"corner-right-up":'',"corner-up-left":'',"corner-up-right":'',cpu:'',"credit-card":'',crop:'',crosshair:'',database:'',delete:'',disc:'',"divide-circle":'',"divide-square":'',divide:'',"dollar-sign":'',"download-cloud":'',download:'',dribbble:'',droplet:'',"edit-2":'',"edit-3":'',edit:'',"external-link":'',"eye-off":'',eye:'',facebook:'',"fast-forward":'',feather:'',figma:'',"file-minus":'',"file-plus":'',"file-text":'',file:'',film:'',filter:'',flag:'',"folder-minus":'',"folder-plus":'',folder:'',framer:'',frown:'',gift:'',"git-branch":'',"git-commit":'',"git-merge":'',"git-pull-request":'',github:'',gitlab:'',globe:'',grid:'',"hard-drive":'',hash:'',headphones:'',heart:'',"help-circle":'',hexagon:'',home:'',image:'',inbox:'',info:'',instagram:'',italic:'',key:'',layers:'',layout:'',"life-buoy":'',"link-2":'',link:'',linkedin:'',list:'',loader:'',lock:'',"log-in":'',"log-out":'',mail:'',"map-pin":'',map:'',"maximize-2":'',maximize:'',meh:'',menu:'',"message-circle":'',"message-square":'',"mic-off":'',mic:'',"minimize-2":'',minimize:'',"minus-circle":'',"minus-square":'',minus:'',monitor:'',moon:'',"more-horizontal":'',"more-vertical":'',"mouse-pointer":'',move:'',music:'',"navigation-2":'',navigation:'',octagon:'',package:'',paperclip:'',"pause-circle":'',pause:'',"pen-tool":'',percent:'',"phone-call":'',"phone-forwarded":'',"phone-incoming":'',"phone-missed":'',"phone-off":'',"phone-outgoing":'',phone:'',"pie-chart":'',"play-circle":'',play:'',"plus-circle":'',"plus-square":'',plus:'',pocket:'',power:'',printer:'',radio:'',"refresh-ccw":'',"refresh-cw":'',repeat:'',rewind:'',"rotate-ccw":'',"rotate-cw":'',rss:'',save:'',scissors:'',search:'',send:'',server:'',settings:'',"share-2":'',share:'',"shield-off":'',shield:'',"shopping-bag":'',"shopping-cart":'',shuffle:'',sidebar:'',"skip-back":'',"skip-forward":'',slack:'',slash:'',sliders:'',smartphone:'',smile:'',speaker:'',square:'',star:'',"stop-circle":'',sun:'',sunrise:'',sunset:'',tablet:'',tag:'',target:'',terminal:'',thermometer:'',"thumbs-down":'',"thumbs-up":'',"toggle-left":'',"toggle-right":'',tool:'',"trash-2":'',trash:'',trello:'',"trending-down":'',"trending-up":'',triangle:'',truck:'',tv:'',twitch:'',twitter:'',type:'',umbrella:'',underline:'',unlock:'',"upload-cloud":'',upload:'',"user-check":'',"user-minus":'',"user-plus":'',"user-x":'',user:'',users:'',"video-off":'',video:'',voicemail:'',"volume-1":'',"volume-2":'',"volume-x":'',volume:'',watch:'',"wifi-off":'',wifi:'',wind:'',"x-circle":'',"x-octagon":'',"x-square":'',x:'',youtube:'',"zap-off":'',zap:'',"zoom-in":'',"zoom-out":''}},function(e){e.exports={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var t=Object.assign||function(e){for(var n=1;n2&&void 0!==arguments[2]?arguments[2]:[];!function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this,e),this.name=n,this.contents=i,this.tags=l,this.attrs=t({},o.default,{class:"feather feather-"+n})}return l(e,[{key:"toSvg",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return""+this.contents+""}},{key:"toString",value:function(){return this.contents}}]),e}();n.default=c},function(e,n,i){"use strict";var t=o(i(12)),l=o(i(39)),r=o(i(38));function o(e){return e&&e.__esModule?e:{default:e}}e.exports={icons:t.default,toSvg:l.default,replace:r.default}},function(e,n,i){e.exports=i(0)},function(e,n,i){var t=i(2)("iterator"),l=!1;try{var r=0,o={next:function(){return{done:!!r++}},return:function(){l=!0}};o[t]=function(){return this},Array.from(o,function(){throw 2})}catch(e){}e.exports=function(e,n){if(!n&&!l)return!1;var i=!1;try{var r={};r[t]=function(){return{next:function(){return{done:i=!0}}}},e(r)}catch(e){}return i}},function(e,n,i){var t=i(30),l=i(2)("toStringTag"),r="Arguments"==t(function(){return arguments}());e.exports=function(e){var n,i,o;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(i=function(e,n){try{return e[n]}catch(e){}}(n=Object(e),l))?i:r?t(n):"Object"==(o=t(n))&&"function"==typeof n.callee?"Arguments":o}},function(e,n,i){var t=i(47),l=i(9),r=i(2)("iterator");e.exports=function(e){if(void 0!=e)return e[r]||e["@@iterator"]||l[t(e)]}},function(e,n,i){"use strict";var t=i(18),l=i(7),r=i(10);e.exports=function(e,n,i){var o=t(n);o in e?l.f(e,o,r(0,i)):e[o]=i}},function(e,n,i){var t=i(2),l=i(9),r=t("iterator"),o=Array.prototype;e.exports=function(e){return void 0!==e&&(l.Array===e||o[r]===e)}},function(e,n,i){var t=i(3);e.exports=function(e,n,i,l){try{return l?n(t(i)[0],i[1]):n(i)}catch(n){var r=e.return;throw void 0!==r&&t(r.call(e)),n}}},function(e,n){e.exports=function(e){if("function"!=typeof e)throw TypeError(String(e)+" is not a function");return e}},function(e,n,i){var t=i(52);e.exports=function(e,n,i){if(t(e),void 0===n)return e;switch(i){case 0:return function(){return e.call(n)};case 1:return function(i){return e.call(n,i)};case 2:return function(i,t){return e.call(n,i,t)};case 3:return function(i,t,l){return e.call(n,i,t,l)}}return function(){return e.apply(n,arguments)}}},function(e,n,i){"use strict";var t=i(53),l=i(24),r=i(51),o=i(50),a=i(27),c=i(49),p=i(48);e.exports=function(e){var n,i,y,h,x=l(e),s="function"==typeof this?this:Array,u=arguments.length,d=u>1?arguments[1]:void 0,f=void 0!==d,g=0,v=p(x);if(f&&(d=t(d,u>2?arguments[2]:void 0,2)),void 0==v||s==Array&&o(v))for(i=new s(n=a(x.length));n>g;g++)c(i,g,f?d(x[g],g):x[g]);else for(h=v.call(x),i=new s;!(y=h.next()).done;g++)c(i,g,f?r(h,d,[y.value,g],!0):y.value);return i.length=g,i}},function(e,n,i){var t=i(32),l=i(54);t({target:"Array",stat:!0,forced:!i(46)(function(e){Array.from(e)})},{from:l})},function(e,n,i){var t=i(6),l=i(3);e.exports=function(e,n){if(l(e),!t(n)&&null!==n)throw TypeError("Can't set "+String(n)+" as a prototype")}},function(e,n,i){var t=i(56);e.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var e,n=!1,i={};try{(e=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set).call(i,[]),n=i instanceof Array}catch(e){}return function(i,l){return t(i,l),n?e.call(i,l):i.__proto__=l,i}}():void 0)},function(e,n,i){var t=i(0).document;e.exports=t&&t.documentElement},function(e,n,i){var t=i(28),l=i(13);e.exports=Object.keys||function(e){return t(e,l)}},function(e,n,i){var t=i(8),l=i(7),r=i(3),o=i(59);e.exports=t?Object.defineProperties:function(e,n){r(e);for(var i,t=o(n),a=t.length,c=0;a>c;)l.f(e,i=t[c++],n[i]);return e}},function(e,n,i){var t=i(3),l=i(60),r=i(13),o=i(15),a=i(58),c=i(34),p=i(16)("IE_PROTO"),y=function(){},h=function(){var e,n=c("iframe"),i=r.length;for(n.style.display="none",a.appendChild(n),n.src=String("javascript:"),(e=n.contentWindow.document).open(),e.write(" - - - - - + + + + + - - - - - + diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html index 2e49fde..8a6e060 100644 --- a/django/templates/label_edit.html +++ b/django/templates/label_edit.html @@ -255,10 +255,104 @@
    {% addtoblock "js" %} + + // Validate rate field (JSON object with currency codes) + const rateField = document.getElementById('id_rate'); + if (rateField) { + rateField.addEventListener('blur', function() { + try { + if (this.value && this.value.trim()) { + const parsed = JSON.parse(this.value); + if (typeof parsed !== 'object' || Array.isArray(parsed)) { + alert('Rate must be a JSON object like {"USD": 105620.00}'); + return; + } + // Validate values are numeric + for (const [key, value] of Object.entries(parsed)) { + if (typeof value !== 'number') { + alert(`Rate value for ${key} must be numeric`); + return; + } + } + } + } catch (e) { + alert('Invalid JSON format for rate field'); + } + }); + } + + // Validate fmv field (JSON object with currency codes) + const fmvField = document.getElementById('id_fmv'); + if (fmvField) { + fmvField.addEventListener('blur', function() { + try { + if (this.value && this.value.trim()) { + const parsed = JSON.parse(this.value); + if (typeof parsed !== 'object' || Array.isArray(parsed)) { + alert('FMV must be a JSON object like {"USD": 1233.45}'); + return; + } + // Validate values are numeric + for (const [key, value] of Object.entries(parsed)) { + if (typeof value !== 'number') { + alert(`FMV value for ${key} must be numeric`); + return; + } + } + } + } catch (e) { + alert('Invalid JSON format for fmv field'); + } + }); + } + + // Validate heights field (JSON array of integers) + const heightsField = document.getElementById('id_heights'); + if (heightsField) { + heightsField.addEventListener('blur', function() { + try { + if (this.value && this.value.trim()) { + const parsed = JSON.parse(this.value); + if (!Array.isArray(parsed)) { + alert('Heights must be a JSON array like [123456, 789012]'); + return; + } + // Validate all values are integers + for (const height of parsed) { + if (!Number.isInteger(height)) { + alert('All heights must be integers'); + return; + } + } + } + } catch (e) { + alert('Invalid JSON format for heights field'); + } + }); + } + + // Validate integer fields (height, fee, value) + const integerFields = ['id_height', 'id_fee', 'id_value']; + integerFields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field) { + field.addEventListener('blur', function() { + if (this.value && this.value.trim()) { + if (!/^-?\d+$/.test(this.value.trim())) { + alert(`${fieldId.replace('id_', '')} must be an integer`); + } + } + }); + } + }); + {% endaddtoblock %} - + {% endblock %} diff --git a/django/templates/label_edit_labeling.html b/django/templates/label_edit_labeling.html index 034cbcc..35416fc 100644 --- a/django/templates/label_edit_labeling.html +++ b/django/templates/label_edit_labeling.html @@ -168,6 +168,8 @@ +// Validate fmv field (JSON object with currency codes) +const fmvField = document.getElementById('id_fmv'); +if (fmvField) { + fmvField.addEventListener('blur', function() { + try { + if (this.value && this.value.trim()) { + const parsed = JSON.parse(this.value); + if (typeof parsed !== 'object' || Array.isArray(parsed)) { + alert('FMV must be a JSON object like {"USD": 1233.45}'); + return; + } + // Validate values are numeric (accept both numbers and numeric strings) + for (const [key, value] of Object.entries(parsed)) { + // Check if it's a number OR a numeric string + const isNumeric = typeof value === 'number' || + (typeof value === 'string' && !isNaN(parseFloat(value)) && isFinite(value)); + if (!isNumeric) { + alert(`FMV value for ${key} must be numeric (got: ${typeof value})`); + return; + } + } + } + } catch (e) { + alert('Invalid JSON format for fmv field'); + } + }); +} +// Validate rate field (JSON object with currency codes) +const rateField = document.getElementById('id_rate'); +if (rateField) { + rateField.addEventListener('blur', function() { + try { + if (this.value && this.value.trim()) { + const parsed = JSON.parse(this.value); + if (typeof parsed !== 'object' || Array.isArray(parsed)) { + alert('Rate must be a JSON object like {"USD": 105620.00}'); + return; + } + // Validate values are numeric (accept both numbers and numeric strings) + for (const [key, value] of Object.entries(parsed)) { + // Check if it's a number OR a numeric string + const isNumeric = typeof value === 'number' || + (typeof value === 'string' && !isNaN(parseFloat(value)) && isFinite(value)); + if (!isNumeric) { + alert(`Rate value for ${key} must be numeric (got: ${typeof value})`); + return; + } + } + } + } catch (e) { + alert('Invalid JSON format for rate field'); + } + }); +} + +// Validate heights field (JSON array of integers) +const heightsField = document.getElementById('id_heights'); +if (heightsField) { + heightsField.addEventListener('blur', function() { + try { + if (this.value && this.value.trim()) { + const parsed = JSON.parse(this.value); + if (!Array.isArray(parsed)) { + alert('Heights must be a JSON array like [123456, 789012]'); + return; + } + // Validate all values are integers + for (const height of parsed) { + if (!Number.isInteger(height)) { + alert('All heights must be integers'); + return; + } + } + } + } catch (e) { + alert('Invalid JSON format for heights field'); + } + }); +} + +// Validate integer fields (height, fee, value) +const integerFields = ['id_height', 'id_fee', 'id_value']; +integerFields.forEach(fieldId => { + const field = document.getElementById(fieldId); + if (field) { + field.addEventListener('blur', function() { + if (this.value && this.value.trim()) { + if (!/^-?\d+$/.test(this.value.trim())) { + alert(`${fieldId.replace('id_', '')} must be an integer`); + } + } + }); + } +}); + +{% endaddtoblock %} + {% endblock %} diff --git a/django/templates/labelbase.html b/django/templates/labelbase.html index 5b4ba2e..d893b5b 100644 --- a/django/templates/labelbase.html +++ b/django/templates/labelbase.html @@ -23,6 +23,31 @@
    {% if label_list %} + + +
    @@ -32,6 +57,8 @@ + + @@ -123,6 +150,7 @@ + +{% endblock %} diff --git a/django/templates/fill_missing_data.html b/django/templates/fill_missing_data.html new file mode 100644 index 0000000..b1d3f93 --- /dev/null +++ b/django/templates/fill_missing_data.html @@ -0,0 +1,128 @@ +{% extends "_base.html" %} +{% load i18n %} +{% load labelbase_tags %} +{% load sekizai_tags %} + +{% block content %} + +{% if labelbase %} +
    +
    +

    Fill Missing Data - {{ labelbase.name }}

    + {% if labelbase.fingerprint %}{{ labelbase.fingerprint }}{% endif %} + {% if labelbase.about %} +

    {{ labelbase.about }}

    + {% endif %} +
    +
    + +
    +

    + Auto-populate BIP-329 additional fields (height, time, value) for outputs from OutputStat data. +

    + + {% if can_fill_from_outputstat %} +
    + {{ can_fill_from_outputstat|length }} output labels can be filled! +
    + {% else %} +
    + All good! All {{ already_complete|length }} output labels have their fields populated. +
    + {% endif %} + + + {% if can_fill_from_outputstat %} +
    +
    +
    + {{ can_fill_from_outputstat|length }} Output labels can be filled from OutputStat + +
    +

    These output labels have OutputStat data available. Can populate: height, time, value.

    +
    +
    +
    +
      + {% for item in can_fill_from_outputstat %} +
    • +
      +
      + {{ item.label.get_type_display }}: {{ item.label.ref }}
      + Label: {{ item.label.label|default:"(empty)" }}
      + Missing fields: + {{ item.missing_fields|join:", " }}
      + Available data: + + height={{ item.output_stat.confirmed_at_block_height }}, + time={{ item.output_stat.confirmed_at_block_time }}, + value={{ item.output_stat.value }} sats + +
      +
      +
      + {% csrf_token %} + + + + + Edit +
      +
      +
    • + {% endfor %} +
    +
    + {% csrf_token %} + + + +
    +
    +
    + {% endif %} + + + {% if already_complete %} +
    +
    +
    + ✓ {{ already_complete|length }} Output labels already have all fields populated + +
    +

    These output labels have all applicable BIP-329 additional fields (height, time, value) populated.

    +
    +
    +
    +
      + {% for label in already_complete %} +
    • +
      +
      + {{ label.get_type_display }}: {{ label.ref }}
      + {{ label.label }} +
      +
      + Edit +
      +
      +
    • + {% endfor %} +
    +
    +
    +
    + {% endif %} + +
    + +{% else %} +

    Labelbase not found.

    +{% endif %} + + + +{% endblock %} diff --git a/django/templates/profile_update_fees.html b/django/templates/profile_update_fees.html new file mode 100644 index 0000000..44e0733 --- /dev/null +++ b/django/templates/profile_update_fees.html @@ -0,0 +1,40 @@ +{% extends "_base.html" %} +{% load i18n %} +{% load sekizai_tags %} +{% load bootstrap %} + +{% block content %} +{% include "profile_header_menu.html" %} +
    +{% block profile_content %}{% endblock %} +
    + +

    + + + Define the fee for new transactions. This will be used to evaluate the health of your unspent transaction outputs. + +

    + 🟢 Healthy fee: < 1% (fee/amount < 0.01)
    + 🟡 Warning: 1-3%
    + 🔴 High: > 3%
    + + + +
    + {% csrf_token %} +

    label origin spendablehealth
    + {{ form|bootstrap }} + + + + + + + +
     
    + +
    + +

    +{% endblock %} diff --git a/django/userprofile/migrations/0019_profile_my_fee.py b/django/userprofile/migrations/0019_profile_my_fee.py new file mode 100644 index 0000000..40457a7 --- /dev/null +++ b/django/userprofile/migrations/0019_profile_my_fee.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2025-12-02 22:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userprofile', '0018_profile_use_attachments'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='my_fee', + field=models.IntegerField(default=1), + ), + ] diff --git a/django/userprofile/migrations/0020_profile_my_fee_rate_threshold.py b/django/userprofile/migrations/0020_profile_my_fee_rate_threshold.py new file mode 100644 index 0000000..829ab35 --- /dev/null +++ b/django/userprofile/migrations/0020_profile_my_fee_rate_threshold.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2025-12-02 22:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userprofile', '0019_profile_my_fee'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='my_fee_rate_threshold', + field=models.IntegerField(default=5), + ), + ] diff --git a/django/userprofile/migrations/0021_auto_20251206_0811.py b/django/userprofile/migrations/0021_auto_20251206_0811.py new file mode 100644 index 0000000..6df22dc --- /dev/null +++ b/django/userprofile/migrations/0021_auto_20251206_0811.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2025-12-06 08:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userprofile', '0020_profile_my_fee_rate_threshold'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='my_fee', + field=models.IntegerField(default=1, help_text='Fee used when broadcasting transactions'), + ), + migrations.AlterField( + model_name='profile', + name='my_fee_rate_threshold', + field=models.IntegerField(default=1, help_text='Overwrite healthy fee'), + ), + ] From f18bce10b0381b2c35c9bdf1085109af328b6867 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Wed, 17 Dec 2025 23:42:52 +0100 Subject: [PATCH 43/50] . --- .../migrations/0012_auto_20251122_0800.py | 54 ++++++++ django/labellabor/urls.py | 6 +- django/labellabor/views.py | 79 +++++++++++- django/templates/home.html | 15 ++- .../templates/label_edit_output_details.html | 115 ++++++++++++++---- 5 files changed, 240 insertions(+), 29 deletions(-) create mode 100644 django/labelbase/migrations/0012_auto_20251122_0800.py diff --git a/django/labelbase/migrations/0012_auto_20251122_0800.py b/django/labelbase/migrations/0012_auto_20251122_0800.py new file mode 100644 index 0000000..c84a26b --- /dev/null +++ b/django/labelbase/migrations/0012_auto_20251122_0800.py @@ -0,0 +1,54 @@ +# Generated by Django 3.2.25 on 2025-11-22 08:00 + +from django.db import migrations, models +import django_cryptography.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('labelbase', '0011_alter_labelbase_operation_mode'), + ] + + operations = [ + migrations.AddField( + model_name='label', + name='fee', + field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='Transaction fee in satoshis (stored as string)', max_length=32, null=True)), + ), + migrations.AddField( + model_name='label', + name='fmv', + field=django_cryptography.fields.encrypt(models.TextField(blank=True, help_text='Fair market value (JSON string)', null=True)), + ), + migrations.AddField( + model_name='label', + name='height', + field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='Block height where transaction was confirmed', max_length=16, null=True)), + ), + migrations.AddField( + model_name='label', + name='heights', + field=django_cryptography.fields.encrypt(models.TextField(blank=True, help_text='Block heights for address activity (JSON array as string)', null=True)), + ), + migrations.AddField( + model_name='label', + name='keypath', + field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='Key derivation path (e.g., /1/123)', max_length=256, null=True)), + ), + migrations.AddField( + model_name='label', + name='rate', + field=django_cryptography.fields.encrypt(models.TextField(blank=True, help_text='Exchange rate at transaction time (JSON string)', null=True)), + ), + migrations.AddField( + model_name='label', + name='time', + field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='ISO-8601 timestamp of the block', max_length=64, null=True)), + ), + migrations.AddField( + model_name='label', + name='value', + field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='Transaction value in satoshis, signed (stored as string)', max_length=32, null=True)), + ), + ] diff --git a/django/labellabor/urls.py b/django/labellabor/urls.py index 0bc5db6..d6c341f 100644 --- a/django/labellabor/urls.py +++ b/django/labellabor/urls.py @@ -52,7 +52,8 @@ from .views import ( CurrencySyncView, CurrencySyncActionView, FillMissingDataView, - FillMissingDataActionView + FillMissingDataActionView, + FillOutputFieldsActionView ) @@ -178,6 +179,9 @@ urlpatterns = [ FillMissingDataView.as_view(), name='fill_missing_data' ), + path('label//fill-output-fields/', + FillOutputFieldsActionView.as_view(), + name='fill_output_fields_action'), path( 'labelbase//fill-missing-data/action/', FillMissingDataActionView.as_view(), diff --git a/django/labellabor/views.py b/django/labellabor/views.py index deab468..f2ba030 100644 --- a/django/labellabor/views.py +++ b/django/labellabor/views.py @@ -1098,6 +1098,7 @@ class LabelUpdateView(UpdateView): context["active_labelbase_id"] = self.object.labelbase.id context["labelbase"] = self.object.labelbase context["action"] = self.kwargs.get('action', 'update') + if context["action"] in ["labeling", "derive-addresses"]: context["labelform"] = LabelForm( request=self.request, labelbase_id=self.object.labelbase.id @@ -1107,15 +1108,32 @@ class LabelUpdateView(UpdateView): mempool_api = self.object.labelbase.get_mempool_api() context["res_tx"] = mempool_api.get_transaction(self.object.ref) - if self.object.type == "xpub": context["address_count"] = self.request.GET.get("address_count", DEFAULT_DERIVE_ADDRESS_COUNT) context["offset"] = int(self.request.GET.get("offset", 0)) if self.object.type == "output": - context["output"] = OutputStat.objects.filter( - user=self.object.labelbase.user, - type_ref_hash=self.object.type_ref_hash).last() + output_stat = OutputStat.objects.filter( + user=self.object.labelbase.user, + type_ref_hash=self.object.type_ref_hash + ).last() + context["output"] = output_stat + + # Convert Unix timestamp to human-readable UTC formats + if output_stat and output_stat.confirmed_at_block_time: + dt = datetime.utcfromtimestamp(output_stat.confirmed_at_block_time) + context["output_block_time_utc"] = dt.strftime('%Y-%m-%d %H:%M:%S') + context["output_block_time_iso"] = dt.strftime('%Y-%m-%dT%H:%M:%SZ') + + # Check for missing BIP-329 fields + if context["action"] == "output-details": + missing_fields = [] + applicable_fields = ['height', 'time', 'value'] + for field in applicable_fields: + value = getattr(self.object, field, None) + if not value or (isinstance(value, str) and not value.strip()): + missing_fields.append(field) + context["missing_fields"] = missing_fields return context @@ -1348,3 +1366,56 @@ class FillMissingDataActionView(View): label.save() return updated + + +class FillOutputFieldsActionView(View): + """Single output field fill action from output-details page""" + def post(self, request, *args, **kwargs): + label_id = self.kwargs["label_id"] + label = get_object_or_404( + Label, + id=label_id, + labelbase__user_id=request.user.id, + type='output' + ) + + # Reuse the existing fill logic + if self._fill_label_from_outputstat(label): + messages.success(request, "Successfully filled BIP-329 fields from OutputStat data.") + else: + messages.error(request, "Could not fill fields. OutputStat data may not be available.") + + return HttpResponseRedirect( + reverse('edit_label', kwargs={'pk': label_id}) + '?action=output-details' + ) + + def _fill_label_from_outputstat(self, label): + """Fill a single label from OutputStat data (reused from FillMissingDataActionView)""" + output_stat = OutputStat.objects.filter( + user=label.labelbase.user, + type_ref_hash=label.type_ref_hash, + network=label.labelbase.network + ).first() + + if not output_stat: + return False + + updated = False + + if not label.height and output_stat.confirmed_at_block_height: + label.height = str(output_stat.confirmed_at_block_height) + updated = True + + if not label.time and output_stat.confirmed_at_block_time: + dt = datetime.utcfromtimestamp(output_stat.confirmed_at_block_time) + label.time = dt.strftime('%Y-%m-%dT%H:%M:%SZ') + updated = True + + if not label.value and output_stat.value: + label.value = str(output_stat.value) + updated = True + + if updated: + label.save() + + return updated diff --git a/django/templates/home.html b/django/templates/home.html index 178e505..8ee6db2 100644 --- a/django/templates/home.html +++ b/django/templates/home.html @@ -320,18 +320,27 @@
    -

    Auto-Fill Missing Data

    -

    Automatically populate transaction metadata from OutputStat records. Fill block heights, timestamps, and values for your outputs with one click, saving time and ensuring accuracy.

    +

    Bulk Auto-Fill Missing Data

    +

    Automatically bulk populate transaction metadata from OutputStat records. Fill block heights, timestamps, and values for your outputs with one click, saving time and ensuring accuracy.

    +
    +
    +

    One-Click BIP-329 Field Population

    +

    Automatically populate transaction metadata like block height, timestamp, and value directly from OutputStat records. View missing fields at a glance and fill them with a single click from the output details page.

    +
    +
    + +

    Fee Health Monitoring

    Instantly visualize the cost-effectiveness of spending your UTXOs. Labelbase calculates and displays fee health status for each spendable output, color-coded to show if transaction fees would consume a healthy percentage of the output value.

    - + +



    diff --git a/django/templates/label_edit_output_details.html b/django/templates/label_edit_output_details.html index 0025a8e..b471888 100644 --- a/django/templates/label_edit_output_details.html +++ b/django/templates/label_edit_output_details.html @@ -1,35 +1,108 @@ {% extends "label_edit.html" %} {% load bootstrap %} {% load i18n %} - {% block label_edit_content %} - + {% if action == "output-details" %} {% if form.instance.type == "output" %} -
    - -
     
    -    Output, ref:  {{ label.ref }}
    -    Output value, in sats: {{ output.value }}
    -    Confirmed at block height: {{ output.confirmed_at_block_height }}
    -    Confirmed at block time: {{ output.confirmed_at_block_time }}
    -    Network: {{ output.get_network_display }}
    -    Spent: {{ output.get_spent_status }} 
    -    {% if output.next_input_attributes %}
    -    Fee estimation will be made based on:
    +  
    +  {% if missing_fields %}
    +  
    + Missing BIP-329 fields: + {% for field in missing_fields %} + {{ field }} + {% endfor %} +
    Click the button below to populate from OutputStat data. +
    + {% else %} +
    + ✓ All fields populated! This label has all applicable BIP-329 fields filled. +
    + {% endif %} - {{ output.next_input_attributes }} + +
    +
    +
    Output Details & BIP-329 Fields
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if output.next_input_attributes %} + + + + + + + {% endif %} +
    Output ref:{{ label.ref }}
    Network:{{ output.get_network_display }}
    Spent status:{{ output.get_spent_status }}
    BIP-329 Fields:
    value: + {% if label.value %} + {{ label.value }} sats + {% else %} + {{ output.value }} sats (available from OutputStat) + {% endif %} +
    height: + {% if label.height %} + {{ label.height }} + {% else %} + {{ output.confirmed_at_block_height }} (available from OutputStat) + {% endif %} +
    time: + {% if label.time %} + {{ label.time }} + {% else %} + {{ output_block_time_utc }} UTC (available from OutputStat) + {% endif %} +
    Fee Estimation:
    + {{ output.next_input_attributes }} + {% if output.next_input_attributes.input_n %} +
    Assuming {{ output.next_input_attributes.input_m }}-of-{{ output.next_input_attributes.input_n }} multisig + {% endif %} +
    - {% if output.next_input_attributes.input_n %} - * Assuming {{ output.next_input_attributes.input_m }}-of-{{ output.next_input_attributes.input_n }} multisig - {% else %} + + {% if missing_fields %} +
    + {% csrf_token %} + +
    {% endif %} - {% endif %} +
    +
    -
    {% endif %} {% endif %} {% endblock %} From e9cd7491511177b4531bc97e364ae935a082d19f Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Thu, 18 Dec 2025 00:32:47 +0100 Subject: [PATCH 44/50] . --- django/labellabor/views.py | 34 ++++- django/templates/_base.html | 6 +- django/templates/home.html | 7 +- django/templates/labelbase_tree_maps.html | 6 + .../labelbase_tree_maps_unspent_outputs.html | 128 +++++++++++------- django/templates/profile_update_fees.html | 8 +- django/userprofile/models.py | 13 +- 7 files changed, 145 insertions(+), 57 deletions(-) diff --git a/django/labellabor/views.py b/django/labellabor/views.py index f2ba030..3f5594f 100644 --- a/django/labellabor/views.py +++ b/django/labellabor/views.py @@ -564,7 +564,7 @@ class TreeMapsView(ListView): context['action'] = self.kwargs.get('action', 'unspent-outputs') return context - def get_queryset(self): + def get_queryset_OLD(self): #action = self.kwargs.get('action', 'unspent-outputs') label_ids = [] @@ -596,6 +596,38 @@ class TreeMapsView(ListView): qs = Label.objects.none() return qs.order_by("id") + def get_queryset(self): + label_ids = [] + + qs = Label.objects.filter( + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["pk"], + ) + + action = self.kwargs.get('action', 'unspent-outputs') + + for l in qs: + if l.type == "output": + output = OutputStat.objects.filter( + user=l.labelbase.user, + type_ref_hash=l.type_ref_hash, + network=l.labelbase.network).last() + if output and output.spent is False: + # For fee-efficiency, only show spendable outputs + if action == 'fee-efficiency': + if l.spendable is True: + label_ids.append(l.id) + else: + label_ids.append(l.id) + + if label_ids: + qs = qs.filter(id__in=label_ids, + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["pk"]) + else: + qs = Label.objects.none() + return qs.order_by("id") + class LabelbasePortfolioView(LabelbaseView): template_name = "labelbase_portfolio.html" diff --git a/django/templates/_base.html b/django/templates/_base.html index a07b2fc..464f5a0 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -185,12 +185,12 @@ {% endwith %}
  • Fix & Manage
  • Sync Fields
  • - {% comment %}
  • UTXOs Health
  • {% endcomment %} -
  • Tree Map
  • - {% comment %}
  • Stats & KPIs
  • {% endcomment %} {% if request.user.profile.use_fiatfinances %}
  • Fiat Finances
  • {% endif %} + {% comment %}
  • UTXOs Health
  • {% endcomment %} +
  • Tree Map
  • + {% comment %}
  • Stats & KPIs
  • {% endcomment %}
  • Import
  • Export
  • {% if labelbase.user.profile.use_hashtags %} diff --git a/django/templates/home.html b/django/templates/home.html index 8ee6db2..51e0c97 100644 --- a/django/templates/home.html +++ b/django/templates/home.html @@ -339,7 +339,12 @@

    Instantly visualize the cost-effectiveness of spending your UTXOs. Labelbase calculates and displays fee health status for each spendable output, color-coded to show if transaction fees would consume a healthy percentage of the output value.

    - +
    +
    +

    UTXO Fee Efficiency Visualization

    +

    Visualize the cost-effectiveness of spending your UTXOs with color-coded treemaps. See at a glance which outputs have healthy fee-to-value ratios and which would be expensive to spend, helping you make informed decisions about consolidation and transaction planning.

    +
    +
    diff --git a/django/templates/labelbase_tree_maps.html b/django/templates/labelbase_tree_maps.html index 36e80e1..b18d500 100644 --- a/django/templates/labelbase_tree_maps.html +++ b/django/templates/labelbase_tree_maps.html @@ -16,6 +16,12 @@ + diff --git a/django/templates/labelbase_tree_maps_unspent_outputs.html b/django/templates/labelbase_tree_maps_unspent_outputs.html index 56fbe2a..326ceb2 100644 --- a/django/templates/labelbase_tree_maps_unspent_outputs.html +++ b/django/templates/labelbase_tree_maps_unspent_outputs.html @@ -11,14 +11,26 @@ - -{% if labelbase.is_testnet %} +{% if action == "fee-efficiency" %} +
    + +
    + Fee Efficiency (VTER): Color shows how efficient it is to spend each output based on the fee-to-value ratio. +

    + 🟢 Healthy: Fee < {{ request.user.profile.my_fee_threshold_healthy }}% of value
    + 🟡 Warning: Fee {{ request.user.profile.my_fee_threshold_healthy }}-{{ request.user.profile.my_fee_threshold_warning }}% of value
    + 🔴 High: Fee > {{ request.user.profile.my_fee_threshold_warning }}% of value +

    + Box size represents output value in sats. Thresholds based on your fee settings. Adjust in Profile → Fees +
    +
    +{% elif labelbase.is_testnet %}
    Important Notice for Testnet Transactions: Testnet coins hold no real-world value and are solely intended for testing purposes.
    -
    {% else %}
    @@ -28,6 +40,7 @@ If the UTXO value is not provided, Labelbase will estimate it based on historica
    {% endif %} +