ea7fffa3a1
- Add SubscriptionPlan model and subscription_plans table (migration 015) - Add billing cycle/period/allow_overage fields to UserProfile (migration 016) - Add subscription_overage_percent config field (replaces overage_factor) - Rewrite check_upload_allowed: use overage_percent, yearly carry-over, no daily cap - Add seed_default_plans(), _plan_to_dict(), get_year_file_count(), _months_elapsed() - Update get_tier/get_all_tiers to be DB-first with TIER_DEFAULTS fallback - Add TIER_DEFAULTS alias (TIERS kept for backward compat) - New /api/plans/ CRUD endpoints (admin-only except list/get) - New /admin/plans Plan Designer page with Alpine.js UI - Add Plan Designer link to admin navigation in base.html - Remove 'Files per day' row from pricing comparison table - Add billing cycle + period start to admin users edit modal - Seed default plans on startup in lifespan handler - Rewrite docs/SubscriptionTiers.md with full plan/overage/API docs - Fix all tests in test_subscription.py (remove daily cap tests, add overage/carry-over tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
311 lines
17 KiB
HTML
311 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-color-scheme-default="{{ ui_default_color_scheme | default('system') }}">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>{% block title %}DocuElevate{% endblock %}</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<!-- Anti-flash dark mode script: runs before CSS/JS to prevent white flash -->
|
|
<script>
|
|
(function () {
|
|
var stored = localStorage.getItem('colorScheme');
|
|
var serverDefault = document.documentElement.getAttribute('data-color-scheme-default') || 'system';
|
|
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
var scheme = stored || serverDefault;
|
|
if (scheme === 'dark' || (scheme === 'system' && prefersDark)) {
|
|
document.documentElement.classList.add('dark');
|
|
}
|
|
})();
|
|
</script>
|
|
<!-- Tailwind CSS or other global CSS references -->
|
|
<link rel="stylesheet" href="/static/styles.css" />
|
|
<!-- Alpine.js moved to head for earlier loading -->
|
|
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
|
{% block head_css %}
|
|
<!-- Tailwind CSS and other CSS -->
|
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
<!-- Font Awesome -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
|
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
|
|
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
{% endblock %}
|
|
{% block head_extra %}{% endblock %}
|
|
<!-- CSRF token for AJAX/fetch requests -->
|
|
<meta name="csrf-token" content="{{ csrf_token | default('', true) }}">
|
|
</head>
|
|
|
|
<body class="bg-gray-50 min-h-screen flex flex-col">
|
|
<!-- Skip to main content link for keyboard/screen reader users -->
|
|
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
|
|
<!-- Global Nav -->
|
|
<nav class="bg-white shadow relative" aria-label="Main navigation">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between h-16 items-center">
|
|
|
|
<!-- Brand + Icon -->
|
|
<div class="flex-shrink-0">
|
|
<a href="/" class="inline-flex items-center space-x-2">
|
|
<!-- Icon -->
|
|
<span
|
|
class="material-symbols-light--folder-managed-outline text-blue-500"
|
|
style="width: 24px; height: 24px;"
|
|
></span>
|
|
<!-- Text -->
|
|
<span class="text-xl font-bold text-blue-500 hover:text-blue-700">
|
|
DocuElevate
|
|
</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Menu Items - using x-data for mobile menu toggle -->
|
|
<div x-data="{ mobileMenuOpen: false, adminMenuOpen: false }">
|
|
<div class="hidden md:flex space-x-4 items-center">
|
|
<a href="/" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/' %}aria-current="page"{% endif %}>Home</a>
|
|
<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">
|
|
<button
|
|
@click="adminMenuOpen = !adminMenuOpen"
|
|
@click.outside="adminMenuOpen = false"
|
|
type="button"
|
|
class="inline-flex items-center text-gray-700 hover:text-gray-900 focus:outline-none"
|
|
aria-haspopup="true"
|
|
aria-label="Admin menu"
|
|
:aria-expanded="adminMenuOpen"
|
|
>
|
|
<i class="fas fa-shield-alt mr-1 text-red-500"></i>
|
|
Admin
|
|
<i class="fas fa-chevron-down ml-1 text-xs"></i>
|
|
</button>
|
|
<div
|
|
x-show="adminMenuOpen"
|
|
x-transition:enter="transition ease-out duration-100 transform"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-75 transform"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50"
|
|
role="menu"
|
|
aria-label="Admin actions"
|
|
>
|
|
<div class="py-1">
|
|
<a href="/settings" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-cog w-4 mr-2 text-gray-500" aria-hidden="true"></i> Settings
|
|
</a>
|
|
<a href="/admin/users" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-users w-4 mr-2 text-blue-500" aria-hidden="true"></i> Users
|
|
</a>
|
|
<a href="/admin/plans" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-layer-group w-4 mr-2 text-indigo-500" aria-hidden="true"></i> Plan Designer
|
|
</a>
|
|
<a href="/admin/credentials" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-key w-4 mr-2 text-yellow-500" aria-hidden="true"></i> Credentials
|
|
</a>
|
|
<a href="/admin/files" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-folder-open w-4 mr-2 text-gray-500" aria-hidden="true"></i> File Manager
|
|
</a>
|
|
<a href="/duplicates" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-clone w-4 mr-2 text-orange-500" aria-hidden="true"></i> Duplicates
|
|
</a>
|
|
<a href="/similarity" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-project-diagram w-4 mr-2 text-blue-500" aria-hidden="true"></i> Similarity
|
|
</a>
|
|
<a href="/admin/queue" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-stream w-4 mr-2 text-blue-500" aria-hidden="true"></i> Queue Monitor
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status (de-emphasized) -->
|
|
<a href="/status" class="text-gray-500 hover:text-gray-700 text-sm" title="System Status">
|
|
<i class="fas fa-circle-dot mr-0.5" aria-hidden="true"></i> Status
|
|
</a>
|
|
|
|
<a href="/about" class="text-gray-700 hover:text-gray-900" {% if request and request.url.path == '/about' %}aria-current="page"{% endif %}>About</a>
|
|
|
|
<!-- Dark mode toggle (desktop) -->
|
|
<button
|
|
id="darkModeToggle"
|
|
onclick="toggleDarkMode()"
|
|
type="button"
|
|
class="p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
|
aria-label="Toggle dark mode"
|
|
title="Toggle dark mode"
|
|
>
|
|
<i class="fas fa-moon"></i>
|
|
</button>
|
|
|
|
<!-- Dynamic Auth Section -->
|
|
<div id="authSection" class="text-gray-700 hover:text-gray-900"></div>
|
|
</div>
|
|
|
|
<!-- Mobile menu button -->
|
|
<button
|
|
@click="mobileMenuOpen = !mobileMenuOpen"
|
|
type="button"
|
|
class="md:hidden inline-flex items-center justify-center p-3 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
|
|
style="min-height:44px;min-width:44px;"
|
|
aria-label="Toggle navigation menu"
|
|
:aria-expanded="mobileMenuOpen"
|
|
>
|
|
<span class="sr-only">Open main menu</span>
|
|
<svg class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Mobile menu, show/hide based on menu state -->
|
|
<div
|
|
x-show="mobileMenuOpen"
|
|
x-transition:enter="transition ease-out duration-100 transform"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-75 transform"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
class="md:hidden absolute top-16 inset-x-0 bg-white shadow-md z-50"
|
|
>
|
|
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
|
<a href="/" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Home</a>
|
|
<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">
|
|
<div class="border-t border-gray-200 mt-1 pt-1">
|
|
<p class="px-3 py-1 text-xs font-semibold text-red-600 uppercase tracking-wider flex items-center">
|
|
<i class="fas fa-shield-alt mr-1" aria-hidden="true"></i> Admin
|
|
</p>
|
|
<a href="/settings" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
|
<i class="fas fa-cog mr-2 text-gray-400" aria-hidden="true"></i> Settings
|
|
</a>
|
|
<a href="/admin/users" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
|
<i class="fas fa-users mr-2 text-blue-400" aria-hidden="true"></i> Users
|
|
</a>
|
|
<a href="/admin/plans" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
|
<i class="fas fa-layer-group mr-2 text-indigo-400" aria-hidden="true"></i> Plan Designer
|
|
</a>
|
|
<a href="/admin/credentials" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
|
<i class="fas fa-key mr-2 text-yellow-400" aria-hidden="true"></i> Credentials
|
|
</a>
|
|
<a href="/admin/files" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
|
<i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i> File Manager
|
|
</a>
|
|
<a href="/duplicates" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
|
<i class="fas fa-clone mr-2 text-orange-400" aria-hidden="true"></i> Duplicates
|
|
</a>
|
|
<a href="/similarity" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
|
<i class="fas fa-project-diagram mr-2 text-blue-400" aria-hidden="true"></i> Similarity
|
|
</a>
|
|
<a href="/admin/queue" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
|
<i class="fas fa-stream mr-2 text-blue-400" aria-hidden="true"></i> Queue Monitor
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status (de-emphasized) in mobile -->
|
|
<a href="/status" class="block px-3 py-3 rounded-md text-sm font-medium text-gray-400 hover:text-gray-600 hover:bg-gray-50">
|
|
<i class="fas fa-circle-dot mr-1" aria-hidden="true"></i> Status
|
|
</a>
|
|
|
|
<a href="/about" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">About</a>
|
|
|
|
<!-- Dark mode toggle (mobile) -->
|
|
<button
|
|
onclick="toggleDarkMode()"
|
|
type="button"
|
|
class="flex items-center w-full px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
|
id="darkModeToggleMobile"
|
|
aria-label="Toggle dark mode"
|
|
>
|
|
<i class="fas fa-moon mr-2" id="darkModeIconMobile"></i>
|
|
<span id="darkModeTextMobile">Dark Mode</span>
|
|
</button>
|
|
|
|
<!-- Mobile Auth Section -->
|
|
<div id="mobileAuthSection" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
|
<!-- Will be populated by JS -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content -->
|
|
<main id="main-content" class="flex-grow">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<!-- Footer -->
|
|
<footer class="bg-white shadow" role="contentinfo">
|
|
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
|
|
DocuElevate 2025 -
|
|
<nav aria-label="Footer navigation" class="inline">
|
|
<a href="/privacy" class="text-blue-500 hover:underline">Privacy</a> -
|
|
<a href="/imprint" class="text-blue-500 hover:underline">Imprint</a> -
|
|
<a href="/terms" class="text-blue-500 hover:underline">Terms</a> -
|
|
<a href="/cookies" class="text-blue-500 hover:underline">Cookies</a> -
|
|
<a href="/license" class="text-blue-500 hover:underline">License</a> -
|
|
<a href="/attribution" class="text-blue-500 hover:underline">Attributions</a>
|
|
</nav> -
|
|
<span class="text-xs">Version {{ app_version|default(version, true) }}{% if release_name %} "{{ release_name }}"{% endif %}</span>
|
|
</div>
|
|
</footer>
|
|
|
|
<!-- Cookie Notice Banner -->
|
|
<!-- DocuElevate uses only strictly necessary session cookies (no tracking/analytics).
|
|
Under GDPR Art. 5(3) ePrivacy Directive, strictly necessary cookies do not require
|
|
prior consent, but transparency is required. This banner provides that notice. -->
|
|
<div id="cookieNotice"
|
|
class="fixed bottom-0 inset-x-0 z-50 bg-gray-800 text-white text-sm px-4 py-3 flex flex-col sm:flex-row items-center justify-between gap-2"
|
|
style="display:none!important"
|
|
aria-live="polite"
|
|
role="region"
|
|
aria-label="Cookie notice">
|
|
<p class="text-center sm:text-left">
|
|
DocuElevate uses only essential session cookies required for authentication and service operation.
|
|
No tracking or analytics cookies are used.
|
|
<a href="/cookies" class="underline hover:text-blue-300 ml-1">Cookie Policy</a> ·
|
|
<a href="/privacy" class="underline hover:text-blue-300 ml-1">Privacy Notice</a>
|
|
</p>
|
|
<button
|
|
id="cookieNoticeAccept"
|
|
type="button"
|
|
class="flex-shrink-0 bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-1.5 rounded focus:outline-none focus:ring-2 focus:ring-blue-400"
|
|
aria-label="Acknowledge cookie notice">
|
|
Got it
|
|
</button>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var NOTICE_KEY = 'cookieNoticeDismissed';
|
|
var notice = document.getElementById('cookieNotice');
|
|
var btn = document.getElementById('cookieNoticeAccept');
|
|
if (!localStorage.getItem(NOTICE_KEY)) {
|
|
notice.style.removeProperty('display');
|
|
}
|
|
if (btn) {
|
|
btn.addEventListener('click', function () {
|
|
localStorage.setItem(NOTICE_KEY, '1');
|
|
notice.style.display = 'none';
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<!-- Common JS (shared) -->
|
|
<script src="/static/js/common.js"></script>
|
|
|
|
<!-- Let child pages define extra scripts if needed -->
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|