feat(ui): internationalize profile, subscription, integrations templates and user menu JS

- Add 155 new translation keys to en.json (common, nav, integrations, profile, subscription sections)
- Add window.__i18n script block to base.html for user menu strings
- Replace all hardcoded English strings in common.js with window.__i18n lookups using safe fallback pattern
- Internationalize profile.html (page title, headings, labels, hints, placeholders, theme options)
- Internationalize subscription.html (page title, plan cards, usage stats, plan actions, badges)
- Internationalize integrations_dashboard.html (header, quota bars, modal, all form fields, action buttons, webhook section, delete modal)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-14 12:30:27 +00:00
parent 36172160ac
commit 5a025576c4
6 changed files with 380 additions and 217 deletions
+17
View File
@@ -548,6 +548,23 @@
})();
</script>
<!-- i18n strings for common.js (user menu) -->
<script>
window.__i18n = {
accountMenu: {{ _("nav.account_menu") | tojson }},
accountMenuFor: {{ _("nav.account_menu_for") | tojson }},
avatar: {{ _("common.avatar") | tojson }},
profileSettings: {{ _("nav.profile_settings") | tojson }},
mySubscription: {{ _("nav.my_subscription") | tojson }},
apiTokens: {{ _("nav.api_tokens") | tojson }},
sharedLinks: {{ _("nav.shared_links") | tojson }},
signOut: {{ _("nav.sign_out") | tojson }},
logIn: {{ _("nav.login") | tojson }},
signUp: {{ _("nav.signup") | tojson }},
getStarted: {{ _("nav.get_started") | tojson }},
};
</script>
<!-- Common JS (shared) -->
<script src="/static/js/common.js"></script>
+116 -125
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Integrations DocuElevate{% endblock %}
{% block title %}{{ _("integrations.page_title") }} DocuElevate{% endblock %}
{% block content %}
<div
@@ -13,10 +13,10 @@
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<i class="fas fa-plug text-indigo-500" aria-hidden="true"></i>
Integrations
{{ _("integrations.page_title") }}
</h1>
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">
Manage your ingestion sources and storage destinations in one place.
{{ _("integrations.subtitle") }}
</p>
</div>
<button
@@ -27,7 +27,7 @@
style="min-height:44px;"
:aria-disabled="!canAddAny"
>
<i class="fas fa-plus mr-2" aria-hidden="true"></i> Add Integration
<i class="fas fa-plus mr-2" aria-hidden="true"></i> {{ _("integrations.add_button") }}
</button>
</div>
@@ -41,24 +41,23 @@
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-700 dark:text-gray-200">
Mailbox Sources:
{{ _("integrations.sources_quota_label") }}
<strong x-text="quota.src_count"></strong>
<template x-if="quota.max_sources !== null && quota.max_sources > 0">
<span> / <strong x-text="quota.max_sources"></strong></span>
</template>
<template x-if="quota.max_sources === null">
<span class="text-gray-400"> / unlimited</span>
<span class="text-gray-400"> {{ _("integrations.quota_unlimited") }}</span>
</template>
<template x-if="quota.max_sources === 0">
<span class="text-gray-400 ml-1">(not available)</span>
<span class="text-gray-400 ml-1">{{ _("integrations.quota_not_available") }}</span>
</template>
</p>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
Plan: <span x-text="quota.tier_name"></span>
{{ _("integrations.plan_label") }} <span x-text="quota.tier_name"></span>
</p>
</div>
<!-- Mini usage bar -->
<template x-if="quota.max_sources !== null && quota.max_sources > 0">
<div class="w-24 flex-shrink-0">
<div class="bg-gray-200 dark:bg-gray-600 rounded-full h-2 overflow-hidden">
<div
@@ -77,7 +76,7 @@
</div>
<template x-if="quota.max_sources !== null && quota.src_count >= quota.max_sources && quota.max_sources >= 0">
<a href="/subscription" class="inline-flex items-center text-xs text-indigo-600 hover:text-indigo-800 mt-2">
<i class="fas fa-arrow-up mr-1" aria-hidden="true"></i> Upgrade Plan
<i class="fas fa-arrow-up mr-1" aria-hidden="true"></i> {{ _("integrations.upgrade_plan") }}
</a>
</template>
</div>
@@ -90,17 +89,17 @@
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-700 dark:text-gray-200">
Storage Destinations:
{{ _("integrations.destinations_quota_label") }}
<strong x-text="quota.dest_count"></strong>
<template x-if="quota.max_destinations !== null">
<span> / <strong x-text="quota.max_destinations"></strong></span>
</template>
<template x-if="quota.max_destinations === null">
<span class="text-gray-400"> / unlimited</span>
<span class="text-gray-400"> {{ _("integrations.quota_unlimited") }}</span>
</template>
</p>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
Plan: <span x-text="quota.tier_name"></span>
{{ _("integrations.plan_label") }} <span x-text="quota.tier_name"></span>
</p>
</div>
<!-- Mini usage bar -->
@@ -123,7 +122,7 @@
</div>
<template x-if="quota.max_destinations !== null && quota.dest_count >= quota.max_destinations">
<a href="/subscription" class="inline-flex items-center text-xs text-indigo-600 hover:text-indigo-800 mt-2">
<i class="fas fa-arrow-up mr-1" aria-hidden="true"></i> Upgrade Plan
<i class="fas fa-arrow-up mr-1" aria-hidden="true"></i> {{ _("integrations.upgrade_plan") }}
</a>
</template>
</div>
@@ -151,7 +150,7 @@
<template x-if="loading">
<div class="text-center py-12 text-gray-400">
<i class="fas fa-spinner fa-spin text-3xl mb-3" aria-hidden="true"></i>
<p>Loading integrations</p>
<p>{{ _("integrations.loading") }}</p>
</div>
</template>
@@ -159,9 +158,9 @@
<template x-if="!loading && integrations.length === 0">
<div class="text-center py-16 bg-white dark:bg-gray-800 rounded-lg shadow">
<i class="fas fa-plug text-5xl text-gray-300 mb-4" aria-hidden="true"></i>
<h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">No integrations configured</h2>
<h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">{{ _("integrations.empty_state") }}</h2>
<p class="text-gray-500 dark:text-gray-400 mb-6 max-w-md mx-auto">
Add your first integration to connect ingestion sources (like IMAP) and storage destinations (like S3, Dropbox, or Google Drive).
{{ _("integrations.no_integrations_body") }}
</p>
<button
type="button"
@@ -170,7 +169,7 @@
class="inline-flex items-center px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white text-sm font-medium rounded-md"
style="min-height:44px;"
>
<i class="fas fa-plus mr-2" aria-hidden="true"></i> Add Your First Integration
<i class="fas fa-plus mr-2" aria-hidden="true"></i> {{ _("integrations.add_first_button") }}
</button>
</div>
</template>
@@ -179,7 +178,7 @@
<template x-if="!loading && sources.length > 0">
<section class="mb-8">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center gap-2">
<i class="fas fa-inbox text-blue-500" aria-hidden="true"></i> Sources
<i class="fas fa-inbox text-blue-500" aria-hidden="true"></i> {{ _("integrations.sources_section") }}
</h2>
<div class="space-y-3">
<template x-for="intg in sources" :key="intg.id">
@@ -196,7 +195,7 @@
<span
class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
:class="intg.is_active ? 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300' : 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'"
x-text="intg.is_active ? 'Active' : 'Paused'"
x-text="intg.is_active ? '{{ _('common.active') }}' : '{{ _('integrations.paused') }}'"
></span>
</p>
<template x-if="intg.last_error">
@@ -216,7 +215,7 @@
:aria-label="`${intg.has_credentials ? 'Re-authorize' : 'Authorize'} ${intg.name}`"
>
<i class="fas fa-key mr-1" aria-hidden="true"></i>
<span x-text="intg.has_credentials ? 'Re-Authorize' : 'Authorize'"></span>
<span x-text="intg.has_credentials ? '{{ _('integrations.authorize_reauth') }}' : '{{ _('integrations.authorize_btn') }}'"></span>
</a>
</template>
<button
@@ -233,7 +232,7 @@
<template x-if="testingId !== intg.id">
<i class="fas fa-plug mr-1" aria-hidden="true"></i>
</template>
Test
{{ _("common.test") }}
</button>
<button
type="button"
@@ -242,7 +241,7 @@
style="min-height:36px;"
:aria-label="`Edit ${intg.name}`"
>
<i class="fas fa-edit mr-1" aria-hidden="true"></i>Edit
<i class="fas fa-edit mr-1" aria-hidden="true"></i>{{ _("common.edit") }}
</button>
<button
type="button"
@@ -251,7 +250,7 @@
style="min-height:36px;"
:aria-label="`Delete ${intg.name}`"
>
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>Delete
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>{{ _("common.delete") }}
</button>
</div>
</div>
@@ -265,7 +264,7 @@
<template x-if="!loading && destinations.length > 0">
<section class="mb-8">
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center gap-2">
<i class="fas fa-hdd text-green-500" aria-hidden="true"></i> Destinations
<i class="fas fa-hdd text-green-500" aria-hidden="true"></i> {{ _("integrations.destinations_section") }}
</h2>
<div class="space-y-3">
<template x-for="intg in destinations" :key="intg.id">
@@ -282,7 +281,7 @@
<span
class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
:class="intg.is_active ? 'bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300' : 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'"
x-text="intg.is_active ? 'Active' : 'Paused'"
x-text="intg.is_active ? '{{ _('common.active') }}' : '{{ _('integrations.paused') }}'"
></span>
</p>
<template x-if="intg.last_error">
@@ -302,7 +301,7 @@
:aria-label="`${intg.has_credentials ? 'Re-authorize' : 'Authorize'} ${intg.name}`"
>
<i class="fas fa-key mr-1" aria-hidden="true"></i>
<span x-text="intg.has_credentials ? 'Re-Authorize' : 'Authorize'"></span>
<span x-text="intg.has_credentials ? '{{ _('integrations.authorize_reauth') }}' : '{{ _('integrations.authorize_btn') }}'"></span>
</a>
</template>
<button
@@ -319,7 +318,7 @@
<template x-if="testingId !== intg.id">
<i class="fas fa-plug mr-1" aria-hidden="true"></i>
</template>
Test
{{ _("common.test") }}
</button>
<button
type="button"
@@ -328,7 +327,7 @@
style="min-height:36px;"
:aria-label="`Edit ${intg.name}`"
>
<i class="fas fa-edit mr-1" aria-hidden="true"></i>Edit
<i class="fas fa-edit mr-1" aria-hidden="true"></i>{{ _("common.edit") }}
</button>
<button
type="button"
@@ -337,7 +336,7 @@
style="min-height:36px;"
:aria-label="`Delete ${intg.name}`"
>
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>Delete
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i>{{ _("common.delete") }}
</button>
</div>
</div>
@@ -371,13 +370,13 @@
<h2
:id="editingIntegration ? 'modal-title-edit' : 'modal-title-create'"
class="text-lg font-semibold text-gray-900 dark:text-white"
x-text="editingIntegration ? 'Edit Integration' : 'Add Integration'"
x-text="editingIntegration ? '{{ _('integrations.modal_title_edit') }}' : '{{ _('integrations.modal_title_add') }}'"
></h2>
<button
type="button"
@click="closeModal()"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 rounded"
aria-label="Close modal"
aria-label="{{ _('integrations.modal_close') }}"
>
<i class="fas fa-times text-xl" aria-hidden="true"></i>
</button>
@@ -388,7 +387,7 @@
<!-- Direction -->
<div>
<label for="intg-direction" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
Direction <span class="text-red-500" aria-hidden="true">*</span>
{{ _("integrations.direction_label") }} <span class="text-red-500" aria-hidden="true">*</span>
</label>
<select
id="intg-direction"
@@ -399,16 +398,16 @@
aria-required="true"
@change="onDirectionChange()"
>
<option value="">— Select —</option>
<option value="SOURCE">Source (ingestion)</option>
<option value="DESTINATION">Destination (storage)</option>
<option value="">{{ _("integrations.direction_placeholder") }}</option>
<option value="SOURCE">{{ _("integrations.direction_source") }}</option>
<option value="DESTINATION">{{ _("integrations.direction_destination") }}</option>
</select>
</div>
<!-- Integration type -->
<div>
<label for="intg-type" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
Integration Type <span class="text-red-500" aria-hidden="true">*</span>
{{ _("integrations.type_label") }} <span class="text-red-500" aria-hidden="true">*</span>
</label>
<select
id="intg-type"
@@ -419,7 +418,7 @@
aria-required="true"
@change="onTypeChange()"
>
<option value="">— Select direction first —</option>
<option value="">{{ _("integrations.type_placeholder") }}</option>
<template x-for="t in availableTypes" :key="t">
<option :value="t" x-text="typeLabel(t)"></option>
</template>
@@ -429,13 +428,13 @@
<!-- Name -->
<div>
<label for="intg-name" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
Label <span class="text-red-500" aria-hidden="true">*</span>
{{ _("integrations.name_label") }} <span class="text-red-500" aria-hidden="true">*</span>
</label>
<input
id="intg-name"
type="text"
x-model="form.name"
placeholder="e.g. Work Gmail, S3 Archive"
placeholder="{{ _('integrations.name_placeholder') }}"
required
maxlength="255"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm"
@@ -448,46 +447,46 @@
<!-- IMAP fields -->
<template x-if="form.integration_type === 'IMAP'">
<div class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">IMAP Settings</p>
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ _("integrations.imap_settings") }}</p>
<div class="grid grid-cols-3 gap-3">
<div class="col-span-2">
<label for="imap-host" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Host <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="imap-host" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.host_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="imap-host" type="text" x-model="form.config.host" placeholder="imap.example.com" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="imap-port" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Port</label>
<label for="imap-port" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.port_label") }}</label>
<input id="imap-port" type="number" x-model.number="form.config.port" min="1" max="65535" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
</div>
<div>
<label for="imap-username" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Username <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="imap-username" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.username_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="imap-username" type="text" x-model="form.config.username" placeholder="user@example.com" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="imap-password" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Password <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="imap-password" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.password_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="imap-password" :type="showPassword ? 'text' : 'password'" x-model="form.credentials.password" :placeholder="editingIntegration ? '(unchanged if blank)' : ''" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div class="flex flex-wrap items-center gap-4">
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="form.config.use_ssl" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Use SSL
{{ _("integrations.use_ssl") }}
</label>
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="form.config.delete_after_process" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Delete emails after processing
{{ _("integrations.delete_emails_label") }}
</label>
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="form.config.gmail_apply_labels" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Apply Gmail labels &amp; star
{{ _("integrations.gmail_labels") }}
</label>
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="showPassword" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Show password
{{ _("integrations.show_password") }}
</label>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
Gmail labels &amp; star: when enabled, processed emails are starred and tagged with an "Ingested" label in Gmail. Only applies to Gmail servers.
{{ _("integrations.gmail_hint") }}
</p>
</div>
</template>
@@ -495,36 +494,36 @@
<!-- S3 fields -->
<template x-if="form.integration_type === 'S3'">
<div class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">S3 Settings</p>
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ _("integrations.s3_settings") }}</p>
<div>
<label for="s3-bucket" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Bucket <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="s3-bucket" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.bucket_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="s3-bucket" type="text" x-model="form.config.bucket" placeholder="my-bucket" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="s3-region" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Region</label>
<label for="s3-region" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.region_label") }}</label>
<input id="s3-region" type="text" x-model="form.config.region" placeholder="us-east-1" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div>
<label for="s3-prefix" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Folder Prefix</label>
<label for="s3-prefix" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.folder_prefix_label") }}</label>
<input id="s3-prefix" type="text" x-model="form.config.folder_prefix" placeholder="documents/" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
</div>
<div>
<label for="s3-endpoint" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Endpoint URL <span class="text-gray-400">(optional, for S3-compatible)</span></label>
<label for="s3-endpoint" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.endpoint_label") }} <span class="text-gray-400">{{ _("integrations.endpoint_optional") }}</span></label>
<input id="s3-endpoint" type="url" x-model="form.config.endpoint_url" placeholder="https://s3.example.com" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div>
<label for="s3-key" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Access Key ID <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="s3-key" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.access_key_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="s3-key" type="text" x-model="form.credentials.access_key_id" :placeholder="editingIntegration ? '(unchanged if blank)' : ''" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div>
<label for="s3-secret" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Secret Access Key <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="s3-secret" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.secret_key_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="s3-secret" :type="showPassword ? 'text' : 'password'" x-model="form.credentials.secret_access_key" :placeholder="editingIntegration ? '(unchanged if blank)' : ''" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="showPassword" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Show secrets
{{ _("integrations.show_secrets") }}
</label>
</div>
</template>
@@ -534,24 +533,24 @@
<div class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider" x-text="form.integration_type === 'NEXTCLOUD' ? 'Nextcloud Settings' : 'WebDAV Settings'"></p>
<div>
<label for="webdav-url" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">URL <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="webdav-url" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.webdav_url_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="webdav-url" type="url" x-model="form.config.url" placeholder="https://cloud.example.com/remote.php/dav/files/user/" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="webdav-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Folder Path</label>
<label for="webdav-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.folder_path_label") }}</label>
<input id="webdav-folder" type="text" x-model="form.config.folder" placeholder="/Documents" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div>
<label for="webdav-user" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Username <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="webdav-user" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.username_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="webdav-user" type="text" x-model="form.credentials.username" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div>
<label for="webdav-pass" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Password <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="webdav-pass" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.password_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="webdav-pass" :type="showPassword ? 'text' : 'password'" x-model="form.credentials.password" :placeholder="editingIntegration ? '(unchanged if blank)' : ''" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="showPassword" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Show password
{{ _("integrations.show_password") }}
</label>
</div>
</template>
@@ -562,29 +561,29 @@
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider" x-text="form.integration_type + ' Settings'"></p>
<div class="grid grid-cols-3 gap-3">
<div class="col-span-2">
<label for="ftp-host" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Host <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="ftp-host" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.host_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="ftp-host" type="text" x-model="form.config.host" placeholder="ftp.example.com" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="ftp-port" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Port</label>
<label for="ftp-port" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.port_label") }}</label>
<input id="ftp-port" type="number" x-model.number="form.config.port" :placeholder="form.integration_type === 'SFTP' ? '22' : '21'" min="1" max="65535" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
</div>
<div>
<label for="ftp-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Remote Path</label>
<label for="ftp-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.remote_path_label") }}</label>
<input id="ftp-folder" type="text" x-model="form.config.folder" placeholder="/upload" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div>
<label for="ftp-user" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Username <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="ftp-user" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.username_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="ftp-user" type="text" x-model="form.credentials.username" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div>
<label for="ftp-pass" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Password <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="ftp-pass" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.password_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="ftp-pass" :type="showPassword ? 'text' : 'password'" x-model="form.credentials.password" :placeholder="editingIntegration ? '(unchanged if blank)' : ''" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="showPassword" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Show password
{{ _("integrations.show_password") }}
</label>
</div>
</template>
@@ -594,13 +593,12 @@
<div class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider" x-text="typeLabel(form.integration_type) + ' Settings'"></p>
<div>
<label for="oauth-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Folder <span class="text-gray-400">(optional)</span></label>
<label for="oauth-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.oauth_folder_label") }} <span class="text-gray-400">{{ _("integrations.folder_optional") }}</span></label>
<input id="oauth-folder" type="text" x-model="form.config.folder" placeholder="/DocuElevate" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div class="bg-blue-50 dark:bg-blue-900/30 rounded-md p-3 text-sm text-blue-700 dark:text-blue-300">
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
Save this integration first, then use the <strong>Authorize</strong> button to complete the OAuth flow.
Your credentials will be stored securely in your personal integration record.
{{ _("integrations.oauth_hint") }}
</div>
</div>
</template>
@@ -608,9 +606,9 @@
<!-- Email (destination) fields -->
<template x-if="form.integration_type === 'EMAIL'">
<div class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Email Forward Settings</p>
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ _("integrations.email_settings") }}</p>
<div>
<label for="email-to" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Recipient Email <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="email-to" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.recipient_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="email-to" type="email" x-model="form.config.recipient" placeholder="archive@example.com" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
</div>
@@ -619,11 +617,11 @@
<!-- Watch Folder fields -->
<template x-if="form.integration_type === 'WATCH_FOLDER'">
<div class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Watch Folder Settings</p>
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ _("integrations.watch_folder_settings") }}</p>
<div>
<label for="wf-source-type" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Source Type <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-source-type" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.source_type_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<select id="wf-source-type" x-model="form.config.source_type" @change="onWatchFolderSourceTypeChange()" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true">
<option value="local">Local Filesystem</option>
<option value="local">{{ _("integrations.source_local") }}</option>
<option value="s3">Amazon S3</option>
<option value="dropbox">Dropbox</option>
<option value="google_drive">Google Drive</option>
@@ -636,7 +634,7 @@
<!-- Local filesystem fields -->
<template x-if="form.config.source_type === 'local'">
<div>
<label for="wf-path" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Folder Path <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-path" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.folder_path_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-path" type="text" x-model="form.config.folder_path" placeholder="/data/inbox" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
</template>
@@ -646,29 +644,29 @@
<div class="space-y-3">
<div class="grid grid-cols-2 gap-3">
<div>
<label for="wf-s3-bucket" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Bucket <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-s3-bucket" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.bucket_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-s3-bucket" type="text" x-model="form.config.bucket" placeholder="my-bucket" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="wf-s3-region" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Region</label>
<label for="wf-s3-region" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.region_label") }}</label>
<input id="wf-s3-region" type="text" x-model="form.config.region" placeholder="us-east-1" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
</div>
<div>
<label for="wf-s3-prefix" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Prefix (folder path)</label>
<label for="wf-s3-prefix" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.s3_prefix_label") }}</label>
<input id="wf-s3-prefix" type="text" x-model="form.config.prefix" placeholder="inbox/" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div>
<label for="wf-s3-endpoint" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Endpoint URL <span class="text-xs text-gray-400">(optional, for S3-compatible)</span></label>
<label for="wf-s3-endpoint" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.endpoint_label") }} <span class="text-xs text-gray-400">{{ _("integrations.endpoint_optional") }}</span></label>
<input id="wf-s3-endpoint" type="text" x-model="form.config.endpoint_url" placeholder="https://s3.example.com" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="wf-s3-ak" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Access Key ID <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-s3-ak" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.access_key_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-s3-ak" type="text" x-model="form.credentials.access_key_id" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="wf-s3-sk" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Secret Access Key <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-s3-sk" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.secret_key_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-s3-sk" :type="showPassword ? 'text' : 'password'" x-model="form.credentials.secret_access_key" :placeholder="editingIntegration ? '(unchanged if blank)' : ''" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
</div>
@@ -679,7 +677,7 @@
<template x-if="form.config.source_type === 'dropbox'">
<div class="space-y-3">
<div>
<label for="wf-dbx-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Folder Path <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-dbx-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.folder_path_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-dbx-folder" type="text" x-model="form.config.folder_path" placeholder="/Inbox/Scanner" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
@@ -717,7 +715,7 @@
<template x-if="form.config.source_type === 'onedrive'">
<div class="space-y-3">
<div>
<label for="wf-od-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Folder Path <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-od-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.folder_path_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-od-folder" type="text" x-model="form.config.folder_path" placeholder="/Documents/Inbox" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
@@ -741,20 +739,20 @@
<template x-if="form.config.source_type === 'nextcloud'">
<div class="space-y-3">
<div>
<label for="wf-nc-url" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Nextcloud URL <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-nc-url" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.nextcloud_settings") }} URL <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-nc-url" type="url" x-model="form.config.url" placeholder="https://cloud.example.com" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="wf-nc-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Folder Path <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-nc-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.folder_path_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-nc-folder" type="text" x-model="form.config.folder_path" placeholder="/Documents/Inbox" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="wf-nc-user" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Username <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-nc-user" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.username_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-nc-user" type="text" x-model="form.credentials.username" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="wf-nc-pass" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Password <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-nc-pass" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.password_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-nc-pass" :type="showPassword ? 'text' : 'password'" x-model="form.credentials.password" :placeholder="editingIntegration ? '(unchanged if blank)' : ''" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
</div>
@@ -765,20 +763,20 @@
<template x-if="form.config.source_type === 'webdav'">
<div class="space-y-3">
<div>
<label for="wf-dav-url" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">WebDAV URL <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-dav-url" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.webdav_url_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-dav-url" type="url" x-model="form.config.url" placeholder="https://webdav.example.com/dav/" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="wf-dav-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Folder Path <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-dav-folder" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.folder_path_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-dav-folder" type="text" x-model="form.config.folder_path" placeholder="/remote.php/webdav/Inbox" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="wf-dav-user" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Username <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-dav-user" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.username_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-dav-user" type="text" x-model="form.credentials.username" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="wf-dav-pass" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Password <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="wf-dav-pass" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.password_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="wf-dav-pass" :type="showPassword ? 'text' : 'password'" x-model="form.credentials.password" :placeholder="editingIntegration ? '(unchanged if blank)' : ''" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
</div>
@@ -789,12 +787,12 @@
<div class="flex flex-wrap items-center gap-4 pt-2">
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="form.config.delete_after_process" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Delete files after processing
{{ _("integrations.delete_files_label") }}
</label>
<template x-if="form.config.source_type !== 'local'">
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="showPassword" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Show secrets
{{ _("integrations.show_secrets") }}
</label>
</template>
</div>
@@ -804,18 +802,18 @@
<!-- Paperless fields -->
<template x-if="form.integration_type === 'PAPERLESS'">
<div class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Paperless NGX Settings</p>
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ _("integrations.paperless_settings") }}</p>
<div>
<label for="paperless-url" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">URL <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="paperless-url" type="url" x-model="form.config.url" placeholder="https://paperless.example.com" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" aria-required="true" />
</div>
<div>
<label for="paperless-token" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">API Token <span class="text-red-500" aria-hidden="true">*</span></label>
<label for="paperless-token" class="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">{{ _("integrations.api_token_label") }} <span class="text-red-500" aria-hidden="true">*</span></label>
<input id="paperless-token" :type="showPassword ? 'text' : 'password'" x-model="form.credentials.api_token" :placeholder="editingIntegration ? '(unchanged if blank)' : ''" :required="!editingIntegration" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-700 dark:text-white text-sm" />
</div>
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="showPassword" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Show token
{{ _("integrations.show_token") }}
</label>
</div>
</template>
@@ -824,38 +822,33 @@
<template x-if="form.integration_type === 'WEBHOOK'">
<div class="space-y-4 border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-xs font-semibold text-purple-500 uppercase tracking-wider">
<i class="fas fa-bolt mr-1" aria-hidden="true"></i> Webhook Ingestion
<i class="fas fa-bolt mr-1" aria-hidden="true"></i> {{ _("integrations.webhook_heading") }}
</p>
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h4 class="text-sm font-semibold text-blue-800 dark:text-blue-200 mb-2">
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
How Webhook Ingestion Works
{{ _("integrations.webhook_how_heading") }}
</h4>
<p class="text-sm text-blue-700 dark:text-blue-300 leading-relaxed">
A webhook integration allows external systems to push documents directly into DocuElevate
via the REST API. Instead of DocuElevate polling for new files (like IMAP), <strong>your
application sends files to DocuElevate</strong> using an HTTP request with an API token for
authentication.
{{ _("integrations.webhook_how_text") }}
</p>
<p class="text-sm text-blue-700 dark:text-blue-300 leading-relaxed mt-2">
This is ideal for <strong>CI/CD pipelines</strong>, <strong>automation scripts</strong>,
<strong>scanner integrations</strong>, or any system that generates documents and needs to
send them for processing.
{{ _("integrations.webhook_ideal_text") }}
</p>
</div>
<div class="bg-gray-50 dark:bg-gray-750 rounded-lg p-4">
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-2">
<i class="fas fa-terminal mr-1" aria-hidden="true"></i>
Quick Start
{{ _("integrations.webhook_quick_start") }}
</h4>
<ol class="text-sm text-gray-600 dark:text-gray-400 space-y-2 list-decimal list-inside">
<li>
Go to <a href="/api-tokens" class="text-indigo-600 hover:underline font-medium">API Tokens</a>
and create a personal token.
{{ _("integrations.webhook_create_token") }}
</li>
<li>Use the token to upload documents via the API:</li>
<li>{{ _("integrations.webhook_upload_with_token") }}</li>
</ol>
<div class="mt-3 relative">
@@ -901,9 +894,7 @@ print(response.json())</code></pre>
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3">
<p class="text-xs text-yellow-800 dark:text-yellow-300">
<i class="fas fa-shield-alt mr-1" aria-hidden="true"></i>
<strong>Security tip:</strong> Create a dedicated API token for each integration and
revoke it immediately if compromised. Tokens can be managed on the
<a href="/api-tokens" class="underline font-medium">API Tokens</a> page.
{{ _("integrations.webhook_security_tip") }}
</p>
</div>
</div>
@@ -914,7 +905,7 @@ print(response.json())</code></pre>
<div class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider" x-text="form.integration_type + ' Settings'"></p>
<p class="text-sm text-gray-500 dark:text-gray-400">
Configuration for this integration type can be provided as JSON after creation via the API.
{{ _("integrations.generic_settings_hint") }}
</p>
</div>
</template>
@@ -923,7 +914,7 @@ print(response.json())</code></pre>
<div class="flex items-center gap-2 pt-2">
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200 cursor-pointer">
<input type="checkbox" x-model="form.is_active" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
Active
{{ _("integrations.active_label") }}
</label>
</div>
@@ -934,7 +925,7 @@ print(response.json())</code></pre>
:class="testResult.success ? 'bg-green-50 border-green-400 text-green-800 dark:bg-opacity-10' : 'bg-red-50 border-red-400 text-red-800 dark:bg-opacity-10'"
role="status"
>
<p class="font-medium" x-text="testResult.success ? 'Connection successful' : 'Connection failed'"></p>
<p class="font-medium" x-text="testResult.success ? '{{ _('integrations.connection_success') }}' : '{{ _('integrations.connection_failed') }}'"></p>
<p class="text-xs mt-0.5" x-text="testResult.message"></p>
</div>
</template>
@@ -962,7 +953,7 @@ print(response.json())</code></pre>
<template x-if="!testing">
<i class="fas fa-plug mr-2" aria-hidden="true"></i>
</template>
Test Connection
{{ _("common.test_connection") }}
</button>
<div class="flex gap-2">
<button
@@ -971,7 +962,7 @@ print(response.json())</code></pre>
class="px-4 py-2 text-sm font-medium rounded border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500"
style="min-height:40px;"
>
Cancel
{{ _("common.cancel") }}
</button>
<button
type="submit"
@@ -982,7 +973,7 @@ print(response.json())</code></pre>
<template x-if="saving">
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>
</template>
<span x-text="editingIntegration ? 'Update' : 'Save'"></span>
<span x-text="editingIntegration ? '{{ _('common.update') }}' : '{{ _('common.save') }}'"></span>
</button>
</div>
</div>
@@ -1003,12 +994,12 @@ print(response.json())</code></pre>
>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-sm p-6" @click.stop>
<h2 id="delete-modal-title" class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Delete Integration?
{{ _("integrations.delete_confirm_title") }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
Are you sure you want to delete
{{ _("integrations.delete_confirm_are_you_sure") }}
<strong x-text="integrationToDelete ? integrationToDelete.name : ''"></strong>?
This action cannot be undone.
{{ _("integrations.delete_confirm_undone") }}
</p>
<div class="flex justify-end gap-2">
<button
@@ -1017,7 +1008,7 @@ print(response.json())</code></pre>
class="px-4 py-2 text-sm font-medium rounded border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500"
style="min-height:40px;"
>
Cancel
{{ _("common.cancel") }}
</button>
<button
type="button"
@@ -1029,7 +1020,7 @@ print(response.json())</code></pre>
<template x-if="deleting">
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>
</template>
Delete
{{ _("common.delete") }}
</button>
</div>
</div>
+33 -34
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Profile Settings DocuElevate{% endblock %}
{% block title %}{{ _("profile.page_title") }}{% endblock %}
{% block content %}
<div
@@ -12,10 +12,10 @@
<header class="mb-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<i class="fas fa-user-circle text-blue-500" aria-hidden="true"></i>
Profile Settings
{{ _("profile.heading") }}
</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage your display name, avatar, language, and theme preferences.
{{ _("profile.subtitle") }}
</p>
</header>
@@ -36,7 +36,7 @@
type="button"
@click="banner.visible = false"
class="ml-auto"
aria-label="Dismiss"
aria-label="{{ _('profile.dismiss') }}"
style="min-height:44px; min-width:44px;"
>
<i class="fas fa-times" aria-hidden="true"></i>
@@ -49,7 +49,7 @@
aria-labelledby="avatar-heading"
>
<h2 id="avatar-heading" class="text-base font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-camera text-gray-400 mr-2" aria-hidden="true"></i>Profile Picture
<i class="fas fa-camera text-gray-400 mr-2" aria-hidden="true"></i>{{ _("profile.avatar_heading") }}
</h2>
<div class="flex flex-col sm:flex-row items-center gap-6">
@@ -57,7 +57,7 @@
<div class="relative flex-shrink-0">
<img
:src="avatarUrl"
alt="Your profile picture"
alt="{{ _('profile.avatar_alt') }}"
class="w-24 h-24 rounded-full object-cover border-2 border-gray-200 dark:border-gray-600"
/>
<template x-if="hasCustomAvatar">
@@ -66,8 +66,8 @@
@click="removeAvatar()"
:disabled="saving"
class="absolute -top-1 -right-1 bg-red-500 hover:bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center shadow focus:outline-none focus:ring-2 focus:ring-red-400"
aria-label="Remove custom avatar"
title="Remove custom avatar"
aria-label="{{ _('profile.avatar_remove') }}"
title="{{ _('profile.avatar_remove') }}"
>
<i class="fas fa-times text-xs" aria-hidden="true"></i>
</button>
@@ -77,8 +77,7 @@
<!-- Upload controls -->
<div class="flex-1 space-y-3">
<p class="text-sm text-gray-600 dark:text-gray-400">
Upload a JPEG, PNG, GIF, or WebP image up to 2 MB.
If no custom picture is set, your <a href="https://gravatar.com" class="underline" target="_blank" rel="noopener noreferrer" aria-label="Gravatar (opens in new tab)">Gravatar</a> is shown.
{{ _("profile.avatar_upload_hint") }} <a href="https://gravatar.com" class="underline" target="_blank" rel="noopener noreferrer" aria-label="{{ _('profile.gravatar_link') }} (opens in new tab)">{{ _("profile.gravatar_link") }}</a> {{ _("profile.avatar_gravatar_suffix") }}
</p>
<label
for="avatar-input"
@@ -86,7 +85,7 @@
style="min-height:44px;"
>
<i class="fas fa-upload" aria-hidden="true"></i>
<span>Choose image</span>
<span>{{ _("profile.choose_image") }}</span>
<input
id="avatar-input"
type="file"
@@ -107,49 +106,49 @@
aria-labelledby="general-heading"
>
<h2 id="general-heading" class="text-base font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-id-card text-gray-400 mr-2" aria-hidden="true"></i>General Information
<i class="fas fa-id-card text-gray-400 mr-2" aria-hidden="true"></i>{{ _("profile.general_heading") }}
</h2>
<div class="space-y-4">
<!-- Display name -->
<div>
<label for="display-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Display Name
{{ _("profile.display_name_label") }}
</label>
<input
id="display-name"
type="text"
x-model="form.display_name"
maxlength="255"
placeholder="Your name as shown in the UI"
placeholder="{{ _('profile.display_name_placeholder') }}"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
style="min-height:44px;"
aria-describedby="display-name-hint"
/>
<p id="display-name-hint" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Leave blank to use your account username or email.
{{ _("profile.display_name_hint") }}
</p>
</div>
<!-- Contact email -->
<div>
<label for="contact-email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Contact / Notification E-mail
{{ _("profile.contact_email_label") }}
</label>
<input
id="contact-email"
type="email"
x-model="form.contact_email"
maxlength="255"
placeholder="you@example.com"
placeholder="{{ _('profile.contact_email_placeholder') }}"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
style="min-height:44px;"
aria-describedby="contact-email-hint"
/>
<p id="contact-email-hint" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Used for system notifications. This does not change your login e-mail.
{{ _("profile.contact_email_hint") }}
</p>
</div>
</div>
@@ -161,14 +160,14 @@
aria-labelledby="prefs-heading"
>
<h2 id="prefs-heading" class="text-base font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-sliders-h text-gray-400 mr-2" aria-hidden="true"></i>Preferences
<i class="fas fa-sliders-h text-gray-400 mr-2" aria-hidden="true"></i>{{ _("profile.preferences_heading") }}
</h2>
<div class="space-y-5">
<!-- Language -->
<div>
<label for="lang-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<i class="fas fa-globe text-gray-400 mr-1" aria-hidden="true"></i>UI Language
<i class="fas fa-globe text-gray-400 mr-1" aria-hidden="true"></i>{{ _("profile.language_label") }}
</label>
<select
id="lang-select"
@@ -177,20 +176,20 @@
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
style="min-height:44px;"
>
<option value="">Auto-detect (from browser)</option>
<option value="">{{ _("profile.language_auto_detect") }}</option>
{% for lang in supported_languages %}
<option value="{{ lang.code }}">{{ lang.flag }} {{ lang.native }} ({{ lang.name }})</option>
{% endfor %}
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Choose your preferred interface language. "Auto-detect" follows your browser settings.
{{ _("profile.language_hint") }}
</p>
</div>
<!-- Theme -->
<fieldset>
<legend class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<i class="fas fa-palette text-gray-400 mr-1" aria-hidden="true"></i>Colour Scheme
<i class="fas fa-palette text-gray-400 mr-1" aria-hidden="true"></i>{{ _("profile.theme_label") }}
</legend>
<div class="flex flex-wrap gap-3">
<label
@@ -202,7 +201,7 @@
style="min-height:44px; min-width:44px;"
>
<input type="radio" name="theme" value="light" x-model="form.preferred_theme" class="sr-only" />
<i class="fas fa-sun" aria-hidden="true"></i> Light
<i class="fas fa-sun" aria-hidden="true"></i> {{ _("profile.theme_light") }}
</label>
<label
class="relative flex items-center gap-2 cursor-pointer rounded-lg border px-4 py-3 text-sm transition-colors
@@ -213,7 +212,7 @@
style="min-height:44px; min-width:44px;"
>
<input type="radio" name="theme" value="dark" x-model="form.preferred_theme" class="sr-only" />
<i class="fas fa-moon" aria-hidden="true"></i> Dark
<i class="fas fa-moon" aria-hidden="true"></i> {{ _("profile.theme_dark") }}
</label>
<label
class="relative flex items-center gap-2 cursor-pointer rounded-lg border px-4 py-3 text-sm transition-colors
@@ -224,11 +223,11 @@
style="min-height:44px; min-width:44px;"
>
<input type="radio" name="theme" value="system" x-model="form.preferred_theme" class="sr-only" />
<i class="fas fa-desktop" aria-hidden="true"></i> System Default
<i class="fas fa-desktop" aria-hidden="true"></i> {{ _("profile.theme_system") }}
</label>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
"System Default" follows your device's dark-mode setting.
{{ _("profile.theme_hint") }}
</p>
</fieldset>
</div>
@@ -245,7 +244,7 @@
style="min-height:44px; min-width:44px;"
>
<i class="fas fa-save" aria-hidden="true"></i>
<span x-text="saving ? 'Saving…' : 'Save Changes'"></span>
<span x-text="saving ? '{{ _('profile.saving') }}' : '{{ _('profile.save_button') }}'"></span>
</button>
</div>
@@ -256,13 +255,13 @@
aria-labelledby="password-heading"
>
<h2 id="password-heading" class="text-base font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-lock text-gray-400 mr-2" aria-hidden="true"></i>Change Password
<i class="fas fa-lock text-gray-400 mr-2" aria-hidden="true"></i>{{ _("profile.password_heading") }}
</h2>
<div class="space-y-4 max-w-md">
<div>
<label for="current-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Current Password
{{ _("profile.current_password") }}
</label>
<input
id="current-password"
@@ -276,7 +275,7 @@
</div>
<div>
<label for="new-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
New Password
{{ _("profile.new_password") }}
</label>
<input
id="new-password"
@@ -288,11 +287,11 @@
style="min-height:44px;"
aria-describedby="new-pw-hint"
/>
<p id="new-pw-hint" class="mt-1 text-xs text-gray-500 dark:text-gray-400">Minimum 8 characters.</p>
<p id="new-pw-hint" class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ _("profile.new_password_hint") }}</p>
</div>
<div>
<label for="confirm-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirm New Password
{{ _("profile.confirm_password") }}
</label>
<input
id="confirm-password"
@@ -314,7 +313,7 @@
style="min-height:44px;"
>
<i class="fas fa-key" aria-hidden="true"></i>
<span x-text="pwSaving ? 'Updating…' : 'Update Password'"></span>
<span x-text="pwSaving ? '{{ _('profile.updating') }}' : '{{ _('profile.update_password') }}'"></span>
</button>
</div>
</div>
+39 -40
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}My Subscription DocuElevate{% endblock %}
{% block title %}{{ _("subscription.page_title") }}{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 max-w-4xl">
@@ -8,9 +8,9 @@
<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
{{ _("subscription.heading") }}
</h1>
<p class="text-gray-500 text-sm mt-1">Your current plan, usage and available upgrades.</p>
<p class="text-gray-500 text-sm mt-1">{{ _("subscription.subtitle") }}</p>
</div>
<!-- Flash messages -->
@@ -21,11 +21,10 @@
<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="font-semibold text-blue-800">{{ _("subscription.single_user_title") }}</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>.
{{ _("subscription.single_user_body_before_link") }}
<a href="/settings" class="underline hover:text-blue-900">{{ _("nav.settings") }}</a>{{ _("subscription.single_user_body_after_link") }}
</p>
</div>
</div>
@@ -37,17 +36,17 @@
<div class="bg-amber-50 border border-amber-300 rounded-xl p-4 mb-6 flex items-start gap-3">
<i class="fas fa-clock text-amber-500 text-xl flex-shrink-0 mt-0.5" aria-hidden="true"></i>
<div class="flex-1">
<p class="font-semibold text-amber-800">Pending plan change scheduled</p>
<p class="font-semibold text-amber-800">{{ _("subscription.pending_title") }}</p>
<p class="text-amber-700 text-sm mt-1">
Your plan will change to <strong>{{ pending_tier.name }}</strong>
on <strong>{% if pending_date %}{{ pending_date.strftime('%B') }} {{ pending_date.day }}, {{ pending_date.year }}{% endif %}</strong>.
You keep all current plan benefits until then.
{{ _("subscription.pending_will_change") }} <strong>{{ pending_tier.name }}</strong>
{{ _("subscription.pending_on") }} <strong>{% if pending_date %}{{ pending_date.strftime('%B') }} {{ pending_date.day }}, {{ pending_date.year }}{% endif %}</strong>.
{{ _("subscription.pending_benefits") }}
</p>
</div>
<button type="button" onclick="cancelPendingChange()"
class="ml-auto flex-shrink-0 text-sm text-amber-700 underline hover:text-amber-900 font-medium"
aria-label="Cancel pending plan change">
Cancel change
aria-label="{{ _('subscription.cancel_pending') }}">
{{ _("subscription.cancel_pending") }}
</button>
</div>
{% endif %}
@@ -68,18 +67,18 @@
{% else %}fa-bolt 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-xs font-semibold text-gray-400 uppercase tracking-widest">{{ _("subscription.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>
<p class="mt-3 text-lg font-bold text-indigo-600">${{ tier.price_monthly }}<span class="text-sm font-normal text-gray-400">{{ _("subscription.per_month") }}</span></p>
{% else %}
<p class="mt-3 text-lg font-bold text-gray-500">Free</p>
<p class="mt-3 text-lg font-bold text-gray-500">{{ _("subscription.free") }}</p>
{% endif %}
{% if period_start %}
<p class="text-xs text-gray-400 mt-2">
<i class="fas fa-calendar-alt mr-1" aria-hidden="true"></i>
Since {{ period_start.strftime('%b') }} {{ period_start.day }}, {{ period_start.year }}
{{ _("subscription.since") }} {{ period_start.strftime('%b') }} {{ period_start.day }}, {{ period_start.year }}
</p>
{% endif %}
</div>
@@ -87,13 +86,13 @@
<!-- 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>
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-widest mb-4">{{ _("subscription.usage_heading") }}</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>
<p class="text-xs text-gray-500 mt-1">{{ _("subscription.lifetime_files") }}</p>
{% if tier.lifetime_file_limit > 0 %}
<div class="mt-2">
{% set lifetime_pct = ((usage.lifetime / tier.lifetime_file_limit) * 100) | int %}
@@ -106,14 +105,14 @@
<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>
<p class="text-xs text-green-600 mt-1">{{ _("subscription.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>
<p class="text-xs text-gray-500 mt-1">{{ _("subscription.today_files") }}</p>
{% if tier.daily_upload_limit > 0 %}
<div class="mt-2">
{% set today_pct = ((usage.today / tier.daily_upload_limit) * 100) | int %}
@@ -123,17 +122,17 @@
{% 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>
<p class="text-xs text-gray-400 mt-1">{{ usage.today }} / {{ tier.daily_upload_limit }} {{ _("subscription.today_label") }}</p>
</div>
{% else %}
<p class="text-xs text-green-600 mt-1">Unlimited</p>
<p class="text-xs text-green-600 mt-1">{{ _("subscription.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>
<p class="text-xs text-gray-500 mt-1">{{ _("subscription.month_files") }}</p>
{% if tier.monthly_upload_limit > 0 %}
<div class="mt-2">
{% set month_pct = ((usage.month / tier.monthly_upload_limit) * 100) | int %}
@@ -143,10 +142,10 @@
{% 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>
<p class="text-xs text-gray-400 mt-1">{{ usage.month }} / {{ tier.monthly_upload_limit }} {{ _("subscription.this_month_label") }}</p>
</div>
{% else %}
<p class="text-xs text-green-600 mt-1">Unlimited</p>
<p class="text-xs text-green-600 mt-1">{{ _("subscription.unlimited") }}</p>
{% endif %}
</div>
@@ -160,7 +159,7 @@
<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
{{ _("subscription.features_heading") }}
</h2>
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-gray-600">
{% for feature in tier.features %}
@@ -176,7 +175,7 @@
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<i class="fas fa-layer-group text-indigo-500" aria-hidden="true"></i>
Available Plans
{{ _("subscription.available_plans_heading") }}
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{% for t in all_tiers %}
@@ -192,11 +191,11 @@
{% if is_current %}
<span class="absolute top-3 right-3 text-xs bg-indigo-100 text-indigo-700 font-semibold rounded-full px-2 py-0.5">
Current
{{ _("subscription.current_badge") }}
</span>
{% elif is_pending %}
<span class="absolute top-3 right-3 text-xs bg-amber-100 text-amber-700 font-semibold rounded-full px-2 py-0.5">
Pending
{{ _("subscription.pending_badge") }}
</span>
{% elif t.badge %}
<span class="absolute top-3 right-3 text-xs bg-indigo-100 text-indigo-700 font-semibold rounded-full px-2 py-0.5">
@@ -208,9 +207,9 @@
<p class="font-bold text-gray-900 pr-16">{{ t.name }}</p>
<p class="text-gray-500 text-xs mt-1 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>
<p class="text-xl font-extrabold text-gray-900">${{ t.price_monthly }}<span class="text-sm font-normal text-gray-400">{{ _("subscription.per_mo") }}</span></p>
{% else %}
<p class="text-xl font-extrabold text-gray-500">Free</p>
<p class="text-xl font-extrabold text-gray-500">{{ _("subscription.free") }}</p>
{% endif %}
<ul class="mt-3 space-y-1 text-xs text-gray-600">
{% for feature in t.features[:3] %}
@@ -220,7 +219,7 @@
</li>
{% endfor %}
{% if t.features | length > 3 %}
<li class="text-indigo-500 font-medium">+ {{ (t.features | length) - 3 }} more</li>
<li class="text-indigo-500 font-medium">+ {{ (t.features | length) - 3 }} {{ _("subscription.more_features_label") }}</li>
{% endif %}
</ul>
</div>
@@ -228,14 +227,14 @@
<div class="mt-4">
{% if is_current and not pending_tier_id %}
<span class="block text-center py-2 px-4 rounded-lg bg-gray-100 text-gray-400 text-sm font-semibold cursor-default">
Your current plan
{{ _("subscription.current_plan_cta") }}
</span>
{% elif is_current and pending_tier_id %}
<!-- Keep current plan (cancel pending downgrade) -->
<button type="button"
onclick="changePlan('{{ t.id }}')"
class="block w-full text-center py-2 px-4 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold transition">
Keep {{ t.name }}
{{ _("subscription.keep_plan_prefix") }} {{ t.name }}
</button>
{% elif is_upgrade %}
{% if t.id == 'business' %}
@@ -247,21 +246,21 @@
<button type="button"
onclick="changePlan('{{ t.id }}')"
class="block w-full text-center py-2 px-4 rounded-lg bg-green-600 hover:bg-green-700 text-white text-sm font-semibold transition">
Upgrade to {{ t.name }}
{{ _("subscription.upgrade_to_prefix") }} {{ t.name }}
</button>
{% endif %}
{% elif is_pending %}
<button type="button"
onclick="cancelPendingChange()"
class="block w-full text-center py-2 px-4 rounded-lg bg-amber-100 hover:bg-amber-200 text-amber-800 text-sm font-semibold transition">
Cancel scheduled change
{{ _("subscription.cancel_scheduled") }}
</button>
{% else %}
<!-- Downgrade -->
<button type="button"
onclick="changePlan('{{ t.id }}')"
class="block w-full text-center py-2 px-4 rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-700 text-sm font-semibold transition">
Downgrade to {{ t.name }}
{{ _("subscription.downgrade_to_prefix") }} {{ t.name }}
</button>
{% endif %}
</div>
@@ -270,7 +269,7 @@
</div>
<p class="text-xs text-gray-400 mt-3">
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
Upgrades take effect immediately. Downgrades are scheduled for the end of your current billing period.
{{ _("subscription.upgrade_info") }}
</p>
</div>
@@ -279,7 +278,7 @@
<!-- 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
<i class="fas fa-arrow-left text-xs" aria-hidden="true"></i> {{ _("subscription.back_to_dashboard") }}
</a>
</div>