diff --git a/Readme.md b/Readme.md index 68c0809..d3d1c86 100644 --- a/Readme.md +++ b/Readme.md @@ -40,6 +40,19 @@ This project is a comprehensive full‑stack web app for doing simple, local fil The backend is a Flask API and the frontend is a React app (Vite). +### Manual dependency installation + +If you run the backend outside Docker, make sure the native tools required by +the conversion pipeline are installed on the host: + +```bash +sudo apt-get update +sudo apt-get install poppler-utils tesseract-ocr ghostscript +``` + +You can also check whether the server image is missing anything by calling +`GET /health/dependencies`. + ### Project Rules These rules define how this project must be implemented and extended: diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 31997e2..409d360 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,7 +1,9 @@ -from flask import Flask, request +from flask import Flask, jsonify, request import os from flask_cors import CORS +from utils.dependency_checker import check_dependency_status + def create_app(): app = Flask(__name__) @@ -46,6 +48,10 @@ def home(): @app.route("/health", methods=["GET", "OPTIONS"]) def _health(): return {"status": "ok"}, 200 + + @app.route("/health/dependencies", methods=["GET", "OPTIONS"]) + def _dependency_health(): + return jsonify(check_dependency_status()), 200 app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024 diff --git a/backend/utils/dependency_checker.py b/backend/utils/dependency_checker.py new file mode 100644 index 0000000..ac423e4 --- /dev/null +++ b/backend/utils/dependency_checker.py @@ -0,0 +1,46 @@ +from shutil import which + + +def _tool_status(name, binary_names, required_for, install_command, optional=False): + installed = any(which(binary) for binary in binary_names) + return { + "installed": installed, + "required_for": required_for, + "install_command": install_command, + "optional": optional, + "binary_names": binary_names, + } + + +def check_dependency_status(): + dependencies = { + "poppler-utils": _tool_status( + "poppler-utils", + ["pdfinfo", "pdftoppm"], + ["PDF to PNG", "PDF info", "PDF merge validation"], + "apt-get install poppler-utils", + ), + "tesseract-ocr": _tool_status( + "tesseract-ocr", + ["tesseract"], + ["Image OCR"], + "apt-get install tesseract-ocr", + ), + "ghostscript": _tool_status( + "ghostscript", + ["gs"], + ["PDF compression", "PDF post-processing"], + "apt-get install ghostscript", + optional=True, + ), + } + + required_missing = [ + name for name, data in dependencies.items() if not data["optional"] and not data["installed"] + ] + + return { + "status": "degraded" if required_missing else "ok", + "dependencies": dependencies, + "missing_required_dependencies": required_missing, + } diff --git a/frontend/src/components/DependencyWarning.jsx b/frontend/src/components/DependencyWarning.jsx new file mode 100644 index 0000000..dfc6306 --- /dev/null +++ b/frontend/src/components/DependencyWarning.jsx @@ -0,0 +1,38 @@ +import React from "react"; +import { AlertTriangle, ExternalLink } from "lucide-react"; + +const DependencyWarning = ({ dependencies = [], status = "ok" }) => { + if (status !== "degraded" || dependencies.length === 0) { + return null; + } + + const labels = dependencies.map((item) => item.name).join(", "); + + return ( +
+
+ +
+

+ Some server dependencies are missing or unavailable. +

+

+ {labels} may be unavailable until the server image includes the + required packages. +

+ + Learn how to fix this + + +
+
+
+ ); +}; + +export default DependencyWarning; diff --git a/frontend/src/components/Layout/Layout.jsx b/frontend/src/components/Layout/Layout.jsx index 8510bfd..1941e5a 100644 --- a/frontend/src/components/Layout/Layout.jsx +++ b/frontend/src/components/Layout/Layout.jsx @@ -2,12 +2,17 @@ import React, { useState, useEffect } from "react"; import { useLocation, Outlet, useNavigate } from "react-router-dom"; import Sidebar from "../Sidebar/Sidebar"; import { Menu, Sun, Moon, Home } from "lucide-react"; +import DependencyWarning from "../DependencyWarning"; const Layout = () => { const isDark = false; const toggleTheme = () => {}; const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); + const [dependencyState, setDependencyState] = useState({ + status: "ok", + dependencies: [], + }); const navigate = useNavigate(); const location = useLocation(); @@ -22,6 +27,42 @@ const Layout = () => { return () => window.removeEventListener("resize", handleResize); }, []); + useEffect(() => { + let cancelled = false; + + const loadDependencyStatus = async () => { + try { + const baseUrl = import.meta.env.VITE_API_URL || "http://localhost:5000"; + const response = await fetch(`${baseUrl}/health/dependencies`); + if (!response.ok) return; + + const payload = await response.json(); + if (cancelled) return; + + const missingDependencies = Object.entries(payload.dependencies || {}) + .filter(([, meta]) => !meta.installed) + .map(([name, meta]) => ({ + name, + requiredFor: meta.required_for || [], + installCommand: meta.install_command || "", + })); + + setDependencyState({ + status: payload.status || "ok", + dependencies: missingDependencies, + }); + } catch (error) { + console.warn("Unable to load dependency health:", error); + } + }; + + loadDependencyStatus(); + + return () => { + cancelled = true; + }; + }, []); + const toggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen); const closeMobileMenu = () => setIsMobileMenuOpen(false); const isLandingPage = location.pathname === "/"; @@ -103,12 +144,18 @@ const Layout = () => { )} {/* Adjusted padding top for mobile so content isn't hidden under sticky header */} -
- +
+ +
+ +
); }; -export default Layout; \ No newline at end of file +export default Layout;