chore: apply ruff formatting and fix whitespace issues
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -7,4 +7,4 @@ name = "python"
|
||||
runtime_version = "3.x.x"
|
||||
|
||||
[[analyzers]]
|
||||
name = "javascript"
|
||||
name = "javascript"
|
||||
|
||||
@@ -150,7 +150,7 @@ PAPERLESS_NGX_API_TOKEN=<PAPERLESS_API_TOKEN>
|
||||
# Optional: JSON mapping of metadata fields to Paperless custom field names
|
||||
# This allows you to map multiple extracted metadata fields to custom fields in Paperless
|
||||
# The mapping format is: {"metadata_field_name": "PaperlessCustomFieldName", ...}
|
||||
# Available metadata fields: absender, empfaenger, correspondent, document_type, language,
|
||||
# Available metadata fields: absender, empfaenger, correspondent, document_type, language,
|
||||
# kommunikationsart, kommunikationskategorie, reference_number, etc.
|
||||
# Example: PAPERLESS_CUSTOM_FIELDS_MAPPING={"absender": "Sender", "empfaenger": "Recipient", "language": "Language", "correspondent": "Correspondent"}
|
||||
# PAPERLESS_CUSTOM_FIELDS_MAPPING=
|
||||
@@ -238,4 +238,4 @@ NOTIFY_ON_FILE_PROCESSED=True
|
||||
|
||||
# Uptime Kuma
|
||||
UPTIME_KUMA_URL=https://status.example.com/api/push/abcdef123456?status=up
|
||||
UPTIME_KUMA_PING_INTERVAL=5
|
||||
UPTIME_KUMA_PING_INTERVAL=5
|
||||
|
||||
@@ -10,10 +10,9 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/frontend/static"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
|
||||
@@ -177,8 +177,8 @@ Example:
|
||||
```markdown
|
||||
### OPENAI_API_KEY
|
||||
|
||||
**Type:** String
|
||||
**Required:** Yes
|
||||
**Type:** String
|
||||
**Required:** Yes
|
||||
**Default:** None
|
||||
|
||||
Your OpenAI API key for metadata extraction.
|
||||
|
||||
@@ -22,13 +22,13 @@ These instructions apply to all files in the `frontend/` directory (templates, C
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-2xl font-bold mb-4">{{ page_title }}</h1>
|
||||
|
||||
|
||||
{% if error_message %}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{{ error_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<!-- Form content -->
|
||||
</form>
|
||||
@@ -72,9 +72,9 @@ These instructions apply to all files in the `frontend/` directory (templates, C
|
||||
<label class="block text-gray-700 text-sm font-bold mb-2" for="file">
|
||||
Document File
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="file"
|
||||
<input
|
||||
type="file"
|
||||
id="file"
|
||||
name="file"
|
||||
class="w-full px-3 py-2 border rounded"
|
||||
required
|
||||
|
||||
@@ -39,15 +39,15 @@ def process_document(
|
||||
) -> DocumentMetadata:
|
||||
"""
|
||||
Process a document and extract metadata.
|
||||
|
||||
|
||||
Args:
|
||||
file_path: Path to the document file
|
||||
user_id: ID of the user uploading the document
|
||||
metadata: Optional additional metadata
|
||||
|
||||
|
||||
Returns:
|
||||
DocumentMetadata object with extracted information
|
||||
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If file doesn't exist
|
||||
ProcessingError: If processing fails
|
||||
@@ -150,7 +150,7 @@ from pydantic_settings import BaseSettings
|
||||
class Settings(BaseSettings):
|
||||
openai_api_key: str
|
||||
max_file_size: int = 10485760 # 10MB default
|
||||
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
```
|
||||
|
||||
@@ -68,9 +68,9 @@ def db_session():
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
|
||||
|
||||
yield session
|
||||
|
||||
|
||||
session.close()
|
||||
Base.metadata.drop_all(engine)
|
||||
|
||||
@@ -99,7 +99,7 @@ def test_upload_document():
|
||||
"/api/documents/upload",
|
||||
files={"file": ("test.pdf", f, "application/pdf")}
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 201
|
||||
assert "id" in response.json()
|
||||
```
|
||||
@@ -131,12 +131,12 @@ def test_openai_metadata_extraction(mocker):
|
||||
"amount": 100.00,
|
||||
"date": "2024-01-01"
|
||||
}
|
||||
|
||||
|
||||
mocker.patch(
|
||||
"app.utils.openai_client.extract_metadata",
|
||||
return_value=mock_response
|
||||
)
|
||||
|
||||
|
||||
result = extract_document_metadata("test.pdf")
|
||||
assert result["document_type"] == "invoice"
|
||||
|
||||
@@ -144,12 +144,12 @@ def test_openai_metadata_extraction(mocker):
|
||||
def test_azure_ocr_processing(mocker):
|
||||
"""Test OCR with mocked Azure service."""
|
||||
mock_text = "Sample extracted text"
|
||||
|
||||
|
||||
mocker.patch(
|
||||
"app.utils.azure_client.extract_text",
|
||||
return_value=mock_text
|
||||
)
|
||||
|
||||
|
||||
result = perform_ocr("test.pdf")
|
||||
assert result == mock_text
|
||||
```
|
||||
@@ -160,7 +160,7 @@ def test_azure_ocr_processing(mocker):
|
||||
def test_create_document(db_session):
|
||||
"""Test document creation in database."""
|
||||
from app.models import Document
|
||||
|
||||
|
||||
doc = Document(
|
||||
filename="test.pdf",
|
||||
user_id=1,
|
||||
@@ -168,7 +168,7 @@ def test_create_document(db_session):
|
||||
)
|
||||
db_session.add(doc)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
assert doc.id is not None
|
||||
assert doc.filename == "test.pdf"
|
||||
```
|
||||
@@ -189,10 +189,10 @@ def test_document_validation():
|
||||
"filename": "", # Empty filename
|
||||
"size": -1 # Invalid size
|
||||
}
|
||||
|
||||
|
||||
# Act
|
||||
result = validate_document(invalid_document)
|
||||
|
||||
|
||||
# Assert
|
||||
assert result.is_valid is False
|
||||
assert "filename" in result.errors
|
||||
|
||||
@@ -17,30 +17,30 @@ jobs:
|
||||
name: Semantic Release
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'christianlouis/DocuElevate'
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install python-semantic-release
|
||||
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
|
||||
- name: Run Semantic Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
semantic-release version --print
|
||||
semantic-release version
|
||||
semantic-release publish
|
||||
|
||||
|
||||
- name: Update build metadata files if changed
|
||||
run: |
|
||||
for f in VERSION BUILD_DATE GIT_SHA RUNTIME_INFO; do
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
git commit -m "chore(release): update build metadata files [skip ci]"
|
||||
git push
|
||||
fi
|
||||
|
||||
|
||||
- name: Trigger Docker Build on Tag
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: actions/github-script@v7
|
||||
@@ -70,4 +70,4 @@ jobs:
|
||||
const ref = context.ref;
|
||||
const tag = ref.replace('refs/tags/', '');
|
||||
console.log(`New tag created: ${tag}`);
|
||||
console.log('Docker build will be triggered automatically by the docker-build workflow');
|
||||
console.log('Docker build will be triggered automatically by the docker-build workflow');
|
||||
|
||||
+1
-1
@@ -19,4 +19,4 @@ mkdocs:
|
||||
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
- requirements: docs/requirements.txt
|
||||
|
||||
Vendored
+1
-1
@@ -4,4 +4,4 @@
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
||||
}
|
||||
|
||||
+19
-19
@@ -1,6 +1,6 @@
|
||||
# Agentic Coding Guide for DocuElevate
|
||||
|
||||
**Version:** 1.0
|
||||
**Version:** 1.0
|
||||
**Last Updated:** 2026-02-06
|
||||
|
||||
This guide helps AI coding agents work effectively with the DocuElevate codebase. It provides context, conventions, and best practices for autonomous code contributions.
|
||||
@@ -80,14 +80,14 @@ DocuElevate/
|
||||
def process_document(file_path: str, metadata: Dict[str, Any]) -> DocumentMetadata:
|
||||
"""
|
||||
Process a document and extract metadata.
|
||||
|
||||
|
||||
Args:
|
||||
file_path: Absolute path to the document file
|
||||
metadata: Additional metadata to include
|
||||
|
||||
|
||||
Returns:
|
||||
DocumentMetadata object with extracted information
|
||||
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If file doesn't exist
|
||||
ProcessingError: If processing fails
|
||||
@@ -225,7 +225,7 @@ logger = logging.getLogger(__name__)
|
||||
def my_background_task(self, param: str):
|
||||
"""
|
||||
Description of what this task does.
|
||||
|
||||
|
||||
Args:
|
||||
param: Description of parameter
|
||||
"""
|
||||
@@ -247,7 +247,7 @@ def my_background_task(self, param: str):
|
||||
```python
|
||||
class MyModel(Base):
|
||||
__tablename__ = "my_table"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
@@ -275,20 +275,20 @@ logger = logging.getLogger(__name__)
|
||||
def upload_to_my_provider(file_path: str, metadata: dict) -> str:
|
||||
"""
|
||||
Upload file to My Provider.
|
||||
|
||||
|
||||
Args:
|
||||
file_path: Local path to file
|
||||
metadata: Document metadata
|
||||
|
||||
|
||||
Returns:
|
||||
URL or ID of uploaded file
|
||||
|
||||
|
||||
Raises:
|
||||
ProviderError: If upload fails
|
||||
"""
|
||||
if not settings.my_provider_api_key:
|
||||
raise ValueError("MY_PROVIDER_API_KEY not configured")
|
||||
|
||||
|
||||
# Implementation
|
||||
pass
|
||||
```
|
||||
@@ -342,11 +342,11 @@ def validate_file_path(file_path: str, base_dir: str = "/workdir") -> Path:
|
||||
try:
|
||||
path = Path(file_path).resolve()
|
||||
base = Path(base_dir).resolve()
|
||||
|
||||
|
||||
# Ensure path is within base directory
|
||||
if not path.is_relative_to(base):
|
||||
raise ValueError("Path outside allowed directory")
|
||||
|
||||
|
||||
return path
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
@@ -467,23 +467,23 @@ def process_large_batch(self, file_ids: List[int]):
|
||||
def complex_function(param1: str, param2: int = 10) -> Dict[str, Any]:
|
||||
"""
|
||||
One-line summary of what the function does.
|
||||
|
||||
|
||||
More detailed explanation if needed. Can span multiple
|
||||
lines and include examples.
|
||||
|
||||
|
||||
Args:
|
||||
param1: Description of param1
|
||||
param2: Description of param2, defaults to 10
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- key1: Description
|
||||
- key2: Description
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If param1 is empty
|
||||
FileNotFoundError: If file doesn't exist
|
||||
|
||||
|
||||
Examples:
|
||||
>>> result = complex_function("test", 5)
|
||||
>>> print(result['key1'])
|
||||
@@ -650,7 +650,7 @@ DocuElevate uses `python-semantic-release` for automated version management.
|
||||
|
||||
#### How It Works
|
||||
1. **PR merges to main** with conventional commits
|
||||
2. **semantic-release analyzes** commit messages
|
||||
2. **semantic-release analyzes** commit messages
|
||||
3. **Automatic updates**:
|
||||
- Bumps `VERSION` file
|
||||
- Updates `CHANGELOG.md`
|
||||
@@ -669,7 +669,7 @@ DocuElevate uses `python-semantic-release` for automated version management.
|
||||
### Pull Requests
|
||||
1. Create PR with descriptive title (conventional format if single change)
|
||||
2. Fill out PR template
|
||||
3. Link related issues
|
||||
3. Link related issues
|
||||
4. Ensure CI passes
|
||||
5. Request reviews
|
||||
6. Address feedback
|
||||
|
||||
+1
-1
@@ -346,4 +346,4 @@ DocuElevate/
|
||||
- **GitHub Discussions:** Questions and community support
|
||||
- **Documentation:** Check `docs/` directory for guides
|
||||
|
||||
Thank you for contributing to DocuElevate!
|
||||
Thank you for contributing to DocuElevate!
|
||||
|
||||
@@ -42,5 +42,3 @@ EXPOSE 8000
|
||||
|
||||
# Default command
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
|
||||
|
||||
+15
-15
@@ -57,8 +57,8 @@ As of February 2026, DocuElevate uses **automated semantic versioning**:
|
||||
## Completed Milestones
|
||||
|
||||
### v0.5.0 - Settings Management & Configuration (February 2026)
|
||||
**Release Date:** February 8, 2026
|
||||
**Status:** ✅ Released
|
||||
**Release Date:** February 8, 2026
|
||||
**Status:** ✅ Released
|
||||
**Theme:** Configuration Management, Security, User Experience
|
||||
|
||||
#### Goals
|
||||
@@ -90,8 +90,8 @@ As of February 2026, DocuElevate uses **automated semantic versioning**:
|
||||
---
|
||||
|
||||
### v0.3.3 - Drag-and-Drop Upload (February 2026)
|
||||
**Release Date:** February 8, 2026
|
||||
**Status:** ✅ Released
|
||||
**Release Date:** February 8, 2026
|
||||
**Status:** ✅ Released
|
||||
**Theme:** User Experience Enhancement
|
||||
|
||||
#### Goals
|
||||
@@ -108,8 +108,8 @@ As of February 2026, DocuElevate uses **automated semantic versioning**:
|
||||
---
|
||||
|
||||
### v0.3.2 - Security & Testing Hardening (February 2026)
|
||||
**Release Date:** February 6, 2026
|
||||
**Status:** ✅ Released
|
||||
**Release Date:** February 6, 2026
|
||||
**Status:** ✅ Released
|
||||
**Theme:** Security, Quality, Testing
|
||||
|
||||
#### Goals
|
||||
@@ -131,8 +131,8 @@ As of February 2026, DocuElevate uses **automated semantic versioning**:
|
||||
## Upcoming Milestones
|
||||
|
||||
### v0.6.0 - Enhanced Search & UI Improvements (April 2026)
|
||||
**Target Date:** April 1, 2026
|
||||
**Status:** 📋 Planned
|
||||
**Target Date:** April 1, 2026
|
||||
**Status:** 📋 Planned
|
||||
**Theme:** User Experience, Search, Performance
|
||||
|
||||
#### Goals
|
||||
@@ -163,8 +163,8 @@ As of February 2026, DocuElevate uses **automated semantic versioning**:
|
||||
---
|
||||
|
||||
### v0.4.5 - Workflow Automation (June 2026)
|
||||
**Target Date:** June 1, 2026
|
||||
**Status:** 📋 Planned
|
||||
**Target Date:** June 1, 2026
|
||||
**Status:** 📋 Planned
|
||||
**Theme:** Automation, Integration, Webhooks
|
||||
|
||||
#### Goals
|
||||
@@ -185,8 +185,8 @@ As of February 2026, DocuElevate uses **automated semantic versioning**:
|
||||
---
|
||||
|
||||
### v0.7.0 - Advanced AI & Multi-language (August 2026)
|
||||
**Target Date:** August 1, 2026
|
||||
**Status:** 📋 Planned
|
||||
**Target Date:** August 1, 2026
|
||||
**Status:** 📋 Planned
|
||||
**Theme:** AI Enhancement, Internationalization
|
||||
|
||||
#### Goals
|
||||
@@ -208,8 +208,8 @@ As of February 2026, DocuElevate uses **automated semantic versioning**:
|
||||
---
|
||||
|
||||
### v1.0.0 - Enterprise Edition (November 2026)
|
||||
**Target Date:** November 1, 2026
|
||||
**Status:** 📋 Planned
|
||||
**Target Date:** November 1, 2026
|
||||
**Status:** 📋 Planned
|
||||
**Theme:** Enterprise Features, Scalability, Multi-tenancy
|
||||
|
||||
This is our first major release, marking production-ready enterprise capabilities.
|
||||
@@ -322,4 +322,4 @@ Starting with v0.6.0, releases are fully automated using `python-semantic-releas
|
||||
|
||||
---
|
||||
|
||||
*This milestone document is updated regularly. For real-time status, check our [GitHub Projects](https://github.com/christianlouis/DocuElevate/projects) board.*
|
||||
*This milestone document is updated regularly. For real-time status, check our [GitHub Projects](https://github.com/christianlouis/DocuElevate/projects) board.*
|
||||
|
||||
@@ -9,15 +9,15 @@ This software includes third-party components with their own licenses:
|
||||
|
||||
SPECIAL NOTICE REGARDING LGPL SOFTWARE:
|
||||
--------------------------------------------------------------------------------
|
||||
DocuElevate incorporates Paramiko, which is licensed under the GNU Lesser General
|
||||
DocuElevate incorporates Paramiko, which is licensed under the GNU Lesser General
|
||||
Public License (LGPL) version 2.1. In accordance with the LGPL:
|
||||
|
||||
1. The complete source code for Paramiko can be obtained from:
|
||||
https://github.com/paramiko/paramiko
|
||||
|
||||
2. This software is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||
2. This software is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||
for more details.
|
||||
|
||||
3. A copy of the GNU Lesser General Public License version 2.1 can be found at:
|
||||
|
||||
@@ -50,7 +50,7 @@ pytest tests/test_oauth_integration_flows.py -v
|
||||
```bash
|
||||
# Auto-detects and uses real OAuth credentials
|
||||
export AUTHENTIK_CLIENT_ID="your-client-id"
|
||||
export AUTHENTIK_CLIENT_SECRET="your-client-secret"
|
||||
export AUTHENTIK_CLIENT_SECRET="your-client-secret"
|
||||
export AUTHENTIK_CONFIG_URL="https://auth.example.com/.well-known/openid-configuration"
|
||||
pytest tests/test_oauth_integration_flows.py -v -m requires_external
|
||||
```
|
||||
@@ -128,7 +128,7 @@ async def test_oauth_callback(mock_authorize, oauth_enabled_app, test_user_info)
|
||||
"access_token": "test-token",
|
||||
"userinfo": test_user_info,
|
||||
}
|
||||
|
||||
|
||||
response = oauth_enabled_app.get("/oauth-callback?code=test-code")
|
||||
assert response.status_code == 302
|
||||
```
|
||||
@@ -185,11 +185,11 @@ jobs:
|
||||
|
||||
## Technical Details
|
||||
|
||||
**Container**: `ghcr.io/navikt/mock-oauth2-server:2.1.1`
|
||||
**Framework**: Testcontainers Python 4.14.1+
|
||||
**Test Framework**: pytest with async support
|
||||
**Languages**: Python 3.12+
|
||||
**Dependencies**: testcontainers, requests, docker
|
||||
**Container**: `ghcr.io/navikt/mock-oauth2-server:2.1.1`
|
||||
**Framework**: Testcontainers Python 4.14.1+
|
||||
**Test Framework**: pytest with async support
|
||||
**Languages**: Python 3.12+
|
||||
**Dependencies**: testcontainers, requests, docker
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
|
||||
DocuElevate automates the handling, extraction, and processing of documents using a variety of services, including:
|
||||
|
||||
- **OpenAI** for metadata extraction and text refinement.
|
||||
- **Dropbox**, **Nextcloud**, and **Google Drive** for file storage and uploads.
|
||||
- **Paperless NGX** for document indexing and management.
|
||||
- **Azure Document Intelligence** for OCR on PDFs.
|
||||
- **Gotenberg** for file-to-PDF conversions.
|
||||
- **Authentik** for authentication and user management.
|
||||
- **OpenAI** for metadata extraction and text refinement.
|
||||
- **Dropbox**, **Nextcloud**, and **Google Drive** for file storage and uploads.
|
||||
- **Paperless NGX** for document indexing and management.
|
||||
- **Azure Document Intelligence** for OCR on PDFs.
|
||||
- **Gotenberg** for file-to-PDF conversions.
|
||||
- **Authentik** for authentication and user management.
|
||||
|
||||
It is designed for flexibility and configurability through environment variables, making it easily customizable for different workflows. The system can fetch documents from multiple IMAP mailboxes, process them (OCR, metadata extraction, PDF conversion), and store them in the desired destinations.
|
||||
|
||||
@@ -59,7 +59,7 @@ The project includes a **UI** for uploading and managing files, and an API docum
|
||||
<div align="center">
|
||||
<img src="docs/upload-view.png" alt="DocuElevate Upload Interface" width="80%" />
|
||||
<p><em>Upload interface for adding new documents</em></p>
|
||||
|
||||
|
||||
<img src="docs/files-view.png" alt="DocuElevate Files View" width="80%" />
|
||||
<p><em>Files view with processed documents and metadata</em></p>
|
||||
</div>
|
||||
@@ -100,28 +100,28 @@ Users can choose to send documents to any combination of these destinations thro
|
||||
|
||||
## Features
|
||||
|
||||
- **Intuitive File Upload**:
|
||||
- **Intuitive File Upload**:
|
||||
- Drag-and-drop file upload on both Upload and Files pages—upload anywhere on the Files page
|
||||
- Real-time upload progress with validation
|
||||
- Support for PDF, Office documents, images, and more (up to 500MB per file)
|
||||
- **Browser Extension**:
|
||||
- **Browser Extension**:
|
||||
- Send files directly from your browser to DocuElevate with one click
|
||||
- Compatible with Chrome, Firefox, Edge, and other Chromium-based browsers
|
||||
- Context menu integration for quick access
|
||||
- See [Browser Extension Guide](docs/BrowserExtension.md) for installation and usage
|
||||
- **Document Upload & Storage**:
|
||||
- **Document Upload & Storage**:
|
||||
- Manual uploads (via API or UI) to Dropbox, Nextcloud, Google Drive, or Paperless
|
||||
- **OCR Processing (Azure)**:
|
||||
- Extract text from scanned PDFs using Azure Document Intelligence
|
||||
- **Metadata Extraction (OpenAI)**:
|
||||
- Use GPT to classify, label, or otherwise enrich the text with structured metadata
|
||||
- **PDF Conversion (Gotenberg)**:
|
||||
- Convert non-PDF attachments (e.g., Word docs, images) into PDFs
|
||||
- **Document Management (Paperless NGX)**:
|
||||
- Store processed documents and metadata in a Paperless NGX instance
|
||||
- **IMAP Integration**:
|
||||
- Fetch documents from multiple mailboxes (including Gmail) and automatically enqueue them for processing
|
||||
- **Authentication**:
|
||||
- **OCR Processing (Azure)**:
|
||||
- Extract text from scanned PDFs using Azure Document Intelligence
|
||||
- **Metadata Extraction (OpenAI)**:
|
||||
- Use GPT to classify, label, or otherwise enrich the text with structured metadata
|
||||
- **PDF Conversion (Gotenberg)**:
|
||||
- Convert non-PDF attachments (e.g., Word docs, images) into PDFs
|
||||
- **Document Management (Paperless NGX)**:
|
||||
- Store processed documents and metadata in a Paperless NGX instance
|
||||
- **IMAP Integration**:
|
||||
- Fetch documents from multiple mailboxes (including Gmail) and automatically enqueue them for processing
|
||||
- **Authentication**:
|
||||
- Secure access to the system using **Authentik** for OAuth2-based login
|
||||
|
||||
## Frameworks Used
|
||||
@@ -235,4 +235,4 @@ For a comprehensive list of all dependencies and their licenses, run:
|
||||
```
|
||||
pip install pip-licenses
|
||||
pip-licenses
|
||||
```
|
||||
```
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# DocuElevate Roadmap
|
||||
|
||||
**Last Updated:** 2026-02-08
|
||||
**Last Updated:** 2026-02-08
|
||||
**Version:** 1.0
|
||||
|
||||
## Vision
|
||||
|
||||
+36
-36
@@ -1,6 +1,6 @@
|
||||
# Security Audit Report
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Date:** 2026-02-12
|
||||
**Status:** Bandit Security Scan Completed - All Critical/High/Medium Issues Resolved
|
||||
|
||||
## Executive Summary
|
||||
@@ -11,8 +11,8 @@ This document tracks security vulnerabilities found in DocuElevate and their rem
|
||||
|
||||
### CVE-2023-36464: PyPDF2/pypdf Infinite Loop Vulnerability ✅ FIXED (2026-02-12)
|
||||
|
||||
**Severity:** Moderate (CVSS: 5.5)
|
||||
**CVE:** [CVE-2023-36464](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-36464)
|
||||
**Severity:** Moderate (CVSS: 5.5)
|
||||
**CVE:** [CVE-2023-36464](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-36464)
|
||||
**Advisory:** [GHSA-4vvm-4w3v-6mr8](https://github.com/advisories/GHSA-4vvm-4w3v-6mr8)
|
||||
|
||||
**Issue:** Certain versions of PyPDF2 (>=2.2.0, <=3.0.1) and pypdf (prior to 3.9.0) contain a vulnerability where specially crafted PDF files can trigger an infinite loop in `__parse_content_stream`, causing 100% CPU usage and potential denial of service.
|
||||
@@ -57,8 +57,8 @@ This document tracks security vulnerabilities found in DocuElevate and their rem
|
||||
### Fixed Issues from Bandit Scan
|
||||
|
||||
#### 1. B324: Weak MD5 Hash Usage (HIGH SEVERITY) ✅ FIXED
|
||||
**Occurrences:** 2
|
||||
**Locations:**
|
||||
**Occurrences:** 2
|
||||
**Locations:**
|
||||
- `app/api/user.py:26` - Gravatar URL generation
|
||||
- `app/auth.py:65` - Gravatar URL generation
|
||||
|
||||
@@ -72,7 +72,7 @@ This document tracks security vulnerabilities found in DocuElevate and their rem
|
||||
```
|
||||
|
||||
#### 2. B402/B321: Insecure FTP Protocol (HIGH SEVERITY) ✅ DOCUMENTED
|
||||
**Occurrences:** 3
|
||||
**Occurrences:** 3
|
||||
**Location:** `app/tasks/upload_to_ftp.py`
|
||||
|
||||
**Issue:** FTP is an insecure protocol vulnerable to eavesdropping and MITM attacks.
|
||||
@@ -88,7 +88,7 @@ This document tracks security vulnerabilities found in DocuElevate and their rem
|
||||
**Security Note:** For production environments, set `ftp_allow_plaintext=False` to prevent fallback to unencrypted FTP.
|
||||
|
||||
#### 3. B507: SSH Host Key Verification Disabled (HIGH SEVERITY) ✅ FIXED
|
||||
**Occurrences:** 1
|
||||
**Occurrences:** 1
|
||||
**Location:** `app/tasks/upload_to_sftp.py:47`
|
||||
|
||||
**Issue:** Using `paramiko.AutoAddPolicy()` automatically trusts unknown SSH host keys, making connections vulnerable to MITM attacks.
|
||||
@@ -103,7 +103,7 @@ This document tracks security vulnerabilities found in DocuElevate and their rem
|
||||
**Security Note:** The default value is now `False` (secure). For development/testing environments where host keys cannot be pre-configured, set `SFTP_DISABLE_HOST_KEY_VERIFICATION=True` (not recommended for production).
|
||||
|
||||
#### 4. B113: Missing Timeout on HTTP Requests (MEDIUM SEVERITY) ✅ FIXED
|
||||
**Occurrences:** 15
|
||||
**Occurrences:** 15
|
||||
**Locations:**
|
||||
- `app/api/dropbox.py` (4 requests calls)
|
||||
- `app/api/google_drive.py` (1 request call)
|
||||
@@ -142,8 +142,8 @@ This document tracks security vulnerabilities found in DocuElevate and their rem
|
||||
## Critical Vulnerabilities (Fixed) ✅
|
||||
|
||||
### 1. Outdated Authlib with Known Vulnerabilities
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** HIGH
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** HIGH
|
||||
**Description:** Authlib version 1.3.2 had two critical vulnerabilities:
|
||||
- CVE: Denial of Service via Oversized JOSE Segments
|
||||
- CVE: JWS/JWT accepts unknown crit headers (RFC violation → possible authz bypass)
|
||||
@@ -151,18 +151,18 @@ This document tracks security vulnerabilities found in DocuElevate and their rem
|
||||
**Fix:** Updated `requirements.txt` to require `authlib>=1.6.5`
|
||||
|
||||
### 2. Starlette DoS Vulnerability
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Description:** Starlette 0.41.3 vulnerable to O(n^2) DoS via Range header merging in `FileResponse`
|
||||
|
||||
**Fix:** Updated `requirements.txt` to require `starlette>=0.49.1`
|
||||
|
||||
### 3. Weak SESSION_SECRET Default
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** HIGH
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** HIGH
|
||||
**Description:** Default SESSION_SECRET value in `app/main.py` was a predictable string that could be exploited if not overridden
|
||||
|
||||
**Fix:**
|
||||
**Fix:**
|
||||
- Enhanced validation in `app/main.py` to raise error if auth is enabled without proper secret
|
||||
- Updated default to be clearly marked as insecure for development only
|
||||
- Added generation instructions in error message
|
||||
@@ -170,8 +170,8 @@ This document tracks security vulnerabilities found in DocuElevate and their rem
|
||||
## Medium Risk Issues (Fixed) ✅
|
||||
|
||||
### 4. Insufficient .gitignore Protection
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Description:** .gitignore didn't adequately protect against accidentally committing sensitive files (credentials, private keys, secrets)
|
||||
|
||||
**Fix:** Enhanced `.gitignore` with comprehensive patterns for:
|
||||
@@ -182,8 +182,8 @@ This document tracks security vulnerabilities found in DocuElevate and their rem
|
||||
- Explicit exclusion of patterns where needed
|
||||
|
||||
### 5. File Upload Size Limits
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Description:** No configurable limits on file upload sizes could lead to resource exhaustion attacks and DoS.
|
||||
|
||||
**Fix:** Implemented configurable file upload size limits with the following features:
|
||||
@@ -329,7 +329,7 @@ For security issues, please follow the guidelines in [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Path Traversal Vulnerability Audit (2026-02-10)
|
||||
|
||||
**Status:** ✅ ALL ISSUES FIXED
|
||||
**Status:** ✅ ALL ISSUES FIXED
|
||||
**Scope:** Comprehensive review of all file path operations for path traversal vulnerabilities
|
||||
|
||||
### Executive Summary
|
||||
@@ -338,11 +338,11 @@ A thorough security audit was conducted on all file path operations in DocuEleva
|
||||
|
||||
### Critical Vulnerability: Path Traversal via GPT Metadata Filename
|
||||
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** CRITICAL
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** CRITICAL
|
||||
**Location:** `app/tasks/embed_metadata_into_pdf.py` (line 144)
|
||||
|
||||
**Description:**
|
||||
**Description:**
|
||||
The `metadata["filename"]` extracted by GPT was used directly in file path operations without sanitization. A malicious document could be crafted to make GPT return metadata containing path traversal sequences (e.g., `../../etc/passwd`, `..\\windows\\system32`), allowing file writes outside the intended `processed/` directory.
|
||||
|
||||
**Attack Vector:**
|
||||
@@ -376,11 +376,11 @@ suggested_filename = os.path.splitext(suggested_filename)[0]
|
||||
|
||||
### Medium Vulnerability: Insecure Path Validation Using String Prefix Check
|
||||
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Location:** `app/tasks/embed_metadata_into_pdf.py` (line 188-193)
|
||||
|
||||
**Description:**
|
||||
**Description:**
|
||||
The code used string-based `startswith()` check to validate if a file was within the workdir/tmp directory before deletion. This is vulnerable to:
|
||||
- Partial directory name matches (e.g., `/workdir/tmp2/` would pass if workdir is `/workdir/tmp`)
|
||||
- Symlink attacks (symlinks are not resolved before checking)
|
||||
@@ -403,7 +403,7 @@ workdir_tmp_path = Path(settings.workdir) / TMP_SUBDIR
|
||||
try:
|
||||
original_file_path = Path(original_file).resolve()
|
||||
workdir_tmp_resolved = workdir_tmp_path.resolve()
|
||||
|
||||
|
||||
# Check if file is within workdir/tmp and exists
|
||||
if original_file_path.is_relative_to(workdir_tmp_resolved) and original_file_path.exists():
|
||||
original_file_path.unlink()
|
||||
@@ -420,11 +420,11 @@ except (ValueError, OSError) as e:
|
||||
|
||||
### Medium Issue: Insufficient Validation of GPT-Extracted Filenames
|
||||
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Status:** ✅ FIXED
|
||||
**Severity:** MEDIUM
|
||||
**Location:** `app/tasks/extract_metadata_with_gpt.py` (after line 124)
|
||||
|
||||
**Description:**
|
||||
**Description:**
|
||||
While the GPT prompt requested filenames in a specific format (YYYY-MM-DD_DescriptiveTitle with only letters, numbers, periods, underscores), there was no validation to enforce this constraint. GPT may not always comply with the format specification, potentially returning:
|
||||
- Filenames with path separators
|
||||
- Filenames with path traversal patterns
|
||||
@@ -449,7 +449,7 @@ if filename:
|
||||
metadata["filename"] = ""
|
||||
```
|
||||
|
||||
**Defense in Depth:**
|
||||
**Defense in Depth:**
|
||||
This validation provides an additional layer of security before the filename reaches `embed_metadata_into_pdf.py`, where it is also sanitized.
|
||||
|
||||
### Security-Positive Findings
|
||||
@@ -502,11 +502,11 @@ def resolve_file_path(base_dir, file_path):
|
||||
"""Safely resolve file path within base directory."""
|
||||
base = Path(base_dir).resolve()
|
||||
target = (base / file_path).resolve()
|
||||
|
||||
|
||||
# Ensure target is within base directory
|
||||
if not target.is_relative_to(base):
|
||||
raise ValueError("Path traversal attempt detected")
|
||||
|
||||
|
||||
return target
|
||||
```
|
||||
|
||||
@@ -610,7 +610,7 @@ All identified path traversal vulnerabilities have been remediated with defense-
|
||||
|
||||
## Security Headers Implementation (2026-02-10)
|
||||
|
||||
**Status:** ✅ COMPLETED
|
||||
**Status:** ✅ COMPLETED
|
||||
**Scope:** HTTP security headers middleware for browser-side security
|
||||
|
||||
### Executive Summary
|
||||
@@ -654,7 +654,7 @@ SECURITY_HEADER_CSP_VALUE="default-src 'self'; script-src 'self' 'unsafe-inline'
|
||||
- Mitigates XSS attack vectors
|
||||
- Customizable per deployment needs
|
||||
|
||||
**Trade-offs:**
|
||||
**Trade-offs:**
|
||||
- Default policy includes `'unsafe-inline'` for compatibility with Tailwind CSS and inline JavaScript
|
||||
- Stricter policies can be configured using nonces or hashes
|
||||
|
||||
@@ -832,4 +832,4 @@ Security headers implementation is complete and production-ready. The middleware
|
||||
|
||||
---
|
||||
|
||||
**Next Audit Due:** 2026-05-07 (Quarterly)
|
||||
**Next Audit Due:** 2026-05-07 (Quarterly)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DocuElevate TODO List
|
||||
|
||||
**Last Updated:** 2026-02-08
|
||||
**Last Updated:** 2026-02-08
|
||||
**Current Version:** v0.5.0
|
||||
|
||||
This document tracks actionable tasks for the current development cycle. For long-term planning, see [ROADMAP.md](ROADMAP.md) and [MILESTONES.md](MILESTONES.md).
|
||||
@@ -10,7 +10,7 @@ This document tracks actionable tasks for the current development cycle. For lon
|
||||
## ⚠️ Important Note on Versioning
|
||||
|
||||
As of this update, DocuElevate uses **automated semantic versioning** via `python-semantic-release`:
|
||||
- **DO NOT** manually edit `VERSION` or `CHANGELOG.md`
|
||||
- **DO NOT** manually edit `VERSION` or `CHANGELOG.md`
|
||||
- Version bumps are automated based on conventional commit messages
|
||||
- See [CONTRIBUTING.md](CONTRIBUTING.md) for commit message format
|
||||
|
||||
|
||||
+18
-18
@@ -4,10 +4,10 @@ This document tracks test coverage improvements for DocuElevate. The goal is to
|
||||
|
||||
## Current Status
|
||||
|
||||
**Initial Coverage**: 45.09%
|
||||
**Current Coverage**: 48.17%
|
||||
**Progress**: +3.08%
|
||||
**Target Coverage**: 60%+ (Phase 1), then 70%, 80%
|
||||
**Initial Coverage**: 45.09%
|
||||
**Current Coverage**: 48.17%
|
||||
**Progress**: +3.08%
|
||||
**Target Coverage**: 60%+ (Phase 1), then 70%, 80%
|
||||
**Remaining to target**: ~12%
|
||||
|
||||
## Completed Tests
|
||||
@@ -21,22 +21,22 @@ This document tracks test coverage improvements for DocuElevate. The goal is to
|
||||
- Test is_encrypted function
|
||||
- Test is_encryption_available
|
||||
- Mock cryptography library for error cases
|
||||
|
||||
|
||||
- [x] `app/celery_worker.py` (0% → 90.62%) ✅
|
||||
- Basic module structure tests (removed tests requiring Redis)
|
||||
|
||||
|
||||
- [x] `app/tasks/uptime_kuma_tasks.py` (0% → 100%) ✅
|
||||
- Test ping_uptime_kuma with valid URL
|
||||
- Test skipping when URL not configured
|
||||
- Test error handling for failed requests
|
||||
|
||||
|
||||
- [x] `app/utils/` package (exports via __init__.py) ✅
|
||||
- Package exports tested in test_reexports.py
|
||||
- Individual module coverage from actual usage
|
||||
|
||||
|
||||
- [x] `app/frontend.py` (0% → 100%) ✅
|
||||
- Simple re-export module, test imports work
|
||||
|
||||
|
||||
- [x] `app/utils/config_validator.py` (0% → Still 0%) ⚠️
|
||||
- Re-export module, coverage is from actual usage
|
||||
|
||||
@@ -46,11 +46,11 @@ This document tracks test coverage improvements for DocuElevate. The goal is to
|
||||
- Test get_unique_filename
|
||||
- Test extract_remote_path
|
||||
- Test filename validation functions
|
||||
|
||||
|
||||
- [x] `app/utils/logging.py` (42.86% → 100%) ✅
|
||||
- Test log_task_progress function
|
||||
- Test various log message formats
|
||||
|
||||
|
||||
- [x] `app/utils/oauth_helper.py` (17.50% → 100%) ✅
|
||||
- Test OAuth token exchange
|
||||
- Test error handling
|
||||
@@ -80,17 +80,17 @@ This document tracks test coverage improvements for DocuElevate. The goal is to
|
||||
- Test Azure connection
|
||||
- Test credential validation
|
||||
- Mock Azure API responses
|
||||
|
||||
|
||||
- [ ] `app/api/dropbox.py` (16.94% → 50%+)
|
||||
- Test OAuth flow (mocked)
|
||||
- Test token validation
|
||||
- Test connection testing
|
||||
|
||||
|
||||
- [ ] `app/api/google_drive.py` (12.94% → 50%+)
|
||||
- Test OAuth flow (mocked)
|
||||
- Test token validation
|
||||
- Test drive connection
|
||||
|
||||
|
||||
- [ ] `app/api/onedrive.py` (13.83% → 50%+)
|
||||
- Test OAuth flow (mocked)
|
||||
- Test token validation
|
||||
@@ -101,7 +101,7 @@ This document tracks test coverage improvements for DocuElevate. The goal is to
|
||||
- Test PDF conversion with various formats
|
||||
- Test Gotenberg integration (mocked)
|
||||
- Test error handling
|
||||
|
||||
|
||||
- [ ] `app/tasks/embed_metadata_into_pdf.py` (19.05% → 50%+)
|
||||
- Test metadata embedding
|
||||
- Test PDF manipulation
|
||||
@@ -124,18 +124,18 @@ These require complex external service mocking:
|
||||
- Test credential validation for each provider
|
||||
- Test failure state management
|
||||
- Test notification system
|
||||
|
||||
|
||||
- [ ] `app/tasks/imap_tasks.py` (0%)
|
||||
- Requires IMAP server mocking
|
||||
- Test email fetching
|
||||
- Test email parsing
|
||||
- Test lock management with Redis
|
||||
|
||||
|
||||
- [ ] `app/tasks/upload_with_rclone.py` (0%)
|
||||
- Test rclone command execution
|
||||
- Test configuration management
|
||||
- Test error handling
|
||||
|
||||
|
||||
- [ ] `app/tasks/extract_metadata_with_gpt.py` (28.79%)
|
||||
- Test GPT metadata extraction
|
||||
- Mock OpenAI API responses
|
||||
|
||||
@@ -185,7 +185,7 @@ pytest tests/test_upload_webdav*.py -v
|
||||
- **Docker:** Must be installed and running
|
||||
- **Memory:** ~100MB per container, ~1GB total for full stack
|
||||
- **Disk:** ~2GB for all Docker images
|
||||
- **Time:**
|
||||
- **Time:**
|
||||
- First run: ~5-10 minutes (image pulls)
|
||||
- Subsequent runs: ~10-60 seconds per test
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ def upload_with_rclone(self, file_path: str, destination: str):
|
||||
# Try to get a public link if possible
|
||||
try:
|
||||
link_cmd = ["rclone", "link", "--config", rclone_config_path, f"{destination}/{filename}"]
|
||||
link_result = subprocess.run(link_cmd, capture_output=True, text=True)
|
||||
link_result = subprocess.run(link_cmd, capture_output=True, text=True, check=False)
|
||||
public_url = link_result.stdout.strip() if link_result.returncode == 0 else None
|
||||
except (subprocess.SubprocessError, OSError) as e:
|
||||
logger.warning(f"[{task_id}] Failed to get public link for {filename}: {str(e)}")
|
||||
|
||||
@@ -6,8 +6,8 @@ Successfully implemented a complete, production-ready browser extension for Docu
|
||||
|
||||
## Implementation Date
|
||||
|
||||
Feature branch: `copilot/add-browser-plugin-for-docuelevate`
|
||||
Commits: 7 commits implementing the complete feature
|
||||
Feature branch: `copilot/add-browser-plugin-for-docuelevate`
|
||||
Commits: 7 commits implementing the complete feature
|
||||
Status: ✅ **COMPLETE AND PRODUCTION-READY**
|
||||
|
||||
## Requirements Met
|
||||
@@ -16,7 +16,7 @@ All requirements from the original issue have been fully satisfied:
|
||||
|
||||
### ✅ Functional Requirements
|
||||
- [x] Capture file URLs from user's browser
|
||||
- [x] Send URLs to DocuElevate API endpoint
|
||||
- [x] Send URLs to DocuElevate API endpoint
|
||||
- [x] Support for Chrome, Firefox, Edge, and Chromium-based browsers
|
||||
- [x] Simple user interaction (one-click + context menu)
|
||||
- [x] Display status/feedback in plugin UI (success, error)
|
||||
@@ -362,7 +362,7 @@ The browser extension implementation is **complete and production-ready**. All r
|
||||
|
||||
---
|
||||
|
||||
**Implementation Team**: GitHub Copilot
|
||||
**Review Status**: All code review feedback addressed
|
||||
**Documentation Status**: Complete
|
||||
**Implementation Team**: GitHub Copilot
|
||||
**Review Status**: All code review feedback addressed
|
||||
**Documentation Status**: Complete
|
||||
**Production Readiness**: ✅ READY
|
||||
|
||||
@@ -12,13 +12,13 @@ For Chrome / Edge / Chromium-based browsers:
|
||||
2. Navigate to extensions page:
|
||||
• Chrome: chrome://extensions/
|
||||
• Edge: edge://extensions/
|
||||
|
||||
|
||||
3. Enable "Developer mode" (toggle in top right)
|
||||
|
||||
|
||||
4. Click "Load unpacked"
|
||||
|
||||
|
||||
5. Select the browser-extension folder
|
||||
|
||||
|
||||
6. Extension is now installed! 🎉
|
||||
|
||||
For Firefox:
|
||||
@@ -34,10 +34,10 @@ For Firefox:
|
||||
1. Click the DocuElevate icon in toolbar
|
||||
2. Enter your server URL:
|
||||
https://your-docuelevate-server.com
|
||||
|
||||
|
||||
3. (Optional) Add session cookie if auth enabled:
|
||||
session=your_session_value
|
||||
|
||||
|
||||
4. Click "Save Configuration"
|
||||
5. Ready to use! 🚀
|
||||
|
||||
@@ -95,7 +95,7 @@ Complete guides available:
|
||||
Problem: Extension not appearing
|
||||
→ Check developer mode is enabled
|
||||
→ Reload the extension
|
||||
|
||||
|
||||
Problem: Can't connect to server
|
||||
→ Verify server URL is correct
|
||||
→ Check server is running
|
||||
|
||||
@@ -127,7 +127,7 @@ The extension can send any URL, but DocuElevate will only process supported file
|
||||
|
||||
**Cause**: The URL doesn't point to a supported file type.
|
||||
|
||||
**Solution**:
|
||||
**Solution**:
|
||||
- Verify the URL ends with a supported file extension
|
||||
- Check that the Content-Type header is set correctly by the server
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ When you first install the extension, you'll see the configuration screen:
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Dimensions**: 400px wide, ~300px height
|
||||
**Dimensions**: 400px wide, ~300px height
|
||||
**Colors**: Green buttons (#4CAF50), clean white background
|
||||
|
||||
### Send File View (Main Interface)
|
||||
@@ -180,7 +180,7 @@ After sending a file via context menu, a system notification appears:
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Notification Type**: Browser native notification
|
||||
**Notification Type**: Browser native notification
|
||||
**Duration**: Auto-dismiss after 5-10 seconds
|
||||
|
||||
## Chrome Extensions Page
|
||||
|
||||
@@ -19,20 +19,20 @@ const showConfigBtn = document.getElementById('show-config');
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Load saved configuration
|
||||
const config = await loadConfig();
|
||||
|
||||
|
||||
if (config.serverUrl) {
|
||||
serverUrlInput.value = config.serverUrl;
|
||||
}
|
||||
|
||||
|
||||
if (config.sessionCookie) {
|
||||
sessionCookieInput.value = config.sessionCookie;
|
||||
}
|
||||
|
||||
|
||||
// Get current tab URL
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
const currentUrl = tabs[0]?.url || '';
|
||||
currentUrlDisplay.textContent = currentUrl;
|
||||
|
||||
|
||||
// Show appropriate section
|
||||
if (config.serverUrl) {
|
||||
showSendSection();
|
||||
@@ -44,12 +44,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Save configuration
|
||||
saveConfigBtn.addEventListener('click', async () => {
|
||||
const serverUrl = serverUrlInput.value.trim();
|
||||
|
||||
|
||||
if (!serverUrl) {
|
||||
showStatus('Please enter a server URL', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(serverUrl);
|
||||
@@ -57,15 +57,15 @@ saveConfigBtn.addEventListener('click', async () => {
|
||||
showStatus('Invalid server URL format', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const config = {
|
||||
serverUrl: serverUrl,
|
||||
sessionCookie: sessionCookieInput.value.trim()
|
||||
};
|
||||
|
||||
|
||||
await saveConfig(config);
|
||||
showStatus('Configuration saved successfully!', 'success');
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
showSendSection();
|
||||
}, 1000);
|
||||
@@ -76,39 +76,39 @@ sendFileBtn.addEventListener('click', async () => {
|
||||
const config = await loadConfig();
|
||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
const currentUrl = tabs[0]?.url || '';
|
||||
|
||||
|
||||
if (!currentUrl) {
|
||||
showStatus('No URL found in current tab', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Disable button and show loading
|
||||
sendFileBtn.disabled = true;
|
||||
sendFileBtn.classList.add('loading');
|
||||
showStatus('Sending file to DocuElevate...', 'info');
|
||||
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
url: currentUrl,
|
||||
filename: filenameInput.value.trim() || null
|
||||
};
|
||||
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
|
||||
// Add session cookie if provided
|
||||
if (config.sessionCookie) {
|
||||
headers['Cookie'] = config.sessionCookie;
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch(`${config.serverUrl}/api/process-url`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
showStatus(
|
||||
|
||||
@@ -9,7 +9,7 @@ chrome.runtime.onInstalled.addListener((details) => {
|
||||
} else if (details.reason === 'update') {
|
||||
console.log('DocuElevate extension updated');
|
||||
}
|
||||
|
||||
|
||||
// Create context menu item
|
||||
chrome.contextMenus.create({
|
||||
id: 'send-to-docuelevate',
|
||||
@@ -31,36 +31,36 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
// Handle sending URL to DocuElevate
|
||||
async function handleSendUrl(data) {
|
||||
const { url, filename, serverUrl, sessionCookie } = data;
|
||||
|
||||
|
||||
if (!url || !serverUrl) {
|
||||
throw new Error('URL and server URL are required');
|
||||
}
|
||||
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
|
||||
if (sessionCookie) {
|
||||
headers['Cookie'] = sessionCookie;
|
||||
}
|
||||
|
||||
|
||||
const payload = {
|
||||
url: url,
|
||||
filename: filename || null
|
||||
};
|
||||
|
||||
|
||||
const response = await fetch(`${serverUrl}/api/process-url`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
@@ -69,18 +69,18 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
if (info.menuItemId === 'send-to-docuelevate') {
|
||||
// Get the URL to send (link URL or page URL)
|
||||
const targetUrl = info.linkUrl || info.pageUrl;
|
||||
|
||||
|
||||
// Load configuration
|
||||
const config = await new Promise((resolve) => {
|
||||
chrome.storage.sync.get(['serverUrl', 'sessionCookie'], resolve);
|
||||
});
|
||||
|
||||
|
||||
if (!config.serverUrl) {
|
||||
// Open popup to configure
|
||||
chrome.action.openPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Send the URL
|
||||
try {
|
||||
const result = await handleSendUrl({
|
||||
@@ -88,7 +88,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
serverUrl: config.serverUrl,
|
||||
sessionCookie: config.sessionCookie
|
||||
});
|
||||
|
||||
|
||||
// Show success notification
|
||||
chrome.notifications.create({
|
||||
type: 'basic',
|
||||
|
||||
@@ -24,7 +24,7 @@ function isDirectFileUrl(url) {
|
||||
'.txt', '.csv', '.rtf', '.jpg', '.jpeg', '.png', '.gif',
|
||||
'.bmp', '.tiff', '.webp', '.svg'
|
||||
];
|
||||
|
||||
|
||||
const urlLower = url.toLowerCase();
|
||||
return fileExtensions.some(ext => urlLower.endsWith(ext));
|
||||
}
|
||||
|
||||
+11
-11
@@ -52,7 +52,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>🧪 DocuElevate Browser Extension Test Page</h1>
|
||||
|
||||
|
||||
<div class="instructions">
|
||||
<h2>How to Test</h2>
|
||||
<ol>
|
||||
@@ -66,18 +66,18 @@
|
||||
<div class="test-section">
|
||||
<h2>📄 Sample Document Links</h2>
|
||||
<p>These links point to sample documents that can be processed by DocuElevate:</p>
|
||||
|
||||
<a href="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
|
||||
|
||||
<a href="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
|
||||
class="test-link" target="_blank">
|
||||
📕 Sample PDF Document (dummy.pdf)
|
||||
</a>
|
||||
|
||||
<a href="https://file-examples.com/storage/fe783c0bd90d0e9c119e536/2017/10/file-sample_150kB.pdf"
|
||||
|
||||
<a href="https://file-examples.com/storage/fe783c0bd90d0e9c119e536/2017/10/file-sample_150kB.pdf"
|
||||
class="test-link" target="_blank">
|
||||
📗 Example PDF File (file-sample_150kB.pdf)
|
||||
</a>
|
||||
|
||||
<a href="https://www.learningcontainer.com/wp-content/uploads/2019/09/sample-pdf-file.pdf"
|
||||
|
||||
<a href="https://www.learningcontainer.com/wp-content/uploads/2019/09/sample-pdf-file.pdf"
|
||||
class="test-link" target="_blank">
|
||||
📘 Learning Container Sample PDF
|
||||
</a>
|
||||
@@ -86,13 +86,13 @@
|
||||
<div class="test-section">
|
||||
<h2>🖼️ Sample Image Links</h2>
|
||||
<p>These links point to sample images that can be processed:</p>
|
||||
|
||||
<a href="https://via.placeholder.com/800x600.png"
|
||||
|
||||
<a href="https://via.placeholder.com/800x600.png"
|
||||
class="test-link" target="_blank">
|
||||
🖼️ Placeholder Image (PNG, 800x600)
|
||||
</a>
|
||||
|
||||
<a href="https://via.placeholder.com/1024x768.jpg"
|
||||
|
||||
<a href="https://via.placeholder.com/1024x768.jpg"
|
||||
class="test-link" target="_blank">
|
||||
📷 Placeholder Image (JPG, 1024x768)
|
||||
</a>
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+4
-4
@@ -71,16 +71,16 @@ def make_api_request(url, max_retries=3):
|
||||
"""Make API request with rate limit handling."""
|
||||
for attempt in range(max_retries):
|
||||
response = requests.get(url)
|
||||
|
||||
|
||||
if response.status_code == 429:
|
||||
# Rate limit exceeded
|
||||
retry_after = int(response.headers.get('Retry-After', 60))
|
||||
print(f"Rate limit exceeded. Retrying after {retry_after} seconds...")
|
||||
time.sleep(retry_after)
|
||||
continue
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
raise Exception("Max retries exceeded")
|
||||
```
|
||||
|
||||
@@ -203,7 +203,7 @@ The DocuElevate browser extension uses this endpoint to send files directly from
|
||||
|
||||
Upload one or more files from your computer for processing.
|
||||
|
||||
**Request**:
|
||||
**Request**:
|
||||
- Multipart form data with file(s)
|
||||
|
||||
**Response**:
|
||||
|
||||
@@ -79,7 +79,7 @@ For larger deployments or when you need more advanced authentication features, O
|
||||
### 1. Create an Application in Authentik
|
||||
|
||||
1. Log in to your Authentik admin interface
|
||||
2. Navigate to "Applications" > "Applications"
|
||||
2. Navigate to "Applications" > "Applications"
|
||||
3. Click "Create"
|
||||
4. Fill in the following details:
|
||||
- **Name**: DocuElevate
|
||||
|
||||
@@ -243,7 +243,7 @@ docker build -t docuelevate .
|
||||
|
||||
**Cause:** Building outside of a Git repository
|
||||
|
||||
**Solution:**
|
||||
**Solution:**
|
||||
- Clone the repository properly with `.git` directory
|
||||
- Or set `GIT_COMMIT_SHA` environment variable
|
||||
|
||||
@@ -261,7 +261,7 @@ docker build --no-cache -t docuelevate .
|
||||
|
||||
**Cause:** Files listed in `.dockerignore`
|
||||
|
||||
**Solution:**
|
||||
**Solution:**
|
||||
- Check `.dockerignore` doesn't block `BUILD_DATE`, `GIT_SHA`, or `RUNTIME_INFO`
|
||||
- The `VERSION` file should always be committed to git
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ PAPERLESS_CUSTOM_FIELDS_MAPPING='{"absender": "Sender", "empfaenger": "Recipient
|
||||
|
||||
### Dropbox
|
||||
|
||||
| **Variable** | **Description** |
|
||||
| **Variable** | **Description** |
|
||||
|-------------------------|--------------------------------------------------|
|
||||
| `DROPBOX_APP_KEY` | Dropbox API app key. |
|
||||
| `DROPBOX_APP_SECRET` | Dropbox API app secret. |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# DocuElevate Configuration
|
||||
# DocuElevate Configuration
|
||||
|
||||
This section contains detailed documentation about configuring DocuElevate for your environment.
|
||||
|
||||
|
||||
@@ -106,17 +106,17 @@ Add security headers to your Nginx configuration:
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name docuelevate.example.com;
|
||||
|
||||
|
||||
# SSL configuration
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8000;
|
||||
proxy_set_header Host $host;
|
||||
@@ -185,7 +185,7 @@ For production use, we recommend setting up a reverse proxy (like Nginx or Traef
|
||||
server {
|
||||
listen 80;
|
||||
server_name docuelevate.example.com;
|
||||
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8000;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
@@ -4,7 +4,7 @@ This guide explains how to set up the Dropbox integration for DocuElevate.
|
||||
|
||||
## Required Configuration Parameters
|
||||
|
||||
| **Variable** | **Description** |
|
||||
| **Variable** | **Description** |
|
||||
|-------------------------|--------------------------------------------------|
|
||||
| `DROPBOX_APP_KEY` | Dropbox API app key |
|
||||
| `DROPBOX_APP_SECRET` | Dropbox API app secret |
|
||||
@@ -64,11 +64,11 @@ The wizard handles all the token exchange steps and provides you with the exact
|
||||
1. Go to the "OAuth 2" tab in your app settings
|
||||
2. Add a redirect URI: `http://localhost` (this is for the authorization flow)
|
||||
3. Generate an authorization URL with these instructions:
|
||||
|
||||
|
||||
```
|
||||
https://www.dropbox.com/oauth2/authorize?client_id=YOUR_APP_KEY&response_type=code&token_access_type=offline
|
||||
```
|
||||
|
||||
|
||||
4. Replace `YOUR_APP_KEY` with your app key
|
||||
5. Open this URL in your browser
|
||||
6. Authorize the app when prompted
|
||||
@@ -78,7 +78,7 @@ The wizard handles all the token exchange steps and provides you with the exact
|
||||
### 5. Exchange the Code for a Refresh Token
|
||||
|
||||
1. Use this curl command to exchange the code for tokens:
|
||||
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.dropboxapi.com/oauth2/token \
|
||||
-d code=YOUR_AUTH_CODE \
|
||||
@@ -87,7 +87,7 @@ The wizard handles all the token exchange steps and provides you with the exact
|
||||
-d client_secret=YOUR_APP_SECRET \
|
||||
-d redirect_uri=http://localhost
|
||||
```
|
||||
|
||||
|
||||
2. From the response, copy the `refresh_token` value
|
||||
|
||||
### 6. Configure DocuElevate
|
||||
|
||||
@@ -108,4 +108,3 @@ Text extraction logs to check for `extracted_text`:
|
||||
- `process_with_azure_document_intelligence` - Azure OCR processing
|
||||
|
||||
Both may contain extracted text in the `detail` field when `status = 'success'`.
|
||||
|
||||
|
||||
@@ -215,9 +215,9 @@ for step in ["hash_file", "create_file_record", "check_text", ...]:
|
||||
# Start
|
||||
update_step_status(db, file.id, step, "in_progress", started_at=now())
|
||||
log_event(db, file.id, step, "in_progress", "Starting...")
|
||||
|
||||
|
||||
# Do work...
|
||||
|
||||
|
||||
# Complete
|
||||
update_step_status(db, file.id, step, "success", completed_at=now())
|
||||
log_event(db, file.id, step, "success", "Completed successfully")
|
||||
@@ -349,6 +349,6 @@ For deploying to existing system:
|
||||
- [ ] Run migration utility: `migrate_all_files(db, dry_run=True)` to test
|
||||
- [ ] Run actual migration: `migrate_all_files(db, dry_run=False)`
|
||||
- [ ] Verify: Check a few files with `verify_migration()`
|
||||
- [ ] Update workers to call `update_step_status()`
|
||||
- [ ] Update workers to call `update_step_status()`
|
||||
- [ ] Update file creation to call `initialize_file_steps()`
|
||||
- [ ] Monitor dashboard for correct status display
|
||||
|
||||
+35
-35
@@ -16,19 +16,19 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
|
||||
- name: Run OAuth tests (mock)
|
||||
run: |
|
||||
pytest tests/test_oauth_integration_flows.py -v
|
||||
@@ -48,19 +48,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if secrets are available (not on external PRs)
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
|
||||
- name: Run OAuth tests (real)
|
||||
env:
|
||||
AUTHENTIK_CLIENT_ID: ${{ secrets.AUTHENTIK_CLIENT_ID }}
|
||||
@@ -84,43 +84,43 @@ jobs:
|
||||
test-mock-oauth:
|
||||
name: OAuth Tests (Mock)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
|
||||
- name: Run mock OAuth tests
|
||||
run: |
|
||||
pytest tests/test_oauth_integration_flows.py \
|
||||
-v \
|
||||
-m "not requires_external"
|
||||
|
||||
|
||||
test-real-oauth:
|
||||
name: OAuth Tests (Real - Internal Only)
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on internal commits where secrets are available
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
|
||||
- name: Run real OAuth tests
|
||||
env:
|
||||
AUTHENTIK_CLIENT_ID: ${{ secrets.AUTHENTIK_CLIENT_ID }}
|
||||
@@ -157,7 +157,7 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
services:
|
||||
mock-oauth:
|
||||
image: ghcr.io/navikt/mock-oauth2-server:2.1.1
|
||||
@@ -168,24 +168,24 @@ jobs:
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
|
||||
- name: Configure OAuth to use service
|
||||
run: |
|
||||
export OAUTH_MOCK_URL=http://localhost:8080
|
||||
export USE_MOCK_OAUTH=true
|
||||
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
pytest tests/test_oauth_integration_flows.py -v
|
||||
@@ -255,7 +255,7 @@ Add this step to debug mock OAuth server issues:
|
||||
|
||||
**Cause**: Real OAuth credentials not configured or not accessible.
|
||||
|
||||
**Solution**:
|
||||
**Solution**:
|
||||
- For local dev: Use mock mode (default)
|
||||
- For CI: Add secrets to GitHub repository settings
|
||||
- Check secret availability: `if github.event_name == 'push'`
|
||||
@@ -291,20 +291,20 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
# Fast mock OAuth tests (always run)
|
||||
mock-oauth-tests:
|
||||
name: OAuth Tests (Mock)
|
||||
name: OAuth Tests (Mock)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements-dev.txt
|
||||
|
||||
|
||||
- name: Run mock OAuth tests
|
||||
run: |
|
||||
pytest tests/test_oauth_integration_flows.py \
|
||||
@@ -312,7 +312,7 @@ jobs:
|
||||
-m "not requires_external" \
|
||||
--cov=app.auth \
|
||||
--cov-report=term-missing
|
||||
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
@@ -324,18 +324,18 @@ jobs:
|
||||
name: OAuth Tests (Real - Internal)
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements-dev.txt
|
||||
|
||||
|
||||
- name: Run real OAuth tests
|
||||
env:
|
||||
AUTHENTIK_CLIENT_ID: ${{ secrets.AUTHENTIK_CLIENT_ID }}
|
||||
@@ -345,7 +345,7 @@ jobs:
|
||||
pytest tests/test_oauth_integration_flows.py \
|
||||
-v \
|
||||
-m requires_external
|
||||
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
|
||||
@@ -138,7 +138,7 @@ If you're using a work/school account provided by your organization:
|
||||
### 2. Configuration based on use case
|
||||
|
||||
**Option A: Access your own OneDrive (Interactive Login)**
|
||||
|
||||
|
||||
This option requires a refresh token:
|
||||
1. Use the auth wizard with your tenant ID, or
|
||||
2. Follow the same manual steps as for personal accounts, but use your work email to sign in
|
||||
@@ -146,7 +146,7 @@ This option requires a refresh token:
|
||||
4. Set the refresh token you receive as `ONEDRIVE_REFRESH_TOKEN`
|
||||
|
||||
**Option B: Access OneDrive as a system service (App-only access)**
|
||||
|
||||
|
||||
This option is for service accounts or automated systems with no user interaction:
|
||||
1. In API permissions, add "Application permissions" instead of "Delegated permissions"
|
||||
2. Add `Files.ReadWrite.All` permission under "Application permissions"
|
||||
@@ -158,15 +158,15 @@ This option is for service accounts or automated systems with no user interactio
|
||||
|
||||
If you encounter errors during authentication:
|
||||
|
||||
1. **Check account permissions**:
|
||||
1. **Check account permissions**:
|
||||
- Ensure your Microsoft account has the necessary permissions to grant access
|
||||
- For corporate accounts, check if your admin has restricted third-party app access
|
||||
|
||||
2. **Permission errors**:
|
||||
2. **Permission errors**:
|
||||
- Verify the app registration has the correct API permissions
|
||||
- For corporate accounts, ensure an admin has consented to the permissions
|
||||
|
||||
3. **Refresh token expired**:
|
||||
3. **Refresh token expired**:
|
||||
- If uploads stop working, you can generate a new refresh token using the auth wizard
|
||||
- Click on "Refresh Token" in the OneDrive setup page
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ from locust import HttpUser, task, between
|
||||
|
||||
class APIUser(HttpUser):
|
||||
wait_time = between(0.1, 0.5)
|
||||
|
||||
|
||||
@task
|
||||
def get_files(self):
|
||||
self.client.get("/api/files")
|
||||
|
||||
@@ -93,7 +93,7 @@ The current value displayed is **always** the effective value after applying pre
|
||||
6. Successfully saved settings will show a 🟢 DB badge
|
||||
7. If any changed setting requires a restart, you'll be notified
|
||||
|
||||
**Important**:
|
||||
**Important**:
|
||||
- You don't need to fill all fields - only change what you want to override
|
||||
- Saving a setting to the database makes it override environment variables
|
||||
- Empty fields are ignored (won't clear existing values)
|
||||
@@ -196,7 +196,7 @@ Settings are stored in the `application_settings` table with:
|
||||
### Can't Access Settings Page
|
||||
|
||||
- **Check authentication**: Make sure you're logged in
|
||||
- **Check admin status**:
|
||||
- **Check admin status**:
|
||||
- Local auth: Verify `ADMIN_USERNAME` and `ADMIN_PASSWORD` are correct
|
||||
- OAuth: Verify your user is in the admin group (configurable via `ADMIN_GROUP_NAME`)
|
||||
- **Check logs**: Look for "Non-admin user attempted to access settings page" messages
|
||||
@@ -243,5 +243,5 @@ python3 test_integration.py
|
||||
## Related Documentation
|
||||
|
||||
- [Configuration Guide](./ConfigurationGuide.md) - Environment variable reference
|
||||
- [Deployment Guide](./DeploymentGuide.md) - Production deployment
|
||||
- [Deployment Guide](./DeploymentGuide.md) - Production deployment
|
||||
- [API Documentation](./API.md) - Full API reference
|
||||
|
||||
@@ -223,7 +223,7 @@ curl -X POST "http://localhost:8000/api/files/123/reprocess-with-cloud-ocr" \
|
||||
```python
|
||||
class FileRecord(Base):
|
||||
__tablename__ = "files"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
filehash = Column(String, unique=True, nullable=False)
|
||||
original_filename = Column(String) # User's original name
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Repository Analysis & Improvement Summary
|
||||
|
||||
**Date:** 2026-02-06
|
||||
**Repository:** christianlouis/DocuElevate
|
||||
**Date:** 2026-02-06
|
||||
**Repository:** christianlouis/DocuElevate
|
||||
**Current Version:** v0.5.0
|
||||
|
||||
## Executive Summary
|
||||
@@ -343,6 +343,6 @@ httpx>=0.26.0
|
||||
|
||||
---
|
||||
|
||||
**Prepared by:** GitHub Copilot Agent
|
||||
**Review Status:** Ready for maintainer review
|
||||
**Prepared by:** GitHub Copilot Agent
|
||||
**Review Status:** Ready for maintainer review
|
||||
**Recommended Action:** Merge and continue with TODO.md priorities
|
||||
|
||||
@@ -61,7 +61,7 @@ Created comprehensive unit tests to verify:
|
||||
1. **Test 1**: Original filename is preserved when parameter is provided
|
||||
- Uploads a file with UUID-based path but provides original filename "Apostille Sverige.pdf"
|
||||
- Verifies the database stores the original filename, not the UUID-based path
|
||||
|
||||
|
||||
2. **Test 2**: Backward compatibility is maintained
|
||||
- Calls `process_document` without the optional parameter
|
||||
- Verifies it falls back to extracting filename from path
|
||||
|
||||
@@ -170,7 +170,7 @@ Phase 1 (Current - MVP):
|
||||
Settings: DB + ENV + DEFAULT
|
||||
Encryption: Fernet (app-level)
|
||||
UI: Custom settings page
|
||||
|
||||
|
||||
Phase 2 (Production - Optional):
|
||||
Settings: DB + ENV + DEFAULT (keep)
|
||||
Secrets: HashiCorp Vault (add)
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
**Status: 100% COMPLETE (Code Implementation)** ✅
|
||||
|
||||
✅ Core settings functionality: 100% complete
|
||||
✅ Encryption implementation: 100% complete
|
||||
✅ Encryption implementation: 100% complete
|
||||
✅ Setup wizard: 100% complete
|
||||
⚠️ Testing: Manual testing recommended
|
||||
⚠️ Documentation: Enhancement recommended
|
||||
|
||||
@@ -121,7 +121,7 @@ Created comprehensive `docs/SettingsManagement.md` covering:
|
||||
# Non-admin users
|
||||
/settings → @require_login → @require_admin_access → Redirect to /
|
||||
|
||||
# Admin users
|
||||
# Admin users
|
||||
/settings → @require_login → @require_admin_access → Settings page renders
|
||||
```
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ A comprehensive security audit was conducted on all file path operations in Docu
|
||||
|
||||
### 1. Critical: Path Traversal via GPT Metadata Filename
|
||||
|
||||
**Severity:** CRITICAL
|
||||
**Location:** `app/tasks/embed_metadata_into_pdf.py` (line 144)
|
||||
**Severity:** CRITICAL
|
||||
**Location:** `app/tasks/embed_metadata_into_pdf.py` (line 144)
|
||||
**Status:** ✅ FIXED
|
||||
|
||||
**Description:**
|
||||
**Description:**
|
||||
The `metadata["filename"]` field extracted by GPT was used directly in file path construction without sanitization. A malicious document could be crafted to make GPT return metadata containing path traversal sequences (e.g., `../../etc/passwd`), allowing file writes outside the intended directory.
|
||||
|
||||
**Attack Scenario:**
|
||||
@@ -39,11 +39,11 @@ final_path = os.path.join(processed_dir, suggested_filename) # Safe
|
||||
|
||||
### 2. Medium: Insecure String-Based Path Validation
|
||||
|
||||
**Severity:** MEDIUM
|
||||
**Location:** `app/tasks/embed_metadata_into_pdf.py` (line 188)
|
||||
**Severity:** MEDIUM
|
||||
**Location:** `app/tasks/embed_metadata_into_pdf.py` (line 188)
|
||||
**Status:** ✅ FIXED
|
||||
|
||||
**Description:**
|
||||
**Description:**
|
||||
Used insecure string-based `startswith()` check to validate file paths before deletion. This approach is vulnerable to:
|
||||
- Partial directory name matches
|
||||
- Symlink attacks (symlinks not resolved)
|
||||
@@ -78,11 +78,11 @@ if original_file_path.is_relative_to(workdir_tmp_resolved):
|
||||
|
||||
### 3. Medium: Insufficient GPT Filename Validation
|
||||
|
||||
**Severity:** MEDIUM
|
||||
**Location:** `app/tasks/extract_metadata_with_gpt.py`
|
||||
**Severity:** MEDIUM
|
||||
**Location:** `app/tasks/extract_metadata_with_gpt.py`
|
||||
**Status:** ✅ FIXED
|
||||
|
||||
**Description:**
|
||||
**Description:**
|
||||
While the GPT prompt requested specific filename format, there was no enforcement. GPT could return filenames with path separators or traversal patterns.
|
||||
|
||||
**Fix Applied:**
|
||||
@@ -221,7 +221,7 @@ All identified path traversal vulnerabilities have been successfully remediated
|
||||
|
||||
---
|
||||
|
||||
**Audit Date:** February 10, 2026
|
||||
**Auditor:** GitHub Copilot Agent
|
||||
**Scope:** All Python file path operations
|
||||
**Audit Date:** February 10, 2026
|
||||
**Auditor:** GitHub Copilot Agent
|
||||
**Scope:** All Python file path operations
|
||||
**Next Review:** Recommended within 6 months or after significant file handling changes
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M96 128a128 128 0 1 0 256 0A128 128 0 1 0 96 128zm94.5 200.2l18.6 31L175.8 483.1l-36-146.9c-2-8.1-9.8-13.4-17.9-11.3C51.9 342.4 0 405.8 0 481.3c0 17 13.8 30.7 30.7 30.7l131.7 0c0 0 0 0 .1 0l5.5 0 112 0 5.5 0c0 0 0 0 .1 0l131.7 0c17 0 30.7-13.8 30.7-30.7c0-75.5-51.9-138.9-121.9-156.4c-8.1-2-15.9 3.3-17.9 11.3l-36 146.9L238.9 359.2l18.6-31c6.4-10.7-1.3-24.2-13.7-24.2L224 304l-19.7 0c-12.4 0-20.1 13.6-13.7 24.2z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M96 128a128 128 0 1 0 256 0A128 128 0 1 0 96 128zm94.5 200.2l18.6 31L175.8 483.1l-36-146.9c-2-8.1-9.8-13.4-17.9-11.3C51.9 342.4 0 405.8 0 481.3c0 17 13.8 30.7 30.7 30.7l131.7 0c0 0 0 0 .1 0l5.5 0 112 0 5.5 0c0 0 0 0 .1 0l131.7 0c17 0 30.7-13.8 30.7-30.7c0-75.5-51.9-138.9-121.9-156.4c-8.1-2-15.9 3.3-17.9 11.3l-36 146.9L238.9 359.2l18.6-31c6.4-10.7-1.3-24.2-13.7-24.2L224 304l-19.7 0c-12.4 0-20.1 13.6-13.7 24.2z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 644 B |
@@ -7,15 +7,15 @@
|
||||
const response = await fetch('/api/auth/whoami');
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
|
||||
const authSection = document.getElementById("authSection");
|
||||
const mobileAuthSection = document.getElementById("mobileAuthSection");
|
||||
|
||||
|
||||
// If we have an email, user is authenticated (the whoami endpoint would have thrown 401 otherwise)
|
||||
if (data.email) {
|
||||
// Get the display name (prefer name, fall back to preferred_username, then email)
|
||||
const displayName = data.name || data.preferred_username || data.email;
|
||||
|
||||
|
||||
// User is logged in
|
||||
let authHTML = `
|
||||
<div class="flex items-center">
|
||||
@@ -26,11 +26,11 @@
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
if (authSection) {
|
||||
authSection.innerHTML = authHTML;
|
||||
}
|
||||
|
||||
|
||||
if (mobileAuthSection) {
|
||||
mobileAuthSection.innerHTML = `
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -49,7 +49,7 @@
|
||||
if (authSection) {
|
||||
authSection.innerHTML = `<a href="/login" class="text-blue-600">Login</a>`;
|
||||
}
|
||||
|
||||
|
||||
if (mobileAuthSection) {
|
||||
mobileAuthSection.innerHTML = `<a href="/login" class="text-blue-600">Login</a>`;
|
||||
}
|
||||
@@ -59,11 +59,11 @@
|
||||
// Fallback if whoami endpoint fails
|
||||
const authSection = document.getElementById("authSection");
|
||||
const mobileAuthSection = document.getElementById("mobileAuthSection");
|
||||
|
||||
|
||||
if (authSection) {
|
||||
authSection.innerHTML = `<a href="/login" class="text-blue-600">Login</a>`;
|
||||
}
|
||||
|
||||
|
||||
if (mobileAuthSection) {
|
||||
mobileAuthSection.innerHTML = `<a href="/login" class="text-blue-600">Login</a>`;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/static/fontawesome/css/all.min.css';
|
||||
document.head.appendChild(link);
|
||||
|
||||
|
||||
console.log('Font Awesome loaded locally');
|
||||
})();
|
||||
|
||||
@@ -8,30 +8,30 @@ const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
|
||||
const ACCEPTED_TYPES = {
|
||||
// PDF files
|
||||
'application/pdf': true,
|
||||
|
||||
|
||||
// Image formats
|
||||
'image/jpeg': true, 'image/jpg': true, 'image/png': true,
|
||||
'image/gif': true, 'image/bmp': true, 'image/tiff': true,
|
||||
'image/webp': true, 'image/svg+xml': true,
|
||||
|
||||
|
||||
// Office document formats - Word
|
||||
'application/msword': true,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': true,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template': true,
|
||||
'application/vnd.ms-word.document.macroEnabled.12': true,
|
||||
|
||||
|
||||
// Excel
|
||||
'application/vnd.ms-excel': true,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': true,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template': true,
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12': true,
|
||||
|
||||
|
||||
// PowerPoint
|
||||
'application/vnd.ms-powerpoint': true,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': true,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template': true,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow': true,
|
||||
|
||||
|
||||
// Other common formats
|
||||
'text/plain': true,
|
||||
'text/csv': true,
|
||||
@@ -57,16 +57,16 @@ const ACCEPTED_EXTENSIONS = [
|
||||
*/
|
||||
function processFiles(files, progressContainer, statusMessage) {
|
||||
if (files.length === 0) return;
|
||||
|
||||
|
||||
if (statusMessage) {
|
||||
statusMessage.textContent = `Processing ${files.length} file(s)...`;
|
||||
}
|
||||
|
||||
|
||||
// Clear previous upload progress
|
||||
if (progressContainer) {
|
||||
progressContainer.innerHTML = "";
|
||||
}
|
||||
|
||||
|
||||
// Process each file
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
@@ -94,32 +94,32 @@ function validateAndUpload(file, progressContainer, statusMessage) {
|
||||
</div>
|
||||
<div class="file-status text-xs text-gray-600 mt-1">Validating...</div>
|
||||
`;
|
||||
|
||||
|
||||
if (progressContainer) {
|
||||
progressContainer.appendChild(fileProgress);
|
||||
}
|
||||
|
||||
|
||||
const progressBar = fileProgress.querySelector(".file-progress-bar");
|
||||
const statusEl = fileProgress.querySelector(".file-status");
|
||||
|
||||
|
||||
// Validate file type by checking both MIME type and extension
|
||||
const isValidMimeType = ACCEPTED_TYPES[file.type] || false;
|
||||
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
|
||||
const isValidExtension = ACCEPTED_EXTENSIONS.includes(fileExtension);
|
||||
|
||||
|
||||
if (!isValidMimeType && !isValidExtension) {
|
||||
statusEl.textContent = `Error: ${file.name} - Unsupported file type`;
|
||||
statusEl.className = "text-xs text-red-500 mt-1";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
statusEl.textContent = `Error: ${file.name} - File size exceeds 500MB limit`;
|
||||
statusEl.className = "text-xs text-red-500 mt-1";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Upload the file
|
||||
uploadFile(file, progressBar, statusEl, statusMessage);
|
||||
}
|
||||
@@ -136,10 +136,10 @@ async function uploadFile(file, progressBar, statusEl, statusMessage) {
|
||||
try {
|
||||
let formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/ui-upload", true);
|
||||
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
@@ -147,7 +147,7 @@ async function uploadFile(file, progressBar, statusEl, statusMessage) {
|
||||
statusEl.textContent = `Uploading: ${Math.round(percentComplete)}%`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
const result = JSON.parse(xhr.responseText);
|
||||
@@ -160,13 +160,13 @@ async function uploadFile(file, progressBar, statusEl, statusMessage) {
|
||||
throw new Error(`Upload failed with status ${xhr.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
xhr.onerror = function() {
|
||||
throw new Error("Network error occurred");
|
||||
};
|
||||
|
||||
|
||||
xhr.send(formData);
|
||||
|
||||
|
||||
} catch (err) {
|
||||
statusEl.textContent = `Error: ${err.message}`;
|
||||
statusEl.className = "text-xs text-red-500 mt-1";
|
||||
@@ -181,21 +181,21 @@ async function uploadFile(file, progressBar, statusEl, statusMessage) {
|
||||
*/
|
||||
function updateOverallStatus(statusMessage) {
|
||||
if (!statusMessage) return;
|
||||
|
||||
|
||||
// Count success/failure
|
||||
const fileStatuses = document.querySelectorAll('.file-status');
|
||||
let completed = 0;
|
||||
let total = fileStatuses.length;
|
||||
|
||||
|
||||
fileStatuses.forEach(status => {
|
||||
if (status.textContent.includes('Success') || status.textContent.includes('Error')) {
|
||||
completed++;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (completed === total) {
|
||||
statusMessage.textContent = `All uploads completed (${completed}/${total})`;
|
||||
|
||||
|
||||
// Trigger a custom event when all uploads are complete
|
||||
const allUploadsComplete = new CustomEvent('allUploadsComplete', {
|
||||
detail: { total: total, completed: completed }
|
||||
@@ -231,38 +231,38 @@ function initDragAndDrop(element, progressContainer, statusMessage, options = {}
|
||||
console.error("Element not found for drag-and-drop initialization");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Add event listeners for drag-and-drop
|
||||
element.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
|
||||
|
||||
// Add visual feedback
|
||||
if (options.dragOverClass) {
|
||||
element.classList.add(options.dragOverClass);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
element.addEventListener("dragleave", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
// Remove visual feedback
|
||||
if (options.dragOverClass) {
|
||||
element.classList.remove(options.dragOverClass);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
element.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
// Remove visual feedback
|
||||
if (options.dragOverClass) {
|
||||
element.classList.remove(options.dragOverClass);
|
||||
}
|
||||
|
||||
|
||||
if (e.dataTransfer.files.length) {
|
||||
processFiles(e.dataTransfer.files, progressContainer, statusMessage);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
@@ -111,7 +111,7 @@ modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
@@ -158,7 +158,7 @@ Library.
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
@@ -216,7 +216,7 @@ instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
@@ -267,7 +267,7 @@ Library will still fall under Section 6.)
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
@@ -329,7 +329,7 @@ restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
@@ -370,7 +370,7 @@ subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
@@ -422,7 +422,7 @@ conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
@@ -456,7 +456,7 @@ SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
@@ -498,4 +498,4 @@ necessary. Here is a sample; alter the names:
|
||||
<signature of Moe Ghoul>, 1 April 1990
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
That's all there is to it!
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="m17.212 20.404l-.108-.885q-.57-.125-.938-.33q-.368-.204-.7-.577l-.835.334l-.539-.815l.689-.577q-.165-.531-.165-1.035t.165-1.034l-.689-.577l.539-.816l.835.335q.332-.393.7-.588q.369-.195.938-.32l.108-.885h1l.107.885q.57.125.938.32t.7.588l.835-.335l.539.816l-.689.576q.166.531.166 1.035t-.166 1.035l.689.577l-.539.815l-.834-.335q-.333.373-.701.578q-.369.205-.938.33l-.107.885zm.5-1.731q.882 0 1.518-.635q.636-.636.636-1.519t-.636-1.518t-1.518-.636t-1.519.636t-.635 1.518t.635 1.519t1.518.635M4 18V6v4.435V10zm.616 1q-.691 0-1.153-.462T3 17.384V6.616q0-.691.463-1.153T4.615 5h4.981l2 2h7.789q.69 0 1.153.463T21 8.616v2.294q-.238-.152-.479-.265q-.24-.112-.521-.21v-1.82q0-.269-.173-.442T19.385 8h-8.19l-2-2h-4.58q-.269 0-.442.173T4 6.616v10.769q0 .269.173.442t.443.173h6.748q.055.275.131.515t.186.485z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="m17.212 20.404l-.108-.885q-.57-.125-.938-.33q-.368-.204-.7-.577l-.835.334l-.539-.815l.689-.577q-.165-.531-.165-1.035t.165-1.034l-.689-.577l.539-.816l.835.335q.332-.393.7-.588q.369-.195.938-.32l.108-.885h1l.107.885q.57.125.938.32t.7.588l.835-.335l.539.816l-.689.576q.166.531.166 1.035t-.166 1.035l.689.577l-.539.815l-.834-.335q-.333.373-.701.578q-.369.205-.938.33l-.107.885zm.5-1.731q.882 0 1.518-.635q.636-.636.636-1.519t-.636-1.518t-1.518-.636t-1.519.636t-.635 1.518t.635 1.519t1.518.635M4 18V6v4.435V10zm.616 1q-.691 0-1.153-.462T3 17.384V6.616q0-.691.463-1.153T4.615 5h4.981l2 2h7.789q.69 0 1.153.463T21 8.616v2.294q-.238-.152-.479-.265q-.24-.112-.521-.21v-1.82q0-.269-.173-.442T19.385 8h-8.19l-2-2h-4.58q-.269 0-.442.173T4 6.616v10.769q0 .269.173.442t.443.173h6.748q.055.275.131.515t.186.485z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 918 B |
@@ -15,4 +15,4 @@ body {
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
Generic single-database configuration.
|
||||
Generic single-database configuration.
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2026-02-11
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2026-02-11
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2026-02-12
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
@@ -21,13 +21,13 @@ def upgrade() -> None:
|
||||
"""Add is_duplicate and duplicate_of_id columns to files table."""
|
||||
# Add is_duplicate column with default False
|
||||
op.add_column("files", sa.Column("is_duplicate", sa.Boolean(), nullable=False, server_default="0"))
|
||||
|
||||
|
||||
# Add duplicate_of_id column as foreign key to self
|
||||
op.add_column("files", sa.Column("duplicate_of_id", sa.Integer(), nullable=True))
|
||||
|
||||
|
||||
# Create index on is_duplicate for efficient filtering
|
||||
op.create_index("ix_files_is_duplicate", "files", ["is_duplicate"])
|
||||
|
||||
|
||||
# Create foreign key relationship
|
||||
op.create_foreign_key("fk_files_duplicate_of_id", "files", "files", ["duplicate_of_id"], ["id"])
|
||||
|
||||
@@ -36,10 +36,10 @@ def downgrade() -> None:
|
||||
"""Remove is_duplicate and duplicate_of_id columns from files table."""
|
||||
# Drop foreign key
|
||||
op.drop_constraint("fk_files_duplicate_of_id", "files", type_="foreignkey")
|
||||
|
||||
|
||||
# Drop index
|
||||
op.drop_index("ix_files_is_duplicate", table_name="files")
|
||||
|
||||
|
||||
# Drop columns
|
||||
op.drop_column("files", "duplicate_of_id")
|
||||
op.drop_column("files", "is_duplicate")
|
||||
|
||||
+3
-1
@@ -124,10 +124,12 @@ ignore = [
|
||||
"PLR0911", # Too many return statements
|
||||
"PLR2004", # Magic value comparison
|
||||
"PLW0603", # Global statement
|
||||
"B008", # Function call in default argument (FastAPI Depends pattern)
|
||||
"B904", # Raise without from (overly strict for error handlers)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = ["S101", "S110", "B017"] # Allow assert, try-except-pass, assert-raises-exception in tests
|
||||
"tests/*" = ["S101", "S110", "B017", "F841", "B007", "E402"] # Allow assert, try-except-pass, assert-raises-exception, unused vars, unused loop vars, module imports not at top in tests
|
||||
|
||||
# pytest configuration
|
||||
[tool.pytest.ini_options]
|
||||
|
||||
@@ -30,4 +30,4 @@ pre-commit>=3.6.0
|
||||
pip-licenses==5.5.1 # For license compliance checking
|
||||
|
||||
# Release automation
|
||||
python-semantic-release>=9.0.0
|
||||
python-semantic-release>=9.0.0
|
||||
|
||||
+1
-1
@@ -34,4 +34,4 @@ boto3>=1.28.0
|
||||
paramiko>=3.4.0 # SSH/SFTP implementation for Python (LGPL license)
|
||||
|
||||
# Notification service
|
||||
apprise>=1.4.0
|
||||
apprise>=1.4.0
|
||||
|
||||
@@ -37,13 +37,13 @@ if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
GIT_SHA=$(git rev-parse --short=7 HEAD)
|
||||
echo "${GIT_SHA}" > GIT_SHA
|
||||
echo "✓ GIT_SHA: ${GIT_SHA}"
|
||||
|
||||
|
||||
# Get full commit SHA for reference
|
||||
GIT_FULL_SHA=$(git rev-parse HEAD)
|
||||
|
||||
|
||||
# Get commit date
|
||||
GIT_COMMIT_DATE=$(git log -1 --format=%cd --date=iso-strict)
|
||||
|
||||
|
||||
# Get branch name (if available)
|
||||
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
||||
else
|
||||
|
||||
@@ -10,7 +10,7 @@ Unlike unit tests that mock external dependencies, these integration tests use *
|
||||
- **Redis** - Real message broker for Celery tasks
|
||||
- **Gotenberg** - Real PDF conversion service
|
||||
- **WebDAV Server** - Real upload target
|
||||
- **SFTP Server** - Real SSH/SFTP server
|
||||
- **SFTP Server** - Real SSH/SFTP server
|
||||
- **MinIO** - Real S3-compatible object storage
|
||||
- **FTP Server** - Real FTP server
|
||||
|
||||
@@ -202,15 +202,15 @@ def test_celery_tasks(celery_app, celery_worker):
|
||||
def test_upload_to_webdav(webdav_container, sample_text_file):
|
||||
"""Upload file to real WebDAV server and verify."""
|
||||
from app.tasks.upload_to_webdav import upload_to_webdav
|
||||
|
||||
|
||||
with patch("app.tasks.upload_to_webdav.settings") as mock_settings:
|
||||
mock_settings.webdav_url = webdav_container["url"] + "/"
|
||||
mock_settings.webdav_username = webdav_container["username"]
|
||||
mock_settings.webdav_password = webdav_container["password"]
|
||||
|
||||
|
||||
# Execute upload
|
||||
result = upload_to_webdav.apply(args=[sample_text_file]).get()
|
||||
|
||||
|
||||
# Verify on server
|
||||
response = requests.get(
|
||||
f"{webdav_container['url']}/test.txt",
|
||||
@@ -226,14 +226,14 @@ def test_upload_to_webdav(webdav_container, sample_text_file):
|
||||
def test_async_upload(redis_container, webdav_container, celery_worker, sample_text_file):
|
||||
"""Queue task in Redis, worker executes, uploads to WebDAV."""
|
||||
from app.tasks.upload_to_webdav import upload_to_webdav
|
||||
|
||||
|
||||
# Queue task (goes to Redis)
|
||||
result = upload_to_webdav.delay(sample_text_file, file_id=1)
|
||||
|
||||
|
||||
# Wait for worker to process
|
||||
while not result.ready():
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
# Verify result
|
||||
assert result.get()["status"] == "Completed"
|
||||
```
|
||||
@@ -299,9 +299,9 @@ Set a breakpoint after test to inspect:
|
||||
```python
|
||||
def test_inspect(webdav_container):
|
||||
result = upload_file()
|
||||
|
||||
|
||||
import pdb; pdb.set_trace() # Container still running here
|
||||
|
||||
|
||||
# Manually inspect: docker ps, docker logs, etc.
|
||||
```
|
||||
|
||||
@@ -391,24 +391,24 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
services:
|
||||
docker:
|
||||
image: docker:latest
|
||||
options: --privileged
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
pytest -m "integration or e2e" -v --tb=short
|
||||
|
||||
@@ -98,7 +98,7 @@ async def test_oauth_callback(mock_authorize, oauth_enabled_app, test_user_info)
|
||||
"access_token": "test-token",
|
||||
"userinfo": test_user_info,
|
||||
}
|
||||
|
||||
|
||||
response = oauth_enabled_app.get("/oauth-callback?code=test-code")
|
||||
assert response.status_code == 302 # Redirects after login
|
||||
```
|
||||
|
||||
@@ -244,9 +244,9 @@ class TestAzureDocumentIntelligenceIntegration:
|
||||
assert len(result.content) > 10, f"OCR text too short: {result.content[:50]}"
|
||||
|
||||
# Verify the generated text is recognizable
|
||||
assert "Acme" in result.content or "Invoice" in result.content, (
|
||||
f"OCR text does not contain expected keywords: {result.content[:200]}"
|
||||
)
|
||||
assert (
|
||||
"Acme" in result.content or "Invoice" in result.content
|
||||
), f"OCR text does not contain expected keywords: {result.content[:200]}"
|
||||
|
||||
# Retrieve the searchable PDF output
|
||||
operation_id = poller.details["operation_id"]
|
||||
@@ -602,9 +602,9 @@ class TestFullOCRMetadataPipeline:
|
||||
|
||||
# The generated invoice should be classified reasonably
|
||||
doc_type = metadata["document_type"].lower()
|
||||
assert any(kw in doc_type for kw in ("invoice", "rechnung", "bill")), (
|
||||
f"Unexpected document_type: {metadata['document_type']}"
|
||||
)
|
||||
assert any(
|
||||
kw in doc_type for kw in ("invoice", "rechnung", "bill")
|
||||
), f"Unexpected document_type: {metadata['document_type']}"
|
||||
finally:
|
||||
os.unlink(pdf_path)
|
||||
|
||||
|
||||
@@ -83,9 +83,9 @@ class TestSplitPdfBySize:
|
||||
# that can cause files to exceed the target size by ~20-50%. We allow 1.5x (50%) margin.
|
||||
PDF_OVERHEAD_MULTIPLIER = 1.5
|
||||
for split_file in split_files:
|
||||
assert os.path.getsize(split_file) <= max_size * PDF_OVERHEAD_MULTIPLIER, (
|
||||
f"Split file {split_file} should respect size limit (with PDF overhead allowance)"
|
||||
)
|
||||
assert (
|
||||
os.path.getsize(split_file) <= max_size * PDF_OVERHEAD_MULTIPLIER
|
||||
), f"Split file {split_file} should respect size limit (with PDF overhead allowance)"
|
||||
|
||||
# Cleanup split files
|
||||
for split_file in split_files:
|
||||
|
||||
@@ -142,9 +142,9 @@ def test_x_frame_options_valid_value(client):
|
||||
x_frame_value = response.headers["X-Frame-Options"]
|
||||
valid_values = ["DENY", "SAMEORIGIN"]
|
||||
# Note: ALLOW-FROM is deprecated in modern browsers; use CSP frame-ancestors instead
|
||||
assert x_frame_value in valid_values or x_frame_value.startswith("ALLOW-FROM"), (
|
||||
f"Invalid X-Frame-Options value: {x_frame_value}"
|
||||
)
|
||||
assert x_frame_value in valid_values or x_frame_value.startswith(
|
||||
"ALLOW-FROM"
|
||||
), f"Invalid X-Frame-Options value: {x_frame_value}"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
|
||||
@@ -305,9 +305,9 @@ class TestWebDAVIntegration:
|
||||
response = requests.get(file_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=10)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.content) == 1024 * 1024, (
|
||||
f"File size mismatch: expected 1MB, got {len(response.content)} bytes"
|
||||
)
|
||||
assert (
|
||||
len(response.content) == 1024 * 1024
|
||||
), f"File size mismatch: expected 1MB, got {len(response.content)} bytes"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
|
||||
Reference in New Issue
Block a user