Compare commits

..
36 Commits
Author SHA1 Message Date
nmemmert cc6c8e8455 Update README with Ubuntu installer instructions and nginx large file upload configuration 2025-11-06 08:49:34 -05:00
nmemmert 6380577791 Fix static file permission handling - add user to www-data group instead of vice versa 2025-11-06 08:31:43 -05:00
nmemmert 3703af0d66 Improve static file permissions handling in installer
- Add fallback for chown failure by using group permissions
- Add www-data to user's group and set appropriate group permissions
- This handles cases where chown is not permitted (e.g., certain filesystems)
2025-11-06 08:29:16 -05:00
nmemmert 3f1b5d1cb8 Fix static file permissions for nginx access
- Set ownership of static/ directory to www-data:www-data
- This resolves 'Permission denied' errors when nginx tries to serve static files like the logo
2025-11-06 08:27:30 -05:00
nmemmert 4dc2b1b8c3 Fix nginx configuration in Ubuntu installer for large file uploads
- Add client_max_body_size 100G to both HTTP and HTTPS nginx configs
- Increase client_body_timeout and client_header_timeout for large uploads
- Update proxy timeouts to 300s for better handling of large files
- Disable proxy_buffering and proxy_request_buffering for large uploads
- This resolves 413 Request Entity Too Large errors from nginx
2025-11-06 08:18:46 -05:00
nmemmert f4611c5aa4 Fix client-side issues: logo loading and JS errors
- Replace base64 logo with static file reference to avoid ERR_INVALID_URL
- Add missing status element to index.html to fix TypeError in XMLHttpRequest
- Reduce MAX_CONTENT_LENGTH from 1000TB to 100GB for practicality
- Update all templates to use logo_url instead of logo_b64
- Update Waitress max_request_body_size to match Flask limit
2025-11-06 08:15:49 -05:00
nmemmert c9e817547a Convert print statements to proper logging for systemd capture
- Convert all print statements in process_pdfs_async to logger.info/logger.error
- Update startup diagnostics to use logger.info instead of print
- Ensure all diagnostic information is captured by systemd logs on remote server
- Add detailed logging for PDF processing steps, file scanning, and error conditions
2025-11-06 07:58:08 -05:00
nmemmert a6b751053a Add comprehensive diagnostics and fix processing issues
- Add startup diagnostics showing directory permissions and PDF support
- Enhanced process_pdfs_async with detailed logging at every step
- Check if PyMuPDF is available and fail gracefully if not
- Scan and log all PDF files found in upload directory
- Improved error handling during file processing
- Sequential processing with progress updates every 10 files
- Clear error messages when no PDFs found or processing fails
- This will show exactly where processing stops on remote server
2025-11-06 07:45:23 -05:00
nmemmert 3215cd9032 Fix all paths to be absolute based on __file__ to work regardless of working directory
- Make RESULTS_DIR, SESSION_FILE_DIR, uploads absolute
- Add error handling around file saving with detailed logging
- Add /test route to check write permissions
- Ensure app works on remote with correct paths and permissions
2025-11-05 16:03:46 -05:00
nmemmert 95a74e9035 Temporarily disable @login_required on index route for debugging
- Remove login_required decorator to test if upload works
- Add debug print to confirm auth is bypassed
- If files appear in uploads after this, the issue is authentication
2025-11-05 16:00:28 -05:00
nmemmert 17f63125b1 Save uploaded files to uploads/job_id folder instead of temp dirs
- Create job_id first, then save files to uploads/job_id/
- Add debug print when index() is called
- Files will now be visible in uploads folder for debugging
2025-11-05 15:58:23 -05:00
nmemmert 163414e8e4 Add debug prints to track job submission and processing start
- Print when POST request is received
- Print when process_pdfs_async is called
- Help diagnose if threads are starting on remote server
2025-11-05 15:57:25 -05:00
nmemmert 111b1c9a6d Fix job sorting and ensure absolute paths for remote deployment
- Change JOBS_FILE and RESULTS_DIR to absolute paths using os.path.abspath
- Fix job sorting in /jobs route to use datetime objects instead of strings
- This should resolve issues with jobs not appearing or processing on remote server
2025-11-05 15:53:54 -05:00
nmemmert 27a6b637c5 Add more debugging for job loading and timestamp handling
- Print if jobs.json exists and file path
- Show timestamp conversion details for each job
- Print sorted jobs count and first job status
- Help diagnose why new jobs aren't appearing
2025-11-05 15:48:24 -05:00
nmemmert c60d6673db Add more debugging prints for job creation, saving, loading, and processing
- Add try-except in save_jobs to catch errors
- Print job IDs when loading jobs in /jobs route
- Print timestamp when job is created
- Print start/end times for processing
- Print successful save confirmation
2025-11-05 15:45:18 -05:00
nmemmert 92919585a2 Remove old session file 2025-11-05 15:42:43 -05:00
nmemmert 99422217bf Add more debugging to job loading and fix sorting
- Add prints in load_jobs and save_jobs to track job persistence
- Change job sorting to use string timestamps to avoid type comparison errors
2025-11-05 15:41:22 -05:00
nmemmert 0d2384ca55 Add debugging prints and switch to sequential processing
- Add print statements to track job creation, processing start, completion, and failures
- Use absolute path for jobs.json to avoid working directory issues
- Temporarily switch to sequential PDF processing to avoid multiprocessing issues on remote server
2025-11-05 15:39:56 -05:00
nmemmert 1d89e1ca36 Fix logo display and job processing issues
- Embed logo as base64 data URL in templates to avoid static file issues
- Fix Flask route endpoint for /jobs (change function name to 'jobs')
- Change upload and session directories to relative paths for better portability
- Ensure directories are created at app startup
2025-11-05 15:24:35 -05:00
nmemmert 9834c081a9 fix 2025-11-05 15:18:05 -05:00
nmemmert 25e4ccad07 Fix PDF support and template URL issues
- Add PDF_SUPPORT checks before using PyMuPDF functions
- Handle missing PyMuPDF gracefully in PDF processing functions
- Fix url_for('jobs') to url_for('jobs_page') in templates
- Prevent AttributeError when fitz is None
2025-11-05 15:06:57 -05:00
nmemmert 12e46e3993 Remove unused section navigation links (Section 1, 2, 3) from main page 2025-11-05 14:58:38 -05:00
nmemmert faa231f9dd Fix Ubuntu install script - make PyMuPDF optional, fix .env check, improve systemd service 2025-11-05 14:53:12 -05:00
nmemmert 54cd87c0f4 Fix template syntax errors - use dictionary access job['key'] instead of job.key 2025-11-05 14:50:53 -05:00
nmemmert 576ea9edec Fix OAuth callback redirect issue - use proper redirect() instead of set_data() 2025-11-05 14:41:41 -05:00
nmemmert 9cf075c384 Fix logo not displaying by setting proper permissions on static files for nginx 2025-11-05 14:39:03 -05:00
nmemmert cad91746ad Update Ubuntu install script default domain to eoypdf.trbc.org 2025-11-05 14:24:46 -05:00
nmemmert 6654ae30c5 Fix authentication callback issues: add cache control headers, prevent double callbacks, improve error handling, and ensure server-side sessions work properly 2025-11-05 13:55:30 -05:00
nmemmert d92f557b4b Fix git clone error by checking if repository already exists and using git pull instead 2025-11-05 13:28:49 -05:00
nmemmert 9ec67b0a45 Ensure web-app branch is properly checked out after cloning repository 2025-11-05 13:26:50 -05:00
nmemmert aa0a86d9be Fix session cookie size issue by implementing server-side sessions with Flask-Session 2025-11-05 13:23:39 -05:00
nmemmert 1ab08805fc Update Ubuntu install script to use correct GitLab repository URL and web-app branch 2025-11-05 12:59:47 -05:00
nmemmert cc14c288d3 Add self-signed SSL certificate generation option to Ubuntu install script 2025-11-05 11:59:32 -05:00
nmemmert cc4745d109 Add nginx reverse proxy and DNS domain configuration to Ubuntu install script 2025-11-05 11:55:13 -05:00
nmemmert 5d6a19e7ca Add interactive Azure AD configuration to Ubuntu install script 2025-11-05 11:44:31 -05:00
nmemmert 144adf64c4 Complete PDF Sort and Merge Flask Application
- Full-featured web application for PDF processing
- Microsoft Entra (Azure AD) authentication with PKCE
- Parallel PDF sorting and merging capabilities
- Job management system with real-time progress tracking
- Modern web UI with responsive design
- Comprehensive error handling and logging
- Ubuntu installation script for easy deployment
- Environment configuration for secure credential management
2025-11-05 11:41:48 -05:00
15 changed files with 3111 additions and 7 deletions

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
# Microsoft Entra (Azure AD) Configuration
# Get these values from your Azure AD app registration in the Azure portal
# Azure AD Tenant ID (Directory ID)
AZURE_TENANT_ID=98367e69-bd4c-47c6-90ef-683461397b64
# Azure AD Client ID (Application ID)
AZURE_CLIENT_ID=5b2137d0-b143-4f1a-85b0-d740d62faebc
# Azure AD Client Secret
AZURE_CLIENT_SECRET=Ql88Q~1wo-_L8vg-csgL4GFhLR_A8klPvphHUcyx
# Azure AD Authority URL (usually https://login.microsoftonline.com/{tenant_id})
AZURE_AUTHORITY=https://login.microsoftonline.com/98367e69-bd4c-47c6-90ef-683461397b64
# Redirect URI (must match what's configured in Azure AD app registration)
# For local development, use: http://localhost:8080/auth/callback
AZURE_REDIRECT_URI=http://localhost:5000/auth/callback
# Scopes to request (openid, profile, email are common)
AZURE_SCOPES=openid profile email
# Flask Configuration
SECRET_KEY=your-secret-key-here-change-this-in-production
+23
View File
@@ -0,0 +1,23 @@
# Microsoft Entra (Azure AD) Configuration
# Copy this file to .env and fill in your actual values
# Azure AD Tenant ID (Directory ID)
AZURE_TENANT_ID=
# Azure AD Client ID (Application ID)
AZURE_CLIENT_ID=
# Azure AD Client Secret
AZURE_CLIENT_SECRET=
# Azure AD Authority URL (usually https://login.microsoftonline.com/{tenant_id})
AZURE_AUTHORITY=
# Redirect URI (must match what's configured in Azure AD app registration)
AZURE_REDIRECT_URI=http://localhost:5000/auth/callback
# Scopes to request (openid, profile, email are common)
AZURE_SCOPES=openid profile email
# Flask Configuration
SECRET_KEY=your-secret-key-here-change-this-in-production
+41
View File
@@ -0,0 +1,41 @@
# Environment variables
.env
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git
.mypy_cache
.pytest_cache
.hypothesis
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Application specific
results/
status.txt
jobs.json
+93
View File
@@ -0,0 +1,93 @@
# Microsoft Entra Authentication Setup Guide
This application uses Microsoft Entra (Azure Active Directory) for authentication. Follow these steps to configure it:
## 1. Create an Azure AD App Registration
1. Go to the [Azure Portal](https://portal.azure.com)
2. Navigate to **Azure Active Directory** > **App registrations**
3. Click **New registration**
4. Enter a name for your app (e.g., "PDF Processor")
5. For **Supported account types**, select "Accounts in this organizational directory only"
6. For **Redirect URI**, select "Web" and enter: `http://localhost:5000/auth/callback`
7. Click **Register**
## 2. Configure Authentication
1. In your app registration, go to **Authentication**
2. Under **Redirect URIs**, ensure `http://localhost:5000/auth/callback` is listed
3. Under **Implicit grant and hybrid flows**, check:
- [x] Access tokens
- [x] ID tokens
4. Click **Save**
## 3. Get Application Credentials
1. Go to **Overview** in your app registration
2. Copy the **Application (client) ID** - this is your `AZURE_CLIENT_ID`
3. Copy the **Directory (tenant) ID** - this is your `AZURE_TENANT_ID`
## 4. Create a Client Secret
1. Go to **Certificates & secrets**
2. Click **New client secret**
3. Enter a description and select an expiration
4. Click **Add**
5. **Important**: Copy the secret value immediately - this is your `AZURE_CLIENT_SECRET`
## 5. Configure API Permissions
1. Go to **API permissions**
2. Click **Add a permission**
3. Select **Microsoft Graph**
4. Select **Delegated permissions**
5. Add these permissions:
- `openid`
- `profile`
- `email`
6. Click **Add permissions**
7. Click **Grant admin consent** for your organization
## 6. Configure Environment Variables
Create a `.env` file in the project root with your Azure AD settings:
```env
# Azure AD Configuration
AZURE_TENANT_ID=your-tenant-id-here
AZURE_CLIENT_ID=your-client-id-here
AZURE_CLIENT_SECRET=your-client-secret-here
AZURE_AUTHORITY=https://login.microsoftonline.com/your-tenant-id-here
AZURE_REDIRECT_URI=http://localhost:5000/auth/callback
AZURE_SCOPES=openid profile email
```
## 7. Update Redirect URI for Production
When deploying to production, update the redirect URI in both:
- Azure AD app registration
- Your `.env` file (change localhost to your production domain)
**Note**: For local development, the app runs on port 5000, so use `http://localhost:5000/auth/callback` as the redirect URI.
## 8. Test the Authentication
1. Start the application: `python app.py`
2. Visit `http://localhost:5000`
3. Click "Login with Microsoft"
4. You should be redirected to Microsoft login
5. After authentication, you'll be redirected back to the app
## Troubleshooting
- **"invalid_client" error**: Check your client ID and secret
- **"invalid_request" error**: Verify the redirect URI matches exactly
- **Permission denied**: Ensure you've granted admin consent for the API permissions
- **Token errors**: Check that your tenant ID is correct
## Security Notes
- Never commit the `.env` file to version control
- Use strong client secrets and rotate them regularly
- In production, use HTTPS for all authentication flows
- Consider implementing token refresh logic for long-running sessions
+276 -7
View File
@@ -1,11 +1,280 @@
# EOY PDF Sort & Merge
# End of Year Statements PDF Processor
## Prerequisite
1. Linux for Windows Subsystem (WSL2)
2. Ubuntu installed from Windows Store
A web application for sorting and merging PDF statements by address and page count, optimized for processing large batches of documents.
To start make sure all files are downloaded and in the same directory. Then run the application.
## Features
./Run.sh
- Upload individual PDFs, ZIP archives, or entire folders
- Automatic address extraction and sorting
- Parallel processing for maximum speed
- Real-time progress tracking
- Web-based interface with progress bars and status updates
- Asynchronous processing to prevent timeouts
- **Microsoft Entra (Azure AD) authentication for secure access**
Project to sort, filter, and merge pdf files.
## Authentication
This application requires Microsoft Entra (Azure Active Directory) authentication. Before running the application, you must:
1. Set up an Azure AD app registration
2. Configure authentication settings in `.env`
3. Grant necessary API permissions
See [AZURE_AD_SETUP.md](AZURE_AD_SETUP.md) for detailed setup instructions.
## Server Recommendations
### VMware Virtual Machine Specifications
For optimal performance with large PDF batches (1000+ files), we recommend the following VM configuration:
#### Minimum Requirements
- **vCPUs**: 4 cores
- **RAM**: 8 GB
- **Storage**: 50 GB SSD
- **OS**: Ubuntu Server 22.04 LTS (or CentOS 8+)
#### Recommended for Production
- **vCPUs**: 8-16 cores (more cores = faster processing)
- **RAM**: 16-32 GB (PDFs load into memory)
- **Storage**: 100 GB NVMe SSD
- **Network**: 1 Gbps connection
#### High-Performance Setup (for 5000+ PDFs)
- **vCPUs**: 16-32 cores
- **RAM**: 64 GB
- **Storage**: 200 GB NVMe SSD
- **OS**: Ubuntu Server 22.04 LTS
### VMware Configuration Tips
1. Enable CPU hot-add and memory hot-plug for future upgrades
2. Use VMXNET3 network adapter for better performance
3. Allocate storage from a high-performance datastore
4. Enable VMware Tools for optimal integration
## Installation Instructions
### Option 1: Automated Ubuntu Installation (Recommended)
For Ubuntu servers, use the automated installation script that handles all dependencies, nginx configuration, and permissions:
```bash
# Download and run the installer
wget https://gitlab.com/trbc-dev/eoy-pdf-sortandmerge/-/raw/web-app/ubuntu-install.sh
chmod +x ubuntu-install.sh
./ubuntu-install.sh
```
The installer will:
- Install Python, nginx, and all dependencies
- Configure nginx with large file upload support (up to 100GB)
- Set up systemd service for automatic startup
- Configure Azure AD authentication
- Set proper file permissions for static assets
- Generate SSL certificates (optional)
**Important**: The installer supports uploads up to 100GB by default. If you need different limits, edit the nginx configuration after installation.
### Option 2: Manual Installation
#### 1. VM Setup
1. Create a new VM in VMware with the specifications above
2. Install Ubuntu Server 22.04 LTS
3. Update the system:
```bash
sudo apt update && sudo apt upgrade -y
```
#### 2. Install Python and Dependencies
```bash
# Install Python 3.10+
sudo apt install python3 python3-pip python3-venv -y
# Create application directory
sudo mkdir -p /opt/pdf-processor
sudo chown $USER:$USER /opt/pdf-processor
cd /opt/pdf-processor
# Clone or copy the application files
# (Assuming you have the files ready)
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
```
#### 3. Install Production Server (Gunicorn)
```bash
pip install gunicorn
# Create systemd service
sudo nano /etc/systemd/system/pdf-processor.service
```
Add the following content:
```ini
[Unit]
Description=PDF Processor Web App
After=network.target
[Service]
User=your-username
Group=your-group
WorkingDirectory=/opt/pdf-processor
Environment="PATH=/opt/pdf-processor/venv/bin"
ExecStart=/opt/pdf-processor/venv/bin/gunicorn -w 4 -b 0.0.0.0:8000 app:app
Restart=always
[Install]
WantedBy=multi-user.target
```
```bash
# Enable and start service
sudo systemctl enable pdf-processor
sudo systemctl start pdf-processor
# Check status
sudo systemctl status pdf-processor
```
### 4. Configure Nginx (Required for Production)
**Important**: Nginx must be configured to handle large file uploads. The default nginx configuration limits uploads to 1MB, which will cause "413 Request Entity Too Large" errors.
```bash
sudo apt install nginx -y
# Create nginx config with large file upload support
sudo nano /etc/nginx/sites-available/pdf-processor
```
Add the following configuration (adjust `client_max_body_size` based on your needs):
```nginx
server {
listen 80;
server_name your-server-ip;
# Large file upload settings - CRITICAL for PDF processing
client_max_body_size 100G; # Allow uploads up to 100GB
client_body_timeout 300s; # 5 minute timeout for uploads
client_header_timeout 60s; # Header timeout
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeout settings for long-running PDF processing
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# Disable buffering for large uploads
proxy_buffering off;
proxy_request_buffering off;
}
# Static files
location /static/ {
alias /path/to/your/app/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
```
```bash
# Enable the site
sudo ln -s /etc/nginx/sites-available/pdf-processor /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
# Test configuration
sudo nginx -t
# Restart nginx
sudo systemctl restart nginx
```
### 5. Access the Application
- **With automated installer**: The installer will provide the exact URL and setup next steps
- **Manual installation**: Open your web browser and navigate to `http://your-vm-ip` (nginx on port 80)
- **Direct access**: `http://your-vm-ip:5000` (Flask development server)
## Troubleshooting
### Common Issues
1. **413 Request Entity Too Large**:
- **Cause**: Nginx upload limit is too small (default is 1MB)
- **Solution**: Ensure `client_max_body_size` is set to at least 100G in nginx config
- **Check**: `sudo nginx -t && sudo systemctl restart nginx`
2. **Permission Denied on Static Files**:
- **Cause**: Nginx can't access logo/images in static directory
- **Solution**: Run the automated installer or manually set permissions:
```bash
sudo usermod -a -G www-data $USER
sudo chmod -R 755 ~/pdf-sort-merge/static/
find ~/pdf-sort-merge/static/ -type f -exec sudo chmod 644 {} \;
sudo systemctl restart nginx
```
3. **Processing Stuck**:
- **Check logs**: `sudo journalctl -u pdf-sort-merge -f` (automated install)
- **Check logs**: `sudo journalctl -u pdf-processor -f` (manual install)
4. **Memory Issues**: Monitor with `htop` and increase RAM if needed
5. **Slow Processing**: Ensure all CPU cores are being utilized
### Installation Issues
- **Automated installer fails**: Check that you're running as a regular user (not root) with sudo privileges
- **Azure AD setup incomplete**: Follow [AZURE_AD_SETUP.md](AZURE_AD_SETUP.md) carefully
- **PyMuPDF installation fails**: The app will still work but PDF processing will be disabled
### Monitoring and Management
```bash
# Check CPU usage during processing
htop
# Monitor application logs (automated install)
sudo journalctl -u pdf-sort-merge -f
# Monitor application logs (manual install)
sudo journalctl -u pdf-processor -f
# Check disk I/O
iotop
# Service management (automated install)
~/pdf-sort-merge/manage.sh status
~/pdf-sort-merge/manage.sh logs
~/pdf-sort-merge/manage.sh restart
~/pdf-sort-merge/manage.sh update # Pull latest code and restart
```
## Security Considerations
- Run behind a reverse proxy (Nginx) in production
- Use HTTPS with SSL certificates
- Restrict access with firewall rules
- Regularly update dependencies
- Monitor for resource usage
## Support
For issues or questions, check the application logs and ensure your VM meets the minimum requirements.
+1132
View File
File diff suppressed because it is too large. Load diff
+30
View File
@@ -0,0 +1,30 @@
{
"84a67896-ea93-4355-9020-ca26bad91316": {
"id": "84a67896-ea93-4355-9020-ca26bad91316",
"timestamp": "2025-11-05 09:10:23.730974",
"file_count": 5559,
"zip_path": "results\\job_84a67896-ea93-4355-9020-ca26bad91316.zip",
"zip_filename": "job_84a67896-ea93-4355-9020-ca26bad91316.zip",
"status": "completed"
},
"20251105_093008": {
"id": "20251105_093008",
"timestamp": "2025-11-05 09:30:08.978343",
"status": "completed",
"message": "Processing complete! ZIP created: 20251105_093008.zip",
"file_count": 5559,
"progress": 100,
"zip_path": "results\\20251105_093008.zip",
"zip_filename": "20251105_093008.zip"
},
"20251105_095440": {
"id": "20251105_095440",
"timestamp": "2025-11-05 09:54:40.248747",
"status": "completed",
"message": "Processing complete! ZIP created: 20251105_095440.zip",
"file_count": 5559,
"progress": 100,
"zip_path": "results\\20251105_095440.zip",
"zip_filename": "20251105_095440.zip"
}
}
+7
View File
@@ -0,0 +1,7 @@
Flask==3.0.0
Flask-Session==0.6.0
Werkzeug==3.0.1
waitress==3.0.0
requests-oauthlib==2.0.0
python-dotenv==1.2.1
PyMuPDF==1.23.7 # Optional - will be installed separately if possible
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

+307
View File
@@ -0,0 +1,307 @@
<!DOCTYPE html>
<html>
<head>
<title>End of Year Statements</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
max-width: 200px;
height: auto;
}
.upload-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.radio-group {
margin-bottom: 15px;
}
.radio-group label {
margin-right: 20px;
}
input[type="file"] {
margin: 10px 0;
}
button {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.progress-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
display: none;
}
progress {
width: 100%;
height: 20px;
}
.result-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
display: none;
}
.error {
color: red;
}
#logArea {
margin-top: 20px;
padding: 10px;
background: #f9f9f9;
border: 1px solid #ddd;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
white-space: pre-line;
}
.clear-btn {
background-color: #6c757d;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-left: 10px;
}
.clear-btn:hover {
background-color: #5a6268;
}
.clear-progress {
margin-top: 10px;
display: none;
}
.clear-progress progress {
width: 100%;
height: 20px;
}
.nav-links {
text-align: center;
margin-bottom: 20px;
}
.nav-links a {
color: #007bff;
text-decoration: none;
margin: 0 15px;
font-weight: bold;
}
.nav-links a:hover {
text-decoration: underline;
}
.auth-section {
margin-top: 20px;
text-align: center;
}
.user-info {
display: inline-block;
background: white;
padding: 10px 20px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.user-info span {
margin-right: 15px;
font-weight: bold;
}
.logout-btn, .login-btn {
background-color: #dc3545;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
display: inline-block;
}
.login-btn {
background-color: #28a745;
}
.logout-btn:hover, .login-btn:hover {
opacity: 0.8;
}
</style>
</head>
<body>
<div class="header">
<img src="{{ logo_url }}" alt="Logo" class="logo">
<h1>End of Year Statements Processor</h1>
<p>Upload PDF files or folders to automatically sort by address and merge by page count</p>
<!-- Authentication section -->
<div class="auth-section">
{% if session.oauth_token %}
<div class="user-info">
<span>Welcome, {{ session.user_info.displayName or session.user_info.givenName or 'User' }}!</span>
<a href="{{ url_for('logout') }}" class="logout-btn">Logout</a>
</div>
{% else %}
<div class="login-prompt">
<a href="{{ url_for('login') }}" class="login-btn">Login with Microsoft</a>
</div>
{% endif %}
</div>
</div>
<div class="nav-links">
<a href="{{ url_for('results') }}">View All Jobs & Downloads</a>
</div>
<div class="upload-section">
<div class="radio-group">
<label>
<input type="radio" name="uploadType" value="files" checked> Upload Files (PDFs or ZIPs)
</label>
<label>
<input type="radio" name="uploadType" value="folder"> Upload Folder
</label>
</div>
<form id="uploadForm" enctype="multipart/form-data">
<div id="filesInput">
<input type="file" name="files" multiple accept=".pdf,.zip" id="fileInput">
</div>
<div id="folderInput" style="display:none;">
<input type="file" webkitdirectory multiple id="folderFileInput">
</div>
<button type="submit" id="submitBtn">Process Files</button>
<button type="button" id="clearBtn" class="clear-btn">Clear/Reset</button>
</form>
<div id="status" style="margin-top: 10px; color: blue;"></div>
<div class="clear-progress" id="clearProgressContainer">
<p>Clearing temporary files...</p>
<progress id="clearProgress" value="0" max="100"></progress>
</div>
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="error">
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<script>
document.querySelectorAll('input[name="uploadType"]').forEach(radio => {
radio.addEventListener('change', function() {
document.getElementById('filesInput').style.display = this.value === 'files' ? 'block' : 'none';
document.getElementById('folderInput').style.display = this.value === 'folder' ? 'block' : 'none';
});
});
document.getElementById('uploadForm').addEventListener('submit', function(e) {
e.preventDefault();
alert("Starting upload and processing... This may take several minutes for large files.");
const formData = new FormData();
const uploadType = document.querySelector('input[name="uploadType"]:checked').value;
const inputElement = uploadType === 'files' ? document.getElementById('fileInput') : document.getElementById('folderFileInput');
for (let file of inputElement.files) {
formData.append('files', file);
}
const xhr = new XMLHttpRequest();
document.getElementById('submitBtn').disabled = true;
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
document.getElementById('status').textContent = `Uploading... ${Math.round(percentComplete)}%`;
}
});
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
// Redirect to job status page
window.location.href = xhr.responseURL || '/';
} else {
document.getElementById('status').textContent = 'Error: ' + xhr.status + ' ' + xhr.statusText;
document.getElementById('submitBtn').disabled = false;
}
});
xhr.addEventListener('error', function() {
document.getElementById('status').textContent = 'Upload failed';
document.getElementById('submitBtn').disabled = false;
});
xhr.open('POST', '/');
xhr.send(formData);
});
</script>
<script>
// Clear/Reset functionality
document.getElementById('clearBtn').addEventListener('click', function() {
if (confirm('This will clear all temporary files and reset the processing state. Continue?')) {
const clearBtn = document.getElementById('clearBtn');
const clearProgressContainer = document.getElementById('clearProgressContainer');
const clearProgress = document.getElementById('clearProgress');
// Disable button and show progress
clearBtn.disabled = true;
clearProgressContainer.style.display = 'block';
clearProgress.value = 0;
// Animate progress bar
let progress = 0;
const progressInterval = setInterval(() => {
progress += Math.random() * 15; // Random increment for visual effect
if (progress > 90) progress = 90; // Cap at 90% until actual completion
clearProgress.value = progress;
}, 100);
fetch('/clear', { method: 'POST' })
.then(response => response.json())
.then(data => {
clearInterval(progressInterval);
clearProgress.value = 100;
// Hide progress after a short delay
setTimeout(() => {
clearProgressContainer.style.display = 'none';
clearBtn.disabled = false;
alert(data.message);
location.reload(); // Refresh the page
}, 500);
})
.catch(err => {
clearInterval(progressInterval);
clearProgressContainer.style.display = 'none';
clearBtn.disabled = false;
alert('Error clearing files: ' + err);
});
}
});
</script>
</body>
</html>
+227
View File
@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html>
<head>
<title>Job Status - End of Year Statements</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
max-width: 200px;
height: auto;
}
.job-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.status-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
progress {
width: 100%;
height: 20px;
}
.status-processing {
color: #007bff;
}
.status-completed {
color: #28a745;
}
.status-failed {
color: #dc3545;
}
.nav-links {
text-align: center;
margin-bottom: 20px;
}
.nav-links a {
color: #007bff;
text-decoration: none;
margin: 0 15px;
font-weight: bold;
}
.nav-links a:hover {
text-decoration: underline;
}
button {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.download-btn {
background-color: #28a745;
}
.back-btn {
background-color: #6c757d;
}
.error {
color: red;
margin-top: 10px;
}
.job-info {
margin-bottom: 20px;
}
.job-info div {
margin-bottom: 5px;
}
.auth-section {
margin-top: 20px;
text-align: center;
}
.user-info {
display: inline-block;
background: white;
padding: 10px 20px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.user-info span {
margin-right: 15px;
font-weight: bold;
}
.logout-btn, .login-btn {
background-color: #dc3545;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
display: inline-block;
}
.login-btn {
background-color: #28a745;
}
.logout-btn:hover, .login-btn:hover {
opacity: 0.8;
}
</style>
</head>
<body>
<div class="header">
<img src="{{ logo_url }}" alt="Logo" class="logo">
<h1>Job Status</h1>
<!-- Authentication section -->
<div class="auth-section">
{% if session.oauth_token %}
<div class="user-info">
<span>Welcome, {{ user_info.displayName or user_info.givenName or 'User' }}!</span>
<a href="{{ url_for('logout') }}" class="logout-btn">Logout</a>
</div>
{% else %}
<div class="login-prompt">
<a href="{{ url_for('login') }}" class="login-btn">Login with Microsoft</a>
</div>
{% endif %}
</div>
</div>
<div class="nav-links">
<a href="{{ url_for('index') }}">Upload New Files</a> |
<a href="{{ url_for('results') }}">View All Jobs & Downloads</a>
</div>
<div class="job-section">
<h2>Job Information</h2>
<div class="job-info">
<div><strong>Job Name:</strong> {{ job['id'] }}</div>
<div><strong>Started:</strong> {{ job['timestamp'].strftime('%Y-%m-%d %H:%M:%S') if job['timestamp'] else 'Unknown' }}</div>
<div><strong>Files:</strong> {{ job['file_count'] if job['file_count'] else 'Processing...' }}</div>
<div><strong>Status:</strong>
<span class="status-{{ job['status'] }}">
{{ job['status'].title() }}
</span>
</div>
</div>
{% if job['status'] == 'processing' %}
<div class="status-section">
<h3>Processing Progress</h3>
<p id="statusMessage">{{ job['message'] }}</p>
<progress id="progressBar" value="{{ job['progress'] }}" max="100"></progress>
<div style="margin-top: 10px;">
<strong>{{ job.progress }}% Complete</strong>
</div>
</div>
{% endif %}
{% if job['status'] == 'completed' %}
<div class="status-section">
<h3>Processing Complete!</h3>
<p>{{ job.message }}</p>
<progress value="100" max="100"></progress>
<div style="margin-top: 20px;">
<a href="{{ url_for('download_job', job_id=job['id']) }}" class="download-btn" style="color: white; text-decoration: none; padding: 10px 20px; border-radius: 4px; display: inline-block;">Download Results</a>
</div>
</div>
{% endif %}
{% if job['status'] == 'failed' %}
<div class="status-section">
<h3>Processing Failed</h3>
<p>{{ job.message }}</p>
{% if job.error %}
<div class="error">
<strong>Error:</strong> {{ job.error }}
</div>
{% endif %}
<progress value="0" max="100"></progress>
</div>
{% endif %}
</div>
<div class="nav-links">
<a href="{{ url_for('index') }}" class="back-btn">Back to Upload</a>
<a href="{{ url_for('results') }}">View All Jobs</a>
</div>
{% if job['status'] == 'processing' %}
<script>
// Auto-refresh for processing jobs
function updateStatus() {
fetch("/api/job/{{ job['id'] }}/status")
.then(response => response.json())
.then(data => {
document.getElementById('statusMessage').textContent = data.message;
document.getElementById('progressBar').value = data.progress;
if (data.status === 'completed' || data.status === 'failed') {
// Reload the page to show completion/failure state
location.reload();
}
})
.catch(err => {
console.log('Status update error:', err);
});
}
// Update status every 2 seconds
setInterval(updateStatus, 2000);
</script>
{% endif %}
</body>
</html>
+242
View File
@@ -0,0 +1,242 @@
<!DOCTYPE html>
<html>
<head>
<title>All Jobs - End of Year Statements</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
max-width: 200px;
height: auto;
}
.jobs-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.job-item {
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin-bottom: 10px;
background: #fafafa;
}
.job-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.job-info {
flex-grow: 1;
}
.job-actions {
display: flex;
gap: 10px;
}
button {
background-color: #007bff;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.delete-btn {
background-color: #dc3545;
}
.download-btn {
background-color: #28a745;
}
.view-btn {
background-color: #17a2b8;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.no-jobs {
text-align: center;
color: #666;
font-style: italic;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
.nav-links {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 20px;
}
.nav-link {
color: #007bff;
text-decoration: none;
font-weight: bold;
padding: 8px 16px;
border: 2px solid #007bff;
border-radius: 4px;
transition: all 0.3s ease;
}
.nav-link:hover {
background-color: #007bff;
color: white;
}
.status-completed {
color: #28a745;
font-weight: bold;
}
.status-processing {
color: #ffc107;
font-weight: bold;
}
.status-failed {
color: #dc3545;
font-weight: bold;
}
.status-queued {
color: #6c757d;
font-weight: bold;
}
.auth-section {
margin-top: 20px;
text-align: center;
}
.user-info {
display: inline-block;
background: white;
padding: 10px 20px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.user-info span {
margin-right: 15px;
font-weight: bold;
}
.logout-btn, .login-btn {
background-color: #dc3545;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
display: inline-block;
}
.login-btn {
background-color: #28a745;
}
.logout-btn:hover, .login-btn:hover {
opacity: 0.8;
}
</style>
</head>
<body>
<div class="header">
<img src="{{ logo_url }}" alt="Logo" class="logo">
<h1>All Jobs</h1>
<p>View all your PDF processing jobs</p>
<!-- Authentication section -->
<div class="auth-section">
{% if session.oauth_token %}
<div class="user-info">
<span>Welcome, {{ user_info.displayName or user_info.givenName or 'User' }}!</span>
<a href="{{ url_for('logout') }}" class="logout-btn">Logout</a>
</div>
{% else %}
<div class="login-prompt">
<a href="{{ url_for('login') }}" class="login-btn">Login with Microsoft</a>
</div>
{% endif %}
</div>
</div>
<div class="nav-links">
<a href="{{ url_for('index') }}" class="nav-link">📤 Upload Files</a>
<a href="{{ url_for('results') }}" class="nav-link">📋 View Results</a>
<a href="{{ url_for('jobs') }}" class="nav-link">📊 View All Jobs</a>
</div>
<a href="{{ url_for('index') }}" class="back-link">← Back to Upload</a>
<a href="{{ url_for('clear_and_upload') }}" class="back-link" style="margin-left: 20px;">🔄 Reprocess Files</a>
<div class="jobs-section">
<h2>All Jobs</h2>
{% if jobs %}
{% for job in jobs %}
<div class="job-item">
<div class="job-header">
<div class="job-info">
<strong>Job Name:</strong> {{ job['id'] }}<br>
<strong>Started:</strong> {{ job['timestamp'].strftime('%Y-%m-%d %H:%M:%S') if job['timestamp'] else 'Unknown' }}<br>
{% if job['file_count'] %}
<strong>Files:</strong> {{ job['file_count'] }}<br>
{% endif %}
<strong>Status:</strong>
{% if job['status'] == 'completed' %}
<span class="status-completed">Completed</span>
{% elif job['status'] == 'processing' %}
<span class="status-processing">Processing</span>
{% elif job['status'] == 'failed' %}
<span class="status-failed">Failed</span>
{% elif job['status'] == 'queued' %}
<span class="status-queued">Queued</span>
{% else %}
<span>{{ job['status']|title }}</span>
{% endif %}
{% if job.get('error') %}
<br><strong>Error:</strong> {{ job['error'] }}
{% endif %}
{% if job.get('message') %}
<br><strong>Message:</strong> {{ job['message'] }}
{% endif %}
</div>
<div class="job-actions">
<a href="{{ url_for('job_status', job_id=job['id']) }}" class="view-btn" style="display: inline-block; padding: 8px 16px; text-decoration: none; color: white; background-color: #17a2b8; border-radius: 4px;">View Details</a>
{% if job['status'] == 'completed' %}
<form action="{{ url_for('download_job', job_id=job['id']) }}" method="get" style="display: inline;">
<button type="submit" class="download-btn">Download ZIP</button>
</form>
{% endif %}
<form action="{{ url_for('delete_job', job_id=job['id']) }}" method="post" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this job?')">
<button type="submit" class="delete-btn">Delete</button>
</form>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="no-jobs">No jobs yet. <a href="{{ url_for('index') }}">Upload some files</a> to get started.</p>
{% endif %}
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 20px;">
{% for message in messages %}
<p style="margin: 0; color: #28a745;">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
</body>
</html>
+219
View File
@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html>
<head>
<title>Processing Results - End of Year Statements</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
max-width: 200px;
height: auto;
}
.results-section {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.job-item {
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin-bottom: 10px;
background: #fafafa;
}
.job-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.job-info {
flex-grow: 1;
}
.job-actions {
display: flex;
gap: 10px;
}
button {
background-color: #007bff;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.delete-btn {
background-color: #dc3545;
}
.download-btn {
background-color: #28a745;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.no-jobs {
text-align: center;
color: #666;
font-style: italic;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
.nav-links {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 20px;
}
.nav-link {
color: #007bff;
text-decoration: none;
font-weight: bold;
padding: 8px 16px;
border: 2px solid #007bff;
border-radius: 4px;
transition: all 0.3s ease;
}
.nav-link:hover {
background-color: #007bff;
color: white;
}
.status-completed {
color: #28a745;
font-weight: bold;
}
.status-failed {
color: #dc3545;
font-weight: bold;
}
.auth-section {
margin-top: 20px;
text-align: center;
}
.user-info {
display: inline-block;
background: white;
padding: 10px 20px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.user-info span {
margin-right: 15px;
font-weight: bold;
}
.logout-btn, .login-btn {
background-color: #dc3545;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
display: inline-block;
}
.login-btn {
background-color: #28a745;
}
.logout-btn:hover, .login-btn:hover {
opacity: 0.8;
}
</style>
</head>
<body>
<div class="header">
<img src="{{ logo_url }}" alt="Logo" class="logo">
<h1>Processing Results</h1>
<p>View and download your completed PDF processing jobs</p>
<!-- Authentication section -->
<div class="auth-section">
{% if session.oauth_token %}
<div class="user-info">
<span>Welcome, {{ user_info.displayName or user_info.givenName or 'User' }}!</span>
<a href="{{ url_for('logout') }}" class="logout-btn">Logout</a>
</div>
{% else %}
<div class="login-prompt">
<a href="{{ url_for('login') }}" class="login-btn">Login with Microsoft</a>
</div>
{% endif %}
</div>
</div>
<div class="nav-links">
<a href="{{ url_for('index') }}" class="nav-link">📤 Upload Files</a>
<a href="{{ url_for('results') }}" class="nav-link">📋 View Results</a>
<a href="{{ url_for('jobs') }}" class="nav-link">📊 View All Jobs</a>
</div>
<a href="{{ url_for('index') }}" class="back-link">← Back to Upload</a>
<a href="{{ url_for('clear_and_upload') }}" class="back-link" style="margin-left: 20px;">🔄 Reprocess Files</a>
<div class="results-section">
<h2>Completed Jobs</h2>
{% if jobs %}
{% for job in jobs %}
<div class="job-item">
<div class="job-header">
<div class="job-info">
<strong>Job Name:</strong> {{ job['id'] }}<br>
<strong>Processed:</strong> {{ job['timestamp'].strftime('%Y-%m-%d %H:%M:%S') if job['timestamp'] else 'Unknown' }}<br>
<strong>Files:</strong> {{ job['file_count'] }}<br>
<strong>Status:</strong>
{% if job['status'] == 'completed' %}
<span class="status-completed">Completed</span>
{% else %}
<span class="status-failed">Failed</span>
{% endif %}
{% if job.get('error') %}
<br><strong>Error:</strong> {{ job['error'] }}
{% endif %}
</div>
<div class="job-actions">
{% if job['status'] == 'completed' %}
<form action="{{ url_for('download_job', job_id=job['id']) }}" method="get" style="display: inline;">
<button type="submit" class="download-btn">Download ZIP</button>
</form>
{% endif %}
<form action="{{ url_for('delete_job', job_id=job['id']) }}" method="post" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this job?')">
<button type="submit" class="delete-btn">Delete</button>
</form>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="no-jobs">No completed jobs yet. <a href="{{ url_for('index') }}">Upload some files</a> to get started.</p>
{% endif %}
</div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div style="background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-top: 20px;">
{% for message in messages %}
<p style="margin: 0; color: #28a745;">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% endwith %}
</body>
</html>
+490
View File
@@ -0,0 +1,490 @@
#!/bin/bash
# PDF Sort and Merge Application - Ubuntu Installation Script
# This script installs and configures the Flask web application for PDF processing
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if running as root
if [[ $EUID -eq 0 ]]; then
log_error "This script should not be run as root. Please run as a regular user with sudo privileges."
exit 1
fi
# Check Ubuntu version
UBUNTU_VERSION=$(lsb_release -rs)
log_info "Detected Ubuntu version: $UBUNTU_VERSION"
# Update system
log_info "Updating system packages..."
sudo apt update && sudo apt upgrade -y
# Install Python 3.9+ and pip
log_info "Installing Python and pip..."
sudo apt install -y python3 python3-pip python3-venv python3-dev
# Install system dependencies for PDF processing
log_info "Installing system dependencies..."
sudo apt install -y build-essential libpoppler-cpp-dev pkg-config
# Install Node.js and npm (for potential frontend assets)
log_info "Installing Node.js and npm..."
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
# Create application directory
APP_DIR="$HOME/pdf-sort-merge"
log_info "Creating application directory: $APP_DIR"
mkdir -p "$APP_DIR"
cd "$APP_DIR"
# Clone or download the application
REPO_URL="https://gitlab.com/trbc-dev/eoy-pdf-sortandmerge.git"
log_info "Cloning application repository..."
if command -v git &> /dev/null; then
sudo apt install -y git
if [[ -d ".git" ]]; then
log_info "Repository already exists, pulling latest changes..."
git pull origin web-app
git checkout web-app
else
log_info "Cloning fresh repository..."
git clone "$REPO_URL" .
git checkout web-app
fi
else
log_warning "Git not available. Please manually download and extract the application files to $APP_DIR"
exit 1
fi
# Set permissions for static files (nginx needs to read them)
log_info "Setting permissions for static files..."
chmod -R 755 static/
find static/ -type f -exec chmod 644 {} \;
# Ensure nginx can access static files - try chown, fallback to group permissions
if sudo chown -R www-data:www-data static/ 2>/dev/null; then
log_success "Static files ownership set to www-data"
else
log_warning "Could not change ownership to www-data, using group permissions instead"
# Add user to www-data group for nginx access
sudo usermod -a -G www-data $USER 2>/dev/null || true
sudo chmod -R 755 static/
find static/ -type f -exec sudo chmod 644 {} \;
log_info "Set group permissions for nginx access"
fi
# Create Python virtual environment
log_info "Creating Python virtual environment..."
python3 -m venv venv
source venv/bin/activate
# Install Python dependencies
log_info "Installing Python dependencies..."
pip install --upgrade pip
pip install -r requirements.txt
# Try to install PyMuPDF (optional for PDF processing)
log_info "Attempting to install PyMuPDF for PDF processing..."
if pip install PyMuPDF==1.23.7; then
log_success "PyMuPDF installed successfully"
else
log_warning "PyMuPDF installation failed - PDF processing will not be available"
log_warning "You can try installing it manually later with: pip install PyMuPDF"
fi
# Create .env file with Azure AD configuration
log_info "Configuring Azure AD authentication..."
echo ""
echo "=================================================================="
echo "AZURE AD CONFIGURATION REQUIRED"
echo "=================================================================="
echo "You need to provide your Azure AD app registration details."
echo "If you don't have them, follow these steps:"
echo "1. Go to https://portal.azure.com"
echo "2. Navigate to Azure Active Directory > App registrations"
echo "3. Create a new registration or use existing one"
echo "4. Note the Application (client) ID and Directory (tenant) ID"
echo "5. Create a client secret under Certificates & secrets"
echo "6. Add redirect URI: http://your-domain-name/auth/callback (will be configured automatically)"
echo "=================================================================="
echo ""
# Prompt for Azure AD credentials
read -p "Enter Azure AD Tenant ID (Directory ID): " AZURE_TENANT_ID
read -p "Enter Azure AD Client ID (Application ID): " AZURE_CLIENT_ID
read -s -p "Enter Azure AD Client Secret: " AZURE_CLIENT_SECRET
echo ""
read -p "Enter DNS domain name (e.g., eoypdf.trbc.org) [eoypdf.trbc.org]: " DNS_DOMAIN
DNS_DOMAIN=${DNS_DOMAIN:-eoypdf.trbc.org}
read -p "Enter server IP/domain (for redirect URI) [$DNS_DOMAIN]: " SERVER_HOST
SERVER_HOST=${SERVER_HOST:-$DNS_DOMAIN}
# Determine if we should use HTTPS
USE_HTTPS=false
GENERATE_SELF_SIGNED=false
if [[ "$DNS_DOMAIN" != "localhost" && "$DNS_DOMAIN" != "127.0.0.1" ]]; then
read -p "Do you have SSL certificates for $DNS_DOMAIN? (y/n) [n]: " HAS_SSL
if [[ "${HAS_SSL,,}" == "y" || "${HAS_SSL,,}" == "yes" ]]; then
USE_HTTPS=true
else
read -p "Would you like to generate self-signed SSL certificates? (y/n) [y]: " GEN_SELF
GEN_SELF=${GEN_SELF:-y}
if [[ "${GEN_SELF,,}" == "y" || "${GEN_SELF,,}" == "yes" ]]; then
USE_HTTPS=true
GENERATE_SELF_SIGNED=true
fi
fi
fi
# Create .env file with real values
cat > .env << EOF
# Microsoft Entra (Azure AD) Configuration
# Get these values from your Azure AD app registration in the Azure portal
# Azure AD Tenant ID (Directory ID)
AZURE_TENANT_ID=$AZURE_TENANT_ID
# Azure AD Client ID (Application ID)
AZURE_CLIENT_ID=$AZURE_CLIENT_ID
# Azure AD Client Secret
AZURE_CLIENT_SECRET=$AZURE_CLIENT_SECRET
# Azure AD Authority URL (usually https://login.microsoftonline.com/{tenant_id})
AZURE_AUTHORITY=https://login.microsoftonline.com/$AZURE_TENANT_ID
# Redirect URI (must match what's configured in Azure AD app registration)
AZURE_REDIRECT_URI=http${USE_HTTPS:+s}://$SERVER_HOST/auth/callback
# Scopes to request (openid, profile, email are common)
AZURE_SCOPES=openid profile email
# Flask Configuration
SECRET_KEY=$(openssl rand -hex 32)
EOF
log_success "Azure AD configuration saved to .env file"
# Generate self-signed SSL certificates if requested
if [[ "$GENERATE_SELF_SIGNED" == "true" ]]; then
log_info "Generating self-signed SSL certificates..."
sudo mkdir -p /etc/ssl/certs /etc/ssl/private
# Generate self-signed certificate
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/pdf-sort-merge.key \
-out /etc/ssl/certs/pdf-sort-merge.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=$DNS_DOMAIN" \
-addext "subjectAltName=DNS:$DNS_DOMAIN"
log_success "Self-signed SSL certificates generated"
fi
# Install and configure nginx
log_info "Installing and configuring nginx..."
sudo apt install -y nginx
# Create nginx configuration
NGINX_CONF="/etc/nginx/sites-available/pdf-sort-merge"
if [[ "$USE_HTTPS" == "true" ]]; then
# HTTPS configuration with SSL
sudo tee "$NGINX_CONF" > /dev/null << EOF
server {
listen 80;
server_name $DNS_DOMAIN;
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name $DNS_DOMAIN;
ssl_certificate /etc/ssl/certs/pdf-sort-merge.crt;
ssl_certificate_key /etc/ssl/private/pdf-sort-merge.key;
# SSL security settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
# Large file upload settings
client_max_body_size 100G;
client_body_timeout 300s;
client_header_timeout 60s;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Proxy to Flask app
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
# WebSocket support (if needed)
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
# Timeout settings
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# Disable buffering for large uploads
proxy_buffering off;
proxy_request_buffering off;
}
# Static files
location /static/ {
alias $APP_DIR/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
EOF
else
# HTTP-only configuration
sudo tee "$NGINX_CONF" > /dev/null << EOF
server {
listen 80;
server_name $DNS_DOMAIN;
# Large file upload settings
client_max_body_size 100G;
client_body_timeout 300s;
client_header_timeout 60s;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Proxy to Flask app
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
# WebSocket support (if needed)
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
# Timeout settings
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# Disable buffering for large uploads
proxy_buffering off;
proxy_request_buffering off;
}
# Static files
location /static/ {
alias $APP_DIR/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
EOF
fi
# Enable the site
sudo ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
# Test nginx configuration
log_info "Testing nginx configuration..."
if sudo nginx -t; then
log_success "Nginx configuration is valid"
else
log_error "Nginx configuration has errors"
exit 1
fi
# Restart nginx
sudo systemctl restart nginx
sudo systemctl enable nginx
log_success "Nginx configured to serve $DNS_DOMAIN on port 80"
# Create systemd service file
log_info "Creating systemd service..."
SERVICE_FILE="/etc/systemd/system/pdf-sort-merge.service"
sudo tee "$SERVICE_FILE" > /dev/null << EOF
[Unit]
Description=PDF Sort and Merge Flask Application
After=network.target
[Service]
Type=simple
User=$USER
WorkingDirectory=$APP_DIR
Environment=PATH=$APP_DIR/venv/bin
Environment=PYTHONPATH=$APP_DIR
ExecStart=$APP_DIR/venv/bin/python $APP_DIR/app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# Set proper permissions
sudo chmod 644 "$SERVICE_FILE"
# Create log directory
log_info "Creating log directory..."
sudo mkdir -p /var/log/pdf-sort-merge
sudo chown $USER:$USER /var/log/pdf-sort-merge
# Create application data directories
log_info "Creating application data directories..."
mkdir -p results static templates
# Create Flask session directory for server-side sessions
log_info "Creating Flask session directory..."
sudo mkdir -p /tmp/flask_sessions
sudo chown $USER:$USER /tmp/flask_sessions
# Set up firewall (optional)
log_info "Configuring firewall..."
sudo ufw allow 80/tcp || log_warning "Failed to configure firewall for port 80 - you may need to do this manually"
if [[ "$USE_HTTPS" == "true" ]]; then
sudo ufw allow 443/tcp || log_warning "Failed to configure firewall for port 443 - you may need to do this manually"
fi
sudo ufw allow ssh/tcp || log_warning "Failed to configure firewall for SSH - you may need to do this manually"
# Enable and start service
log_info "Enabling and starting service..."
sudo systemctl daemon-reload
sudo systemctl enable pdf-sort-merge
sudo systemctl start pdf-sort-merge
# Check service status
sleep 3
if sudo systemctl is-active --quiet pdf-sort-merge; then
log_success "Service started successfully!"
else
log_error "Service failed to start. Check logs with: sudo journalctl -u pdf-sort-merge -f"
fi
# Create management script
log_info "Creating management script..."
cat > manage.sh << 'EOF'
#!/bin/bash
case "$1" in
start)
sudo systemctl start pdf-sort-merge
echo "Service started"
;;
stop)
sudo systemctl stop pdf-sort-merge
echo "Service stopped"
;;
restart)
sudo systemctl restart pdf-sort-merge
echo "Service restarted"
;;
status)
sudo systemctl status pdf-sort-merge
;;
logs)
sudo journalctl -u pdf-sort-merge -f
;;
update)
cd /home/$USER/pdf-sort-merge
source venv/bin/activate
git pull
pip install -r requirements.txt
sudo systemctl restart pdf-sort-merge
echo "Application updated"
;;
*)
echo "Usage: $0 {start|stop|restart|status|logs|update}"
exit 1
;;
esac
EOF
chmod +x manage.sh
# Final instructions
log_success "Installation completed!"
echo ""
echo "=================================================================="
echo "NEXT STEPS:"
echo "1. Point your DNS for $DNS_DOMAIN to this server's IP address"
echo ""
if [[ "$GENERATE_SELF_SIGNED" == "true" ]]; then
echo "2. IMPORTANT: Self-signed certificates are now active!"
echo " - Browsers will show a security warning - click 'Advanced' and 'Proceed'"
echo " - For production, consider getting a proper SSL certificate from Let's Encrypt"
echo ""
fi
echo "3. Update your Azure AD app registration redirect URI to:"
if [[ "$USE_HTTPS" == "true" ]]; then
echo " https://$SERVER_HOST/auth/callback"
else
echo " http://$SERVER_HOST/auth/callback"
fi
echo ""
echo "4. Access the application at:"
if [[ "$USE_HTTPS" == "true" ]]; then
echo " https://$DNS_DOMAIN"
else
echo " http://$DNS_DOMAIN"
fi
echo ""
echo "MANAGEMENT COMMANDS:"
echo " Start service: sudo systemctl start pdf-sort-merge"
echo " Stop service: sudo systemctl stop pdf-sort-merge"
echo " Restart nginx: sudo systemctl restart nginx"
echo " Check status: sudo systemctl status pdf-sort-merge"
echo " View logs: sudo journalctl -u pdf-sort-merge -f"
echo " Update app: ./manage.sh update"
echo "=================================================================="
# Check if .env needs configuration
if [[ -z "$AZURE_TENANT_ID" || -z "$AZURE_CLIENT_ID" || -z "$AZURE_CLIENT_SECRET" ]]; then
log_warning "IMPORTANT: Azure AD configuration is incomplete. Please check your .env file!"
fi