style: fix all flake8 linter errors across app/ directory

- Run Black formatter and isort on all app/ files
- Remove unused imports (F401) across multiple files
- Add # noqa: F401 for intentional re-exports in celery_worker.py,
  tasks/__init__.py, utils.py, frontend.py, views/base.py
- Fix f-strings without placeholders (F541) in azure.py, notification.py,
  check_credentials.py, upload_to_onedrive.py, settings.py
- Fix bare except (E722) in upload_to_sftp.py
- Fix block comment format (E265) in models.py
- Move imports to top of file to fix E402 in celery_app.py, celery_worker.py
- Fix line-too-long (E501) by wrapping strings in multiple files
- Remove unused variable (F841) in upload_to_nextcloud.py

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-08 17:42:33 +00:00
parent 7827b97e06
commit d08040ac4a
73 changed files with 2200 additions and 2185 deletions
+5 -3
View File
@@ -1,16 +1,18 @@
"""
Aggregated view routers for the application.
"""
from fastapi import APIRouter
from app.views.dropbox import router as dropbox_router
# Import all the view routers
from app.views.general import router as general_router
from app.views.status import router as status_router
from app.views.onedrive import router as onedrive_router
from app.views.dropbox import router as dropbox_router
from app.views.google_drive import router as google_drive_router
from app.views.license_routes import router as license_router # Add the license router
from app.views.onedrive import router as onedrive_router
from app.views.settings import router as settings_router
from app.views.status import router as status_router
from app.views.wizard import router as wizard_router
# Create a main router that includes all the view routers
+12 -7
View File
@@ -1,15 +1,17 @@
"""
Base setup for views, containing shared functionality and imports.
"""
from fastapi import APIRouter, Request, Depends, HTTPException
from fastapi.templating import Jinja2Templates
from pathlib import Path
from sqlalchemy.orm import Session
import logging
from app.auth import require_login
from app.database import SessionLocal
import logging
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Request # noqa: F401
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session # noqa: F401
from app.auth import require_login # noqa: F401
from app.config import settings
from app.database import SessionLocal
# Set up Jinja2 templates
templates_dir = Path(__file__).parent.parent.parent / "frontend" / "templates"
@@ -22,6 +24,7 @@ templates.env.globals["max"] = max
# Customize Jinja2Templates to include app_version in all templates
original_template_response = templates.TemplateResponse
def template_response_with_version(*args, **kwargs):
"""Wrapper for TemplateResponse to include version in all templates"""
# If context dict is provided, add version to it
@@ -31,11 +34,13 @@ def template_response_with_version(*args, **kwargs):
kwargs["context"].setdefault("version", settings.version)
return original_template_response(*args, **kwargs)
templates.TemplateResponse = template_response_with_version
# Set up logging
logger = logging.getLogger(__name__)
def get_db():
"""
Dependency to get a database session.
+15 -18
View File
@@ -1,12 +1,14 @@
"""
Dropbox integration views for setup and OAuth callback.
"""
from fastapi import Request
from app.views.base import APIRouter, templates, require_login, settings
from app.views.base import APIRouter, require_login, settings, templates
router = APIRouter()
@router.get("/dropbox-setup")
@require_login
async def dropbox_setup_page(request: Request):
@@ -15,10 +17,8 @@ async def dropbox_setup_page(request: Request):
Shows configuration status and setup instructions.
"""
# Check Dropbox configuration
is_configured = bool(settings.dropbox_app_key and
settings.dropbox_app_secret and
settings.dropbox_refresh_token)
is_configured = bool(settings.dropbox_app_key and settings.dropbox_app_secret and settings.dropbox_refresh_token)
return templates.TemplateResponse(
"dropbox.html",
{
@@ -27,10 +27,11 @@ async def dropbox_setup_page(request: Request):
"app_key_value": settings.dropbox_app_key or "",
"app_secret_value": settings.dropbox_app_secret if settings.dropbox_app_secret else "",
"refresh_token_value": settings.dropbox_refresh_token if settings.dropbox_refresh_token else "",
"folder_path": settings.dropbox_folder or "/Documents/Uploads" # Default folder path
}
"folder_path": settings.dropbox_folder or "/Documents/Uploads", # Default folder path
},
)
@router.get("/dropbox-callback")
@require_login
async def dropbox_callback(request: Request, code: str = None, error: str = None):
@@ -39,27 +40,23 @@ async def dropbox_callback(request: Request, code: str = None, error: str = None
Automatically exchanges the code for a token and saves it to the configuration.
"""
if error:
return templates.TemplateResponse(
"dropbox_callback_error.html",
{"request": request, "error": error}
)
return templates.TemplateResponse("dropbox_callback_error.html", {"request": request, "error": error})
if not code:
return templates.TemplateResponse(
"dropbox_callback_error.html",
{"request": request, "error": "No authorization code received from Dropbox"}
"dropbox_callback_error.html", {"request": request, "error": "No authorization code received from Dropbox"}
)
# Display the processing page with automatic token exchange
# Note: We provide empty strings for app_key_value and app_secret_value
# to prevent overriding what's in sessionStorage
return templates.TemplateResponse(
"dropbox_callback.html",
{
"request": request,
"request": request,
"code": code,
"app_key_value": "", # The callback will prioritize sessionStorage values
"app_secret_value": "", # The callback will prioritize sessionStorage values
"folder_path": "" # The callback will prioritize sessionStorage values
}
"folder_path": "", # The callback will prioritize sessionStorage values
},
)
+9 -6
View File
@@ -2,13 +2,14 @@
File management views for displaying and managing files.
"""
from fastapi import Request, Depends, Query
from sqlalchemy.orm import Session
from typing import Optional
from app.views.base import APIRouter, templates, require_login, get_db, logger
from app.utils.file_status import get_files_processing_status
from fastapi import Depends, Query, Request
from sqlalchemy.orm import Session
from app.config import settings
from app.utils.file_status import get_files_processing_status
from app.views.base import APIRouter, get_db, logger, require_login, templates
router = APIRouter()
@@ -31,8 +32,9 @@ def files_page(
"""
try:
# Import the model here to avoid circular imports
from sqlalchemy import asc, desc, or_
from app.models import FileRecord, ProcessingLog
from sqlalchemy import desc, asc, or_
# Start with base query
query = db.query(FileRecord)
@@ -159,9 +161,10 @@ def file_detail_page(request: Request, file_id: int, db: Session = Depends(get_d
Return the file detail page showing processing history and file information
"""
try:
from app.models import FileRecord, ProcessingLog
import os
from app.models import FileRecord, ProcessingLog
# Find the file record
file_record = db.query(FileRecord).filter(FileRecord.id == file_id).first()
+38 -30
View File
@@ -1,75 +1,83 @@
"""
General routes for the application homepage and basic pages.
"""
from fastapi import Request, HTTPException, Depends
from fastapi.responses import FileResponse, RedirectResponse
from pathlib import Path
from datetime import date
from pathlib import Path
from fastapi import Depends, HTTPException, Request
from fastapi.responses import FileResponse, RedirectResponse
from sqlalchemy.orm import Session
from app.views.base import APIRouter, templates, require_login, get_db, logger
from app.utils.config_validator import get_provider_status, validate_storage_configs
from app.views.base import APIRouter, get_db, logger, require_login, templates
router = APIRouter()
@router.get("/", include_in_schema=False)
async def serve_index(request: Request, db: Session = Depends(get_db)):
"""
Serve the index/home page.
If the system requires initial setup, redirect to the setup wizard.
"""
# Check if setup wizard is needed
from app.utils.setup_wizard import is_setup_required
from app.utils.settings_service import get_setting_from_db
from app.utils.setup_wizard import is_setup_required
# Check if setup was explicitly skipped
setup_skipped = get_setting_from_db(db, "_setup_wizard_skipped")
# Check setup completion query param
setup_complete = request.query_params.get("setup") == "complete"
if not setup_skipped and not setup_complete and is_setup_required():
logger.info("System requires initial setup, redirecting to wizard")
return RedirectResponse(url="/setup?step=1", status_code=303)
# Get provider information from config validator
providers = get_provider_status()
# Count configured providers
configured_providers = sum(1 for provider in providers.values() if provider['configured'])
configured_providers = sum(1 for provider in providers.values() if provider["configured"])
# Count different types of storage targets
storage_issues = validate_storage_configs()
configured_storage_targets = sum(1 for provider, issues in storage_issues.items()
if not issues and provider in ['dropbox', 'nextcloud', 'sftp',
's3', 'ftp', 'webdav',
'google_drive', 'onedrive'])
configured_storage_targets = sum(
1
for provider, issues in storage_issues.items()
if not issues
and provider in ["dropbox", "nextcloud", "sftp", "s3", "ftp", "webdav", "google_drive", "onedrive"]
)
# Query the actual file count from the database
processed_files = 0
try:
# Import the model here to avoid circular imports
from app.models import FileRecord
processed_files = db.query(FileRecord).count()
except Exception as e:
# Log error but continue (don't break the page if DB query fails)
logger.error(f"Error counting files: {str(e)}")
# Create stats object to pass to the template
stats = {
"processed_files": processed_files,
"active_integrations": configured_providers,
"storage_targets": configured_storage_targets
"storage_targets": configured_storage_targets,
}
return templates.TemplateResponse("index.html", {"request": request, "stats": stats})
@router.get("/about", include_in_schema=False)
async def serve_about(request: Request):
"""Serve the about page."""
return templates.TemplateResponse("about.html", {"request": request})
@router.get("/privacy", include_in_schema=False)
async def serve_privacy(request: Request):
"""Serve the privacy policy page."""
@@ -77,17 +85,20 @@ async def serve_privacy(request: Request):
current_date = date.today().strftime("%B %d, %Y")
return templates.TemplateResponse("privacy.html", {"request": request, "current_date": current_date})
@router.get("/imprint", include_in_schema=False)
async def serve_imprint(request: Request):
"""Serve the imprint/impressum page."""
return templates.TemplateResponse("imprint.html", {"request": request})
@router.get("/upload", include_in_schema=False)
@require_login
async def serve_upload(request: Request):
"""Serve the upload page."""
return templates.TemplateResponse("upload.html", {"request": request})
@router.get("/favicon.ico", include_in_schema=False)
def favicon():
"""Serve the favicon."""
@@ -97,6 +108,7 @@ def favicon():
raise HTTPException(status_code=404, detail="Favicon not found")
return FileResponse(favicon_path)
@router.get("/license", include_in_schema=False)
async def serve_license(request: Request):
"""Serve the license page."""
@@ -108,7 +120,7 @@ async def serve_license(request: Request):
]
license_text = None
# Try to read from any of the possible locations
for path in possible_locations:
try:
@@ -117,7 +129,7 @@ async def serve_license(request: Request):
break # File found and read, exit loop
except (FileNotFoundError, PermissionError):
continue # Try next location
# If license text is still None, use embedded text
if license_text is None:
license_text = """
@@ -130,13 +142,8 @@ The full license text could not be located on this system.
Please visit http://www.apache.org/licenses/LICENSE-2.0 for the complete license text.
"""
return templates.TemplateResponse(
"license.html",
{
"request": request,
"license_text": license_text
}
)
return templates.TemplateResponse("license.html", {"request": request, "license_text": license_text})
@router.get("/cookies", include_in_schema=False)
async def serve_cookies(request: Request):
@@ -144,6 +151,7 @@ async def serve_cookies(request: Request):
current_date = date.today().strftime("%B %d, %Y")
return templates.TemplateResponse("cookies.html", {"request": request, "current_date": current_date})
@router.get("/terms", include_in_schema=False)
async def serve_terms(request: Request):
"""Serve the terms of service page."""
+31 -42
View File
@@ -1,14 +1,17 @@
"""
Google Drive integration views for setup and OAuth callback.
"""
from fastapi import Request
from fastapi.responses import RedirectResponse
import urllib.parse
from app.views.base import APIRouter, templates, require_login, settings
from fastapi import Request
from fastapi.responses import RedirectResponse
from app.views.base import APIRouter, require_login, settings, templates
router = APIRouter()
@router.get("/google-drive-setup")
@require_login
async def google_drive_setup_page(request: Request):
@@ -17,24 +20,24 @@ async def google_drive_setup_page(request: Request):
Shows configuration status and setup instructions.
"""
# Check if using OAuth
use_oauth = getattr(settings, 'google_drive_use_oauth', False)
use_oauth = getattr(settings, "google_drive_use_oauth", False)
# Check Google Drive OAuth configuration
oauth_configured = bool(settings.google_drive_client_id and
settings.google_drive_client_secret and
settings.google_drive_refresh_token)
oauth_configured = bool(
settings.google_drive_client_id and settings.google_drive_client_secret and settings.google_drive_refresh_token
)
# Check Google Drive service account configuration
sa_configured = bool(settings.google_drive_credentials_json)
# Overall configuration status
is_configured = (use_oauth and oauth_configured) or (not use_oauth and sa_configured)
if settings.google_drive_folder_id:
is_configured = is_configured and True
else:
is_configured = False
# Get configuration values to display status (hide sensitive values)
return templates.TemplateResponse(
"google_drive.html",
@@ -51,10 +54,11 @@ async def google_drive_setup_page(request: Request):
"refresh_token": bool(settings.google_drive_refresh_token),
"refresh_token_value": settings.google_drive_refresh_token or "",
"folder_id": settings.google_drive_folder_id or "",
"has_credentials_json": bool(settings.google_drive_credentials_json)
}
"has_credentials_json": bool(settings.google_drive_credentials_json),
},
)
@router.get("/google-drive-callback")
@require_login
async def google_drive_callback(request: Request, code: str = None, error: str = None, state: str = None):
@@ -63,48 +67,33 @@ async def google_drive_callback(request: Request, code: str = None, error: str =
Now automatically exchanges the code for a token and saves it to the configuration.
"""
if error:
return templates.TemplateResponse(
"google_drive_callback_error.html",
{"request": request, "error": error}
)
return templates.TemplateResponse("google_drive_callback_error.html", {"request": request, "error": error})
if not code:
return templates.TemplateResponse(
"google_drive_callback_error.html",
{"request": request, "error": "No authorization code received from Google"}
{"request": request, "error": "No authorization code received from Google"},
)
# Display the processing page with automatic token exchange
return templates.TemplateResponse(
"google_drive_callback.html",
{
"request": request,
"code": code,
"state": state
}
)
return templates.TemplateResponse("google_drive_callback.html", {"request": request, "code": code, "state": state})
@router.get("/google-drive-auth-start")
@require_login
async def google_drive_auth_start(
request: Request,
client_id: str,
redirect_uri: str = None
):
async def google_drive_auth_start(request: Request, client_id: str, redirect_uri: str = None):
"""
Start the Google Drive OAuth flow by redirecting to Google's authorization page.
"""
if not redirect_uri:
redirect_uri = f"{request.url.scheme}://{request.url.netloc}/google-drive-callback"
# Create the authorization URL with required scopes
# Use only drive.file scope to minimize required permissions
scopes = [
"https://www.googleapis.com/auth/drive.file" # Access to files created or opened by the app
]
scope_str = urllib.parse.quote(' '.join(scopes))
scopes = ["https://www.googleapis.com/auth/drive.file"] # Access to files created or opened by the app
scope_str = urllib.parse.quote(" ".join(scopes))
auth_url = (
f"https://accounts.google.com/o/oauth2/auth"
f"?client_id={client_id}"
@@ -114,5 +103,5 @@ async def google_drive_auth_start(
f"&access_type=offline"
f"&prompt=consent" # Force to show consent screen to get refresh token
)
return RedirectResponse(url=auth_url)
+7 -5
View File
@@ -1,12 +1,13 @@
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import PlainTextResponse, HTMLResponse
from pathlib import Path
import os
from app.views.base import templates, require_login
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import HTMLResponse, PlainTextResponse
from app.views.base import templates
router = APIRouter()
@router.get("/licenses/lgpl.txt", response_class=PlainTextResponse)
async def get_lgpl_license():
"""
@@ -15,10 +16,11 @@ async def get_lgpl_license():
license_path = Path("frontend/static/licenses/lgpl.txt")
if not license_path.exists():
raise HTTPException(status_code=404, detail="License file not found")
with open(license_path, "r") as f:
return f.read()
@router.get("/attribution", response_class=HTMLResponse, include_in_schema=False)
async def serve_attribution(request: Request):
"""
+17 -17
View File
@@ -1,12 +1,14 @@
"""
OneDrive integration views for setup and OAuth callback.
"""
from fastapi import Request
from app.views.base import APIRouter, templates, require_login, settings
from app.views.base import APIRouter, require_login, settings, templates
router = APIRouter()
@router.get("/onedrive-setup")
@require_login
async def onedrive_setup_page(request: Request):
@@ -15,10 +17,10 @@ async def onedrive_setup_page(request: Request):
Shows configuration status and setup instructions.
"""
# Check OneDrive configuration
is_configured = bool(settings.onedrive_client_id and
settings.onedrive_client_secret and
settings.onedrive_refresh_token)
is_configured = bool(
settings.onedrive_client_id and settings.onedrive_client_secret and settings.onedrive_refresh_token
)
# Get configuration values to display status (hide sensitive values)
return templates.TemplateResponse(
"onedrive.html",
@@ -32,10 +34,11 @@ async def onedrive_setup_page(request: Request):
"tenant_id": settings.onedrive_tenant_id,
"refresh_token": bool(settings.onedrive_refresh_token),
"refresh_token_value": settings.onedrive_refresh_token if settings.onedrive_refresh_token else "",
"folder_path": settings.onedrive_folder_path or "Documents/Uploads" # Default folder path
}
"folder_path": settings.onedrive_folder_path or "Documents/Uploads", # Default folder path
},
)
@router.get("/onedrive-callback")
@require_login
async def onedrive_callback(request: Request, code: str = None, error: str = None):
@@ -44,25 +47,22 @@ async def onedrive_callback(request: Request, code: str = None, error: str = Non
Now automatically exchanges the code for a token and saves it to the configuration.
"""
if error:
return templates.TemplateResponse(
"onedrive_callback_error.html",
{"request": request, "error": error}
)
return templates.TemplateResponse("onedrive_callback_error.html", {"request": request, "error": error})
if not code:
return templates.TemplateResponse(
"onedrive_callback_error.html",
{"request": request, "error": "No authorization code received from Microsoft"}
{"request": request, "error": "No authorization code received from Microsoft"},
)
# Display the processing page with automatic token exchange
return templates.TemplateResponse(
"onedrive_callback.html",
{
"request": request,
"request": request,
"code": code,
"client_id_value": settings.onedrive_client_id or "",
"client_secret_value": settings.onedrive_client_secret or "",
"tenant_id": settings.onedrive_tenant_id or "common"
}
"tenant_id": settings.onedrive_tenant_id or "common",
},
)
+33 -35
View File
@@ -2,17 +2,18 @@
Settings management views for the application.
"""
import os
import logging
import inspect
import logging
import os
from functools import wraps
from fastapi import Request, Depends, HTTPException, status
from fastapi import Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from app.views.base import APIRouter, templates, require_login, settings, get_db
from app.utils.settings_service import get_settings_by_category, get_setting_metadata, SETTING_METADATA
from app.utils.config_validator.masking import mask_sensitive_value
from app.utils.settings_service import get_setting_metadata, get_settings_by_category
from app.views.base import APIRouter, get_db, require_login, settings, templates
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -21,23 +22,25 @@ router = APIRouter()
def require_admin_access(func):
"""
Decorator to require admin access for a route.
This decorator checks if the user in the session has admin privileges.
If not, redirects to the home page. Works with both sync and async functions,
though FastAPI route handlers should always be async.
"""
@wraps(func)
async def wrapper(request: Request, *args, **kwargs):
user = request.session.get("user")
if not user or not user.get("is_admin"):
logger.warning(f"Non-admin user attempted to access admin-only route")
logger.warning("Non-admin user attempted to access admin-only route")
return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
# FastAPI route handlers are async, but we support sync for flexibility
if inspect.iscoroutinefunction(func):
return await func(request, *args, **kwargs)
else:
return func(request, *args, **kwargs)
return wrapper
@@ -47,19 +50,20 @@ def require_admin_access(func):
async def settings_page(request: Request, db: Session = Depends(get_db)):
"""
Settings management page - admin only.
This page is a convenience feature to view and edit settings.
Values are displayed in precedence order: Database > Environment > Defaults
"""
try:
# Get settings from database
from app.utils.settings_service import get_all_settings_from_db
db_settings = get_all_settings_from_db(db)
# Get settings organized by category
categories = get_settings_by_category()
# Build settings data for display
settings_data = {}
for category, keys in categories.items():
@@ -67,7 +71,7 @@ async def settings_page(request: Request, db: Session = Depends(get_db)):
for key in keys:
# Get current value from settings (already has precedence applied)
value = getattr(settings, key, None)
# Determine the source of this setting
# Check if it's in the database
if key in db_settings:
@@ -84,35 +88,29 @@ async def settings_page(request: Request, db: Session = Depends(get_db)):
source = "default"
source_label = "DEFAULT"
source_color = "gray"
# Get metadata
metadata = get_setting_metadata(key)
# Mask sensitive values
display_value = value
if metadata.get("sensitive") and value:
display_value = mask_sensitive_value(value)
settings_data[category].append({
"key": key,
"display_value": display_value if display_value is not None else "",
"metadata": metadata,
"source": source,
"source_label": source_label,
"source_color": source_color
})
settings_data[category].append(
{
"key": key,
"display_value": display_value if display_value is not None else "",
"metadata": metadata,
"source": source,
"source_label": source_label,
"source_color": source_color,
}
)
return templates.TemplateResponse(
"settings.html",
{
"request": request,
"settings_data": settings_data,
"app_version": settings.version
}
"settings.html", {"request": request, "settings_data": settings_data, "app_version": settings.version}
)
except Exception as e:
logger.error(f"Error loading settings page: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to load settings page"
)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to load settings page")
+44 -42
View File
@@ -1,17 +1,19 @@
"""
Status and configuration views for the application.
"""
from fastapi import Request
from fastapi.responses import JSONResponse
from datetime import datetime
import os
import logging
from app.views.base import APIRouter, templates, require_login, settings
import logging
import os
from datetime import datetime
from fastapi import Request
from app.views.base import APIRouter, require_login, settings, templates
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/status")
@require_login
async def status_dashboard(request: Request):
@@ -19,75 +21,74 @@ async def status_dashboard(request: Request):
Status dashboard showing all configured integration targets
"""
from app.utils.config_validator import get_provider_status
# Get provider status
providers = get_provider_status()
# Get build date from settings
build_date = getattr(settings, 'build_date', 'Unknown')
build_date = getattr(settings, "build_date", "Unknown")
# Try to get container information
container_info = {}
try:
# Check for Docker environment
if os.path.exists('/.dockerenv'):
if os.path.exists("/.dockerenv"):
# We're inside a Docker container
container_info['is_docker'] = True
container_info["is_docker"] = True
# Try to get container ID
try:
with open('/proc/self/cgroup', 'r') as f:
with open("/proc/self/cgroup", "r") as f:
for line in f:
if 'docker' in line:
container_id = line.split('/')[-1].strip()
container_info['id'] = container_id[:12] # Short ID format
if "docker" in line:
container_id = line.split("/")[-1].strip()
container_info["id"] = container_id[:12] # Short ID format
break
except Exception:
container_info['id'] = 'Unknown'
container_info["id"] = "Unknown"
# Get Git commit SHA from settings
try:
git_sha = settings.git_sha
container_info['git_sha'] = git_sha[:7] if git_sha and git_sha != 'unknown' else 'Unknown'
container_info["git_sha"] = git_sha[:7] if git_sha and git_sha != "unknown" else "Unknown"
except Exception:
container_info['git_sha'] = 'Unknown'
container_info["git_sha"] = "Unknown"
# Try to get runtime information
try:
container_info['runtime_info'] = settings.runtime_info
container_info["runtime_info"] = settings.runtime_info
except Exception:
pass
else:
container_info['is_docker'] = False
container_info["is_docker"] = False
# If not in Docker, get Git info from settings
try:
git_sha = settings.git_sha
container_info['git_sha'] = git_sha[:7] if git_sha and git_sha != 'unknown' else 'Unknown'
container_info["git_sha"] = git_sha[:7] if git_sha and git_sha != "unknown" else "Unknown"
except Exception:
container_info['git_sha'] = 'Unknown'
container_info["git_sha"] = "Unknown"
except Exception:
container_info = {'is_docker': False, 'id': 'Unknown', 'git_sha': 'Unknown'}
container_info = {"is_docker": False, "id": "Unknown", "git_sha": "Unknown"}
# Get notification URLs for the notification box
notification_urls = getattr(settings, 'notification_urls', [])
notification_urls = getattr(settings, "notification_urls", [])
return templates.TemplateResponse(
"status_dashboard.html",
{
"request": request,
"request": request,
"providers": providers,
"app_version": settings.version,
"build_date": build_date,
"debug_enabled": getattr(settings, 'debug', False),
"debug_enabled": getattr(settings, "debug", False),
"last_check": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"container_info": container_info,
"settings": {
"notification_urls": notification_urls
}
}
"settings": {"notification_urls": notification_urls},
},
)
@router.get("/env")
@require_login
async def env_debug(request: Request):
@@ -97,17 +98,18 @@ async def env_debug(request: Request):
"""
# Use the actual debug setting from configuration
debug_enabled = settings.debug
# Get settings data
from app.utils.config_validator import get_settings_for_display
settings_data = get_settings_for_display(show_values=debug_enabled)
return templates.TemplateResponse(
"env_debug.html",
{
"request": request,
"request": request,
"settings": settings_data,
"debug_enabled": debug_enabled,
"app_version": settings.version
}
"app_version": settings.version,
},
)
+21 -30
View File
@@ -2,21 +2,16 @@
Setup wizard views for initial system configuration.
"""
import os
import logging
import secrets
from fastapi import Request, Depends, Form
from fastapi import Depends, Form, Request
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from app.views.base import APIRouter, templates, get_db
from app.utils.setup_wizard import (
is_setup_required,
get_required_settings,
get_wizard_steps,
get_missing_required_settings
)
from app.utils.settings_service import save_setting_to_db
from app.utils.setup_wizard import get_wizard_steps
from app.views.base import APIRouter, get_db, templates
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -26,26 +21,26 @@ router = APIRouter()
async def setup_wizard(request: Request, step: int = 1):
"""
Setup wizard for first-time configuration.
This wizard guides users through configuring essential settings
needed for the system to operate properly.
"""
# Get wizard steps
wizard_steps = get_wizard_steps()
max_step = max(wizard_steps.keys())
# Validate step number
if step < 1:
step = 1
elif step > max_step:
step = max_step
# Get settings for current step
current_settings = wizard_steps.get(step, [])
# Get step category (all settings in a step should have same category)
step_category = current_settings[0].get("wizard_category", "Configuration") if current_settings else "Configuration"
return templates.TemplateResponse(
"setup_wizard.html",
{
@@ -54,59 +49,55 @@ async def setup_wizard(request: Request, step: int = 1):
"max_step": max_step,
"settings": current_settings,
"step_category": step_category,
"progress_percent": int((step / max_step) * 100)
}
"progress_percent": int((step / max_step) * 100),
},
)
@router.post("/setup")
async def setup_wizard_save(
request: Request,
step: int = Form(...),
db: Session = Depends(get_db)
):
async def setup_wizard_save(request: Request, step: int = Form(...), db: Session = Depends(get_db)):
"""
Save settings from the current wizard step.
"""
try:
# Get form data
form_data = await request.form()
# Get settings for current step
wizard_steps = get_wizard_steps()
current_settings = wizard_steps.get(step, [])
# Save each setting from the form
saved_count = 0
for setting in current_settings:
key = setting["key"]
value = form_data.get(key)
# Skip empty values unless it's explicitly allowed
if value and value.strip():
# Auto-generate session_secret if needed
if key == "session_secret" and value == "auto-generate":
value = secrets.token_hex(32)
logger.info("Auto-generated session secret")
# Save to database
if save_setting_to_db(db, key, value):
saved_count += 1
logger.info(f"Setup wizard: Saved {key}")
logger.info(f"Setup wizard step {step}: Saved {saved_count} settings")
# Determine next step
max_step = max(wizard_steps.keys())
next_step = step + 1
if next_step > max_step:
# Setup complete, redirect to home
return RedirectResponse(url="/?setup=complete", status_code=303)
else:
# Go to next step
return RedirectResponse(url=f"/setup?step={next_step}", status_code=303)
except Exception as e:
logger.error(f"Error saving wizard settings: {e}")
return RedirectResponse(url=f"/setup?step={step}&error=save_failed", status_code=303)
@@ -116,7 +107,7 @@ async def setup_wizard_save(
async def setup_wizard_skip(request: Request):
"""
Skip the setup wizard (for advanced users).
Creates a marker to indicate setup was skipped.
"""
try: