labelbase/django/templates/label_edit.html
Xavier Fiechter fff7ac548c wip
2025-12-12 23:47:04 +01:00

314 lines
13 KiB
HTML

{% extends "_base.html" %}
{% load i18n %}
{% load sekizai_tags %}
{% load labelbase_tags %}
{% load backgroundtask_tags %}
{% load attachments_tags %}
{% block title %}{{ object.type }} {{ object.ref }}{% endblock %}
{% block content %}
{% get_attachments_for object.get_label_attachment as my_attachments %}
<div class="row">
<ul class="nav nav-tabs" style="padding-top: 2rem;">
<li class="nav-item">
<a class="nav-link {% if action == "update" %}active{% endif %}" aria-current="page" href="{% url 'edit_label' object.id %}">Label Detail</a>
</li>
{% if object.type == "tx" %}
<li class="nav-item">
<a class="nav-link {% if action == "labeling" %}active{% endif %}" href="{% url 'edit_label' object.id %}labeling/">Transaction Labeling</a>
</li>
{% endif %}
{% if object.type == "output" %}
<li class="nav-item">
<a class="nav-link {% if action == "output-details" %}active{% endif %}" href="{% url 'edit_label' object.id %}output-details/">Output Details</a>
</li>
{% endif %}
{% if object.type == "xpub" %}
<li class="nav-item">
{% switch object.ref|slice:":4" %}
{% case "xpub" %}
{% case "tpub" %}
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/?derivation=m/44&address_count={{ address_count }}&offset={{ offset }}">Derive Addresses</a>
{% case "ypub" %}
{% case "upub" %}
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/?derivation=m/49&address_count={{ address_count }}&offset={{ offset }}">Derive Addresses</a>
{% case "zpub" %}
{% case "vpub" %}
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/?derivation=m/84&address_count={{ address_count }}&offset={{ offset }}">Derive Addresses</a>
{% else %}
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/">Derive Addresses</a>
{% endswitch %}
</li>
{% endif %}
{% if object.labelbase.user.profile.use_attachments %}
<li class="nav-item">
{% is_self_hosted as my_labelbase_is_self_hosted %}
{% if my_labelbase_is_self_hosted %}
<a class="nav-link {% if action == "attachments" %}active{% endif %}" aria-current="page" href="{% url 'edit_label' object.id %}attachments/">
Attachments
{% if my_attachments.count %}
<span class="badge text-bg-secondary top-0 rounded-pill">{% attachments_count object.get_label_attachment %}</span>
{% endif %}
</a>
{% else %}
<a class="nav-link disabled">Attachments</a>
{% endif %}
</li>
{% endif %}
{% if object.ref == "54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713" %}
<li class="nav-item">
<a class="nav-link" href="/static/bitcoin.pdf">Bitcoin White Paper</a>
</li>
{% endif %}
</ul>
</div>
<div style="padding-top: 0rem; padding-bottom: 2.1rem; ">
{% if output.last_error %}
<div class="alert bd-callout bd-callout-danger">
<div class="btn-group" role="group" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;">
<button type="button" class="btn btn-sm btn-outline-danger dropdown-toggle " data-bs-toggle="dropdown" aria-expanded="false">
Actions
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=none">Verify output status</a></li>
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=true">Mark output as spent</a></li>
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=false">Mark output as unspent</a></li>
</ul>
</div>
<strong>Ouch!</strong> Got "{{ output.last_error.message }}{% if output.last_error.code %} (code {{ output.last_error.code }}){% endif %}" from Electrum.
</div>
{% endif %}
{% is_label_id_in_queue object.id as is_in_queue %}
{% if is_in_queue %}
<div class="alert bd-callout bd-callout-info">
<strong>Output in queue!</strong> This output is currently in the processing queue. It will be checked shortly.
</div>
{% else %}
{% if object.type == "output" %}
{% if output.get_spent_status == "spent" %}
<div class="bd-callout bd-callout-warning">
<strong>Output spent!</strong> Blockchain records indicate that this output has been spent in another transaction.
</div>
{% elif output.get_spent_status == "unspent" %}
<div class="bd-callout bd-callout-good">
<strong>Output unspent!</strong> Blockchain records indicate that this output has not been spent yet.
</div>
{% elif output.get_spent_status == "unconfirmed" %}
<div class="alert bd-callout bd-callout-info">
<div class="btn-group" role="group" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;">
<button type="button" class="btn btn-sm btn-outline-info dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Actions
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=none">
Verify output status
</a>
</li>
</ul>
</div>
<strong>Output unconfirmed!</strong> Blockchain records indicate that this output has not been confirmed yet.
</div>
{% else %}
<div class="bd-callout bd-callout-warning">
<strong>Unknown status:</strong> The status of this output could not be determined.
</div>
{% endif %}
{% endif %}
{% endif %}
{% block label_edit_content %}
{% endblock %}
</div>
<div class="modal" tabindex="-1" id="deleteLabelModal">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete label?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning" role="alert">
<strong>Warning:</strong>
<br>
<br>
This action will permanently delete the label and cannot be undone. Are you sure you want to proceed?
</div>
</div>
<form method="post" action="{% url 'del_label' object.pk %}">{% csrf_token %}
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">CANCEL</button>
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">DELETE</button>
</div>
</form>
</div>
</div>
</div>
<script>
{% addtoblock "js" %}
// BIP-329 field visibility based on type
const fieldsByType = {
'tx': ['origin', 'height', 'time', 'fee', 'value', 'rate'],
'addr': ['origin', 'keypath', 'heights'],
'pubkey': ['origin', 'keypath'],
'input': ['origin', 'keypath', 'value', 'fmv', 'height', 'time'],
'output': ['origin', 'spendable', 'keypath', 'value', 'fmv', 'height', 'time'],
'xpub': ['origin']
};
function updateFieldVisibility() {
const typeField = document.getElementById('id_type');
if (!typeField) return;
const selectedType = typeField.value;
const allowedFields = fieldsByType[selectedType] || [];
// All additional fields (excluding core fields: type, ref, label)
const allAdditionalFields = ['origin', 'spendable', 'height', 'time', 'fee', 'value', 'rate', 'keypath', 'fmv', 'heights'];
allAdditionalFields.forEach(fieldName => {
const field = document.getElementById(`id_${fieldName}`);
if (field) {
// Find the parent form-group/control-group div
const wrapper = field.closest('.form-group') || field.closest('.control-group') || field.closest('.mb-3') || field.parentElement.parentElement;
if (wrapper) {
if (allowedFields.includes(fieldName)) {
wrapper.style.display = '';
field.removeAttribute('disabled');
} else {
wrapper.style.display = 'none';
field.setAttribute('disabled', 'disabled');
field.value = ''; // Clear hidden fields
}
}
}
});
}
// Run on page load
document.addEventListener('DOMContentLoaded', function() {
updateFieldVisibility();
// Run when type changes
const typeField = document.getElementById('id_type');
if (typeField) {
typeField.addEventListener('change', updateFieldVisibility);
}
});
// 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 %}
</script>
{% endblock %}