i18n: translate 6 HTML Jinja2 templates with _() calls
Replace all hardcoded English visible text with {{ _("key") }} calls in:
- queue_dashboard.html: headings, stat cards, table headers, empty states
- api_tokens.html: form labels, table headers, status badges, aria-labels
- similarity_dashboard.html: headings, stat labels, controls, empty states
- backup.html: buttons, config labels, table, retention section, confirm dialog
- shared_links.html: form labels/options, table headers, status badges
- duplicates.html: tabs, role tags, near-dup form, explanation bullets
Follows i18n rules: placeholder/aria-label/title attrs translated,
JS strings in <script> left as-is, Alpine.js bindings use Jinja2
server-side evaluation pattern for dynamic translated strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,32 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}API Tokens – DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("api_tokens.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="apiTokens()" x-init="loadTokens()" class="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<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-key text-yellow-500" aria-hidden="true"></i>
|
||||
API Tokens
|
||||
{{ _("api_tokens.heading") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400 text-sm leading-relaxed max-w-2xl">
|
||||
Create personal API tokens to interact with the DocuElevate API programmatically.
|
||||
Use tokens for webhook uploads, CI/CD pipelines, or any script that needs to upload
|
||||
or retrieve documents.
|
||||
{{ _("api_tokens.intro") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Create token section -->
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6" aria-labelledby="create-token-heading">
|
||||
<h2 id="create-token-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Create New Token</h2>
|
||||
<h2 id="create-token-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ _("api_tokens.create_heading") }}</h2>
|
||||
<form @submit.prevent="createToken()" class="flex flex-col sm:flex-row gap-3">
|
||||
<div class="flex-1">
|
||||
<label for="token-name" class="sr-only">Token name</label>
|
||||
<label for="token-name" class="sr-only">{{ _("api_tokens.token_name_label") }}</label>
|
||||
<input
|
||||
id="token-name"
|
||||
type="text"
|
||||
x-model="newTokenName"
|
||||
placeholder="e.g. CI Pipeline, Webhook Upload, My Script"
|
||||
placeholder="{{ _('api_tokens.token_name_placeholder') }}"
|
||||
required
|
||||
minlength="1"
|
||||
maxlength="255"
|
||||
@@ -44,7 +42,7 @@
|
||||
style="min-height:40px; min-width:44px;"
|
||||
>
|
||||
<i class="fas fa-plus mr-2" aria-hidden="true"></i>
|
||||
<span x-text="creating ? 'Creating…' : 'Create Token'"></span>
|
||||
<span x-text="creating ? '{{ _('api_tokens.creating') }}' : '{{ _('api_tokens.create_token') }}'"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -54,9 +52,9 @@
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mt-0.5 text-lg" aria-hidden="true"></i>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-green-800 dark:text-green-200 text-sm">Token created successfully!</p>
|
||||
<p class="font-semibold text-green-800 dark:text-green-200 text-sm">{{ _("api_tokens.token_created") }}</p>
|
||||
<p class="text-green-700 dark:text-green-300 text-xs mt-1">
|
||||
Copy this token now — it will <strong>not be shown again</strong>.
|
||||
{{ _("api_tokens.copy_warning_1") }} <strong>{{ _("api_tokens.copy_warning_2") }}</strong>.
|
||||
</p>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<code
|
||||
@@ -72,10 +70,10 @@
|
||||
hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500
|
||||
transition-colors"
|
||||
style="min-height:40px; min-width:44px;"
|
||||
:aria-label="copied ? 'Copied!' : 'Copy token to clipboard'"
|
||||
:aria-label="copied ? '{{ _('common.copied') }}' : '{{ _('api_tokens.copy_to_clipboard') }}'"
|
||||
>
|
||||
<i :class="copied ? 'fas fa-check text-green-600' : 'fas fa-copy'" aria-hidden="true"></i>
|
||||
<span class="ml-1 hidden sm:inline" x-text="copied ? 'Copied!' : 'Copy'"></span>
|
||||
<span class="ml-1 hidden sm:inline" x-text="copied ? '{{ _('common.copied') }}' : '{{ _('common.copy') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,11 +86,11 @@
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6" aria-labelledby="usage-heading">
|
||||
<h2 id="usage-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
|
||||
<i class="fas fa-code text-blue-500 mr-2" aria-hidden="true"></i>
|
||||
Usage Example
|
||||
{{ _("api_tokens.usage_heading") }}
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mb-3">
|
||||
Use your API token in the <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded text-xs">Authorization</code>
|
||||
header with any API request:
|
||||
{{ _("api_tokens.usage_intro_pre") }} <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded text-xs">Authorization</code>
|
||||
{{ _("api_tokens.usage_intro_post") }}
|
||||
</p>
|
||||
<div class="relative">
|
||||
<pre class="bg-gray-900 text-green-400 rounded-lg p-4 text-sm overflow-x-auto font-mono leading-relaxed"><code>curl -X POST "<span x-text="baseUrl"></span>/api/files/ui-upload" \
|
||||
@@ -104,7 +102,7 @@
|
||||
class="absolute top-2 right-2 px-2 py-1 bg-gray-700 text-gray-300 rounded text-xs hover:bg-gray-600
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors"
|
||||
style="min-height:30px; min-width:30px;"
|
||||
aria-label="Copy upload example to clipboard"
|
||||
aria-label="{{ _('api_tokens.copy_upload_example') }}"
|
||||
>
|
||||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||||
</button>
|
||||
@@ -114,14 +112,14 @@
|
||||
<!-- Tokens list -->
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden" aria-labelledby="tokens-heading">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 id="tokens-heading" class="text-lg font-semibold text-gray-900 dark:text-white">Your Tokens</h2>
|
||||
<h2 id="tokens-heading" class="text-lg font-semibold text-gray-900 dark:text-white">{{ _("api_tokens.your_tokens") }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<template x-if="loading">
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-spinner fa-spin text-2xl mb-2" aria-hidden="true"></i>
|
||||
<p class="text-sm">Loading tokens…</p>
|
||||
<p class="text-sm">{{ _("api_tokens.loading_tokens") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -129,24 +127,24 @@
|
||||
<template x-if="!loading && tokens.length === 0">
|
||||
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-key text-4xl mb-3 text-gray-300 dark:text-gray-600" aria-hidden="true"></i>
|
||||
<p class="font-medium">No API tokens yet</p>
|
||||
<p class="text-sm mt-1">Create your first token above to get started.</p>
|
||||
<p class="font-medium">{{ _("api_tokens.no_tokens_heading") }}</p>
|
||||
<p class="text-sm mt-1">{{ _("api_tokens.no_tokens_help") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Tokens table -->
|
||||
<template x-if="!loading && tokens.length > 0">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm" aria-label="API Tokens">
|
||||
<table class="w-full text-sm" aria-label="{{ _('api_tokens.table_aria') }}">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-750 text-left">
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Name</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Token Prefix</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Created</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Last Used</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Last IP</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">Status</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider sr-only">Actions</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_name") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_prefix") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_created") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_last_used") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("api_tokens.col_last_ip") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider">{{ _("common.status") }}</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 uppercase text-xs tracking-wider sr-only">{{ _("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@@ -159,7 +157,7 @@
|
||||
<code class="bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded text-xs font-mono" x-text="token.token_prefix + '…'"></code>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500 dark:text-gray-400" x-text="formatDate(token.created_at)"></td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500 dark:text-gray-400" x-text="token.last_used_at ? formatDate(token.last_used_at) : 'Never'"></td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500 dark:text-gray-400" x-text="token.last_used_at ? formatDate(token.last_used_at) : '{{ _('api_tokens.never') }}'"></td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-500 dark:text-gray-400">
|
||||
<code x-show="token.last_used_ip" class="bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded text-xs font-mono" x-text="token.last_used_ip"></code>
|
||||
<span x-show="!token.last_used_ip" class="text-gray-400">—</span>
|
||||
@@ -168,7 +166,7 @@
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
||||
:class="token.is_active ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'"
|
||||
x-text="token.is_active ? 'Active' : 'Revoked'"
|
||||
x-text="token.is_active ? '{{ _('api_tokens.status_active') }}' : '{{ _('api_tokens.status_revoked') }}'"
|
||||
></span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
@@ -181,10 +179,10 @@
|
||||
dark:text-red-400 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md
|
||||
focus:outline-none focus:ring-2 focus:ring-red-500 disabled:opacity-50 transition-colors"
|
||||
style="min-height:36px; min-width:44px;"
|
||||
:aria-label="'Revoke token ' + token.name"
|
||||
:aria-label="'{{ _('api_tokens.revoke_prefix') }} ' + token.name"
|
||||
>
|
||||
<i :class="revoking === token.id ? 'fas fa-spinner fa-spin' : 'fas fa-trash-alt'" class="mr-1" aria-hidden="true"></i>
|
||||
Revoke
|
||||
{{ _("api_tokens.revoke") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Backup Management{% endblock %}
|
||||
{% block title %}{{ _("backup.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8" x-data="backupDashboard()">
|
||||
@@ -9,10 +9,10 @@
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">
|
||||
<i class="fas fa-database mr-2 text-blue-600" aria-hidden="true"></i>
|
||||
Backup Management
|
||||
{{ _("backup.heading") }}
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Database backups are created automatically: hourly (4 days), daily (3 weeks), weekly (13 weeks).
|
||||
{{ _("backup.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
@@ -21,26 +21,26 @@
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-clock mr-1 text-blue-500" aria-hidden="true"></i> Backup Now (Hourly)
|
||||
<i class="fas fa-clock mr-1 text-blue-500" aria-hidden="true"></i> {{ _("backup.trigger_hourly") }}
|
||||
</button>
|
||||
<button @click="triggerBackup('daily')"
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-calendar-day mr-1 text-green-500" aria-hidden="true"></i> Backup Now (Daily)
|
||||
<i class="fas fa-calendar-day mr-1 text-green-500" aria-hidden="true"></i> {{ _("backup.trigger_daily") }}
|
||||
</button>
|
||||
<button @click="triggerBackup('weekly')"
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-calendar-week mr-1 text-purple-500" aria-hidden="true"></i> Backup Now (Weekly)
|
||||
<i class="fas fa-calendar-week mr-1 text-purple-500" aria-hidden="true"></i> {{ _("backup.trigger_weekly") }}
|
||||
</button>
|
||||
<button @click="runCleanup()"
|
||||
:disabled="triggering"
|
||||
type="button"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 min-h-[44px]"
|
||||
title="Run retention cleanup now">
|
||||
<i class="fas fa-broom mr-1 text-orange-400" aria-hidden="true"></i> Clean Up
|
||||
title="{{ _('backup.cleanup_title') }}">
|
||||
<i class="fas fa-broom mr-1 text-orange-400" aria-hidden="true"></i> {{ _("backup.clean_up") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,18 +60,18 @@
|
||||
role="dialog" aria-modal="true" :aria-labelledby="'confirmTitle'">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-sm w-full p-6"
|
||||
@keydown.escape.window="confirmOpen = false">
|
||||
<h2 id="confirmTitle" class="text-lg font-semibold text-gray-900 mb-2">Confirm action</h2>
|
||||
<h2 id="confirmTitle" class="text-lg font-semibold text-gray-900 mb-2">{{ _("backup.confirm_title") }}</h2>
|
||||
<p class="text-sm text-gray-600 mb-4" x-text="confirmMsg"></p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button @click="confirmOpen = false; confirmResolve(false)"
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 min-h-[44px]">
|
||||
Cancel
|
||||
{{ _("common.cancel") }}
|
||||
</button>
|
||||
<button @click="confirmOpen = false; confirmResolve(true)"
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 min-h-[44px]">
|
||||
Confirm
|
||||
{{ _("backup.confirm_btn") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,36 +81,36 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<!-- Backup enabled -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Auto-backup</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_auto_backup") }}</p>
|
||||
<p class="mt-1 text-lg font-semibold {% if backup_enabled %}text-green-600{% else %}text-red-600{% endif %}">
|
||||
{% if backup_enabled %}<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> Enabled
|
||||
{% else %}<i class="fas fa-times-circle mr-1" aria-hidden="true"></i> Disabled{% endif %}
|
||||
{% if backup_enabled %}<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> {{ _("common.enabled") }}
|
||||
{% else %}<i class="fas fa-times-circle mr-1" aria-hidden="true"></i> {{ _("common.disabled") }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Remote destination -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Remote destination</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_remote_dest") }}</p>
|
||||
<p class="mt-1 text-lg font-semibold text-gray-800">
|
||||
{% if backup_remote_destination %}
|
||||
<i class="fas fa-cloud-upload-alt mr-1 text-blue-500" aria-hidden="true"></i>
|
||||
{{ backup_remote_destination }}
|
||||
{% else %}
|
||||
<span class="text-gray-400"><i class="fas fa-hdd mr-1" aria-hidden="true"></i> Local only</span>
|
||||
<span class="text-gray-400"><i class="fas fa-hdd mr-1" aria-hidden="true"></i> {{ _("backup.local_only") }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Retention hourly -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Retention</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_retention") }}</p>
|
||||
<p class="mt-1 text-sm text-gray-700">
|
||||
<span class="font-semibold">{{ backup_retain_hourly }}</span> hourly ·
|
||||
<span class="font-semibold">{{ backup_retain_daily }}</span> daily ·
|
||||
<span class="font-semibold">{{ backup_retain_weekly }}</span> weekly
|
||||
<span class="font-semibold">{{ backup_retain_hourly }}</span> {{ _("backup.type_hourly") }} ·
|
||||
<span class="font-semibold">{{ backup_retain_daily }}</span> {{ _("backup.type_daily") }} ·
|
||||
<span class="font-semibold">{{ backup_retain_weekly }}</span> {{ _("backup.type_weekly") }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Total backup size -->
|
||||
<div class="bg-white shadow rounded-lg p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Total local size</p>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">{{ _("backup.config_total_size") }}</p>
|
||||
<p class="mt-1 text-lg font-semibold text-gray-800" id="totalSize">
|
||||
{{ (total_size / 1048576) | round(2) }} MB
|
||||
</p>
|
||||
@@ -120,17 +120,17 @@
|
||||
<!-- Upload restore section -->
|
||||
<div class="bg-white shadow rounded-lg p-6 mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-1">
|
||||
<i class="fas fa-upload mr-2 text-yellow-500" aria-hidden="true"></i>Restore from File
|
||||
<i class="fas fa-upload mr-2 text-yellow-500" aria-hidden="true"></i>{{ _("backup.restore_heading") }}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Upload a <code>.db.gz</code> backup archive to restore the database.
|
||||
<strong class="text-red-600">This will overwrite all current data.</strong>
|
||||
{{ _("backup.restore_desc_pre") }} <code>.db.gz</code> {{ _("backup.restore_desc_mid") }}
|
||||
<strong class="text-red-600">{{ _("backup.restore_desc_warning") }}</strong>
|
||||
</p>
|
||||
<form id="restoreForm" @submit.prevent="submitRestore" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="flex flex-col sm:flex-row gap-3 items-start sm:items-end">
|
||||
<div>
|
||||
<label for="restoreFile" class="block text-sm font-medium text-gray-700 mb-1">Backup archive (.db.gz)</label>
|
||||
<label for="restoreFile" class="block text-sm font-medium text-gray-700 mb-1">{{ _("backup.restore_file_label") }}</label>
|
||||
<input type="file" id="restoreFile" name="file" accept=".gz"
|
||||
required
|
||||
class="block w-full text-sm text-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||
@@ -139,7 +139,7 @@
|
||||
:disabled="restoring"
|
||||
class="inline-flex items-center px-4 py-2 bg-yellow-500 hover:bg-yellow-600 text-white text-sm font-medium rounded-md disabled:opacity-50 min-h-[44px]">
|
||||
<i class="fas fa-undo mr-2" aria-hidden="true"></i>
|
||||
<span x-text="restoring ? 'Restoring…' : 'Restore'"></span>
|
||||
<span x-text="restoring ? '{{ _('backup.restoring') }}' : '{{ _('backup.restore_btn') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -149,30 +149,30 @@
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-list mr-2 text-gray-500" aria-hidden="true"></i>Backup Archives
|
||||
<span class="ml-2 text-sm font-normal text-gray-400">({{ records | length }} records)</span>
|
||||
<i class="fas fa-list mr-2 text-gray-500" aria-hidden="true"></i>{{ _("backup.archives_heading") }}
|
||||
<span class="ml-2 text-sm font-normal text-gray-400">({{ records | length }} {{ _("backup.records_label") }})</span>
|
||||
</h2>
|
||||
<!-- Tier filter -->
|
||||
<div class="flex gap-2 text-sm">
|
||||
<button @click="filterType = ''" :class="filterType === '' ? 'bg-gray-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">All</button>
|
||||
<button @click="filterType = 'hourly'" :class="filterType === 'hourly' ? 'bg-blue-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">Hourly</button>
|
||||
<button @click="filterType = 'daily'" :class="filterType === 'daily' ? 'bg-green-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">Daily</button>
|
||||
<button @click="filterType = 'weekly'" :class="filterType === 'weekly' ? 'bg-purple-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">Weekly</button>
|
||||
<button @click="filterType = ''" :class="filterType === '' ? 'bg-gray-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("common.all") }}</button>
|
||||
<button @click="filterType = 'hourly'" :class="filterType === 'hourly' ? 'bg-blue-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("backup.type_hourly") }}</button>
|
||||
<button @click="filterType = 'daily'" :class="filterType === 'daily' ? 'bg-green-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("backup.type_daily") }}</button>
|
||||
<button @click="filterType = 'weekly'" :class="filterType === 'weekly' ? 'bg-purple-200 font-semibold' : 'hover:bg-gray-100'" class="px-2 py-1 rounded">{{ _("backup.type_weekly") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if records %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200" aria-label="Backup archives">
|
||||
<table class="min-w-full divide-y divide-gray-200" aria-label="{{ _('backup.table_aria') }}">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Filename</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Size</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Storage</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_filename") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_type") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_created") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_size") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("common.status") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("backup.col_storage") }}</th>
|
||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ _("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@@ -186,15 +186,15 @@
|
||||
<td class="px-4 py-3 text-sm">
|
||||
{% if r.backup_type == 'hourly' %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<i class="fas fa-clock mr-1" aria-hidden="true"></i> hourly
|
||||
<i class="fas fa-clock mr-1" aria-hidden="true"></i> {{ _("backup.type_hourly") }}
|
||||
</span>
|
||||
{% elif r.backup_type == 'daily' %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
||||
<i class="fas fa-calendar-day mr-1" aria-hidden="true"></i> daily
|
||||
<i class="fas fa-calendar-day mr-1" aria-hidden="true"></i> {{ _("backup.type_daily") }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
|
||||
<i class="fas fa-calendar-week mr-1" aria-hidden="true"></i> weekly
|
||||
<i class="fas fa-calendar-week mr-1" aria-hidden="true"></i> {{ _("backup.type_weekly") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -209,7 +209,7 @@
|
||||
<td class="px-4 py-3 text-sm">
|
||||
{% if r.status == 'ok' %}
|
||||
<span class="inline-flex items-center text-green-700">
|
||||
<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> ok
|
||||
<i class="fas fa-check-circle mr-1" aria-hidden="true"></i> {{ _("backup.status_ok") }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center text-red-600">
|
||||
@@ -220,7 +220,7 @@
|
||||
<td class="px-4 py-3 text-sm text-gray-600">
|
||||
{% if r.local_path %}
|
||||
<span class="inline-flex items-center" title="{{ r.local_path }}">
|
||||
<i class="fas fa-hdd mr-1 text-gray-400" aria-hidden="true"></i> local
|
||||
<i class="fas fa-hdd mr-1 text-gray-400" aria-hidden="true"></i> {{ _("backup.storage_local") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if r.remote_destination %}
|
||||
@@ -236,15 +236,15 @@
|
||||
{% if r.local_path %}
|
||||
<a href="/api/admin/backup/{{ r.id }}/download"
|
||||
class="inline-flex items-center px-2 py-1 text-xs border border-gray-300 rounded hover:bg-gray-50 text-gray-700 min-h-[32px]"
|
||||
aria-label="Download backup {{ r.filename }}">
|
||||
<i class="fas fa-download mr-1" aria-hidden="true"></i> Download
|
||||
aria-label="{{ _('common.download') }} {{ r.filename }}">
|
||||
<i class="fas fa-download mr-1" aria-hidden="true"></i> {{ _("common.download") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button @click="deleteBackup({{ r.id }}, '{{ r.filename }}')"
|
||||
type="button"
|
||||
class="inline-flex items-center px-2 py-1 text-xs border border-red-300 rounded hover:bg-red-50 text-red-600 ml-1 min-h-[32px]"
|
||||
aria-label="Delete backup {{ r.filename }}">
|
||||
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> Delete
|
||||
aria-label="{{ _('common.delete') }} {{ r.filename }}">
|
||||
<i class="fas fa-trash-alt mr-1" aria-hidden="true"></i> {{ _("common.delete") }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -255,8 +255,8 @@
|
||||
{% else %}
|
||||
<div class="px-6 py-12 text-center text-gray-400">
|
||||
<i class="fas fa-database text-4xl mb-3" aria-hidden="true"></i>
|
||||
<p class="text-lg font-medium">No backups yet.</p>
|
||||
<p class="text-sm">Click <em>Backup Now</em> to create your first backup.</p>
|
||||
<p class="text-lg font-medium">{{ _("backup.no_backups") }}</p>
|
||||
<p class="text-sm">{{ _("backup.no_backups_hint_pre") }}<em>{{ _("backup.backup_now") }}</em>{{ _("backup.no_backups_hint_post") }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -264,16 +264,15 @@
|
||||
<!-- Retention explanation -->
|
||||
<div class="mt-8 bg-gray-50 border border-gray-200 rounded-lg p-5">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">
|
||||
<i class="fas fa-info-circle mr-1 text-blue-400" aria-hidden="true"></i> Retention policy
|
||||
<i class="fas fa-info-circle mr-1 text-blue-400" aria-hidden="true"></i> {{ _("backup.retention_heading") }}
|
||||
</h3>
|
||||
<ul class="text-sm text-gray-600 space-y-1 list-disc list-inside">
|
||||
<li><strong>Hourly</strong> – kept for 4 days ({{ backup_retain_hourly }} snapshots)</li>
|
||||
<li><strong>Daily</strong> – kept for 3 weeks ({{ backup_retain_daily }} snapshots)</li>
|
||||
<li><strong>Weekly</strong> – kept for ~3 months ({{ backup_retain_weekly }} snapshots)</li>
|
||||
<li><strong>{{ _("backup.type_hourly") }}</strong> {{ _("backup.retention_hourly_detail") }} ({{ backup_retain_hourly }} {{ _("backup.snapshots") }})</li>
|
||||
<li><strong>{{ _("backup.type_daily") }}</strong> {{ _("backup.retention_daily_detail") }} ({{ backup_retain_daily }} {{ _("backup.snapshots") }})</li>
|
||||
<li><strong>{{ _("backup.type_weekly") }}</strong> {{ _("backup.retention_weekly_detail") }} ({{ backup_retain_weekly }} {{ _("backup.snapshots") }})</li>
|
||||
</ul>
|
||||
<p class="text-xs text-gray-400 mt-2">
|
||||
Backups beyond these limits are automatically pruned after each new backup is created.
|
||||
Use the <em>Clean Up</em> button to apply retention manually.
|
||||
{{ _("backup.retention_note_pre") }}<em>{{ _("backup.clean_up") }}</em>{{ _("backup.retention_note_post") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Duplicate Documents - DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("duplicates.page_title") }}{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
@@ -88,22 +88,20 @@
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="dup-container px-4 py-8">
|
||||
<h1 class="text-2xl font-bold mb-2">Duplicate Documents</h1>
|
||||
<h1 class="text-2xl font-bold mb-2">{{ _("duplicates.heading") }}</h1>
|
||||
<p class="text-gray-500 mb-6 text-sm">
|
||||
Documents with identical content (same SHA-256 hash) are shown as
|
||||
<span class="dup-badge dup-badge-exact">exact duplicates</span>.
|
||||
Use the <strong>Near-Duplicate Finder</strong> tab to detect documents with the
|
||||
same scanned content but different hashes — for example, a document scanned
|
||||
twice.
|
||||
{{ _("duplicates.subtitle_1") }}
|
||||
<span class="dup-badge dup-badge-exact">{{ _("duplicates.exact_duplicates_badge") }}</span>.
|
||||
{{ _("duplicates.subtitle_2_pre") }} <strong>{{ _("duplicates.tab_near") }}</strong> {{ _("duplicates.subtitle_2_post") }}
|
||||
</p>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs" role="tablist" aria-label="Duplicate detection tabs">
|
||||
<div class="tabs" role="tablist" aria-label="{{ _('duplicates.tabs_aria') }}">
|
||||
<button class="tab-btn active" role="tab" aria-selected="true"
|
||||
aria-controls="tab-exact" id="btn-exact"
|
||||
onclick="switchTab('exact')">
|
||||
<i class="fas fa-clone mr-1" aria-hidden="true"></i>
|
||||
Exact Duplicates
|
||||
{{ _("duplicates.tab_exact") }}
|
||||
{% if total_groups is defined %}
|
||||
<span class="ml-1 text-xs bg-red-100 text-red-700 font-bold px-1.5 rounded-full">
|
||||
{{ total_groups }}
|
||||
@@ -114,7 +112,7 @@
|
||||
aria-controls="tab-near" id="btn-near"
|
||||
onclick="switchTab('near')">
|
||||
<i class="fas fa-layer-group mr-1" aria-hidden="true"></i>
|
||||
Near-Duplicate Finder
|
||||
{{ _("duplicates.tab_near") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -123,12 +121,12 @@
|
||||
|
||||
{% if groups %}
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Found <strong>{{ total_groups }}</strong> duplicate group(s) with
|
||||
<strong>{{ total_duplicate_files }}</strong> duplicate file(s) total.
|
||||
{{ _("duplicates.found_prefix") }} <strong>{{ total_groups }}</strong> {{ _("duplicates.found_groups") }}
|
||||
<strong>{{ total_duplicate_files }}</strong> {{ _("duplicates.found_files") }}
|
||||
</p>
|
||||
|
||||
{% for group in groups %}
|
||||
<section class="dup-group" aria-label="Duplicate group {{ loop.index }}">
|
||||
<section class="dup-group" aria-label="{{ _('duplicates.group_aria') }} {{ loop.index }}">
|
||||
<div class="dup-group-header">
|
||||
<i class="fas fa-fingerprint text-gray-400" aria-hidden="true"></i>
|
||||
<span class="text-xs font-mono text-gray-500" title="SHA-256 hash">
|
||||
@@ -136,14 +134,14 @@
|
||||
</span>
|
||||
<span class="dup-badge dup-badge-exact">
|
||||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||||
{{ group.duplicate_count }} duplicate{{ 's' if group.duplicate_count != 1 else '' }}
|
||||
{{ group.duplicate_count }} {{ _("duplicates.dup_singular") if group.duplicate_count == 1 else _("duplicates.dup_plural") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Original file -->
|
||||
{% if group.original %}
|
||||
<div class="dup-file-row original">
|
||||
<span class="dup-role-tag tag-original">Original</span>
|
||||
<span class="dup-role-tag tag-original">{{ _("duplicates.role_original") }}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="dup-filename">
|
||||
<a href="/files/{{ group.original.id }}"
|
||||
@@ -164,8 +162,8 @@
|
||||
<div class="dup-actions">
|
||||
<a href="/files/{{ group.original.id }}"
|
||||
class="text-sm text-blue-600 hover:underline"
|
||||
aria-label="View original file {{ group.original.original_filename }}">
|
||||
<i class="fas fa-external-link-alt" aria-hidden="true"></i> View
|
||||
aria-label="{{ _('duplicates.view_original_aria') }} {{ group.original.original_filename }}">
|
||||
<i class="fas fa-external-link-alt" aria-hidden="true"></i> {{ _("common.view") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,7 +172,7 @@
|
||||
<!-- Duplicate files -->
|
||||
{% for dup in group.duplicates %}
|
||||
<div class="dup-file-row duplicate">
|
||||
<span class="dup-role-tag tag-duplicate">Duplicate</span>
|
||||
<span class="dup-role-tag tag-duplicate">{{ _("duplicates.role_duplicate") }}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="dup-filename">
|
||||
<a href="/files/{{ dup.id }}" class="hover:text-blue-600">
|
||||
@@ -194,8 +192,8 @@
|
||||
<div class="dup-actions">
|
||||
<a href="/files/{{ dup.id }}"
|
||||
class="text-sm text-blue-600 hover:underline"
|
||||
aria-label="View duplicate file {{ dup.original_filename }}">
|
||||
<i class="fas fa-external-link-alt" aria-hidden="true"></i> View
|
||||
aria-label="{{ _('duplicates.view_dup_aria') }} {{ dup.original_filename }}">
|
||||
<i class="fas fa-external-link-alt" aria-hidden="true"></i> {{ _("common.view") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,23 +203,23 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<nav class="pagination" aria-label="Pagination">
|
||||
<nav class="pagination" aria-label="{{ _('duplicates.pagination_aria') }}">
|
||||
<button class="page-btn" onclick="changePage({{ pagination.page - 1 }})"
|
||||
{% if pagination.page <= 1 %}disabled{% endif %}
|
||||
aria-label="Previous page">
|
||||
aria-label="{{ _('common.prev_page') }}">
|
||||
<i class="fas fa-chevron-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% for p in range(1, pagination.pages + 1) %}
|
||||
<button class="page-btn {% if p == pagination.page %}current{% endif %}"
|
||||
onclick="changePage({{ p }})"
|
||||
aria-label="Page {{ p }}"
|
||||
aria-label="{{ _('common.page') }} {{ p }}"
|
||||
{% if p == pagination.page %}aria-current="page"{% endif %}>
|
||||
{{ p }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
<button class="page-btn" onclick="changePage({{ pagination.page + 1 }})"
|
||||
{% if pagination.page >= pagination.pages %}disabled{% endif %}
|
||||
aria-label="Next page">
|
||||
aria-label="{{ _('common.next_page') }}">
|
||||
<i class="fas fa-chevron-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</nav>
|
||||
@@ -230,11 +228,9 @@
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-check-circle text-green-400" aria-hidden="true"></i>
|
||||
<p class="font-semibold text-lg text-gray-700">No exact duplicates found</p>
|
||||
<p class="font-semibold text-lg text-gray-700">{{ _("duplicates.no_exact_heading") }}</p>
|
||||
<p class="text-sm mt-1">
|
||||
Every file in the system has a unique SHA-256 hash.
|
||||
Use the <strong>Near-Duplicate Finder</strong> tab to check for
|
||||
re-scanned documents.
|
||||
{{ _("duplicates.no_exact_detail_pre") }} <strong>{{ _("duplicates.tab_near") }}</strong> {{ _("duplicates.no_exact_detail_post") }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -246,32 +242,29 @@
|
||||
<div class="near-dup-search">
|
||||
<h3>
|
||||
<i class="fas fa-search mr-1 text-yellow-500" aria-hidden="true"></i>
|
||||
Find Near-Duplicates for a Document
|
||||
{{ _("duplicates.near_dup_heading") }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 mb-4">
|
||||
Select a document to check whether any other files contain the same (or very
|
||||
similar) content — even if they were scanned at different times and have
|
||||
different SHA-256 hashes. Similarity is computed from OCR text embeddings;
|
||||
documents without extracted text cannot be compared.
|
||||
{{ _("duplicates.near_dup_desc") }}
|
||||
</p>
|
||||
|
||||
<form id="nearDupForm" onsubmit="findNearDups(event)">
|
||||
<div class="nd-form">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="ndFileId">File ID</label>
|
||||
<label for="ndFileId">{{ _("duplicates.file_id_label") }}</label>
|
||||
<input type="number" id="ndFileId" name="file_id" min="1"
|
||||
placeholder="e.g. 42" required
|
||||
placeholder="{{ _('duplicates.file_id_placeholder') }}" required
|
||||
style="min-height:44px;">
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="ndThreshold">Similarity threshold</label>
|
||||
<label for="ndThreshold">{{ _("duplicates.threshold_label") }}</label>
|
||||
<input type="number" id="ndThreshold" name="threshold"
|
||||
min="0" max="1" step="0.05"
|
||||
value="{{ near_duplicate_threshold }}"
|
||||
style="min-height:44px;">
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="ndLimit">Max results</label>
|
||||
<label for="ndLimit">{{ _("duplicates.max_results_label") }}</label>
|
||||
<select id="ndLimit" name="limit" style="min-height:44px;">
|
||||
<option value="5" selected>5</option>
|
||||
<option value="10">10</option>
|
||||
@@ -282,7 +275,7 @@
|
||||
class="bg-yellow-500 hover:bg-yellow-600 text-white font-bold
|
||||
px-5 rounded-lg transition-colors"
|
||||
style="min-height:44px;">
|
||||
<i class="fas fa-search mr-1" aria-hidden="true"></i> Find
|
||||
<i class="fas fa-search mr-1" aria-hidden="true"></i> {{ _("duplicates.find_btn") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -296,20 +289,14 @@
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm text-blue-800">
|
||||
<p class="font-semibold mb-1">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
How near-duplicate detection works
|
||||
{{ _("duplicates.how_it_works_heading") }}
|
||||
</p>
|
||||
<ul class="list-disc ml-5 space-y-1">
|
||||
<li>After OCR processes a document, its text is converted to a numeric
|
||||
<em>embedding vector</em> using an AI language model.</li>
|
||||
<li>Near-duplicate detection compares these vectors using <em>cosine
|
||||
similarity</em>: a score of 1.0 means identical content, 0.0 means
|
||||
completely different.</li>
|
||||
<li>A threshold of <strong>{{ near_duplicate_threshold }}</strong> means
|
||||
documents must share at least
|
||||
{{ (near_duplicate_threshold * 100)|round|int }}% semantic similarity
|
||||
to be flagged.</li>
|
||||
<li>Documents scanned twice (even with slight differences) will typically
|
||||
score <strong>> 0.90</strong>.</li>
|
||||
<li>{{ _("duplicates.how_step1_pre") }} <em>{{ _("duplicates.embedding_vector") }}</em> {{ _("duplicates.how_step1_post") }}</li>
|
||||
<li>{{ _("duplicates.how_step2_pre") }} <em>{{ _("duplicates.cosine_similarity") }}</em>{{ _("duplicates.how_step2_post") }}</li>
|
||||
<li>{{ _("duplicates.how_step3_pre") }} <strong>{{ near_duplicate_threshold }}</strong> {{ _("duplicates.how_step3_mid") }}
|
||||
{{ (near_duplicate_threshold * 100)|round|int }}% {{ _("duplicates.how_step3_post") }}</li>
|
||||
<li>{{ _("duplicates.how_step4") }} <strong>> 0.90</strong>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div><!-- /tab-near -->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DocuElevate - Login</title>
|
||||
<title>{{ _("app.name") }} - {{ _("auth.login_title") }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
||||
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
|
||||
@@ -12,10 +12,10 @@
|
||||
<body class="bg-gray-100 h-screen flex items-center justify-center">
|
||||
<main class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full" role="main">
|
||||
<div class="flex justify-center mb-6">
|
||||
<img src="/static/images/logo_writing.svg" alt="DocuElevate Logo" class="h-16">
|
||||
<img src="/static/images/logo_writing.svg" alt="{{ _('auth.logo_alt') }}" class="h-16">
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-bold text-center text-gray-800 mb-6">Welcome to DocuElevate</h1>
|
||||
<h1 class="text-2xl font-bold text-center text-gray-800 mb-6">{{ _("dashboard.welcome") }}</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6" role="alert">
|
||||
@@ -31,33 +31,33 @@
|
||||
|
||||
<!-- Local authentication form -->
|
||||
<div class="mb-8" id="local-auth">
|
||||
<h2 class="text-lg font-semibold mb-4 text-gray-700">Sign in with username</h2>
|
||||
<h2 class="text-lg font-semibold mb-4 text-gray-700">{{ _("auth.sign_in_with_username") }}</h2>
|
||||
<form method="POST" action="/auth" class="space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token | default('', true) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token | default(\'\', true) }}">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">Username or Email</label>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">{{ _("auth.username_or_email") }}</label>
|
||||
<input type="text" id="username" name="username" required
|
||||
autocomplete="username"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">{{ _("auth.password_label") }}</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Sign in
|
||||
{{ _("auth.sign_in") }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="mt-3 text-center space-x-3">
|
||||
<a href="/forgot-password" class="text-sm text-blue-600 hover:text-blue-500">
|
||||
Forgot password?
|
||||
{{ _("auth.forgot_password") }}
|
||||
</a>
|
||||
<span class="text-gray-300" aria-hidden="true">|</span>
|
||||
<a href="/forgot-username" class="text-sm text-blue-600 hover:text-blue-500">
|
||||
Forgot username?
|
||||
{{ _("auth.forgot_username") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,14 +69,14 @@
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-white text-gray-500">Or continue with</span>
|
||||
<span class="px-2 bg-white text-gray-500">{{ _("auth.or_continue_with") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-3">
|
||||
<a href="/oauth-login"
|
||||
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<span class="sr-only">Sign in with SSO</span>
|
||||
<span class="sr-only">{{ _("auth.sign_in_sso") }}</span>
|
||||
<i class="fas fa-lock mr-2" aria-hidden="true"></i>
|
||||
{{ oauth_provider_name }}
|
||||
</a>
|
||||
@@ -85,21 +85,21 @@
|
||||
|
||||
<div class="mt-8 text-center">
|
||||
<a href="/" class="text-sm font-medium text-blue-600 hover:text-blue-500">
|
||||
Return to Home
|
||||
{{ _("auth.return_home") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if allow_signup %}
|
||||
<div class="mt-4 text-center">
|
||||
<span class="text-sm text-gray-600">Don't have an account?</span>
|
||||
<span class="text-sm text-gray-600">{{ _("auth.no_account") }}</span>
|
||||
<a href="/signup" class="ml-1 text-sm font-medium text-indigo-600 hover:text-indigo-500">
|
||||
Create account
|
||||
{{ _("auth.create_account") }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
<div class="fixed bottom-4 text-center w-full text-xs text-gray-500">
|
||||
DocuElevate {{ app_version|default('', true) }}
|
||||
{{ _("app.name") }} {{ app_version|default(\'\', true) }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Queue Monitor — DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("queue.page_title") }}{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<script src="/static/js/common.js"></script>
|
||||
@@ -11,20 +11,20 @@
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold mb-2">
|
||||
<i class="fas fa-stream mr-2 text-blue-500" aria-hidden="true"></i>Queue Monitor
|
||||
<i class="fas fa-stream mr-2 text-blue-500" aria-hidden="true"></i>{{ _("queue.heading") }}
|
||||
</h1>
|
||||
<p class="text-gray-600">Real-time view of the document processing pipeline and Celery task queues.</p>
|
||||
<p class="text-gray-600">{{ _("queue.subtitle") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div id="queueLoading" class="text-center py-12">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-blue-400" aria-hidden="true"></i>
|
||||
<p class="mt-2 text-gray-500">Loading queue statistics…</p>
|
||||
<p class="mt-2 text-gray-500">{{ _("queue.loading_stats") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div id="queueError" class="hidden bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6" role="alert">
|
||||
<p><strong>Error:</strong> <span id="queueErrorMsg"></span></p>
|
||||
<p><strong>{{ _("common.error") }}:</strong> <span id="queueErrorMsg"></span></p>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard content (hidden until loaded) -->
|
||||
@@ -39,7 +39,7 @@
|
||||
<i class="fas fa-inbox text-yellow-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Queued Tasks</p>
|
||||
<p class="text-sm font-medium text-gray-500">{{ _("queue.queued_tasks") }}</p>
|
||||
<p id="totalQueued" class="text-2xl font-bold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
<i class="fas fa-cogs text-blue-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Active Tasks</p>
|
||||
<p class="text-sm font-medium text-gray-500">{{ _("queue.active_tasks") }}</p>
|
||||
<p id="activeTasks" class="text-2xl font-bold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
<i class="fas fa-file-alt text-indigo-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Files Processing</p>
|
||||
<p class="text-sm font-medium text-gray-500">{{ _("queue.files_processing") }}</p>
|
||||
<p id="filesProcessing" class="text-2xl font-bold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@
|
||||
<i class="fas fa-server text-green-600 text-xl" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Workers Online</p>
|
||||
<p class="text-sm font-medium text-gray-500">{{ _("queue.workers_online") }}</p>
|
||||
<p id="workersOnline" class="text-2xl font-bold text-gray-900">0</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,19 +89,19 @@
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-5 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-list-ol mr-2 text-gray-500" aria-hidden="true"></i>Redis Queues
|
||||
<i class="fas fa-list-ol mr-2 text-gray-500" aria-hidden="true"></i>{{ _("queue.redis_queues") }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b">
|
||||
<th class="pb-2">Queue</th>
|
||||
<th class="pb-2 text-right">Pending</th>
|
||||
<th class="pb-2">{{ _("queue.col_queue") }}</th>
|
||||
<th class="pb-2 text-right">{{ _("common.pending") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="queueRows">
|
||||
<tr><td colspan="2" class="py-3 text-gray-400 text-center">No data</td></tr>
|
||||
<tr><td colspan="2" class="py-3 text-gray-400 text-center">{{ _("queue.no_data") }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -111,19 +111,19 @@
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-5 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-database mr-2 text-gray-500" aria-hidden="true"></i>Processing Pipeline
|
||||
<i class="fas fa-database mr-2 text-gray-500" aria-hidden="true"></i>{{ _("queue.processing_pipeline") }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b">
|
||||
<th class="pb-2">State</th>
|
||||
<th class="pb-2 text-right">Files</th>
|
||||
<th class="pb-2">{{ _("queue.col_state") }}</th>
|
||||
<th class="pb-2 text-right">{{ _("queue.col_files") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dbSummaryRows">
|
||||
<tr><td colspan="2" class="py-3 text-gray-400 text-center">No data</td></tr>
|
||||
<tr><td colspan="2" class="py-3 text-gray-400 text-center">{{ _("queue.no_data") }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -134,7 +134,7 @@
|
||||
<div class="bg-white rounded-lg shadow mb-8">
|
||||
<div class="px-5 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-play-circle mr-2 text-blue-500" aria-hidden="true"></i>Active Tasks
|
||||
<i class="fas fa-play-circle mr-2 text-blue-500" aria-hidden="true"></i>{{ _("queue.active_tasks") }}
|
||||
<span id="activeCount" class="ml-1 text-sm font-normal text-gray-500">(0)</span>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -142,13 +142,13 @@
|
||||
<table class="w-full text-sm" id="activeTable">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b">
|
||||
<th class="pb-2">Task</th>
|
||||
<th class="pb-2">Arguments</th>
|
||||
<th class="pb-2">Task ID</th>
|
||||
<th class="pb-2">{{ _("queue.col_task") }}</th>
|
||||
<th class="pb-2">{{ _("queue.col_arguments") }}</th>
|
||||
<th class="pb-2">{{ _("queue.col_task_id") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="activeRows">
|
||||
<tr><td colspan="3" class="py-3 text-gray-400 text-center">No active tasks</td></tr>
|
||||
<tr><td colspan="3" class="py-3 text-gray-400 text-center">{{ _("queue.no_active_tasks") }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -158,20 +158,20 @@
|
||||
<div class="bg-white rounded-lg shadow mb-8">
|
||||
<div class="px-5 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-file-medical-alt mr-2 text-indigo-500" aria-hidden="true"></i>Recently Processing Files
|
||||
<i class="fas fa-file-medical-alt mr-2 text-indigo-500" aria-hidden="true"></i>{{ _("queue.recently_processing") }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-5 overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-gray-500 border-b">
|
||||
<th class="pb-2">File</th>
|
||||
<th class="pb-2">Current Step</th>
|
||||
<th class="pb-2 text-right">Actions</th>
|
||||
<th class="pb-2">{{ _("queue.col_file") }}</th>
|
||||
<th class="pb-2">{{ _("queue.col_current_step") }}</th>
|
||||
<th class="pb-2 text-right">{{ _("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentRows">
|
||||
<tr><td colspan="3" class="py-3 text-gray-400 text-center">No files currently processing</td></tr>
|
||||
<tr><td colspan="3" class="py-3 text-gray-400 text-center">{{ _("queue.no_files_processing") }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -180,8 +180,8 @@
|
||||
<!-- Auto-refresh info -->
|
||||
<div class="text-center text-gray-400 text-xs mb-4">
|
||||
<i class="fas fa-sync-alt mr-1" aria-hidden="true"></i>
|
||||
Auto-refreshes every <span id="refreshInterval">10</span> seconds —
|
||||
Last updated: <span id="lastUpdated">—</span>
|
||||
{{ _("queue.auto_refresh_1") }} <span id="refreshInterval">10</span> {{ _("queue.auto_refresh_2") }} —
|
||||
{{ _("queue.last_updated") }} <span id="lastUpdated">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Shared Links – DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("shared.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="sharedLinks()" x-init="init(); loadLinks()" class="container mx-auto px-4 py-8 max-w-5xl">
|
||||
<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-share-alt text-blue-500" aria-hidden="true"></i>
|
||||
Shared Links
|
||||
{{ _("shared.heading") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400 text-sm leading-relaxed max-w-2xl">
|
||||
Share documents with anyone via a time-limited or view-limited link.
|
||||
Recipients do not need a DocuElevate account. Links can be password-protected
|
||||
and revoked at any time.
|
||||
{{ _("shared.subtitle") }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Create link section -->
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6" aria-labelledby="create-link-heading">
|
||||
<h2 id="create-link-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Create New Shared Link</h2>
|
||||
<h2 id="create-link-heading" class="text-lg font-semibold text-gray-900 dark:text-white mb-4">{{ _("shared.create_heading") }}</h2>
|
||||
|
||||
<form @submit.prevent="createLink()" class="space-y-4">
|
||||
<!-- File ID -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 items-start sm:items-end">
|
||||
<div class="flex-1">
|
||||
<label for="file-id-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
File ID <span class="text-red-500" aria-hidden="true">*</span>
|
||||
{{ _("shared.file_id_label") }} <span class="text-red-500" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="file-id-input"
|
||||
type="number"
|
||||
x-model.number="newLink.file_id"
|
||||
placeholder="e.g. 42"
|
||||
placeholder="{{ _('shared.file_id_placeholder') }}"
|
||||
required
|
||||
min="1"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -39,20 +37,20 @@
|
||||
aria-required="true"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Find the file ID on the <a href="/files" class="underline hover:text-blue-600">Files</a> page or in the document detail URL.
|
||||
{{ _("shared.file_id_help_pre") }} <a href="/files" class="underline hover:text-blue-600">{{ _("shared.files_link") }}</a> {{ _("shared.file_id_help_post") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Label (optional) -->
|
||||
<div class="flex-1">
|
||||
<label for="link-label-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Label <span class="text-gray-400 font-normal">(optional)</span>
|
||||
{{ _("shared.label_label") }} <span class="text-gray-400 font-normal">({{ _("shared.optional") }})</span>
|
||||
</label>
|
||||
<input
|
||||
id="link-label-input"
|
||||
type="text"
|
||||
x-model="newLink.label"
|
||||
placeholder="e.g. Shared with Bob"
|
||||
placeholder="{{ _('shared.label_placeholder') }}"
|
||||
maxlength="255"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white text-sm"
|
||||
@@ -65,7 +63,7 @@
|
||||
<!-- Expiry -->
|
||||
<div class="flex-1">
|
||||
<label for="expires-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Expiry
|
||||
{{ _("shared.expiry_label") }}
|
||||
</label>
|
||||
<select
|
||||
id="expires-select"
|
||||
@@ -73,28 +71,28 @@
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white text-sm"
|
||||
>
|
||||
<option :value="null">Never</option>
|
||||
<option value="1">1 hour</option>
|
||||
<option value="6">6 hours</option>
|
||||
<option value="12">12 hours</option>
|
||||
<option value="24">24 hours (1 day)</option>
|
||||
<option value="72">3 days</option>
|
||||
<option value="168">7 days</option>
|
||||
<option value="336">14 days</option>
|
||||
<option value="720">30 days</option>
|
||||
<option :value="null">{{ _("shared.expiry_never") }}</option>
|
||||
<option value="1">{{ _("shared.expiry_1h") }}</option>
|
||||
<option value="6">{{ _("shared.expiry_6h") }}</option>
|
||||
<option value="12">{{ _("shared.expiry_12h") }}</option>
|
||||
<option value="24">{{ _("shared.expiry_24h") }}</option>
|
||||
<option value="72">{{ _("shared.expiry_3d") }}</option>
|
||||
<option value="168">{{ _("shared.expiry_7d") }}</option>
|
||||
<option value="336">{{ _("shared.expiry_14d") }}</option>
|
||||
<option value="720">{{ _("shared.expiry_30d") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Max views -->
|
||||
<div class="flex-1">
|
||||
<label for="max-views-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Max downloads <span class="text-gray-400 font-normal">(optional)</span>
|
||||
{{ _("shared.max_downloads_label") }} <span class="text-gray-400 font-normal">({{ _("shared.optional") }})</span>
|
||||
</label>
|
||||
<input
|
||||
id="max-views-input"
|
||||
type="number"
|
||||
x-model.number="newLink.max_views"
|
||||
placeholder="Unlimited"
|
||||
placeholder="{{ _('shared.unlimited_placeholder') }}"
|
||||
min="1"
|
||||
max="10000"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -105,13 +103,13 @@
|
||||
<!-- Password -->
|
||||
<div class="flex-1">
|
||||
<label for="link-password-input" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Password <span class="text-gray-400 font-normal">(optional)</span>
|
||||
{{ _("shared.password_label") }} <span class="text-gray-400 font-normal">({{ _("shared.optional") }})</span>
|
||||
</label>
|
||||
<input
|
||||
id="link-password-input"
|
||||
type="password"
|
||||
x-model="newLink.password"
|
||||
placeholder="Leave blank for no password"
|
||||
placeholder="{{ _('shared.password_placeholder') }}"
|
||||
maxlength="128"
|
||||
autocomplete="new-password"
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
|
||||
@@ -135,7 +133,7 @@
|
||||
style="min-height:40px; min-width:44px;"
|
||||
>
|
||||
<i class="fas fa-share-alt mr-2" aria-hidden="true"></i>
|
||||
<span x-text="creating ? 'Creating…' : 'Create Link'"></span>
|
||||
<span x-text="creating ? '{{ _('shared.creating') }}' : '{{ _('shared.create_btn') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -146,9 +144,9 @@
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mt-0.5 text-lg" aria-hidden="true"></i>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-green-800 dark:text-green-200 text-sm">Shared link created!</p>
|
||||
<p class="font-semibold text-green-800 dark:text-green-200 text-sm">{{ _("shared.link_created") }}</p>
|
||||
<p class="text-green-700 dark:text-green-300 text-xs mt-1">
|
||||
Copy and send this link to the recipient.
|
||||
{{ _("shared.link_created_help") }}
|
||||
</p>
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<code
|
||||
@@ -164,10 +162,10 @@
|
||||
hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
transition-colors"
|
||||
style="min-height:40px; min-width:44px;"
|
||||
:aria-label="copied ? 'Copied!' : 'Copy link to clipboard'"
|
||||
:aria-label="copied ? '{{ _('common.copied') }}' : '{{ _('shared.copy_link_aria') }}'"
|
||||
>
|
||||
<i :class="copied ? 'fas fa-check text-green-600' : 'fas fa-copy'" aria-hidden="true"></i>
|
||||
<span class="ml-1 hidden sm:inline" x-text="copied ? 'Copied!' : 'Copy'"></span>
|
||||
<span class="ml-1 hidden sm:inline" x-text="copied ? '{{ _('common.copied') }}' : '{{ _('common.copy') }}'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,28 +178,28 @@
|
||||
<section class="bg-white dark:bg-gray-800 shadow rounded-lg p-6" aria-labelledby="links-heading">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 id="links-heading" class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Your Shared Links
|
||||
{{ _("shared.your_links") }}
|
||||
<span
|
||||
x-show="links.length > 0"
|
||||
class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||
x-text="links.length"
|
||||
aria-label="number of links"
|
||||
aria-label="{{ _('shared.links_count_aria') }}"
|
||||
></span>
|
||||
</h2>
|
||||
<button
|
||||
@click="loadLinks()"
|
||||
type="button"
|
||||
class="text-sm text-blue-600 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
|
||||
aria-label="Refresh shared links list"
|
||||
aria-label="{{ _('shared.refresh_aria') }}"
|
||||
>
|
||||
<i class="fas fa-sync-alt mr-1" aria-hidden="true"></i>Refresh
|
||||
<i class="fas fa-sync-alt mr-1" aria-hidden="true"></i>{{ _("common.refresh") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<template x-if="loading">
|
||||
<p class="text-sm text-gray-400 dark:text-gray-500 py-4 text-center">
|
||||
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>Loading…
|
||||
<i class="fas fa-spinner fa-spin mr-2" aria-hidden="true"></i>{{ _("common.loading") }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -209,22 +207,22 @@
|
||||
<template x-if="!loading && links.length === 0">
|
||||
<div class="text-center py-10 text-gray-400 dark:text-gray-500">
|
||||
<i class="fas fa-share-alt text-3xl mb-3" aria-hidden="true"></i>
|
||||
<p class="text-sm">No shared links yet. Create one above to get started.</p>
|
||||
<p class="text-sm">{{ _("shared.no_links") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Links list -->
|
||||
<template x-if="!loading && links.length > 0">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm" aria-label="Shared links">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm" aria-label="{{ _('shared.table_aria') }}">
|
||||
<thead>
|
||||
<tr class="text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th scope="col" class="pb-3 pr-4">File / Label</th>
|
||||
<th scope="col" class="pb-3 pr-4">Link</th>
|
||||
<th scope="col" class="pb-3 pr-4">Expiry</th>
|
||||
<th scope="col" class="pb-3 pr-4">Views</th>
|
||||
<th scope="col" class="pb-3 pr-4">Status</th>
|
||||
<th scope="col" class="pb-3">Actions</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("shared.col_file_label") }}</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("shared.col_link") }}</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("shared.col_expiry") }}</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("shared.col_views") }}</th>
|
||||
<th scope="col" class="pb-3 pr-4">{{ _("common.status") }}</th>
|
||||
<th scope="col" class="pb-3">{{ _("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
@@ -235,7 +233,7 @@
|
||||
<p class="font-medium text-gray-800 dark:text-gray-200 truncate" x-text="link.original_filename || ('File #' + link.file_id)"></p>
|
||||
<p x-show="link.label" class="text-xs text-gray-500 dark:text-gray-400 truncate" x-text="link.label"></p>
|
||||
<p x-show="link.has_password" class="text-xs text-yellow-600 dark:text-yellow-400 mt-0.5">
|
||||
<i class="fas fa-lock text-xs" aria-hidden="true"></i> Password protected
|
||||
<i class="fas fa-lock text-xs" aria-hidden="true"></i> {{ _("shared.password_protected") }}
|
||||
</p>
|
||||
</td>
|
||||
<!-- Link -->
|
||||
@@ -246,8 +244,8 @@
|
||||
type="button"
|
||||
@click="copyUrl(link.share_url)"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded"
|
||||
aria-label="Copy link"
|
||||
title="Copy link"
|
||||
aria-label="{{ _('shared.copy_link_aria') }}"
|
||||
title="{{ _('shared.copy_link_title') }}"
|
||||
style="min-height:32px;min-width:32px;"
|
||||
>
|
||||
<i class="fas fa-copy text-xs" aria-hidden="true"></i>
|
||||
@@ -257,8 +255,8 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="p-1 text-gray-400 hover:text-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-500 rounded"
|
||||
aria-label="Open shared link"
|
||||
title="Open in new tab"
|
||||
aria-label="{{ _('shared.open_link_aria') }}"
|
||||
title="{{ _('shared.open_in_new_tab') }}"
|
||||
style="min-height:32px;min-width:32px;"
|
||||
>
|
||||
<i class="fas fa-external-link-alt text-xs" aria-hidden="true"></i>
|
||||
@@ -278,22 +276,22 @@
|
||||
<td class="py-3 pr-4 whitespace-nowrap">
|
||||
<template x-if="!link.is_active">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300">
|
||||
Revoked
|
||||
{{ _("shared.status_revoked") }}
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="link.is_active && isExpired(link)">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300">
|
||||
Expired
|
||||
{{ _("shared.status_expired") }}
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="link.is_active && isViewLimitReached(link)">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300">
|
||||
Limit reached
|
||||
{{ _("shared.status_limit_reached") }}
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="link.is_active && !isExpired(link) && !isViewLimitReached(link)">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
|
||||
Active
|
||||
{{ _("shared.status_active") }}
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
@@ -307,9 +305,9 @@
|
||||
border border-red-300 dark:border-red-700 rounded hover:bg-red-50 dark:hover:bg-red-900/20
|
||||
focus:outline-none focus:ring-2 focus:ring-red-500 transition-colors"
|
||||
style="min-height:32px;min-width:44px;"
|
||||
:aria-label="'Revoke shared link for ' + (link.original_filename || 'file')"
|
||||
:aria-label="'{{ _('shared.revoke_aria_prefix') }} ' + (link.original_filename || '{{ _('shared.revoke_aria_file') }}')"
|
||||
>
|
||||
<i class="fas fa-ban mr-1" aria-hidden="true"></i> Revoke
|
||||
<i class="fas fa-ban mr-1" aria-hidden="true"></i> {{ _("shared.revoke_btn") }}
|
||||
</button>
|
||||
<span x-show="!link.is_active" class="text-xs text-gray-400 dark:text-gray-500">—</span>
|
||||
</td>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Document Similarity - DocuElevate{% endblock %}
|
||||
{% block title %}{{ _("similarity.page_title") }}{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<style>
|
||||
@@ -97,45 +97,43 @@
|
||||
<main id="main-content" class="sim-container px-4 py-8">
|
||||
<h1 class="text-2xl font-bold mb-2">
|
||||
<i class="fas fa-project-diagram text-blue-500" aria-hidden="true"></i>
|
||||
Document Similarity
|
||||
{{ _("similarity.heading") }}
|
||||
</h1>
|
||||
<p class="text-gray-500 mb-6 text-sm">
|
||||
Pairs of documents with high semantic similarity, ranked by score.
|
||||
Embeddings are computed during document ingestion; a background task
|
||||
also backfills any files that were processed before this feature was enabled.
|
||||
{{ _("similarity.subtitle") }}
|
||||
</p>
|
||||
|
||||
<!-- Embedding coverage stats -->
|
||||
<div class="stats-bar">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ total_files }}</div>
|
||||
<div class="stat-label">Total Files</div>
|
||||
<div class="stat-label">{{ _("similarity.stat_total_files") }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: #2563eb;">{{ files_with_embedding }}</div>
|
||||
<div class="stat-label">With Embedding</div>
|
||||
<div class="stat-label">{{ _("similarity.stat_with_embedding") }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color: {% if files_missing_embedding > 0 %}#d97706{% else %}#059669{% endif %};">
|
||||
{{ files_missing_embedding }}
|
||||
</div>
|
||||
<div class="stat-label">Missing Embedding</div>
|
||||
<div class="stat-label">{{ _("similarity.stat_missing_embedding") }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-sm" style="word-break: break-all;">{{ embedding_model }}</div>
|
||||
<div class="stat-label">Embedding Model</div>
|
||||
<div class="stat-label">{{ _("similarity.stat_embedding_model") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if files_missing_embedding > 0 %}
|
||||
<div style="background: #fffff0; border: 1px solid #fefcbf; border-radius: 0.5rem; padding: 0.75rem 1rem; margin-bottom: 1.5rem; font-size: 0.85rem; color: #975a16;">
|
||||
<i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<strong>{{ files_missing_embedding }}</strong> file(s) have OCR text but no embedding yet.
|
||||
The background task will compute them automatically every 5 minutes, or you can
|
||||
<strong>{{ files_missing_embedding }}</strong> {{ _("similarity.files_missing_text") }}
|
||||
{{ _("similarity.backfill_auto") }}
|
||||
<button onclick="triggerBackfill()" id="backfill-btn"
|
||||
style="background: #d97706; color: white; border: none; padding: 0.2rem 0.6rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.8rem; min-height: 44px; min-width: 44px;"
|
||||
aria-label="Trigger embedding computation for all files missing embeddings">
|
||||
trigger it now
|
||||
aria-label="{{ _('similarity.trigger_aria') }}">
|
||||
{{ _("similarity.trigger_now") }}
|
||||
</button>.
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -144,12 +142,12 @@
|
||||
<div class="controls">
|
||||
<form id="pairsForm" onsubmit="loadPairs(event)" class="controls-form">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="pairThreshold">Min. similarity</label>
|
||||
<label for="pairThreshold">{{ _("similarity.min_similarity_label") }}</label>
|
||||
<input type="number" id="pairThreshold" min="0" max="1" step="0.05"
|
||||
value="{{ default_threshold }}" style="min-height:44px;">
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="pairLimit">Per page</label>
|
||||
<label for="pairLimit">{{ _("similarity.per_page_label") }}</label>
|
||||
<select id="pairLimit" style="min-height:44px;">
|
||||
<option value="25">25</option>
|
||||
<option value="50" selected>50</option>
|
||||
@@ -159,7 +157,7 @@
|
||||
<button type="submit"
|
||||
style="min-height:44px; background-color: #2563eb; color: white; font-weight: 700;
|
||||
padding: 0 1.25rem; border: none; border-radius: 0.5rem; cursor: pointer;">
|
||||
<i class="fas fa-search" aria-hidden="true"></i> Find Pairs
|
||||
<i class="fas fa-search" aria-hidden="true"></i> {{ _("similarity.find_pairs_btn") }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -167,23 +165,23 @@
|
||||
<!-- Results area -->
|
||||
<div id="pairs-loading" style="text-align: center; padding: 2rem; color: #718096;">
|
||||
<i class="fas fa-spinner fa-spin" aria-hidden="true" style="font-size: 1.5rem; margin-bottom: 0.5rem;"></i>
|
||||
<p>Scanning for similar document pairs…</p>
|
||||
<p>{{ _("similarity.scanning") }}</p>
|
||||
</div>
|
||||
<div id="pairs-content" style="display: none;" aria-live="polite"></div>
|
||||
<div id="pairs-empty" style="display: none;" class="empty-state">
|
||||
<i class="fas fa-check-circle text-green-400" aria-hidden="true"></i>
|
||||
<p class="font-semibold text-lg text-gray-700">No similar pairs found</p>
|
||||
<p class="font-semibold text-lg text-gray-700">{{ _("similarity.no_pairs_heading") }}</p>
|
||||
<p class="text-sm mt-1" id="pairs-empty-detail">
|
||||
No document pairs exceed the similarity threshold.
|
||||
{{ _("similarity.no_pairs_detail") }}
|
||||
</p>
|
||||
</div>
|
||||
<div id="pairs-error" style="display: none;" class="empty-state">
|
||||
<i class="fas fa-exclamation-triangle text-red-400" aria-hidden="true"></i>
|
||||
<p class="font-semibold text-lg text-gray-700" id="pairs-error-msg">Failed to load pairs.</p>
|
||||
<p class="font-semibold text-lg text-gray-700" id="pairs-error-msg">{{ _("similarity.load_error") }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<nav id="pairs-pagination" class="pagination" style="display: none;" aria-label="Similarity pairs pagination"></nav>
|
||||
<nav id="pairs-pagination" class="pagination" style="display: none;" aria-label="{{ _('similarity.pagination_aria') }}"></nav>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Upload Document{% endblock %}
|
||||
{% block title %}{{ _("upload.page_title") }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="flex flex-col items-center justify-center p-4 sm:p-8">
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-6 sm:mb-8">Upload Files</h1>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold mb-6 sm:mb-8">{{ _("upload.page_title") }}</h1>
|
||||
|
||||
<!-- File Upload Section -->
|
||||
<div class="w-full max-w-2xl mb-6 sm:mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Upload from Device</h2>
|
||||
<h2 class="text-xl font-semibold mb-4">{{ _("upload.section_device") }}</h2>
|
||||
<form action="/api/ui-upload" method="POST" enctype="multipart/form-data">
|
||||
<div
|
||||
id="dropZone"
|
||||
class="border-4 border-dashed border-gray-300 rounded-lg p-6 sm:p-8 bg-white text-center w-full"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="File upload area. Drag and drop files here, or press Enter to browse files."
|
||||
aria-label="{{ _('upload.drop_zone_aria') }}"
|
||||
ondragover="handleDragOver(event)"
|
||||
ondragleave="handleDragLeave(event)"
|
||||
>
|
||||
<p class="text-gray-500 mb-4 hidden sm:block">
|
||||
Drag & drop files or folders here, or click to select files.
|
||||
{{ _("upload.drop_hint_desktop") }}
|
||||
</p>
|
||||
<p class="text-gray-500 mb-4 sm:hidden">
|
||||
Tap to select files or use the camera button below.
|
||||
{{ _("upload.drop_hint_mobile") }}
|
||||
</p>
|
||||
<input
|
||||
id="fileInput"
|
||||
@@ -31,7 +31,7 @@
|
||||
onchange="handleFileSelect(event)"
|
||||
name="files"
|
||||
multiple
|
||||
aria-label="Select files to upload"
|
||||
aria-label="{{ _('upload.select_file') }}"
|
||||
accept="application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,image/*"
|
||||
/>
|
||||
<!-- Browse Files button (all devices) -->
|
||||
@@ -41,11 +41,11 @@
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 mb-4"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<i class="fas fa-folder-open mr-2" aria-hidden="true"></i> Browse Files
|
||||
<i class="fas fa-folder-open mr-2" aria-hidden="true"></i> {{ _("upload.browse_button") }}
|
||||
</button>
|
||||
<div class="text-sm text-gray-500 mt-2">
|
||||
<p>Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images</p>
|
||||
<p>Maximum size: 500MB per file • Directories are uploaded recursively</p>
|
||||
<p>{{ _("upload.file_types") }}</p>
|
||||
<p>{{ _("upload.file_size_hint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
class="w-full flex items-center justify-center gap-2 bg-green-500 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 cursor-pointer"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
<i class="fas fa-camera" aria-hidden="true"></i> Take Photo / Scan Document
|
||||
<i class="fas fa-camera" aria-hidden="true"></i> {{ _("upload.camera_button") }}
|
||||
<input
|
||||
id="cameraInput"
|
||||
type="file"
|
||||
@@ -63,7 +63,7 @@
|
||||
onchange="handleFileSelect(event)"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
aria-label="Take photo or scan document"
|
||||
aria-label="{{ _('upload.camera_button') }}"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -72,40 +72,40 @@
|
||||
|
||||
<!-- URL Upload Section -->
|
||||
<div class="w-full max-w-2xl mb-6 sm:mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Upload from URL</h2>
|
||||
<h2 class="text-xl font-semibold mb-4">{{ _("upload.section_url") }}</h2>
|
||||
<div class="bg-white border border-gray-300 rounded-lg p-4 sm:p-6">
|
||||
<form id="urlUploadForm" class="space-y-4">
|
||||
<div>
|
||||
<label for="urlInput" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
File URL
|
||||
{{ _("upload.url_label") }}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="urlInput"
|
||||
name="url"
|
||||
placeholder="https://example.com/document.pdf"
|
||||
placeholder="{{ _('upload.url_placeholder') }}"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
style="min-height:44px;"
|
||||
required
|
||||
/>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Enter a direct link to a file (PDF, Office documents, or images)
|
||||
{{ _("upload.url_description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="urlFilename" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Filename (optional)
|
||||
{{ _("upload.filename_label") }}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="urlFilename"
|
||||
name="filename"
|
||||
placeholder="my-document.pdf"
|
||||
placeholder="{{ _('upload.filename_placeholder') }}"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
style="min-height:44px;"
|
||||
/>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Leave empty to use filename from URL
|
||||
{{ _("upload.filename_description") }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -113,7 +113,7 @@
|
||||
class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200"
|
||||
style="min-height:44px;"
|
||||
>
|
||||
Download and Process
|
||||
{{ _("upload.download_button") }}
|
||||
</button>
|
||||
</form>
|
||||
<div id="urlStatusMessage" class="mt-4" aria-live="polite"></div>
|
||||
|
||||
Reference in New Issue
Block a user