{% extends "console/layout.html" %}
{% block title %}{{ "addons.title" | t }} · {{ site_name | default('RustPBX') }}{% endblock %}
{% block content %}
<div class="p-4 sm:p-6" x-data="addonsPage()">
<div class="mx-auto max-w-7xl space-y-6">
<div class="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div class="space-y-0.5">
<p class="text-[11px] font-semibold uppercase tracking-wide text-sky-600">{{ "notifications.system" | t
}}</p>
{% if commerce_enabled %}
<h1 class="text-2xl font-semibold text-slate-900">{{ "addons.title" | t }} & {{ "licenses.title" | t
}}</h1>
<p class="text-sm text-slate-500 sm:max-w-xl">{{ "addons.manage_addons_licenses" | t }}</p>
{% else %}
<h1 class="text-2xl font-semibold text-slate-900">{{ "addons.title" | t }}</h1>
<p class="text-sm text-slate-500 sm:max-w-xl">{{ "addons.manage_addons" | t }}</p>
{% endif %}
</div>
{% if commerce_enabled %}
<a href="https://miuda.ai/try" target="_blank"
class="inline-flex items-center gap-2 rounded-md bg-sky-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-500 transition-colors self-start lg:self-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
{{ "addons.get_renew" | t }}
</a>
{% endif %}
</div>
{# Banner: shown when at least one commercial addon has no valid license #}
{% if commerce_enabled and has_unlicensed_commercial %}
<div data-unlicensed-banner class="rounded-lg bg-amber-50 border border-amber-200 p-4 flex items-start gap-3">
<svg class="h-5 w-5 text-amber-500 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd" />
</svg>
<div class="flex-1">
<p class="text-sm font-medium text-amber-800">{{ "addons.unlicensed_title" | t }}</p>
<p class="text-xs text-amber-700 mt-0.5">
{{ "addons.unlicensed_desc" | t }}
<a href="https://miuda.ai/try" target="_blank" class="underline font-medium">{{
"addons.get_license_at" | t }}</a>
</p>
</div>
</div>
{% endif %}
<div class="border-b border-slate-200">
<nav class="-mb-px flex gap-6" aria-label="Tabs">
<button type="button"
class="whitespace-nowrap border-b-2 px-1 pb-3 text-sm font-medium transition-colors" :class="activeTab === 'addons'
? 'border-sky-600 text-sky-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'"
@click="setTab('addons')">
{{ "addons.addons_tab" | t }}
</button>
{% if commerce_enabled %}
<button type="button"
class="whitespace-nowrap border-b-2 px-1 pb-3 text-sm font-medium transition-colors" :class="activeTab === 'licenses'
? 'border-sky-600 text-sky-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'"
@click="setTab('licenses')">
{{ "addons.licenses_tab" | t }}
</button>
{% endif %}
</nav>
</div>
<div x-show="activeTab === 'addons'">
<section class="overflow-hidden rounded-xl bg-white ring-1 ring-black/5">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-200 text-left text-sm">
<thead class="bg-slate-50 text-xs font-semibold uppercase tracking-wide text-slate-500">
<tr>
<th scope="col" class="px-4 py-2">{{ "addons.name" | t }}</th>
<th scope="col" class="px-4 py-2">{{ "addons.description" | t }}</th>
{% if commerce_enabled %}
<th scope="col" class="px-4 py-2">{{ "addons.license" | t }}</th>
{% endif %}
<th scope="col" class="px-4 py-2">{{ "addons.status" | t }}</th>
<th scope="col" class="px-4 py-2">{{ "addons.actions" | t }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 bg-white text-sm text-slate-600">
{% for addon in addons %}
<tr class="hover:bg-slate-50">
<td class="whitespace-nowrap px-4 py-2">
<div class="font-medium text-slate-900">
<a href="{{ base_path | safe }}/addons/{{ addon.id }}"
class="hover:text-sky-600">{{
addon.name }}</a>
</div>
<div class="text-xs text-slate-400">{{ addon.id }}</div>
{% if addon.restart_required %}
<span
class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-800 ring-1 ring-inset ring-yellow-600/20 mt-1">{{
"addons.restart_required" | t }}</span>
{% endif %}
</td>
<td class="px-4 py-2">
<div>{{ addon.description }}</div>
<div class="mt-1 text-xs text-slate-500">
<span class="font-semibold">{{ addon.category }}</span>
{% if addon.bundle %}
<span class="mx-1">·</span>
<span>{{ addon.bundle }}</span>
{% endif %}
</div>
</td>
{% if commerce_enabled %}
<td class="whitespace-nowrap px-4 py-2">
{% if addon.license_status %}
{% if addon.license_status == "Valid" %}
<span
class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">Valid</span>
{% elif addon.license_status == "Expired" %}
<span
class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10">Expired</span>
{% elif addon.license_status == "Not Licensed" %}
<span
class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-yellow-600/10">Not
Licensed</span>
{% else %}
<span
class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/10">{{
addon.license_status }}</span>
{% endif %}
{% if addon.license_expiry %}
<div class="text-xs text-slate-500 mt-1">Exp: {{ addon.license_expiry }}</div>
{% endif %}
{% else %}
{% if addon.category == "Commercial" %}
<button type="button" @click="setTab('licenses')"
class="inline-flex items-center gap-1 text-sky-600 hover:text-sky-800 text-xs font-medium">
<svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11H9v2H7v2h2v2h2v-2h2v-2h-2V7z"
clip-rule="evenodd" />
</svg>
Add License
</button>
{% else %}
<span class="text-slate-400 text-xs">Free</span>
{% endif %}
{% endif %}
</td>
{% endif %}
<td class="whitespace-nowrap px-4 py-2">
<button type="button"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-600 focus:ring-offset-2"
:class="{{ addon.enabled|lower }} ? 'bg-sky-600' : 'bg-slate-200'"
@click="toggleAddon('{{ addon.id }}', {{ addon.enabled|lower }})">
<span class="sr-only">Use setting</span>
<span aria-hidden="true"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
:class="{{ addon.enabled|lower }} ? 'translate-x-5' : 'translate-x-0'"></span>
</button>
</td>
<td class="px-4 py-2 whitespace-nowrap">
{% if addon.config_url %}
<a href="{{ addon.config_url }}"
class="text-sky-600 hover:text-sky-900 text-sm font-medium">Configure</a>
{% endif %}
{% if commerce_enabled and addon.category == "Commercial" %}
{% if addon.license_status == "Valid" %}
<a href="{{ base_path | safe }}/addons/{{ addon.id }}"
class="ml-2 text-green-600 hover:text-green-800 text-xs font-medium">Licensed
✓</a>
{% elif addon.license_status == "Expired" %}
<button type="button" @click="setTab('licenses')"
class="text-red-600 hover:text-red-800 text-xs font-medium">Renew
License</button>
{% else %}
<button type="button" @click="setTab('licenses')"
class="text-sky-600 hover:text-sky-800 text-xs font-medium">Add License</button>
{% endif %}
{% elif not addon.config_url %}
<span class="text-slate-400 text-xs">—</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="px-4 py-6 text-center text-sm text-slate-500">
No addons installed.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
{% if commerce_enabled %}
<div x-show="activeTab === 'licenses'" x-data="licenseForm()" class="space-y-4">
<div class="rounded-xl bg-white p-5 shadow-sm ring-1 ring-black/5">
<h2 class="text-sm font-semibold text-slate-800 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-4 text-slate-400">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
</svg>
Add / Update a License Key
</h2>
<div>
<label class="block text-xs font-medium text-slate-500 mb-1">License Key</label>
<div class="flex rounded-md shadow-sm">
<input type="text" x-model="licenseKey"
class="block w-full rounded-none rounded-l-md border border-slate-200 px-3 py-2 text-sm text-slate-700 placeholder-slate-400 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200"
placeholder="LICENSE-XXXX-XXXX-XXXX" @keydown.enter="verifyLicense">
<button type="button" @click="verifyLicense" :disabled="loading"
class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 disabled:opacity-60 disabled:cursor-not-allowed transition-colors">
<svg x-show="loading" class="animate-spin h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
<span x-show="!loading">Verify</span>
<span x-show="loading" x-cloak>Verifying…</span>
</button>
</div>
</div>
<div x-show="error" x-cloak
class="mt-3 rounded-md bg-red-50 border border-red-200 p-3 flex items-start gap-2">
<svg class="h-4 w-4 text-red-500 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clip-rule="evenodd" />
</svg>
<p class="text-xs text-red-700" x-text="error"></p>
</div>
<div x-show="verifiedPlan" x-cloak class="mt-3 rounded-md bg-green-50 border border-green-200 p-3">
<div class="flex items-start gap-2">
<svg class="h-4 w-4 text-green-600 flex-shrink-0 mt-0.5" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
<div>
<p class="text-xs font-semibold text-green-800">License verified and saved!</p>
<p class="text-xs text-green-700 mt-0.5">
Plan: <span class="font-medium" x-text="verifiedPlan"></span>
<template x-if="verifiedExpiry">
<span> — valid until <span class="font-medium"
x-text="verifiedExpiry"></span></span>
</template>
<template x-if="!verifiedExpiry">
<span> — <span class="font-medium">lifetime</span></span>
</template>
</p>
<template x-if="verifiedScope && verifiedScope.length > 0">
<p class="text-xs text-green-700 mt-0.5">
Covers: <span class="font-medium" x-text="verifiedScope.join(', ')"></span>
</p>
</template>
<template x-if="!verifiedScope || verifiedScope.length === 0">
<p class="text-xs text-green-700 mt-0.5">Covers: <span class="font-medium">All
addons</span></p>
</template>
</div>
</div>
<p class="text-xs text-green-600 mt-2">{{ "licenses.license_active" | t }}</p>
</div>
</div>
{% if licenses | length == 0 %}
<div class="rounded-xl bg-white p-10 shadow-sm ring-1 ring-black/5 text-center">
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-7 text-slate-400">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" />
</svg>
</div>
<h3 class="mt-4 text-base font-semibold text-slate-900">No license keys configured</h3>
<p class="mt-1 text-sm text-slate-500">
Enter your license key above to verify and save it, or get one at miuda.ai.
</p>
<div class="mt-6 flex items-center justify-center gap-3">
<a href="https://miuda.ai/try" target="_blank"
class="inline-flex items-center gap-2 rounded-md bg-sky-600 px-4 py-2 text-sm font-semibold text-white hover:bg-sky-500 transition-colors">
Get a License
</a>
</div>
</div>
{% else %}
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
{% for lic in licenses %}
<div class="rounded-xl bg-white p-5 shadow-sm ring-1 ring-black/5 flex flex-col gap-3">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Key</p>
<p class="mt-0.5 text-sm font-semibold text-slate-900 truncate font-mono">{{ lic.key_name }}
</p>
</div>
{% if lic.status == "Valid" %}
<span
class="flex-shrink-0 inline-flex items-center gap-1 rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-semibold text-green-700 ring-1 ring-inset ring-green-600/20">
<svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" />
</svg>
Valid
</span>
{% elif lic.status == "Expired" or lic.status == "Trial Expired" %}
<span
class="flex-shrink-0 inline-flex items-center gap-1 rounded-full bg-red-50 px-2.5 py-0.5 text-xs font-semibold text-red-700 ring-1 ring-inset ring-red-600/20">
<svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clip-rule="evenodd" />
</svg>
{{ lic.status }}
</span>
{% elif lic.is_trial %}
<span
class="flex-shrink-0 inline-flex items-center gap-1 rounded-full bg-sky-50 px-2.5 py-0.5 text-xs font-semibold text-sky-700 ring-1 ring-inset ring-sky-600/20">
{{ lic.status }}
</span>
{% else %}
<span
class="flex-shrink-0 inline-flex items-center gap-1 rounded-full bg-yellow-50 px-2.5 py-0.5 text-xs font-semibold text-yellow-700 ring-1 ring-inset ring-yellow-600/20">
<svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd" />
</svg>
{{ lic.status }}
</span>
{% endif %}
</div>
<dl class="grid grid-cols-2 gap-x-4 gap-y-2 text-xs">
{% if lic.plan %}
<div>
<dt class="font-medium text-slate-500">Plan</dt>
<dd class="mt-0.5 text-slate-800 font-semibold">{{ lic.plan }}</dd>
</div>
{% endif %}
<div>
<dt class="font-medium text-slate-500">Expires</dt>
<dd class="mt-0.5 text-slate-800">{{ lic.expiry | default('Lifetime') }}</dd>
</div>
</dl>
<div>
<p class="text-xs font-medium text-slate-500">Covers</p>
{% if lic.scope %}
<div class="mt-1 flex flex-wrap gap-1">
{% for addon_id in lic.scope %}
<span
class="inline-flex rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700">{{
addon_id }}</span>
{% endfor %}
</div>
{% else %}
<p class="mt-0.5 text-xs text-slate-600 font-medium">All addons</p>
{% endif %}
</div>
<div class="border-t border-slate-100 pt-3">
<p class="text-xs font-medium text-slate-500">Assigned to</p>
<div class="mt-1 flex flex-wrap gap-1">
{% for addon_id in lic.addon_ids %}
<a href="{{ bp }}/addons/{{ addon_id }}"
class="inline-flex rounded-md bg-sky-50 px-2 py-0.5 text-xs font-medium text-sky-700 hover:bg-sky-100 transition-colors">
{{ addon_id }}
</a>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="rounded-lg bg-slate-50 border border-slate-200 p-4 flex items-start gap-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-5 text-slate-400 flex-shrink-0 mt-0.5">
<path stroke-linecap="round" stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
</svg>
<div class="text-xs text-slate-600">
<p class="font-medium text-slate-700">How to configure a license</p>
<p class="mt-1">Use the form above to verify and save a key — the server automatically determines
which addons the key covers. For keys that require an authorized email, add <code
class="bg-slate-200 px-1 py-0.5 rounded">email = "you@example.com"</code>
under <code class="bg-slate-200 px-1 py-0.5 rounded">[licenses]</code> in your
<code class="bg-slate-200 px-1 py-0.5 rounded">config.toml</code>.
</p>
</div>
</div>
{% endif %}
</div>
{% endif %}{# commerce_enabled #}
</div>
</div>
<script>
const addonsTranslations = window.__i18n_t || {};
const i18n = (key) => addonsTranslations[key] || key;
document.addEventListener('alpine:init', () => {
Alpine.data('addonsPage', () => ({
activeTab: 'addons',
init() {
const hash = window.location.hash.replace('#', '');
if (hash === 'licenses') {
this.activeTab = 'licenses';
}
},
setTab(tab) {
this.activeTab = tab;
history.replaceState(null, '', tab === 'addons' ? '#addons' : '#licenses');
},
toggleAddon(id, currentState) {
const action = currentState ? 'disable' : 'enable';
const event = new CustomEvent('console:confirm', {
detail: {
title: `${action.charAt(0).toUpperCase() + action.slice(1)} Addon?`,
message: i18n('addons.confirm_message').replace('{action}', action).replace('{id}', id),
confirmLabel: action.charAt(0).toUpperCase() + action.slice(1),
destructive: currentState,
onConfirm: () => this.performToggle(id, !currentState)
}
});
window.dispatchEvent(event);
},
async performToggle(id, newState) {
try {
const response = await fetch('{{ api_prefix | safe }}/addons/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: id, enabled: newState }),
});
const result = await response.json();
if (result.success) {
window.location.reload();
} else {
alert(i18n('addons.failed_toggle') + ': ' + (result.message || i18n('common.unknown_error')));
}
} catch (e) {
alert(i18n('common.error') + ': ' + e.message);
}
}
}));
Alpine.data('licenseForm', () => ({
licenseKey: '',
error: '',
verifiedPlan: '',
verifiedExpiry: '',
verifiedScope: null,
loading: false,
success: false,
async verifyLicense() {
this.error = '';
this.verifiedPlan = '';
this.verifiedExpiry = '';
this.verifiedScope = null;
this.success = false;
const key = this.licenseKey.trim();
if (!key) {
this.error = 'Please enter a license key.';
return;
}
this.loading = true;
try {
const response = await fetch('{{ api_prefix | safe }}/licenses/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ license_key: key })
});
const result = await response.json();
if (response.ok && result.success) {
this.verifiedPlan = result.plan || 'unknown';
this.verifiedExpiry = result.expiry || '';
this.verifiedScope = result.scope || null;
this.success = true;
const banner = document.querySelector('[data-unlicensed-banner]');
if (banner) banner.remove();
} else {
this.error = result.message || 'Verification failed.';
}
} catch (e) {
this.error = 'Network error — please try again.';
} finally {
this.loading = false;
}
}
}));
});
</script>
{% endblock %}