{% extends "console/layout.html" %}
{% block title %}{{ "licenses.title" | t }} · {{ site_name | default('RustPBX') }}{% endblock %}
{% block content %}
<div class="p-4 sm:p-6" x-data="licenseForm()">
<div class="mx-auto max-w-7xl space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-semibold text-slate-900">{{ "licenses.title" | t }}</h1>
<p class="text-sm text-slate-500 mt-1">
{{ "licenses.subtitle" | t }}
</p>
</div>
<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">
<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>
{{ "licenses.get_renew" | t }}
</a>
</div>
<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>
{{ "licenses.add_update_license" | t }}
</h2>
<div>
<label class="block text-xs font-medium text-slate-500 mb-1">{{ "licenses.license_key" | t }}</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">{{ "licenses.verify_save" | t }}</span>
<span x-show="loading" x-cloak>{{ "licenses.verifying" | t }}</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">{{ "licenses.license_verified" | t }}</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">{{ "licenses.no_licenses" | t }}</h3>
<p class="mt-1 text-sm text-slate-500">
Enter an addon ID and your license key above, or get one at miuda.ai.
</p>
<div class="mt-6 flex items-center justify-center gap-3">
<a href="{{ bp }}/addons"
class="inline-flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50 transition-colors">
{{ "licenses.go_to_addons" | t }}
</a>
<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">
{{ "licenses.get_license" | t }}
</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">{{ "licenses.key" | t }}
</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>
{{ "licenses.valid" | t }}
</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">{{ "licenses.plan" | t }}</dt>
<dd class="mt-0.5 text-slate-800 font-semibold">{{ lic.plan }}</dd>
</div>
{% endif %}
<div>
<dt class="font-medium text-slate-500">{{ "licenses.expires" | t }}</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">{{ "licenses.covers" | t }}</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">{{ "licenses.all_addons" | t }}</p>
{% endif %}
</div>
<div class="border-t border-slate-100 pt-3">
<p class="text-xs font-medium text-slate-500">{{ "licenses.assigned_to" | t }}</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">{{ "licenses.how_to_configure" | t }}</p>
<p class="mt-1">{{ "licenses.how_to_configure_desc" | t }}</p>
</div>
</div>
{% endif %}
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('licenseForm', () => ({
licenseKey: '',
error: '',
verifiedPlan: '',
verifiedExpiry: '',
verifiedScope: null,
loading: false,
async verifyLicense() {
this.error = '';
this.verifiedPlan = '';
this.verifiedExpiry = '';
this.verifiedScope = null;
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;
} else {
this.error = result.message || 'Verification failed.';
}
} catch (e) {
this.error = 'Network error — please try again.';
} finally {
this.loading = false;
}
}
}))
})
</script>
{% endblock %}