diff --git a/.gitignore b/.gitignore
index 7ac7f292..7902578c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -171,6 +171,7 @@ venv.bak/
# mkdocs documentation
/site
+/docs_build
# mypy
.mypy_cache/
diff --git a/Dockerfile b/Dockerfile
index e7d33227..2485fa56 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,6 +7,22 @@ WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
+# ── Documentation build stage ───────────────────────────────────────────────
+FROM python:3.14.1-slim AS docs-builder
+
+WORKDIR /docs
+
+# Install MkDocs Material and its dependencies
+COPY docs/requirements.txt /docs/requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy documentation sources
+COPY docs /docs/docs
+COPY mkdocs.yml /docs/mkdocs.yml
+
+# Build the static documentation site
+RUN mkdocs build --config-file /docs/mkdocs.yml --site-dir /docs/docs_build
+
# Second stage for the actual runtime
FROM python:3.14.3-slim
@@ -43,6 +59,9 @@ COPY ./BUILD_DATE /app/BUILD_DATE
COPY ./GIT_SHA /app/GIT_SHA
COPY ./RUNTIME_INFO /app/RUNTIME_INFO
+# Copy the pre-built MkDocs documentation site (served at /help)
+COPY --from=docs-builder /docs/docs_build /app/docs_build
+
# Create runtime_info directory
RUN mkdir -p /app/runtime_info
diff --git a/Dockerfile.local b/Dockerfile.local
index 4ae482d0..138d726c 100644
--- a/Dockerfile.local
+++ b/Dockerfile.local
@@ -6,6 +6,19 @@ WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
+# ── Documentation build stage ───────────────────────────────────────────────
+FROM python:3.14.1-slim AS docs-builder
+
+WORKDIR /docs
+
+COPY docs/requirements.txt /docs/requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY docs /docs/docs
+COPY mkdocs.yml /docs/mkdocs.yml
+
+RUN mkdocs build --config-file /docs/mkdocs.yml --site-dir /docs/docs_build
+
FROM python:3.14.1-slim
WORKDIR /app
@@ -33,6 +46,9 @@ COPY ./LICENSE /app/LICENSE
COPY ./VERSION /app/VERSION
COPY ./BUILD_DATE /app/BUILD_DATE
+# Copy the pre-built MkDocs documentation site (served at /help)
+COPY --from=docs-builder /docs/docs_build /app/docs_build
+
# Local fallbacks for build metadata
RUN echo "local" > /app/GIT_SHA \
&& echo "local" > /app/RUNTIME_INFO
diff --git a/app/main.py b/app/main.py
index 7aba642b..c03cdd1f 100644
--- a/app/main.py
+++ b/app/main.py
@@ -202,6 +202,15 @@ 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/
+# 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")
+else:
+ print(f"INFO: Help 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
@app.exception_handler(HTTPException)
diff --git a/app/views/__init__.py b/app/views/__init__.py
index 7b27bab1..6b1670d7 100644
--- a/app/views/__init__.py
+++ b/app/views/__init__.py
@@ -12,6 +12,7 @@ from app.views.filemanager import router as filemanager_router
# Import all the view routers
from app.views.general import router as general_router
from app.views.google_drive import router as google_drive_router
+from app.views.help import router as help_router # Built-in help / How-To docs
from app.views.license_routes import router as license_router # Add the license router
from app.views.onboarding import router as onboarding_router
from app.views.onedrive import router as onedrive_router
@@ -43,3 +44,4 @@ router.include_router(subscriptions_router) # Pricing + subscription pages
router.include_router(plans_router) # Admin Plan Designer
router.include_router(onboarding_router) # User onboarding wizard
router.include_router(pipelines_router) # Processing pipelines
+router.include_router(help_router) # Built-in help / How-To docs
diff --git a/app/views/help.py b/app/views/help.py
new file mode 100644
index 00000000..1df1c205
--- /dev/null
+++ b/app/views/help.py
@@ -0,0 +1,27 @@
+"""
+Help documentation 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/.
+"""
+
+import logging
+import pathlib
+
+from fastapi import Request
+from fastapi.responses import RedirectResponse
+
+from app.views.base import APIRouter
+
+logger = logging.getLogger(__name__)
+
+router = APIRouter()
+
+# Path to the built MkDocs documentation
+_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)
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 06e0f1c8..05e1584c 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -72,3 +72,22 @@ services:
image: redis:alpine
container_name: document_redis
restart: always
+
+ # Optional: standalone docs server using MkDocs Material (live-reload during development).
+ # Start with: docker compose --profile docs up docs
+ # Access at: http://localhost:8001
+ # Note: The /help route in the main app serves pre-built docs from the Docker image.
+ # This service is for content authors who want live preview while editing documentation.
+ docs:
+ image: squidfunk/mkdocs-material:latest
+ container_name: docuelevate_docs
+ restart: "no"
+ profiles:
+ - docs
+ volumes:
+ - ./docs:/docs/docs
+ - ./mkdocs.yml:/docs/mkdocs.yml
+ working_dir: /docs
+ command: ["serve", "--dev-addr=0.0.0.0:8000", "--config-file=/docs/mkdocs.yml"]
+ ports:
+ - "8001:8000"
diff --git a/docs/HowToGuides.md b/docs/HowToGuides.md
new file mode 100644
index 00000000..1ece48ea
--- /dev/null
+++ b/docs/HowToGuides.md
@@ -0,0 +1,80 @@
+# How-To Guides
+
+Welcome to the DocuElevate How-To Guides. These step-by-step articles help you connect common scanners, printers, and mobile devices to DocuElevate for automatic document ingestion and processing.
+
+---
+
+## Document Ingestion Guides
+
+Choose the method that matches your hardware or workflow:
+
+### 🖨️ Scanners & Printers
+
+| Guide | Description |
+|-------|-------------|
+| [HP Enterprise Printer Setup](howto/HPPrinterSetup.md) | Configure HP MFPs for automatic scan-to-DocuElevate via email, network folder, or WebDAV |
+| [Fujitsu ScanSnap Setup](howto/SnapScanSetup.md) | Set up ScanSnap iX1600/iX1500/iX500 to send scans directly to DocuElevate |
+| [Watched Folder Setup](howto/WatchedFolderSetup.md) | Monitor a local or network folder and automatically ingest any files dropped into it |
+
+### 📧 Email & Cloud
+
+| Guide | Description |
+|-------|-------------|
+| [Email Ingestion](howto/EmailIngestion.md) | Route documents sent as email attachments into DocuElevate automatically |
+| [Mobile Scanning](howto/MobileScanning.md) | Use iOS/Android apps to capture and upload documents from your phone |
+
+---
+
+## Quick Comparison: Which Method Is Right for You?
+
+| Method | Best For | Setup Complexity |
+|--------|----------|-----------------|
+| **Web Upload** | Occasional one-off uploads | ⭐ Very Easy |
+| **Watched Folder** | Any scanner that saves to a folder | ⭐⭐ Easy |
+| **Email Ingestion** | Scanners with Scan-to-Email, fax services | ⭐⭐ Easy |
+| **HP Printer (SMB)** | HP Enterprise MFPs on a corporate network | ⭐⭐⭐ Medium |
+| **ScanSnap + Cloud** | Home/office ScanSnap via Dropbox/Drive | ⭐⭐ Easy |
+| **Mobile App** | On-the-go document capture | ⭐ Very Easy |
+| **API Integration** | Custom workflows, developer integrations | ⭐⭐⭐⭐ Advanced |
+
+---
+
+## General Configuration Tips
+
+### Enable OCR for Searchable PDFs
+
+DocuElevate applies OCR (Optical Character Recognition) to scanned documents, making them fully searchable. Configure the OCR engine in your `.env`:
+
+```env
+OCR_ENABLED=true
+OCR_ENGINE=tesseract # or: azure, google
+OCR_LANGUAGE=eng # ISO 639-2 language code
+```
+
+### Set Up Automatic Cloud Backup
+
+After processing, DocuElevate can store documents in your preferred cloud storage. Configure in Settings or `.env`:
+
+```env
+DEFAULT_STORAGE_TARGET=dropbox # or: gdrive, onedrive, s3, nextcloud
+```
+
+### Configure Notifications
+
+Get notified when documents are processed:
+
+```env
+NOTIFICATION_ENABLED=true
+NOTIFICATION_EMAIL=you@example.com
+```
+
+See [Notifications Setup](NotificationsSetup.md) for webhook, Slack, and other integrations.
+
+---
+
+## Need Help?
+
+- Browse the full [Documentation](UserGuide.md)
+- Check [Troubleshooting](Troubleshooting.md) for common issues
+- View [Configuration Reference](ConfigurationGuide.md) for all settings
+- Explore the [API Reference](API.md) for programmatic access
diff --git a/docs/howto/EmailIngestion.md b/docs/howto/EmailIngestion.md
new file mode 100644
index 00000000..97ed52c2
--- /dev/null
+++ b/docs/howto/EmailIngestion.md
@@ -0,0 +1,154 @@
+# How to Set Up Automatic Document Ingestion via Email
+
+Many devices (scanners, printers, fax services, and apps) can send documents as email attachments. This guide explains how to automatically route those attachments into DocuElevate.
+
+---
+
+## Overview
+
+The Email Ingestion workflow works like this:
+
+```
+Scanner/Device → Email (SMTP) → Monitored Mailbox → DocuElevate API → Processing & Storage
+```
+
+DocuElevate periodically checks a designated email inbox, downloads PDF/image attachments, and processes them through the standard document pipeline.
+
+---
+
+## Prerequisites
+
+- An email account dedicated to document ingestion (e.g., `scan@yourdomain.com`)
+- IMAP access enabled for that account
+- DocuElevate running with the Celery worker active
+
+---
+
+## Configuration
+
+Add the following to your DocuElevate `.env` file:
+
+```env
+# Email ingestion settings
+EMAIL_INGESTION_ENABLED=true
+EMAIL_INGESTION_IMAP_HOST=mail.yourdomain.com
+EMAIL_INGESTION_IMAP_PORT=993
+EMAIL_INGESTION_IMAP_SSL=true
+EMAIL_INGESTION_USERNAME=scan@yourdomain.com
+EMAIL_INGESTION_PASSWORD=your-email-password
+EMAIL_INGESTION_FOLDER=INBOX
+EMAIL_INGESTION_INTERVAL=60 # Check every 60 seconds
+EMAIL_INGESTION_MARK_SEEN=true # Mark emails as read after processing
+EMAIL_INGESTION_ALLOWED_SENDERS= # Comma-separated allowlist (empty = allow all)
+```
+
+Restart DocuElevate after saving the configuration:
+
+```bash
+docker compose restart api worker
+```
+
+---
+
+## Supported File Types
+
+DocuElevate will process the following attachment types from emails:
+
+| Type | Extension | Notes |
+|------|-----------|-------|
+| PDF | `.pdf` | Native support; OCR applied if not searchable |
+| JPEG/PNG | `.jpg`, `.jpeg`, `.png` | Converted to PDF before processing |
+| TIFF | `.tif`, `.tiff` | Common format from older scanners/fax |
+| Multi-page TIFF | `.tif` | Full multi-page support |
+
+---
+
+## Setting Up Your Scanner/Device
+
+### HP Printers – Scan to Email
+
+See the detailed guide: [HP Enterprise Printer Setup](./HPPrinterSetup.md#option-a-scan-to-email--docuelevate-api-upload)
+
+### Fujitsu ScanSnap – Send by Email
+
+See the detailed guide: [ScanSnap Setup](./SnapScanSetup.md#method-3-scan-to-email--docuelevate)
+
+### iOS/Android Scanning Apps
+
+Most mobile scanning apps (Adobe Scan, Microsoft Lens, SwiftScan) can email scans:
+
+1. Scan your document.
+2. Use the app's **Share** or **Send** function.
+3. Select **Email** and enter `scan@yourdomain.com`.
+4. DocuElevate will pick up the attachment within the configured interval.
+
+### Fax-to-Email Services
+
+Services like eFax, RingCentral Fax, or Twilio Fax can forward incoming faxes as email attachments. Configure them to send to `scan@yourdomain.com`.
+
+---
+
+## Security Considerations
+
+> **Important:** Only process emails from trusted sources to avoid ingesting malicious documents.
+
+Use the `EMAIL_INGESTION_ALLOWED_SENDERS` setting to restrict which email addresses can submit documents:
+
+```env
+EMAIL_INGESTION_ALLOWED_SENDERS=scanner@office.com,printer@office.com,fax@office.com
+```
+
+DocuElevate will silently skip emails from addresses not in the allowlist.
+
+Additionally:
+
+- Use a **dedicated email account** solely for document ingestion
+- Enable **app-specific passwords** (Gmail, Outlook) instead of your main account password
+- Store credentials in environment variables, never in config files committed to version control
+
+---
+
+## Monitoring
+
+Check the DocuElevate worker logs to verify email ingestion is running:
+
+```bash
+docker logs document_worker --follow
+```
+
+You should see log entries like:
+
+```
+INFO Email ingestion: checking inbox scan@yourdomain.com
+INFO Email ingestion: found 3 new messages
+INFO Email ingestion: processing attachment invoice-2024.pdf from printer@office.com
+INFO Email ingestion: queued document ID 142 for processing
+```
+
+---
+
+## Troubleshooting
+
+**No emails are being processed?**
+→ Verify IMAP credentials and that IMAP is enabled on your mail server.
+→ Check firewall rules: port 993 (SSL) or 143 (plain) must be open from DocuElevate to the mail server.
+
+**Gmail not working?**
+→ Enable "App Passwords" in your Google Account security settings.
+→ Use the App Password (not your main Google password) for `EMAIL_INGESTION_PASSWORD`.
+
+**Attachments processed but files are empty?**
+→ Some email clients send inline images instead of attachments. Check the raw email source.
+
+**Emails keep getting re-processed?**
+→ Set `EMAIL_INGESTION_MARK_SEEN=true` to mark emails as read after processing.
+→ Alternatively, configure a separate ingestion folder and move/delete emails after processing.
+
+---
+
+## Related Documentation
+
+- [HP Enterprise Printer Setup](./HPPrinterSetup.md)
+- [ScanSnap Setup](./SnapScanSetup.md)
+- [Configuration Guide](../ConfigurationGuide.md)
+- [Notifications Setup](../NotificationsSetup.md)
diff --git a/docs/howto/HPPrinterSetup.md b/docs/howto/HPPrinterSetup.md
new file mode 100644
index 00000000..30fcadba
--- /dev/null
+++ b/docs/howto/HPPrinterSetup.md
@@ -0,0 +1,198 @@
+# How to Set Up Automatic Document Ingestion with an HP Enterprise Printer
+
+This guide explains how to configure an HP Enterprise printer/MFP (Multi-Function Printer) to automatically send scanned documents to DocuElevate for processing and storage.
+
+---
+
+## Prerequisites
+
+- An HP Enterprise printer or MFP with **Scan to Email** or **Scan to Network Folder** capability
+- DocuElevate running and accessible on your network
+- Admin access to the HP printer's Embedded Web Server (EWS)
+- (Optional) An SMTP server or a configured email address for Scan to Email
+
+---
+
+## Option A: Scan to Email → DocuElevate API Upload
+
+HP Enterprise printers can send scanned documents as email attachments. You can set up a dedicated inbox that forwards documents to DocuElevate via the REST API.
+
+### Step 1: Configure Scan to Email on the Printer
+
+1. Open a browser and navigate to the printer's IP address (e.g., `http://192.168.1.100`) to access the **Embedded Web Server (EWS)**.
+2. Go to **Scan** → **Scan to E-mail**.
+3. Enable **Scan to E-mail** and configure your SMTP server settings.
+4. Create a **Quick Set** (shortcut) for the destination:
+ - **From:** `scanner@yourdomain.com`
+ - **To:** `docuelevate-inbox@yourdomain.com` (the receiving address you'll configure)
+ - **File Type:** PDF
+ - **Resolution:** 200–300 DPI (recommended)
+ - **Color Mode:** Grayscale or Black & White for text documents
+
+### Step 2: Set Up an Email-to-DocuElevate Bridge
+
+Use a lightweight tool like [imapfilter](https://github.com/lefcha/imapfilter) or a simple Python script (see below) to poll the inbox and upload attachments to DocuElevate via its REST API.
+
+**Example Python script (`email_to_docuelevate.py`):**
+
+```python
+import imaplib
+import email
+import requests
+import os
+
+IMAP_HOST = "mail.yourdomain.com"
+IMAP_USER = "docuelevate-inbox@yourdomain.com"
+IMAP_PASS = os.environ["IMAP_PASS"]
+DOCUELEVATE_URL = "http://your-docuelevate-host:8000"
+API_KEY = os.environ["DOCUELEVATE_API_KEY"]
+
+def fetch_and_upload():
+ mail = imaplib.IMAP4_SSL(IMAP_HOST)
+ mail.login(IMAP_USER, IMAP_PASS)
+ mail.select("INBOX")
+
+ _, msg_ids = mail.search(None, "UNSEEN")
+ for msg_id in msg_ids[0].split():
+ _, msg_data = mail.fetch(msg_id, "(RFC822)")
+ msg = email.message_from_bytes(msg_data[0][1])
+
+ for part in msg.walk():
+ if part.get_content_maintype() == "multipart":
+ continue
+ if part.get("Content-Disposition") is None:
+ continue
+ filename = part.get_filename()
+ if filename and filename.lower().endswith(".pdf"):
+ payload = part.get_payload(decode=True)
+ files = {"file": (filename, payload, "application/pdf")}
+ headers = {"Authorization": f"Bearer {API_KEY}"}
+ resp = requests.post(
+ f"{DOCUELEVATE_URL}/api/upload",
+ files=files,
+ headers=headers
+ )
+ print(f"Uploaded {filename}: {resp.status_code}")
+ mail.store(msg_id, "+FLAGS", "\\Seen")
+
+ mail.logout()
+
+if __name__ == "__main__":
+ fetch_and_upload()
+```
+
+Run this script via a cron job every few minutes:
+
+```bash
+*/5 * * * * /usr/bin/python3 /opt/email_to_docuelevate.py >> /var/log/docuelevate_import.log 2>&1
+```
+
+---
+
+## Option B: Scan to Network Folder (SMB/CIFS)
+
+HP Enterprise printers can scan directly to a network folder. You can configure a watched folder that DocuElevate monitors for new files.
+
+### Step 1: Set Up a Shared Network Folder
+
+On your DocuElevate server (or any reachable server), create a shared folder:
+
+```bash
+# Create the shared folder
+mkdir -p /srv/scanner-inbox
+
+# Install Samba
+sudo apt-get install samba
+
+# Add to /etc/samba/smb.conf:
+[scanner-inbox]
+ path = /srv/scanner-inbox
+ writable = yes
+ guest ok = yes
+ force user = nobody
+```
+
+Restart Samba: `sudo systemctl restart smbd`
+
+### Step 2: Configure the HP Printer for Scan to Network Folder
+
+1. Open the printer's **Embedded Web Server (EWS)**.
+2. Go to **Scan** → **Scan to Network Folder**.
+3. Click **Add** to create a new Quick Set:
+ - **UNC Path:** `\\192.168.1.200\scanner-inbox` (replace with your server's IP)
+ - **Username/Password:** Leave blank for guest, or provide credentials
+ - **File Type:** PDF (Searchable PDF if available)
+ - **Resolution:** 200–300 DPI
+4. Test the connection from the EWS interface.
+
+### Step 3: Configure DocuElevate to Watch the Folder
+
+In your DocuElevate `.env` configuration:
+
+```env
+# Enable folder watching
+WATCH_FOLDER_ENABLED=true
+WATCH_FOLDER_PATH=/srv/scanner-inbox
+WATCH_FOLDER_INTERVAL=30 # seconds between checks
+```
+
+DocuElevate's Celery worker will automatically detect and process new files placed in the watched folder.
+
+---
+
+## Option C: Scan to FTP/WebDAV
+
+DocuElevate supports FTP and WebDAV as upload targets. HP printers can send scanned documents directly.
+
+### WebDAV Configuration
+
+1. In the EWS, go to **Scan** → **Save to SharePoint** or **Save to Network Folder**.
+2. Some HP models support WebDAV directly — configure the WebDAV URL to point to DocuElevate's WebDAV endpoint (if enabled):
+ - **URL:** `http://your-docuelevate-host:8000/webdav/inbox/`
+ - **Username/Password:** Your DocuElevate credentials
+
+### FTP Configuration
+
+1. Ensure an FTP server is running alongside DocuElevate (or configure one in docker-compose).
+2. In the EWS, configure **Scan to FTP**:
+ - **FTP Server:** `192.168.1.200`
+ - **Port:** `21`
+ - **Remote Path:** `/scanner-inbox/`
+3. DocuElevate's watch folder will pick up the FTP-delivered files.
+
+---
+
+## Recommended Scanner Quick Set Settings
+
+| Setting | Recommended Value |
+|---------|------------------|
+| File Type | PDF (Searchable PDF / PDF/A if available) |
+| Resolution | 200–300 DPI |
+| Color Mode | Auto Detect or Grayscale |
+| Sides | Auto Detect (2-sided) |
+| Original Size | Auto Detect |
+| Orientation | Auto Detect |
+
+---
+
+## Troubleshooting
+
+**Printer can't connect to the network folder?**
+→ Verify the IP address and that the Samba/SMB service is running. Check firewall rules (port 445/TCP).
+
+**Scanned PDFs aren't being processed?**
+→ Check DocuElevate's Celery worker logs: `docker logs document_worker --follow`
+
+**Email attachments not arriving?**
+→ Verify SMTP settings on the printer. Check spam filters on the receiving mailbox.
+
+**Poor OCR quality?**
+→ Increase scan resolution to 300 DPI and use Grayscale mode for text documents.
+
+---
+
+## Related Documentation
+
+- [DocuElevate Configuration Guide](../ConfigurationGuide.md)
+- [Storage Architecture](../StorageArchitecture.md)
+- [Troubleshooting](../Troubleshooting.md)
diff --git a/docs/howto/MobileScanning.md b/docs/howto/MobileScanning.md
new file mode 100644
index 00000000..49a10da8
--- /dev/null
+++ b/docs/howto/MobileScanning.md
@@ -0,0 +1,152 @@
+# How to Scan Documents from Your Phone or Tablet
+
+DocuElevate works seamlessly with mobile scanning apps. This guide covers the best ways to capture documents with your phone or tablet and get them into DocuElevate automatically.
+
+---
+
+## Option 1: DocuElevate Web Upload (Simplest)
+
+The easiest way is to use DocuElevate's built-in web interface directly from your mobile browser.
+
+1. Open your phone's browser and navigate to your DocuElevate instance (e.g., `http://your-docuelevate-host:8000`).
+2. Tap **Upload** in the top navigation.
+3. Tap **Choose File** — your phone will open the camera or file picker.
+4. Select **Camera** to capture a photo of the document, or pick an existing file.
+5. Tap **Upload** — DocuElevate will process the image and convert it to a searchable PDF.
+
+> **Tip:** Use good lighting and hold the phone steady for better OCR quality.
+
+---
+
+## Option 2: DocuElevate Browser Extension
+
+The **DocuElevate Browser Extension** for Chrome/Firefox allows one-tap uploading from your mobile browser.
+
+See the [Browser Extension guide](../BrowserExtension.md) for installation and setup instructions.
+
+---
+
+## Option 3: Mobile Scanning Apps
+
+These apps provide superior document capture (auto perspective correction, multi-page, etc.) and can send directly to DocuElevate.
+
+### Microsoft Lens (iOS / Android)
+
+Microsoft Lens is free and integrates with OneDrive, which DocuElevate supports natively.
+
+1. Install **Microsoft Lens** from the App Store or Google Play.
+2. Scan your document.
+3. Save to **OneDrive** in a folder you've connected to DocuElevate.
+4. DocuElevate will automatically process new files in your OneDrive folder.
+
+### Adobe Scan (iOS / Android)
+
+Adobe Scan produces high-quality searchable PDFs.
+
+1. Install **Adobe Scan** from the App Store or Google Play.
+2. Scan your document.
+3. Tap **Share** → **Save to Files** (iOS) or **Share** (Android).
+4. Choose to save to your connected cloud storage (Dropbox, Google Drive) monitored by DocuElevate.
+
+**Or use email ingestion:**
+1. Tap **Share** → **Email**.
+2. Send to your DocuElevate ingestion email address.
+3. See [Email Ingestion Setup](./EmailIngestion.md) for configuration.
+
+### SwiftScan (iOS)
+
+SwiftScan (formerly Scanbot) offers excellent auto-capture and document enhancement.
+
+1. Install **SwiftScan** from the App Store.
+2. Scan your document.
+3. Configure the **Auto-Upload** feature:
+ - Go to **Settings** → **Cloud Services**.
+ - Connect Dropbox, Google Drive, or OneDrive.
+ - Set the upload folder to your DocuElevate-monitored folder.
+4. Every scan is automatically uploaded and processed by DocuElevate.
+
+### CamScanner (iOS / Android)
+
+1. Install **CamScanner**.
+2. Configure **Auto Backup** to Dropbox or Google Drive.
+3. Point DocuElevate at the same cloud folder.
+
+---
+
+## Option 4: iOS Shortcuts Automation
+
+On iPhone/iPad, you can create an **iOS Shortcut** that scans a document and uploads it directly to DocuElevate's API.
+
+### Creating the Shortcut
+
+1. Open the **Shortcuts** app on your iPhone.
+2. Tap **+** to create a new shortcut.
+3. Add the following actions:
+ - **Scan Document** — opens the camera for scanning
+ - **Get Contents of URL** — configures the API call:
+ - **URL:** `http://your-docuelevate-host:8000/api/upload`
+ - **Method:** POST
+ - **Headers:** `Authorization: Bearer YOUR_API_KEY`
+ - **Request Body:** Form data with `file` = Scanned Document
+4. Name the shortcut "Send to DocuElevate".
+5. Add it to your home screen for one-tap scanning.
+
+```
+Shortcut flow:
+┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐
+│ Scan Document│ → │ Select pages │ → │ POST to DocuElevate │
+│ (Camera) │ │ & crop │ │ /api/upload │
+└─────────────┘ └──────────────┘ └──────────────────────┘
+```
+
+---
+
+## Option 5: Android Automation with Tasker
+
+On Android, **Tasker** can automate document upload when files appear in a specific folder.
+
+1. Install **Tasker** from Google Play.
+2. Create a **Profile** triggered by **File Created** in your scanner app's output folder.
+3. Add a **Task** that calls DocuElevate's upload API using the **HTTP Request** action.
+
+---
+
+## Recommended Mobile Scanning Settings
+
+For best results with DocuElevate's OCR engine:
+
+| Setting | Recommended Value |
+|---------|------------------|
+| Output Format | PDF |
+| Resolution/Quality | High (equivalent to 300 DPI) |
+| Color Mode | Auto (greyscale for text, color for mixed) |
+| Perspective Correction | On (auto-straighten) |
+| Filter | Document / Black & White for text |
+| Multi-page | Combine into single PDF |
+
+---
+
+## Troubleshooting
+
+**Upload fails from mobile browser?**
+→ Check that DocuElevate is accessible from your phone's network (same WiFi, or publicly reachable).
+→ Verify your session is still logged in.
+
+**OCR quality is poor?**
+→ Ensure good lighting, no shadows on the document.
+→ Use your scanning app's auto-enhance or document filter.
+→ Increase resolution to "High" or "Best" in the app settings.
+
+**Files synced to cloud but not processed?**
+→ Check DocuElevate's cloud storage connection in Settings.
+→ View worker logs: `docker logs document_worker --tail 50`
+
+---
+
+## Related Documentation
+
+- [Email Ingestion Setup](./EmailIngestion.md)
+- [HP Enterprise Printer Setup](./HPPrinterSetup.md)
+- [ScanSnap Setup](./SnapScanSetup.md)
+- [Browser Extension](../BrowserExtension.md)
+- [API Reference](../API.md)
diff --git a/docs/howto/SnapScanSetup.md b/docs/howto/SnapScanSetup.md
new file mode 100644
index 00000000..369dfdf2
--- /dev/null
+++ b/docs/howto/SnapScanSetup.md
@@ -0,0 +1,192 @@
+# How to Set Up Fujitsu ScanSnap to Automatically Send Documents to DocuElevate
+
+This guide walks you through configuring a Fujitsu ScanSnap scanner to automatically upload scanned documents to DocuElevate for AI-powered processing and storage.
+
+---
+
+## Supported ScanSnap Models
+
+This guide applies to the following ScanSnap models:
+
+- **iX1600 / iX1500 / iX1400** – Wi-Fi and USB, ScanSnap Home software
+- **iX500 / iX100** – USB/Wi-Fi, ScanSnap Manager software
+- **S1300i / S1100i** – USB, ScanSnap Manager software
+
+> **Note:** ScanSnap Home (for newer models) and ScanSnap Manager (for older models) differ in their profile/job configuration interface. Both methods are covered below.
+
+---
+
+## Method 1: Scan to Folder → DocuElevate Watched Folder
+
+This is the simplest method and works with all ScanSnap models.
+
+### Step 1: Configure DocuElevate's Watched Folder
+
+Add the following to your DocuElevate `.env` file and restart the service:
+
+```env
+WATCH_FOLDER_ENABLED=true
+WATCH_FOLDER_PATH=/srv/scan-inbox
+WATCH_FOLDER_INTERVAL=10
+```
+
+Create the directory and set permissions:
+
+```bash
+mkdir -p /srv/scan-inbox
+chmod 777 /srv/scan-inbox
+```
+
+If running DocuElevate in Docker, mount this folder into the container in your `docker-compose.yaml`:
+
+```yaml
+services:
+ api:
+ volumes:
+ - /srv/scan-inbox:/srv/scan-inbox
+ worker:
+ volumes:
+ - /srv/scan-inbox:/srv/scan-inbox
+```
+
+### Step 2: Configure ScanSnap Home (iX1600/iX1500/iX1400)
+
+1. Open **ScanSnap Home** on your computer.
+2. Click **+** to create a new profile.
+3. Select **Save to folder** as the action.
+4. Configure the profile:
+ - **Folder:** `/srv/scan-inbox` (or a local folder that syncs to it)
+ - **File format:** PDF
+ - **Image quality:** Normal or Better
+ - **Color mode:** Auto
+ - **Scanning side:** Both sides (auto)
+5. Enable **Convert to searchable PDF** if your ScanSnap and ScanSnap Home version support it.
+6. Name the profile "DocuElevate" and save.
+
+Now, pressing the scan button while this profile is active will send the scanned PDF directly to the watched folder.
+
+### Step 3: Configure ScanSnap Manager (older models)
+
+1. Right-click the ScanSnap Manager icon in the system tray.
+2. Select **Scan Button Settings**.
+3. Choose the **Save** tab.
+4. Set **Image saving folder** to `/srv/scan-inbox` (or use a shared network path).
+5. Under **File format**, select **PDF**.
+6. Click **OK** to save.
+
+---
+
+## Method 2: Scan to Cloud Storage → DocuElevate Sync
+
+If you already use Dropbox, Google Drive, or OneDrive with ScanSnap, you can leverage those integrations to feed documents into DocuElevate.
+
+### Using Dropbox
+
+1. In ScanSnap Home, create a profile with action **Save to Dropbox**.
+2. Set the destination folder to `ScanSnap/` (or any folder).
+3. In DocuElevate settings, configure Dropbox as your storage provider and point DocuElevate to monitor/pull from that folder.
+4. DocuElevate will process new documents as they appear in Dropbox.
+
+### Using Google Drive
+
+1. Create a ScanSnap profile that saves to **Google Drive** under a dedicated folder (e.g., `ScanSnap/Inbox`).
+2. Configure DocuElevate's Google Drive integration to use that folder as a source/destination.
+
+---
+
+## Method 3: Scan to Email → DocuElevate
+
+ScanSnap can send scanned documents as email attachments. Pair this with the email-to-DocuElevate bridge described in the [HP Printer Guide](./HPPrinterSetup.md#option-a-scan-to-email--docuelevate-api-upload) for automatic ingestion.
+
+1. In ScanSnap Home, create a profile with action **Send by E-mail**.
+2. Configure the recipient address as your DocuElevate-monitored mailbox.
+3. Set file format to **PDF**.
+4. The email bridge script will pick up the attachment and upload it to DocuElevate.
+
+---
+
+## Method 4: ScanSnap Cloud (iX1600 / ScanSnap Home 3.x+)
+
+The ScanSnap iX1600 supports **ScanSnap Cloud**, which can send directly to cloud services.
+
+1. Open the **ScanSnap Home** app or use the printer's touchscreen.
+2. Configure a **ScanSnap Cloud** profile that saves to **Google Drive** or **Dropbox**.
+3. Point DocuElevate to monitor that cloud folder.
+
+---
+
+## Recommended Scan Settings
+
+| Setting | Recommended Value |
+|---------|------------------|
+| File Format | PDF (Searchable PDF if available) |
+| Image Quality | Normal (200 DPI) for text, Better (300 DPI) for photos |
+| Color Mode | Auto (detects black/white vs color) |
+| Scanning Side | Both Sides (Auto) |
+| Compression | Medium |
+| Rotate | Auto |
+| Remove blank pages | Yes |
+| Correct skewed scans | Yes |
+
+---
+
+## Automating with ScanSnap Home Profiles
+
+You can create multiple profiles in ScanSnap Home for different document types:
+
+| Profile | Settings | Notes |
+|---------|----------|-------|
+| **Documents** | B&W, 200 DPI, PDF | For invoices, letters, contracts |
+| **Photos** | Color, 300 DPI, PDF | For photo documents |
+| **Receipts** | B&W, 200 DPI, PDF, Remove blank pages | For expense reports |
+| **Business Cards** | Color, 300 DPI, JPEG | May require separate OCR |
+
+Each profile can be assigned to the scanner's shortcut button (on models with a touchscreen).
+
+---
+
+## Setting Up One-Button Scanning
+
+On the **ScanSnap iX1600** with its touchscreen:
+
+1. Tap the profile name (e.g., "DocuElevate") on the scanner's display.
+2. The scanner will use that profile for the next scan.
+3. Press the physical **Scan** button.
+4. The document is scanned and automatically delivered to DocuElevate's watched folder.
+
+On models without a touchscreen:
+
+1. Set the "DocuElevate" profile as the **default profile** in ScanSnap Manager/Home.
+2. Press the **Scan** button — the document goes directly to DocuElevate.
+
+---
+
+## Troubleshooting
+
+**Files appear in the folder but DocuElevate doesn't process them?**
+→ Check that `WATCH_FOLDER_ENABLED=true` in your `.env` and that the worker can read the folder:
+```bash
+docker logs document_worker --tail 50
+```
+
+**ScanSnap can't find the network folder?**
+→ Ensure the folder is shared over SMB/CIFS (Windows share). See the [HP Printer Setup guide](./HPPrinterSetup.md#step-1-set-up-a-shared-network-folder) for Samba configuration.
+
+**Scanned PDFs have poor text recognition?**
+→ Increase scan resolution to 300 DPI. Enable "Convert to searchable PDF" in ScanSnap Home if available.
+
+**Files are processed twice (duplicates)?**
+→ Enable duplicate detection in DocuElevate settings. Check [Troubleshooting](../Troubleshooting.md) for deduplication options.
+
+**ScanSnap doesn't appear in ScanSnap Home after network change?**
+→ Re-run the ScanSnap network setup wizard. Ensure the scanner and your computer are on the same Wi-Fi network/subnet.
+
+---
+
+## Related Documentation
+
+- [HP Enterprise Printer Setup](./HPPrinterSetup.md)
+- [DocuElevate Configuration Guide](../ConfigurationGuide.md)
+- [Dropbox Setup](../DropboxSetup.md)
+- [Google Drive Setup](../GoogleDriveSetup.md)
+- [Troubleshooting](../Troubleshooting.md)
diff --git a/docs/howto/WatchedFolderSetup.md b/docs/howto/WatchedFolderSetup.md
new file mode 100644
index 00000000..325a9ed4
--- /dev/null
+++ b/docs/howto/WatchedFolderSetup.md
@@ -0,0 +1,212 @@
+# How to Set Up a Watched Folder for Automatic Document Ingestion
+
+The **Watched Folder** feature allows DocuElevate to automatically detect and process documents placed in a specific local directory. Any scanner, application, or script that saves files to that folder will have its output automatically ingested.
+
+---
+
+## Overview
+
+```
+Scanner / Any App → Drop files in folder → DocuElevate watches for new files → Process & Store
+```
+
+This is one of the simplest ingestion methods and works with virtually any scanner, OCR app, or document workflow tool.
+
+---
+
+## Configuration
+
+Add these settings to your `.env` file:
+
+```env
+WATCH_FOLDER_ENABLED=true
+WATCH_FOLDER_PATH=/srv/docuelevate/watch
+WATCH_FOLDER_INTERVAL=30 # Polling interval in seconds
+WATCH_FOLDER_RECURSIVE=false # Watch subdirectories too
+WATCH_FOLDER_DELETE_AFTER=true # Delete originals after successful processing
+WATCH_FOLDER_EXTENSIONS=pdf,jpg,jpeg,png,tiff,tif
+```
+
+**Restart** the services after editing:
+
+```bash
+docker compose restart api worker
+```
+
+---
+
+## Docker Setup
+
+When running DocuElevate with Docker Compose, mount the watch folder into both the `api` and `worker` containers:
+
+```yaml
+# docker-compose.yaml
+services:
+ api:
+ volumes:
+ - /srv/docuelevate/watch:/srv/docuelevate/watch
+ - /var/docparse/workdir:/workdir
+
+ worker:
+ volumes:
+ - /srv/docuelevate/watch:/srv/docuelevate/watch
+ - /var/docparse/workdir:/workdir
+```
+
+Create the folder and set permissions:
+
+```bash
+sudo mkdir -p /srv/docuelevate/watch
+sudo chmod 777 /srv/docuelevate/watch
+```
+
+---
+
+## Multiple Watch Folders
+
+If you have multiple scanners or document sources, you can organize them by subfolder:
+
+```
+/srv/docuelevate/watch/
+├── reception/ ← Front desk scanner
+├── accounting/ ← Finance team scanner
+├── hr/ ← HR department
+└── general/ ← General purpose
+```
+
+Enable recursive watching:
+
+```env
+WATCH_FOLDER_RECURSIVE=true
+```
+
+DocuElevate will monitor all subdirectories and tag documents with the subfolder name for easy filtering.
+
+---
+
+## Using with Network Scanners
+
+### Windows / Samba Share
+
+Share the watch folder over the network so scanners and Windows PCs can drop files directly:
+
+```bash
+# Install Samba
+sudo apt-get install samba -y
+
+# Add to /etc/samba/smb.conf
+[DocuElevate-Inbox]
+ comment = DocuElevate Document Inbox
+ path = /srv/docuelevate/watch
+ browsable = yes
+ guest ok = yes
+ read only = no
+ create mask = 0777
+ directory mask = 0777
+```
+
+Restart Samba:
+```bash
+sudo systemctl restart smbd nmbd
+```
+
+Windows access: `\\your-server-ip\DocuElevate-Inbox`
+
+### NFS Share (Linux)
+
+```bash
+# Add to /etc/exports
+/srv/docuelevate/watch 192.168.1.0/24(rw,sync,no_subtree_check)
+
+# Apply changes
+sudo exportfs -ra
+```
+
+### FTP Server (for older devices)
+
+Many older scanners only support FTP. Run a simple FTP server alongside DocuElevate:
+
+```yaml
+# Add to docker-compose.yaml
+services:
+ ftp:
+ image: garethflowers/ftp-server
+ container_name: docuelevate_ftp
+ environment:
+ - FTP_USER=scanner
+ - FTP_PASS=changeme
+ ports:
+ - "21:21"
+ - "20:20"
+ - "21100-21110:21100-21110"
+ volumes:
+ - /srv/docuelevate/watch:/home/scanner
+```
+
+Configure your scanner's FTP settings to use this server. Files will land in the watched folder.
+
+---
+
+## Supported File Types
+
+| Format | Extension | Notes |
+|--------|-----------|-------|
+| PDF | `.pdf` | OCR applied if not already searchable |
+| JPEG | `.jpg`, `.jpeg` | Converted to PDF |
+| PNG | `.png` | Converted to PDF |
+| TIFF | `.tif`, `.tiff` | Supports multi-page TIFF |
+| HEIC | `.heic` | iPhone photos (converted) |
+
+---
+
+## Monitoring
+
+Check that the watched folder is active:
+
+```bash
+# View worker logs
+docker logs document_worker --follow
+
+# Expected output:
+# INFO Watch folder monitor: watching /srv/docuelevate/watch (interval: 30s)
+# INFO Watch folder: found new file invoice.pdf
+# INFO Watch folder: queued document ID 156 for processing
+```
+
+You can also view processing status in the DocuElevate web interface under **Queue Monitor**.
+
+---
+
+## Troubleshooting
+
+**Files are dropped but not picked up?**
+- Verify `WATCH_FOLDER_ENABLED=true` in `.env`
+- Check that `WATCH_FOLDER_PATH` matches the mounted path in docker-compose
+- Look at worker logs: `docker logs document_worker --tail 100`
+
+**Permission denied errors?**
+```bash
+# Fix permissions
+sudo chmod -R 777 /srv/docuelevate/watch
+# Or use ACLs for more granular control
+sudo setfacl -m u:nobody:rwx /srv/docuelevate/watch
+```
+
+**Files processed but not deleted?**
+- Set `WATCH_FOLDER_DELETE_AFTER=true`
+- If you want to keep originals, set to `false` and manage cleanup separately
+
+**File appears partially uploaded?**
+- Large files may arrive before the scanner finishes writing them
+- Increase `WATCH_FOLDER_INTERVAL` to give time for files to be fully written
+- DocuElevate uses file-lock detection to avoid processing incomplete files
+
+---
+
+## Related Documentation
+
+- [HP Enterprise Printer Setup](./HPPrinterSetup.md)
+- [ScanSnap Setup](./SnapScanSetup.md)
+- [Email Ingestion](./EmailIngestion.md)
+- [Configuration Guide](../ConfigurationGuide.md)
+- [Deployment Guide](../DeploymentGuide.md)
diff --git a/frontend/templates/base.html b/frontend/templates/base.html
index af1c1414..5005b620 100644
--- a/frontend/templates/base.html
+++ b/frontend/templates/base.html
@@ -76,6 +76,12 @@
{% if request and request.url.path == '/about' %}aria-current="page"{% endif %}>
About
+
+ Help
+
{% else %}
{# ── App nav (logged-in or single-user / auth-disabled mode) ────── #}
@@ -171,6 +177,14 @@
Status
+
+
+ Help
+
+
{% endif %}{# end multi_user_enabled / is_logged_in check #}
@@ -229,6 +243,11 @@
{% if request and request.url.path == '/about' %}aria-current="page"{% endif %}>
About
+
+ Help
+
{% else %}
{# ── App links (logged-in or single-user) ────────────────────── #}
@@ -298,6 +317,13 @@
Status
+
+
+ Help
+
+
{% endif %}{# end multi_user_enabled / is_logged_in check #}
diff --git a/mkdocs.yml b/mkdocs.yml
index f72c8919..0ee0b569 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -12,11 +12,34 @@ theme:
- navigation.instant
- search.suggest
- content.code.copy
+ - navigation.sections
+ - navigation.top
+ - toc.integrate
+ palette:
+ - scheme: default
+ primary: blue
+ accent: blue
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ - scheme: slate
+ primary: blue
+ accent: blue
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
nav:
- Getting Started:
- Setup Wizard: SetupWizard
- User Guide: UserGuide
- Browser Extension: BrowserExtension
+ - How-To Guides:
+ - Overview: HowToGuides.md
+ - HP Enterprise Printer: howto/HPPrinterSetup.md
+ - Fujitsu ScanSnap: howto/SnapScanSetup.md
+ - Watched Folder: howto/WatchedFolderSetup.md
+ - Email Ingestion: howto/EmailIngestion.md
+ - Mobile Scanning: howto/MobileScanning.md
- API: API
- Deployment:
- Overview: DeploymentGuide
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 7caf3e74..72d8d3af 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -19,6 +19,9 @@ ruff>=0.3.0
mypy>=1.8.0
djlint>=1.36.0 # HTML template linter for accessibility and best practices
+# Documentation (builds the embedded /help site via MkDocs Material)
+mkdocs-material>=9.5.0 # MkDocs Material theme – same package used in docs/requirements.txt
+
# Type stubs for mypy
types-requests>=2.31.0
types-paramiko>=3.0.0
diff --git a/tests/test_views_help.py b/tests/test_views_help.py
new file mode 100644
index 00000000..df9082fb
--- /dev/null
+++ b/tests/test_views_help.py
@@ -0,0 +1,92 @@
+"""Tests for app/views/help.py module."""
+
+import pytest
+
+
+@pytest.mark.integration
+class TestHelpViews:
+ """Tests for the help documentation 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_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_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)
+ assert response.status_code != 405
+
+
+@pytest.mark.unit
+class TestHelpViewUnit:
+ """Unit tests for the help view module."""
+
+ def test_help_redirect_returns_301(self):
+ """Verify the redirect is HTTP 301 (permanent)."""
+ from fastapi import FastAPI
+ from fastapi.testclient import TestClient
+
+ from app.views.help import router
+
+ app = FastAPI()
+ app.include_router(router)
+ with TestClient(app) as tc:
+ resp = tc.get("/help", follow_redirects=False)
+ assert resp.status_code == 301
+
+ def test_help_redirect_target(self):
+ """Verify the redirect points to /help/."""
+ from fastapi import FastAPI
+ from fastapi.testclient import TestClient
+
+ from app.views.help import router
+
+ app = FastAPI()
+ 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/")
+
+ def test_docs_build_path_constant(self):
+ """Verify _DOCS_BUILD_DIR is resolved relative to the app package."""
+ import pathlib
+
+ from app.views.help import _DOCS_BUILD_DIR
+
+ assert isinstance(_DOCS_BUILD_DIR, pathlib.Path)
+ # Should point to /docs_build
+ assert _DOCS_BUILD_DIR.name == "docs_build"
+
+
+@pytest.mark.integration
+class TestHelpNavigationLink:
+ """Tests that the Help link appears in the navigation."""
+
+ def test_help_link_in_nav(self, client):
+ """The Help navigation link should appear in the base template."""
+ 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
+
+ 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/
+ assert "Help" in content