chore: apply ruff formatting and fix whitespace issues

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-13 09:12:11 +00:00
parent 43bc58770d
commit b03ebab747
96 changed files with 991 additions and 993 deletions
+4 -4
View File
@@ -7,19 +7,19 @@
<p class="text-gray-700 mb-6 leading-relaxed">
Welcome to <strong>DocuElevate</strong> your modern, intelligent solution for document processing!
We've built DocuElevate to completely transform the way you handle your documents from upload
to extraction, from processing to storage.
to extraction, from processing to storage.
</p>
<!-- Our Story Section -->
<section class="bg-white shadow rounded p-6 mb-8">
<h2 class="text-2xl font-semibold mb-2">Our Story</h2>
<p class="text-gray-600 mb-4">
DocuElevate was created with one goal in mind: to simplify and streamline document management
DocuElevate was created with one goal in mind: to simplify and streamline document management
for everyone, whether you're a small startup or a large enterprise.
</p>
<p class="text-gray-600">
We harness the power of OpenAI for metadata extraction and text refinement, integrate seamlessly
with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document
We harness the power of OpenAI for metadata extraction and text refinement, integrate seamlessly
with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document
Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions.
</p>
</section>
+6 -6
View File
@@ -5,7 +5,7 @@
{% block content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-4">Third-Party Software Attributions</h1>
<div class="bg-white shadow-md rounded-lg p-6 mb-6">
<p class="mb-4">
DocuElevate uses several open source libraries and tools. We are grateful to the
@@ -21,11 +21,11 @@
</div>
<div class="ml-3">
<p class="text-sm text-yellow-700">
<strong>Special Attribution:</strong> This software includes Paramiko, which is licensed under LGPL. The source code for Paramiko is available at
<strong>Special Attribution:</strong> This software includes Paramiko, which is licensed under LGPL. The source code for Paramiko is available at
<a href="https://github.com/paramiko/paramiko" class="font-medium underline text-yellow-700 hover:text-yellow-600">
https://github.com/paramiko/paramiko
</a>.
A copy of the LGPL license can be found
A copy of the LGPL license can be found
<a href="/static/licenses/lgpl.txt" class="font-medium underline text-yellow-700 hover:text-yellow-600">
here
</a>.
@@ -33,7 +33,7 @@
</div>
</div>
</div>
<h2 class="text-xl font-semibold mb-2">Python Dependencies</h2>
<ul class="list-disc pl-5 mb-4">
<li class="mb-2">
@@ -138,7 +138,7 @@
<a href="https://github.com/caronc/apprise" class="text-blue-600 hover:underline">https://github.com/caronc/apprise</a>
</li>
</ul>
<h2 class="text-xl font-semibold mb-2">Docker Images</h2>
<ul class="list-disc pl-5 mb-4">
<li class="mb-2">
@@ -152,7 +152,7 @@
<a href="https://github.com/gotenberg/gotenberg" class="text-blue-600 hover:underline">https://github.com/gotenberg/gotenberg</a>
</li>
</ul>
<h2 class="text-xl font-semibold mb-2">Frontend Dependencies</h2>
<ul class="list-disc pl-5 mb-4">
<li class="mb-2">
+21 -21
View File
@@ -13,8 +13,8 @@
<!-- Tailwind CSS and other CSS -->
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
{% endblock %}
{% block head_extra %}{% endblock %}
@@ -24,12 +24,12 @@
<!-- Global Nav -->
<nav class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between h-16 items-center">
<!-- Brand + Icon -->
<div class="flex-shrink-0">
<a href="/" class="inline-flex items-center space-x-2">
<!-- Icon -->
<span
<span
class="material-symbols-light--folder-managed-outline text-blue-500"
style="width: 24px; height: 24px;"
></span>
@@ -39,7 +39,7 @@
</span>
</a>
</div>
<!-- Menu Items - using x-data for mobile menu toggle -->
<div x-data="{ mobileMenuOpen: false }">
<div class="hidden md:flex space-x-4 items-center">
@@ -49,15 +49,15 @@
<a href="/status" class="text-gray-700 hover:text-gray-900">Status</a>
<a href="/settings" class="text-gray-700 hover:text-gray-900">Settings</a>
<a href="/about" class="text-gray-700 hover:text-gray-900">About</a>
<!-- Dynamic Auth Section -->
<div id="authSection" class="text-gray-700 hover:text-gray-900"></div>
</div>
<!-- Mobile menu button -->
<button
@click="mobileMenuOpen = !mobileMenuOpen"
type="button"
<button
@click="mobileMenuOpen = !mobileMenuOpen"
type="button"
class="md:hidden inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
:aria-expanded="mobileMenuOpen"
>
@@ -66,10 +66,10 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<!-- Mobile menu, show/hide based on menu state -->
<div
x-show="mobileMenuOpen"
<div
x-show="mobileMenuOpen"
x-transition:enter="transition ease-out duration-100 transform"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
@@ -85,7 +85,7 @@
<a href="/status" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Status</a>
<a href="/settings" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Settings</a>
<a href="/about" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">About</a>
<!-- Mobile Auth Section -->
<div id="mobileAuthSection" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<!-- Will be populated by JS -->
@@ -104,13 +104,13 @@
<!-- Footer -->
<footer class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
DocuElevate 2025 -
<a href="/privacy" class="text-blue-500 hover:underline">Privacy</a> -
<a href="/imprint" class="text-blue-500 hover:underline">Imprint</a> -
<a href="/terms" class="text-blue-500 hover:underline">Terms</a> -
<a href="/cookies" class="text-blue-500 hover:underline">Cookies</a> -
<a href="/license" class="text-blue-500 hover:underline">License</a> -
<a href="/attribution" class="text-blue-500 hover:underline">Attributions</a> -
DocuElevate 2025 -
<a href="/privacy" class="text-blue-500 hover:underline">Privacy</a> -
<a href="/imprint" class="text-blue-500 hover:underline">Imprint</a> -
<a href="/terms" class="text-blue-500 hover:underline">Terms</a> -
<a href="/cookies" class="text-blue-500 hover:underline">Cookies</a> -
<a href="/license" class="text-blue-500 hover:underline">License</a> -
<a href="/attribution" class="text-blue-500 hover:underline">Attributions</a> -
<span class="text-xs">Version {{ app_version|default(version, true) }}</span>
</div>
</footer>
+34 -34
View File
@@ -8,10 +8,10 @@
<p class="text-gray-600 mb-4">
Configure the Dropbox integration for DocuElevate using our setup wizard.
</p>
<div class="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 my-4" role="alert">
<p class="font-bold">Current Status:</p>
<p>Dropbox integration is
<p>Dropbox integration is
{% if is_configured %}
<span class="text-green-700 font-semibold">configured</span>.
{% else %}
@@ -22,7 +22,7 @@
<p class="mt-2"><strong>Target folder:</strong> {{ folder_path }}</p>
{% endif %}
</div>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 my-4">
<div class="flex">
<div class="flex-shrink-0">
@@ -41,7 +41,7 @@
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
<h2 class="text-2xl font-semibold mb-4">Quick Setup Guide</h2>
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Step 1: Create a Dropbox App</h3>
<ol class="list-decimal ml-6 space-y-3">
@@ -80,30 +80,30 @@
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
<h2 class="text-2xl font-semibold mb-4">Complete Setup with Wizard</h2>
<div class="space-y-4">
<div>
<label for="app-key" class="block text-sm font-medium text-gray-700">App Key</label>
<input type="text" id="app-key" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Dropbox app key" value="{{ app_key_value }}">
</div>
<div>
<label for="app-secret" class="block text-sm font-medium text-gray-700">App Secret</label>
<input type="password" id="app-secret" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Dropbox app secret" value="{{ app_secret_value }}">
</div>
<div>
<label for="folder-path" class="block text-sm font-medium text-gray-700">Folder Path (Optional)</label>
<input type="text" id="folder-path" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="/Documents/Uploads" value="{{ folder_path }}">
<p class="text-xs text-gray-500 mt-1">Enter the folder path where files should be uploaded (e.g., /Documents/Uploads)</p>
</div>
<div>
<button id="start-auth-flow" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Start Authentication Flow
</button>
</div>
<!-- Token validation and status -->
<div id="token-status" class="mt-6 {{ 'hidden' if not is_configured else '' }}">
<div class="rounded-md {{ 'bg-green-50' if is_configured else 'bg-yellow-50' }} p-4">
@@ -130,7 +130,7 @@
</div>
</div>
</div>
<div class="mt-4 flex space-x-3">
<button id="test-token" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Test Token
@@ -139,25 +139,25 @@
Refresh Token
</button>
</div>
<!-- Configuration for Worker Nodes section -->
<div class="mt-6 p-4 bg-gray-100 rounded-md">
<h3 class="font-medium text-lg mb-2">Configuration for Worker Nodes</h3>
<p class="text-sm text-gray-600 mb-3">
Copy these environment variables to configure all worker nodes:
</p>
<div class="relative">
<pre id="env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code>DROPBOX_APP_KEY={{ app_key_value }}
DROPBOX_APP_SECRET={{ app_secret_value|default('YOUR_APP_SECRET', true) }}
DROPBOX_REFRESH_TOKEN={{ refresh_token_value|default('YOUR_REFRESH_TOKEN', true) }}
DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}</code></pre>
<button id="copy-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
Copy
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Add these variables to your .env file or environment configuration.
</p>
@@ -188,7 +188,7 @@ DROPBOX_FOLDER={{ folder_path|default('/Documents/Uploads', true) }}</code></pre
Back to Status
</a>
</div>
<!-- Result Modal -->
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
@@ -222,19 +222,19 @@ document.addEventListener('DOMContentLoaded', function() {
const refreshTokenBtn = document.getElementById('refresh-token-btn');
const tokenStatus = document.getElementById('token-status');
const appSecretInput = document.getElementById('app-secret');
// Modal elements
const resultModal = document.getElementById('resultModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalClose = document.getElementById('modalClose');
// Modal functions
function showModal(status, title, message) {
modalTitle.textContent = title;
modalMessage.textContent = message;
// Set the appropriate icon
if (status === 'success') {
modalIcon.innerHTML = `
@@ -251,24 +251,24 @@ document.addEventListener('DOMContentLoaded', function() {
`;
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4';
}
resultModal.classList.remove('hidden');
}
function hideModal() {
resultModal.classList.add('hidden');
}
// Close modal when clicking the close button
modalClose.addEventListener('click', hideModal);
// Close modal when clicking outside of it
resultModal.addEventListener('click', function(e) {
if (e.target === resultModal) {
hideModal();
}
});
// Start Authentication Flow button click
startAuthFlowBtn.addEventListener('click', function() {
const appKey = document.getElementById('app-key').value.trim();
@@ -280,7 +280,7 @@ document.addEventListener('DOMContentLoaded', function() {
showModal('error', 'Validation Error', 'Please enter your App Key');
return;
}
if (!appSecret) {
showModal('error', 'Validation Error', 'Please enter your App Secret');
return;
@@ -295,7 +295,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Generate the authorization URL
const authUrl = `https://www.dropbox.com/oauth2/authorize?client_id=${encodeURIComponent(appKey)}&response_type=code&token_access_type=offline&redirect_uri=${encodeURIComponent(redirectUri)}`;
// Redirect the user to the Dropbox login page
window.location.href = authUrl;
});
@@ -305,7 +305,7 @@ document.addEventListener('DOMContentLoaded', function() {
testTokenBtn.addEventListener('click', function() {
testTokenBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Testing...';
testTokenBtn.disabled = true;
fetch('/api/dropbox/test-token')
.then(response => response.json())
.then(data => {
@@ -336,13 +336,13 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
}
// Refresh Token button click
if (refreshTokenBtn) {
refreshTokenBtn.addEventListener('click', function() {
showModal('info', 'Confirm', 'This will start a new authentication flow to obtain a fresh token from Dropbox. Continue?');
modalClose.textContent = "Cancel";
// Add a confirm button
const confirmBtn = document.createElement('button');
confirmBtn.className = 'ml-2 px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-300';
@@ -351,10 +351,10 @@ document.addEventListener('DOMContentLoaded', function() {
hideModal();
startAuthFlowBtn.click();
});
// Add to modal
modalClose.parentNode.appendChild(confirmBtn);
// Make sure to remove the confirm button when modal is closed
const removeConfirmBtn = function() {
if (confirmBtn.parentNode) {
@@ -363,11 +363,11 @@ document.addEventListener('DOMContentLoaded', function() {
modalClose.textContent = "Close";
modalClose.removeEventListener('click', removeConfirmBtn);
};
modalClose.addEventListener('click', removeConfirmBtn, { once: true });
});
}
// Copy Environment Variables Button
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
if (copyEnvVarsBtn) {
@@ -389,14 +389,14 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
}
// Try to retrieve app secret from session storage (if coming back from auth)
if (appSecretInput && !appSecretInput.value && sessionStorage.getItem('dropbox_app_secret')) {
appSecretInput.value = sessionStorage.getItem('dropbox_app_secret');
// Clear it after use
sessionStorage.removeItem('dropbox_app_secret');
}
// If token is not configured but we have an app key, show the token status section
if (document.getElementById('app-key').value && !tokenStatus.classList.contains('hidden')) {
tokenStatus.classList.remove('hidden');
+28 -28
View File
@@ -11,15 +11,15 @@
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
<p class="text-gray-600 mt-2">Please wait while we complete the Dropbox authorization process...</p>
</div>
<div class="flex justify-center my-6">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
<div id="processing-message" class="text-center text-gray-700">
<p>Exchanging authorization code for refresh token...</p>
</div>
<div id="error-container" class="hidden mt-6">
<div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
@@ -42,7 +42,7 @@
</a>
</div>
</div>
<div id="success-container" class="hidden mt-6">
<div class="rounded-md bg-green-50 p-4">
<div class="flex">
@@ -58,26 +58,26 @@
</div>
</div>
</div>
<div class="mt-6 p-4 bg-gray-100 rounded-md">
<h3 class="font-medium text-lg mb-2">Configuration for Worker Nodes</h3>
<p class="text-sm text-gray-600 mb-3">
Copy these environment variables to configure all worker nodes:
</p>
<div class="relative">
<pre id="env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code></code></pre>
<button id="copy-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
Copy
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Add these variables to your .env file or environment configuration.
</p>
</div>
<div class="mt-4 text-center">
<a href="/status" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Go to Status Page
@@ -92,26 +92,26 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
const code = "{{ code }}";
// Get credentials from session storage (these take precedence over server-provided values)
const appKey = sessionStorage.getItem('dropbox_app_key') || "{{ app_key_value }}";
const appSecret = sessionStorage.getItem('dropbox_app_secret') || "{{ app_secret_value }}";
const folderPath = sessionStorage.getItem('dropbox_folder_path') || "{{ folder_path }}" || '/Documents/Uploads';
const redirectUri = window.location.origin + "/dropbox-callback";
// Automatically exchange the code for a refresh token
if (code) {
if (!appKey || !appSecret) {
showError("Missing App Key or App Secret. Please go back to the setup page and try again.");
return;
}
exchangeCode(code, appKey, appSecret, redirectUri);
} else {
showError("No authorization code was found in the URL");
}
function exchangeCode(code, appKey, appSecret, redirectUri) {
const formData = new FormData();
formData.append('client_id', appKey);
@@ -119,10 +119,10 @@ document.addEventListener('DOMContentLoaded', function() {
formData.append('redirect_uri', redirectUri);
formData.append('code', code);
formData.append('folder_path', folderPath);
document.getElementById('processing-message').innerHTML =
document.getElementById('processing-message').innerHTML =
'<p>Exchanging authorization code for refresh token...</p>';
fetch('/api/dropbox/exchange-token', {
method: 'POST',
body: formData
@@ -140,15 +140,15 @@ document.addEventListener('DOMContentLoaded', function() {
// Update settings in memory
const updateFormData = new FormData();
updateFormData.append('refresh_token', data.refresh_token);
// Use the app key and app secret from session storage
if (appKey) updateFormData.append('app_key', appKey);
if (appSecret) updateFormData.append('app_secret', appSecret);
if (folderPath) updateFormData.append('folder_path', folderPath);
document.getElementById('processing-message').innerHTML =
document.getElementById('processing-message').innerHTML =
'<p>Updating system settings with new token...</p>';
return fetch('/api/dropbox/update-settings', {
method: 'POST',
body: updateFormData
@@ -166,7 +166,7 @@ document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => {
window.location.href = '/status';
}, 10000);
// Clean up session storage
sessionStorage.removeItem('dropbox_app_key');
sessionStorage.removeItem('dropbox_app_secret');
@@ -180,23 +180,23 @@ document.addEventListener('DOMContentLoaded', function() {
showError(error.message);
});
}
function showError(message) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('error-container').classList.remove('hidden');
document.getElementById('error-message').innerText = message;
// Hide the spinner when showing error
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
}
function showSuccess(refreshToken, appKey, appSecret, folderPath) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('success-container').classList.remove('hidden');
// Hide the spinner when showing success
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
// Update the environment variables pre block with the new token
const envVarsCode = document.querySelector('#env-vars code');
if (envVarsCode) {
@@ -205,7 +205,7 @@ DROPBOX_APP_SECRET=${appSecret}
DROPBOX_REFRESH_TOKEN=${refreshToken}
DROPBOX_FOLDER=${folderPath || '/Documents/Uploads'}`;
}
// Add copy functionality
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
if (copyEnvVarsBtn) {
@@ -11,7 +11,7 @@
<h2 class="text-2xl font-bold mt-4">Authorization Failed</h2>
<p class="text-gray-600 mt-2">Sorry, we couldn't complete the Dropbox authorization.</p>
</div>
<div class="mb-6">
<div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
@@ -29,7 +29,7 @@
</div>
</div>
</div>
<div class="mt-6">
<a href="/dropbox-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Return to Setup
+1 -1
View File
@@ -69,7 +69,7 @@
Configuration is loaded from environment variables or .env files.
Make sure your environment variables are correctly set.
</p>
<div class="mt-4">
<a href="/api/diagnostic/settings" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
View API Diagnostic
+96 -96
View File
@@ -27,7 +27,7 @@
.back-button i {
margin-right: 0.5rem;
}
.detail-card {
background-color: white;
border-radius: 0.5rem;
@@ -41,7 +41,7 @@
margin-bottom: 1rem;
color: #2d3748;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
@@ -64,7 +64,7 @@
font-size: 1rem;
word-break: break-word;
}
/* Status badges */
.status-badge {
display: inline-block;
@@ -94,7 +94,7 @@
background-color: #E5E7EB;
color: #374151;
}
/* Step summary section */
.step-summary {
display: grid;
@@ -158,7 +158,7 @@
background-color: #ecc94b;
color: white;
}
/* Processing logs - collapsible */
.timeline {
position: relative;
@@ -243,7 +243,7 @@
font-size: 0.75rem;
color: #a0aec0;
}
.logs-toggle {
cursor: pointer;
user-select: none;
@@ -298,7 +298,7 @@
.timeline-detail.visible {
display: block;
}
.no-logs {
text-align: center;
padding: 3rem;
@@ -309,7 +309,7 @@
margin-bottom: 1rem;
opacity: 0.5;
}
.error-message {
background-color: #FEE2E2;
border: 1px solid #F87171;
@@ -318,7 +318,7 @@
border-radius: 0.25rem;
margin-bottom: 1rem;
}
.file-status-indicator {
display: inline-flex;
align-items: center;
@@ -335,7 +335,7 @@
background-color: #FEE2E2;
color: #991B1B;
}
/* Flow visualization with branches */
.flow-stage {
display: flex;
@@ -416,7 +416,7 @@
margin-left: 19px;
margin-bottom: 0.5rem;
}
/* Branch visualization */
.flow-branches {
margin-left: 56px;
@@ -507,7 +507,7 @@
background-color: #cbd5e0;
cursor: not-allowed;
}
/* Text Modal Styles */
.text-modal {
position: fixed;
@@ -528,7 +528,7 @@
border-radius: 0.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* PDF Canvas Viewer Styles */
.pdf-viewer-container {
border: 2px solid #e2e8f0;
@@ -589,12 +589,12 @@
const fileId = {{ file.id | tojson }};
const button = document.getElementById('reprocess-btn');
const statusDiv = document.getElementById('reprocess-status');
// Disable button and show loading
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
statusDiv.innerHTML = '';
try {
const response = await fetch(`/api/files/${fileId}/reprocess`, {
method: 'POST',
@@ -602,9 +602,9 @@
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
statusDiv.innerHTML = `
<div style="background-color: #D1FAE5; color: #065F46; padding: 1rem; border-radius: 0.25rem; margin-top: 1rem;">
@@ -633,20 +633,20 @@
button.innerHTML = '<i class="fas fa-redo"></i> Retry Processing';
}
}
// JavaScript for handling per-subtask retry
async function retrySubtask(subtaskName, buttonId) {
const fileId = {{ file.id | tojson }};
const button = document.getElementById(buttonId);
const statusDiv = document.getElementById('subtask-status-' + subtaskName);
// Disable button and show loading
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Retrying...';
if (statusDiv) {
statusDiv.innerHTML = '';
}
try {
const response = await fetch(`/api/files/${fileId}/retry-subtask?subtask_name=${subtaskName}`, {
method: 'POST',
@@ -654,9 +654,9 @@
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
if (statusDiv) {
statusDiv.innerHTML = `
@@ -690,12 +690,12 @@
button.innerHTML = '<i class="fas fa-redo"></i> Retry';
}
}
// Toggle logs visibility
function toggleLogs() {
const logsContent = document.getElementById('logs-content');
const toggleIcon = document.getElementById('logs-toggle-icon');
if (logsContent.classList.contains('expanded')) {
logsContent.classList.remove('expanded');
toggleIcon.classList.remove('fa-chevron-up');
@@ -721,13 +721,13 @@
icon.classList.add('fa-chevron-up');
}
}
// JavaScript for metadata JSON toggle
function toggleMetadata() {
const jsonView = document.getElementById('metadata-json-view');
const icon = document.getElementById('metadata-toggle-icon');
const btn = document.getElementById('metadata-toggle-btn');
if (jsonView.style.display === 'none') {
jsonView.style.display = 'block';
icon.classList.remove('fa-chevron-down');
@@ -740,18 +740,18 @@
btn.innerHTML = '<i id="metadata-toggle-icon" class="fas fa-chevron-down"></i> Show JSON';
}
}
// JavaScript for text modal toggle with on-demand loading
let textCache = { original: null, processed: null };
async function loadAndShowText(type, fileId) {
const modalId = type + '-text-modal';
const loadingId = type + '-text-loading';
const contentId = type + '-text-content';
// Show modal immediately
toggleTextModal(modalId);
// If already loaded, just show it
if (textCache[type]) {
document.getElementById(loadingId).style.display = 'none';
@@ -759,20 +759,20 @@
document.getElementById(contentId).textContent = textCache[type];
return;
}
// Show loading state
document.getElementById(loadingId).style.display = 'block';
document.getElementById(contentId).style.display = 'none';
try {
const response = await fetch(`/files/${fileId}/text/${type}`);
if (!response.ok) {
throw new Error('Failed to extract text');
}
const data = await response.json();
textCache[type] = data.text;
// Show the text
document.getElementById(loadingId).style.display = 'none';
document.getElementById(contentId).style.display = 'block';
@@ -788,7 +788,7 @@
`;
}
}
function toggleTextModal(modalId) {
const modal = document.getElementById(modalId);
if (modal.style.display === 'none' || modal.style.display === '') {
@@ -799,7 +799,7 @@
document.body.style.overflow = 'auto'; // Re-enable scrolling
}
}
// Close modal when clicking outside the content
window.onclick = function(event) {
const modals = document.querySelectorAll('.text-modal');
@@ -810,26 +810,26 @@
}
});
}
// PDF.js viewer functionality
const pdfViewers = {
original: { currentPage: 1, totalPages: 0, pdfDoc: null },
processed: { currentPage: 1, totalPages: 0, pdfDoc: null }
};
async function loadPDF(type, fileId) {
const url = `/files/${fileId}/preview/${type}`;
const canvasWrapper = document.getElementById(`${type}-canvas-wrapper`);
try {
// Load PDF document
const loadingTask = pdfjsLib.getDocument(url);
const pdf = await loadingTask.promise;
pdfViewers[type].pdfDoc = pdf;
pdfViewers[type].totalPages = pdf.numPages;
pdfViewers[type].currentPage = 1;
// Clear loading message and render first page
canvasWrapper.innerHTML = '';
await renderPage(type);
@@ -844,58 +844,58 @@
`;
}
}
async function renderPage(type) {
const viewer = pdfViewers[type];
if (!viewer.pdfDoc) return;
const canvasWrapper = document.getElementById(`${type}-canvas-wrapper`);
const page = await viewer.pdfDoc.getPage(viewer.currentPage);
// Calculate scale to fit container width (max 600px width)
const viewport = page.getViewport({ scale: 1.0 });
const scale = Math.min(600 / viewport.width, 2.0);
const scaledViewport = page.getViewport({ scale });
// Create canvas for this page
const canvas = document.createElement('canvas');
canvas.className = 'pdf-canvas';
canvas.height = scaledViewport.height;
canvas.width = scaledViewport.width;
const context = canvas.getContext('2d');
const renderContext = {
canvasContext: context,
viewport: scaledViewport
};
// Clear previous canvas and render new one
canvasWrapper.innerHTML = '';
canvasWrapper.appendChild(canvas);
await page.render(renderContext).promise;
}
function changePage(type, delta) {
const viewer = pdfViewers[type];
const newPage = viewer.currentPage + delta;
if (newPage >= 1 && newPage <= viewer.totalPages) {
viewer.currentPage = newPage;
renderPage(type);
updatePageInfo(type);
}
}
function updatePageInfo(type) {
const viewer = pdfViewers[type];
document.getElementById(`${type}-page-info`).textContent =
document.getElementById(`${type}-page-info`).textContent =
`Page ${viewer.currentPage} of ${viewer.totalPages}`;
document.getElementById(`${type}-prev-btn`).disabled = viewer.currentPage === 1;
document.getElementById(`${type}-next-btn`).disabled = viewer.currentPage === viewer.totalPages;
}
// Load PDFs when page loads
document.addEventListener('DOMContentLoaded', function() {
const fileId = {{ file.id | tojson }};
@@ -916,13 +916,13 @@
<i class="fas fa-arrow-left"></i>
Back to File List
</a>
{% if error %}
<div class="error-message">
<p><strong>Error:</strong> {{ error }}</p>
</div>
{% else %}
<!-- Overall Processing Status Banner -->
{% if step_summary %}
<div style="margin-bottom: 1.5rem; padding: 1.5rem; background-color: #f0f9ff; border-left: 4px solid #3b82f6; border-radius: 0.5rem;">
@@ -980,7 +980,7 @@
}
</style>
{% endif %}
<!-- File Information Card -->
<div class="detail-card">
<h3>File Information</h3>
@@ -1051,7 +1051,7 @@
</div>
</div>
</div>
<!-- GPT Metadata Card -->
{% if gpt_metadata %}
<div class="detail-card">
@@ -1061,7 +1061,7 @@
<i id="metadata-toggle-icon" class="fas fa-chevron-down"></i> Show JSON
</button>
</div>
<div class="detail-grid">
{% if gpt_metadata.document_type %}
<div class="detail-item">
@@ -1069,49 +1069,49 @@
<span class="detail-value">{{ gpt_metadata.document_type }}</span>
</div>
{% endif %}
{% if gpt_metadata.filename %}
<div class="detail-item">
<span class="detail-label">Suggested Filename</span>
<span class="detail-value" style="font-family: monospace; font-size: 0.875rem;">{{ gpt_metadata.filename }}</span>
</div>
{% endif %}
{% if gpt_metadata.date %}
<div class="detail-item">
<span class="detail-label">Document Date</span>
<span class="detail-value">{{ gpt_metadata.date }}</span>
</div>
{% endif %}
{% if gpt_metadata.absender %}
<div class="detail-item">
<span class="detail-label">Sender (Absender)</span>
<span class="detail-value">{{ gpt_metadata.absender }}</span>
</div>
{% endif %}
{% if gpt_metadata.empfaenger %}
<div class="detail-item">
<span class="detail-label">Recipient (Empfänger)</span>
<span class="detail-value">{{ gpt_metadata.empfaenger }}</span>
</div>
{% endif %}
{% if gpt_metadata.betrag %}
<div class="detail-item">
<span class="detail-label">Amount (Betrag)</span>
<span class="detail-value">{{ gpt_metadata.betrag }}</span>
</div>
{% endif %}
{% if gpt_metadata.kontonummer %}
<div class="detail-item">
<span class="detail-label">Account Number</span>
<span class="detail-value" style="font-family: monospace;">{{ gpt_metadata.kontonummer }}</span>
</div>
{% endif %}
{% if gpt_metadata.tags %}
<div class="detail-item">
<span class="detail-label">Tags</span>
@@ -1125,18 +1125,18 @@
</div>
{% endif %}
</div>
<!-- Collapsible JSON view -->
<div id="metadata-json-view" style="display: none; margin-top: 1rem;">
<pre style="background-color: #1a202c; color: #e2e8f0; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; font-size: 0.875rem; line-height: 1.5;">{{ gpt_metadata | tojson(indent=2) }}</pre>
</div>
</div>
{% endif %}
<!-- PDF Preview Card -->
<div class="detail-card">
<h3>Document Previews</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1rem;">
<!-- Original PDF Preview -->
<div>
@@ -1155,8 +1155,8 @@
</div>
</div>
</div>
<button
onclick="loadAndShowText('original', {{ file.id }})"
<button
onclick="loadAndShowText('original', {{ file.id }})"
style="margin-top: 0.5rem; background-color: #4299e1; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; width: 100%;"
>
<i class="fas fa-file-alt"></i> View Extracted Text
@@ -1168,7 +1168,7 @@
</div>
{% endif %}
</div>
<!-- Processed PDF Preview -->
<div>
<h4 style="font-weight: 600; color: #2d3748; margin-bottom: 0.5rem;">Processed Document</h4>
@@ -1186,8 +1186,8 @@
</div>
</div>
</div>
<button
onclick="loadAndShowText('processed', {{ file.id }})"
<button
onclick="loadAndShowText('processed', {{ file.id }})"
style="margin-top: 0.5rem; background-color: #48bb78; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; width: 100%;"
>
<i class="fas fa-file-alt"></i> View Extracted Text
@@ -1201,15 +1201,15 @@
</div>
</div>
</div>
<!-- Text Modals (Hidden by default, loaded on-demand) -->
<!-- Original Text Modal -->
<div id="original-text-modal" class="text-modal" style="display: none;">
<div class="text-modal-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 2px solid #e2e8f0;">
<h3 style="margin: 0; color: #2d3748;">Extracted Text (Original)</h3>
<button
onclick="toggleTextModal('original-text-modal')"
<button
onclick="toggleTextModal('original-text-modal')"
style="background-color: #f56565; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
>
<i class="fas fa-times"></i> Close
@@ -1222,14 +1222,14 @@
<pre id="original-text-content" style="display: none; background-color: #1a202c; color: #e2e8f0; padding: 1.5rem; border-radius: 0.5rem; max-height: 600px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; font-size: 0.875rem; line-height: 1.6;"></pre>
</div>
</div>
<!-- Processed Text Modal -->
<div id="processed-text-modal" class="text-modal" style="display: none;">
<div class="text-modal-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; padding-bottom: 1rem; border-bottom: 2px solid #e2e8f0;">
<h3 style="margin: 0; color: #2d3748;">Extracted Text (Processed)</h3>
<button
onclick="toggleTextModal('processed-text-modal')"
<button
onclick="toggleTextModal('processed-text-modal')"
style="background-color: #f56565; color: white; padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;"
>
<i class="fas fa-times"></i> Close
@@ -1242,7 +1242,7 @@
<pre id="processed-text-content" style="display: none; background-color: #1a202c; color: #e2e8f0; padding: 1.5rem; border-radius: 0.5rem; max-height: 600px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; font-size: 0.875rem; line-height: 1.6;"></pre>
</div>
</div>
<!-- Step Summary Card -->
{% if step_summary %}
<div class="detail-card">
@@ -1278,7 +1278,7 @@
{% endif %}
</div>
</div>
{% if step_summary.total_upload_tasks > 0 %}
<div class="summary-card upload-steps">
<div class="summary-title">Upload Destinations</div>
@@ -1314,7 +1314,7 @@
</div>
</div>
{% endif %}
<!-- Processing History Card (Collapsible) -->
<div class="detail-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
@@ -1369,7 +1369,7 @@
</div>
{% endif %}
</div>
<!-- Process Flow Visualization Card -->
{% if flow_data %}
<div class="detail-card">
@@ -1385,7 +1385,7 @@
{% elif stage.status in ['pending', 'queued'] %}<i class="fas fa-clock"></i>
{% else %}<i class="fas fa-circle"></i>{% endif %}
</div>
<!-- Stage content -->
<div class="flow-content {{ stage.status.lower().replace(' ', '_') }}">
<div class="flow-title">{{ stage.label }}</div>
@@ -1396,9 +1396,9 @@
<div style="color: #718096; font-size: 0.875rem; font-style: italic;">Not executed</div>
{% endif %}
{% if stage.can_retry and stage.status == 'failure' %}
<button
id="retry-btn-{{ stage.key }}"
class="retry-btn"
<button
id="retry-btn-{{ stage.key }}"
class="retry-btn"
onclick="retrySubtask('{{ stage.key }}', 'retry-btn-{{ stage.key }}')"
style="margin-top: 0.5rem;">
<i class="fas fa-redo"></i> Retry from this step
@@ -1411,7 +1411,7 @@
{% endif %}
</div>
</div>
<!-- Upload branches (if this stage has them) -->
{% if stage.is_branch_parent and stage.branches %}
<div class="flow-branches">
@@ -1427,9 +1427,9 @@
<div class="branch-title">
<span>{{ branch.label }}</span>
{% if branch.can_retry and branch.status == 'failure' %}
<button
id="retry-btn-{{ branch.key }}"
class="retry-btn"
<button
id="retry-btn-{{ branch.key }}"
class="retry-btn"
onclick="retrySubtask('{{ branch.key }}', 'retry-btn-{{ branch.key }}')">
<i class="fas fa-redo"></i> Retry
</button>
@@ -1449,7 +1449,7 @@
{% endfor %}
</div>
{% endif %}
<!-- Connector line (except for last item) -->
{% if not loop.last %}
<div class="flow-connector"></div>
@@ -1458,7 +1458,7 @@
</div>
</div>
{% endif %}
<!-- File Preview Card -->
{% if file_exists or processed_exists %}
<div class="detail-card">
@@ -1489,7 +1489,7 @@
</div>
</div>
{% endif %}
{% if processed_exists %}
<div>
<h4 style="font-weight: 600; color: #2d3748; margin-bottom: 1rem;">Processed File</h4>
@@ -1510,7 +1510,7 @@
</div>
</div>
{% endif %}
{% endif %}
</div>
{% endblock %}
+5 -5
View File
@@ -10,7 +10,7 @@
console.log('Before Alpine init on /files page');
document.addEventListener('alpine:init', () => {
console.log('Alpine.js initialized in files view');
});
console.log('After Alpine init listener registration');
</script>
@@ -57,7 +57,7 @@
<h2 class="text-3xl font-bold mb-6">File Records</h2>
<!-- Grid.js will render the table in this container -->
<div id="gridjs-wrapper"></div>
<!-- Confirmation Modal (temporarily disabled) -->
<div id="confirmDeleteModal" class="confirm-delete-modal hidden">
<div class="confirm-delete-content">
@@ -104,7 +104,7 @@
const response = await fetch(`/api/files/${fileId}`, {
method: 'DELETE'
});
if (response.ok) {
// Success - reload the grid
grid.forceRender();
@@ -132,7 +132,7 @@
// Handle delete confirmation
confirmDelete.addEventListener('click', async function() {
if (!fileToDelete) return;
deleteFile(fileToDelete.id);
closeDeleteModal();
});
@@ -151,7 +151,7 @@
{ id: 'file_size', name: 'File Size', formatter: (size) => `${(size / 1024).toFixed(2)} KB` },
{ id: 'mime_type', name: 'Mime Type' },
{ id: 'created_at', name: 'Created At' },
{
{
id: 'actions',
name: 'Actions',
formatter: (_, row) => {
+60 -60
View File
@@ -54,7 +54,7 @@
font-weight: 400;
margin-top: 0.5rem;
}
/* Upload progress modal */
.upload-modal {
display: none;
@@ -95,14 +95,14 @@
.close-modal-btn:hover {
opacity: 0.8;
}
<style>
.file-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
.file-table th,
.file-table th,
.file-table td {
padding: 0.75rem;
text-align: left;
@@ -133,7 +133,7 @@
opacity: 1;
font-weight: bold;
}
/* Status badges */
.status-badge {
display: inline-block;
@@ -163,7 +163,7 @@
background-color: #E5E7EB;
color: #374151;
}
/* Action buttons */
.action-btn {
color: #3182ce;
@@ -185,7 +185,7 @@
.action-btn.delete:hover {
background-color: #fed7d7;
}
/* Filters section */
.filters-section {
background-color: #f7fafc;
@@ -234,7 +234,7 @@
.filter-item button.clear:hover {
background-color: #4a5568;
}
/* Pagination */
.pagination {
display: flex;
@@ -271,7 +271,7 @@
color: white;
border-color: #3182ce;
}
/* Modal styles */
.modal {
display: none;
@@ -325,7 +325,7 @@
.modal-btn-delete:hover {
background-color: #c53030;
}
.error-message {
background-color: #FEE2E2;
border: 1px solid #F87171;
@@ -363,7 +363,7 @@
<div class="container mx-auto px-4 py-8">
<h2 class="text-3xl font-bold mb-6">File Records</h2>
{% if error %}
<div class="error-message">
<p><strong>Error:</strong> {{ error }}</p>
@@ -378,7 +378,7 @@
<label for="search">Search Filename</label>
<input type="text" id="search" name="search" value="{{ search }}" placeholder="Enter filename...">
</div>
<div class="filter-item">
<label for="mime_type">MIME Type</label>
<select id="mime_type" name="mime_type">
@@ -388,7 +388,7 @@
{% endfor %}
</select>
</div>
<div class="filter-item">
<label for="status">Status</label>
<select id="status" name="status">
@@ -400,24 +400,24 @@
<option value="duplicate" {% if status == "duplicate" %}selected{% endif %}>Duplicate</option>
</select>
</div>
<div class="filter-item">
<label>&nbsp;</label>
<button type="submit">Apply Filters</button>
</div>
<div class="filter-item">
<label>&nbsp;</label>
<button type="button" class="clear" onclick="clearFilters()">Clear</button>
</div>
<!-- Hidden fields to preserve sort order -->
<input type="hidden" name="sort_by" value="{{ sort_by }}">
<input type="hidden" name="sort_order" value="{{ sort_order }}">
<input type="hidden" name="per_page" value="{{ pagination.per_page }}">
</form>
</div>
<!-- Bulk Actions Section -->
<div id="bulkActionsBar" class="filters-section" style="display: none; background-color: #e6f3ff;">
<div style="display: flex; justify-content: space-between; align-items: center;">
@@ -437,7 +437,7 @@
</div>
</div>
</div>
<!-- File table -->
<div class="overflow-x-auto">
<table class="file-table" id="fileTable">
@@ -535,13 +535,13 @@
</tbody>
</table>
</div>
<!-- Pagination -->
{% if pagination.total_pages > 1 %}
<div class="pagination">
<div class="pagination-info">
Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ min(pagination.page * pagination.per_page, pagination.total_items) }}
Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ min(pagination.page * pagination.per_page, pagination.total_items) }}
of {{ pagination.total_items }} files
</div>
<div class="pagination-buttons">
@@ -549,13 +549,13 @@
<button class="pagination-button" onclick="goToPage(1)">First</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})">Previous</button>
{% endif %}
{% for p in range(max(1, pagination.page - 2), min(pagination.total_pages + 1, pagination.page + 3)) %}
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})">
{{ p }}
</button>
{% endfor %}
{% if pagination.page < pagination.total_pages %}
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})">Next</button>
<button class="pagination-button" onclick="goToPage({{ pagination.total_pages }})">Last</button>
@@ -563,7 +563,7 @@
</div>
</div>
{% endif %}
<!-- Delete confirmation modal -->
<div id="deleteModal" class="modal">
<div class="modal-content">
@@ -575,41 +575,41 @@
</div>
</div>
</div>
<script>
// Modal functionality
const deleteModal = document.getElementById('deleteModal');
const cancelDelete = document.getElementById('cancelDelete');
const confirmDelete = document.getElementById('confirmDelete');
let currentFileId = null;
function showDeleteModal(fileId, event) {
if (event) event.stopPropagation();
currentFileId = fileId;
deleteModal.style.display = 'flex';
}
function viewFileDetail(fileId, event) {
if (event) event.stopPropagation();
window.location.href = `/files/${fileId}/detail`;
}
cancelDelete.addEventListener('click', () => {
deleteModal.style.display = 'none';
});
confirmDelete.addEventListener('click', () => {
deleteFile(currentFileId);
deleteModal.style.display = 'none';
});
// Close modal if clicking outside of it
window.addEventListener('click', (event) => {
if (event.target === deleteModal) {
deleteModal.style.display = 'none';
}
});
function deleteFile(fileId) {
fetch(`/api/files/${fileId}`, {
method: 'DELETE',
@@ -645,35 +645,35 @@
alert(`Error deleting file: ${error.message}`);
});
}
function sortTable(column) {
const urlParams = new URLSearchParams(window.location.search);
const currentSortBy = urlParams.get('sort_by') || 'created_at';
const currentSortOrder = urlParams.get('sort_order') || 'desc';
// Toggle sort order if clicking the same column
let newSortOrder = 'asc';
if (column === currentSortBy) {
newSortOrder = currentSortOrder === 'asc' ? 'desc' : 'asc';
}
urlParams.set('sort_by', column);
urlParams.set('sort_order', newSortOrder);
urlParams.set('page', '1'); // Reset to first page on sort
window.location.search = urlParams.toString();
}
function goToPage(page) {
const urlParams = new URLSearchParams(window.location.search);
urlParams.set('page', page);
window.location.search = urlParams.toString();
}
function clearFilters() {
window.location.href = '/files';
}
// Bulk selection functionality
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll');
@@ -683,48 +683,48 @@
});
updateBulkActionsBar();
}
function updateBulkActionsBar() {
const checkboxes = document.querySelectorAll('.file-checkbox:checked');
const selectedCount = checkboxes.length;
const bulkActionsBar = document.getElementById('bulkActionsBar');
const selectedCountEl = document.getElementById('selectedCount');
if (selectedCount > 0) {
bulkActionsBar.style.display = 'block';
selectedCountEl.textContent = selectedCount;
} else {
bulkActionsBar.style.display = 'none';
}
// Update "select all" checkbox state
const allCheckboxes = document.querySelectorAll('.file-checkbox');
const selectAll = document.getElementById('selectAll');
selectAll.checked = allCheckboxes.length > 0 && selectedCount === allCheckboxes.length;
}
function clearSelection() {
document.querySelectorAll('.file-checkbox').forEach(cb => cb.checked = false);
document.getElementById('selectAll').checked = false;
updateBulkActionsBar();
}
function getSelectedFileIds() {
const checkboxes = document.querySelectorAll('.file-checkbox:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.value));
}
function bulkDelete() {
const fileIds = getSelectedFileIds();
if (fileIds.length === 0) {
alert('Please select files to delete');
return;
}
if (!confirm(`Are you sure you want to delete ${fileIds.length} file(s)?`)) {
return;
}
fetch('/api/files/bulk-delete', {
method: 'POST',
headers: {
@@ -749,18 +749,18 @@
alert(`Error deleting files: ${error.message}`);
});
}
function bulkReprocess() {
const fileIds = getSelectedFileIds();
if (fileIds.length === 0) {
alert('Please select files to reprocess');
return;
}
if (!confirm(`Are you sure you want to reprocess ${fileIds.length} file(s)?`)) {
return;
}
fetch('/api/files/bulk-reprocess', {
method: 'POST',
headers: {
@@ -791,55 +791,55 @@
alert(`Error reprocessing files: ${error.message}`);
});
}
// ===== Drag-and-Drop Upload Functionality =====
const dropOverlay = document.getElementById('dropOverlay');
const uploadModal = document.getElementById('uploadModal');
const uploadStatusMessage = document.getElementById('uploadStatusMessage');
const uploadProgressContainer = document.getElementById('uploadProgressContainer');
let dragCounter = 0; // Track nested drag events
// Show overlay when dragging files over the window
window.addEventListener('dragenter', (e) => {
e.preventDefault();
dragCounter++;
// Only show overlay if dragging files
if (e.dataTransfer.types.includes('Files')) {
dropOverlay.classList.add('active');
}
});
window.addEventListener('dragleave', (e) => {
e.preventDefault();
dragCounter--;
if (dragCounter === 0) {
dropOverlay.classList.remove('active');
}
});
window.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
window.addEventListener('drop', (e) => {
e.preventDefault();
dragCounter = 0;
dropOverlay.classList.remove('active');
if (e.dataTransfer.files.length > 0) {
// Show upload modal
uploadModal.classList.add('active');
uploadProgressContainer.innerHTML = '';
// Process the dropped files
processFiles(e.dataTransfer.files, uploadProgressContainer, uploadStatusMessage);
}
});
// Listen for upload completion event and reload the page to show new files
window.addEventListener('allUploadsComplete', (e) => {
// Wait 2 seconds to let users see the success message
@@ -847,7 +847,7 @@
window.location.reload();
}, 2000);
});
function closeUploadModal() {
uploadModal.classList.remove('active');
}
+89 -89
View File
@@ -8,10 +8,10 @@
<p class="text-gray-600 mb-4">
Configure the Google Drive integration for DocuElevate using our setup wizard.
</p>
<div class="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 my-4" role="alert">
<p class="font-bold">Current Status:</p>
<p>Google Drive integration is
<p>Google Drive integration is
{% if is_configured %}
<span class="text-green-700 font-semibold">configured</span>.
{% else %}
@@ -27,7 +27,7 @@
<p class="mt-2"><strong>Target folder ID:</strong> {{ folder_id }}</p>
{% endif %}
</div>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 my-4">
<div class="flex">
<div class="flex-shrink-0">
@@ -47,7 +47,7 @@
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
<div class="flex justify-between items-start mb-4">
<h2 class="text-2xl font-semibold">Authentication Method</h2>
{% if is_configured %}
<span class="px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
Configured
@@ -58,7 +58,7 @@
</span>
{% endif %}
</div>
<div class="flex space-x-4 mb-6">
<button id="oauth-tab-btn" class="px-4 py-2 rounded-md {{ 'text-white bg-blue-600' if use_oauth else 'text-gray-700 bg-gray-200' }}">
OAuth User Account
@@ -67,7 +67,7 @@
Service Account
</button>
</div>
<!-- OAuth Tab Content -->
<div id="oauth-tab" class="{{ 'block' if use_oauth else 'hidden' }}">
<div class="mb-6">
@@ -132,24 +132,24 @@
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Step 5: Complete OAuth Setup</h3>
<p class="mb-4">Now enter your Client ID and Client Secret below, and we'll help you complete the OAuth flow. You can set the folder ID after authentication is complete.</p>
<div class="space-y-4">
<div>
<label for="client-id" class="block text-sm font-medium text-gray-700">Client ID</label>
<input type="text" id="client-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your OAuth Client ID" value="{{ client_id_value }}">
</div>
<div>
<label for="client-secret" class="block text-sm font-medium text-gray-700">Client Secret</label>
<input type="password" id="client-secret" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your OAuth Client Secret" value="{{ client_secret_value }}">
</div>
<div>
<label for="folder-id" class="block text-sm font-medium text-gray-700">Folder ID (Optional)</label>
<input type="text" id="folder-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Google Drive folder ID" value="{{ folder_id }}">
<p class="text-xs text-gray-500 mt-1">Optional: You can set this after authentication if you prefer</p>
</div>
<div>
<button id="select-folder-btn" class="mb-2 inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="h-5 w-5 mr-2 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
@@ -159,7 +159,7 @@
Select Folder with Picker
</button>
</div>
<div>
<button id="start-oauth-flow" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Start Authentication Flow
@@ -168,14 +168,14 @@
</div>
</div>
</div>
<!-- Service Account Tab Content -->
<div id="sa-tab" class="{{ 'block' if not use_oauth else 'hidden' }}">
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Service Account Configuration</h3>
<p class="mb-4">Service accounts allow for server-to-server authentication without user involvement. This method is useful for background tasks.</p>
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
@@ -190,14 +190,14 @@
</div>
</div>
</div>
<div class="space-y-4">
<div>
<label for="sa-folder-id" class="block text-sm font-medium text-gray-700">Folder ID</label>
<input type="text" id="sa-folder-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Google Drive folder ID" value="{{ folder_id }}">
<p class="text-xs text-gray-500 mt-1">This is the ID from the Google Drive folder URL where files will be saved</p>
</div>
<div>
<label for="sa-credentials" class="block text-sm font-medium text-gray-700">Service Account Credentials</label>
<div class="mt-1">
@@ -213,7 +213,7 @@
</a>
</div>
</div>
<div>
<button id="save-sa-settings" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Save Service Account Settings
@@ -223,11 +223,11 @@
</div>
</div>
</div>
<!-- Token validation and status -->
<div id="token-status" class="mt-6 bg-white shadow-md rounded-lg p-6 {{ 'hidden' if not is_configured else '' }}">
<h2 class="text-xl font-semibold mb-4">Connection Status</h2>
<div class="rounded-md {{ 'bg-green-50' if is_configured else 'bg-yellow-50' }} p-4">
<div class="flex">
<div class="flex-shrink-0">
@@ -252,7 +252,7 @@
</div>
</div>
</div>
<div class="mt-4 flex space-x-3">
<button id="test-connection" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Test Connection
@@ -261,47 +261,47 @@
Refresh Token
</button>
</div>
<!-- Configuration for Worker Nodes section -->
<div id="oauth-env-section" class="mt-6 p-4 bg-gray-100 rounded-md {{ 'hidden' if not use_oauth else '' }}">
<h3 class="font-medium text-lg mb-2">OAuth Configuration for Worker Nodes</h3>
<p class="text-sm text-gray-600 mb-3">
Copy these environment variables to configure all worker nodes:
</p>
<div class="relative">
<pre id="oauth-env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code>GOOGLE_DRIVE_USE_OAUTH=true
GOOGLE_DRIVE_CLIENT_ID={{ client_id_value }}
GOOGLE_DRIVE_CLIENT_SECRET={{ client_secret_value|default('YOUR_CLIENT_SECRET', true) }}
GOOGLE_DRIVE_REFRESH_TOKEN={{ refresh_token_value|default('YOUR_REFRESH_TOKEN', true) }}
GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}</code></pre>
<button id="copy-oauth-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
Copy
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Add these variables to your .env file or environment configuration.
</p>
</div>
<div id="sa-env-section" class="mt-6 p-4 bg-gray-100 rounded-md {{ 'hidden' if use_oauth else '' }}">
<h3 class="font-medium text-lg mb-2">Service Account Configuration for Worker Nodes</h3>
<p class="text-sm text-gray-600 mb-3">
Copy these environment variables to configure all worker nodes:
</p>
<div class="relative">
<pre id="sa-env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code>GOOGLE_DRIVE_USE_OAUTH=false
GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
# Make sure to add your GOOGLE_DRIVE_CREDENTIALS_JSON to your .env file</code></pre>
<button id="copy-sa-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
Copy
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Add these variables to your .env file along with your service account credentials JSON.
</p>
@@ -330,7 +330,7 @@ GOOGLE_DRIVE_FOLDER_ID={{ folder_id|default('YOUR_FOLDER_ID', true) }}
Back to Status
</a>
</div>
<!-- Result Modal -->
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
@@ -363,7 +363,7 @@ document.addEventListener('DOMContentLoaded', function() {
const saTabBtn = document.getElementById('sa-tab-btn');
const oauthTab = document.getElementById('oauth-tab');
const saTab = document.getElementById('sa-tab');
// Form elements
const clientIdInput = document.getElementById('client-id');
const clientSecretInput = document.getElementById('client-secret');
@@ -372,27 +372,27 @@ document.addEventListener('DOMContentLoaded', function() {
const startOauthFlowBtn = document.getElementById('start-oauth-flow');
const saveSaSettingsBtn = document.getElementById('save-sa-settings');
const selectFolderBtn = document.getElementById('select-folder-btn');
// Status and test elements
const tokenStatus = document.getElementById('token-status');
const testConnectionBtn = document.getElementById('test-connection');
const refreshTokenBtn = document.getElementById('refresh-token-btn');
// Environment sections
const oauthEnvSection = document.getElementById('oauth-env-section');
const saEnvSection = document.getElementById('sa-env-section');
// Modal elements
const resultModal = document.getElementById('resultModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalClose = document.getElementById('modalClose');
// Google Picker variables
let pickerApiLoaded = false;
let pickerOAuthToken = null;
// Google Picker API functions
function loadPickerApi() {
gapi.load('picker', {
@@ -401,7 +401,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}
// Load the Google API Loader script if select folder button exists
if (selectFolderBtn) {
const script = document.createElement('script');
@@ -410,7 +410,7 @@ document.addEventListener('DOMContentLoaded', function() {
loadPickerApi();
};
document.body.appendChild(script);
// Load the GSI Client for OAuth
const gsiScript = document.createElement('script');
gsiScript.src = 'https://accounts.google.com/gsi/client';
@@ -434,14 +434,14 @@ document.addEventListener('DOMContentLoaded', function() {
showPicker(clientId, data.access_token);
} else {
// If no valid token exists, inform the user they need to authenticate first
showModal('error', 'Authentication Required',
showModal('error', 'Authentication Required',
'You need to complete OAuth authentication before selecting a folder. ' +
'Please click "Start Authentication Flow" first and then select a folder after authentication.');
}
})
.catch(error => {
console.error('Error checking token:', error);
showModal('error', 'Authentication Required',
showModal('error', 'Authentication Required',
'Unable to verify authentication status. Please complete the OAuth flow first.');
});
}
@@ -451,13 +451,13 @@ document.addEventListener('DOMContentLoaded', function() {
showModal('error', 'Authentication Required', 'No access token available. Please authenticate first.');
return;
}
// Use the folders view specifically
const folderView = new google.picker.DocsView(google.picker.ViewId.FOLDERS)
.setIncludeFolders(true)
.setSelectFolderEnabled(true)
.setMode(google.picker.DocsViewMode.LIST); // Use LIST mode which doesn't require broader permissions
const picker = new google.picker.PickerBuilder()
.addView(folderView)
.setOAuthToken(oauthToken)
@@ -468,25 +468,25 @@ document.addEventListener('DOMContentLoaded', function() {
.setSelectableMimeTypes('application/vnd.google-apps.folder') // Allow only folder selection
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
}
function pickerCallback(data) {
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
const folder = data[google.picker.Response.DOCUMENTS][0];
const folderId = folder[google.picker.Document.ID];
const folderName = folder[google.picker.Document.NAME];
// Update the folder ID input
folderIdInput.value = folderId;
if (saFolderIdInput) {
saFolderIdInput.value = folderId;
}
// Automatically save the folder ID to ensure it's stored
saveFolderId(folderId);
// Show success message
showModal('success', 'Folder Selected', `You selected folder: "${folderName}" (ID: ${folderId}) and saved it to your configuration.`);
}
@@ -496,21 +496,21 @@ document.addEventListener('DOMContentLoaded', function() {
function saveFolderId(folderId) {
// Check which tab is active to determine if we're using OAuth or Service Account
const isOauthActive = !oauthTab.classList.contains('hidden');
// Prepare form data
const formData = new FormData();
formData.append('folder_id', folderId);
formData.append('use_oauth', isOauthActive ? 'true' : 'false');
// If using OAuth, also include client credentials if available
if (isOauthActive) {
const clientId = clientIdInput.value.trim();
const clientSecret = clientSecretInput.value.trim();
if (clientId) formData.append('client_id', clientId);
if (clientSecret) formData.append('client_secret', clientSecret);
}
// Send the folder ID to be saved server-side
fetch('/api/google-drive/save-settings', {
method: 'POST',
@@ -531,7 +531,7 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Folder ID saved successfully');
// Make the token status visible if it was hidden
tokenStatus.classList.remove('hidden');
// Update environment variables display if they exist
updateEnvVarsDisplay();
}
@@ -540,24 +540,24 @@ document.addEventListener('DOMContentLoaded', function() {
console.error('Error saving folder ID:', error);
});
}
// Function to update environment variables display
function updateEnvVarsDisplay() {
const folderId = folderIdInput.value || saFolderIdInput.value || 'YOUR_FOLDER_ID';
// Update OAuth env vars if the element exists
const oauthEnvVarsCode = document.querySelector('#oauth-env-vars code');
if (oauthEnvVarsCode) {
const clientId = clientIdInput.value || 'YOUR_CLIENT_ID';
const clientSecret = clientSecretInput.value ? 'YOUR_CLIENT_SECRET' : 'YOUR_CLIENT_SECRET';
oauthEnvVarsCode.textContent = `GOOGLE_DRIVE_USE_OAUTH=true
GOOGLE_DRIVE_CLIENT_ID=${clientId}
GOOGLE_DRIVE_CLIENT_SECRET=${clientSecret}
GOOGLE_DRIVE_REFRESH_TOKEN=YOUR_REFRESH_TOKEN
GOOGLE_DRIVE_FOLDER_ID=${folderId}`;
}
// Update Service Account env vars if the element exists
const saEnvVarsCode = document.querySelector('#sa-env-vars code');
if (saEnvVarsCode) {
@@ -571,7 +571,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
function showModal(status, title, message) {
modalTitle.textContent = title;
modalMessage.textContent = message;
// Set the appropriate icon
if (status === 'success') {
modalIcon.innerHTML = `
@@ -588,14 +588,14 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
`;
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4';
}
resultModal.classList.remove('hidden');
}
function hideModal() {
resultModal.classList.add('hidden');
}
// Close modal when clicking the close button or outside
modalClose.addEventListener('click', hideModal);
resultModal.addEventListener('click', function(e) {
@@ -603,12 +603,12 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
hideModal();
}
});
// Attach Google Picker button event listener
if (selectFolderBtn) {
selectFolderBtn.addEventListener('click', createPicker);
}
// Tab switching
oauthTabBtn.addEventListener('click', function() {
oauthTabBtn.className = 'px-4 py-2 rounded-md text-white bg-blue-600';
@@ -618,7 +618,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
oauthEnvSection.classList.remove('hidden');
saEnvSection.classList.add('hidden');
});
saTabBtn.addEventListener('click', function() {
saTabBtn.className = 'px-4 py-2 rounded-md text-white bg-blue-600';
oauthTabBtn.className = 'px-4 py-2 rounded-md text-gray-700 bg-gray-200';
@@ -627,39 +627,39 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
saEnvSection.classList.remove('hidden');
oauthEnvSection.classList.add('hidden');
});
// Sync folder IDs between tabs
folderIdInput.addEventListener('input', function() {
if (saFolderIdInput) {
saFolderIdInput.value = folderIdInput.value;
}
});
if (saFolderIdInput) {
saFolderIdInput.addEventListener('input', function() {
folderIdInput.value = saFolderIdInput.value;
});
}
// Start OAuth flow button
if (startOauthFlowBtn) {
startOauthFlowBtn.addEventListener('click', function() {
const clientId = clientIdInput.value.trim();
const clientSecret = clientSecretInput.value.trim();
const folderId = folderIdInput.value.trim();
if (!clientId) {
showModal('error', 'Validation Error', 'Please enter your Client ID');
return;
}
if (!clientSecret) {
showModal('error', 'Validation Error', 'Please enter your Client Secret');
return;
}
// Don't require folder ID, make it optional
// Save values to session storage for use after redirect
sessionStorage.setItem('google_drive_client_id', clientId);
sessionStorage.setItem('google_drive_client_secret', clientSecret);
@@ -667,35 +667,35 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
sessionStorage.setItem('google_drive_folder_id', folderId);
}
sessionStorage.setItem('google_drive_use_oauth', 'true');
// Create redirect URI
const redirectUri = `${window.location.origin}/google-drive-callback`;
// Redirect to auth start endpoint
window.location.href = `/google-drive-auth-start?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
});
}
// Save service account settings button
if (saveSaSettingsBtn) {
saveSaSettingsBtn.addEventListener('click', function() {
const folderId = saFolderIdInput.value.trim();
if (!folderId) {
showModal('error', 'Validation Error', 'Please enter your Google Drive Folder ID');
return;
}
// Prepare form data
const formData = new FormData();
formData.append('folder_id', folderId);
formData.append('use_oauth', 'false');
// Send update request
const originalText = saveSaSettingsBtn.textContent;
saveSaSettingsBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Saving...';
saveSaSettingsBtn.disabled = true;
fetch('/api/google-drive/save-settings', {
method: 'POST',
body: formData
@@ -712,7 +712,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
if (data.status === 'success') {
showModal('success', 'Settings Saved', 'Google Drive service account settings have been saved');
tokenStatus.classList.remove('hidden');
// Update environment variables display
const saEnvVarsCode = document.querySelector('#sa-env-vars code');
if (saEnvVarsCode) {
@@ -733,14 +733,14 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
});
});
}
// Test connection button
if (testConnectionBtn) {
testConnectionBtn.addEventListener('click', function() {
const originalText = testConnectionBtn.textContent;
testConnectionBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Testing...';
testConnectionBtn.disabled = true;
fetch('/api/google-drive/test-token')
.then(response => response.json())
.then(data => {
@@ -763,32 +763,32 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
});
});
}
// Refresh token button
if (refreshTokenBtn) {
refreshTokenBtn.addEventListener('click', function() {
showModal('info', 'Confirm', 'This will start a new OAuth flow to obtain a fresh token. Continue?');
modalClose.textContent = "Cancel";
// Add a confirm button
const confirmBtn = document.createElement('button');
confirmBtn.className = 'ml-2 px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-300';
confirmBtn.textContent = 'Continue';
confirmBtn.addEventListener('click', function() {
hideModal();
// Check if we have necessary info before starting flow
if (!clientIdInput.value.trim() || !clientSecretInput.value.trim()) {
showModal('error', 'Missing Information', 'Please enter your Client ID and Client Secret first');
return;
}
startOauthFlowBtn.click();
});
// Add to modal
modalClose.parentNode.appendChild(confirmBtn);
// Clean up modal when closed
const onModalClose = function() {
if (confirmBtn.parentNode) {
@@ -797,15 +797,15 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
modalClose.textContent = "Close";
resultModal.removeEventListener('hidden', onModalClose);
};
resultModal.addEventListener('hidden', onModalClose);
});
}
// Copy environment variables buttons
const copyOAuthEnvVarsBtn = document.getElementById('copy-oauth-env-vars');
const copySAEnvVarsBtn = document.getElementById('copy-sa-env-vars');
if (copyOAuthEnvVarsBtn) {
copyOAuthEnvVarsBtn.addEventListener('click', function() {
const envVarsText = document.getElementById('oauth-env-vars').textContent;
@@ -823,7 +823,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId}
});
});
}
if (copySAEnvVarsBtn) {
copySAEnvVarsBtn.addEventListener('click', function() {
const envVarsText = document.getElementById('sa-env-vars').textContent;
+57 -57
View File
@@ -11,15 +11,15 @@
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
<p class="text-gray-600 mt-2">Please wait while we complete the Google Drive authorization process...</p>
</div>
<div class="flex justify-center my-6">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
<div id="processing-message" class="text-center text-gray-700">
<p>Exchanging authorization code for refresh token...</p>
</div>
<div id="error-container" class="hidden mt-6">
<div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
@@ -42,7 +42,7 @@
</a>
</div>
</div>
<div id="folder-selection-container" class="hidden mt-6">
<div class="rounded-md bg-blue-50 p-4 mb-6">
<div class="flex">
@@ -58,7 +58,7 @@
</div>
</div>
</div>
<div class="space-y-4 mb-6">
<div>
<label for="folder-id-input" class="block text-sm font-medium text-gray-700">Google Drive Folder ID</label>
@@ -75,7 +75,7 @@
You can paste a folder ID directly or use the selector to pick a folder. Root folder is "root".
</p>
</div>
<div>
<button id="save-folder-btn" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Save Settings
@@ -83,7 +83,7 @@
</div>
</div>
</div>
<div id="success-container" class="hidden mt-6">
<div class="rounded-md bg-green-50 p-4">
<div class="flex">
@@ -99,26 +99,26 @@
</div>
</div>
</div>
<div class="mt-6 p-4 bg-gray-100 rounded-md">
<h3 class="font-medium text-lg mb-2">Configuration for Worker Nodes</h3>
<p class="text-sm text-gray-600 mb-3">
Copy these environment variables to configure all worker nodes:
</p>
<div class="relative">
<pre id="env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code></code></pre>
<button id="copy-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
Copy
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Add these variables to your .env file or environment configuration.
</p>
</div>
<div class="mt-4 text-center">
<a href="/status" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Go to Status Page
@@ -133,34 +133,34 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
const code = "{{ code }}";
// Get credentials from session storage
const clientId = sessionStorage.getItem('google_drive_client_id');
const clientSecret = sessionStorage.getItem('google_drive_client_secret');
const folderId = sessionStorage.getItem('google_drive_folder_id');
const redirectUri = window.location.origin + "/google-drive-callback";
let accessToken = null;
let refreshToken = null;
// Define folderIdInput at the top level so it's accessible throughout the script
const folderIdInput = document.getElementById('folder-id-input');
const folderSelectBtn = document.getElementById('folder-select-picker-btn');
const saveFolderBtn = document.getElementById('save-folder-btn');
// Automatically exchange the code for a refresh token
if (code) {
if (!clientId || !clientSecret) {
showError("Missing Client ID or Client Secret. Please go back to the setup page and try again.");
return;
}
exchangeCode(code, clientId, clientSecret, redirectUri, folderId);
} else {
showError("No authorization code was found in the URL");
}
function exchangeCode(code, clientId, clientSecret, redirectUri, folderId) {
const formData = new FormData();
formData.append('client_id', clientId);
@@ -170,10 +170,10 @@ document.addEventListener('DOMContentLoaded', function() {
if (folderId) {
formData.append('folder_id', folderId);
}
document.getElementById('processing-message').innerHTML =
document.getElementById('processing-message').innerHTML =
'<p>Exchanging authorization code for refresh token...</p>';
fetch('/api/google-drive/exchange-token', {
method: 'POST',
body: formData
@@ -191,11 +191,11 @@ document.addEventListener('DOMContentLoaded', function() {
// Store tokens
refreshToken = data.refresh_token;
accessToken = data.access_token;
// Update settings in memory first
const updateFormData = new FormData();
updateFormData.append('refresh_token', data.refresh_token);
// Use the client ID and client secret from session storage
updateFormData.append('client_id', clientId);
updateFormData.append('client_secret', clientSecret);
@@ -203,10 +203,10 @@ document.addEventListener('DOMContentLoaded', function() {
updateFormData.append('folder_id', folderId);
}
updateFormData.append('use_oauth', 'true');
document.getElementById('processing-message').innerHTML =
document.getElementById('processing-message').innerHTML =
'<p>Updating system settings with new token...</p>';
return fetch('/api/google-drive/update-settings', {
method: 'POST',
body: updateFormData
@@ -225,10 +225,10 @@ document.addEventListener('DOMContentLoaded', function() {
// Show folder selection UI
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('folder-selection-container').classList.remove('hidden');
// Initialize Google Picker for folder selection
loadGooglePicker(accessToken, clientId);
return null; // Return null to avoid further .then() processing
}
});
@@ -246,17 +246,17 @@ document.addEventListener('DOMContentLoaded', function() {
showError(error.message);
});
}
function showError(message) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('folder-selection-container').classList.add('hidden');
document.getElementById('error-container').classList.remove('hidden');
document.getElementById('error-message').innerText = message;
// Hide the spinner when showing error
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
}
function saveSettings(refreshToken, clientId, clientSecret, folderId) {
const saveFormData = new FormData();
saveFormData.append('refresh_token', refreshToken);
@@ -266,10 +266,10 @@ document.addEventListener('DOMContentLoaded', function() {
saveFormData.append('folder_id', folderId);
}
saveFormData.append('use_oauth', 'true');
document.getElementById('processing-message').innerHTML =
document.getElementById('processing-message').innerHTML =
'<p>Saving settings to configuration...</p>';
return fetch('/api/google-drive/save-settings', {
method: 'POST',
body: saveFormData
@@ -300,15 +300,15 @@ document.addEventListener('DOMContentLoaded', function() {
};
});
}
function showSuccess(refreshToken, clientId, clientSecret, folderId, inMemoryOnly) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('folder-selection-container').classList.add('hidden');
document.getElementById('success-container').classList.remove('hidden');
// Hide the spinner when showing success
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
// Update the environment variables pre block with the new token
const envVarsCode = document.querySelector('#env-vars code');
if (envVarsCode) {
@@ -318,7 +318,7 @@ GOOGLE_DRIVE_CLIENT_SECRET=${clientSecret}
GOOGLE_DRIVE_REFRESH_TOKEN=${refreshToken}
GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
}
// If settings were only saved in memory, add a warning
if (inMemoryOnly) {
const successContainer = document.getElementById('success-container');
@@ -333,18 +333,18 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
</div>
<div class="ml-3">
<p class="text-sm font-medium text-yellow-800">
Note: Settings were saved in memory only. The .env file could not be updated.
Note: Settings were saved in memory only. The .env file could not be updated.
Make sure to add these environment variables to your configuration files manually.
</p>
</div>
</div>
`;
// Insert warning after the success message but before the environment vars section
const envVarsSection = document.querySelector('#success-container .mt-6');
successContainer.insertBefore(warningDiv, envVarsSection);
}
// Add copy functionality
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
if (copyEnvVarsBtn) {
@@ -366,18 +366,18 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
});
});
}
// Clear session storage
sessionStorage.removeItem('google_drive_client_id');
sessionStorage.removeItem('google_drive_client_secret');
sessionStorage.removeItem('google_drive_folder_id');
// In 10 seconds, redirect to status page
setTimeout(() => {
window.location.href = '/status';
}, 10000);
}
// Handle folder selection UI
if (saveFolderBtn) {
saveFolderBtn.addEventListener('click', function() {
@@ -385,11 +385,11 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
alert('Error: Folder input element not found');
return;
}
const folderId = folderIdInput.value.trim() || 'root';
saveFolderBtn.disabled = true;
saveFolderBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Saving...';
saveSettings(refreshToken, clientId, clientSecret, folderId)
.then(result => {
showSuccess(result.refresh_token, result.client_id, result.client_secret, result.folderId);
@@ -401,7 +401,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
});
});
}
// Function to load and initialize the Google Picker
function loadGooglePicker(accessToken, clientId) {
// Load the Google API Loader script
@@ -414,13 +414,13 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
};
document.body.appendChild(script);
}
// Initialize and setup the Google Picker
function initGooglePicker(accessToken, clientId) {
if (!accessToken || !clientId || !folderSelectBtn) {
return;
}
// Setup the click handler for the folder select button
folderSelectBtn.addEventListener('click', function() {
// Create the folder picker view
@@ -428,7 +428,7 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
.setIncludeFolders(true)
.setSelectFolderEnabled(true)
.setMode(google.picker.DocsViewMode.LIST); // Use LIST mode to work with the drive.file scope
// Create and render the picker
const picker = new google.picker.PickerBuilder()
.addView(folderView)
@@ -437,35 +437,35 @@ GOOGLE_DRIVE_FOLDER_ID=${folderId || 'YOUR_FOLDER_ID'}`;
.setTitle('Select a folder for DocuElevate')
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
});
}
// Callback function for picker
function pickerCallback(data) {
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
const folder = data[google.picker.Response.DOCUMENTS][0];
const folderId = folder[google.picker.Document.ID];
const folderName = folder[google.picker.Document.NAME];
// Update the folder ID input
if (folderIdInput) {
folderIdInput.value = folderId;
// Add visual confirmation instead of alert
const confirmationMsg = document.createElement('div');
confirmationMsg.className = 'mt-2 text-sm text-green-600';
confirmationMsg.innerHTML = `<svg class="inline-block h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg> Selected folder: "${folderName}"`;
// Remove previous confirmation if it exists
const existingConfirmation = folderIdInput.parentNode.querySelector('.text-green-600');
if (existingConfirmation) {
existingConfirmation.remove();
}
// Insert the confirmation message after the input field
folderIdInput.parentNode.appendChild(confirmationMsg);
}
@@ -11,7 +11,7 @@
<h2 class="text-2xl font-bold mt-4">Authorization Failed</h2>
<p class="text-gray-600 mt-2">Sorry, we couldn't complete the Google Drive authorization.</p>
</div>
<div class="mb-6">
<div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
@@ -29,7 +29,7 @@
</div>
</div>
</div>
<div class="mt-6">
<a href="/google-drive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Return to Setup
+2 -2
View File
@@ -24,7 +24,7 @@
Website: www.docuelevate.com
</p>
</div>
<div class="bg-white shadow rounded-lg p-6 mb-6">
<h2 class="text-2xl font-semibold mb-4">Business Registration</h2>
<p class="text-gray-700 mb-3">
@@ -56,7 +56,7 @@
<div class="bg-white shadow rounded-lg p-6 mb-6">
<h2 class="text-2xl font-semibold mb-4">Online Dispute Resolution</h2>
<p class="text-gray-700 mb-3">
The European Commission provides a platform for online dispute resolution (OS):
The European Commission provides a platform for online dispute resolution (OS):
<a href="https://ec.europa.eu/consumers/odr/" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:underline">https://ec.europa.eu/consumers/odr/</a>
</p>
<p class="text-gray-700 mb-3">
+8 -8
View File
@@ -5,31 +5,31 @@
{% block content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-4">License Information</h1>
<div class="bg-white shadow-md rounded-lg p-6 mb-6">
<h2 class="text-xl font-semibold mb-4">Apache License 2.0</h2>
<div class="prose">
<pre class="whitespace-pre-wrap text-sm font-mono bg-gray-50 p-4 rounded border overflow-auto max-h-96 mb-4">{{ license_text }}</pre>
</div>
<p class="mt-4 text-gray-600">
DocuElevate is distributed under the Apache License 2.0, which is a permissive
open-source software license that allows you to use, modify, distribute, and
contribute to the project.
</p>
</div>
<div class="bg-white shadow-md rounded-lg p-6">
<h2 class="text-xl font-semibold mb-4">Related Information</h2>
<p class="mb-3 text-gray-600">
While this license governs the use of our software, please also review our
<a href="/terms" class="text-blue-600 hover:underline">Terms of Service</a> and
<a href="/privacy" class="text-blue-600 hover:underline">Privacy Policy</a> for
While this license governs the use of our software, please also review our
<a href="/terms" class="text-blue-600 hover:underline">Terms of Service</a> and
<a href="/privacy" class="text-blue-600 hover:underline">Privacy Policy</a> for
information about using the DocuElevate service.
</p>
<p class="text-gray-600">
For more information about DocuElevate, please visit the
For more information about DocuElevate, please visit the
<a href="/about" class="text-blue-600 hover:underline">About page</a>.
</p>
</div>
+9 -9
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocuElevate - Login</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=="
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-gray-100 h-screen flex items-center justify-center">
@@ -16,7 +16,7 @@
</div>
<h1 class="text-2xl font-bold text-center text-gray-800 mb-6">Welcome to DocuElevate</h1>
{% if error %}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6" role="alert">
<p>{{ error }}</p>
@@ -35,16 +35,16 @@
<form method="POST" action="/auth" class="space-y-4">
<div>
<label for="username" class="block text-sm font-medium text-gray-700">Username</label>
<input type="text" id="username" name="username" required
<input type="text" id="username" name="username" 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>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
<input type="password" id="password" name="password" required
<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
</button>
@@ -63,7 +63,7 @@
</div>
<div class="mt-6 grid grid-cols-1 gap-3">
<a href="/oauth-login"
<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>
<i class="fas fa-lock mr-2"></i>
@@ -71,7 +71,7 @@
</a>
</div>
{% endif %}
<div class="mt-8 text-center">
<a href="/" class="text-sm font-medium text-blue-600 hover:text-blue-500">
Return to Home
+39 -39
View File
@@ -8,10 +8,10 @@
<p class="text-gray-600 mb-4">
Configure the Microsoft OneDrive integration for DocuElevate using our setup wizard.
</p>
<div class="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 my-4" role="alert">
<p class="font-bold">Current Status:</p>
<p>OneDrive integration is
<p>OneDrive integration is
{% if is_configured %}
<span class="text-green-700 font-semibold">configured</span>.
{% else %}
@@ -22,7 +22,7 @@
<p class="mt-2"><strong>Target folder:</strong> {{ folder_path }}</p>
{% endif %}
</div>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 my-4">
<div class="flex">
<div class="flex-shrink-0">
@@ -41,7 +41,7 @@
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
<h2 class="text-2xl font-semibold mb-4">Quick Setup Guide</h2>
<div class="mb-6">
<h3 class="text-xl font-medium mb-4">Step 1: Register an Azure Application</h3>
<ol class="list-decimal ml-6 space-y-3">
@@ -94,36 +94,36 @@
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
<h2 class="text-2xl font-semibold mb-4">Complete Setup with Wizard</h2>
<div class="space-y-4">
<div>
<label for="client-id" class="block text-sm font-medium text-gray-700">Client ID</label>
<input type="text" id="client-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Azure AD application client ID" value="{{ client_id_value }}">
</div>
<div>
<label for="client-secret" class="block text-sm font-medium text-gray-700">Client Secret</label>
<input type="password" id="client-secret" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Azure AD application client secret" value="{{ client_secret_value }}">
</div>
<div>
<label for="tenant-id" class="block text-sm font-medium text-gray-700">Tenant ID (Optional)</label>
<input type="text" id="tenant-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="common" value="{{ tenant_id }}">
<p class="text-xs text-gray-500 mt-1">Use "common" for personal accounts or your organization's Tenant ID for corporate accounts</p>
</div>
<div>
<label for="folder-path" class="block text-sm font-medium text-gray-700">Folder Path (Optional)</label>
<input type="text" id="folder-path" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Documents/Uploads" value="{{ folder_path }}">
<p class="text-xs text-gray-500 mt-1">Enter the folder path where files should be uploaded (e.g., Documents/Uploads)</p>
</div>
<div>
<button id="start-auth-flow" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Start Authentication Flow
</button>
</div>
<!-- Token validation and status -->
<div id="token-status" class="mt-6 {{ 'hidden' if not is_configured else '' }}">
<div class="rounded-md {{ 'bg-green-50' if is_configured else 'bg-yellow-50' }} p-4">
@@ -150,7 +150,7 @@
</div>
</div>
</div>
<div class="mt-4 flex space-x-3">
<button id="test-token" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Test Token
@@ -159,26 +159,26 @@
Refresh Token
</button>
</div>
<!-- Configuration for Worker Nodes section -->
<div class="mt-6 p-4 bg-gray-100 rounded-md">
<h3 class="font-medium text-lg mb-2">Configuration for Worker Nodes</h3>
<p class="text-sm text-gray-600 mb-3">
Copy these environment variables to configure all worker nodes:
</p>
<div class="relative">
<pre id="env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code>ONEDRIVE_CLIENT_ID={{ client_id_value }}
ONEDRIVE_CLIENT_SECRET={{ client_secret_value|default('YOUR_CLIENT_SECRET', true) }}
ONEDRIVE_TENANT_ID={{ tenant_id }}
ONEDRIVE_REFRESH_TOKEN={{ refresh_token_value|default('YOUR_REFRESH_TOKEN', true) }}
ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code></pre>
<button id="copy-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
Copy
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Add these variables to your .env file or environment configuration.
</p>
@@ -209,7 +209,7 @@ ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code>
Back to Status
</a>
</div>
<!-- Result Modal -->
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
@@ -243,19 +243,19 @@ document.addEventListener('DOMContentLoaded', function() {
const refreshTokenBtn = document.getElementById('refresh-token-btn');
const tokenStatus = document.getElementById('token-status');
const clientSecretInput = document.getElementById('client-secret');
// Modal elements
const resultModal = document.getElementById('resultModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalClose = document.getElementById('modalClose');
// Modal functions
function showModal(status, title, message) {
modalTitle.textContent = title;
modalMessage.textContent = message;
// Set the appropriate icon
if (status === 'success') {
modalIcon.innerHTML = `
@@ -272,24 +272,24 @@ document.addEventListener('DOMContentLoaded', function() {
`;
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4';
}
resultModal.classList.remove('hidden');
}
function hideModal() {
resultModal.classList.add('hidden');
}
// Close modal when clicking the close button
modalClose.addEventListener('click', hideModal);
// Close modal when clicking outside of it
resultModal.addEventListener('click', function(e) {
if (e.target === resultModal) {
hideModal();
}
});
// Start Authentication Flow button click
startAuthFlowBtn.addEventListener('click', function() {
const clientId = document.getElementById('client-id').value.trim();
@@ -302,7 +302,7 @@ document.addEventListener('DOMContentLoaded', function() {
showModal('error', 'Validation Error', 'Please enter your Client ID');
return;
}
if (!clientSecret) {
showModal('error', 'Validation Error', 'Please enter your Client Secret');
return;
@@ -312,14 +312,14 @@ document.addEventListener('DOMContentLoaded', function() {
sessionStorage.setItem('onedrive_client_id', clientId);
sessionStorage.setItem('onedrive_client_secret', clientSecret);
sessionStorage.setItem('onedrive_tenant_id', tenantId);
if (folderPath) {
sessionStorage.setItem('onedrive_folder_path', folderPath);
}
// Generate the authorization URL with .default scope
const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?client_id=${encodeURIComponent(clientId)}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&response_mode=query&scope=${encodeURIComponent('https://graph.microsoft.com/.default offline_access')}&prompt=consent`;
// Redirect the user to the Microsoft login page
window.location.href = authUrl;
});
@@ -329,7 +329,7 @@ document.addEventListener('DOMContentLoaded', function() {
testTokenBtn.addEventListener('click', function() {
testTokenBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Testing...';
testTokenBtn.disabled = true;
fetch('/api/onedrive/test-token')
.then(response => response.json())
.then(data => {
@@ -360,13 +360,13 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
}
// Refresh Token button click
if (refreshTokenBtn) {
refreshTokenBtn.addEventListener('click', function() {
showModal('info', 'Confirm', 'This will start a new authentication flow to obtain a fresh token from Microsoft. Continue?');
modalClose.textContent = "Cancel";
// Add a confirm button
const confirmBtn = document.createElement('button');
confirmBtn.className = 'ml-2 px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-300';
@@ -375,10 +375,10 @@ document.addEventListener('DOMContentLoaded', function() {
hideModal();
startAuthFlowBtn.click();
});
// Add to modal
modalClose.parentNode.appendChild(confirmBtn);
// Make sure to remove the confirm button when modal is closed
const removeConfirmBtn = function() {
if (confirmBtn.parentNode) {
@@ -387,11 +387,11 @@ document.addEventListener('DOMContentLoaded', function() {
modalClose.textContent = "Close";
modalClose.removeEventListener('click', removeConfirmBtn);
};
modalClose.addEventListener('click', removeConfirmBtn, { once: true });
});
}
// Copy Environment Variables Button
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
if (copyEnvVarsBtn) {
@@ -413,30 +413,30 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
}
// Try to retrieve values from session storage (if coming back from auth or browser refresh)
if (clientSecretInput && !clientSecretInput.value && sessionStorage.getItem('onedrive_client_secret')) {
clientSecretInput.value = sessionStorage.getItem('onedrive_client_secret');
}
// Also check for client ID in session storage
const clientIdInput = document.getElementById('client-id');
if (clientIdInput && !clientIdInput.value && sessionStorage.getItem('onedrive_client_id')) {
clientIdInput.value = sessionStorage.getItem('onedrive_client_id');
}
// Check for tenant ID in session storage
const tenantIdInput = document.getElementById('tenant-id');
if (tenantIdInput && !tenantIdInput.value && sessionStorage.getItem('onedrive_tenant_id')) {
tenantIdInput.value = sessionStorage.getItem('onedrive_tenant_id');
}
// Check for folder path in session storage
const folderPathInput = document.getElementById('folder-path');
if (folderPathInput && !folderPathInput.value && sessionStorage.getItem('onedrive_folder_path')) {
folderPathInput.value = sessionStorage.getItem('onedrive_folder_path');
}
// If token is not configured but we have a client ID, show the token status section
if (document.getElementById('client-id').value && !tokenStatus.classList.contains('hidden')) {
tokenStatus.classList.remove('hidden');
+30 -30
View File
@@ -11,15 +11,15 @@
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
<p class="text-gray-600 mt-2">Please wait while we complete the OneDrive authorization process...</p>
</div>
<div class="flex justify-center my-6">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
<div id="processing-message" class="text-center text-gray-700">
<p>Exchanging authorization code for refresh token...</p>
</div>
<div id="error-container" class="hidden mt-6">
<div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
@@ -42,7 +42,7 @@
</a>
</div>
</div>
<div id="success-container" class="hidden mt-6">
<div class="rounded-md bg-green-50 p-4">
<div class="flex">
@@ -58,26 +58,26 @@
</div>
</div>
</div>
<div class="mt-6 p-4 bg-gray-100 rounded-md">
<h3 class="font-medium text-lg mb-2">Configuration for Worker Nodes</h3>
<p class="text-sm text-gray-600 mb-3">
Copy these environment variables to configure all worker nodes:
</p>
<div class="relative">
<pre id="env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code></code></pre>
<button id="copy-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
Copy
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
Add these variables to your .env file or environment configuration.
</p>
</div>
<div class="mt-4 text-center">
<a href="/status" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Go to Status Page
@@ -92,27 +92,27 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
const code = "{{ code }}";
// Get credentials from session storage (these take precedence over server-provided values)
const clientId = sessionStorage.getItem('onedrive_client_id') || "{{ client_id_value }}";
const clientSecret = sessionStorage.getItem('onedrive_client_secret') || "{{ client_secret_value }}";
const tenantId = sessionStorage.getItem('onedrive_tenant_id') || "{{ tenant_id }}" || "common";
const folderPath = sessionStorage.getItem('onedrive_folder_path') || "";
const redirectUri = window.location.origin + "/onedrive-callback";
// Automatically exchange the code for a refresh token
if (code) {
if (!clientId || !clientSecret) {
showError("Missing Client ID or Client Secret. Please go back to the setup page and try again.");
return;
}
exchangeCode(code, clientId, clientSecret, redirectUri, tenantId, folderPath);
} else {
showError("No authorization code was found in the URL");
}
function exchangeCode(code, clientId, clientSecret, redirectUri, tenantId, folderPath) {
const formData = new FormData();
formData.append('client_id', clientId);
@@ -120,12 +120,12 @@ document.addEventListener('DOMContentLoaded', function() {
formData.append('redirect_uri', redirectUri);
formData.append('code', code);
formData.append('tenant_id', tenantId);
// Show more details in processing message
document.getElementById('processing-message').innerHTML =
document.getElementById('processing-message').innerHTML =
'<p>Exchanging authorization code for refresh token...</p>' +
'<p class="text-xs text-gray-500 mt-2">Using tenant: ' + (tenantId || 'common') + '</p>';
fetch('/api/onedrive/exchange-token', {
method: 'POST',
body: formData
@@ -143,19 +143,19 @@ document.addEventListener('DOMContentLoaded', function() {
// Instead of saving to .env file, update settings in memory
const updateFormData = new FormData();
updateFormData.append('refresh_token', data.refresh_token);
// Use the values from session storage
updateFormData.append('client_id', clientId);
updateFormData.append('client_secret', clientSecret);
updateFormData.append('tenant_id', tenantId);
if (folderPath) {
updateFormData.append('folder_path', folderPath);
}
document.getElementById('processing-message').innerHTML =
document.getElementById('processing-message').innerHTML =
'<p>Updating system settings with new token...</p>';
return fetch('/api/onedrive/update-settings', {
method: 'POST',
body: updateFormData
@@ -169,12 +169,12 @@ document.addEventListener('DOMContentLoaded', function() {
}).then(() => {
// Show the success message and environment variables
showSuccess(data.refresh_token, clientId, clientSecret, tenantId, folderPath);
// In 10 seconds, redirect to status page (giving more time to copy)
setTimeout(() => {
window.location.href = '/status';
}, 10000);
// Clean up session storage
sessionStorage.removeItem('onedrive_client_id');
sessionStorage.removeItem('onedrive_client_secret');
@@ -189,23 +189,23 @@ document.addEventListener('DOMContentLoaded', function() {
showError(error.message);
});
}
function showError(message) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('error-container').classList.remove('hidden');
document.getElementById('error-message').innerText = message;
// Hide the spinner when showing error
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
}
function showSuccess(refreshToken, clientId, clientSecret, tenantId, folderPath) {
document.getElementById('processing-message').classList.add('hidden');
document.getElementById('success-container').classList.remove('hidden');
// Hide the spinner when showing success
document.querySelector('.animate-spin').parentNode.classList.add('hidden');
// Update the environment variables pre block with the new token
const envVarsCode = document.querySelector('#env-vars code');
if (envVarsCode) {
@@ -215,7 +215,7 @@ ONEDRIVE_TENANT_ID=${tenantId || 'common'}
ONEDRIVE_REFRESH_TOKEN=${refreshToken}
ONEDRIVE_FOLDER_PATH=${folderPath || 'Documents/Uploads'}`;
}
// Add copy functionality
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
if (copyEnvVarsBtn) {
@@ -11,7 +11,7 @@
<h2 class="text-2xl font-bold mt-4">Authorization Failed</h2>
<p class="text-gray-600 mt-2">Sorry, we couldn't complete the OneDrive authorization.</p>
</div>
<div class="mb-6">
<div class="bg-red-50 border-l-4 border-red-400 p-4">
<div class="flex">
@@ -29,7 +29,7 @@
</div>
</div>
</div>
<div class="mt-6">
<a href="/onedrive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Return to Setup
+13 -13
View File
@@ -40,7 +40,7 @@
<!-- Alert Messages -->
<div x-show="showAlert" x-transition class="mb-4">
<div :class="alertType === 'success' ? 'bg-green-100 border-green-500 text-green-700' : 'bg-red-100 border-red-500 text-red-700'"
<div :class="alertType === 'success' ? 'bg-green-100 border-green-500 text-green-700' : 'bg-red-100 border-red-500 text-red-700'"
class="border-l-4 p-4" role="alert">
<p class="font-bold" x-text="alertTitle"></p>
<p x-text="alertMessage"></p>
@@ -85,7 +85,7 @@
{{ setting.source_label }}
</span>
</div>
<p class="text-xs text-gray-500 mb-2">
{{ setting.metadata.description }}
</p>
@@ -93,9 +93,9 @@
{% if setting.metadata.type == 'boolean' %}
<!-- Boolean/Checkbox Input -->
<div class="flex items-center">
<input
type="checkbox"
id="{{ setting.key }}"
<input
type="checkbox"
id="{{ setting.key }}"
name="{{ setting.key }}"
:checked="formData['{{ setting.key }}'] === 'true' || formData['{{ setting.key }}'] === true"
@change="formData['{{ setting.key }}'] = $event.target.checked ? 'true' : 'false'"
@@ -111,9 +111,9 @@
{% if setting.metadata.sensitive %}
<!-- Sensitive Field with Show/Hide Toggle -->
<div class="relative">
<input
<input
:type="showPassword['{{ setting.key }}'] ? 'text' : 'password'"
id="{{ setting.key }}"
id="{{ setting.key }}"
name="{{ setting.key }}"
x-model="formData['{{ setting.key }}']"
class="setting-input w-full px-3 py-2 pr-24 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
@@ -126,7 +126,7 @@
<i class="fas fa-lock"></i>
</span>
<!-- Show/Hide Toggle -->
<button
<button
type="button"
@click="togglePassword('{{ setting.key }}')"
class="text-gray-400 hover:text-gray-600 focus:outline-none"
@@ -138,9 +138,9 @@
</div>
{% else %}
<!-- Non-Sensitive Field -->
<input
type="text"
id="{{ setting.key }}"
<input
type="text"
id="{{ setting.key }}"
name="{{ setting.key }}"
x-model="formData['{{ setting.key }}']"
class="setting-input w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
@@ -159,14 +159,14 @@
<!-- Action Buttons -->
<div class="flex justify-end space-x-4 mt-6">
<button
<button
type="button"
@click="resetForm"
class="px-6 py-2 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Reset
</button>
<button
<button
type="submit"
:disabled="saving"
class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
+14 -14
View File
@@ -23,7 +23,7 @@
{% block content %}
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-3xl mx-auto">
<!-- Wizard Header -->
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-900 mb-2">
@@ -44,7 +44,7 @@
<div class="w-full bg-gray-200 rounded-full h-3">
<div class="bg-indigo-600 h-3 rounded-full transition-all duration-500" style="width: {{ progress_percent }}%"></div>
</div>
<!-- Step Indicators -->
<div class="flex justify-between mt-4">
{% for step_num in range(1, max_step + 1) %}
@@ -66,7 +66,7 @@
<!-- Wizard Card -->
<div class="bg-white rounded-lg shadow-xl overflow-hidden">
<!-- Card Header -->
<div class="bg-indigo-600 px-6 py-4">
<h2 class="text-2xl font-bold text-white">
@@ -79,7 +79,7 @@
<!-- Card Body -->
<form method="post" action="/setup" class="px-6 py-8">
<input type="hidden" name="step" value="{{ current_step }}">
{% if request.query_params.get('error') == 'save_failed' %}
<div class="mb-6 bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">⚠️ Error</p>
@@ -96,7 +96,7 @@
<span class="text-red-600">*</span>
{% endif %}
</label>
<p class="text-xs text-gray-500 mb-3">
{{ setting.description }}
</p>
@@ -106,8 +106,8 @@
<div class="space-y-2">
<div class="flex items-center space-x-4">
<label class="inline-flex items-center">
<input type="radio" name="session_secret_mode" value="auto" checked
class="form-radio text-indigo-600"
<input type="radio" name="session_secret_mode" value="auto" checked
class="form-radio text-indigo-600"
onchange="document.getElementById('session_secret').value = 'auto-generate'; document.getElementById('session_secret').disabled = true;">
<span class="ml-2 text-sm">Auto-generate (recommended)</span>
</label>
@@ -118,7 +118,7 @@
<span class="ml-2 text-sm">Enter manually</span>
</label>
</div>
<input
<input
type="{% if setting.sensitive %}password{% else %}text{% endif %}"
id="{{ setting.key }}"
name="{{ setting.key }}"
@@ -130,7 +130,7 @@
</div>
{% else %}
<!-- Regular input -->
<input
<input
type="{% if setting.sensitive %}password{% else %}text{% endif %}"
id="{{ setting.key }}"
name="{{ setting.key }}"
@@ -140,7 +140,7 @@
{% if setting.default is none or setting.key in ['admin_password', 'openai_api_key', 'azure_ai_key', 'azure_endpoint'] %}required{% endif %}
/>
{% endif %}
{% if setting.key == 'admin_password' %}
<p class="mt-2 text-xs text-amber-600">
<i class="fas fa-exclamation-triangle"></i>
@@ -166,17 +166,17 @@
</a>
{% endif %}
</div>
<div class="flex space-x-4">
{% if current_step > 1 %}
<a href="/setup?step={{ current_step - 1 }}"
<a href="/setup?step={{ current_step - 1 }}"
class="px-6 py-3 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fas fa-arrow-left mr-2"></i>
Previous
</a>
{% endif %}
<button type="submit"
<button type="submit"
class="px-6 py-3 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 shadow-lg">
{% if current_step < max_step %}
Next Step
+57 -57
View File
@@ -55,7 +55,7 @@
<div class="border-t border-gray-200">
<div class="px-4 py-5 sm:p-6">
<p class="text-sm text-gray-500">{{ provider.description }}</p>
<!-- NextCloud or link to provider URL -->
{% if provider.configured and name == "NextCloud" %}
{% if provider.details and provider.details.url %}
@@ -73,7 +73,7 @@
</a>
</div>
{% endif %}
<div class="mt-4 flex items-center justify-between">
<div>
{% if provider.configured %}
@@ -92,21 +92,21 @@
</span>
{% endif %}
</div>
<!-- Action buttons -->
<div class="flex space-x-2">
{% if provider.configured and provider.details %}
<button
<button
class="view-details-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="{{ name }}"
data-details="{{ provider.details|tojson|forceescape }}">
View Details
</button>
{% endif %}
<!-- Notification Test Button -->
{% if name == "Notifications" and provider.configured %}
<button
<button
id="testNotificationBtn"
class="test-generic-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-endpoint="{{ provider.test_endpoint }}"
@@ -114,21 +114,21 @@
Test Notifications
</button>
{% endif %}
<!-- Generic Test Button for any provider with test_endpoint -->
{% if provider.testable and provider.configured and provider.test_endpoint and name != "Notifications" %}
<button
<button
class="test-generic-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-endpoint="{{ provider.test_endpoint }}"
data-method="{{ provider.test_method|default('GET') }}">
Test {{ name }}
</button>
{% endif %}
<!-- Provider-specific buttons -->
{% if name == "Dropbox" %}
{% if provider.configured %}
<button
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="dropbox">
Test Connection
@@ -144,7 +144,7 @@
{% endif %}
{% elif name == "OneDrive" %}
{% if provider.configured %}
<button
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="onedrive">
Test Connection
@@ -160,7 +160,7 @@
{% endif %}
{% elif name == "Google Drive" %}
{% if provider.configured %}
<button
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="google_drive">
Test Connection
@@ -176,7 +176,7 @@
{% endif %}
{% elif name == "OpenAI" %}
{% if provider.configured %}
<button
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="openai">
Test Connection
@@ -184,7 +184,7 @@
{% endif %}
{% elif name == "Azure AI" %}
{% if provider.configured %}
<button
<button
class="test-provider-btn inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
data-provider="azure">
Test Connection
@@ -217,14 +217,14 @@
<p class="text-sm text-gray-600 mt-1">
For more detailed configuration settings and environment variables, check the environment debug page.
</p>
<div class="mt-4">
<a href="/env" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
View Detailed Configuration
</a>
</div>
</div>
<!-- Result Modal -->
<div id="resultModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-1/2 lg:w-1/3 shadow-lg rounded-md bg-white">
@@ -246,7 +246,7 @@
</div>
</div>
</div>
<!-- Details Modal -->
<div id="detailsModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50" aria-modal="true" role="dialog">
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-2/3 lg:w-1/2 shadow-lg rounded-md bg-white">
@@ -285,19 +285,19 @@ document.addEventListener('DOMContentLoaded', function() {
const modalMessage = document.getElementById('modalMessage');
const modalIcon = document.getElementById('modalIcon');
const modalClose = document.getElementById('modalClose');
// Details Modal elements
const detailsModal = document.getElementById('detailsModal');
const detailsModalTitle = document.getElementById('detailsModalTitle');
const detailsContent = document.getElementById('detailsContent');
const closeDetailsModal = document.getElementById('closeDetailsModal');
const closeDetailsBtn = document.getElementById('closeDetailsBtn');
// Modal functions
function showModal(status, title, message) {
modalTitle.textContent = title;
modalMessage.textContent = message;
// Set the appropriate icon using Font Awesome
if (status === 'success') {
modalIcon.innerHTML = '<i class="fa-solid fa-check text-green-600 fa-2x"></i>';
@@ -306,25 +306,25 @@ document.addEventListener('DOMContentLoaded', function() {
modalIcon.innerHTML = '<i class="fa-solid fa-xmark text-red-600 fa-2x"></i>';
modalIcon.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4';
}
resultModal.classList.remove('hidden');
}
function hideModal() {
resultModal.classList.add('hidden');
}
function showDetailsModal(providerName, details) {
detailsModalTitle.textContent = providerName + ' Configuration Details';
// Clear previous content
detailsContent.innerHTML = '';
// Create and populate the details list
if (details && Object.keys(details).length > 0) {
const table = document.createElement('table');
table.className = 'min-w-full divide-y divide-gray-200';
const thead = document.createElement('thead');
thead.className = 'bg-gray-50';
thead.innerHTML = `
@@ -333,101 +333,101 @@ document.addEventListener('DOMContentLoaded', function() {
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Value</th>
</tr>
`;
const tbody = document.createElement('tbody');
tbody.className = 'bg-white divide-y divide-gray-200';
let count = 0;
for (const [key, value] of Object.entries(details)) {
const row = document.createElement('tr');
row.className = count % 2 === 0 ? 'bg-white' : 'bg-gray-50';
const keyCell = document.createElement('td');
keyCell.className = 'px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900';
keyCell.textContent = key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' ');
const valueCell = document.createElement('td');
valueCell.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-500';
// Check if value contains sensitive information that should be masked
const sensitiveKeys = ['token', 'password', 'secret', 'key', 'credentials'];
const isSensitive = sensitiveKeys.some(sKey => key.toLowerCase().includes(sKey));
if (isSensitive && value !== 'Not set' && value !== '') {
valueCell.textContent = value.slice(4) + '********' + value.slice(-4);
// For better readability, we can also use HTML to mask the middle part of the string
valueCell.innerHTML = value.slice(0, 4) + '<span class="text-gray-400">********</span>' + value.slice(-4);
valueCell.innerHTML = value.slice(0, 4) + '<span class="text-gray-400">********</span>' + value.slice(-4);
} else {
valueCell.textContent = value;
}
row.appendChild(keyCell);
row.appendChild(valueCell);
tbody.appendChild(row);
count++;
}
table.appendChild(thead);
table.appendChild(tbody);
detailsContent.appendChild(table);
} else {
detailsContent.innerHTML = '<p class="text-sm text-gray-500">No details available</p>';
}
detailsModal.classList.remove('hidden');
}
function hideDetailsModal() {
detailsModal.classList.add('hidden');
}
// Close modal when clicking the close button
modalClose.addEventListener('click', hideModal);
// Close modal when clicking outside of it
resultModal.addEventListener('click', function(e) {
if (e.target === resultModal) {
hideModal();
}
});
// Close details modal
closeDetailsModal.addEventListener('click', hideDetailsModal);
closeDetailsBtn.addEventListener('click', hideDetailsModal);
// Close details modal when clicking outside
detailsModal.addEventListener('click', function(e) {
if (e.target === detailsModal) {
hideDetailsModal();
}
});
// View Details button handlers
const detailsButtons = document.querySelectorAll('.view-details-btn');
detailsButtons.forEach(button => {
button.addEventListener('click', function() {
const providerName = this.getAttribute('data-provider');
let detailsData = {};
try {
detailsData = JSON.parse(this.getAttribute('data-details'));
} catch (e) {
console.error('Error parsing details data:', e);
}
showDetailsModal(providerName, detailsData);
});
});
// Test notifications
const testNotificationBtn = document.getElementById('testNotificationBtn');
if (testNotificationBtn) {
testNotificationBtn.addEventListener('click', function() {
const originalText = this.innerHTML;
this.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i> Sending...';
this.disabled = true;
fetch('/api/diagnostic/test-notification', {
method: 'POST',
})
@@ -451,7 +451,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
}
// Generic test button functionality
const testGenericBtns = document.querySelectorAll('.test-generic-btn:not(#testNotificationBtn)');
testGenericBtns.forEach(button => {
@@ -459,10 +459,10 @@ document.addEventListener('DOMContentLoaded', function() {
const originalText = this.innerHTML;
const endpoint = this.getAttribute('data-endpoint');
const method = this.getAttribute('data-method') || 'GET';
this.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i> Testing...';
this.disabled = true;
fetch(endpoint, {
method: method,
})
@@ -475,7 +475,7 @@ document.addEventListener('DOMContentLoaded', function() {
message += `<br><br><div class="bg-blue-50 p-3 rounded mt-2">
<span class="font-medium">Token valid for:</span> ${data.token_info.expires_in_human}
</div>`;
modalTitle.textContent = 'Test Successful';
modalMessage.innerHTML = message;
modalIcon.innerHTML = '<i class="fa-solid fa-check text-green-600 fa-2x"></i>';
@@ -503,17 +503,17 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
});
// Test provider connections
const testButtons = document.querySelectorAll('.test-provider-btn');
testButtons.forEach(button => {
button.addEventListener('click', function() {
const provider = this.getAttribute('data-provider');
const originalText = this.textContent;
this.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i> Testing...';
this.disabled = true;
let endpoint = '';
if (provider === 'dropbox') {
endpoint = '/api/dropbox/test-token';
@@ -526,20 +526,20 @@ document.addEventListener('DOMContentLoaded', function() {
} else if (provider === 'azure') {
endpoint = '/api/azure/test';
}
fetch(endpoint)
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Create successful message
let message = data.message || 'Connection successful';
// Add token expiration info if available (especially for Google Drive)
if (data.token_info && data.token_info.expires_in_human) {
message += `<br><br><div class="bg-blue-50 p-3 rounded mt-2">
<span class="font-medium">Token valid for:</span> ${data.token_info.expires_in_human}
</div>`;
// Show the message with HTML
modalTitle.textContent = 'Connection Test Successful';
modalMessage.innerHTML = message;
+6 -6
View File
@@ -115,12 +115,12 @@
if (e.dataTransfer.files.length) {
// Clear previous upload progress
uploadProgress.innerHTML = "";
// Create progress container
const progressContainer = document.createElement("div");
progressContainer.className = "space-y-2";
uploadProgress.appendChild(progressContainer);
// Use the shared processFiles function
processFiles(e.dataTransfer.files, progressContainer, statusMessage);
}
@@ -130,12 +130,12 @@
if (e.target.files.length) {
// Clear previous upload progress
uploadProgress.innerHTML = "";
// Create progress container
const progressContainer = document.createElement("div");
progressContainer.className = "space-y-2";
uploadProgress.appendChild(progressContainer);
// Use the shared processFiles function
processFiles(e.target.files, progressContainer, statusMessage);
}
@@ -168,7 +168,7 @@
// Show loading state
showUrlStatus("Downloading file from URL...", "info");
const submitButton = urlUploadForm.querySelector('button[type="submit"]');
const originalButtonText = submitButton.textContent;
submitButton.textContent = "Processing...";
@@ -198,7 +198,7 @@
// Clear form
urlInput.value = "";
urlFilename.value = "";
// Optionally redirect to files page after short delay
setTimeout(() => {
window.location.href = "/files";