diff --git a/app/views/__init__.py b/app/views/__init__.py index c57c027a..ca2e9952 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -15,6 +15,7 @@ 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.imap_accounts import router as imap_accounts_router +from app.views.integrations import router as integrations_router # Unified integrations dashboard 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 @@ -48,4 +49,5 @@ 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(imap_accounts_router) # Per-user IMAP ingestion accounts +router.include_router(integrations_router) # Unified integrations dashboard router.include_router(help_router) # Built-in help / How-To docs diff --git a/app/views/integrations.py b/app/views/integrations.py new file mode 100644 index 00000000..232115ec --- /dev/null +++ b/app/views/integrations.py @@ -0,0 +1,116 @@ +"""User-facing view for the unified Sources & Destinations dashboard.""" + +import logging + +from fastapi import HTTPException, Request, status +from sqlalchemy.orm import Session + +from app.models import IntegrationDirection, IntegrationType, UserIntegration +from app.utils.subscription import get_tier, get_user_tier_id +from app.utils.user_scope import get_current_owner_id +from app.views.base import APIRouter, Depends, get_db, require_login, templates + +logger = logging.getLogger(__name__) +router = APIRouter() + +# Source types that consume the mailbox quota (mirrored from app.api.integrations) +_MAILBOX_SOURCE_TYPES = {IntegrationType.IMAP} +_FREE_TIER_ID = "free" + + +def _get_max_destinations(tier: dict) -> int | None: + """Return the maximum number of storage destinations allowed by *tier*. + + Returns ``None`` for unlimited, or a positive int for the cap. + """ + tier_id: str = tier.get("id", _FREE_TIER_ID) + max_dest: int = tier.get("max_storage_destinations", 0) + if tier_id == _FREE_TIER_ID: + return max_dest if max_dest > 0 else 1 + if max_dest == 0: + return None + return max_dest + + +def _get_max_sources(tier: dict) -> int | None: + """Return the maximum number of IMAP sources allowed by *tier*. + + Returns ``None`` for unlimited, ``0`` for no access, or a positive int. + """ + tier_id: str = tier.get("id", _FREE_TIER_ID) + max_mb: int = tier.get("max_mailboxes", 0) + if tier_id == _FREE_TIER_ID: + return 0 + if max_mb == 0: + return None + return max_mb + + +@router.get("/integrations") +@require_login +async def integrations_dashboard(request: Request, db: Session = Depends(get_db)): + """Render the unified Sources & Destinations dashboard.""" + try: + owner_id = get_current_owner_id(request) + + dest_count = 0 + src_count = 0 + max_destinations: int | None = 1 + max_sources: int | None = 0 + can_add_destination = False + can_add_source = False + tier_name = "Free" + tier_id = "free" + + if owner_id: + from sqlalchemy import func + + dest_count = ( + db.query(func.count()) + .select_from(UserIntegration) + .filter( + UserIntegration.owner_id == owner_id, + UserIntegration.direction == IntegrationDirection.DESTINATION, + ) + .scalar() + ) or 0 + src_count = ( + db.query(func.count()) + .select_from(UserIntegration) + .filter( + UserIntegration.owner_id == owner_id, + UserIntegration.direction == IntegrationDirection.SOURCE, + UserIntegration.integration_type.in_(list(_MAILBOX_SOURCE_TYPES)), + ) + .scalar() + ) or 0 + tier_id = get_user_tier_id(db, owner_id) + tier = get_tier(tier_id, db) + tier_name = tier.get("name", tier_id) + max_destinations = _get_max_destinations(tier) + max_sources = _get_max_sources(tier) + can_add_destination = max_destinations is None or dest_count < max_destinations + can_add_source = max_sources is None or (max_sources > 0 and src_count < max_sources) + + return templates.TemplateResponse( + "integrations_dashboard.html", + { + "request": request, + "dest_count": dest_count, + "src_count": src_count, + "max_destinations": max_destinations, + "max_sources": max_sources, + "can_add_destination": can_add_destination, + "can_add_source": can_add_source, + "tier_id": tier_id, + "tier_name": tier_name, + }, + ) + except HTTPException: + raise + except Exception as exc: + logger.error(f"Error loading integrations dashboard: {exc}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to load integrations dashboard", + ) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index d06943a4..7cb85543 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -140,6 +140,39 @@ In addition to the system-wide IMAP mailboxes configured by the administrator vi The quota bar on the Email Ingestion page shows your current usage against your plan limit. If you have reached the limit, delete an existing account or upgrade your plan. +#### Integrations Dashboard + +The **Integrations** page (`/integrations`) provides a unified view of all your configured ingestion sources and storage destinations. Instead of managing each integration type separately, you can create, edit, test, and delete any integration from a single dashboard. + +**Opening the dashboard:** Click **Integrations** in the top navigation bar. + +**Quota indicators** at the top of the page show your current usage: +- **Mailbox Sources** — how many IMAP ingestion accounts you have vs. your plan limit +- **Storage Destinations** — how many storage targets you have vs. your plan limit +- An **Upgrade Plan** link appears when you have reached your plan limit + +**Adding a new integration:** + +1. Click **Add Integration**. +2. Choose a **Direction** — Source (ingestion) or Destination (storage). +3. Choose an **Integration Type** (e.g. IMAP, S3, Dropbox, WebDAV). +4. Fill in the type-specific fields — the form adapts dynamically based on your choice: + - **IMAP** — host, port, username, password, SSL toggle + - **S3** — bucket, region, access key, secret key + - **WebDAV / Nextcloud** — URL, folder, username, password + - **FTP / SFTP** — host, port, remote path, username, password + - **Dropbox / Google Drive / OneDrive** — folder path, with a link to the OAuth setup page + - **Email Forward** — recipient email address + - **Watch Folder** — folder path + - **Paperless NGX** — URL and API token +5. Click **Test Connection** to verify the settings before saving. +6. Click **Save** to persist the integration. + +**Managing existing integrations:** +- Click **Test** on any card to re-verify the connection. +- Click **Edit** to update the configuration or credentials. +- Click **Delete** to permanently remove the integration. + ### Watch Folders (Automatic Folder Ingestion) Watch folders allow DocuElevate to automatically monitor directories for new files and ingest them without any manual action. diff --git a/frontend/templates/base.html b/frontend/templates/base.html index ffb7f7c6..4ef16130 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -106,10 +106,10 @@ {% if request and request.url.path == '/pipelines' %}aria-current="page"{% endif %}> Pipelines - - Email Ingestion + {% if request and request.url.path == '/integrations' %}aria-current="page"{% endif %}> + Integrations @@ -278,10 +278,10 @@ {% if request and request.url.path == '/pipelines' %}aria-current="page"{% endif %}> Pipelines - - Email Ingestion + {% if request and request.url.path == '/integrations' %}aria-current="page"{% endif %}> + Integrations diff --git a/frontend/templates/integrations_dashboard.html b/frontend/templates/integrations_dashboard.html new file mode 100644 index 00000000..434ba630 --- /dev/null +++ b/frontend/templates/integrations_dashboard.html @@ -0,0 +1,1128 @@ +{% extends "base.html" %} +{% block title %}Integrations – DocuElevate{% endblock %} + +{% block content %} +
+ Manage your ingestion sources and storage destinations in one place. +
++ Mailbox Sources: + + + / + + + / unlimited + + + (not available) + +
++ Plan: +
++ Storage Destinations: + + + / + + + / unlimited + +
++ Plan: +
+Loading integrations…
++ Add your first integration to connect ingestion sources (like IMAP) and storage destinations (like S3, Dropbox, or Google Drive). +
+ ++ + +
+ + + ++ + +
+ + + +