Merge pull request #555 from christianlouis/copilot/restructure-help-page

feat(help): replace MkDocs redirect with user-facing Help Center + Zammad integration
This commit is contained in:
Christian Krakau-Louis
2026-03-08 18:58:02 +01:00
committed by GitHub
10 changed files with 789 additions and 50 deletions
+12
View File
@@ -478,3 +478,15 @@ EMBEDDING_MODEL=text-embedding-3-small
# Maximum tokens to send to the embedding model. Set below the model's
# context window (e.g. 8000 for an 8192-token model).
EMBEDDING_MAX_TOKENS=8000
# **Support / Help Center Zammad Integration**
# Base URL of your Zammad instance (required for chat and ticket form).
# ZAMMAD_URL=https://zammad.example.com
# Show a live-chat widget on the Help Center page (requires an online Zammad agent).
# ZAMMAD_CHAT_ENABLED=false
# Zammad chat topic ID (see Zammad → Channels → Chat → Topics).
# ZAMMAD_CHAT_ID=1
# Show a "Submit a Ticket" feedback form on the Help Center page.
# ZAMMAD_FORM_ENABLED=false
# Support e-mail address displayed on the Help Center page.
# SUPPORT_EMAIL=support@example.com
+27
View File
@@ -914,6 +914,33 @@ class Settings(BaseSettings):
description="Allowed request headers for CORS. Use ['*'] to allow all headers.",
)
# ---------------------------------------------------------------------------
# Support / Help Center Zammad integration
# ---------------------------------------------------------------------------
zammad_url: Optional[str] = Field(
default=None,
description=(
"Base URL of your Zammad instance (e.g. https://zammad.example.com). "
"Required for the chat widget and feedback form on the Help Center page."
),
)
zammad_chat_enabled: bool = Field(
default=False,
description="Show the Zammad live-chat widget on the Help Center page.",
)
zammad_chat_id: int = Field(
default=1,
description="Zammad chat topic ID to use for the live-chat widget.",
)
zammad_form_enabled: bool = Field(
default=False,
description="Show the Zammad feedback / ticket form on the Help Center page.",
)
support_email: Optional[str] = Field(
default=None,
description="Support e-mail address displayed on the Help Center page.",
)
@model_validator(mode="before")
@classmethod
def strip_outer_quotes(cls, data: Any) -> Any:
+11 -4
View File
@@ -137,7 +137,12 @@ async def lifespan(app: FastAPI):
notify_shutdown()
app = FastAPI(title="DocuElevate", lifespan=lifespan)
app = FastAPI(
title="DocuElevate",
lifespan=lifespan,
docs_url="/admin/api-docs",
redoc_url="/admin/api-redoc",
)
# Initialize rate limiter and attach to app state
limiter = create_limiter(redis_url=settings.redis_url, enabled=settings.rate_limiting_enabled)
@@ -202,14 +207,16 @@ if os.path.exists(static_dir):
else:
print(f"WARNING: Static directory not found at {static_dir}. Static files will not be served.")
# Mount the built MkDocs documentation site at /help/
# Mount the built MkDocs developer documentation at /developer-docs/
# These docs target administrators and developers, not end-users.
# The user-facing Help Center is served by the /help view instead.
# The docs are pre-built into docs_build/ during the Docker image build.
# When running locally, run `mkdocs build` from the repo root first.
docs_build_dir = pathlib.Path(__file__).parents[1] / "docs_build"
if os.path.exists(docs_build_dir):
app.mount("/help", StaticFiles(directory=str(docs_build_dir), html=True), name="help_docs")
app.mount("/developer-docs", StaticFiles(directory=str(docs_build_dir), html=True), name="developer_docs")
else:
print(f"INFO: Help docs not found at {docs_build_dir}. Run 'mkdocs build' to generate them.")
print(f"INFO: Developer docs not found at {docs_build_dir}. Run 'mkdocs build' to generate them.")
# Custom exception handlers that return JSON for API routes and HTML for frontend routes
+41
View File
@@ -2223,6 +2223,47 @@ SETTING_METADATA = {
"required": False,
"restart_required": False,
},
# Support / Help Center Zammad integration
"zammad_url": {
"category": "Support",
"description": "Base URL of your Zammad instance (e.g. https://zammad.example.com). Required for the chat widget and feedback form.",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
},
"zammad_chat_enabled": {
"category": "Support",
"description": "Show a Zammad live-chat widget on the Help Center page.",
"type": "boolean",
"sensitive": False,
"required": False,
"restart_required": False,
},
"zammad_chat_id": {
"category": "Support",
"description": "Zammad chat topic ID (see Channels → Chat → Topics in Zammad admin). Default: 1.",
"type": "integer",
"sensitive": False,
"required": False,
"restart_required": False,
},
"zammad_form_enabled": {
"category": "Support",
"description": "Show a 'Submit a Ticket' feedback form on the Help Center page.",
"type": "boolean",
"sensitive": False,
"required": False,
"restart_required": False,
},
"support_email": {
"category": "Support",
"description": "Support e-mail address displayed on the Help Center page.",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
},
}
+25 -9
View File
@@ -1,27 +1,43 @@
"""
Help documentation view routes.
Help Center view routes.
Serves the built MkDocs documentation site at /help.
The static site is built during the Docker image build and placed at docs_build/.
Renders a user-facing, SEO-optimised Help Center page at ``/help``.
The page is designed for SaaS end-users and includes guidance on
features, integrations, workflows, and optional Zammad support widgets
(live-chat and feedback form).
The developer-oriented MkDocs documentation is served separately at
``/docs`` and is intentionally **not** cross-linked from this page.
"""
import logging
import pathlib
from fastapi import Request
from fastapi.responses import RedirectResponse
from app.views.base import APIRouter
from app.config import settings
from app.views.base import APIRouter, templates
logger = logging.getLogger(__name__)
router = APIRouter()
# Path to the built MkDocs documentation
# Path to the built MkDocs documentation (kept for reference / backwards compat)
_DOCS_BUILD_DIR = pathlib.Path(__file__).parents[2] / "docs_build"
@router.get("/help", include_in_schema=False)
async def help_redirect(request: Request) -> RedirectResponse:
"""Redirect /help to /help/ so the MkDocs index is served correctly."""
return RedirectResponse(url="/help/", status_code=301)
async def help_center(request: Request):
"""Render the end-user Help Center page."""
return templates.TemplateResponse(
"help.html",
{
"request": request,
"external_hostname": settings.external_hostname,
"zammad_url": settings.zammad_url,
"zammad_chat_enabled": settings.zammad_chat_enabled,
"zammad_chat_id": settings.zammad_chat_id,
"zammad_form_enabled": settings.zammad_form_enabled,
"support_email": settings.support_email,
},
)
+24
View File
@@ -1118,6 +1118,30 @@ Administrators can set the **site-wide default** colour scheme that is applied w
UI_DEFAULT_COLOR_SCHEME=dark
```
### Support / Help Center Zammad Integration
The Help Center page (`/help`) can optionally integrate with a [Zammad](https://zammad.com/) instance to offer live chat and a ticket-creation form directly within DocuElevate.
| **Variable** | **Description** | **Default** |
|-------------------------|-------------------------------------------------------------------------------------------------------|-------------|
| `ZAMMAD_URL` | Base URL of your Zammad instance (e.g. `https://zammad.example.com`). Required for chat and form. | *(unset)* |
| `ZAMMAD_CHAT_ENABLED` | Show a Zammad live-chat widget on the Help Center page. | `false` |
| `ZAMMAD_CHAT_ID` | Zammad chat topic ID (see *Channels → Chat → Topics* in Zammad admin). | `1` |
| `ZAMMAD_FORM_ENABLED` | Show a "Submit a Ticket" feedback form on the Help Center page. | `false` |
| `SUPPORT_EMAIL` | Support e-mail address displayed on the Help Center page. | *(unset)* |
**Example:**
```dotenv
ZAMMAD_URL=https://zammad.example.com
ZAMMAD_CHAT_ENABLED=true
ZAMMAD_CHAT_ID=1
ZAMMAD_FORM_ENABLED=true
SUPPORT_EMAIL=support@example.com
```
> **Note:** The live-chat widget requires at least one Zammad agent to be online. If no agent is available, the widget will not appear. Enable Zammad's debug mode (`debug: true`) for troubleshooting.
## Duplicate Document Detection
DocuElevate detects and flags documents that share the same content, even if they arrive as separate uploads.
+20 -7
View File
@@ -172,6 +172,13 @@
{% if request and request.url.path == '/status' %}aria-current="page"{% endif %}>
<i class="fas fa-circle-dot w-4 mr-2 text-gray-500" aria-hidden="true"></i> Status
</a>
<div class="border-t border-gray-100 my-1"></div>
<a href="/admin/api-docs" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-code w-4 mr-2 text-teal-500" aria-hidden="true"></i> API Docs
</a>
<a href="/developer-docs/" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="fas fa-book w-4 mr-2 text-teal-500" aria-hidden="true"></i> Developer Docs
</a>
</div>
</div>
</div>
@@ -179,11 +186,11 @@
{% endif %}{# end multi_user_enabled / is_logged_in check #}
<!-- Help always visible, for every visitor regardless of auth state -->
<a href="/help/"
<a href="/help"
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
aria-label="Help and How-To Guides"
title="Help &amp; How-To Guides"
{% if request and request.url.path.startswith('/help') %}aria-current="page"{% endif %}>
aria-label="Help Center"
title="Help Center"
{% if request and request.url.path == '/help' %}aria-current="page"{% endif %}>
<i class="fas fa-circle-question mr-1 text-gray-400" aria-hidden="true"></i>Help
</a>
@@ -314,16 +321,22 @@
{% if request and request.url.path == '/status' %}aria-current="page"{% endif %}>
<i class="fas fa-circle-dot mr-2 text-gray-400" aria-hidden="true"></i> Status
</a>
<a href="/admin/api-docs" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-code mr-2 text-teal-400" aria-hidden="true"></i> API Docs
</a>
<a href="/developer-docs/" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-book mr-2 text-teal-400" aria-hidden="true"></i> Developer Docs
</a>
</div>
</div>
{% endif %}{# end multi_user_enabled / is_logged_in check #}
<!-- Help always visible, for every visitor regardless of auth state -->
<a href="/help/"
<a href="/help"
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
aria-label="Help and How-To Guides"
{% if request and request.url.path.startswith('/help') %}aria-current="page"{% endif %}>
aria-label="Help Center"
{% if request and request.url.path == '/help' %}aria-current="page"{% endif %}>
<i class="fas fa-circle-question mr-2 text-gray-400" aria-hidden="true"></i>Help
</a>
+512
View File
@@ -0,0 +1,512 @@
{% extends "base.html" %}
{% block title %}Help Center DocuElevate{% endblock %}
{% block head_extra %}
<!-- SEO meta tags -->
<meta name="description" content="DocuElevate Help Center learn how to upload, process, and route your documents automatically. Get started with sources, destinations, workflows, and integrations.">
<meta name="keywords" content="DocuElevate, help, support, document processing, OCR, cloud storage, workflow automation, SaaS">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://{{ external_hostname }}/help">
<!-- Open Graph -->
<meta property="og:title" content="Help Center DocuElevate">
<meta property="og:description" content="Everything you need to get started with DocuElevate: upload documents, automate workflows, and connect your favourite cloud storage.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://{{ external_hostname }}/help">
<!-- Structured data (JSON-LD) for FAQ rich results -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "How do I upload documents to DocuElevate?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Navigate to the Upload page, drag-and-drop your files or click Choose File, and DocuElevate will automatically process them with OCR and AI metadata extraction."
}
},
{
"@type": "Question",
"name": "Which cloud storage providers does DocuElevate support?",
"acceptedAnswer": {
"@type": "Answer",
"text": "DocuElevate integrates with Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, WebDAV, SFTP, FTP, and Paperless-ngx."
}
},
{
"@type": "Question",
"name": "Can I automatically ingest documents from email?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Configure one or more IMAP mailboxes under Email Ingestion and DocuElevate will poll for new attachments and process them automatically."
}
},
{
"@type": "Question",
"name": "How do processing pipelines work?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Pipelines let you chain processing steps (OCR, AI extraction, format conversion) and route the result to one or more destinations. Create and manage them from the Pipelines page."
}
}
]
}
</script>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-10 max-w-6xl">
<!-- ═══════════════════════ Hero Section ═══════════════════════ -->
<header class="text-center mb-14">
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 mb-4">
<i class="fas fa-life-ring text-blue-500 mr-2" aria-hidden="true"></i>Help Center
</h1>
<p class="text-lg md:text-xl text-gray-600 max-w-2xl mx-auto leading-relaxed">
Everything you need to get the most out of DocuElevate.
Browse topics below or search for what you need.
</p>
</header>
<!-- ═══════════════════════ Quick-start Cards ═══════════════════════ -->
<section aria-labelledby="quickstart-heading" class="mb-16">
<h2 id="quickstart-heading" class="text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-rocket text-blue-500 mr-2" aria-hidden="true"></i>Quick Start
</h2>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<!-- Card: Upload -->
<article class="bg-white shadow rounded-lg p-6 hover:shadow-lg transition-shadow">
<div class="flex items-center mb-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-blue-100 text-blue-600 mr-3" aria-hidden="true">
<i class="fas fa-cloud-arrow-up"></i>
</span>
<h3 class="text-lg font-semibold text-gray-800">Upload Documents</h3>
</div>
<p class="text-gray-600 text-sm leading-relaxed">
Drag &amp; drop files onto the <strong>Upload</strong> page or click <em>Choose File</em>.
DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.
</p>
</article>
<!-- Card: Connect Storage -->
<article class="bg-white shadow rounded-lg p-6 hover:shadow-lg transition-shadow">
<div class="flex items-center mb-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-green-100 text-green-600 mr-3" aria-hidden="true">
<i class="fas fa-plug"></i>
</span>
<h3 class="text-lg font-semibold text-gray-800">Connect Storage</h3>
</div>
<p class="text-gray-600 text-sm leading-relaxed">
Head to <strong>Settings → Integrations</strong> and link your cloud accounts.
Processed documents are automatically routed to every destination you configure.
</p>
</article>
<!-- Card: Automate -->
<article class="bg-white shadow rounded-lg p-6 hover:shadow-lg transition-shadow">
<div class="flex items-center mb-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-purple-100 text-purple-600 mr-3" aria-hidden="true">
<i class="fas fa-wand-magic-sparkles"></i>
</span>
<h3 class="text-lg font-semibold text-gray-800">Automate Workflows</h3>
</div>
<p class="text-gray-600 text-sm leading-relaxed">
Create <strong>Pipelines</strong> to define multi-step processing and routing rules.
Combine OCR, AI extraction, format conversion, and delivery in a single flow.
</p>
</article>
</div>
</section>
<!-- ═══════════════════════ Sources ═══════════════════════ -->
<section aria-labelledby="sources-heading" class="mb-16">
<h2 id="sources-heading" class="text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-arrow-right-to-bracket text-blue-500 mr-2" aria-hidden="true"></i>Sources Getting Documents In
</h2>
<div class="grid gap-6 sm:grid-cols-2">
<article class="bg-white shadow rounded-lg p-6">
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-upload mr-2 text-gray-400" aria-hidden="true"></i>Web Upload</h3>
<p class="text-gray-600 text-sm">
The fastest way to get started. Open the <strong>Upload</strong> page, drop one or more files, and DocuElevate takes care of the rest.
Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.
</p>
</article>
<article class="bg-white shadow rounded-lg p-6">
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-envelope-open-text mr-2 text-gray-400" aria-hidden="true"></i>Email Ingestion (IMAP)</h3>
<p class="text-gray-600 text-sm">
Forward documents to a dedicated mailbox. Under <strong>Email Ingestion</strong>, add one or more IMAP accounts.
DocuElevate polls for new messages and processes attachments automatically.
</p>
</article>
<article class="bg-white shadow rounded-lg p-6">
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-code mr-2 text-gray-400" aria-hidden="true"></i>REST API</h3>
<p class="text-gray-600 text-sm">
Integrate programmatically by <code class="bg-gray-100 px-1 rounded text-xs">POST</code>-ing files to <code class="bg-gray-100 px-1 rounded text-xs">/api/upload</code>.
Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.
</p>
</article>
<article class="bg-white shadow rounded-lg p-6">
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-print mr-2 text-gray-400" aria-hidden="true"></i>Scanner &amp; Mobile</h3>
<p class="text-gray-600 text-sm">
Point network scanners (HP, Fujitsu ScanSnap, Brother) at DocuElevate's upload endpoint or
use any mobile scanning app that supports custom HTTP destinations.
</p>
</article>
</div>
</section>
<!-- ═══════════════════════ Destinations ═══════════════════════ -->
<section aria-labelledby="destinations-heading" class="mb-16">
<h2 id="destinations-heading" class="text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-arrow-right-from-bracket text-blue-500 mr-2" aria-hidden="true"></i>Destinations Where Documents Go
</h2>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div class="flex items-start bg-white shadow rounded-lg p-5">
<i class="fab fa-dropbox text-blue-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="font-semibold text-gray-800 text-sm">Dropbox</h3>
<p class="text-gray-500 text-xs">OAuth-linked. Files land in your chosen folder.</p>
</div>
</div>
<div class="flex items-start bg-white shadow rounded-lg p-5">
<i class="fab fa-google-drive text-green-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="font-semibold text-gray-800 text-sm">Google Drive</h3>
<p class="text-gray-500 text-xs">Service account or OAuth. Supports shared drives.</p>
</div>
</div>
<div class="flex items-start bg-white shadow rounded-lg p-5">
<i class="fab fa-microsoft text-blue-600 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="font-semibold text-gray-800 text-sm">OneDrive</h3>
<p class="text-gray-500 text-xs">Microsoft Graph API integration.</p>
</div>
</div>
<div class="flex items-start bg-white shadow rounded-lg p-5">
<i class="fab fa-aws text-yellow-600 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="font-semibold text-gray-800 text-sm">Amazon S3</h3>
<p class="text-gray-500 text-xs">Any S3-compatible bucket (AWS, MinIO, Wasabi).</p>
</div>
</div>
<div class="flex items-start bg-white shadow rounded-lg p-5">
<i class="fas fa-cloud text-blue-400 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="font-semibold text-gray-800 text-sm">Nextcloud / WebDAV</h3>
<p class="text-gray-500 text-xs">Self-hosted cloud storage via WebDAV.</p>
</div>
</div>
<div class="flex items-start bg-white shadow rounded-lg p-5">
<i class="fas fa-leaf text-green-600 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="font-semibold text-gray-800 text-sm">Paperless-ngx</h3>
<p class="text-gray-500 text-xs">Push documents straight into Paperless for archival.</p>
</div>
</div>
<div class="flex items-start bg-white shadow rounded-lg p-5">
<i class="fas fa-server text-gray-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="font-semibold text-gray-800 text-sm">SFTP / FTP</h3>
<p class="text-gray-500 text-xs">Secure file transfer to any server.</p>
</div>
</div>
<div class="flex items-start bg-white shadow rounded-lg p-5">
<i class="fas fa-envelope text-red-400 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="font-semibold text-gray-800 text-sm">Email Forwarding</h3>
<p class="text-gray-500 text-xs">Processed files sent as SMTP attachments.</p>
</div>
</div>
<div class="flex items-start bg-white shadow rounded-lg p-5">
<i class="fas fa-link text-indigo-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
<div>
<h3 class="font-semibold text-gray-800 text-sm">Webhook</h3>
<p class="text-gray-500 text-xs">POST metadata to any external endpoint.</p>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════ Workflows / Pipelines ═══════════════════════ -->
<section aria-labelledby="workflows-heading" class="mb-16">
<h2 id="workflows-heading" class="text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-diagram-project text-blue-500 mr-2" aria-hidden="true"></i>Workflows &amp; Pipelines
</h2>
<div class="bg-white shadow rounded-lg p-6">
<div class="grid gap-6 md:grid-cols-2">
<div>
<h3 class="font-semibold text-gray-800 mb-2">What is a Pipeline?</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-4">
A <strong>Pipeline</strong> is a series of processing steps that run automatically whenever a document is ingested.
Each step can transform, enrich, or route the document.
</p>
<h3 class="font-semibold text-gray-800 mb-2">Typical Steps</h3>
<ol class="list-decimal list-inside text-gray-600 text-sm space-y-1">
<li>Convert to PDF (Gotenberg)</li>
<li>OCR extract text (Azure, Tesseract, Mistral …)</li>
<li>AI metadata extraction (OpenAI, Claude, Gemini …)</li>
<li>Deliver to one or more destinations</li>
</ol>
</div>
<div>
<h3 class="font-semibold text-gray-800 mb-2">Creating a Pipeline</h3>
<ol class="list-decimal list-inside text-gray-600 text-sm space-y-2 leading-relaxed">
<li>Go to <strong>Pipelines</strong> in the main menu.</li>
<li>Click <em>New Pipeline</em> and give it a name.</li>
<li>Add the processing steps you need.</li>
<li>Choose one or more delivery destinations.</li>
<li>Save new documents will be processed through this pipeline automatically.</li>
</ol>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════ Data Ingestion Tools ═══════════════════════ -->
<section aria-labelledby="ingestion-heading" class="mb-16">
<h2 id="ingestion-heading" class="text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-toolbox text-blue-500 mr-2" aria-hidden="true"></i>Ingesting Data with Standard Tools
</h2>
<div class="bg-white shadow rounded-lg p-6 space-y-6">
<article>
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-terminal mr-2 text-gray-400" aria-hidden="true"></i>cURL</h3>
<p class="text-gray-600 text-sm mb-2">Upload a file from any terminal:</p>
<pre class="bg-gray-50 text-sm rounded p-3 overflow-x-auto"><code>curl -X POST https://your-instance/api/upload \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@/path/to/document.pdf"</code></pre>
</article>
<article>
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i>Watched Folders</h3>
<p class="text-gray-600 text-sm">
Use <strong>inotifywait</strong> (Linux), <strong>fswatch</strong> (macOS), or any file-watcher script to
<code class="bg-gray-100 px-1 rounded text-xs">POST</code> new files as soon as they appear in a local directory.
</p>
</article>
<article>
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-print mr-2 text-gray-400" aria-hidden="true"></i>Network Scanners</h3>
<p class="text-gray-600 text-sm">
Configure your HP, Fujitsu ScanSnap, or Brother scanner to use DocuElevate's upload URL as a
<em>Scan-to-Network</em> destination. Scans are processed the moment they arrive.
</p>
</article>
<article>
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-mobile-screen-button mr-2 text-gray-400" aria-hidden="true"></i>Mobile Scanning Apps</h3>
<p class="text-gray-600 text-sm">
Apps like <strong>Adobe Scan</strong>, <strong>Microsoft Lens</strong>, or <strong>Genius Scan</strong>
can export to a custom HTTP endpoint. Point them at <code class="bg-gray-100 px-1 rounded text-xs">/api/upload</code>.
</p>
</article>
<article>
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-shuffle mr-2 text-gray-400" aria-hidden="true"></i>Zapier / n8n / Make</h3>
<p class="text-gray-600 text-sm">
Connect DocuElevate to thousands of other apps via integration platforms.
Use the HTTP/Webhook module to <code class="bg-gray-100 px-1 rounded text-xs">POST</code> files to the upload API.
</p>
</article>
</div>
</section>
<!-- ═══════════════════════ FAQ ═══════════════════════ -->
<section aria-labelledby="faq-heading" class="mb-16">
<h2 id="faq-heading" class="text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-circle-question text-blue-500 mr-2" aria-hidden="true"></i>Frequently Asked Questions
</h2>
<div class="space-y-4" x-data="{ open: null }">
<div class="bg-white shadow rounded-lg">
<button type="button"
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
@click="open = open === 1 ? null : 1"
:aria-expanded="open === 1 ? 'true' : 'false'"
aria-controls="faq-1">
<span>How do I upload documents?</span>
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 1 }" aria-hidden="true"></i>
</button>
<div id="faq-1" x-show="open === 1" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
Navigate to the <strong>Upload</strong> page, drag-and-drop your files or click <em>Choose File</em>.
DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.
</div>
</div>
<div class="bg-white shadow rounded-lg">
<button type="button"
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
@click="open = open === 2 ? null : 2"
:aria-expanded="open === 2 ? 'true' : 'false'"
aria-controls="faq-2">
<span>Which file formats are supported?</span>
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 2 }" aria-hidden="true"></i>
</button>
<div id="faq-2" x-show="open === 2" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML.
Non-PDF files are automatically converted to PDF before processing.
</div>
</div>
<div class="bg-white shadow rounded-lg">
<button type="button"
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
@click="open = open === 3 ? null : 3"
:aria-expanded="open === 3 ? 'true' : 'false'"
aria-controls="faq-3">
<span>Can I ingest documents from email?</span>
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 3 }" aria-hidden="true"></i>
</button>
<div id="faq-3" x-show="open === 3" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
Yes. Go to <strong>Email Ingestion</strong>, add an IMAP account, and DocuElevate will poll for new
messages and process attachments automatically.
</div>
</div>
<div class="bg-white shadow rounded-lg">
<button type="button"
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
@click="open = open === 4 ? null : 4"
:aria-expanded="open === 4 ? 'true' : 'false'"
aria-controls="faq-4">
<span>How do processing pipelines work?</span>
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 4 }" aria-hidden="true"></i>
</button>
<div id="faq-4" x-show="open === 4" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
Pipelines let you chain processing steps OCR, AI extraction, format conversion and route the
result to one or more destinations. Create and manage them from the <strong>Pipelines</strong> page.
</div>
</div>
<div class="bg-white shadow rounded-lg">
<button type="button"
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
@click="open = open === 5 ? null : 5"
:aria-expanded="open === 5 ? 'true' : 'false'"
aria-controls="faq-5">
<span>Is my data secure?</span>
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 5 }" aria-hidden="true"></i>
</button>
<div id="faq-5" x-show="open === 5" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents
longer than necessary. See the <a href="/privacy" class="text-blue-600 hover:underline">Privacy Notice</a> for full details.
</div>
</div>
</div>
</section>
<!-- ═══════════════════════ Contact / Support ═══════════════════════ -->
<section aria-labelledby="support-heading" class="mb-10">
<h2 id="support-heading" class="text-2xl font-bold text-gray-800 mb-6">
<i class="fas fa-headset text-blue-500 mr-2" aria-hidden="true"></i>Contact Support
</h2>
<div class="grid gap-6 md:grid-cols-2">
<!-- Left column: info -->
<div class="bg-white shadow rounded-lg p-6 space-y-4">
<p class="text-gray-600 text-sm leading-relaxed">
Can't find what you're looking for? Our support team is here to help.
</p>
{% if support_email %}
<div class="flex items-center text-sm text-gray-700">
<i class="fas fa-envelope text-blue-500 mr-2" aria-hidden="true"></i>
<a href="mailto:{{ support_email }}" class="text-blue-600 hover:underline">{{ support_email }}</a>
</div>
{% endif %}
{% if zammad_chat_enabled and zammad_url %}
<div class="flex items-center text-sm text-gray-700">
<i class="fas fa-comments text-blue-500 mr-2" aria-hidden="true"></i>
<span>Live chat available look for the chat bubble in the bottom-right corner.</span>
</div>
{% endif %}
</div>
<!-- Right column: Zammad ticket form -->
{% if zammad_form_enabled and zammad_url %}
<div class="bg-white shadow rounded-lg p-6">
<h3 class="font-semibold text-gray-800 mb-3">
<i class="fas fa-ticket mr-2 text-gray-400" aria-hidden="true"></i>Open a Support Ticket
</h3>
<p class="text-gray-600 text-sm mb-4">
Describe your issue below and we'll get back to you as soon as possible.
</p>
<button id="zammad-feedback-form"
type="button"
class="inline-flex items-center bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-5 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 min-h-[44px]">
<i class="fas fa-paper-plane mr-2" aria-hidden="true"></i>Submit a Ticket
</button>
</div>
{% elif not zammad_form_enabled and not support_email and not zammad_chat_enabled %}
<div class="bg-white shadow rounded-lg p-6 flex flex-col items-center justify-center text-center">
<i class="fas fa-info-circle text-blue-400 text-2xl mb-2" aria-hidden="true"></i>
<p class="text-gray-500 text-sm">
Contact your administrator for support information.
</p>
</div>
{% endif %}
</div>
</section>
</div>
{% endblock %}
{% block scripts %}
{% if zammad_url and zammad_form_enabled %}
<!-- Zammad Feedback Form -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK"
crossorigin="anonymous"></script>
<script id="zammad_form_script" src="{{ zammad_url }}/assets/form/form.js"></script>
<script>
$(function() {
$('#zammad-feedback-form').ZammadForm({
messageTitle: 'DocuElevate Support',
messageSubmit: 'Submit',
messageThankYou: 'Thank you for your request (#%s)! We will get back to you shortly.',
modal: true
});
});
</script>
{% endif %}
{% if zammad_url and zammad_chat_enabled %}
<!-- Zammad Live Chat Widget -->
<script src="{{ zammad_url }}/assets/chat/chat-no-jquery.min.js"></script>
<script>
(function() {
new ZammadChat({
fontSize: '12px',
flat: true,
chatId: {{ zammad_chat_id | int }},
title: '<strong>Chat</strong> with us!'
});
})();
</script>
{% endif %}
{% endblock %}
+1 -1
View File
@@ -46,7 +46,7 @@ class TestHealthEndpoints:
def test_docs_endpoint(self, client: TestClient):
"""Test that API documentation is accessible."""
response = client.get("/docs")
response = client.get("/admin/api-docs")
assert response.status_code == 200
assert "swagger" in response.text.lower() or "openapi" in response.text.lower()
+116 -29
View File
@@ -1,33 +1,50 @@
"""Tests for app/views/help.py module."""
"""Tests for app/views/help.py module user-facing Help Center."""
import pytest
@pytest.mark.integration
class TestHelpViews:
"""Tests for the help documentation view routes."""
"""Tests for the Help Center view routes."""
def test_help_redirect(self, client):
"""GET /help should redirect permanently to /help/."""
response = client.get("/help", follow_redirects=False)
assert response.status_code == 301
assert response.headers["location"] in ("/help/", "http://testserver/help/")
def test_help_returns_200(self, client):
"""GET /help should return the Help Center page."""
response = client.get("/help")
assert response.status_code == 200
def test_help_redirect_follows(self, client):
"""Following /help redirect leads to /help/ (docs served or 404 if not built)."""
# In test environments docs_build/ is not present, so /help/ may 404.
# We only verify the initial redirect works; the final landing page depends
# on whether the docs have been built (they are built only in Docker images).
response = client.get("/help", follow_redirects=False)
assert response.status_code == 301
# Redirect target must be /help/
location = response.headers.get("location", "")
assert location.endswith("/help/")
def test_help_contains_help_center_title(self, client):
"""The page should contain the Help Center heading."""
response = client.get("/help")
assert b"Help Center" in response.content
def test_help_contains_quick_start_section(self, client):
"""The page should contain the Quick Start section."""
response = client.get("/help")
assert b"Quick Start" in response.content
def test_help_contains_sources_section(self, client):
"""The page should contain the Sources section."""
response = client.get("/help")
assert b"Sources" in response.content
def test_help_contains_destinations_section(self, client):
"""The page should contain the Destinations section."""
response = client.get("/help")
assert b"Destinations" in response.content
def test_help_contains_faq_section(self, client):
"""The page should contain the FAQ section."""
response = client.get("/help")
assert b"Frequently Asked Questions" in response.content
def test_help_contains_support_section(self, client):
"""The page should contain the Contact Support section."""
response = client.get("/help")
assert b"Contact Support" in response.content
def test_help_route_is_registered(self, client):
"""Verify the /help route exists in the app router."""
# A GET to /help must not return 405 Method Not Allowed
response = client.get("/help", follow_redirects=False)
response = client.get("/help")
assert response.status_code != 405
@@ -35,31 +52,52 @@ class TestHelpViews:
class TestHelpViewUnit:
"""Unit tests for the help view module."""
def test_help_redirect_returns_301(self):
"""Verify the redirect is HTTP 301 (permanent)."""
def test_help_returns_200(self):
"""Verify the help center returns HTTP 200."""
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette.middleware.sessions import SessionMiddleware
from app.views.help import router
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="test-secret")
app.include_router(router)
with TestClient(app) as tc:
resp = tc.get("/help", follow_redirects=False)
assert resp.status_code == 301
resp = tc.get("/help")
assert resp.status_code == 200
def test_help_redirect_target(self):
"""Verify the redirect points to /help/."""
def test_help_page_has_seo_meta(self):
"""Verify SEO meta tags are present in the response."""
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette.middleware.sessions import SessionMiddleware
from app.views.help import router
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="test-secret")
app.include_router(router)
with TestClient(app) as tc:
resp = tc.get("/help", follow_redirects=False)
assert resp.headers["location"].rstrip("/").endswith("help") or resp.headers["location"].endswith("/help/")
resp = tc.get("/help")
assert b'name="description"' in resp.content
assert b'name="robots"' in resp.content
def test_help_page_has_structured_data(self):
"""Verify JSON-LD structured data is present."""
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette.middleware.sessions import SessionMiddleware
from app.views.help import router
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="test-secret")
app.include_router(router)
with TestClient(app) as tc:
resp = tc.get("/help")
assert b"application/ld+json" in resp.content
assert b"FAQPage" in resp.content
def test_docs_build_path_constant(self):
"""Verify _DOCS_BUILD_DIR is resolved relative to the app package."""
@@ -71,6 +109,38 @@ class TestHelpViewUnit:
# Should point to <repo_root>/docs_build
assert _DOCS_BUILD_DIR.name == "docs_build"
def test_zammad_chat_hidden_when_disabled(self):
"""Chat widget markup should not appear when Zammad chat is disabled."""
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette.middleware.sessions import SessionMiddleware
from app.views.help import router
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="test-secret")
app.include_router(router)
with TestClient(app) as tc:
resp = tc.get("/help")
# Default settings: zammad_chat_enabled=False → no chat script
assert b"ZammadChat" not in resp.content
def test_zammad_form_hidden_when_disabled(self):
"""Ticket form markup should not appear when Zammad form is disabled."""
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette.middleware.sessions import SessionMiddleware
from app.views.help import router
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="test-secret")
app.include_router(router)
with TestClient(app) as tc:
resp = tc.get("/help")
# Default settings: zammad_form_enabled=False → no form script
assert b"ZammadForm" not in resp.content
@pytest.mark.integration
class TestHelpNavigationLink:
@@ -81,12 +151,29 @@ class TestHelpNavigationLink:
response = client.get("/about")
assert response.status_code == 200
# The Help link should be present somewhere in the rendered page
assert b"/help/" in response.content
assert b"/help" in response.content
def test_help_link_has_accessible_text(self, client):
"""The Help link should have visible text for accessibility."""
response = client.get("/about")
assert response.status_code == 200
content = response.text
# Should include the word "Help" associated with /help/
# Should include the word "Help" associated with /help
assert "Help" in content
@pytest.mark.integration
class TestAdminDocsLinks:
"""Tests that admin doc links are in the admin menu markup."""
def test_api_docs_link_in_admin_menu(self, client):
"""The admin menu HTML should contain the API Docs link."""
response = client.get("/about")
assert response.status_code == 200
assert b"/admin/api-docs" in response.content
def test_developer_docs_link_in_admin_menu(self, client):
"""The admin menu HTML should contain the Developer Docs link."""
response = client.get("/about")
assert response.status_code == 200
assert b"/developer-docs/" in response.content