chore: apply ruff formatting and fix whitespace issues
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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> </label>
|
||||
<button type="submit">Apply Filters</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="filter-item">
|
||||
<label> </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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user