Merge pull request #19 from christianlouis/ui-improvements

Refactored frontend + added template engine
This commit is contained in:
Christian Krakau-Louis
2025-03-26 02:48:10 +01:00
committed by GitHub
16 changed files with 361 additions and 531 deletions
-1
View File
@@ -1,5 +1,4 @@
# app/auth.py # app/auth.py
# app/auth.py
import os import os
from functools import wraps from functools import wraps
+23 -31
View File
@@ -1,39 +1,31 @@
# app/frontend.py (new file or inline in main.py) # app/frontend.py
from fastapi import APIRouter, Request, status
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app.auth import require_login
import os import os
from pathlib import Path
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
from app.auth import require_login
router = APIRouter() router = APIRouter()
# 1) Serve the folder that contains index.html, etc. # Point templates_dir to "frontend/templates"
# e.g. "frontend" is relative to your project root templates_dir = Path(__file__).parent.parent / "frontend" / "templates"
frontend_folder = os.path.join(os.path.dirname(__file__), "..", "frontend") templates = Jinja2Templates(directory=str(templates_dir))
# If you just want to serve the entire folder as static: @router.get("/", include_in_schema=False)
router.mount("/static", StaticFiles(directory=frontend_folder), name="static") async def serve_index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
# 2) For the root route ("/"), return the index.html
@router.get("/about", include_in_schema=False)
@router.get("/upload", response_class=FileResponse) async def serve_about(request: Request):
return templates.TemplateResponse("about.html", {"request": request})
@router.get("/upload", include_in_schema=False)
@require_login @require_login
async def serve_upload(request: Request): async def serve_upload(request: Request):
return os.path.join(frontend_folder, "upload.html") return templates.TemplateResponse("upload.html", {"request": request})
# 3) Serve favicon.ico from the frontend folder @router.get("/favicon.ico", include_in_schema=False)
@router.get("/favicon.ico", response_class=FileResponse)
def favicon(): def favicon():
return os.path.join(frontend_folder, "favicon.ico") # If you have a real favicon in `frontend/static/favicon.ico`:
favicon_path = Path(__file__).parent.parent / "frontend" / "static" / "favicon.ico"
""" @router.exception_handler(404) return str(favicon_path)
async def custom_404_handler(request: Request, exc):
return FileResponse("frontend/404.html", status_code=status.HTTP_404_NOT_FOUND) """
@router.get("/", response_class=FileResponse)
async def serve_index(request: Request):
return os.path.join(frontend_folder, "index.html")
@router.get("/about", response_class=FileResponse)
async def serve_about(request: Request):
return os.path.join(frontend_folder, "about.html")
+15 -9
View File
@@ -3,10 +3,14 @@ import os
from fastapi import FastAPI, HTTPException, UploadFile, File, status, Request from fastapi import FastAPI, HTTPException, UploadFile, File, status, Request
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from starlette.config import Config from starlette.config import Config
from starlette.middleware.trustedhost import TrustedHostMiddleware from starlette.middleware.trustedhost import TrustedHostMiddleware
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from pathlib import Path
from app.database import init_db from app.database import init_db
from app.config import settings from app.config import settings
from app.tasks.upload_to_s3 import upload_to_s3 from app.tasks.upload_to_s3 import upload_to_s3
@@ -14,13 +18,11 @@ from app.tasks.upload_to_dropbox import upload_to_dropbox
from app.tasks.upload_to_paperless import upload_to_paperless from app.tasks.upload_to_paperless import upload_to_paperless
from app.tasks.upload_to_nextcloud import upload_to_nextcloud from app.tasks.upload_to_nextcloud import upload_to_nextcloud
from app.tasks.send_to_all import send_to_all_destinations from app.tasks.send_to_all import send_to_all_destinations
from pathlib import Path
from app.api import router as api_router from app.api import router as api_router
from app.frontend import router as frontend_router from app.frontend import router as frontend_router
from app.auth import router as auth_router from app.auth import router as auth_router
# Load configuration from .env for the session key # Load configuration from .env for the session key
config = Config(".env") config = Config(".env")
SESSION_SECRET = config( SESSION_SECRET = config(
@@ -30,12 +32,10 @@ SESSION_SECRET = config(
app = FastAPI(title="Document Processing API") app = FastAPI(title="Document Processing API")
# 1) Session Middleware (for request.session to work) # 1) Session Middleware (for request.session to work)
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET) app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET)
# 2) Respect the X-Forwarded-* headers from Traefik # 2) Respect the X-Forwarded-* headers from Traefik
# so your request.url_for(...) uses https://docparse.hosterra.net
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*") app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
# 3) (Optional but recommended) Restrict valid hosts: # 3) (Optional but recommended) Restrict valid hosts:
@@ -45,12 +45,14 @@ app.add_middleware(TrustedHostMiddleware, allowed_hosts=[
"127.0.0.1" "127.0.0.1"
]) ])
# Mount the static folder for CSS/JS:
frontend_static_dir = Path(__file__).parent.parent / "frontend" / "static"
app.mount("/static", StaticFiles(directory=frontend_static_dir), name="static")
@app.on_event("startup") @app.on_event("startup")
def on_startup(): def on_startup():
init_db() # Create tables if they don't exist init_db() # Create tables if they don't exist
@app.post("/process/") @app.post("/process/")
def process(file_path: str): def process(file_path: str):
""" """
@@ -166,10 +168,15 @@ async def ui_upload(file: UploadFile = File(...)):
task = upload_to_s3.delay(target_path) task = upload_to_s3.delay(target_path)
return {"task_id": task.id, "status": "queued"} return {"task_id": task.id, "status": "queued"}
# Custom 404 - we can still return the Jinja2 template, or the old static file:
# For a dynamic 404 using the base layout, see "frontend/404.html" usage below:
@app.exception_handler(404) @app.exception_handler(404)
async def custom_404_handler(request: Request, exc: HTTPException): async def custom_404_handler(request: Request, exc: HTTPException):
return FileResponse( # Serve the 404 template directly
"/app/frontend/404.html", templates = Jinja2Templates(directory=str(frontend_static_dir.parent / "templates"))
return templates.TemplateResponse(
"404.html",
{"request": request},
status_code=status.HTTP_404_NOT_FOUND status_code=status.HTTP_404_NOT_FOUND
) )
@@ -177,4 +184,3 @@ async def custom_404_handler(request: Request, exc: HTTPException):
app.include_router(frontend_router) app.include_router(frontend_router)
app.include_router(auth_router) app.include_router(auth_router)
app.include_router(api_router, prefix="/api") app.include_router(api_router, prefix="/api")
-93
View File
@@ -1,93 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>404 - Oops, DocuNova Lost the Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Tailwind CSS via CDN -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gradient-to-br from-blue-50 to-white min-h-screen flex flex-col">
<!-- Navigation Bar -->
<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 -->
<div class="flex-shrink-0">
<a href="/" class="text-xl font-bold text-blue-500 hover:text-blue-700">
DocuNova
</a>
</div>
<!-- Menu Items -->
<div class="flex space-x-6 items-center">
<a href="/" class="text-gray-700 hover:text-gray-900">Home</a>
<a href="/upload" class="text-gray-700 hover:text-gray-900">Upload</a>
<a href="/about" class="text-gray-700 hover:text-gray-900">About</a>
<a href="/files" class="text-gray-700 hover:text-gray-900">Files</a>
<!-- Dynamic Auth Section -->
<div id="authSection" class="text-gray-700 hover:text-gray-900"></div>
</div>
</div>
</nav>
<!-- Main Content Area -->
<main class="flex-grow flex flex-col items-center justify-center text-center px-4">
<div class="max-w-xl">
<h1 class="text-7xl font-extrabold text-blue-600 mb-6">404</h1>
<h2 class="text-3xl font-bold text-gray-800 mb-4">Oops, we couldnt find that page!</h2>
<p class="text-gray-600 mb-8">
It seems DocuNova has misplaced the document you were looking for. Whether it got lost in the cloud or hidden between the files, dont worry weve got your back.
</p>
<a href="/" class="inline-block bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors">
&larr; Return Home
</a>
</div>
</main>
<!-- Footer -->
<footer class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
&copy; 2025 DocuNova. All rights reserved.
</div>
</footer>
<!-- JavaScript for dynamic auth section -->
<script>
(async function checkAuth() {
try {
const resp = await fetch("/api/whoami");
if (resp.ok) {
const data = await resp.json();
// data.picture is expected to be a Gravatar URL
const authSection = document.getElementById("authSection");
authSection.innerHTML = '';
const img = document.createElement('img');
img.src = data.picture;
img.alt = 'User Avatar';
img.className = 'inline-block h-8 w-8 rounded-full mr-2';
const textNode = document.createTextNode('Logged in as ');
const strong = document.createElement('strong');
strong.textContent = data.email;
const logoutLink = document.createElement('a');
logoutLink.href = '/logout';
logoutLink.className = 'ml-4 text-blue-600 hover:text-blue-800';
logoutLink.textContent = 'Logout';
authSection.appendChild(img);
authSection.appendChild(textNode);
authSection.appendChild(strong);
authSection.appendChild(logoutLink);
} else {
document.getElementById("authSection").innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
} catch (error) {
document.getElementById("authSection").innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
})();
</script>
</body>
</html>
-132
View File
@@ -1,132 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>About DocuNova</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Tailwind CSS via CDN -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col">
<!-- Navigation Bar -->
<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 -->
<div class="flex-shrink-0">
<a href="/" class="text-xl font-bold text-blue-500 hover:text-blue-700">
DocuNova
</a>
</div>
<!-- Menu items -->
<div class="flex space-x-4 items-center">
<a href="/" class="text-gray-700 hover:text-gray-900">Home</a>
<a href="/upload" class="text-gray-700 hover:text-gray-900">Upload</a>
<a href="/about" class="text-gray-700 hover:text-gray-900">About</a>
<a href="/files" class="text-gray-700 hover:text-gray-900">Files</a>
<!-- Dynamic auth section -->
<div id="authSection" class="text-gray-700 hover:text-gray-900"></div>
</div>
</div>
</nav>
<!-- Main Content Area -->
<main class="flex-grow container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-4">About DocuNova</h1>
<p class="text-gray-700 mb-6 leading-relaxed">
Welcome to <strong>DocuNova</strong> your modern, intelligent solution for document processing! Weve built DocuNova to completely transform the way you handle your documents from upload to extraction, from processing to storage. With cutting-edge technologies and a user-first design, DocuNova makes managing your documents as easy as a click.
</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">
DocuNova was created with one goal in mind: to simplify and streamline document management for everyone, whether youre a small startup or a large enterprise. Tired of clunky, outdated systems, we set out to design a platform that is intuitive, flexible, and packed with powerful features.
</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 Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions. And while weve implemented AWS S3 for now, were always evolving!
</p>
</section>
<!-- Key Features Section -->
<section class="bg-white shadow rounded p-6 mb-8">
<h2 class="text-2xl font-semibold mb-2">Key Features</h2>
<ul class="list-disc list-inside text-gray-600">
<li>Simple and secure file uploads with drag & drop support</li>
<li>Automated metadata extraction, indexing, and version control</li>
<li>Integration with popular cloud services and storage platforms</li>
<li>OCR and intelligent document processing powered by AI</li>
<li>IMAP integration for automated document fetching</li>
<li>Highly configurable via environment variables for custom workflows</li>
<li>Docker-ready for easy deployment and scalability</li>
</ul>
</section>
<!-- Meet the Creator Section -->
<section class="bg-white shadow rounded p-6 mb-8">
<h2 class="text-2xl font-semibold mb-2">Meet the Creator</h2>
<p class="text-gray-600">
DocuNova is passionately developed by <a href="https://www.christianlouis.de" target="_blank" class="text-blue-600 hover:underline">Christian Krakau-Louis</a>, a visionary committed to solving real-world challenges with innovative technology. His dedication and expertise ensure that every feature in DocuNova is crafted with you in mind.
</p>
</section>
<!-- GitHub Link Section -->
<section class="bg-white shadow rounded p-6">
<h2 class="text-2xl font-semibold mb-2">Get Involved</h2>
<p class="text-gray-600 mb-4">
Want to dive into the code, contribute ideas, or simply check out the magic behind DocuNova? Visit our GitHub repository to see the project in action!
</p>
<a href="https://github.com/christianlouis/document-processor" target="_blank" class="flex items-center space-x-2 text-blue-600 hover:text-blue-800">
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="GitHub Logo" class="h-6 w-6" />
<span>View DocuNova on GitHub</span>
</a>
</section>
</main>
<!-- Footer -->
<footer class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
&copy; 2025 DocuNova. All rights reserved.
</div>
</footer>
<!-- JavaScript for dynamic auth section -->
<script>
(async function checkAuth() {
try {
const resp = await fetch("/api/whoami");
if (resp.ok) {
const data = await resp.json();
// data.picture is expected to be a Gravatar URL
const authSection = document.getElementById("authSection");
authSection.innerHTML = '';
const img = document.createElement('img');
img.src = data.picture;
img.alt = 'User Avatar';
img.className = 'inline-block h-8 w-8 rounded-full mr-2';
const textNode = document.createTextNode('Logged in as ');
const strong = document.createElement('strong');
strong.textContent = data.email;
const logoutLink = document.createElement('a');
logoutLink.href = '/logout';
logoutLink.className = 'ml-4 text-blue-600 hover:text-blue-800';
logoutLink.textContent = 'Logout';
authSection.appendChild(img);
authSection.appendChild(textNode);
authSection.appendChild(strong);
authSection.appendChild(logoutLink);
} else {
document.getElementById("authSection").innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
} catch (error) {
document.getElementById("authSection").innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
})();
</script>
</body>
</html>
-108
View File
@@ -1,108 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>DocuNova - Intelligent Document Processing</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Tailwind CSS via CDN -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col">
<!-- Navigation Bar -->
<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 -->
<div class="flex-shrink-0">
<a href="/" class="text-xl font-bold text-blue-500 hover:text-blue-700">
DocuNova
</a>
</div>
<!-- Menu items -->
<div class="flex space-x-4 items-center">
<a href="/" class="text-gray-700 hover:text-gray-900">Home</a>
<a href="/upload" class="text-gray-700 hover:text-gray-900">Upload</a>
<a href="/about" class="text-gray-700 hover:text-gray-900">About</a>
<a href="/files" class="text-gray-700 hover:text-gray-900">Files</a>
<!-- Dynamic auth section -->
<div id="authSection" class="text-gray-700 hover:text-gray-900"></div>
</div>
</div>
</nav>
<!-- Main Content Area -->
<main class="flex-grow container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-4">Welcome to DocuNova</h1>
<p class="text-gray-700 mb-8">
Your intelligent solution for processing, managing, and organizing documents effortlessly.
</p>
<!-- Sample Sections -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="bg-white shadow rounded p-6">
<h2 class="text-2xl font-semibold mb-2">Upload Documents</h2>
<p class="text-gray-600">
Quickly upload and process your files with our user-friendly interface.
</p>
<a href="/upload" class="mt-4 inline-block text-blue-600 hover:text-blue-800">
Get Started &rarr;
</a>
</div>
<div class="bg-white shadow rounded p-6">
<h2 class="text-2xl font-semibold mb-2">Manage Your Files</h2>
<p class="text-gray-600">
View, organize, and collaborate on your processed files in one central hub.
</p>
<a href="/files" class="mt-4 inline-block text-blue-600 hover:text-blue-800">
View Files &rarr;
</a>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
&copy; 2025 DocuNova. All rights reserved.
</div>
</footer>
<!-- JavaScript for dynamic auth section -->
<script>
(async function checkAuth() {
try {
const resp = await fetch("/api/whoami");
if (resp.ok) {
const data = await resp.json();
// data.picture is expected to be a Gravatar URL
const authSection = document.getElementById("authSection");
authSection.innerHTML = '';
const img = document.createElement('img');
img.src = data.picture;
img.alt = 'User Avatar';
img.className = 'inline-block h-8 w-8 rounded-full mr-2';
const textNode = document.createTextNode('Logged in as ');
const strong = document.createElement('strong');
strong.textContent = data.email;
const logoutLink = document.createElement('a');
logoutLink.href = '/logout';
logoutLink.className = 'ml-4 text-blue-600 hover:text-blue-800';
logoutLink.textContent = 'Logout';
authSection.appendChild(img);
authSection.appendChild(textNode);
authSection.appendChild(strong);
authSection.appendChild(logoutLink);
} else {
document.getElementById("authSection").innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
} catch (error) {
document.getElementById("authSection").innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
})();
</script>
</body>
</html>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+46
View File
@@ -0,0 +1,46 @@
// frontend/static/js/common.js
(async function checkAuth() {
try {
const resp = await fetch("/api/whoami");
if (resp.ok) {
const data = await resp.json();
const authSection = document.getElementById("authSection");
if (!authSection) return;
authSection.innerHTML = '';
const img = document.createElement('img');
img.src = data.picture;
img.alt = 'User Avatar';
img.className = 'inline-block h-8 w-8 rounded-full mr-2';
const textNode = document.createTextNode('Logged in as ');
const strong = document.createElement('strong');
strong.textContent = data.email;
const logoutLink = document.createElement('a');
logoutLink.href = '/logout';
logoutLink.className = 'ml-4 text-blue-600 hover:text-blue-800';
logoutLink.textContent = 'Logout';
authSection.appendChild(img);
authSection.appendChild(textNode);
authSection.appendChild(strong);
authSection.appendChild(logoutLink);
} else {
const authSection = document.getElementById("authSection");
if (authSection) {
authSection.innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
}
} catch (error) {
// Fallback if whoami endpoint fails
const authSection = document.getElementById("authSection");
if (authSection) {
authSection.innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
}
})();
+1
View File
@@ -0,0 +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>

After

Width:  |  Height:  |  Size: 917 B

+18
View File
@@ -0,0 +1,18 @@
/* frontend/static/styles.css */
/* Example overrides or additional styling */
body {
/* Your global overrides can go here if needed */
}
.material-symbols-light--folder-managed-outline {
display: inline-block;
width: 96px;
height: 96px;
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' 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'/%3E%3C/svg%3E");
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
+17
View File
@@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block title %}404 - Not Found{% endblock %}
{% block content %}
<div class="flex flex-col items-center justify-center text-center px-4 py-16">
<div class="max-w-xl">
<h1 class="text-7xl font-extrabold text-blue-600 mb-6">404</h1>
<h2 class="text-3xl font-bold text-gray-800 mb-4">Oops, we couldnt find that page!</h2>
<p class="text-gray-600 mb-8">
It seems DocuNova has misplaced the document you were looking for. Dont worry weve got your back.
</p>
<a href="/" class="inline-block bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors">
&larr; Return Home
</a>
</div>
</div>
{% endblock %}
+63
View File
@@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block title %}About DocuNova{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-4">About DocuNova</h1>
<p class="text-gray-700 mb-6 leading-relaxed">
Welcome to <strong>DocuNova</strong> your modern, intelligent solution for document processing!
Weve built DocuNova to completely transform the way you handle your documents from upload
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">
DocuNova was created with one goal in mind: to simplify and streamline document management
for everyone, whether youre 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
Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions.
</p>
</section>
<!-- Key Features Section -->
<section class="bg-white shadow rounded p-6 mb-8">
<h2 class="text-2xl font-semibold mb-2">Key Features</h2>
<ul class="list-disc list-inside text-gray-600">
<li>Simple and secure file uploads with drag & drop support</li>
<li>Automated metadata extraction, indexing, and version control</li>
<li>Integration with popular cloud services and storage platforms</li>
<li>OCR and intelligent document processing powered by AI</li>
<li>IMAP integration for automated document fetching</li>
<li>Highly configurable via environment variables</li>
<li>Docker-ready for easy deployment</li>
</ul>
</section>
<!-- Meet the Creator Section -->
<section class="bg-white shadow rounded p-6 mb-8">
<h2 class="text-2xl font-semibold mb-2">Meet the Creator</h2>
<p class="text-gray-600">
DocuNova is passionately developed by
<a href="https://www.christianlouis.de" target="_blank" class="text-blue-600 hover:underline">Christian Krakau-Louis</a>.
</p>
</section>
<!-- GitHub Link Section -->
<section class="bg-white shadow rounded p-6">
<h2 class="text-2xl font-semibold mb-2">Get Involved</h2>
<p class="text-gray-600 mb-4">
Want to dive into the code, contribute ideas, or simply check out the magic behind DocuNova?
Visit our GitHub repository!
</p>
<a href="https://github.com/christianlouis/document-processor" target="_blank" class="flex items-center space-x-2 text-blue-600 hover:text-blue-800">
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="GitHub Logo" class="h-6 w-6" />
<span>View DocuNova on GitHub</span>
</a>
</section>
</div>
{% endblock %}
+63
View File
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{% block title %}DocuNova{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Tailwind CSS or other global CSS references -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="/static/styles.css" />
{% block head_extra %}{% endblock %}
</head>
<body class="bg-gray-50 min-h-screen flex flex-col">
<!-- 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
class="material-symbols-light--folder-managed-outline text-blue-500"
style="width: 24px; height: 24px;"
></span>
<!-- Text -->
<span class="text-xl font-bold text-blue-500 hover:text-blue-700">
DocuNova
</span>
</a>
</div>
<!-- Menu Items -->
<div class="flex space-x-4 items-center">
<a href="/" class="text-gray-700 hover:text-gray-900">Home</a>
<a href="/upload" class="text-gray-700 hover:text-gray-900">Upload</a>
<a href="/about" class="text-gray-700 hover:text-gray-900">About</a>
<a href="/files" class="text-gray-700 hover:text-gray-900">Files</a>
<!-- Dynamic Auth Section -->
<div id="authSection" class="text-gray-700 hover:text-gray-900"></div>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="flex-grow">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
&copy; 2025 DocuNova. All rights reserved.
</div>
</footer>
<!-- Common JS (shared) -->
<script src="/static/js/common.js"></script>
<!-- Let child pages define extra scripts if needed -->
{% block scripts %}{% endblock %}
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}Home - DocuNova{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-4">Welcome to DocuNova</h1>
<p class="text-gray-700 mb-8">
Your intelligent solution for processing, managing, and organizing documents effortlessly.
</p>
<!-- Sample sections -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="bg-white shadow rounded p-6">
<h2 class="text-2xl font-semibold mb-2">Upload Documents</h2>
<p class="text-gray-600">
Quickly upload and process your files with our user-friendly interface.
</p>
<a href="/upload" class="mt-4 inline-block text-blue-600 hover:text-blue-800">
Get Started &rarr;
</a>
</div>
<div class="bg-white shadow rounded p-6">
<h2 class="text-2xl font-semibold mb-2">Manage Your Files</h2>
<p class="text-gray-600">
View, organize, and collaborate on your processed files in one central hub.
</p>
<a href="/files" class="mt-4 inline-block text-blue-600 hover:text-blue-800">
View Files &rarr;
</a>
</div>
</div>
</div>
{% endblock %}
+83
View File
@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block title %}Upload Document{% endblock %}
{% block content %}
<div class="flex flex-col items-center justify-center p-8">
<h1 class="text-3xl font-bold mb-8">Upload a File</h1>
<div
id="dropZone"
class="border-4 border-dashed border-gray-300 rounded-lg p-8 bg-white text-center w-full max-w-lg"
ondrop="handleDrop(event)"
ondragover="handleDragOver(event)"
>
<p class="text-gray-500 mb-4">
Drag & drop a file here, or click to select a file.
</p>
<input
id="fileInput"
type="file"
class="hidden"
onchange="handleFileSelect(event)"
/>
</div>
<div id="statusMessage" class="mt-4 text-gray-700"></div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Drag-and-drop / file input logic
const dropZone = document.getElementById("dropZone");
const fileInput = document.getElementById("fileInput");
const statusMessage = document.getElementById("statusMessage");
dropZone.addEventListener("click", () => fileInput.click());
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
dropZone.classList.add("bg-gray-100");
}
function handleDrop(e) {
e.preventDefault();
dropZone.classList.remove("bg-gray-100");
if (e.dataTransfer.files.length) {
uploadFile(e.dataTransfer.files[0]);
}
}
function handleFileSelect(e) {
if (e.target.files.length) {
uploadFile(e.target.files[0]);
}
}
async function uploadFile(file) {
// Check file type
const acceptedTypes = ['application/pdf'];
if (!acceptedTypes.includes(file.type)) {
statusMessage.textContent = `Error: Only PDF files are accepted.`;
return;
}
statusMessage.textContent = `Uploading ${file.name}...`;
try {
let formData = new FormData();
formData.append("file", file);
const response = await fetch("/ui-upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Upload failed with status ${response.status}`);
}
const result = await response.json();
statusMessage.textContent = `File ${file.name} uploaded. Task ID: ${result.task_id}`;
} catch (err) {
statusMessage.textContent = `Error: ${err}`;
}
}
</script>
{% endblock %}
-157
View File
@@ -1,157 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>DocuNova - Upload</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Tailwind CSS via CDN -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col">
<!-- Navigation Bar -->
<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 -->
<div class="flex-shrink-0">
<a href="/" class="text-xl font-bold text-blue-500 hover:text-blue-700">
DocuNova
</a>
</div>
<!-- Menu Items -->
<div class="flex space-x-4 items-center">
<a href="/" class="text-gray-700 hover:text-gray-900">Home</a>
<a href="/upload" class="text-gray-700 hover:text-gray-900">Upload</a>
<a href="/about" class="text-gray-700 hover:text-gray-900">About</a>
<a href="/files" class="text-gray-700 hover:text-gray-900">Files</a>
<!-- Dynamic Auth Section -->
<div id="authSection" class="text-gray-700 hover:text-gray-900"></div>
</div>
</div>
</nav>
<!-- Main Content Area -->
<main class="flex-grow flex flex-col items-center justify-center p-8">
<h1 class="text-3xl font-bold mb-8">Upload a File</h1>
<div
id="dropZone"
class="border-4 border-dashed border-gray-300 rounded-lg p-8 bg-white text-center w-full max-w-lg"
ondrop="handleDrop(event)"
ondragover="handleDragOver(event)"
>
<p class="text-gray-500 mb-4">
Drag & drop a file here, or click to select a file.
</p>
<input
id="fileInput"
type="file"
class="hidden"
onchange="handleFileSelect(event)"
/>
</div>
<div id="statusMessage" class="mt-4 text-gray-700"></div>
</main>
<!-- Footer -->
<footer class="bg-white shadow mt-auto">
<div class="max-w-7xl mx-auto px-4 py-4 text-center text-gray-600">
&copy; 2025 DocuNova. All rights reserved.
</div>
</footer>
<!-- JavaScript -->
<script>
// Drag-and-drop / file input logic
const dropZone = document.getElementById("dropZone");
const fileInput = document.getElementById("fileInput");
const statusMessage = document.getElementById("statusMessage");
dropZone.addEventListener("click", () => fileInput.click());
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
dropZone.classList.add("bg-gray-100");
}
function handleDrop(e) {
e.preventDefault();
dropZone.classList.remove("bg-gray-100");
if (e.dataTransfer.files.length) {
uploadFile(e.dataTransfer.files[0]);
}
}
function handleFileSelect(e) {
if (e.target.files.length) {
uploadFile(e.target.files[0]);
}
}
async function uploadFile(file) {
// Check file type
const acceptedTypes = ['application/pdf'];
if (!acceptedTypes.includes(file.type)) {
statusMessage.textContent = `Error: Only PDF files are accepted.`;
return;
}
statusMessage.textContent = `Uploading ${file.name}...`;
try {
let formData = new FormData();
formData.append("file", file);
const response = await fetch("/ui-upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Upload failed with status ${response.status}`);
}
const result = await response.json();
statusMessage.textContent = `File ${file.name} uploaded. Task ID: ${result.task_id}`;
} catch (err) {
statusMessage.textContent = `Error: ${err}`;
}
}
<!-- JavaScript for dynamic auth section -->
(async function checkAuth() {
try {
const resp = await fetch("/api/whoami");
if (resp.ok) {
const data = await resp.json();
// data.picture is expected to be a Gravatar URL
const authSection = document.getElementById("authSection");
authSection.innerHTML = '';
const img = document.createElement('img');
img.src = data.picture;
img.alt = 'User Avatar';
img.className = 'inline-block h-8 w-8 rounded-full mr-2';
const textNode = document.createTextNode('Logged in as ');
const strong = document.createElement('strong');
strong.textContent = data.email;
const logoutLink = document.createElement('a');
logoutLink.href = '/logout';
logoutLink.className = 'ml-4 text-blue-600 hover:text-blue-800';
logoutLink.textContent = 'Logout';
authSection.appendChild(img);
authSection.appendChild(textNode);
authSection.appendChild(strong);
authSection.appendChild(logoutLink);
} else {
document.getElementById("authSection").innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
} catch (error) {
document.getElementById("authSection").innerHTML =
`<a href="/login" class="text-blue-600">Login</a>`;
}
})();
</script>
</body>
</html>