feat(subscriptions): add SaaS subscription tiers, pricing page, and enforced upload quotas

- Add Free / Starter / Professional / Business tiers with lifetime, daily, and monthly
  file limits (app/utils/subscription.py)
- Add subscription_tier column to UserProfile model + migration 014
- Enforce quotas at upload time (HTTP 402 on violation) in /api/ui-upload
- New REST API: GET /api/subscriptions/tiers, /my, /platform (admin)
- New pages: /pricing (marketing, public) and /subscription (per-user status)
- Enhanced dashboard: SaaS stats (files today/month, OCR count, active users)
  in multi-user mode; original single-user layout preserved
- Admin users page: show Plan badge, allow tier editing via dropdown
- Navigation: add Pricing link + subscription icon in user header
- Tests: 23 unit tests for subscription tier logic
- Docs: docs/SubscriptionTiers.md

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-06 15:53:12 +00:00
parent 70dd35dec4
commit 179f6125e8
18 changed files with 2014 additions and 18 deletions
+13 -3
View File
@@ -141,25 +141,35 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', fun
if (authSection) {
authSection.textContent = ''; // Clear existing content
const container = document.createElement('div');
container.className = 'flex items-center';
container.className = 'flex items-center gap-2';
const img = document.createElement('img');
img.src = data.picture;
img.alt = 'Avatar';
img.className = 'w-8 h-8 rounded-full mr-2';
img.className = 'w-8 h-8 rounded-full';
const span = document.createElement('span');
span.textContent = displayName;
// Subscription link
const planLink = document.createElement('a');
planLink.href = '/subscription';
planLink.className = 'text-xs text-indigo-600 hover:text-indigo-800 font-medium hidden md:inline';
planLink.title = 'My subscription';
const planIcon = document.createElement('i');
planIcon.className = 'fas fa-layer-group';
planLink.appendChild(planIcon);
const logoutLink = document.createElement('a');
logoutLink.href = '/logout';
logoutLink.className = 'ml-3 text-red-600 hover:text-red-800';
logoutLink.className = 'text-red-600 hover:text-red-800';
const icon = document.createElement('i');
icon.className = 'fas fa-sign-out-alt';
logoutLink.appendChild(icon);
container.appendChild(img);
container.appendChild(span);
container.appendChild(planLink);
container.appendChild(logoutLink);
authSection.appendChild(container);
}
+50 -3
View File
@@ -68,6 +68,7 @@
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Display Name</th>
<th scope="col" class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Documents</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Upload</th>
<th scope="col" class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Plan</th>
<th scope="col" class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Upload Limit</th>
<th scope="col" class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
@@ -76,14 +77,14 @@
<tbody class="bg-white divide-y divide-gray-200">
<template x-if="loading">
<tr>
<td colspan="7" class="px-4 py-8 text-center text-gray-400">
<td colspan="8" class="px-4 py-8 text-center text-gray-400">
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i> Loading users…
</td>
</tr>
</template>
<template x-if="!loading && users.length === 0">
<tr>
<td colspan="7" class="px-4 py-8 text-center text-gray-400">
<td colspan="8" class="px-4 py-8 text-center text-gray-400">
<i class="fas fa-users-slash text-3xl block mb-2" aria-hidden="true"></i>
No users found.
<span x-show="search"> Try a different search term.</span>
@@ -117,6 +118,29 @@
<!-- Last upload -->
<td class="px-4 py-3 text-sm text-gray-500 whitespace-nowrap"
x-text="user.last_upload ? formatDate(user.last_upload) : '—'"></td>
<!-- Subscription plan -->
<td class="px-4 py-3 text-sm text-center">
<span
:class="{
'bg-gray-100 text-gray-600': !user.subscription_tier || user.subscription_tier === 'free',
'bg-blue-100 text-blue-700': user.subscription_tier === 'starter',
'bg-indigo-100 text-indigo-700': user.subscription_tier === 'professional',
'bg-purple-100 text-purple-700': user.subscription_tier === 'business',
}"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold capitalize"
>
<i
:class="{
'fas fa-seedling': !user.subscription_tier || user.subscription_tier === 'free',
'fas fa-rocket': user.subscription_tier === 'starter',
'fas fa-star': user.subscription_tier === 'professional',
'fas fa-building': user.subscription_tier === 'business',
}"
aria-hidden="true"
></i>
<span x-text="user.subscription_tier || 'free'"></span>
</span>
</td>
<!-- Upload limit -->
<td class="px-4 py-3 text-sm text-center text-gray-700">
<span x-show="user.daily_upload_limit !== null && user.daily_upload_limit !== undefined"
@@ -300,6 +324,26 @@
</label>
</div>
<!-- Subscription tier -->
<div>
<label for="modal-tier" class="block text-sm font-medium text-gray-700 mb-1">
Subscription Plan
</label>
<select
id="modal-tier"
x-model="form.subscription_tier"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 bg-white"
>
<option value="free">Free — 25 lifetime files</option>
<option value="starter">Starter — $9/mo (10/day, 100/mo)</option>
<option value="professional">Professional — $29/mo (50/day, 500/mo)</option>
<option value="business">Business — $79/mo (unlimited)</option>
</select>
<p class="text-xs text-gray-400 mt-1">
Sets the quota limits for this user. Limits are enforced on upload.
</p>
</div>
</div>
<!-- Footer -->
@@ -394,6 +438,7 @@ function adminUsersApp() {
daily_upload_limit: null,
notes: '',
is_blocked: false,
subscription_tier: 'free',
},
// Delete modal
@@ -446,7 +491,7 @@ function adminUsersApp() {
openCreateModal() {
this.isCreate = true;
this.modalTitle = 'Add User Profile';
this.form = { user_id: '', display_name: '', daily_upload_limit: null, notes: '', is_blocked: false };
this.form = { user_id: '', display_name: '', daily_upload_limit: null, notes: '', is_blocked: false, subscription_tier: 'free' };
this.modalOpen = true;
},
@@ -460,6 +505,7 @@ function adminUsersApp() {
? user.daily_upload_limit : null,
notes: user.notes || '',
is_blocked: !!user.is_blocked,
subscription_tier: user.subscription_tier || 'free',
};
this.modalOpen = true;
},
@@ -473,6 +519,7 @@ function adminUsersApp() {
? Number(this.form.daily_upload_limit) : null,
notes: this.form.notes || null,
is_blocked: !!this.form.is_blocked,
subscription_tier: this.form.subscription_tier || 'free',
};
const uid = encodeURIComponent(this.form.user_id);
const resp = await fetch(`/api/admin/users/${uid}`, {
+2
View File
@@ -63,6 +63,7 @@
<a href="/upload" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/upload' %}aria-current="page"{% endif %}>Upload</a>
<a href="/files" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/files' %}aria-current="page"{% endif %}>Files</a>
<a href="/search" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/search' %}aria-current="page"{% endif %}>Search</a>
<a href="/pricing" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/pricing' %}aria-current="page"{% endif %}>Pricing</a>
<!-- Admin dropdown (shown only for admin users via JS) -->
<div id="adminMenuContainer" class="relative hidden">
@@ -171,6 +172,7 @@
<a href="/upload" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Upload</a>
<a href="/files" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Files</a>
<a href="/search" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Search</a>
<a href="/pricing" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Pricing</a>
<!-- Admin section in mobile menu (shown only for admin users via JS) -->
<div id="mobileAdminSection" class="hidden">
+298
View File
@@ -4,6 +4,302 @@
{% block content %}
<div class="container mx-auto px-4 py-8">
{% if multi_user_enabled %}
{# ══════════════════════════════════════════════════════════════════════════ #}
{# MULTI-USER / SAAS DASHBOARD #}
{# ══════════════════════════════════════════════════════════════════════════ #}
<!-- Hero -->
<div class="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-xl shadow-lg text-white p-8 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 class="text-3xl font-bold mb-1">DocuElevate</h1>
<p class="text-blue-100 text-sm">Intelligent document processing &amp; management — built for teams</p>
</div>
<div class="flex flex-wrap gap-3">
<a href="/upload"
class="bg-white text-blue-700 hover:bg-blue-50 font-semibold py-2 px-5 rounded-lg transition duration-200 flex items-center shadow-sm">
<i class="fas fa-upload mr-2" aria-hidden="true"></i> Upload
</a>
<a href="/files"
class="bg-transparent border border-white text-white hover:bg-white hover:text-blue-700 font-semibold py-2 px-5 rounded-lg transition duration-200 flex items-center">
<i class="fas fa-list mr-2" aria-hidden="true"></i> Browse Files
</a>
</div>
</div>
</div>
<!-- Platform stats row (admin only) -->
{% if is_admin %}
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-widest mb-3">Platform overview</h2>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-3">
<div class="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
<i class="fas fa-file-alt text-blue-600" aria-hidden="true"></i>
</div>
<div>
<p class="text-2xl font-extrabold text-blue-600">{{ stats.processed_files | default(0) }}</p>
<p class="text-gray-500 text-xs">Total files</p>
</div>
</div>
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-3">
<div class="h-10 w-10 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0">
<i class="fas fa-calendar-day text-green-600" aria-hidden="true"></i>
</div>
<div>
<p class="text-2xl font-extrabold text-green-600">{{ stats.files_today | default(0) }}</p>
<p class="text-gray-500 text-xs">Files today</p>
</div>
</div>
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-3">
<div class="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center flex-shrink-0">
<i class="fas fa-calendar-alt text-indigo-600" aria-hidden="true"></i>
</div>
<div>
<p class="text-2xl font-extrabold text-indigo-600">{{ stats.files_month | default(0) }}</p>
<p class="text-gray-500 text-xs">Files this month</p>
</div>
</div>
<div class="bg-white rounded-xl shadow p-5 flex items-center gap-3">
<div class="h-10 w-10 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0">
<i class="fas fa-users text-purple-600" aria-hidden="true"></i>
</div>
<div>
<p class="text-2xl font-extrabold text-purple-600">{{ stats.unique_users | default(0) }}</p>
<p class="text-gray-500 text-xs">Active users</p>
</div>
</div>
</div>
{% endif %}
<!-- My usage / subscription widget -->
{% if user_tier and user_usage %}
<h2 class="text-sm font-semibold text-gray-400 uppercase tracking-widest mb-3">My usage</h2>
<div class="bg-white rounded-xl shadow p-6 mb-8">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
<div class="flex items-center gap-3">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold
{% if user_tier.id == 'free' %}bg-gray-100 text-gray-600
{% elif user_tier.id == 'starter' %}bg-blue-100 text-blue-700
{% elif user_tier.id == 'professional' %}bg-indigo-100 text-indigo-700
{% else %}bg-purple-100 text-purple-700{% endif %}">
<i class="fas
{% if user_tier.id == 'free' %}fa-seedling
{% elif user_tier.id == 'starter' %}fa-rocket
{% elif user_tier.id == 'professional' %}fa-star
{% else %}fa-building{% endif %}
mr-1" aria-hidden="true"></i>
{{ user_tier.name }} Plan
</span>
{% if user_tier.id != 'business' %}
<a href="/pricing" class="text-xs text-indigo-600 hover:text-indigo-800 font-medium">
Upgrade <i class="fas fa-arrow-up-right-from-square ml-0.5 text-xs" aria-hidden="true"></i>
</a>
{% endif %}
</div>
<a href="/subscription" class="text-xs text-gray-500 hover:text-gray-700">
View full details →
</a>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- Lifetime -->
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-gray-500">Lifetime files</span>
<span class="font-semibold text-gray-700">
{{ user_usage.lifetime }}{% if user_tier.lifetime_file_limit > 0 %} / {{ user_tier.lifetime_file_limit }}{% endif %}
</span>
</div>
{% if user_tier.lifetime_file_limit > 0 %}
{% set pct = ([((user_usage.lifetime / user_tier.lifetime_file_limit) * 100) | int, 100] | min) %}
<div class="w-full bg-gray-100 rounded-full h-2" role="progressbar"
aria-valuenow="{{ user_usage.lifetime }}" aria-valuemax="{{ user_tier.lifetime_file_limit }}">
<div class="h-2 rounded-full {% if pct >= 90 %}bg-red-500{% elif pct >= 70 %}bg-yellow-500{% else %}bg-green-500{% endif %}"
style="width: {{ pct }}%"></div>
</div>
{% else %}
<div class="text-xs text-green-600">Unlimited</div>
{% endif %}
</div>
<!-- Today -->
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-gray-500">Files today</span>
<span class="font-semibold text-gray-700">
{{ user_usage.today }}{% if user_tier.daily_upload_limit > 0 %} / {{ user_tier.daily_upload_limit }}{% endif %}
</span>
</div>
{% if user_tier.daily_upload_limit > 0 %}
{% set pct = ([((user_usage.today / user_tier.daily_upload_limit) * 100) | int, 100] | min) %}
<div class="w-full bg-gray-100 rounded-full h-2" role="progressbar"
aria-valuenow="{{ user_usage.today }}" aria-valuemax="{{ user_tier.daily_upload_limit }}">
<div class="h-2 rounded-full {% if pct >= 90 %}bg-red-500{% elif pct >= 70 %}bg-yellow-500{% else %}bg-blue-500{% endif %}"
style="width: {{ pct }}%"></div>
</div>
{% else %}
<div class="text-xs text-green-600">Unlimited</div>
{% endif %}
</div>
<!-- This month -->
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-gray-500">Files this month</span>
<span class="font-semibold text-gray-700">
{{ user_usage.month }}{% if user_tier.monthly_upload_limit > 0 %} / {{ user_tier.monthly_upload_limit }}{% endif %}
</span>
</div>
{% if user_tier.monthly_upload_limit > 0 %}
{% set pct = ([((user_usage.month / user_tier.monthly_upload_limit) * 100) | int, 100] | min) %}
<div class="w-full bg-gray-100 rounded-full h-2" role="progressbar"
aria-valuenow="{{ user_usage.month }}" aria-valuemax="{{ user_tier.monthly_upload_limit }}">
<div class="h-2 rounded-full {% if pct >= 90 %}bg-red-500{% elif pct >= 70 %}bg-yellow-500{% else %}bg-indigo-500{% endif %}"
style="width: {{ pct }}%"></div>
</div>
{% else %}
<div class="text-xs text-green-600">Unlimited</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
<!-- Widget grid (multi-user) -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<!-- Quick Actions -->
<div class="bg-white rounded-xl shadow p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-bolt text-yellow-500" aria-hidden="true"></i> Quick Actions
</h2>
<ul class="space-y-2">
<li>
<a href="/upload" class="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition group">
<span class="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0 group-hover:bg-blue-200">
<i class="fas fa-upload text-blue-600 text-sm" aria-hidden="true"></i>
</span>
<div>
<p class="text-sm font-medium text-gray-800">Upload Document</p>
<p class="text-xs text-gray-400">Process a new file</p>
</div>
</a>
</li>
<li>
<a href="/files" class="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition group">
<span class="h-8 w-8 rounded-full bg-indigo-100 flex items-center justify-center flex-shrink-0 group-hover:bg-indigo-200">
<i class="fas fa-list text-indigo-600 text-sm" aria-hidden="true"></i>
</span>
<div>
<p class="text-sm font-medium text-gray-800">My Documents</p>
<p class="text-xs text-gray-400">Browse your processed files</p>
</div>
</a>
</li>
<li>
<a href="/subscription" class="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition group">
<span class="h-8 w-8 rounded-full bg-purple-100 flex items-center justify-center flex-shrink-0 group-hover:bg-purple-200">
<i class="fas fa-layer-group text-purple-600 text-sm" aria-hidden="true"></i>
</span>
<div>
<p class="text-sm font-medium text-gray-800">My Subscription</p>
<p class="text-xs text-gray-400">View plan &amp; usage details</p>
</div>
</a>
</li>
<li>
<a href="/search" class="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition group">
<span class="h-8 w-8 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0 group-hover:bg-green-200">
<i class="fas fa-search text-green-600 text-sm" aria-hidden="true"></i>
</span>
<div>
<p class="text-sm font-medium text-gray-800">Search</p>
<p class="text-xs text-gray-400">Full-text search across documents</p>
</div>
</a>
</li>
</ul>
</div>
<!-- Processing stats -->
<div class="bg-white rounded-xl shadow p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-chart-bar text-blue-500" aria-hidden="true"></i> Processing Stats
</h2>
<ul class="space-y-3 text-sm">
<li class="flex items-center justify-between py-2 border-b border-gray-100">
<span class="text-gray-600 flex items-center gap-2">
<i class="fas fa-file-alt text-blue-400 w-4" aria-hidden="true"></i> Total processed
</span>
<span class="font-bold text-gray-900">{{ stats.processed_files | default(0) }}</span>
</li>
<li class="flex items-center justify-between py-2 border-b border-gray-100">
<span class="text-gray-600 flex items-center gap-2">
<i class="fas fa-eye text-indigo-400 w-4" aria-hidden="true"></i> Files with OCR
</span>
<span class="font-bold text-gray-900">{{ stats.files_with_ocr | default(0) }}</span>
</li>
<li class="flex items-center justify-between py-2 border-b border-gray-100">
<span class="text-gray-600 flex items-center gap-2">
<i class="fas fa-calendar-day text-green-400 w-4" aria-hidden="true"></i> Today
</span>
<span class="font-bold text-gray-900">{{ stats.files_today | default(0) }}</span>
</li>
<li class="flex items-center justify-between py-2">
<span class="text-gray-600 flex items-center gap-2">
<i class="fas fa-calendar-alt text-orange-400 w-4" aria-hidden="true"></i> This month
</span>
<span class="font-bold text-gray-900">{{ stats.files_month | default(0) }}</span>
</li>
</ul>
</div>
<!-- Upgrade CTA or Integrations -->
{% if user_tier and user_tier.id != 'business' %}
<div class="bg-gradient-to-br from-indigo-600 to-purple-700 rounded-xl shadow p-6 text-white flex flex-col justify-between">
<div>
<h2 class="text-lg font-semibold mb-2 flex items-center gap-2">
<i class="fas fa-arrow-up-right-dots" aria-hidden="true"></i> Upgrade your plan
</h2>
<p class="text-indigo-100 text-sm mb-4">
Unlock more documents, more destinations and priority support.
</p>
<ul class="space-y-1.5 text-sm text-indigo-100">
<li class="flex items-center gap-2"><i class="fas fa-check-circle text-indigo-300" aria-hidden="true"></i> Higher daily &amp; monthly limits</li>
<li class="flex items-center gap-2"><i class="fas fa-check-circle text-indigo-300" aria-hidden="true"></i> More storage destinations</li>
<li class="flex items-center gap-2"><i class="fas fa-check-circle text-indigo-300" aria-hidden="true"></i> More OCR pages</li>
</ul>
</div>
<a href="/pricing"
class="mt-6 inline-flex items-center justify-center bg-white text-indigo-700 font-semibold px-5 py-2.5 rounded-lg hover:bg-indigo-50 transition shadow">
View plans &amp; pricing <i class="fas fa-arrow-right ml-2 text-xs" aria-hidden="true"></i>
</a>
</div>
{% else %}
<div class="bg-white rounded-xl shadow p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-plug text-green-500" aria-hidden="true"></i> Integrations
</h2>
<div class="space-y-3 text-sm text-gray-600">
<div class="flex items-center justify-between">
<span>Active integrations</span>
<span class="font-bold text-green-600">{{ stats.active_integrations | default(0) }}</span>
</div>
<div class="flex items-center justify-between">
<span>Storage targets</span>
<span class="font-bold text-indigo-600">{{ stats.storage_targets | default(0) }}</span>
</div>
</div>
<a href="/status" class="mt-4 inline-flex items-center text-xs text-indigo-600 hover:text-indigo-800">
View system status <i class="fas fa-arrow-right ml-1" aria-hidden="true"></i>
</a>
</div>
{% endif %}
</div>
{% else %}
{# ══════════════════════════════════════════════════════════════════════════ #}
{# SINGLE-USER DASHBOARD (original layout, preserved as-is) #}
{# ══════════════════════════════════════════════════════════════════════════ #}
<!-- Hero / Quick Actions -->
<div class="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-xl shadow-lg text-white p-8 mb-8">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
@@ -154,6 +450,8 @@
</div>
</div>
{% endif %}{# end multi_user_enabled #}
</div>
{% endblock %}
+373
View File
@@ -0,0 +1,373 @@
{% extends "base.html" %}
{% block title %}Pricing & Plans DocuElevate{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- ── Hero ──────────────────────────────────────────────────────────────── -->
<div class="bg-gradient-to-br from-blue-700 via-indigo-700 to-purple-700 text-white py-20 px-4">
<div class="max-w-4xl mx-auto text-center">
<span class="inline-block bg-white/20 text-white text-xs font-semibold uppercase tracking-widest px-3 py-1 rounded-full mb-4">
Simple, transparent pricing
</span>
<h1 class="text-4xl sm:text-5xl font-extrabold mb-4 leading-tight">
Choose the plan that's right for you
</h1>
<p class="text-indigo-100 text-lg max-w-2xl mx-auto">
From free exploration to unlimited enterprise processing — scale as your document workflows grow.
</p>
<!-- Annual / Monthly toggle (cosmetic — actual billing handled separately) -->
<div class="mt-8 inline-flex items-center bg-white/10 rounded-full p-1 gap-1" x-data="{ annual: false }">
<button
@click="annual = false"
:class="!annual ? 'bg-white text-indigo-700 shadow' : 'text-white'"
class="px-5 py-2 rounded-full text-sm font-semibold transition-all duration-200"
>Monthly</button>
<button
@click="annual = true"
:class="annual ? 'bg-white text-indigo-700 shadow' : 'text-white'"
class="px-5 py-2 rounded-full text-sm font-semibold transition-all duration-200"
>
Annual <span class="ml-1 text-xs bg-green-400 text-green-900 rounded-full px-2 py-0.5 font-bold">Save ~17%</span>
</button>
<!-- Tier cards — responsive 4-column grid -->
<div class="hidden" x-effect="$el.classList.remove('hidden')"></div>
</div>
</div>
</div>
<!-- ── Tier cards ────────────────────────────────────────────────────────── -->
<div class="max-w-7xl mx-auto px-4 -mt-10 pb-20" x-data="{ annual: false }">
<!-- Recreate the toggle state here so cards react to the hero toggle too -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 items-stretch">
{% for tier in tiers %}
<div class="relative flex flex-col rounded-2xl shadow-lg overflow-hidden
{% if tier.highlight %}ring-4 ring-indigo-500 scale-105 z-10 bg-white{% else %}bg-white{% endif %}
transition-transform duration-200 hover:shadow-xl"
>
<!-- Popular badge -->
{% if tier.badge %}
<div class="absolute top-0 right-0 mt-4 mr-4">
<span class="
{% if tier.badge == 'Most Popular' %}bg-indigo-600 text-white
{% elif tier.badge == 'Best Value' %}bg-green-600 text-white
{% else %}bg-gray-600 text-white{% endif %}
text-xs font-bold uppercase tracking-wide px-3 py-1 rounded-full shadow"
>
{{ tier.badge }}
</span>
</div>
{% endif %}
<!-- Card header -->
<div class="p-6 pb-4 border-b border-gray-100">
<h2 class="text-xl font-bold text-gray-900">{{ tier.name }}</h2>
<p class="text-gray-500 text-sm mt-1">{{ tier.tagline }}</p>
<!-- Price -->
<div class="mt-4">
<template x-if="!annual">
<div>
{% if tier.price_monthly == 0 %}
<span class="text-4xl font-extrabold text-gray-900">Free</span>
{% else %}
<span class="text-4xl font-extrabold text-gray-900">${{ tier.price_monthly }}</span>
<span class="text-gray-500 text-sm">/month</span>
{% endif %}
</div>
</template>
<template x-if="annual">
<div>
{% if tier.price_yearly == 0 %}
<span class="text-4xl font-extrabold text-gray-900">Free</span>
{% else %}
<span class="text-4xl font-extrabold text-gray-900">${{ tier.price_yearly }}</span>
<span class="text-gray-500 text-sm">/year</span>
<div class="text-xs text-green-600 font-semibold mt-0.5">
~${{ (tier.price_yearly / 12) | round(0) | int }}/month billed annually
</div>
{% endif %}
</div>
</template>
</div>
</div>
<!-- Features list -->
<div class="p-6 flex-1">
<ul class="space-y-3 text-sm text-gray-600">
{% for feature in tier.features %}
<li class="flex items-start gap-2">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
<span>{{ feature }}</span>
</li>
{% endfor %}
</ul>
</div>
<!-- CTA button -->
<div class="p-6 pt-0">
{% if tier.id == 'free' %}
<a href="/login"
class="block w-full text-center py-3 px-4 rounded-lg border-2 border-blue-600 text-blue-600 font-semibold hover:bg-blue-50 transition"
>{{ tier.cta }}</a>
{% elif tier.id == 'business' %}
<a href="mailto:sales@docuelevate.io?subject=Business+Plan+Enquiry"
class="block w-full text-center py-3 px-4 rounded-lg bg-gray-800 text-white font-semibold hover:bg-gray-700 transition"
>{{ tier.cta }}</a>
{% elif tier.highlight %}
<a href="/login"
class="block w-full text-center py-3 px-4 rounded-lg bg-indigo-600 text-white font-semibold hover:bg-indigo-700 transition shadow-md"
>{{ tier.cta }}</a>
{% else %}
<a href="/login"
class="block w-full text-center py-3 px-4 rounded-lg bg-blue-600 text-white font-semibold hover:bg-blue-700 transition"
>{{ tier.cta }}</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<!-- ── Comparison table ───────────────────────────────────────────────── -->
<div class="mt-20">
<h2 class="text-2xl font-bold text-gray-900 text-center mb-8">Full feature comparison</h2>
<div class="overflow-x-auto rounded-2xl shadow">
<table class="min-w-full bg-white divide-y divide-gray-200" aria-label="Plan comparison">
<thead>
<tr class="bg-gray-50">
<th scope="col" class="px-6 py-4 text-left text-sm font-semibold text-gray-700 w-1/3">Feature</th>
{% for tier in tiers %}
<th scope="col" class="px-4 py-4 text-center text-sm font-semibold
{% if tier.highlight %}text-indigo-700{% else %}text-gray-700{% endif %}">
{{ tier.name }}
</th>
{% endfor %}
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr class="bg-gray-50/50">
<th scope="row" colspan="5" class="px-6 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider text-left">
File Processing
</th>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Lifetime file limit</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.lifetime_file_limit == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.lifetime_file_limit }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Files per day</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.daily_upload_limit == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.daily_upload_limit }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Files per month</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.monthly_upload_limit == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.monthly_upload_limit }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Max file size</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.max_file_size_mb == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.max_file_size_mb }} MB</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="bg-gray-50/50">
<th scope="row" colspan="5" class="px-6 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider text-left">
OCR & AI
</th>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">OCR pages / month</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.max_ocr_pages_monthly == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.max_ocr_pages_monthly | int | string | replace("2500", "2 500") }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">AI metadata extraction</td>
<td class="px-4 py-3 text-center">
<span class="text-xs text-gray-500">Basic</span>
</td>
{% for tier in tiers[1:] %}
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
{% endfor %}
</tr>
<tr class="bg-gray-50/50">
<th scope="row" colspan="5" class="px-6 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider text-left">
Integrations
</th>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Storage destinations</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.max_storage_destinations == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.max_storage_destinations }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Email ingestion</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-times text-gray-300" aria-label="Not included"></i>
</td>
{% for tier in tiers[1:] %}
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Webhooks</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-times text-gray-300" aria-label="Not included"></i>
</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-times text-gray-300" aria-label="Not included"></i>
</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">API access</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-times text-gray-300" aria-label="Not included"></i>
</td>
{% for tier in tiers[1:] %}
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
{% endfor %}
</tr>
<tr class="bg-gray-50/50">
<th scope="row" colspan="5" class="px-6 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider text-left">
Support
</th>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Support level</td>
<td class="px-4 py-3 text-center text-xs text-gray-500">Community</td>
<td class="px-4 py-3 text-center text-xs text-gray-700">Email</td>
<td class="px-4 py-3 text-center text-xs text-gray-700 font-medium">Priority email</td>
<td class="px-4 py-3 text-center text-xs text-indigo-700 font-bold">Dedicated</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ── FAQ ───────────────────────────────────────────────────────────── -->
<div class="mt-20 max-w-3xl mx-auto">
<h2 class="text-2xl font-bold text-gray-900 text-center mb-8">Frequently asked questions</h2>
<div class="space-y-4" x-data="{ open: null }">
{% set faqs = [
("What counts as a 'processed file'?",
"Every document you upload and run through the DocuElevate pipeline counts as one processed file — including OCR, AI metadata extraction, and storage delivery."),
("What happens when I reach the Free tier lifetime limit?",
"Once your 25-file lifetime quota is reached you will see a friendly upgrade prompt on the upload page and any further upload attempts will return a payment-required error until you upgrade to a paid plan."),
("Can I change my plan at any time?",
"Yes. Upgrades take effect immediately. Downgrades take effect at the start of the next billing cycle. Unused quota does not roll over between billing periods."),
("Is there an annual discount?",
"Yes — paying annually saves approximately 17% compared to monthly billing. The savings are shown in the annual pricing above."),
("Do you offer a trial for paid plans?",
"The Free tier lets you try DocuElevate with up to 25 files at no cost and with no credit card required. Paid trials can be arranged — contact sales."),
("What is a 'storage destination'?",
"A storage destination is any cloud or self-hosted storage you configure as an output — Dropbox, Google Drive, OneDrive, Nextcloud, S3, SFTP, FTP, WebDAV, or Paperless-ngx each count as one destination."),
] %}
{% for q, a in faqs %}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<button
@click="open = (open === {{ loop.index0 }}) ? null : {{ loop.index0 }}"
class="w-full flex justify-between items-center px-6 py-4 text-left text-sm font-semibold text-gray-800 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
:aria-expanded="open === {{ loop.index0 }}"
>
<span>{{ q }}</span>
<i class="fas text-indigo-400 ml-4 flex-shrink-0 transition-transform duration-200"
:class="open === {{ loop.index0 }} ? 'fa-chevron-up' : 'fa-chevron-down'"
aria-hidden="true"></i>
</button>
<div x-show="open === {{ loop.index0 }}" x-transition class="px-6 pb-4 text-sm text-gray-600">
{{ a }}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- ── CTA strip ─────────────────────────────────────────────────────── -->
<div class="mt-20 rounded-2xl bg-gradient-to-r from-blue-600 to-indigo-700 text-white p-10 text-center shadow-xl">
<h2 class="text-2xl font-bold mb-2">Ready to get started?</h2>
<p class="text-indigo-100 mb-6">Start with the free tier — no credit card required.</p>
<a href="/login"
class="inline-block bg-white text-indigo-700 font-bold px-8 py-3 rounded-lg hover:bg-indigo-50 transition shadow">
Create your free account
</a>
</div>
</div>
</div>
{% endblock %}
+218
View File
@@ -0,0 +1,218 @@
{% extends "base.html" %}
{% block title %}My Subscription DocuElevate{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 flex items-center gap-3">
<i class="fas fa-layer-group text-indigo-500" aria-hidden="true"></i>
My Subscription
</h1>
<p class="text-gray-500 text-sm mt-1">Your current plan, usage and available upgrades.</p>
</div>
{% if not multi_user_enabled %}
<!-- Single-user notice -->
<div class="bg-blue-50 border border-blue-200 rounded-xl p-6 flex items-start gap-4">
<i class="fas fa-info-circle text-blue-500 text-2xl flex-shrink-0 mt-0.5" aria-hidden="true"></i>
<div>
<p class="font-semibold text-blue-800">Multi-user mode is not enabled.</p>
<p class="text-blue-700 text-sm mt-1">
Subscription tiers apply when multi-user mode is active. Your instance currently runs in
single-user mode with no processing limits.
An admin can enable multi-user mode via <a href="/settings" class="underline hover:text-blue-900">Settings</a>.
</p>
</div>
</div>
{% else %}
<!-- Current plan card -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Plan badge -->
<div class="bg-white rounded-xl shadow p-6 flex flex-col items-center text-center
{% if tier_id == 'professional' %}ring-2 ring-indigo-500{% endif %}">
<div class="h-14 w-14 rounded-full
{% if tier_id == 'free' %}bg-gray-100{% elif tier_id == 'starter' %}bg-blue-100{% elif tier_id == 'professional' %}bg-indigo-100{% else %}bg-purple-100{% endif %}
flex items-center justify-center mb-3">
<i class="fas
{% if tier_id == 'free' %}fa-seedling text-gray-500
{% elif tier_id == 'starter' %}fa-rocket text-blue-600
{% elif tier_id == 'professional' %}fa-star text-indigo-600
{% else %}fa-building text-purple-600{% endif %}
text-2xl" aria-hidden="true"></i>
</div>
<p class="text-xs font-semibold text-gray-400 uppercase tracking-widest">Current Plan</p>
<p class="text-2xl font-extrabold text-gray-900 mt-1">{{ tier.name }}</p>
<p class="text-gray-500 text-sm mt-1">{{ tier.tagline }}</p>
{% if tier.price_monthly > 0 %}
<p class="mt-3 text-lg font-bold text-indigo-600">${{ tier.price_monthly }}<span class="text-sm font-normal text-gray-400">/month</span></p>
{% else %}
<p class="mt-3 text-lg font-bold text-gray-500">Free</p>
{% endif %}
</div>
<!-- Usage this period -->
{% if usage %}
<div class="md:col-span-2 bg-white rounded-xl shadow p-6">
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-widest mb-4">Usage</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- Lifetime -->
<div class="text-center">
<p class="text-3xl font-extrabold text-gray-900">{{ usage.lifetime }}</p>
<p class="text-xs text-gray-500 mt-1">Total files (lifetime)</p>
{% if tier.lifetime_file_limit > 0 %}
<div class="mt-2">
{% set lifetime_pct = ((usage.lifetime / tier.lifetime_file_limit) * 100) | int %}
<div class="w-full bg-gray-200 rounded-full h-1.5" role="progressbar"
aria-valuenow="{{ usage.lifetime }}" aria-valuemax="{{ tier.lifetime_file_limit }}">
<div class="h-1.5 rounded-full
{% if lifetime_pct >= 90 %}bg-red-500{% elif lifetime_pct >= 70 %}bg-yellow-500{% else %}bg-green-500{% endif %}"
style="width: {{ [lifetime_pct, 100] | min }}%"></div>
</div>
<p class="text-xs text-gray-400 mt-1">{{ usage.lifetime }} / {{ tier.lifetime_file_limit }}</p>
</div>
{% else %}
<p class="text-xs text-green-600 mt-1">Unlimited</p>
{% endif %}
</div>
<!-- Today -->
<div class="text-center">
<p class="text-3xl font-extrabold text-gray-900">{{ usage.today }}</p>
<p class="text-xs text-gray-500 mt-1">Files today</p>
{% if tier.daily_upload_limit > 0 %}
<div class="mt-2">
{% set today_pct = ((usage.today / tier.daily_upload_limit) * 100) | int %}
<div class="w-full bg-gray-200 rounded-full h-1.5" role="progressbar"
aria-valuenow="{{ usage.today }}" aria-valuemax="{{ tier.daily_upload_limit }}">
<div class="h-1.5 rounded-full
{% if today_pct >= 90 %}bg-red-500{% elif today_pct >= 70 %}bg-yellow-500{% else %}bg-blue-500{% endif %}"
style="width: {{ [today_pct, 100] | min }}%"></div>
</div>
<p class="text-xs text-gray-400 mt-1">{{ usage.today }} / {{ tier.daily_upload_limit }} today</p>
</div>
{% else %}
<p class="text-xs text-green-600 mt-1">Unlimited</p>
{% endif %}
</div>
<!-- This month -->
<div class="text-center">
<p class="text-3xl font-extrabold text-gray-900">{{ usage.month }}</p>
<p class="text-xs text-gray-500 mt-1">Files this month</p>
{% if tier.monthly_upload_limit > 0 %}
<div class="mt-2">
{% set month_pct = ((usage.month / tier.monthly_upload_limit) * 100) | int %}
<div class="w-full bg-gray-200 rounded-full h-1.5" role="progressbar"
aria-valuenow="{{ usage.month }}" aria-valuemax="{{ tier.monthly_upload_limit }}">
<div class="h-1.5 rounded-full
{% if month_pct >= 90 %}bg-red-500{% elif month_pct >= 70 %}bg-yellow-500{% else %}bg-indigo-500{% endif %}"
style="width: {{ [month_pct, 100] | min }}%"></div>
</div>
<p class="text-xs text-gray-400 mt-1">{{ usage.month }} / {{ tier.monthly_upload_limit }} this month</p>
</div>
{% else %}
<p class="text-xs text-green-600 mt-1">Unlimited</p>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
<!-- Plan features included -->
<div class="bg-white rounded-xl shadow p-6 mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-list-check text-green-500" aria-hidden="true"></i>
What's included in your plan
</h2>
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-gray-600">
{% for feature in tier.features %}
<li class="flex items-start gap-2">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
<span>{{ feature }}</span>
</li>
{% endfor %}
</ul>
</div>
<!-- Upgrade options (only show tiers above current) -->
{% set tier_order = ['free', 'starter', 'professional', 'business'] %}
{% set current_index = tier_order.index(tier_id) %}
{% set upgrade_tiers = all_tiers | selectattr('id', 'ne', tier_id) | list %}
{% if tier_id != 'business' %}
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-arrow-up-right-dots text-indigo-500" aria-hidden="true"></i>
Upgrade your plan
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{% for t in all_tiers %}
{% if tier_order.index(t.id) > current_index %}
<div class="bg-white rounded-xl border-2
{% if t.highlight %}border-indigo-500{% else %}border-gray-200{% endif %}
p-5 flex flex-col justify-between hover:shadow-md transition">
<div>
<div class="flex items-center justify-between mb-2">
<span class="font-bold text-gray-900">{{ t.name }}</span>
{% if t.badge %}
<span class="text-xs bg-indigo-100 text-indigo-700 font-semibold rounded-full px-2 py-0.5">{{ t.badge }}</span>
{% endif %}
</div>
<p class="text-gray-500 text-xs mb-3">{{ t.tagline }}</p>
{% if t.price_monthly > 0 %}
<p class="text-xl font-extrabold text-gray-900">${{ t.price_monthly }}<span class="text-sm font-normal text-gray-400">/mo</span></p>
{% endif %}
<ul class="mt-3 space-y-1.5 text-xs text-gray-600">
{% for feature in t.features[:4] %}
<li class="flex items-start gap-1.5">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
{{ feature }}
</li>
{% endfor %}
{% if t.features | length > 4 %}
<li class="text-indigo-500 font-medium">+ {{ (t.features | length) - 4 }} more features</li>
{% endif %}
</ul>
</div>
<div class="mt-4">
{% if t.id == 'business' %}
<a href="mailto:sales@docuelevate.io?subject=Business+Plan+Enquiry"
class="block text-center py-2 px-4 rounded-lg bg-gray-800 text-white text-sm font-semibold hover:bg-gray-700 transition">
Contact Sales
</a>
{% else %}
<a href="/pricing"
class="block text-center py-2 px-4 rounded-lg
{% if t.highlight %}bg-indigo-600 hover:bg-indigo-700{% else %}bg-blue-600 hover:bg-blue-700{% endif %}
text-white text-sm font-semibold transition">
Upgrade to {{ t.name }}
</a>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
<!-- Back link -->
<div class="mt-8">
<a href="/" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium flex items-center gap-1">
<i class="fas fa-arrow-left text-xs" aria-hidden="true"></i> Back to Dashboard
</a>
</div>
</div>
{% endblock %}