updated documentation folder

This commit is contained in:
Quality System Admin
2025-11-03 21:17:10 +02:00
parent 8d47e6e82d
commit 1ade0b5681
29 changed files with 6113 additions and 32 deletions

View File

@@ -27,6 +27,7 @@ DB_RETRY_INTERVAL=2
DB_DATA_PATH=/srv/docker-test/mariadb
LOGS_PATH=/srv/docker-test/logs
INSTANCE_PATH=/srv/docker-test/instance
BACKUP_PATH=/srv/docker-test/backups
# ============================================================================
# APPLICATION CONFIGURATION

74
README.md Normal file
View File

@@ -0,0 +1,74 @@
# Quality Recticel Application
Production traceability and quality management system.
## 📚 Documentation
All development and deployment documentation has been moved to the **[documentation](./documentation/)** folder.
### Quick Links
- **[Documentation Index](./documentation/README.md)** - Complete documentation overview
- **[Database Setup](./documentation/DATABASE_DOCKER_SETUP.md)** - Database configuration guide
- **[Docker Guide](./documentation/DOCKER_QUICK_REFERENCE.md)** - Docker commands reference
- **[Backup System](./documentation/BACKUP_SYSTEM.md)** - Database backup documentation
## 🚀 Quick Start
```bash
# Start application
cd /srv/quality_app/py_app
bash start_production.sh
# Stop application
bash stop_production.sh
# View logs
tail -f /srv/quality_app/logs/error.log
```
## 📦 Docker Deployment
```bash
# Start with Docker Compose
docker-compose up -d
# View logs
docker-compose logs -f web
# Stop services
docker-compose down
```
## 🔐 Default Access
- **URL**: http://localhost:8781
- **Username**: superadmin
- **Password**: superadmin123
## 📁 Project Structure
```
quality_app/
├── documentation/ # All documentation files
├── py_app/ # Flask application
├── backups/ # Database backups
├── logs/ # Application logs
├── docker-compose.yml # Docker configuration
└── Dockerfile # Container image definition
```
## 📖 For More Information
See the **[documentation](./documentation/)** folder for comprehensive guides on:
- Setup and deployment
- Docker configuration
- Database management
- Backup and restore procedures
- Application features
---
**Version**: 1.0.0
**Last Updated**: November 3, 2025

View File

@@ -144,6 +144,11 @@ services:
# ======================================================================
TZ: ${TZ:-Europe/Bucharest}
LANG: ${LANG:-en_US.UTF-8}
# ======================================================================
# Backup Configuration
# ======================================================================
BACKUP_PATH: ${BACKUP_PATH:-/srv/quality_recticel/backups}
ports:
- "${APP_PORT:-8781}:8781"
@@ -155,6 +160,9 @@ services:
# Instance configuration directory
- ${INSTANCE_PATH:-/srv/docker-test/instance}:/app/instance
# Database backups directory
- ${BACKUP_PATH:-/srv/docker-test/backups}:/srv/quality_recticel/backups
# ⚠️ DEVELOPMENT ONLY: Mount application code for live updates
# DISABLE IN PRODUCTION - causes configuration and security issues
# - ./py_app:/app

View File

@@ -0,0 +1,205 @@
# Database Backup System Documentation
## Overview
The Quality Recticel application now includes a comprehensive database backup management system accessible from the Settings page for superadmin and admin users.
## Features
### 1. Manual Backup
- **Backup Now** button creates an immediate full database backup
- Uses `mysqldump` to create complete SQL export
- Includes all tables, triggers, routines, and events
- Each backup is timestamped: `backup_trasabilitate_YYYYMMDD_HHMMSS.sql`
### 2. Scheduled Backups
Configure automated backups with:
- **Enable/Disable**: Toggle scheduled backups on/off
- **Backup Time**: Set time of day for automatic backup (default: 02:00)
- **Frequency**: Choose Daily, Weekly, or Monthly backups
- **Retention Period**: Automatically delete backups older than N days (default: 30 days)
### 3. Backup Management
- **List Backups**: View all available backup files with size and creation date
- **Download**: Download any backup file to your local computer
- **Delete**: Remove old or unnecessary backup files
- **Restore**: (Superadmin only) Restore database from a backup file
## Configuration
### Backup Path
The backup location can be configured in three ways (priority order):
1. **Environment Variable** (Docker):
```yaml
# docker-compose.yml
environment:
BACKUP_PATH: /srv/quality_recticel/backups
volumes:
- /srv/docker-test/backups:/srv/quality_recticel/backups
```
2. **Configuration File**:
```ini
# py_app/instance/external_server.conf
backup_path=/srv/quality_app/backups
```
3. **Default Path**: `/srv/quality_app/backups`
### .env Configuration
Add to your `.env` file:
```bash
BACKUP_PATH=/srv/docker-test/backups
```
## Usage
### Access Backup Management
1. Login as **superadmin** or **admin**
2. Navigate to **Settings** page
3. Scroll to **💾 Database Backup Management** card
4. The backup management interface is only visible to superadmin/admin users
### Create Manual Backup
1. Click **⚡ Backup Now** button
2. Wait for confirmation message
3. New backup appears in the list
### Configure Scheduled Backups
1. Check **Enable Scheduled Backups**
2. Set desired backup time (24-hour format)
3. Select frequency (Daily/Weekly/Monthly)
4. Set retention period (days to keep backups)
5. Click **💾 Save Schedule**
### Download Backup
1. Locate backup in the list
2. Click **⬇️ Download** button
3. File downloads to your computer
### Delete Backup
1. Locate backup in the list
2. Click **🗑️ Delete** button
3. Confirm deletion
### Restore Backup (Superadmin Only)
⚠️ **WARNING**: Restore will replace current database!
1. This feature requires superadmin privileges
2. API endpoint: `/api/backup/restore/<filename>`
3. Use with extreme caution
## Technical Details
### Backup Module
Location: `py_app/app/database_backup.py`
Key Class: `DatabaseBackupManager`
Methods:
- `create_backup()`: Create new backup
- `list_backups()`: Get all backup files
- `delete_backup(filename)`: Remove backup file
- `restore_backup(filename)`: Restore from backup
- `get_backup_schedule()`: Get current schedule
- `save_backup_schedule(schedule)`: Update schedule
- `cleanup_old_backups(days)`: Remove old backups
### API Endpoints
| Endpoint | Method | Access | Description |
|----------|--------|--------|-------------|
| `/api/backup/create` | POST | Admin+ | Create new backup |
| `/api/backup/list` | GET | Admin+ | List all backups |
| `/api/backup/download/<filename>` | GET | Admin+ | Download backup file |
| `/api/backup/delete/<filename>` | DELETE | Admin+ | Delete backup file |
| `/api/backup/schedule` | GET/POST | Admin+ | Get/Set backup schedule |
| `/api/backup/restore/<filename>` | POST | Superadmin | Restore from backup |
### Backup File Format
- **Format**: SQL dump file (`.sql`)
- **Compression**: Not compressed (can be gzip manually if needed)
- **Contents**: Complete database with structure and data
- **Metadata**: Stored in `backups_metadata.json`
### Schedule Storage
Schedule configuration stored in: `{BACKUP_PATH}/backup_schedule.json`
Example:
```json
{
"enabled": true,
"time": "02:00",
"frequency": "daily",
"retention_days": 30
}
```
## Security Considerations
1. **Access Control**: Backup features restricted to admin and superadmin users
2. **Path Traversal Protection**: Filenames validated to prevent directory traversal attacks
3. **Credentials**: Database credentials read from `external_server.conf`
4. **Backup Location**: Should be on different mount point than application for safety
## Maintenance
### Disk Space
Monitor backup directory size:
```bash
du -sh /srv/quality_app/backups
```
### Manual Cleanup
Remove old backups manually:
```bash
find /srv/quality_app/backups -name "*.sql" -mtime +30 -delete
```
### Backup Verification
Test restore in development environment:
```bash
mysql -u root -p trasabilitate < backup_trasabilitate_20251103_020000.sql
```
## Troubleshooting
### Backup Fails
- Check database credentials in `external_server.conf`
- Ensure `mysqldump` is installed
- Verify write permissions on backup directory
- Check disk space availability
### Scheduled Backups Not Running
- TODO: Implement scheduled backup daemon/cron job
- Check backup schedule is enabled
- Verify time format is correct (HH:MM)
### Cannot Download Backup
- Check backup file exists
- Verify file permissions
- Ensure adequate network bandwidth
## Future Enhancements
### Planned Features (Task 4)
- [ ] Implement APScheduler for automated scheduled backups
- [ ] Add backup to external storage (S3, FTP, etc.)
- [ ] Email notifications for backup success/failure
- [ ] Backup compression (gzip)
- [ ] Incremental backups
- [ ] Backup encryption
- [ ] Backup verification tool
## Support
For issues or questions about the backup system:
1. Check application logs: `/srv/quality_app/logs/error.log`
2. Verify backup directory permissions
3. Test manual backup first before relying on scheduled backups
4. Keep at least 2 recent backups before deleting old ones
---
**Created**: November 3, 2025
**Module**: Database Backup Management
**Version**: 1.0.0

148
documentation/README.md Normal file
View File

@@ -0,0 +1,148 @@
# Quality Recticel Application - Documentation
This folder contains all development and deployment documentation for the Quality Recticel application.
## Documentation Index
### Setup & Deployment
- **[DATABASE_DOCKER_SETUP.md](./DATABASE_DOCKER_SETUP.md)** - Complete guide for database configuration and Docker setup
- **[DOCKER_IMPROVEMENTS.md](./DOCKER_IMPROVEMENTS.md)** - Detailed changelog of Docker-related improvements and optimizations
- **[DOCKER_QUICK_REFERENCE.md](./DOCKER_QUICK_REFERENCE.md)** - Quick reference guide for common Docker commands and operations
### Features & Systems
- **[BACKUP_SYSTEM.md](./BACKUP_SYSTEM.md)** - Database backup management system documentation
- Manual and scheduled backups
- Backup configuration and management
- Restore procedures
## Quick Links
### Application Structure
```
quality_app/
├── py_app/ # Python application code
│ ├── app/ # Flask application modules
│ │ ├── __init__.py # App factory
│ │ ├── routes.py # Main routes
│ │ ├── daily_mirror.py # Daily Mirror module
│ │ ├── database_backup.py # Backup system
│ │ ├── templates/ # HTML templates
│ │ └── static/ # CSS, JS, images
│ ├── instance/ # Configuration files
│ └── requirements.txt # Python dependencies
├── backups/ # Database backups
├── logs/ # Application logs
├── documentation/ # This folder
└── docker-compose.yml # Docker configuration
```
### Key Configuration Files
- `py_app/instance/external_server.conf` - Database connection settings
- `docker-compose.yml` - Docker services configuration
- `.env` - Environment variables (create from .env.example)
- `py_app/gunicorn.conf.py` - Gunicorn WSGI server settings
### Access Levels
The application uses a 4-tier role system:
1. **Superadmin** (Level 100) - Full system access
2. **Admin** (Level 90) - Administrative access
3. **Manager** (Level 70) - Module management
4. **Worker** (Level 50) - Basic operations
### Modules
- **Quality** - Production scanning and quality reports
- **Warehouse** - Warehouse management
- **Labels** - Label printing and management
- **Daily Mirror** - Business intelligence and reporting
## Development Notes
### Recent Changes (November 2025)
1. **SQLAlchemy Removal** - Simplified to direct MariaDB connections
2. **Daily Mirror Module** - Fully integrated with access control
3. **Backup System** - Complete database backup management
4. **Access Control** - Superadmin gets automatic full access
5. **Docker Optimization** - Production-ready configuration
### Common Tasks
**Start Application:**
```bash
cd /srv/quality_app/py_app
bash start_production.sh
```
**Stop Application:**
```bash
cd /srv/quality_app/py_app
bash stop_production.sh
```
**View Logs:**
```bash
tail -f /srv/quality_app/logs/error.log
tail -f /srv/quality_app/logs/access.log
```
**Create Backup:**
- Login as superadmin/admin
- Go to Settings page
- Click "Backup Now" button
**Check Application Status:**
```bash
ps aux | grep gunicorn | grep trasabilitate
```
## Support & Maintenance
### Log Locations
- **Access Log**: `/srv/quality_app/logs/access.log`
- **Error Log**: `/srv/quality_app/logs/error.log`
- **Backup Location**: `/srv/quality_app/backups/`
### Database
- **Host**: localhost (or as configured)
- **Port**: 3306
- **Database**: trasabilitate
- **User**: trasabilitate
### Default Login
- **Username**: superadmin
- **Password**: superadmin123
⚠️ **Change default credentials in production!**
## Contributing
When adding new documentation:
1. Place markdown files in this folder
2. Update this README with links
3. Use clear, descriptive filenames
4. Include date and version when applicable
## Version History
- **v1.0.0** (November 2025) - Initial production release
- Docker deployment ready
- Backup system implemented
- Daily Mirror module integrated
- SQLAlchemy removed
---
**Last Updated**: November 3, 2025
**Application**: Quality Recticel Traceability System
**Technology Stack**: Flask, MariaDB, Gunicorn, Docker

View File

@@ -433,3 +433,72 @@
192.168.0.132 - - [22/Oct/2025:11:22:49 +0300] "GET / HTTP/1.1" 200 1627 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2032
192.168.0.132 - - [22/Oct/2025:11:22:50 +0300] "GET /favicon.ico HTTP/1.1" 404 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 1773
192.168.0.132 - - [22/Oct/2025:18:45:12 +0300] "GET /favicon.ico HTTP/1.1" 404 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 1959
127.0.0.1 - - [03/Nov/2025:20:06:54 +0200] "GET / HTTP/1.1" 200 1688 "-" "curl/8.14.1" 63649 µs
192.168.0.132 - - [03/Nov/2025:20:09:42 +0200] "GET /user_management_simple HTTP/1.1" 200 45867 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 113475 µs
127.0.0.1 - - [03/Nov/2025:20:16:12 +0200] "GET / HTTP/1.1" 200 1688 "-" "curl/8.14.1" 64029 µs
192.168.0.132 - - [03/Nov/2025:20:16:45 +0200] "GET /user_management_simple HTTP/1.1" 200 46876 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 62091 µs
192.168.0.132 - - [03/Nov/2025:20:17:23 +0200] "POST /edit_user_simple HTTP/1.1" 302 233 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 31144 µs
192.168.0.132 - - [03/Nov/2025:20:17:24 +0200] "GET /user_management_simple HTTP/1.1" 200 47317 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 96688 µs
192.168.0.132 - - [03/Nov/2025:20:17:44 +0200] "POST /edit_user_simple HTTP/1.1" 302 233 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 9053 µs
192.168.0.132 - - [03/Nov/2025:20:17:44 +0200] "GET /user_management_simple HTTP/1.1" 200 47635 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 6762 µs
192.168.0.132 - - [03/Nov/2025:20:18:17 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 12037 µs
192.168.0.132 - - [03/Nov/2025:20:18:20 +0200] "GET /warehouse HTTP/1.1" 200 2987 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 65997 µs
192.168.0.132 - - [03/Nov/2025:20:18:29 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/warehouse" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 13728 µs
192.168.0.132 - - [03/Nov/2025:20:18:31 +0200] "GET /etichete HTTP/1.1" 200 3204 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 9236 µs
192.168.0.132 - - [03/Nov/2025:20:18:31 +0200] "GET /static/css/print_module.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/etichete" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 13002 µs
192.168.0.132 - - [03/Nov/2025:20:18:33 +0200] "GET /upload_data HTTP/1.1" 200 11226 "https://quality.moto-adv.com/etichete" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 21495 µs
192.168.0.132 - - [03/Nov/2025:20:18:43 +0200] "GET /logout HTTP/1.1" 302 189 "https://quality.moto-adv.com/upload_data" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2430 µs
192.168.0.132 - - [03/Nov/2025:20:18:43 +0200] "GET / HTTP/1.1" 200 1688 "https://quality.moto-adv.com/upload_data" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 8582 µs
192.168.0.132 - - [03/Nov/2025:20:18:49 +0200] "POST / HTTP/1.1" 302 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 6791 µs
192.168.0.132 - - [03/Nov/2025:20:18:49 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2657 µs
192.168.0.132 - - [03/Nov/2025:20:18:54 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 7222 µs
192.168.0.132 - - [03/Nov/2025:20:18:54 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2610 µs
192.168.0.132 - - [03/Nov/2025:20:18:59 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 29065 µs
192.168.0.132 - - [03/Nov/2025:20:18:59 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 51296 µs
192.168.0.132 - - [03/Nov/2025:20:19:45 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 8535 µs
192.168.0.132 - - [03/Nov/2025:20:19:45 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 12117 µs
127.0.0.1 - - [03/Nov/2025:20:21:14 +0200] "GET /daily_mirror/ HTTP/1.1" 302 189 "-" "curl/8.14.1" 2105 µs
127.0.0.1 - - [03/Nov/2025:20:24:47 +0200] "GET /daily_mirror/ HTTP/1.1" 302 189 "-" "curl/8.14.1" 20248 µs
192.168.0.132 - - [03/Nov/2025:20:24:57 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 29323 µs
192.168.0.132 - - [03/Nov/2025:20:24:57 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 51985 µs
192.168.0.132 - - [03/Nov/2025:20:25:00 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2846 µs
192.168.0.132 - - [03/Nov/2025:20:25:02 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2657 µs
192.168.0.132 - - [03/Nov/2025:20:25:03 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 7317 µs
192.168.0.132 - - [03/Nov/2025:20:25:03 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 70160 µs
192.168.0.132 - - [03/Nov/2025:20:25:10 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 3335 µs
192.168.0.132 - - [03/Nov/2025:20:26:57 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 69436 µs
192.168.0.132 - - [03/Nov/2025:20:26:58 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 11113 µs
192.168.0.132 - - [03/Nov/2025:20:26:58 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2802 µs
192.168.0.132 - - [03/Nov/2025:20:27:00 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 29247 µs
192.168.0.132 - - [03/Nov/2025:20:27:00 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 50880 µs
127.0.0.1 - - [03/Nov/2025:20:28:14 +0200] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.14.1" 63789 µs
192.168.0.132 - - [03/Nov/2025:20:30:34 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 69748 µs
192.168.0.132 - - [03/Nov/2025:20:30:36 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 20964 µs
192.168.0.132 - - [03/Nov/2025:20:30:36 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 51986 µs
192.168.0.132 - - [03/Nov/2025:20:30:39 +0200] "GET /settings HTTP/1.1" 200 10631 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 31292 µs
192.168.0.132 - - [03/Nov/2025:20:31:04 +0200] "GET /user_management_simple HTTP/1.1" 200 47635 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 117040 µs
127.0.0.1 - - [03/Nov/2025:20:42:01 +0200] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.14.1" 63181 µs
192.168.0.132 - - [03/Nov/2025:20:42:16 +0200] "GET /settings HTTP/1.1" 200 10631 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 87361 µs
192.168.0.132 - - [03/Nov/2025:20:42:22 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 12878 µs
192.168.0.132 - - [03/Nov/2025:20:42:23 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 8459 µs
192.168.0.132 - - [03/Nov/2025:20:42:24 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2708 µs
192.168.0.132 - - [03/Nov/2025:20:42:25 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 7038 µs
192.168.0.132 - - [03/Nov/2025:20:42:25 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2642 µs
192.168.0.132 - - [03/Nov/2025:20:42:30 +0200] "GET /logout HTTP/1.1" 302 189 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2612 µs
192.168.0.132 - - [03/Nov/2025:20:42:30 +0200] "GET / HTTP/1.1" 200 1688 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 8570 µs
192.168.0.132 - - [03/Nov/2025:20:42:35 +0200] "POST / HTTP/1.1" 302 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 7549 µs
192.168.0.132 - - [03/Nov/2025:20:42:35 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2668 µs
192.168.0.132 - - [03/Nov/2025:20:42:37 +0200] "GET /daily_mirror/main HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 6891 µs
192.168.0.132 - - [03/Nov/2025:20:42:37 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2666 µs
192.168.0.132 - - [03/Nov/2025:20:45:16 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2631 µs
192.168.0.132 - - [03/Nov/2025:20:45:18 +0200] "GET /daily_mirror/main HTTP/1.1" 200 10549 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 18688 µs
192.168.0.132 - - [03/Nov/2025:20:45:21 +0200] "GET /daily_mirror/build_database HTTP/1.1" 200 31802 "https://quality.moto-adv.com/daily_mirror/main" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 56457 µs
192.168.0.132 - - [03/Nov/2025:20:45:21 +0200] "GET /static/daily_mirror_tune.css HTTP/1.1" 404 207 "https://quality.moto-adv.com/daily_mirror/build_database" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 5332 µs
192.168.0.132 - - [03/Nov/2025:20:45:30 +0200] "GET /daily_mirror/main HTTP/1.1" 200 10549 "https://quality.moto-adv.com/daily_mirror/main" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 77877 µs
192.168.0.132 - - [03/Nov/2025:20:45:32 +0200] "GET /daily_mirror/main HTTP/1.1" 200 10549 "https://quality.moto-adv.com/daily_mirror/main" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 6436 µs
192.168.0.132 - - [03/Nov/2025:20:45:35 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/daily_mirror/main" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2671 µs
192.168.0.132 - - [03/Nov/2025:20:46:50 +0200] "GET /settings HTTP/1.1" 200 10631 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 87865 µs
127.0.0.1 - - [03/Nov/2025:21:08:25 +0200] "GET /settings HTTP/1.1" 302 189 "-" "curl/8.14.1" 19562 µs
192.168.0.132 - - [03/Nov/2025:21:08:33 +0200] "GET /settings HTTP/1.1" 200 19546 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 94529 µs
192.168.0.132 - - [03/Nov/2025:21:08:33 +0200] "GET /api/backup/list HTTP/1.1" 200 30 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 21173 µs
192.168.0.132 - - [03/Nov/2025:21:08:33 +0200] "GET /api/backup/schedule HTTP/1.1" 200 101 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 23522 µs

View File

@@ -125,3 +125,773 @@
[2025-10-22 20:52:12 +0300] [299445] [INFO] Worker exiting (pid: 299445)
[2025-10-22 20:52:12 +0300] [299443] [INFO] Worker exiting (pid: 299443)
[2025-10-22 20:52:13 +0300] [299414] [INFO] Shutting down: Master
[2025-11-03 20:05:59 +0200] [395583] [INFO] Starting gunicorn 23.0.0
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🚀 Trasabilitate Application - Starting Server
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
[2025-11-03 20:05:59 +0200] [395583] [INFO] 📍 Configuration:
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Workers: 9
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Worker Class: sync
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Timeout: 120s
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Bind: 0.0.0.0:8781
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Preload App: True
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Max Requests: 1000 (+/- 100)
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
[2025-11-03 20:05:59 +0200] [395583] [INFO] Listening at: http://0.0.0.0:8781 (395583)
[2025-11-03 20:05:59 +0200] [395583] [INFO] Using worker: sync
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
[2025-11-03 20:05:59 +0200] [395583] [INFO] ✅ Trasabilitate Application Server is READY!
[2025-11-03 20:05:59 +0200] [395583] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:05:59 +0200] [395608] [INFO] Booting worker with pid: 395608
[2025-11-03 20:05:59 +0200] [395608] [INFO] ✨ Worker spawned successfully (pid: 395608)
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:05:59 +0200] [395609] [INFO] Booting worker with pid: 395609
[2025-11-03 20:05:59 +0200] [395609] [INFO] ✨ Worker spawned successfully (pid: 395609)
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:05:59 +0200] [395610] [INFO] Booting worker with pid: 395610
[2025-11-03 20:05:59 +0200] [395610] [INFO] ✨ Worker spawned successfully (pid: 395610)
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:05:59 +0200] [395611] [INFO] Booting worker with pid: 395611
[2025-11-03 20:05:59 +0200] [395611] [INFO] ✨ Worker spawned successfully (pid: 395611)
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:05:59 +0200] [395612] [INFO] Booting worker with pid: 395612
[2025-11-03 20:05:59 +0200] [395612] [INFO] ✨ Worker spawned successfully (pid: 395612)
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:05:59 +0200] [395613] [INFO] Booting worker with pid: 395613
[2025-11-03 20:05:59 +0200] [395613] [INFO] ✨ Worker spawned successfully (pid: 395613)
[2025-11-03 20:06:00 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:06:00 +0200] [395614] [INFO] Booting worker with pid: 395614
[2025-11-03 20:06:00 +0200] [395614] [INFO] ✨ Worker spawned successfully (pid: 395614)
[2025-11-03 20:06:00 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:06:00 +0200] [395615] [INFO] Booting worker with pid: 395615
[2025-11-03 20:06:00 +0200] [395615] [INFO] ✨ Worker spawned successfully (pid: 395615)
[2025-11-03 20:06:00 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:06:00 +0200] [395616] [INFO] Booting worker with pid: 395616
[2025-11-03 20:06:00 +0200] [395616] [INFO] ✨ Worker spawned successfully (pid: 395616)
[2025-11-03 20:16:00 +0200] [395610] [INFO] Worker exiting (pid: 395610)
[2025-11-03 20:16:00 +0200] [395609] [INFO] Worker exiting (pid: 395609)
[2025-11-03 20:16:00 +0200] [395608] [INFO] Worker exiting (pid: 395608)
[2025-11-03 20:16:00 +0200] [395583] [INFO] Handling signal: term
[2025-11-03 20:16:00 +0200] [395613] [INFO] Worker exiting (pid: 395613)
[2025-11-03 20:16:00 +0200] [395611] [INFO] Worker exiting (pid: 395611)
[2025-11-03 20:16:00 +0200] [395614] [INFO] Worker exiting (pid: 395614)
[2025-11-03 20:16:00 +0200] [395612] [INFO] Worker exiting (pid: 395612)
[2025-11-03 20:16:00 +0200] [395615] [INFO] Worker exiting (pid: 395615)
[2025-11-03 20:16:00 +0200] [395616] [INFO] Worker exiting (pid: 395616)
Traceback (most recent call last):
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 223, in run
handler()
~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 256, in handle_term
raise StopIteration
StopIteration
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/srv/quality_recticel/recticel/bin/gunicorn", line 8, in <module>
sys.exit(run())
~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 66, in run
WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]", prog=prog).run()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 235, in run
super().run()
~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 71, in run
Arbiter(self).run()
~~~~~~~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 226, in run
self.halt()
~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 341, in halt
self.stop()
~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 395, in stop
time.sleep(0.1)
~~~~~~~~~~^^^^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 241, in handle_chld
self.reap_workers()
~~~~~~~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 559, in reap_workers
self.cfg.child_exit(self, worker)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "/srv/quality_app/py_app/gunicorn.conf.py", line 167, in child_exit
server.log.info("👋 Worker %s exited", worker.pid)
AttributeError: 'WorkerTmp' object has no attribute 'last_mtime'
[2025-11-03 20:16:05 +0200] [395971] [INFO] Starting gunicorn 23.0.0
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🚀 Trasabilitate Application - Starting Server
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
[2025-11-03 20:16:05 +0200] [395971] [INFO] 📍 Configuration:
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Workers: 9
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Worker Class: sync
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Timeout: 120s
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Bind: 0.0.0.0:8781
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Preload App: True
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Max Requests: 1000 (+/- 100)
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
[2025-11-03 20:16:05 +0200] [395971] [INFO] Listening at: http://0.0.0.0:8781 (395971)
[2025-11-03 20:16:05 +0200] [395971] [INFO] Using worker: sync
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
[2025-11-03 20:16:05 +0200] [395971] [INFO] ✅ Trasabilitate Application Server is READY!
[2025-11-03 20:16:05 +0200] [395971] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:16:05 +0200] [395976] [INFO] Booting worker with pid: 395976
[2025-11-03 20:16:05 +0200] [395976] [INFO] ✨ Worker spawned successfully (pid: 395976)
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:16:05 +0200] [395977] [INFO] Booting worker with pid: 395977
[2025-11-03 20:16:05 +0200] [395977] [INFO] ✨ Worker spawned successfully (pid: 395977)
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:16:05 +0200] [395978] [INFO] Booting worker with pid: 395978
[2025-11-03 20:16:05 +0200] [395978] [INFO] ✨ Worker spawned successfully (pid: 395978)
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:16:05 +0200] [395979] [INFO] Booting worker with pid: 395979
[2025-11-03 20:16:05 +0200] [395979] [INFO] ✨ Worker spawned successfully (pid: 395979)
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:16:05 +0200] [395980] [INFO] Booting worker with pid: 395980
[2025-11-03 20:16:05 +0200] [395980] [INFO] ✨ Worker spawned successfully (pid: 395980)
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:16:05 +0200] [395981] [INFO] Booting worker with pid: 395981
[2025-11-03 20:16:05 +0200] [395981] [INFO] ✨ Worker spawned successfully (pid: 395981)
[2025-11-03 20:16:06 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:16:06 +0200] [395982] [INFO] Booting worker with pid: 395982
[2025-11-03 20:16:06 +0200] [395982] [INFO] ✨ Worker spawned successfully (pid: 395982)
[2025-11-03 20:16:06 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:16:06 +0200] [395983] [INFO] Booting worker with pid: 395983
[2025-11-03 20:16:06 +0200] [395983] [INFO] ✨ Worker spawned successfully (pid: 395983)
[2025-11-03 20:16:06 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:16:06 +0200] [395984] [INFO] Booting worker with pid: 395984
[2025-11-03 20:16:06 +0200] [395984] [INFO] ✨ Worker spawned successfully (pid: 395984)
Session user: superadmin superadmin
Session user: superadmin superadmin
All form data received: {'username': 'superadmin', 'password': 'Vanessa_13/05'}
Raw form input: 'superadmin' 'Vanessa_13/05'
External DB query result (with modules): ('superadmin', 'Vanessa_13/05', 'superadmin', None)
Logged in as: superadmin superadmin modules: ['quality', 'warehouse', 'labels']
Session user: superadmin superadmin
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
[2025-11-03 20:23:46 +0200] [395971] [INFO] Handling signal: term
[2025-11-03 20:23:46 +0200] [395977] [INFO] Worker exiting (pid: 395977)
[2025-11-03 20:23:46 +0200] [395976] [INFO] Worker exiting (pid: 395976)
[2025-11-03 20:23:46 +0200] [395978] [INFO] Worker exiting (pid: 395978)
[2025-11-03 20:23:46 +0200] [395979] [INFO] Worker exiting (pid: 395979)
[2025-11-03 20:23:46 +0200] [395980] [INFO] Worker exiting (pid: 395980)
[2025-11-03 20:23:46 +0200] [395981] [INFO] Worker exiting (pid: 395981)
[2025-11-03 20:23:46 +0200] [395982] [INFO] Worker exiting (pid: 395982)
[2025-11-03 20:23:46 +0200] [395983] [INFO] Worker exiting (pid: 395983)
[2025-11-03 20:23:46 +0200] [395984] [INFO] Worker exiting (pid: 395984)
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395980 exited
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395977 exited
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395976 exited
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395981 exited
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395982 exited
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Worker 395979 exited
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Worker 395984 exited
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Worker 395978 exited
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Worker 395983 exited
[2025-11-03 20:23:48 +0200] [395971] [INFO] Shutting down: Master
[2025-11-03 20:23:48 +0200] [395971] [INFO] ============================================================
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Trasabilitate Application - Shutting Down
[2025-11-03 20:23:48 +0200] [395971] [INFO] ============================================================
[2025-11-03 20:23:54 +0200] [396278] [INFO] Starting gunicorn 23.0.0
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🚀 Trasabilitate Application - Starting Server
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
[2025-11-03 20:23:54 +0200] [396278] [INFO] 📍 Configuration:
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Workers: 9
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Worker Class: sync
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Timeout: 120s
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Bind: 0.0.0.0:8781
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Preload App: True
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Max Requests: 1000 (+/- 100)
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
[2025-11-03 20:23:54 +0200] [396278] [INFO] Listening at: http://0.0.0.0:8781 (396278)
[2025-11-03 20:23:54 +0200] [396278] [INFO] Using worker: sync
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
[2025-11-03 20:23:54 +0200] [396278] [INFO] ✅ Trasabilitate Application Server is READY!
[2025-11-03 20:23:54 +0200] [396278] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:23:54 +0200] [396305] [INFO] Booting worker with pid: 396305
[2025-11-03 20:23:54 +0200] [396305] [INFO] ✨ Worker spawned successfully (pid: 396305)
[2025-11-03 20:23:54 +0200] [396306] [INFO] Booting worker with pid: 396306
[2025-11-03 20:23:54 +0200] [396306] [INFO] ✨ Worker spawned successfully (pid: 396306)
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:23:54 +0200] [396307] [INFO] Booting worker with pid: 396307
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:23:54 +0200] [396307] [INFO] ✨ Worker spawned successfully (pid: 396307)
[2025-11-03 20:23:54 +0200] [396308] [INFO] Booting worker with pid: 396308
[2025-11-03 20:23:54 +0200] [396308] [INFO] ✨ Worker spawned successfully (pid: 396308)
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:23:55 +0200] [396309] [INFO] Booting worker with pid: 396309
[2025-11-03 20:23:55 +0200] [396309] [INFO] ✨ Worker spawned successfully (pid: 396309)
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:23:55 +0200] [396310] [INFO] Booting worker with pid: 396310
[2025-11-03 20:23:55 +0200] [396310] [INFO] ✨ Worker spawned successfully (pid: 396310)
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:23:55 +0200] [396311] [INFO] Booting worker with pid: 396311
[2025-11-03 20:23:55 +0200] [396311] [INFO] ✨ Worker spawned successfully (pid: 396311)
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:23:55 +0200] [396312] [INFO] Booting worker with pid: 396312
[2025-11-03 20:23:55 +0200] [396312] [INFO] ✨ Worker spawned successfully (pid: 396312)
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:23:55 +0200] [396313] [INFO] Booting worker with pid: 396313
[2025-11-03 20:23:55 +0200] [396313] [INFO] ✨ Worker spawned successfully (pid: 396313)
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
Session user: superadmin superadmin
Session user: superadmin superadmin
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
Session user: superadmin superadmin
[2025-11-03 20:26:06 +0200] [396278] [INFO] Handling signal: term
[2025-11-03 20:26:06 +0200] [396307] [INFO] Worker exiting (pid: 396307)
[2025-11-03 20:26:06 +0200] [396305] [INFO] Worker exiting (pid: 396305)
[2025-11-03 20:26:06 +0200] [396306] [INFO] Worker exiting (pid: 396306)
[2025-11-03 20:26:06 +0200] [396308] [INFO] Worker exiting (pid: 396308)
[2025-11-03 20:26:06 +0200] [396309] [INFO] Worker exiting (pid: 396309)
[2025-11-03 20:26:06 +0200] [396310] [INFO] Worker exiting (pid: 396310)
[2025-11-03 20:26:06 +0200] [396311] [INFO] Worker exiting (pid: 396311)
[2025-11-03 20:26:06 +0200] [396312] [INFO] Worker exiting (pid: 396312)
[2025-11-03 20:26:06 +0200] [396313] [INFO] Worker exiting (pid: 396313)
[2025-11-03 20:26:06 +0200] [396278] [INFO] 👋 Worker 396305 exited
[2025-11-03 20:26:06 +0200] [396278] [INFO] 👋 Worker 396306 exited
[2025-11-03 20:26:06 +0200] [396278] [INFO] 👋 Worker 396308 exited
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396312 exited
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396309 exited
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396307 exited
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396310 exited
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396311 exited
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396313 exited
[2025-11-03 20:26:07 +0200] [396278] [INFO] Shutting down: Master
[2025-11-03 20:26:07 +0200] [396278] [INFO] ============================================================
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Trasabilitate Application - Shutting Down
[2025-11-03 20:26:07 +0200] [396278] [INFO] ============================================================
[2025-11-03 20:26:14 +0200] [396699] [INFO] Starting gunicorn 23.0.0
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🚀 Trasabilitate Application - Starting Server
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
[2025-11-03 20:26:14 +0200] [396699] [INFO] 📍 Configuration:
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Workers: 9
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Worker Class: sync
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Timeout: 120s
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Bind: 0.0.0.0:8781
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Preload App: True
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Max Requests: 1000 (+/- 100)
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
[2025-11-03 20:26:14 +0200] [396699] [INFO] Listening at: http://0.0.0.0:8781 (396699)
[2025-11-03 20:26:14 +0200] [396699] [INFO] Using worker: sync
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
[2025-11-03 20:26:14 +0200] [396699] [INFO] ✅ Trasabilitate Application Server is READY!
[2025-11-03 20:26:14 +0200] [396699] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:26:14 +0200] [396708] [INFO] Booting worker with pid: 396708
[2025-11-03 20:26:14 +0200] [396708] [INFO] ✨ Worker spawned successfully (pid: 396708)
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:26:14 +0200] [396709] [INFO] Booting worker with pid: 396709
[2025-11-03 20:26:14 +0200] [396709] [INFO] ✨ Worker spawned successfully (pid: 396709)
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:26:14 +0200] [396710] [INFO] Booting worker with pid: 396710
[2025-11-03 20:26:14 +0200] [396710] [INFO] ✨ Worker spawned successfully (pid: 396710)
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:26:14 +0200] [396711] [INFO] Booting worker with pid: 396711
[2025-11-03 20:26:14 +0200] [396711] [INFO] ✨ Worker spawned successfully (pid: 396711)
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:26:14 +0200] [396712] [INFO] Booting worker with pid: 396712
[2025-11-03 20:26:14 +0200] [396712] [INFO] ✨ Worker spawned successfully (pid: 396712)
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:26:14 +0200] [396713] [INFO] Booting worker with pid: 396713
[2025-11-03 20:26:14 +0200] [396713] [INFO] ✨ Worker spawned successfully (pid: 396713)
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:26:14 +0200] [396714] [INFO] Booting worker with pid: 396714
[2025-11-03 20:26:14 +0200] [396714] [INFO] ✨ Worker spawned successfully (pid: 396714)
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:26:14 +0200] [396719] [INFO] Booting worker with pid: 396719
[2025-11-03 20:26:14 +0200] [396719] [INFO] ✨ Worker spawned successfully (pid: 396719)
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:26:14 +0200] [396725] [INFO] Booting worker with pid: 396725
[2025-11-03 20:26:14 +0200] [396725] [INFO] ✨ Worker spawned successfully (pid: 396725)
Session user: superadmin superadmin
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
[2025-11-03 20:29:44 +0200] [396699] [INFO] Handling signal: term
[2025-11-03 20:29:44 +0200] [396710] [INFO] Worker exiting (pid: 396710)
[2025-11-03 20:29:44 +0200] [396709] [INFO] Worker exiting (pid: 396709)
[2025-11-03 20:29:44 +0200] [396708] [INFO] Worker exiting (pid: 396708)
[2025-11-03 20:29:44 +0200] [396711] [INFO] Worker exiting (pid: 396711)
[2025-11-03 20:29:44 +0200] [396712] [INFO] Worker exiting (pid: 396712)
[2025-11-03 20:29:44 +0200] [396713] [INFO] Worker exiting (pid: 396713)
[2025-11-03 20:29:44 +0200] [396714] [INFO] Worker exiting (pid: 396714)
[2025-11-03 20:29:44 +0200] [396719] [INFO] Worker exiting (pid: 396719)
[2025-11-03 20:29:44 +0200] [396725] [INFO] Worker exiting (pid: 396725)
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396709 exited
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396708 exited
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396711 exited
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396713 exited
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396714 exited
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396719 exited
[2025-11-03 20:29:46 +0200] [396699] [INFO] 👋 Worker 396710 exited
[2025-11-03 20:29:46 +0200] [396699] [INFO] 👋 Worker 396725 exited
[2025-11-03 20:29:46 +0200] [396699] [INFO] 👋 Worker 396712 exited
[2025-11-03 20:29:46 +0200] [396699] [INFO] Shutting down: Master
[2025-11-03 20:29:46 +0200] [396699] [INFO] ============================================================
[2025-11-03 20:29:46 +0200] [396699] [INFO] 👋 Trasabilitate Application - Shutting Down
[2025-11-03 20:29:46 +0200] [396699] [INFO] ============================================================
[2025-11-03 20:29:52 +0200] [397053] [INFO] Starting gunicorn 23.0.0
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🚀 Trasabilitate Application - Starting Server
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
[2025-11-03 20:29:52 +0200] [397053] [INFO] 📍 Configuration:
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Workers: 9
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Worker Class: sync
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Timeout: 120s
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Bind: 0.0.0.0:8781
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Preload App: True
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Max Requests: 1000 (+/- 100)
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
[2025-11-03 20:29:52 +0200] [397053] [INFO] Listening at: http://0.0.0.0:8781 (397053)
[2025-11-03 20:29:52 +0200] [397053] [INFO] Using worker: sync
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
[2025-11-03 20:29:52 +0200] [397053] [INFO] ✅ Trasabilitate Application Server is READY!
[2025-11-03 20:29:52 +0200] [397053] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:29:52 +0200] [397076] [INFO] Booting worker with pid: 397076
[2025-11-03 20:29:52 +0200] [397076] [INFO] ✨ Worker spawned successfully (pid: 397076)
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:29:52 +0200] [397077] [INFO] Booting worker with pid: 397077
[2025-11-03 20:29:52 +0200] [397077] [INFO] ✨ Worker spawned successfully (pid: 397077)
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:29:52 +0200] [397078] [INFO] Booting worker with pid: 397078
[2025-11-03 20:29:52 +0200] [397078] [INFO] ✨ Worker spawned successfully (pid: 397078)
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:29:52 +0200] [397079] [INFO] Booting worker with pid: 397079
[2025-11-03 20:29:52 +0200] [397079] [INFO] ✨ Worker spawned successfully (pid: 397079)
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:29:52 +0200] [397080] [INFO] Booting worker with pid: 397080
[2025-11-03 20:29:52 +0200] [397080] [INFO] ✨ Worker spawned successfully (pid: 397080)
[2025-11-03 20:29:53 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:29:53 +0200] [397081] [INFO] Booting worker with pid: 397081
[2025-11-03 20:29:53 +0200] [397081] [INFO] ✨ Worker spawned successfully (pid: 397081)
[2025-11-03 20:29:53 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:29:53 +0200] [397082] [INFO] Booting worker with pid: 397082
[2025-11-03 20:29:53 +0200] [397082] [INFO] ✨ Worker spawned successfully (pid: 397082)
[2025-11-03 20:29:53 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:29:53 +0200] [397083] [INFO] Booting worker with pid: 397083
[2025-11-03 20:29:53 +0200] [397083] [INFO] ✨ Worker spawned successfully (pid: 397083)
[2025-11-03 20:29:53 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:29:53 +0200] [397084] [INFO] Booting worker with pid: 397084
[2025-11-03 20:29:53 +0200] [397084] [INFO] ✨ Worker spawned successfully (pid: 397084)
Session user: superadmin superadmin
Session user: superadmin superadmin
[2025-11-03 20:35:12 +0200] [397053] [INFO] Handling signal: term
[2025-11-03 20:35:12 +0200] [397078] [INFO] Worker exiting (pid: 397078)
[2025-11-03 20:35:12 +0200] [397076] [INFO] Worker exiting (pid: 397076)
[2025-11-03 20:35:12 +0200] [397079] [INFO] Worker exiting (pid: 397079)
[2025-11-03 20:35:12 +0200] [397081] [INFO] Worker exiting (pid: 397081)
[2025-11-03 20:35:12 +0200] [397077] [INFO] Worker exiting (pid: 397077)
[2025-11-03 20:35:12 +0200] [397082] [INFO] Worker exiting (pid: 397082)
[2025-11-03 20:35:12 +0200] [397083] [INFO] Worker exiting (pid: 397083)
[2025-11-03 20:35:12 +0200] [397080] [INFO] Worker exiting (pid: 397080)
[2025-11-03 20:35:12 +0200] [397084] [INFO] Worker exiting (pid: 397084)
[2025-11-03 20:35:13 +0200] [397053] [INFO] 👋 Worker 397081 exited
[2025-11-03 20:35:13 +0200] [397053] [INFO] 👋 Worker 397079 exited
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397076 exited
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397083 exited
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397084 exited
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397077 exited
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397078 exited
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397082 exited
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397080 exited
[2025-11-03 20:35:14 +0200] [397053] [INFO] Shutting down: Master
[2025-11-03 20:35:14 +0200] [397053] [INFO] ============================================================
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Trasabilitate Application - Shutting Down
[2025-11-03 20:35:14 +0200] [397053] [INFO] ============================================================
[2025-11-03 20:35:21 +0200] [397553] [INFO] Starting gunicorn 23.0.0
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🚀 Trasabilitate Application - Starting Server
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
[2025-11-03 20:35:21 +0200] [397553] [INFO] 📍 Configuration:
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Workers: 9
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Worker Class: sync
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Timeout: 120s
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Bind: 0.0.0.0:8781
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Preload App: True
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Max Requests: 1000 (+/- 100)
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
[2025-11-03 20:35:21 +0200] [397553] [INFO] Listening at: http://0.0.0.0:8781 (397553)
[2025-11-03 20:35:21 +0200] [397553] [INFO] Using worker: sync
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
[2025-11-03 20:35:21 +0200] [397553] [INFO] ✅ Trasabilitate Application Server is READY!
[2025-11-03 20:35:21 +0200] [397553] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:35:21 +0200] [397580] [INFO] Booting worker with pid: 397580
[2025-11-03 20:35:21 +0200] [397580] [INFO] ✨ Worker spawned successfully (pid: 397580)
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:35:21 +0200] [397581] [INFO] Booting worker with pid: 397581
[2025-11-03 20:35:21 +0200] [397581] [INFO] ✨ Worker spawned successfully (pid: 397581)
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:35:21 +0200] [397582] [INFO] Booting worker with pid: 397582
[2025-11-03 20:35:21 +0200] [397582] [INFO] ✨ Worker spawned successfully (pid: 397582)
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:35:21 +0200] [397583] [INFO] Booting worker with pid: 397583
[2025-11-03 20:35:21 +0200] [397583] [INFO] ✨ Worker spawned successfully (pid: 397583)
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:35:21 +0200] [397584] [INFO] Booting worker with pid: 397584
[2025-11-03 20:35:21 +0200] [397584] [INFO] ✨ Worker spawned successfully (pid: 397584)
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:35:21 +0200] [397585] [INFO] Booting worker with pid: 397585
[2025-11-03 20:35:21 +0200] [397585] [INFO] ✨ Worker spawned successfully (pid: 397585)
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:35:21 +0200] [397586] [INFO] Booting worker with pid: 397586
[2025-11-03 20:35:21 +0200] [397586] [INFO] ✨ Worker spawned successfully (pid: 397586)
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:35:21 +0200] [397587] [INFO] Booting worker with pid: 397587
[2025-11-03 20:35:21 +0200] [397587] [INFO] ✨ Worker spawned successfully (pid: 397587)
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:35:21 +0200] [397588] [INFO] Booting worker with pid: 397588
[2025-11-03 20:35:21 +0200] [397588] [INFO] ✨ Worker spawned successfully (pid: 397588)
[2025-11-03 20:39:58 +0200] [397553] [INFO] Handling signal: term
[2025-11-03 20:39:58 +0200] [397580] [INFO] Worker exiting (pid: 397580)
[2025-11-03 20:39:58 +0200] [397581] [INFO] Worker exiting (pid: 397581)
[2025-11-03 20:39:58 +0200] [397584] [INFO] Worker exiting (pid: 397584)
[2025-11-03 20:39:58 +0200] [397582] [INFO] Worker exiting (pid: 397582)
[2025-11-03 20:39:58 +0200] [397585] [INFO] Worker exiting (pid: 397585)
[2025-11-03 20:39:58 +0200] [397587] [INFO] Worker exiting (pid: 397587)
[2025-11-03 20:39:58 +0200] [397583] [INFO] Worker exiting (pid: 397583)
[2025-11-03 20:39:58 +0200] [397586] [INFO] Worker exiting (pid: 397586)
[2025-11-03 20:39:58 +0200] [397588] [INFO] Worker exiting (pid: 397588)
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397581 exited
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397585 exited
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397580 exited
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397588 exited
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397583 exited
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397586 exited
[2025-11-03 20:40:00 +0200] [397553] [INFO] 👋 Worker 397582 exited
[2025-11-03 20:40:00 +0200] [397553] [INFO] 👋 Worker 397584 exited
[2025-11-03 20:40:00 +0200] [397553] [INFO] 👋 Worker 397587 exited
[2025-11-03 20:40:00 +0200] [397553] [INFO] Shutting down: Master
[2025-11-03 20:40:00 +0200] [397553] [INFO] ============================================================
[2025-11-03 20:40:00 +0200] [397553] [INFO] 👋 Trasabilitate Application - Shutting Down
[2025-11-03 20:40:00 +0200] [397553] [INFO] ============================================================
Traceback (most recent call last):
File "/srv/quality_recticel/recticel/bin/gunicorn", line 8, in <module>
sys.exit(run())
~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 66, in run
WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]", prog=prog).run()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 235, in run
super().run()
~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 71, in run
Arbiter(self).run()
~~~~~~~^^^^^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 57, in __init__
self.setup(app)
~~~~~~~~~~^^^^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 117, in setup
self.app.wsgi()
~~~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 66, in wsgi
self.callable = self.load()
~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 57, in load
return self.load_wsgiapp()
~~~~~~~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 47, in load_wsgiapp
return util.import_app(self.app_uri)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/util.py", line 370, in import_app
mod = importlib.import_module(module)
File "/usr/lib/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/srv/quality_app/py_app/wsgi.py", line 15, in <module>
application = create_app()
File "/srv/quality_app/py_app/app/__init__.py", line 11, in create_app
from app.routes import bp as main_bp, warehouse_bp
File "/srv/quality_app/py_app/app/routes.py", line 6, in <module>
from .models import User
File "/srv/quality_app/py_app/app/models.py", line 1, in <module>
from . import db
ImportError: cannot import name 'db' from 'app' (/srv/quality_app/py_app/app/__init__.py)
Traceback (most recent call last):
File "/srv/quality_recticel/recticel/bin/gunicorn", line 8, in <module>
sys.exit(run())
~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 66, in run
WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]", prog=prog).run()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 235, in run
super().run()
~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 71, in run
Arbiter(self).run()
~~~~~~~^^^^^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 57, in __init__
self.setup(app)
~~~~~~~~~~^^^^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 117, in setup
self.app.wsgi()
~~~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 66, in wsgi
self.callable = self.load()
~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 57, in load
return self.load_wsgiapp()
~~~~~~~~~~~~~~~~~^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 47, in load_wsgiapp
return util.import_app(self.app_uri)
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/util.py", line 370, in import_app
mod = importlib.import_module(module)
File "/usr/lib/python3.13/importlib/__init__.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/srv/quality_app/py_app/wsgi.py", line 15, in <module>
application = create_app()
File "/srv/quality_app/py_app/app/__init__.py", line 11, in create_app
from app.routes import bp as main_bp, warehouse_bp
File "/srv/quality_app/py_app/app/routes.py", line 10, in <module>
from app.settings import (
...<10 lines>...
)
File "/srv/quality_app/py_app/app/settings.py", line 2, in <module>
from .models import User
File "/srv/quality_app/py_app/app/models.py", line 1, in <module>
from . import db
ImportError: cannot import name 'db' from 'app' (/srv/quality_app/py_app/app/__init__.py)
[2025-11-03 20:41:50 +0200] [398202] [INFO] Starting gunicorn 23.0.0
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🚀 Trasabilitate Application - Starting Server
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
[2025-11-03 20:41:50 +0200] [398202] [INFO] 📍 Configuration:
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Workers: 9
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Worker Class: sync
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Timeout: 120s
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Bind: 0.0.0.0:8781
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Preload App: True
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Max Requests: 1000 (+/- 100)
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
[2025-11-03 20:41:50 +0200] [398202] [INFO] Listening at: http://0.0.0.0:8781 (398202)
[2025-11-03 20:41:50 +0200] [398202] [INFO] Using worker: sync
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
[2025-11-03 20:41:50 +0200] [398202] [INFO] ✅ Trasabilitate Application Server is READY!
[2025-11-03 20:41:50 +0200] [398202] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:41:50 +0200] [398224] [INFO] Booting worker with pid: 398224
[2025-11-03 20:41:50 +0200] [398224] [INFO] ✨ Worker spawned successfully (pid: 398224)
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:41:50 +0200] [398225] [INFO] Booting worker with pid: 398225
[2025-11-03 20:41:50 +0200] [398225] [INFO] ✨ Worker spawned successfully (pid: 398225)
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:41:50 +0200] [398226] [INFO] Booting worker with pid: 398226
[2025-11-03 20:41:50 +0200] [398226] [INFO] ✨ Worker spawned successfully (pid: 398226)
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:41:50 +0200] [398227] [INFO] Booting worker with pid: 398227
[2025-11-03 20:41:50 +0200] [398227] [INFO] ✨ Worker spawned successfully (pid: 398227)
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:41:51 +0200] [398228] [INFO] Booting worker with pid: 398228
[2025-11-03 20:41:51 +0200] [398228] [INFO] ✨ Worker spawned successfully (pid: 398228)
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:41:51 +0200] [398235] [INFO] Booting worker with pid: 398235
[2025-11-03 20:41:51 +0200] [398235] [INFO] ✨ Worker spawned successfully (pid: 398235)
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:41:51 +0200] [398236] [INFO] Booting worker with pid: 398236
[2025-11-03 20:41:51 +0200] [398236] [INFO] ✨ Worker spawned successfully (pid: 398236)
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:41:51 +0200] [398237] [INFO] Booting worker with pid: 398237
[2025-11-03 20:41:51 +0200] [398237] [INFO] ✨ Worker spawned successfully (pid: 398237)
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:41:51 +0200] [398238] [INFO] Booting worker with pid: 398238
[2025-11-03 20:41:51 +0200] [398238] [INFO] ✨ Worker spawned successfully (pid: 398238)
Session user: superadmin superadmin
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
All form data received: {'username': 'superadmin', 'password': 'Vanessa_13/05'}
Raw form input: 'superadmin' 'Vanessa_13/05'
External DB query result (with modules): ('superadmin', 'Vanessa_13/05', 'superadmin', 'quality,warehouse,labels,daily_mirror')
Logged in as: superadmin superadmin modules: ['quality', 'warehouse', 'labels']
Session user: superadmin superadmin
Error loading Daily Mirror main page: daily_mirror_main.html
Session user: superadmin superadmin
Session user: superadmin superadmin
Session user: superadmin superadmin
[2025-11-03 20:45:46 +0200] [398202] [INFO] Handling signal: term
[2025-11-03 20:45:46 +0200] [398226] [INFO] Worker exiting (pid: 398226)
[2025-11-03 20:45:46 +0200] [398225] [INFO] Worker exiting (pid: 398225)
[2025-11-03 20:45:46 +0200] [398224] [INFO] Worker exiting (pid: 398224)
[2025-11-03 20:45:46 +0200] [398237] [INFO] Worker exiting (pid: 398237)
[2025-11-03 20:45:46 +0200] [398228] [INFO] Worker exiting (pid: 398228)
[2025-11-03 20:45:46 +0200] [398236] [INFO] Worker exiting (pid: 398236)
[2025-11-03 20:45:46 +0200] [398227] [INFO] Worker exiting (pid: 398227)
[2025-11-03 20:45:46 +0200] [398235] [INFO] Worker exiting (pid: 398235)
[2025-11-03 20:45:46 +0200] [398238] [INFO] Worker exiting (pid: 398238)
[2025-11-03 20:45:46 +0200] [398202] [INFO] 👋 Worker 398237 exited
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398238 exited
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398226 exited
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398235 exited
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398225 exited
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398236 exited
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398228 exited
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398224 exited
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398227 exited
[2025-11-03 20:45:47 +0200] [398202] [INFO] Shutting down: Master
[2025-11-03 20:45:47 +0200] [398202] [INFO] ============================================================
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Trasabilitate Application - Shutting Down
[2025-11-03 20:45:47 +0200] [398202] [INFO] ============================================================
[2025-11-03 20:45:53 +0200] [398661] [INFO] Starting gunicorn 23.0.0
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
[2025-11-03 20:45:53 +0200] [398661] [INFO] 🚀 Trasabilitate Application - Starting Server
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
[2025-11-03 20:45:53 +0200] [398661] [INFO] 📍 Configuration:
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Workers: 9
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Worker Class: sync
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Timeout: 120s
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Bind: 0.0.0.0:8781
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Preload App: True
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Max Requests: 1000 (+/- 100)
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
[2025-11-03 20:45:53 +0200] [398661] [INFO] Listening at: http://0.0.0.0:8781 (398661)
[2025-11-03 20:45:53 +0200] [398661] [INFO] Using worker: sync
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
[2025-11-03 20:45:53 +0200] [398661] [INFO] ✅ Trasabilitate Application Server is READY!
[2025-11-03 20:45:53 +0200] [398661] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
[2025-11-03 20:45:53 +0200] [398661] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
[2025-11-03 20:45:53 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:45:53 +0200] [398683] [INFO] Booting worker with pid: 398683
[2025-11-03 20:45:53 +0200] [398683] [INFO] ✨ Worker spawned successfully (pid: 398683)
[2025-11-03 20:45:53 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:45:53 +0200] [398684] [INFO] Booting worker with pid: 398684
[2025-11-03 20:45:53 +0200] [398684] [INFO] ✨ Worker spawned successfully (pid: 398684)
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:45:54 +0200] [398685] [INFO] Booting worker with pid: 398685
[2025-11-03 20:45:54 +0200] [398685] [INFO] ✨ Worker spawned successfully (pid: 398685)
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:45:54 +0200] [398686] [INFO] Booting worker with pid: 398686
[2025-11-03 20:45:54 +0200] [398686] [INFO] ✨ Worker spawned successfully (pid: 398686)
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:45:54 +0200] [398687] [INFO] Booting worker with pid: 398687
[2025-11-03 20:45:54 +0200] [398687] [INFO] ✨ Worker spawned successfully (pid: 398687)
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:45:54 +0200] [398690] [INFO] Booting worker with pid: 398690
[2025-11-03 20:45:54 +0200] [398690] [INFO] ✨ Worker spawned successfully (pid: 398690)
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:45:54 +0200] [398695] [INFO] Booting worker with pid: 398695
[2025-11-03 20:45:54 +0200] [398695] [INFO] ✨ Worker spawned successfully (pid: 398695)
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:45:54 +0200] [398696] [INFO] Booting worker with pid: 398696
[2025-11-03 20:45:54 +0200] [398696] [INFO] ✨ Worker spawned successfully (pid: 398696)
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 20:45:54 +0200] [398697] [INFO] Booting worker with pid: 398697
[2025-11-03 20:45:54 +0200] [398697] [INFO] ✨ Worker spawned successfully (pid: 398697)
[2025-11-03 21:06:13 +0200] [398661] [INFO] Handling signal: term
[2025-11-03 21:06:13 +0200] [398683] [INFO] Worker exiting (pid: 398683)
[2025-11-03 21:06:13 +0200] [398684] [INFO] Worker exiting (pid: 398684)
[2025-11-03 21:06:13 +0200] [398685] [INFO] Worker exiting (pid: 398685)
[2025-11-03 21:06:13 +0200] [398686] [INFO] Worker exiting (pid: 398686)
[2025-11-03 21:06:13 +0200] [398687] [INFO] Worker exiting (pid: 398687)
[2025-11-03 21:06:13 +0200] [398695] [INFO] Worker exiting (pid: 398695)
[2025-11-03 21:06:13 +0200] [398690] [INFO] Worker exiting (pid: 398690)
[2025-11-03 21:06:13 +0200] [398696] [INFO] Worker exiting (pid: 398696)
[2025-11-03 21:06:13 +0200] [398697] [INFO] Worker exiting (pid: 398697)
[2025-11-03 21:06:13 +0200] [398661] [INFO] 👋 Worker 398683 exited
[2025-11-03 21:06:13 +0200] [398661] [INFO] 👋 Worker 398686 exited
[2025-11-03 21:06:13 +0200] [398661] [INFO] 👋 Worker 398697 exited
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398685 exited
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398695 exited
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398684 exited
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398690 exited
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398696 exited
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398687 exited
[2025-11-03 21:06:14 +0200] [398661] [INFO] Shutting down: Master
[2025-11-03 21:06:14 +0200] [398661] [INFO] ============================================================
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Trasabilitate Application - Shutting Down
[2025-11-03 21:06:14 +0200] [398661] [INFO] ============================================================
[2025-11-03 21:06:20 +0200] [399048] [INFO] Starting gunicorn 23.0.0
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🚀 Trasabilitate Application - Starting Server
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
[2025-11-03 21:06:20 +0200] [399048] [INFO] 📍 Configuration:
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Workers: 9
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Worker Class: sync
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Timeout: 120s
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Bind: 0.0.0.0:8781
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Preload App: True
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Max Requests: 1000 (+/- 100)
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
[2025-11-03 21:06:20 +0200] [399048] [INFO] Listening at: http://0.0.0.0:8781 (399048)
[2025-11-03 21:06:20 +0200] [399048] [INFO] Using worker: sync
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
[2025-11-03 21:06:20 +0200] [399048] [INFO] ✅ Trasabilitate Application Server is READY!
[2025-11-03 21:06:20 +0200] [399048] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 21:06:20 +0200] [399070] [INFO] Booting worker with pid: 399070
[2025-11-03 21:06:20 +0200] [399070] [INFO] ✨ Worker spawned successfully (pid: 399070)
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 21:06:20 +0200] [399071] [INFO] Booting worker with pid: 399071
[2025-11-03 21:06:20 +0200] [399071] [INFO] ✨ Worker spawned successfully (pid: 399071)
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 21:06:20 +0200] [399072] [INFO] Booting worker with pid: 399072
[2025-11-03 21:06:20 +0200] [399072] [INFO] ✨ Worker spawned successfully (pid: 399072)
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 21:06:20 +0200] [399073] [INFO] Booting worker with pid: 399073
[2025-11-03 21:06:20 +0200] [399073] [INFO] ✨ Worker spawned successfully (pid: 399073)
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 21:06:20 +0200] [399076] [INFO] Booting worker with pid: 399076
[2025-11-03 21:06:20 +0200] [399076] [INFO] ✨ Worker spawned successfully (pid: 399076)
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 21:06:20 +0200] [399080] [INFO] Booting worker with pid: 399080
[2025-11-03 21:06:20 +0200] [399080] [INFO] ✨ Worker spawned successfully (pid: 399080)
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 21:06:21 +0200] [399082] [INFO] Booting worker with pid: 399082
[2025-11-03 21:06:21 +0200] [399082] [INFO] ✨ Worker spawned successfully (pid: 399082)
[2025-11-03 21:06:21 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 21:06:21 +0200] [399083] [INFO] Booting worker with pid: 399083
[2025-11-03 21:06:21 +0200] [399083] [INFO] ✨ Worker spawned successfully (pid: 399083)
[2025-11-03 21:06:21 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
[2025-11-03 21:06:21 +0200] [399084] [INFO] Booting worker with pid: 399084
[2025-11-03 21:06:21 +0200] [399084] [INFO] ✨ Worker spawned successfully (pid: 399084)
Backup directory ensured: /srv/quality_app/backups
Backup directory ensured: /srv/quality_app/backups

View File

@@ -1,16 +1,12 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
# Application uses direct MariaDB connections via external_server.conf
# No SQLAlchemy ORM needed - all database operations use raw SQL
from app.routes import bp as main_bp, warehouse_bp
from app.daily_mirror import daily_mirror_bp
@@ -21,7 +17,4 @@ def create_app():
# Add 'now' function to Jinja2 globals
app.jinja_env.globals['now'] = datetime.now
with app.app_context():
db.create_all() # Create database tables if they don't exist
return app

View File

@@ -41,8 +41,8 @@ def requires_role(min_role_level=None, required_modules=None, page=None):
# Module requirement checking
if required_modules:
if user_role in ['superadmin', 'admin']:
# Superadmin and admin have access to all modules
if user_role == 'superadmin':
# Superadmin has access to all modules
pass
else:
if not any(module in user_modules for module in required_modules):
@@ -77,6 +77,10 @@ def requires_labels_module(f):
"""Decorator for labels module access"""
return requires_role(required_modules=['labels'])(f)
def requires_daily_mirror_module(f):
"""Decorator for daily mirror module access"""
return requires_role(required_modules=['daily_mirror'])(f)
def quality_manager_plus(f):
"""Decorator for quality module manager+ access"""
return requires_role(min_role_level=70, required_modules=['quality'])(f)
@@ -87,4 +91,8 @@ def warehouse_manager_plus(f):
def labels_manager_plus(f):
"""Decorator for labels module manager+ access"""
return requires_role(min_role_level=70, required_modules=['labels'])(f)
return requires_role(min_role_level=70, required_modules=['labels'])(f)
def daily_mirror_manager_plus(f):
"""Decorator for daily mirror module manager+ access"""
return requires_role(min_role_level=70, required_modules=['daily_mirror'])(f)

View File

@@ -26,10 +26,15 @@ def check_daily_mirror_access():
flash('Please log in to access this page.')
return redirect(url_for('main.login'))
# Check if user has admin+ access
# Superadmin has access to everything
user_role = session.get('role', '')
if user_role not in ['superadmin', 'admin']:
flash('Access denied: Admin privileges required for Daily Mirror.')
if user_role == 'superadmin':
return None # Access granted
# Check if user has daily_mirror module access
user_modules = session.get('modules', [])
if 'daily_mirror' not in user_modules:
flash('Access denied: Daily Mirror module access required.')
return redirect(url_for('main.dashboard'))
return None # Access granted
@@ -37,13 +42,19 @@ def check_daily_mirror_access():
def check_daily_mirror_api_access():
"""Helper function to check API access for Daily Mirror"""
# Check if user is logged in and has admin+ access
# Check if user is logged in
if 'user' not in session:
return jsonify({'error': 'Authentication required'}), 401
# Superadmin has access to everything
user_role = session.get('role', '')
if user_role not in ['superadmin', 'admin']:
return jsonify({'error': 'Admin privileges required'}), 403
if user_role == 'superadmin':
return None # Access granted
# Check if user has daily_mirror module access
user_modules = session.get('modules', [])
if 'daily_mirror' not in user_modules:
return jsonify({'error': 'Daily Mirror module access required'}), 403
return None # Access granted

View File

@@ -0,0 +1,431 @@
"""
Database Backup Management Module
Quality Recticel Application
This module provides functionality for backing up and restoring the MariaDB database,
including scheduled backups, manual backups, and backup file management.
"""
import os
import subprocess
import json
from datetime import datetime, timedelta
from pathlib import Path
import configparser
from flask import current_app
import mariadb
class DatabaseBackupManager:
"""Manages database backup operations"""
def __init__(self):
"""Initialize the backup manager with configuration from external_server.conf"""
self.config = self._load_database_config()
self.backup_path = self._get_backup_path()
self._ensure_backup_directory()
def _load_database_config(self):
"""Load database configuration from external_server.conf"""
try:
settings_file = os.path.join(current_app.instance_path, 'external_server.conf')
config = {}
if os.path.exists(settings_file):
with open(settings_file, 'r') as f:
for line in f:
if '=' in line:
key, value = line.strip().split('=', 1)
config[key] = value
return {
'host': config.get('server_domain', 'localhost'),
'port': config.get('port', '3306'),
'database': config.get('database_name', 'trasabilitate'),
'user': config.get('username', 'trasabilitate'),
'password': config.get('password', '')
}
except Exception as e:
print(f"Error loading database config: {e}")
return None
def _get_backup_path(self):
"""Get backup path from environment or use default"""
# Check environment variable (set in docker-compose)
backup_path = os.environ.get('BACKUP_PATH', '/srv/quality_app/backups')
# Check if custom path is set in config
try:
settings_file = os.path.join(current_app.instance_path, 'external_server.conf')
if os.path.exists(settings_file):
with open(settings_file, 'r') as f:
for line in f:
if line.startswith('backup_path='):
backup_path = line.strip().split('=', 1)[1]
break
except Exception as e:
print(f"Error reading backup path from config: {e}")
return backup_path
def _ensure_backup_directory(self):
"""Ensure backup directory exists"""
try:
Path(self.backup_path).mkdir(parents=True, exist_ok=True)
print(f"Backup directory ensured: {self.backup_path}")
except Exception as e:
print(f"Error creating backup directory: {e}")
def create_backup(self, backup_name=None):
"""
Create a complete backup of the database
Args:
backup_name (str, optional): Custom name for the backup file
Returns:
dict: Result with success status, message, and backup file path
"""
try:
if not self.config:
return {
'success': False,
'message': 'Database configuration not loaded'
}
# Generate backup filename
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
if backup_name:
filename = f"{backup_name}_{timestamp}.sql"
else:
filename = f"backup_{self.config['database']}_{timestamp}.sql"
backup_file = os.path.join(self.backup_path, filename)
# Build mysqldump command
cmd = [
'mysqldump',
f"--host={self.config['host']}",
f"--port={self.config['port']}",
f"--user={self.config['user']}",
f"--password={self.config['password']}",
'--single-transaction',
'--routines',
'--triggers',
'--events',
'--add-drop-database',
'--databases',
self.config['database']
]
# Execute mysqldump and save to file
with open(backup_file, 'w') as f:
result = subprocess.run(
cmd,
stdout=f,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
# Get file size
file_size = os.path.getsize(backup_file)
file_size_mb = file_size / (1024 * 1024)
# Save backup metadata
self._save_backup_metadata(filename, file_size)
return {
'success': True,
'message': f'Backup created successfully',
'filename': filename,
'file_path': backup_file,
'size': f"{file_size_mb:.2f} MB",
'timestamp': timestamp
}
else:
error_msg = result.stderr
print(f"Backup error: {error_msg}")
return {
'success': False,
'message': f'Backup failed: {error_msg}'
}
except Exception as e:
print(f"Exception during backup: {e}")
return {
'success': False,
'message': f'Backup failed: {str(e)}'
}
def _save_backup_metadata(self, filename, file_size):
"""Save metadata about the backup"""
try:
metadata_file = os.path.join(self.backup_path, 'backups_metadata.json')
# Load existing metadata
metadata = []
if os.path.exists(metadata_file):
with open(metadata_file, 'r') as f:
metadata = json.load(f)
# Add new backup metadata
metadata.append({
'filename': filename,
'size': file_size,
'timestamp': datetime.now().isoformat(),
'database': self.config['database']
})
# Save updated metadata
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2)
except Exception as e:
print(f"Error saving backup metadata: {e}")
def list_backups(self):
"""
List all available backups
Returns:
list: List of backup information dictionaries
"""
try:
backups = []
# Get all .sql files in backup directory
if os.path.exists(self.backup_path):
for filename in os.listdir(self.backup_path):
if filename.endswith('.sql'):
file_path = os.path.join(self.backup_path, filename)
file_stat = os.stat(file_path)
backups.append({
'filename': filename,
'size': file_stat.st_size,
'size_mb': f"{file_stat.st_size / (1024 * 1024):.2f}",
'created': datetime.fromtimestamp(file_stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S'),
'timestamp': file_stat.st_ctime
})
# Sort by timestamp (newest first)
backups.sort(key=lambda x: x['timestamp'], reverse=True)
return backups
except Exception as e:
print(f"Error listing backups: {e}")
return []
def delete_backup(self, filename):
"""
Delete a backup file
Args:
filename (str): Name of the backup file to delete
Returns:
dict: Result with success status and message
"""
try:
# Security: ensure filename doesn't contain path traversal
if '..' in filename or '/' in filename:
return {
'success': False,
'message': 'Invalid filename'
}
file_path = os.path.join(self.backup_path, filename)
if os.path.exists(file_path):
os.remove(file_path)
# Update metadata
self._remove_backup_metadata(filename)
return {
'success': True,
'message': f'Backup {filename} deleted successfully'
}
else:
return {
'success': False,
'message': 'Backup file not found'
}
except Exception as e:
print(f"Error deleting backup: {e}")
return {
'success': False,
'message': f'Failed to delete backup: {str(e)}'
}
def _remove_backup_metadata(self, filename):
"""Remove metadata entry for deleted backup"""
try:
metadata_file = os.path.join(self.backup_path, 'backups_metadata.json')
if os.path.exists(metadata_file):
with open(metadata_file, 'r') as f:
metadata = json.load(f)
# Filter out the deleted backup
metadata = [m for m in metadata if m['filename'] != filename]
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2)
except Exception as e:
print(f"Error removing backup metadata: {e}")
def restore_backup(self, filename):
"""
Restore database from a backup file
Args:
filename (str): Name of the backup file to restore
Returns:
dict: Result with success status and message
"""
try:
# Security: ensure filename doesn't contain path traversal
if '..' in filename or '/' in filename:
return {
'success': False,
'message': 'Invalid filename'
}
file_path = os.path.join(self.backup_path, filename)
if not os.path.exists(file_path):
return {
'success': False,
'message': 'Backup file not found'
}
# Build mysql restore command
cmd = [
'mysql',
f"--host={self.config['host']}",
f"--port={self.config['port']}",
f"--user={self.config['user']}",
f"--password={self.config['password']}"
]
# Execute mysql restore
with open(file_path, 'r') as f:
result = subprocess.run(
cmd,
stdin=f,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
return {
'success': True,
'message': f'Database restored successfully from {filename}'
}
else:
error_msg = result.stderr
print(f"Restore error: {error_msg}")
return {
'success': False,
'message': f'Restore failed: {error_msg}'
}
except Exception as e:
print(f"Exception during restore: {e}")
return {
'success': False,
'message': f'Restore failed: {str(e)}'
}
def get_backup_schedule(self):
"""Get current backup schedule configuration"""
try:
schedule_file = os.path.join(self.backup_path, 'backup_schedule.json')
if os.path.exists(schedule_file):
with open(schedule_file, 'r') as f:
return json.load(f)
# Default schedule
return {
'enabled': False,
'time': '02:00', # 2 AM
'frequency': 'daily', # daily, weekly, monthly
'retention_days': 30 # Keep backups for 30 days
}
except Exception as e:
print(f"Error loading backup schedule: {e}")
return None
def save_backup_schedule(self, schedule):
"""
Save backup schedule configuration
Args:
schedule (dict): Schedule configuration
Returns:
dict: Result with success status and message
"""
try:
schedule_file = os.path.join(self.backup_path, 'backup_schedule.json')
with open(schedule_file, 'w') as f:
json.dump(schedule, f, indent=2)
return {
'success': True,
'message': 'Backup schedule saved successfully'
}
except Exception as e:
print(f"Error saving backup schedule: {e}")
return {
'success': False,
'message': f'Failed to save schedule: {str(e)}'
}
def cleanup_old_backups(self, retention_days=30):
"""
Delete backups older than retention_days
Args:
retention_days (int): Number of days to keep backups
Returns:
dict: Result with count of deleted backups
"""
try:
deleted_count = 0
cutoff_time = datetime.now() - timedelta(days=retention_days)
if os.path.exists(self.backup_path):
for filename in os.listdir(self.backup_path):
if filename.endswith('.sql'):
file_path = os.path.join(self.backup_path, filename)
file_time = datetime.fromtimestamp(os.path.getctime(file_path))
if file_time < cutoff_time:
os.remove(file_path)
self._remove_backup_metadata(filename)
deleted_count += 1
print(f"Deleted old backup: {filename}")
return {
'success': True,
'deleted_count': deleted_count,
'message': f'Cleaned up {deleted_count} old backup(s)'
}
except Exception as e:
print(f"Error cleaning up old backups: {e}")
return {
'success': False,
'message': f'Cleanup failed: {str(e)}'
}

View File

@@ -3,8 +3,6 @@ import os
import mariadb
from datetime import datetime, timedelta
from flask import Blueprint, render_template, redirect, url_for, request, flash, session, current_app, jsonify, send_from_directory
from .models import User
from . import db
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
import csv
@@ -94,9 +92,9 @@ def login():
except:
user_modules = []
# Superadmin and admin have access to all modules
if user['role'] in ['superadmin', 'admin']:
user_modules = ['quality', 'warehouse', 'labels']
# Superadmin has access to all modules
if user['role'] == 'superadmin':
user_modules = ['quality', 'warehouse', 'labels', 'daily_mirror']
session['modules'] = user_modules
print("Logged in as:", session.get('user'), session.get('role'), "modules:", user_modules)
@@ -3580,4 +3578,137 @@ def help(page='index'):
# "pdf_url": "https://your-linux-server/generate_labels_pdf/15",
# "printer_name": "default",
# "copies": 1
# }
# }
# ========================================
# DATABASE BACKUP MANAGEMENT ROUTES
# ========================================
@bp.route('/api/backup/create', methods=['POST'])
@admin_plus
def api_backup_create():
"""Create a new database backup"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
result = backup_manager.create_backup()
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'Backup failed: {str(e)}'
}), 500
@bp.route('/api/backup/list', methods=['GET'])
@admin_plus
def api_backup_list():
"""List all available backups"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
backups = backup_manager.list_backups()
return jsonify({
'success': True,
'backups': backups
})
except Exception as e:
return jsonify({
'success': False,
'message': f'Failed to list backups: {str(e)}'
}), 500
@bp.route('/api/backup/download/<filename>', methods=['GET'])
@admin_plus
def api_backup_download(filename):
"""Download a backup file"""
try:
from app.database_backup import DatabaseBackupManager
from flask import send_file
import os
backup_manager = DatabaseBackupManager()
backup_path = backup_manager.backup_path
file_path = os.path.join(backup_path, filename)
# Security: ensure filename doesn't contain path traversal
if '..' in filename or '/' in filename:
return jsonify({
'success': False,
'message': 'Invalid filename'
}), 400
if os.path.exists(file_path):
return send_file(file_path, as_attachment=True, download_name=filename)
else:
return jsonify({
'success': False,
'message': 'Backup file not found'
}), 404
except Exception as e:
return jsonify({
'success': False,
'message': f'Download failed: {str(e)}'
}), 500
@bp.route('/api/backup/delete/<filename>', methods=['DELETE'])
@admin_plus
def api_backup_delete(filename):
"""Delete a backup file"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
result = backup_manager.delete_backup(filename)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'Delete failed: {str(e)}'
}), 500
@bp.route('/api/backup/schedule', methods=['GET', 'POST'])
@admin_plus
def api_backup_schedule():
"""Get or save backup schedule configuration"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
if request.method == 'POST':
schedule = request.json
result = backup_manager.save_backup_schedule(schedule)
return jsonify(result)
else:
schedule = backup_manager.get_backup_schedule()
return jsonify({
'success': True,
'schedule': schedule
})
except Exception as e:
return jsonify({
'success': False,
'message': f'Schedule operation failed: {str(e)}'
}), 500
@bp.route('/api/backup/restore/<filename>', methods=['POST'])
@superadmin_only
def api_backup_restore(filename):
"""Restore database from a backup file (superadmin only)"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
result = backup_manager.restore_backup(filename)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'Restore failed: {str(e)}'
}), 500

View File

@@ -1,6 +1,4 @@
from flask import render_template, request, session, redirect, url_for, flash, current_app, jsonify
from .models import User
from . import db
from .permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role
import mariadb
import os

View File

@@ -0,0 +1,447 @@
{% extends "base.html" %}
{% block title %}Daily Mirror - Quality Recticel{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">📈 Daily Mirror</h1>
<p class="text-muted">Generate comprehensive daily production reports</p>
</div>
<div>
<a href="{{ url_for('daily_mirror.daily_mirror_history_route') }}" class="btn btn-outline-primary">
<i class="fas fa-history"></i> View History
</a>
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
</div>
</div>
<!-- Date Selection Card -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-calendar-alt"></i> Select Report Date
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<label for="reportDate" class="form-label">Report Date:</label>
<input type="date" class="form-control" id="reportDate" value="{{ today }}">
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="button" class="btn btn-primary" onclick="generateDailyReport()">
<i class="fas fa-chart-line"></i> Generate Report
</button>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="button" class="btn btn-success" onclick="setTodayDate()">
<i class="fas fa-calendar-day"></i> Today's Report
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="row mb-4" style="display: none;">
<div class="col-12">
<div class="card">
<div class="card-body text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 mb-0">Generating daily report...</p>
</div>
</div>
</div>
</div>
<!-- Daily Report Results -->
<div id="reportResults" style="display: none;">
<!-- Key Metrics Overview -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-tachometer-alt"></i> Daily Production Overview
<span id="reportDateDisplay" class="badge bg-primary ms-2"></span>
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="metric-card orders-quantity">
<div class="metric-icon">
<i class="fas fa-clipboard-list"></i>
</div>
<div class="metric-content">
<h3 id="ordersQuantity">-</h3>
<p>Orders Quantity</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card production-launched">
<div class="metric-icon">
<i class="fas fa-play-circle"></i>
</div>
<div class="metric-content">
<h3 id="productionLaunched">-</h3>
<p>Production Launched</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card production-finished">
<div class="metric-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="metric-content">
<h3 id="productionFinished">-</h3>
<p>Production Finished</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card orders-delivered">
<div class="metric-icon">
<i class="fas fa-truck"></i>
</div>
<div class="metric-content">
<h3 id="ordersDelivered">-</h3>
<p>Orders Delivered</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quality Control Metrics -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-search"></i> Quality Control Scans
</h5>
</div>
<div class="card-body">
<div class="quality-stats">
<div class="row">
<div class="col-4">
<div class="stat-item">
<h4 id="qualityTotalScans">-</h4>
<p>Total Scans</p>
</div>
</div>
<div class="col-4">
<div class="stat-item approved">
<h4 id="qualityApprovedScans">-</h4>
<p>Approved</p>
</div>
</div>
<div class="col-4">
<div class="stat-item rejected">
<h4 id="qualityRejectedScans">-</h4>
<p>Rejected</p>
</div>
</div>
</div>
<div class="mt-3">
<div class="progress">
<div id="qualityApprovalBar" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
</div>
<p class="text-center mt-2 mb-0">
Approval Rate: <span id="qualityApprovalRate" class="fw-bold">0%</span>
</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-clipboard-check"></i> Finish Goods Quality
</h5>
</div>
<div class="card-body">
<div class="quality-stats">
<div class="row">
<div class="col-4">
<div class="stat-item">
<h4 id="fgQualityTotalScans">-</h4>
<p>Total Scans</p>
</div>
</div>
<div class="col-4">
<div class="stat-item approved">
<h4 id="fgQualityApprovedScans">-</h4>
<p>Approved</p>
</div>
</div>
<div class="col-4">
<div class="stat-item rejected">
<h4 id="fgQualityRejectedScans">-</h4>
<p>Rejected</p>
</div>
</div>
</div>
<div class="mt-3">
<div class="progress">
<div id="fgQualityApprovalBar" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
</div>
<p class="text-center mt-2 mb-0">
Approval Rate: <span id="fgQualityApprovalRate" class="fw-bold">0%</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Export and Actions -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-download"></i> Export Options
</h5>
</div>
<div class="card-body">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-success" onclick="exportReportPDF()">
<i class="fas fa-file-pdf"></i> Export PDF
</button>
<button type="button" class="btn btn-outline-primary" onclick="exportReportExcel()">
<i class="fas fa-file-excel"></i> Export Excel
</button>
<button type="button" class="btn btn-outline-info" onclick="printReport()">
<i class="fas fa-print"></i> Print Report
</button>
<button type="button" class="btn btn-outline-secondary" onclick="shareReport()">
<i class="fas fa-share"></i> Share Report
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Error Message -->
<div id="errorMessage" class="row mb-4" style="display: none;">
<div class="col-12">
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle"></i>
<strong>Error:</strong> <span id="errorText"></span>
</div>
</div>
</div>
</div>
<style>
.metric-card {
display: flex;
align-items: center;
padding: 1.5rem;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 10px;
margin-bottom: 1rem;
transition: transform 0.2s ease;
}
.metric-card:hover {
transform: translateY(-2px);
}
.metric-card.orders-quantity {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
}
.metric-card.production-launched {
background: linear-gradient(135deg, #f3e5f5 0%, #ce93d8 100%);
}
.metric-card.production-finished {
background: linear-gradient(135deg, #e8f5e8 0%, #a5d6a7 100%);
}
.metric-card.orders-delivered {
background: linear-gradient(135deg, #fff3e0 0%, #ffcc02 100%);
}
.metric-icon {
font-size: 2.5rem;
margin-right: 1rem;
opacity: 0.8;
}
.metric-content h3 {
font-size: 2rem;
font-weight: bold;
margin: 0;
color: #2c3e50;
}
.metric-content p {
margin: 0;
color: #6c757d;
font-weight: 500;
}
.quality-stats .stat-item {
text-align: center;
padding: 1rem 0;
}
.quality-stats .stat-item h4 {
font-size: 1.5rem;
font-weight: bold;
margin: 0;
color: #2c3e50;
}
.quality-stats .stat-item.approved h4 {
color: #28a745;
}
.quality-stats .stat-item.rejected h4 {
color: #dc3545;
}
.quality-stats .stat-item p {
margin: 0;
color: #6c757d;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.metric-card {
flex-direction: column;
text-align: center;
}
.metric-icon {
margin-right: 0;
margin-bottom: 0.5rem;
}
}
</style>
<script>
function setTodayDate() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('reportDate').value = today;
generateDailyReport();
}
function generateDailyReport() {
const reportDate = document.getElementById('reportDate').value;
if (!reportDate) {
showError('Please select a report date');
return;
}
// Show loading indicator
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('reportResults').style.display = 'none';
document.getElementById('errorMessage').style.display = 'none';
// Make API call to get daily data
fetch(`/daily_mirror/api/data?date=${reportDate}`)
.then(response => response.json())
.then(data => {
if (data.error) {
showError(data.error);
return;
}
// Update display with data
updateDailyReport(data);
// Hide loading and show results
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('reportResults').style.display = 'block';
})
.catch(error => {
console.error('Error generating daily report:', error);
showError('Failed to generate daily report. Please try again.');
});
}
function updateDailyReport(data) {
// Update date display
document.getElementById('reportDateDisplay').textContent = data.date;
// Update key metrics
document.getElementById('ordersQuantity').textContent = data.orders_quantity.toLocaleString();
document.getElementById('productionLaunched').textContent = data.production_launched.toLocaleString();
document.getElementById('productionFinished').textContent = data.production_finished.toLocaleString();
document.getElementById('ordersDelivered').textContent = data.orders_delivered.toLocaleString();
// Update quality control data
document.getElementById('qualityTotalScans').textContent = data.quality_scans.total_scans.toLocaleString();
document.getElementById('qualityApprovedScans').textContent = data.quality_scans.approved_scans.toLocaleString();
document.getElementById('qualityRejectedScans').textContent = data.quality_scans.rejected_scans.toLocaleString();
document.getElementById('qualityApprovalRate').textContent = data.quality_scans.approval_rate + '%';
document.getElementById('qualityApprovalBar').style.width = data.quality_scans.approval_rate + '%';
// Update FG quality data
document.getElementById('fgQualityTotalScans').textContent = data.fg_quality_scans.total_scans.toLocaleString();
document.getElementById('fgQualityApprovedScans').textContent = data.fg_quality_scans.approved_scans.toLocaleString();
document.getElementById('fgQualityRejectedScans').textContent = data.fg_quality_scans.rejected_scans.toLocaleString();
document.getElementById('fgQualityApprovalRate').textContent = data.fg_quality_scans.approval_rate + '%';
document.getElementById('fgQualityApprovalBar').style.width = data.fg_quality_scans.approval_rate + '%';
}
function showError(message) {
document.getElementById('errorText').textContent = message;
document.getElementById('errorMessage').style.display = 'block';
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('reportResults').style.display = 'none';
}
function exportReportPDF() {
alert('PDF export functionality will be implemented soon.');
}
function exportReportExcel() {
alert('Excel export functionality will be implemented soon.');
}
function printReport() {
window.print();
}
function shareReport() {
alert('Share functionality will be implemented soon.');
}
// Auto-generate today's report on page load
document.addEventListener('DOMContentLoaded', function() {
generateDailyReport();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,760 @@
{% extends "base.html" %}
{% block title %}Build Database - Daily Mirror{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='daily_mirror_tune.css') }}">
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">🔨 Build Database</h1>
<p class="text-muted">Upload Excel files to populate Daily Mirror database tables</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6 mb-4">
<!-- Card 1: Upload Excel File -->
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-upload"></i> Upload Excel File
</h5>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data" id="uploadForm">
<!-- Table Selection -->
<div class="form-group mb-4">
<label for="target_table" class="form-label">
<strong>Select Target Table:</strong>
</label>
<select class="form-control" name="target_table" id="target_table" required>
<option value="">-- Choose a table --</option>
{% for table in available_tables %}
<option value="{{ table.name }}" data-description="{{ table.description }}">
{{ table.display }}
</option>
{% endfor %}
</select>
<small id="tableDescription" class="form-text text-muted mt-2">
Select a table to see its description.
</small>
</div>
<!-- File Upload -->
<div class="form-group mb-4">
<label for="excel_file" class="form-label">
<strong>Select Excel File:</strong>
</label>
<input type="file" class="form-control" name="excel_file" id="excel_file"
accept=".xlsx,.xls" required>
<small class="form-text text-muted">
Accepted formats: .xlsx, .xls (Maximum file size: 10MB)
</small>
</div>
<!-- Upload Button -->
<div class="text-center">
<button type="button" class="btn btn-primary btn-lg" id="uploadBtn">
<i class="fas fa-cloud-upload-alt"></i> Upload and Process File
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<!-- Card 2: Excel File Format Instructions -->
<div class="card h-100">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle"></i> Excel File Format Instructions
</h5>
</div>
<div class="card-body">
<div class="accordion" id="formatAccordion">
<!-- Production Data Format -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#productionCollapse" aria-expanded="false">
🏭 Production Data Format
</button>
</h2>
<div id="productionCollapse" class="accordion-collapse collapse" data-bs-parent="#formatAccordion">
<div class="accordion-body">
<p><strong>Expected columns for Production Data:</strong></p>
<div class="row">
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Production Order ID</code> <span class="text-muted">Unique identifier</span></li>
<li><code>Customer Code</code> <span class="text-muted">Customer code</span></li>
<li><code>Customer Name</code> <span class="text-muted">Customer name</span></li>
<li><code>Article Code</code> <span class="text-muted">Article code</span></li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Article Description</code> <span class="text-muted">Description</span></li>
<li><code>Quantity</code> <span class="text-muted">To produce</span></li>
<li><code>Production Date</code> <span class="text-muted">Date</span></li>
<li><code>Status</code> <span class="text-muted">Production status</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Orders Data Format -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#ordersCollapse" aria-expanded="false">
🛒 Orders Data Format
</button>
</h2>
<div id="ordersCollapse" class="accordion-collapse collapse" data-bs-parent="#formatAccordion">
<div class="accordion-body">
<p><strong>Expected columns for Orders Data:</strong></p>
<div class="row">
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Order ID</code> <span class="text-muted">Unique identifier</span></li>
<li><code>Customer Code</code> <span class="text-muted">Customer code</span></li>
<li><code>Customer Name</code> <span class="text-muted">Customer name</span></li>
<li><code>Article Code</code> <span class="text-muted">Article code</span></li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Article Description</code> <span class="text-muted">Description</span></li>
<li><code>Quantity Ordered</code> <span class="text-muted">Ordered</span></li>
<li><code>Order Date</code> <span class="text-muted">Date</span></li>
<li><code>Status</code> <span class="text-muted">Order status</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Delivery Data Format -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#deliveryCollapse" aria-expanded="false">
🚚 Delivery Data Format (Articole livrate)
</button>
</h2>
<div id="deliveryCollapse" class="accordion-collapse collapse" data-bs-parent="#formatAccordion">
<div class="accordion-body">
<p><strong>Expected columns for Delivery Data:</strong></p>
<div class="row">
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Shipment ID</code> <span class="text-muted">Unique shipment identifier</span></li>
<li><code>Order ID</code> <span class="text-muted">Related order</span></li>
<li><code>Customer</code> <span class="text-muted">Customer info</span></li>
<li><code>Article</code> <span class="text-muted">Code/description</span></li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Quantity Delivered</code> <span class="text-muted">Delivered quantity</span></li>
<li><code>Delivery Date</code> <span class="text-muted">Date</span></li>
<li><code>Status</code> <span class="text-muted">Delivery status</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Upload Result Modal (Better Solution) -->
<div class="modal fade" id="uploadResultModal" tabindex="-1" aria-labelledby="uploadResultModalLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" id="modalHeader">
<h5 class="modal-title" id="uploadResultModalLabel">
<i class="fas fa-check-circle"></i> Upload Result
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="modalCloseBtn"></button>
</div>
<div class="modal-body">
<div id="uploadResultContent" class="text-center py-3">
<!-- Result content will be inserted here -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" id="modalOkBtn">OK</button>
</div>
</div>
</div>
</div>
<style>
/* Additional modal fixes specific to this page */
#uploadResultModal {
z-index: 10000 !important;
pointer-events: auto !important;
}
#uploadResultModal .modal-dialog {
pointer-events: auto !important;
z-index: 10001 !important;
}
#uploadResultModal .modal-content {
pointer-events: auto !important;
background-color: white !important;
}
#uploadResultModal .modal-backdrop {
z-index: 9998 !important;
}
#uploadResultModal .btn-close,
#uploadResultModal .modal-footer button {
pointer-events: auto !important;
cursor: pointer !important;
opacity: 1 !important;
}
/* Ensure modal can be dismissed */
.modal-backdrop.show {
pointer-events: auto !important;
}
/* Dark mode modal fixes */
body.dark-mode #uploadResultModal .modal-content {
background-color: #2d3748 !important;
}
.accordion-button:not(.collapsed) {
background-color: #e7f3ff;
color: #0066cc;
}
.form-control:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.btn-primary {
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}
/* Result stats styling */
.upload-stats {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
margin-top: 20px;
}
.stat-box {
text-align: center;
padding: 15px;
min-width: 100px;
margin: 5px;
border-radius: 8px;
}
.stat-box.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
}
.stat-box.warning {
background-color: #fff3cd;
border: 1px solid #ffeeba;
}
.stat-box.error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
display: block;
}
.stat-label {
font-size: 0.9rem;
color: #666;
}
/* Reduce font size for Excel Format Instructions card rows */
.col-lg-6:nth-child(2) .card-body {
font-size: 0.95rem;
}
/* Make accordion button labels smaller */
.accordion-button {
font-size: 1rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
/* Override h2 size in accordion headers */
.accordion-header {
font-size: 1rem;
}
.accordion-header h2 {
font-size: 1rem;
margin: 0;
}
/* Modal summary list styling */
#uploadResultContent ul {
list-style-type: none;
padding-left: 0;
margin: 10px auto;
max-width: 400px;
}
#uploadResultContent ul li {
padding: 5px 0;
font-size: 1rem;
}
#uploadResultContent ul li::before {
content: '✓ ';
color: #28a745;
font-weight: bold;
margin-right: 5px;
}
/* Make "Expected columns" text smaller in accordion bodies */
.accordion-body p {
font-size: 0.9rem;
}
.accordion-body strong {
font-size: 0.9rem;
}
/* Dark mode styles */
body.dark-mode .card {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
box-shadow: 0 4px 6px rgba(255, 255, 255, 0.1);
}
body.dark-mode .card-header {
background-color: #4a5568;
color: #e2e8f0;
border-bottom: 1px solid rgba(226, 232, 240, 0.2);
}
body.dark-mode .form-control {
background-color: #4a5568;
border-color: #6b7280;
color: #e2e8f0;
}
body.dark-mode .form-control:focus {
background-color: #4a5568;
border-color: #007bff;
color: #e2e8f0;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
body.dark-mode .accordion-button {
background-color: #374151;
color: #e2e8f0;
border-color: #6b7280;
}
body.dark-mode .accordion-button:not(.collapsed) {
background-color: #1e3a8a;
color: #bfdbfe;
}
body.dark-mode .accordion-body {
background-color: #374151;
color: #e2e8f0;
}
body.dark-mode code {
background-color: #374151;
color: #e2e8f0;
border: 1px solid #6b7280;
}
body.dark-mode .modal-content {
background-color: #2d3748;
color: #e2e8f0;
}
body.dark-mode .modal-header {
border-bottom-color: #4a5568;
}
body.dark-mode .modal-footer {
border-top-color: #4a5568;
}
body.dark-mode .stat-box.success {
background-color: #1e4620;
border-color: #2d5a2e;
color: #a3d9a5;
}
body.dark-mode .stat-box.warning {
background-color: #5a4a1e;
border-color: #6b5a2d;
color: #f4d88f;
}
body.dark-mode .stat-box.error {
background-color: #5a1e1e;
border-color: #6b2d2d;
color: #f8a3a8;
}
body.dark-mode .stat-label {
color: #a0aec0;
}
body.dark-mode .text-muted {
color: #a0aec0 !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize theme on page load
const savedTheme = localStorage.getItem('theme');
const body = document.body;
if (savedTheme === 'dark') {
body.classList.add('dark-mode');
} else {
body.classList.remove('dark-mode');
}
const tableSelect = document.getElementById('target_table');
const tableDescription = document.getElementById('tableDescription');
const uploadBtn = document.getElementById('uploadBtn');
const uploadForm = document.getElementById('uploadForm');
const fileInput = document.getElementById('excel_file');
// Update table description when selection changes
tableSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
if (selectedOption.value) {
const description = selectedOption.getAttribute('data-description');
tableDescription.innerHTML = `<strong>Selected:</strong> ${description}`;
tableDescription.className = 'form-text text-info mt-2';
} else {
tableDescription.innerHTML = 'Select a table to see its description.';
tableDescription.className = 'form-text text-muted mt-2';
}
});
// File input change handler to show file info
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const fileName = file.name;
const fileSize = (file.size / 1024 / 1024).toFixed(2);
console.log(`Selected file: ${fileName} (${fileSize} MB)`);
}
});
// Upload button click handler (AJAX submission)
uploadBtn.addEventListener('click', function(e) {
e.preventDefault();
// Validate file selection
if (!fileInput.files.length) {
alert('Please select an Excel file to upload.');
return;
}
// Validate table selection
if (!tableSelect.value) {
alert('Please select a target table.');
return;
}
// Check file size (10MB limit)
const file = fileInput.files[0];
if (file.size > 10 * 1024 * 1024) {
alert('File size must be less than 10MB.');
return;
}
// Prepare form data
const formData = new FormData(uploadForm);
// Show loading state
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
uploadBtn.disabled = true;
// Submit via AJAX
fetch('/daily_mirror/build_database', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
return response.json().then(err => Promise.reject(err));
}
return response.json();
})
.then(result => {
// Reset button
uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload and Process File';
uploadBtn.disabled = false;
// Show result in modal
showUploadResult(result);
// Reset form
uploadForm.reset();
tableDescription.innerHTML = 'Select a table to see its description.';
tableDescription.className = 'form-text text-muted mt-2';
})
.catch(error => {
// Reset button
uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload and Process File';
uploadBtn.disabled = false;
// Show error in modal
showUploadError(error);
});
});
function showUploadResult(result) {
const modal = new bootstrap.Modal(document.getElementById('uploadResultModal'));
const modalHeader = document.getElementById('modalHeader');
const modalTitle = document.getElementById('uploadResultModalLabel');
const content = document.getElementById('uploadResultContent');
// Determine overall status
const hasErrors = result.error_count && result.error_count > 0;
const hasSuccess = result.created_rows > 0 || result.updated_rows > 0;
// Update modal header color
if (hasErrors && !hasSuccess) {
modalHeader.className = 'modal-header bg-danger text-white';
modalTitle.innerHTML = '<i class="fas fa-times-circle"></i> Upload Failed';
} else if (hasErrors && hasSuccess) {
modalHeader.className = 'modal-header bg-warning text-dark';
modalTitle.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Upload Completed with Warnings';
} else {
modalHeader.className = 'modal-header bg-success text-white';
modalTitle.innerHTML = '<i class="fas fa-check-circle"></i> Upload Successful';
}
// Build result content with stats
let html = '<div class="upload-stats">';
// Total rows processed from Excel
html += `
<div class="stat-box ${hasErrors ? 'warning' : 'success'}">
<span class="stat-value">${result.total_rows || 0}</span>
<span class="stat-label">Rows Processed</span>
</div>
`;
// Created rows (new in database)
if (result.created_rows > 0) {
html += `
<div class="stat-box success">
<span class="stat-value">${result.created_rows}</span>
<span class="stat-label">New Rows Created</span>
</div>
`;
}
// Updated rows (existing in database)
if (result.updated_rows > 0) {
html += `
<div class="stat-box success">
<span class="stat-value">${result.updated_rows}</span>
<span class="stat-label">Rows Updated</span>
</div>
`;
}
// Errors
if (hasErrors) {
html += `
<div class="stat-box error">
<span class="stat-value">${result.error_count}</span>
<span class="stat-label">Errors</span>
</div>
`;
}
html += '</div>';
// Add detailed summary message
const successCount = (result.created_rows || 0) + (result.updated_rows || 0);
if (successCount > 0) {
let msg = `<p class="mt-3 mb-0"><strong>Successfully processed ${result.total_rows} rows from Excel:</strong></p>`;
msg += '<ul class="text-start">';
if (result.created_rows > 0) {
msg += `<li>${result.created_rows} new ${result.created_rows === 1 ? 'record' : 'records'} created in database</li>`;
}
if (result.updated_rows > 0) {
msg += `<li>${result.updated_rows} existing ${result.updated_rows === 1 ? 'record' : 'records'} updated</li>`;
}
msg += '</ul>';
html += msg;
}
if (hasErrors) {
html += `<p class="text-danger mb-0"><strong>⚠️ ${result.error_count} ${result.error_count === 1 ? 'row' : 'rows'} could not be processed due to errors.</strong></p>`;
}
// Add auto-close countdown for successful uploads without errors
if (!hasErrors && successCount > 0) {
html += `<p class="text-muted mt-2 mb-0" id="autoCloseCountdown"><small>This window will close automatically in <span id="countdown">3</span> seconds...</small></p>`;
}
content.innerHTML = html;
modal.show();
// Get modal element
const modalElement = document.getElementById('uploadResultModal');
// Add explicit close handlers
const okBtn = document.getElementById('modalOkBtn');
const closeBtn = document.getElementById('modalCloseBtn');
if (okBtn) {
okBtn.onclick = function() {
modal.hide();
// Also trigger Bootstrap's native close
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}
if (closeBtn) {
closeBtn.onclick = function() {
modal.hide();
// Also trigger Bootstrap's native close
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}
// Auto-close after 3 seconds for successful uploads without errors
if (!hasErrors && successCount > 0) {
let countdown = 3;
const countdownInterval = setInterval(function() {
countdown--;
const countdownSpan = document.getElementById('countdown');
if (countdownSpan) {
countdownSpan.textContent = countdown;
}
if (countdown <= 0) {
clearInterval(countdownInterval);
}
}, 1000);
setTimeout(function() {
clearInterval(countdownInterval);
modal.hide();
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
}, 3000);
}
}
function showUploadError(error) {
const modal = new bootstrap.Modal(document.getElementById('uploadResultModal'));
const modalHeader = document.getElementById('modalHeader');
const modalTitle = document.getElementById('uploadResultModalLabel');
const content = document.getElementById('uploadResultContent');
// Update modal header
modalHeader.className = 'modal-header bg-danger text-white';
modalTitle.innerHTML = '<i class="fas fa-times-circle"></i> Upload Error';
// Show error message
const errorMsg = error.error || error.message || 'An unexpected error occurred during upload.';
content.innerHTML = `
<div class="alert alert-danger mb-0">
<i class="fas fa-exclamation-triangle"></i>
<strong>Error:</strong> ${errorMsg}
</div>
`;
modal.show();
// Get modal element
const modalElement = document.getElementById('uploadResultModal');
// Add explicit close handlers
const okBtn = document.getElementById('modalOkBtn');
const closeBtn = document.getElementById('modalCloseBtn');
if (okBtn) {
okBtn.onclick = function() {
modal.hide();
// Also trigger Bootstrap's native close
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}
if (closeBtn) {
closeBtn.onclick = function() {
modal.hide();
// Also trigger Bootstrap's native close
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,449 @@
{% extends "base.html" %}
{% block title %}Daily Mirror History - Quality Recticel{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">📋 Daily Mirror History</h1>
<p class="text-muted">Analyze historical daily production reports and trends</p>
</div>
<div>
<a href="{{ url_for('daily_mirror.daily_mirror_route') }}" class="btn btn-outline-success">
<i class="fas fa-chart-line"></i> Create New Report
</a>
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
</div>
</div>
<!-- Date Range Selection -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-calendar-week"></i> Select Date Range
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<label for="startDate" class="form-label">Start Date:</label>
<input type="date" class="form-control" id="startDate" value="{{ start_date }}">
</div>
<div class="col-md-3">
<label for="endDate" class="form-label">End Date:</label>
<input type="date" class="form-control" id="endDate" value="{{ end_date }}">
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="button" class="btn btn-primary" onclick="loadHistoryData()">
<i class="fas fa-search"></i> Load History
</button>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(7)">Last 7 days</button>
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(30)">Last 30 days</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="row mb-4" style="display: none;">
<div class="col-12">
<div class="card">
<div class="card-body text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 mb-0">Loading historical data...</p>
</div>
</div>
</div>
</div>
<!-- Summary Statistics -->
<div id="summaryStats" style="display: none;">
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-chart-bar"></i> Period Summary
<span id="periodRange" class="badge bg-secondary ms-2"></span>
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="summary-metric">
<h4 id="totalOrdersQuantity">-</h4>
<p>Total Orders Quantity</p>
<small id="avgOrdersQuantity" class="text-muted">Avg: -</small>
</div>
</div>
<div class="col-md-3">
<div class="summary-metric">
<h4 id="totalProductionLaunched">-</h4>
<p>Total Production Launched</p>
<small id="avgProductionLaunched" class="text-muted">Avg: -</small>
</div>
</div>
<div class="col-md-3">
<div class="summary-metric">
<h4 id="totalProductionFinished">-</h4>
<p>Total Production Finished</p>
<small id="avgProductionFinished" class="text-muted">Avg: -</small>
</div>
</div>
<div class="col-md-3">
<div class="summary-metric">
<h4 id="totalOrdersDelivered">-</h4>
<p>Total Orders Delivered</p>
<small id="avgOrdersDelivered" class="text-muted">Avg: -</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Historical Data Table -->
<div id="historyTable" style="display: none;">
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-table"></i> Historical Daily Reports
</h5>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="exportHistoryCSV()">
<i class="fas fa-file-csv"></i> CSV
</button>
<button type="button" class="btn btn-outline-success" onclick="exportHistoryExcel()">
<i class="fas fa-file-excel"></i> Excel
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="historyDataTable">
<thead class="table-dark">
<tr>
<th>Date</th>
<th>Orders Quantity</th>
<th>Production Launched</th>
<th>Production Finished</th>
<th>Orders Delivered</th>
<th>Quality Approval Rate</th>
<th>FG Quality Approval Rate</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="historyTableBody">
<!-- Data will be populated here -->
</tbody>
</table>
</div>
<!-- Pagination -->
<nav aria-label="History pagination" id="historyPagination" style="display: none;">
<ul class="pagination pagination-sm justify-content-center">
<!-- Pagination will be populated here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Chart Visualization -->
<div id="chartVisualization" style="display: none;">
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-chart-line"></i> Trend Analysis
</h5>
</div>
<div class="card-body">
<canvas id="trendChart" height="100"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Error Message -->
<div id="errorMessage" class="row mb-4" style="display: none;">
<div class="col-12">
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle"></i>
<strong>Error:</strong> <span id="errorText"></span>
</div>
</div>
</div>
</div>
<style>
.summary-metric {
text-align: center;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 1rem;
}
.summary-metric h4 {
font-size: 1.8rem;
font-weight: bold;
color: #2c3e50;
margin: 0;
}
.summary-metric p {
margin: 0.5rem 0;
color: #6c757d;
font-weight: 500;
}
.table th {
white-space: nowrap;
}
.approval-rate {
font-weight: bold;
}
.approval-rate.high {
color: #28a745;
}
.approval-rate.medium {
color: #ffc107;
}
.approval-rate.low {
color: #dc3545;
}
@media (max-width: 768px) {
.table-responsive {
font-size: 0.9rem;
}
}
</style>
<script>
let historyData = [];
let currentPage = 1;
const itemsPerPage = 20;
function setDateRange(days) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - days);
document.getElementById('endDate').value = endDate.toISOString().split('T')[0];
document.getElementById('startDate').value = startDate.toISOString().split('T')[0];
loadHistoryData();
}
function loadHistoryData() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
if (!startDate || !endDate) {
showError('Please select both start and end dates');
return;
}
if (new Date(startDate) > new Date(endDate)) {
showError('Start date cannot be after end date');
return;
}
// Show loading indicator
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('summaryStats').style.display = 'none';
document.getElementById('historyTable').style.display = 'none';
document.getElementById('chartVisualization').style.display = 'none';
document.getElementById('errorMessage').style.display = 'none';
// Make API call to get historical data
fetch(`/daily_mirror/api/history_data?start_date=${startDate}&end_date=${endDate}`)
.then(response => response.json())
.then(data => {
if (data.error) {
showError(data.error);
return;
}
historyData = data.history;
// Update displays
updateSummaryStats(data);
updateHistoryTable();
updateTrendChart();
// Hide loading and show results
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('summaryStats').style.display = 'block';
document.getElementById('historyTable').style.display = 'block';
document.getElementById('chartVisualization').style.display = 'block';
})
.catch(error => {
console.error('Error loading history data:', error);
showError('Failed to load historical data. Please try again.');
});
}
function updateSummaryStats(data) {
const history = data.history;
if (history.length === 0) {
document.getElementById('periodRange').textContent = 'No Data';
return;
}
document.getElementById('periodRange').textContent = `${data.start_date} to ${data.end_date}`;
// Calculate totals and averages
const totals = history.reduce((acc, day) => {
acc.ordersQuantity += day.orders_quantity;
acc.productionLaunched += day.production_launched;
acc.productionFinished += day.production_finished;
acc.ordersDelivered += day.orders_delivered;
return acc;
}, { ordersQuantity: 0, productionLaunched: 0, productionFinished: 0, ordersDelivered: 0 });
const avgDivisor = history.length;
document.getElementById('totalOrdersQuantity').textContent = totals.ordersQuantity.toLocaleString();
document.getElementById('avgOrdersQuantity').textContent = `Avg: ${Math.round(totals.ordersQuantity / avgDivisor).toLocaleString()}`;
document.getElementById('totalProductionLaunched').textContent = totals.productionLaunched.toLocaleString();
document.getElementById('avgProductionLaunched').textContent = `Avg: ${Math.round(totals.productionLaunched / avgDivisor).toLocaleString()}`;
document.getElementById('totalProductionFinished').textContent = totals.productionFinished.toLocaleString();
document.getElementById('avgProductionFinished').textContent = `Avg: ${Math.round(totals.productionFinished / avgDivisor).toLocaleString()}`;
document.getElementById('totalOrdersDelivered').textContent = totals.ordersDelivered.toLocaleString();
document.getElementById('avgOrdersDelivered').textContent = `Avg: ${Math.round(totals.ordersDelivered / avgDivisor).toLocaleString()}`;
}
function updateHistoryTable() {
const tbody = document.getElementById('historyTableBody');
tbody.innerHTML = '';
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const pageData = historyData.slice(startIndex, endIndex);
pageData.forEach(day => {
const row = document.createElement('tr');
const qualityRate = day.quality_scans.approval_rate;
const fgQualityRate = day.fg_quality_scans.approval_rate;
row.innerHTML = `
<td><strong>${day.date}</strong></td>
<td>${day.orders_quantity.toLocaleString()}</td>
<td>${day.production_launched.toLocaleString()}</td>
<td>${day.production_finished.toLocaleString()}</td>
<td>${day.orders_delivered.toLocaleString()}</td>
<td><span class="approval-rate ${getApprovalRateClass(qualityRate)}">${qualityRate}%</span></td>
<td><span class="approval-rate ${getApprovalRateClass(fgQualityRate)}">${fgQualityRate}%</span></td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewDayDetails('${day.date}')">
<i class="fas fa-eye"></i> View
</button>
</td>
`;
tbody.appendChild(row);
});
updatePagination();
}
function getApprovalRateClass(rate) {
if (rate >= 95) return 'high';
if (rate >= 85) return 'medium';
return 'low';
}
function updatePagination() {
const totalPages = Math.ceil(historyData.length / itemsPerPage);
if (totalPages <= 1) {
document.getElementById('historyPagination').style.display = 'none';
return;
}
document.getElementById('historyPagination').style.display = 'block';
// Pagination implementation can be added here
}
function updateTrendChart() {
// Chart implementation using Chart.js can be added here
// For now, we'll show a placeholder
const canvas = document.getElementById('trendChart');
const ctx = canvas.getContext('2d');
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw placeholder text
ctx.font = '16px Arial';
ctx.fillStyle = '#6c757d';
ctx.textAlign = 'center';
ctx.fillText('Trend chart visualization will be implemented here', canvas.width / 2, canvas.height / 2);
}
function viewDayDetails(date) {
// Navigate to daily mirror with specific date
window.open(`{{ url_for('daily_mirror.daily_mirror_route') }}?date=${date}`, '_blank');
}
function exportHistoryCSV() {
alert('CSV export functionality will be implemented soon.');
}
function exportHistoryExcel() {
alert('Excel export functionality will be implemented soon.');
}
function showError(message) {
document.getElementById('errorText').textContent = message;
document.getElementById('errorMessage').style.display = 'block';
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('summaryStats').style.display = 'none';
document.getElementById('historyTable').style.display = 'none';
document.getElementById('chartVisualization').style.display = 'none';
}
// Auto-load data on page load
document.addEventListener('DOMContentLoaded', function() {
loadHistoryData();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,262 @@
{% extends "base.html" %}
{% block title %}Daily Mirror - Quality Recticel{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">📊 Daily Mirror</h1>
<p class="text-muted">Business Intelligence and Production Reporting</p>
</div>
<div>
<!-- Buttons removed; now present in top header -->
</div>
</div>
</div>
</div>
<!-- Daily Mirror Cards -->
<div class="row">
<!-- Card 1: Build Database -->
<div class="col-lg-6 col-md-6 mb-4">
<div class="card h-100 daily-mirror-card">
<div class="card-body d-flex flex-column">
<div class="text-center mb-3">
<div class="feature-icon bg-primary">
<i class="fas fa-database"></i>
</div>
</div>
<h5 class="card-title text-center">Build Database</h5>
<p class="card-text flex-grow-1 text-center">
Upload Excel files to create and populate tables.
</p>
<div class="mt-auto">
<a href="{{ url_for('daily_mirror.daily_mirror_build_database') }}" class="btn btn-primary btn-block w-100">
<i class="fas fa-hammer"></i> Build Database
</a>
</div>
</div>
</div>
</div>
<!-- Card 2: Tune Database -->
<div class="col-lg-6 col-md-6 mb-4">
<div class="card h-100 daily-mirror-card">
<div class="card-body d-flex flex-column">
<div class="text-center mb-3">
<div class="feature-icon bg-warning">
<i class="fas fa-edit"></i>
</div>
</div>
<h5 class="card-title text-center">Tune Database</h5>
<p class="card-text flex-grow-1 text-center">
Edit and update records after import.
</p>
<div class="mt-auto">
<a href="{{ url_for('daily_mirror.tune_production_data') }}" class="btn btn-warning btn-block w-100 btn-sm mb-2">
<i class="fas fa-industry"></i> Production Orders
</a>
<a href="{{ url_for('daily_mirror.tune_orders_data') }}" class="btn btn-warning btn-block w-100 btn-sm mb-2">
<i class="fas fa-shopping-cart"></i> Customer Orders
</a>
<a href="{{ url_for('daily_mirror.tune_delivery_data') }}" class="btn btn-warning btn-block w-100 btn-sm">
<i class="fas fa-truck"></i> Delivery Records
</a>
</div>
</div>
</div>
</div>
<!-- Card 3: Daily Mirror -->
<div class="col-lg-6 col-md-6 mb-4">
<div class="card h-100 daily-mirror-card">
<div class="card-body d-flex flex-column">
<div class="text-center mb-3">
<div class="feature-icon bg-success">
<i class="fas fa-chart-line"></i>
</div>
</div>
<h5 class="card-title text-center">Daily Mirror</h5>
<p class="card-text flex-grow-1 text-center">
Generate daily production reports.
</p>
<div class="mt-auto">
<a href="{{ url_for('daily_mirror.daily_mirror_route') }}" class="btn btn-success btn-block w-100">
<i class="fas fa-plus-circle"></i> Create Daily Report
</a>
</div>
</div>
</div>
</div>
<!-- Card 4: Daily Mirror History -->
<div class="col-lg-6 col-md-6 mb-4">
<div class="card h-100 daily-mirror-card">
<div class="card-body d-flex flex-column">
<div class="text-center mb-3">
<div class="feature-icon bg-info">
<i class="fas fa-history"></i>
</div>
</div>
<h5 class="card-title text-center">Daily Mirror History</h5>
<p class="card-text flex-grow-1 text-center">
View historical production reports.
</p>
<div class="mt-auto">
<a href="{{ url_for('daily_mirror.daily_mirror_history_route') }}" class="btn btn-info btn-block w-100">
<i class="fas fa-chart-bar"></i> View History
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.daily-mirror-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background-color: var(--card-bg);
color: var(--text-color);
}
.daily-mirror-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 15px rgba(0,0,0,0.15);
}
.feature-icon {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
color: white;
font-size: 2rem;
}
/* Light mode styles */
body:not(.dark-mode) .daily-mirror-card {
background-color: #ffffff;
color: #333333;
border: 1px solid #e0e0e0;
}
body:not(.dark-mode) .card-text {
color: #666666;
}
body:not(.dark-mode) .text-muted {
color: #6c757d !important;
}
/* Dark mode styles */
body.dark-mode .daily-mirror-card {
background-color: #2d3748;
color: #e2e8f0;
border: 1px solid #4a5568;
box-shadow: 0 2px 4px rgba(255,255,255,0.1);
}
body.dark-mode .daily-mirror-card:hover {
box-shadow: 0 4px 15px rgba(255,255,255,0.15);
}
body.dark-mode .card-text {
color: #cbd5e0;
}
body.dark-mode .text-muted {
color: #a0aec0 !important;
}
body.dark-mode .h3 {
color: #e2e8f0;
}
body.dark-mode .container-fluid {
color: #e2e8f0;
}
/* Ensure buttons maintain their intended colors in both themes */
.btn-primary {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
border: none;
color: white;
}
.btn-warning {
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
border: none;
color: #212529;
}
.btn-success {
background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
border: none;
color: white;
}
.btn-info {
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
border: none;
color: white;
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d 0%, #545b62 100%);
border: none;
color: white;
}
@media (max-width: 768px) {
.feature-icon {
width: 60px;
height: 60px;
font-size: 1.5rem;
}
}</style>
<script>
// Initialize theme on page load
document.addEventListener('DOMContentLoaded', function() {
// Apply saved theme from localStorage
const savedTheme = localStorage.getItem('theme');
const body = document.body;
if (savedTheme === 'dark') {
body.classList.add('dark-mode');
} else {
body.classList.remove('dark-mode');
}
// Update theme toggle button text if it exists
const themeToggleButton = document.getElementById('theme-toggle');
if (themeToggleButton) {
if (body.classList.contains('dark-mode')) {
themeToggleButton.textContent = 'Change to Light Mode';
} else {
themeToggleButton.textContent = 'Change to Dark Mode';
}
}
});
function showComingSoon(feature) {
alert(`${feature} functionality will be available in a future update!\n\nThis feature is currently under development and will include advanced capabilities for enhanced Daily Mirror operations.`);
}
// Auto-refresh quick stats every 5 minutes
setInterval(function() {
// This could be implemented to refresh the quick stats
console.log('Auto-refresh daily stats (not implemented yet)');
}, 300000); // 5 minutes
</script>
{% endblock %}

View File

@@ -0,0 +1,549 @@
{% extends "base.html" %}
{% block title %}Tune Delivery Data - Daily Mirror{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/daily_mirror_tune.css') }}">
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">🚚 Tune Delivery Data</h1>
<p class="text-muted">Edit and update delivery records information</p>
</div>
<div>
<!-- Buttons removed; now present in top header -->
</div>
</div>
</div>
</div>
<!-- Filters Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-filter"></i> Filters and Search
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<label for="searchInput" class="form-label">Search</label>
<input type="text" class="form-control" id="searchInput"
placeholder="Search by shipment, customer, or article...">
</div>
<div class="col-md-3 mb-3">
<label for="statusFilter" class="form-label">Delivery Status</label>
<select class="form-control" id="statusFilter">
<option value="">All Statuses</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="customerFilter" class="form-label">Customer</label>
<select class="form-control" id="customerFilter">
<option value="">All Customers</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="recordsPerPage" class="form-label">Records per page</label>
<select class="form-control" id="recordsPerPage">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div class="row">
<div class="col-12">
<button class="btn btn-primary" onclick="loadDeliveryData()">
<i class="fas fa-search"></i> Apply Filters
</button>
<button class="btn btn-secondary" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Data Table Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-table"></i> Delivery Records Data
</h5>
<div class="d-flex align-items-center">
<span id="recordsInfo" class="text-muted me-3"></span>
{% if session.get('role') == 'superadmin' %}
<button class="btn btn-danger btn-sm me-2" onclick="clearAllDelivery()" id="clearAllBtn">
<i class="fas fa-trash-alt"></i> Clear All Delivery
</button>
{% endif %}
<button class="btn btn-success btn-sm" onclick="saveAllChanges()">
<i class="fas fa-save"></i> Save All Changes
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="deliveryTable">
<thead class="table-dark">
<tr>
<th>Shipment ID</th>
<th>Customer</th>
<th>Order ID</th>
<th>Article Code</th>
<th>Description</th>
<th>Quantity</th>
<th>Shipment Date</th>
<th>Delivery Date</th>
<th>Status</th>
<th>Total Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="deliveryTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<!-- Loading indicator -->
<div id="loadingIndicator" class="text-center py-4" style="display: none;">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p class="mt-2">Loading data...</p>
</div>
<!-- No data message -->
<div id="noDataMessage" class="text-center py-4" style="display: none;">
<i class="fas fa-info-circle fa-2x text-muted"></i>
<p class="mt-2 text-muted">No delivery records found</p>
</div>
</div>
<!-- Pagination -->
<div class="card-footer">
<nav aria-label="Delivery data pagination">
<ul class="pagination pagination-sm justify-content-center mb-0" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit Delivery Record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editForm">
<input type="hidden" id="editRecordId">
<div class="row">
<div class="col-md-6 mb-3">
<label for="editShipmentId" class="form-label">Shipment ID</label>
<input type="text" class="form-control" id="editShipmentId" readonly>
</div>
<div class="col-md-6 mb-3">
<label for="editOrderId" class="form-label">Order ID</label>
<input type="text" class="form-control" id="editOrderId">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editCustomerCode" class="form-label">Customer Code</label>
<input type="text" class="form-control" id="editCustomerCode">
</div>
<div class="col-md-6 mb-3">
<label for="editCustomerName" class="form-label">Customer Name</label>
<input type="text" class="form-control" id="editCustomerName">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editArticleCode" class="form-label">Article Code</label>
<input type="text" class="form-control" id="editArticleCode">
</div>
<div class="col-md-6 mb-3">
<label for="editQuantity" class="form-label">Quantity Delivered</label>
<input type="number" class="form-control" id="editQuantity">
</div>
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">Article Description</label>
<textarea class="form-control" id="editDescription" rows="2"></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editShipmentDate" class="form-label">Shipment Date</label>
<input type="date" class="form-control" id="editShipmentDate">
</div>
<div class="col-md-6 mb-3">
<label for="editDeliveryDate" class="form-label">Delivery Date</label>
<input type="date" class="form-control" id="editDeliveryDate">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editDeliveryStatus" class="form-label">Delivery Status</label>
<select class="form-control" id="editDeliveryStatus">
<option value="Finalizat">Finalizat</option>
<option value="Proiect">Proiect</option>
<option value="SHIPPED">Shipped</option>
<option value="DELIVERED">Delivered</option>
<option value="RETURNED">Returned</option>
<option value="PARTIAL">Partial</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="editTotalValue" class="form-label">Total Value (€)</label>
<input type="number" step="0.01" class="form-control" id="editTotalValue">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancel
</button>
<button type="button" class="btn btn-primary" onclick="saveRecord()">
<i class="fas fa-save"></i> Save Changes
</button>
</div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentPerPage = 50;
let currentSearch = '';
let currentStatusFilter = '';
let currentCustomerFilter = '';
document.addEventListener('DOMContentLoaded', function() {
// Initialize theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
}
// Load initial data
loadDeliveryData();
// Setup search on enter key
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
loadDeliveryData();
}
});
});
function loadDeliveryData(page = 1) {
currentPage = page;
currentPerPage = document.getElementById('recordsPerPage').value;
currentSearch = document.getElementById('searchInput').value;
currentStatusFilter = document.getElementById('statusFilter').value;
currentCustomerFilter = document.getElementById('customerFilter').value;
// Show loading indicator
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('deliveryTableBody').style.display = 'none';
document.getElementById('noDataMessage').style.display = 'none';
const params = new URLSearchParams({
page: currentPage,
per_page: currentPerPage,
search: currentSearch,
status: currentStatusFilter,
customer: currentCustomerFilter
});
fetch(`/daily_mirror/api/tune/delivery_data?${params}`)
.then(response => response.json())
.then(data => {
document.getElementById('loadingIndicator').style.display = 'none';
if (data.success) {
if (data.data.length === 0) {
document.getElementById('noDataMessage').style.display = 'block';
} else {
displayDeliveryData(data.data);
updatePagination(data);
updateRecordsInfo(data);
// Populate filter dropdowns on first load
if (currentPage === 1) {
populateCustomerFilter(data.customers);
populateStatusFilter(data.statuses);
}
}
} else {
console.error('Error loading data:', data.error);
alert('Error loading delivery data: ' + data.error);
}
})
.catch(error => {
document.getElementById('loadingIndicator').style.display = 'none';
console.error('Error:', error);
alert('Error loading delivery data: ' + error.message);
});
}
function displayDeliveryData(data) {
const tbody = document.getElementById('deliveryTableBody');
tbody.innerHTML = '';
tbody.style.display = 'table-row-group';
data.forEach(record => {
const row = document.createElement('tr');
row.innerHTML = `
<td><strong>${record.shipment_id}</strong></td>
<td>
<small class="text-muted d-block">${record.customer_code}</small>
${record.customer_name}
</td>
<td>${record.order_id || '-'}</td>
<td><code>${record.article_code}</code></td>
<td><small>${record.article_description || '-'}</small></td>
<td><span class="badge bg-info">${record.quantity_delivered}</span></td>
<td>${record.shipment_date || '-'}</td>
<td>${record.delivery_date || '-'}</td>
<td><span class="badge bg-success">${record.delivery_status}</span></td>
<td><strong>€${parseFloat(record.total_value || 0).toFixed(2)}</strong></td>
<td>
<button class="btn btn-sm btn-primary" onclick="editRecord(${record.id})"
title="Edit Delivery">
<i class="fas fa-edit"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
}
function populateCustomerFilter(customers) {
const filter = document.getElementById('customerFilter');
const currentValue = filter.value;
filter.innerHTML = '<option value="">All Customers</option>';
customers.forEach(customer => {
const option = document.createElement('option');
option.value = customer.code;
option.textContent = `${customer.code} - ${customer.name}`;
filter.appendChild(option);
});
filter.value = currentValue;
}
function populateStatusFilter(statuses) {
const filter = document.getElementById('statusFilter');
const currentValue = filter.value;
filter.innerHTML = '<option value="">All Statuses</option>';
statuses.forEach(status => {
const option = document.createElement('option');
option.value = status;
option.textContent = status;
filter.appendChild(option);
});
filter.value = currentValue;
}
function updatePagination(data) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
if (data.total_pages <= 1) return;
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${data.page === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadDeliveryData(${data.page - 1})">Previous</a>`;
pagination.appendChild(prevLi);
// Page numbers
const startPage = Math.max(1, data.page - 2);
const endPage = Math.min(data.total_pages, data.page + 2);
for (let i = startPage; i <= endPage; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === data.page ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" onclick="loadDeliveryData(${i})">${i}</a>`;
pagination.appendChild(li);
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${data.page === data.total_pages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadDeliveryData(${data.page + 1})">Next</a>`;
pagination.appendChild(nextLi);
}
function updateRecordsInfo(data) {
const start = (data.page - 1) * data.per_page + 1;
const end = Math.min(data.page * data.per_page, data.total_records);
document.getElementById('recordsInfo').textContent =
`Showing ${start}-${end} of ${data.total_records} deliveries`;
}
function clearFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('customerFilter').value = '';
loadDeliveryData(1);
}
function editRecord(recordId) {
// Get data via API for editing
fetch(`/daily_mirror/api/tune/delivery_data?page=${currentPage}&per_page=${currentPerPage}&search=${currentSearch}&status=${currentStatusFilter}&customer=${currentCustomerFilter}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const recordData = data.data.find(record => record.id === recordId);
if (recordData) {
populateEditModal(recordData);
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
editModal.show();
}
}
})
.catch(error => {
console.error('Error:', error);
alert('Error loading record data: ' + error.message);
});
}
function populateEditModal(record) {
document.getElementById('editRecordId').value = record.id;
document.getElementById('editShipmentId').value = record.shipment_id;
document.getElementById('editOrderId').value = record.order_id || '';
document.getElementById('editCustomerCode').value = record.customer_code;
document.getElementById('editCustomerName').value = record.customer_name;
document.getElementById('editArticleCode').value = record.article_code;
document.getElementById('editDescription').value = record.article_description || '';
document.getElementById('editQuantity').value = record.quantity_delivered;
document.getElementById('editShipmentDate').value = record.shipment_date;
document.getElementById('editDeliveryDate').value = record.delivery_date;
document.getElementById('editDeliveryStatus').value = record.delivery_status;
document.getElementById('editTotalValue').value = record.total_value;
}
function saveRecord() {
const recordId = document.getElementById('editRecordId').value;
const data = {
customer_code: document.getElementById('editCustomerCode').value,
customer_name: document.getElementById('editCustomerName').value,
order_id: document.getElementById('editOrderId').value,
article_code: document.getElementById('editArticleCode').value,
article_description: document.getElementById('editDescription').value,
quantity_delivered: parseInt(document.getElementById('editQuantity').value) || 0,
shipment_date: document.getElementById('editShipmentDate').value,
delivery_date: document.getElementById('editDeliveryDate').value,
delivery_status: document.getElementById('editDeliveryStatus').value,
total_value: parseFloat(document.getElementById('editTotalValue').value) || 0
};
fetch(`/daily_mirror/api/tune/delivery_data/${recordId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
// Close modal
const editModal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
editModal.hide();
// Reload data
loadDeliveryData(currentPage);
// Show success message
alert('Delivery record updated successfully!');
} else {
alert('Error updating delivery record: ' + result.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error updating delivery record: ' + error.message);
});
}
function saveAllChanges() {
alert('Save All Changes functionality will be implemented for bulk operations.');
}
function clearAllDelivery() {
if (!confirm('⚠️ WARNING: This will permanently delete ALL delivery records from the database!\n\nAre you absolutely sure you want to continue?')) {
return;
}
// Second confirmation for safety
if (!confirm('This action CANNOT be undone! All delivery data will be lost.\n\nClick OK to proceed with deletion.')) {
return;
}
const clearBtn = document.getElementById('clearAllBtn');
const originalText = clearBtn.innerHTML;
clearBtn.disabled = true;
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
fetch('/daily_mirror/clear_delivery', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
// Reload the page to show empty table
loadDeliveryData(1);
} else {
alert('❌ Error: ' + (data.message || 'Failed to clear delivery records'));
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ Error clearing delivery records: ' + error.message);
})
.finally(() => {
clearBtn.disabled = false;
clearBtn.innerHTML = originalText;
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,753 @@
{% extends "base.html" %}
{% block title %}Tune Orders Data - Daily Mirror{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/daily_mirror_tune.css') }}">
<style>
/* Force modal width - using viewport width for maximum responsiveness */
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
margin: 1.75rem auto !important;
}
.modal#editModal .modal-dialog.modal-xl {
max-width: 95vw !important;
width: 95vw !important;
}
/* Override ALL Bootstrap media queries */
@media (min-width: 576px) {
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
}
}
@media (min-width: 992px) {
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
}
}
@media (min-width: 1200px) {
.modal#editModal .modal-dialog,
.modal#editModal .modal-dialog.modal-xl {
max-width: 95vw !important;
width: 95vw !important;
}
}
/* Ensure pointer events work */
.modal#editModal .modal-dialog {
pointer-events: auto !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">🛒 Tune Orders Data</h1>
<p class="text-muted">Edit and update customer orders information</p>
</div>
<div>
<!-- Buttons removed; now present in top header -->
</div>
</div>
</div>
</div>
<!-- Filters Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-filter"></i> Filters and Search
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<label for="searchInput" class="form-label">Search</label>
<input type="text" class="form-control" id="searchInput"
placeholder="Search by order, customer, or article...">
</div>
<div class="col-md-3 mb-3">
<label for="statusFilter" class="form-label">Order Status</label>
<select class="form-control" id="statusFilter">
<option value="">All Statuses</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="customerFilter" class="form-label">Customer</label>
<select class="form-control" id="customerFilter">
<option value="">All Customers</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="recordsPerPage" class="form-label">Records per page</label>
<select class="form-control" id="recordsPerPage">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div class="row">
<div class="col-12">
<button class="btn btn-primary" onclick="loadOrdersData()">
<i class="fas fa-search"></i> Apply Filters
</button>
<button class="btn btn-secondary" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Data Table Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-table"></i> Customer Orders Data
</h5>
<div class="d-flex align-items-center">
<span id="recordsInfo" class="text-muted me-3"></span>
{% if session.get('role') == 'superadmin' %}
<button class="btn btn-danger btn-sm me-2" onclick="clearAllOrders()" id="clearAllBtn">
<i class="fas fa-trash-alt"></i> Clear All Orders
</button>
{% endif %}
<button class="btn btn-success btn-sm" onclick="saveAllChanges()">
<i class="fas fa-save"></i> Save All Changes
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="ordersTable">
<thead class="table-dark">
<tr>
<th>Order Line</th>
<th>Customer</th>
<th>Client Order Line</th>
<th>Article Code</th>
<th>Description</th>
<th>Quantity</th>
<th>Delivery Date</th>
<th>Status</th>
<th>Priority</th>
<th>Product Group</th>
<th>Order Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="ordersTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<!-- Loading indicator -->
<div id="loadingIndicator" class="text-center py-4" style="display: none;">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p class="mt-2">Loading data...</p>
</div>
<!-- No data message -->
<div id="noDataMessage" class="text-center py-4" style="display: none;">
<i class="fas fa-info-circle fa-2x text-muted"></i>
<p class="mt-2 text-muted">No orders found</p>
</div>
</div>
<!-- Pagination -->
<div class="card-footer">
<nav aria-label="Orders data pagination">
<ul class="pagination pagination-sm justify-content-center mb-0" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable" style="max-width: 95vw !important; width: 95vw !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit Customer Order</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editForm">
<input type="hidden" id="editRecordId">
<div class="row">
<!-- Column 1: Order Information -->
<div class="col-md-4">
<div class="mb-3">
<label for="editOrderLine" class="form-label">Order Line</label>
<input type="text" class="form-control" id="editOrderLine" readonly>
</div>
<div class="mb-3">
<label for="editOrderId" class="form-label">Order ID</label>
<input type="text" class="form-control" id="editOrderId">
</div>
<div class="mb-3">
<label for="editLineNumber" class="form-label">Line Number</label>
<input type="text" class="form-control" id="editLineNumber">
</div>
<div class="mb-3">
<label for="editClientOrderLine" class="form-label">Client Order Line</label>
<input type="text" class="form-control" id="editClientOrderLine" readonly>
</div>
<div class="mb-3">
<label for="editOrderDate" class="form-label">Order Date</label>
<input type="date" class="form-control" id="editOrderDate">
</div>
<div class="mb-3">
<label for="editDeliveryDate" class="form-label">Delivery Date</label>
<input type="date" class="form-control" id="editDeliveryDate">
</div>
<div class="mb-3">
<label for="editOrderStatus" class="form-label">Order Status</label>
<select class="form-control" id="editOrderStatus">
<option value="PENDING">Pending</option>
<option value="CONFIRMED">Confirmed</option>
<option value="Confirmat">Confirmat</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="FINISHED">Finished</option>
<option value="DELIVERED">Delivered</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<div class="mb-3">
<label for="editPriority" class="form-label">Priority</label>
<select class="form-control" id="editPriority">
<option value="LOW">Low</option>
<option value="NORMAL">Normal</option>
<option value="HIGH">High</option>
<option value="URGENT">Urgent</option>
</select>
</div>
</div>
<!-- Column 2: Customer and Article Information -->
<div class="col-md-4">
<div class="mb-3">
<label for="editCustomerCode" class="form-label">Customer Code</label>
<input type="text" class="form-control" id="editCustomerCode">
</div>
<div class="mb-3">
<label for="editCustomerName" class="form-label">Customer Name</label>
<input type="text" class="form-control" id="editCustomerName">
</div>
<div class="mb-3">
<label for="editArticleCode" class="form-label">Article Code</label>
<input type="text" class="form-control" id="editArticleCode">
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">Article Description</label>
<textarea class="form-control" id="editDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="editQuantity" class="form-label">Quantity Requested</label>
<input type="number" class="form-control" id="editQuantity">
</div>
<div class="mb-3">
<label for="editBalance" class="form-label">Balance</label>
<input type="number" step="0.01" class="form-control" id="editBalance">
</div>
<div class="mb-3">
<label for="editUnitOfMeasure" class="form-label">Unit of Measure</label>
<input type="text" class="form-control" id="editUnitOfMeasure">
</div>
<div class="mb-3">
<label for="editArticleStatus" class="form-label">Article Status</label>
<input type="text" class="form-control" id="editArticleStatus">
</div>
</div>
<!-- Column 3: Production Information -->
<div class="col-md-4">
<div class="mb-3">
<label for="editProductGroup" class="form-label">Product Group</label>
<input type="text" class="form-control" id="editProductGroup">
</div>
<div class="mb-3">
<label for="editModel" class="form-label">Model</label>
<input type="text" class="form-control" id="editModel">
</div>
<div class="mb-3">
<label for="editProductionOrder" class="form-label">Production Order</label>
<input type="text" class="form-control" id="editProductionOrder">
</div>
<div class="mb-3">
<label for="editProductionStatus" class="form-label">Production Status</label>
<input type="text" class="form-control" id="editProductionStatus">
</div>
<div class="mb-3">
<label for="editClosed" class="form-label">Closed</label>
<input type="text" class="form-control" id="editClosed">
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer d-flex justify-content-between">
<button type="button" class="btn btn-danger" onclick="deleteRecord()" style="min-width: 150px;">
<i class="fas fa-trash"></i> Delete Record
</button>
<div>
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal" style="min-width: 100px;">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveRecord()" style="min-width: 150px;">
<i class="fas fa-save"></i> Save Changes
</button>
</div>
</div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentPerPage = 50;
let currentSearch = '';
let currentStatusFilter = '';
let currentCustomerFilter = '';
document.addEventListener('DOMContentLoaded', function() {
// Initialize theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
}
// Load initial data
loadOrdersData();
// Setup search on enter key
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
loadOrdersData();
}
});
});
function loadOrdersData(page = 1) {
currentPage = page;
currentPerPage = document.getElementById('recordsPerPage').value;
currentSearch = document.getElementById('searchInput').value;
currentStatusFilter = document.getElementById('statusFilter').value;
currentCustomerFilter = document.getElementById('customerFilter').value;
// Show loading indicator
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('ordersTableBody').style.display = 'none';
document.getElementById('noDataMessage').style.display = 'none';
const params = new URLSearchParams({
page: currentPage,
per_page: currentPerPage,
search: currentSearch,
status: currentStatusFilter,
customer: currentCustomerFilter
});
fetch(`/daily_mirror/api/tune/orders_data?${params}`)
.then(response => response.json())
.then(data => {
document.getElementById('loadingIndicator').style.display = 'none';
if (data.success) {
if (data.data.length === 0) {
document.getElementById('noDataMessage').style.display = 'block';
} else {
displayOrdersData(data.data);
updatePagination(data);
updateRecordsInfo(data);
// Populate filter dropdowns on first load
if (currentPage === 1) {
populateCustomerFilter(data.customers);
populateStatusFilter(data.statuses);
}
}
} else {
console.error('Error loading data:', data.error);
alert('Error loading orders data: ' + data.error);
}
})
.catch(error => {
document.getElementById('loadingIndicator').style.display = 'none';
console.error('Error:', error);
alert('Error loading orders data: ' + error.message);
});
}
function displayOrdersData(data) {
const tbody = document.getElementById('ordersTableBody');
tbody.innerHTML = '';
tbody.style.display = 'table-row-group';
data.forEach(record => {
const row = document.createElement('tr');
row.innerHTML = `
<td><strong>${record.order_line}</strong></td>
<td>
<small class="text-muted d-block">${record.customer_code}</small>
${record.customer_name}
</td>
<td>${record.client_order_line || '-'}</td>
<td><code>${record.article_code}</code></td>
<td><small>${record.article_description || '-'}</small></td>
<td><span class="badge bg-info">${record.quantity_requested}</span></td>
<td>${record.delivery_date || '-'}</td>
<td><span class="badge bg-primary">${record.order_status}</span></td>
<td><span class="badge bg-warning">${record.priority || 'NORMAL'}</span></td>
<td><small>${record.product_group || '-'}</small></td>
<td>${record.order_date || '-'}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="editRecord(${record.id})"
title="Edit Order">
<i class="fas fa-edit"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
}
function populateCustomerFilter(customers) {
const filter = document.getElementById('customerFilter');
// Keep the "All Customers" option and add new ones
const currentValue = filter.value;
filter.innerHTML = '<option value="">All Customers</option>';
customers.forEach(customer => {
const option = document.createElement('option');
option.value = customer.code;
option.textContent = `${customer.code} - ${customer.name}`;
filter.appendChild(option);
});
filter.value = currentValue;
}
function populateStatusFilter(statuses) {
const filter = document.getElementById('statusFilter');
const currentValue = filter.value;
filter.innerHTML = '<option value="">All Statuses</option>';
statuses.forEach(status => {
const option = document.createElement('option');
option.value = status;
option.textContent = status;
filter.appendChild(option);
});
filter.value = currentValue;
}
function updatePagination(data) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
if (data.total_pages <= 1) return;
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${data.page === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadOrdersData(${data.page - 1})">Previous</a>`;
pagination.appendChild(prevLi);
// Page numbers
const startPage = Math.max(1, data.page - 2);
const endPage = Math.min(data.total_pages, data.page + 2);
for (let i = startPage; i <= endPage; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === data.page ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" onclick="loadOrdersData(${i})">${i}</a>`;
pagination.appendChild(li);
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${data.page === data.total_pages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadOrdersData(${data.page + 1})">Next</a>`;
pagination.appendChild(nextLi);
}
function updateRecordsInfo(data) {
const start = (data.page - 1) * data.per_page + 1;
const end = Math.min(data.page * data.per_page, data.total_records);
document.getElementById('recordsInfo').textContent =
`Showing ${start}-${end} of ${data.total_records} orders`;
}
function clearFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('customerFilter').value = '';
loadOrdersData(1);
}
function editRecord(recordId) {
// Find the record data from the current display
const rows = document.querySelectorAll('#ordersTableBody tr');
let recordData = null;
// Get data via API for editing
fetch(`/daily_mirror/api/tune/orders_data?page=${currentPage}&per_page=${currentPerPage}&search=${currentSearch}&status=${currentStatusFilter}&customer=${currentCustomerFilter}`)
.then(response => response.json())
.then(data => {
if (data.success) {
recordData = data.data.find(record => record.id === recordId);
if (recordData) {
populateEditModal(recordData);
// Get modal elements
const modalElement = document.getElementById('editModal');
const modalDialog = modalElement.querySelector('.modal-dialog');
// Remove any existing modal instances to prevent conflicts
const existingModal = bootstrap.Modal.getInstance(modalElement);
if (existingModal) {
existingModal.dispose();
}
const modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true
});
// Show the modal first
modal.show();
// Force the modal dialog width AFTER modal is shown - using 95% of viewport width
setTimeout(() => {
if (modalDialog) {
console.log('Applying width after modal shown');
modalDialog.style.setProperty('max-width', '95vw', 'important');
modalDialog.style.setProperty('width', '95vw', 'important');
modalDialog.style.setProperty('margin', '1.75rem auto', 'important');
// Also apply to modal content for good measure
const modalContent = modalDialog.querySelector('.modal-content');
if (modalContent) {
modalContent.style.setProperty('width', '100%', 'important');
}
console.log('Modal dialog computed width:', window.getComputedStyle(modalDialog).width);
}
}, 100);
}
}
})
.catch(error => {
console.error('Error:', error);
alert('Error loading record data: ' + error.message);
});
}
function populateEditModal(record) {
document.getElementById('editRecordId').value = record.id;
document.getElementById('editOrderLine').value = record.order_line;
document.getElementById('editOrderId').value = record.order_id;
document.getElementById('editLineNumber').value = record.line_number || '';
document.getElementById('editCustomerCode').value = record.customer_code;
document.getElementById('editCustomerName').value = record.customer_name;
document.getElementById('editClientOrderLine').value = record.client_order_line || '';
document.getElementById('editArticleCode').value = record.article_code;
document.getElementById('editDescription').value = record.article_description || '';
document.getElementById('editQuantity').value = record.quantity_requested;
document.getElementById('editBalance').value = record.balance || '';
document.getElementById('editUnitOfMeasure').value = record.unit_of_measure || '';
document.getElementById('editDeliveryDate').value = record.delivery_date;
document.getElementById('editOrderDate').value = record.order_date;
document.getElementById('editOrderStatus').value = record.order_status;
document.getElementById('editArticleStatus').value = record.article_status || '';
document.getElementById('editPriority').value = record.priority || 'NORMAL';
document.getElementById('editProductGroup').value = record.product_group || '';
document.getElementById('editProductionOrder').value = record.production_order || '';
document.getElementById('editProductionStatus').value = record.production_status || '';
document.getElementById('editModel').value = record.model || '';
document.getElementById('editClosed').value = record.closed || '';
}
function saveRecord() {
const recordId = document.getElementById('editRecordId').value;
const data = {
order_line: document.getElementById('editOrderLine').value,
order_id: document.getElementById('editOrderId').value,
line_number: document.getElementById('editLineNumber').value,
customer_code: document.getElementById('editCustomerCode').value,
customer_name: document.getElementById('editCustomerName').value,
client_order_line: document.getElementById('editClientOrderLine').value,
article_code: document.getElementById('editArticleCode').value,
article_description: document.getElementById('editDescription').value,
quantity_requested: parseInt(document.getElementById('editQuantity').value) || 0,
balance: parseFloat(document.getElementById('editBalance').value) || null,
unit_of_measure: document.getElementById('editUnitOfMeasure').value,
delivery_date: document.getElementById('editDeliveryDate').value,
order_date: document.getElementById('editOrderDate').value,
order_status: document.getElementById('editOrderStatus').value,
article_status: document.getElementById('editArticleStatus').value,
priority: document.getElementById('editPriority').value,
product_group: document.getElementById('editProductGroup').value,
production_order: document.getElementById('editProductionOrder').value,
production_status: document.getElementById('editProductionStatus').value,
model: document.getElementById('editModel').value,
closed: document.getElementById('editClosed').value
};
fetch(`/daily_mirror/api/tune/orders_data/${recordId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
// Close modal
const editModal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
editModal.hide();
// Reload data
loadOrdersData(currentPage);
// Show success message
alert('Order updated successfully!');
} else {
alert('Error updating order: ' + result.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error updating order: ' + error.message);
});
}
function deleteRecord() {
const recordId = document.getElementById('editRecordId').value;
const orderLine = document.getElementById('editOrderLine').value;
if (!confirm(`⚠️ Are you sure you want to delete order line "${orderLine}"?\n\nThis action cannot be undone.`)) {
return;
}
fetch(`/daily_mirror/api/tune/orders_data/${recordId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
// Close modal and reload data
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
modal.hide();
alert('Record deleted successfully!');
loadOrdersData(currentPage);
})
.catch(error => {
console.error('Error deleting record:', error);
alert('Error deleting record: ' + error.message);
});
}
function saveAllChanges() {
alert('Save All Changes functionality will be implemented for bulk operations.');
}
function clearAllOrders() {
if (!confirm('⚠️ WARNING: This will permanently delete ALL customer orders from the database!\n\nAre you absolutely sure you want to continue?')) {
return;
}
// Second confirmation for safety
if (!confirm('This action CANNOT be undone! All customer order data will be lost.\n\nClick OK to proceed with deletion.')) {
return;
}
const clearBtn = document.getElementById('clearAllBtn');
const originalText = clearBtn.innerHTML;
clearBtn.disabled = true;
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
fetch('/daily_mirror/clear_orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
// Reload the page to show empty table
loadOrdersData(1);
} else {
alert('❌ Error: ' + (data.message || 'Failed to clear orders'));
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ Error clearing orders: ' + error.message);
})
.finally(() => {
clearBtn.disabled = false;
clearBtn.innerHTML = originalText;
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,683 @@
{% extends "base.html" %}
{% block title %}Tune Production Data - Daily Mirror{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/daily_mirror_tune.css') }}">
<style>
/* Force modal width - using viewport width for maximum responsiveness */
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
margin: 1.75rem auto !important;
}
.modal#editModal .modal-dialog.modal-xl {
max-width: 95vw !important;
width: 95vw !important;
}
/* Override ALL Bootstrap media queries */
@media (min-width: 576px) {
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
}
}
@media (min-width: 992px) {
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
}
}
@media (min-width: 1200px) {
.modal#editModal .modal-dialog,
.modal#editModal .modal-dialog.modal-xl {
max-width: 95vw !important;
width: 95vw !important;
}
}
/* Ensure pointer events work */
.modal#editModal .modal-dialog {
pointer-events: auto !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">🏭 Tune Production Data</h1>
<p class="text-muted">Edit and update production orders information</p>
</div>
<div>
<!-- Buttons removed; now present in top header -->
</div>
</div>
</div>
</div>
<!-- Filters Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-filter"></i> Filters and Search
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<label for="searchInput" class="form-label">Search</label>
<input type="text" class="form-control" id="searchInput"
placeholder="Search by order, customer, or article...">
</div>
<div class="col-md-3 mb-3">
<label for="statusFilter" class="form-label">Production Status</label>
<select class="form-control" id="statusFilter">
<option value="">All Statuses</option>
<option value="PENDING">Pending</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="FINISHED">Finished</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label for="customerFilter" class="form-label">Customer</label>
<select class="form-control" id="customerFilter">
<option value="">All Customers</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="recordsPerPage" class="form-label">Records per page</label>
<select class="form-control" id="recordsPerPage">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div class="row">
<div class="col-12">
<button class="btn btn-primary" onclick="loadProductionData()">
<i class="fas fa-search"></i> Apply Filters
</button>
<button class="btn btn-secondary" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Data Table Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-table"></i> Production Orders Data
</h5>
<div class="d-flex align-items-center">
<span id="recordsInfo" class="text-muted me-3"></span>
{% if session.get('role') == 'superadmin' %}
<button class="btn btn-danger btn-sm me-2" onclick="clearAllProductionOrders()" id="clearAllBtn">
<i class="fas fa-trash-alt"></i> Clear All Orders
</button>
{% endif %}
<button class="btn btn-success btn-sm" onclick="saveAllChanges()">
<i class="fas fa-save"></i> Save All Changes
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="productionTable">
<thead class="table-dark">
<tr>
<th>Production Order</th>
<th>Opened for Order</th>
<th>Client Order-Line</th>
<th>Customer</th>
<th>Article Code</th>
<th>Description</th>
<th>Quantity</th>
<th>Delivery Date</th>
<th>Status</th>
<th>Machine</th>
<th>Planning Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="productionTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<!-- Loading indicator -->
<div id="loadingIndicator" class="text-center py-4" style="display: none;">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p class="mt-2">Loading data...</p>
</div>
<!-- No data message -->
<div id="noDataMessage" class="text-center py-4" style="display: none;">
<i class="fas fa-info-circle fa-2x text-muted"></i>
<p class="mt-2 text-muted">No production orders found</p>
</div>
</div>
<!-- Pagination -->
<div class="card-footer">
<nav aria-label="Production data pagination">
<ul class="pagination pagination-sm justify-content-center mb-0" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable" style="max-width: 95vw !important; width: 95vw !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit Production Order</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="modalCloseBtn"></button>
</div>
<div class="modal-body">
<form id="editForm">
<input type="hidden" id="editRecordId">
<!-- Production Order (Full Width) -->
<div class="row mb-3">
<div class="col-12">
<label for="editProductionOrder" class="form-label fw-bold">Production Order</label>
<input type="text" class="form-control form-control-lg" id="editProductionOrder" readonly>
</div>
</div>
<hr class="my-3">
<!-- Three Column Layout -->
<div class="row">
<!-- Column 1 -->
<div class="col-md-4">
<div class="mb-3">
<label for="editOpenForOrderLine" class="form-label">Opened for Order-Line</label>
<input type="text" class="form-control" id="editOpenForOrderLine" readonly>
</div>
<div class="mb-3">
<label for="editCustomerCode" class="form-label">Customer Code</label>
<input type="text" class="form-control" id="editCustomerCode">
</div>
<div class="mb-3">
<label for="editArticleCode" class="form-label">Article Code</label>
<input type="text" class="form-control" id="editArticleCode" readonly>
</div>
<div class="mb-3">
<label for="editDeliveryDate" class="form-label">Delivery Date</label>
<input type="date" class="form-control" id="editDeliveryDate">
</div>
</div>
<!-- Column 2 -->
<div class="col-md-4">
<div class="mb-3">
<label for="editClientOrderLine" class="form-label">Client Order-Line</label>
<input type="text" class="form-control" id="editClientOrderLine" readonly>
</div>
<div class="mb-3">
<label for="editCustomerName" class="form-label">Customer Name</label>
<input type="text" class="form-control" id="editCustomerName">
</div>
<div class="mb-3">
<label for="editQuantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="editQuantity">
</div>
<div class="mb-3">
<label for="editStatus" class="form-label">Production Status</label>
<select class="form-control" id="editStatus">
<option value="PENDING">Pending</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="FINISHED">Finished</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
</div>
<!-- Column 3 -->
<div class="col-md-4">
<div class="mb-3">
<label for="editMachine" class="form-label">Machine Code</label>
<input type="text" class="form-control" id="editMachine">
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">Article Description</label>
<textarea class="form-control" id="editDescription" rows="6"></textarea>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer d-flex justify-content-between">
<button type="button" class="btn btn-danger" onclick="deleteRecord()" style="min-width: 150px;">
<i class="fas fa-trash"></i> Delete Record
</button>
<div>
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal" style="min-width: 100px;">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveRecord()" style="min-width: 150px;">
<i class="fas fa-save"></i> Save Changes
</button>
</div>
</div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentData = [];
let hasChanges = false;
document.addEventListener('DOMContentLoaded', function() {
// Initialize theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
}
// Load initial data
loadProductionData();
});
function loadProductionData(page = 1) {
currentPage = page;
// Show loading
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('productionTableBody').innerHTML = '';
document.getElementById('noDataMessage').style.display = 'none';
// Get filter values
const search = document.getElementById('searchInput').value;
const status = document.getElementById('statusFilter').value;
const customer = document.getElementById('customerFilter').value;
const perPage = document.getElementById('recordsPerPage').value;
// Build query parameters
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (search) params.append('search', search);
if (status) params.append('status', status);
if (customer) params.append('customer', customer);
// Fetch data
fetch(`/daily_mirror/api/tune/production_data?${params}`)
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
currentData = data.records;
renderTable(data);
renderPagination(data);
updateRecordsInfo(data);
})
.catch(error => {
console.error('Error loading production data:', error);
alert('Error loading data: ' + error.message);
})
.finally(() => {
document.getElementById('loadingIndicator').style.display = 'none';
});
}
function renderTable(data) {
const tbody = document.getElementById('productionTableBody');
if (data.records.length === 0) {
document.getElementById('noDataMessage').style.display = 'block';
return;
}
tbody.innerHTML = data.records.map((record, index) => `
<tr id="row-${record.id}">
<td><strong>${record.production_order}</strong></td>
<td>${record.open_for_order_line || ''}</td>
<td>${record.client_order_line || ''}</td>
<td>${record.customer_code}<br><small class="text-muted">${record.customer_name || ''}</small></td>
<td>${record.article_code || ''}</td>
<td><small>${record.article_description || ''}</small></td>
<td>${record.quantity_requested || ''}</td>
<td>${record.delivery_date || ''}</td>
<td><span class="badge bg-${getStatusColor(record.production_status)}">${record.production_status || ''}</span></td>
<td>${record.machine_code || ''}</td>
<td>${record.data_planificare || ''}</td>
<td>
<button class="btn btn-primary btn-action btn-sm" onclick="editRecord(${record.id})" title="Edit">
<i class="fas fa-edit"></i>
</button>
</td>
</tr>
`).join('');
}
function getStatusColor(status) {
switch(status) {
case 'PENDING': return 'warning';
case 'IN_PROGRESS': return 'info';
case 'FINISHED': return 'success';
case 'CANCELLED': return 'danger';
default: return 'secondary';
}
}
function renderPagination(data) {
const pagination = document.getElementById('pagination');
if (data.total_pages <= 1) {
pagination.innerHTML = '';
return;
}
let paginationHTML = '';
// Previous button
if (data.page > 1) {
paginationHTML += `<li class="page-item"><a class="page-link" href="#" onclick="loadProductionData(${data.page - 1})">Previous</a></li>`;
}
// Page numbers
for (let i = Math.max(1, data.page - 2); i <= Math.min(data.total_pages, data.page + 2); i++) {
const active = i === data.page ? 'active' : '';
paginationHTML += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="loadProductionData(${i})">${i}</a></li>`;
}
// Next button
if (data.page < data.total_pages) {
paginationHTML += `<li class="page-item"><a class="page-link" href="#" onclick="loadProductionData(${data.page + 1})">Next</a></li>`;
}
pagination.innerHTML = paginationHTML;
}
function updateRecordsInfo(data) {
const info = document.getElementById('recordsInfo');
const start = (data.page - 1) * data.per_page + 1;
const end = Math.min(data.page * data.per_page, data.total);
info.textContent = `Showing ${start}-${end} of ${data.total} records`;
}
function editRecord(recordId) {
const record = currentData.find(r => r.id === recordId);
if (!record) return;
// Populate the edit form
document.getElementById('editRecordId').value = record.id;
document.getElementById('editProductionOrder').value = record.production_order;
document.getElementById('editOpenForOrderLine').value = record.open_for_order_line || '';
document.getElementById('editClientOrderLine').value = record.client_order_line || '';
document.getElementById('editCustomerCode').value = record.customer_code || '';
document.getElementById('editCustomerName').value = record.customer_name || '';
document.getElementById('editArticleCode').value = record.article_code || '';
document.getElementById('editDescription').value = record.article_description || '';
document.getElementById('editQuantity').value = record.quantity_requested || '';
document.getElementById('editDeliveryDate').value = record.delivery_date || '';
document.getElementById('editStatus').value = record.production_status || '';
document.getElementById('editMachine').value = record.machine_code || '';
// Explicitly enable all editable fields
const editableFields = ['editCustomerCode',
'editCustomerName', 'editDescription', 'editQuantity',
'editDeliveryDate', 'editStatus', 'editMachine'];
editableFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.disabled = false;
field.removeAttribute('disabled');
field.removeAttribute('readonly');
field.style.backgroundColor = '#ffffff';
field.style.color = '#000000';
field.style.opacity = '1';
field.style.pointerEvents = 'auto';
field.style.cursor = 'text';
field.style.userSelect = 'text';
field.tabIndex = 0;
}
});
// Show the modal with proper configuration
const modalElement = document.getElementById('editModal');
const modalDialog = modalElement.querySelector('.modal-dialog');
// Remove any existing modal instances to prevent conflicts
const existingModal = bootstrap.Modal.getInstance(modalElement);
if (existingModal) {
existingModal.dispose();
}
const modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true
});
// Show the modal first
modal.show();
// Force the modal dialog width AFTER modal is shown - using 95% of viewport width
setTimeout(() => {
if (modalDialog) {
console.log('Applying width after modal shown');
modalDialog.style.setProperty('max-width', '95vw', 'important');
modalDialog.style.setProperty('width', '95vw', 'important');
modalDialog.style.setProperty('margin', '1.75rem auto', 'important');
// Also apply to modal content for good measure
const modalContent = modalDialog.querySelector('.modal-content');
if (modalContent) {
modalContent.style.setProperty('width', '100%', 'important');
}
console.log('Modal dialog computed width:', window.getComputedStyle(modalDialog).width);
}
}, 100);
// Ensure form inputs are focusable and interactive after modal is shown
modalElement.addEventListener('shown.bs.modal', function () {
// Re-enable all fields after modal animation completes
editableFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.disabled = false;
field.removeAttribute('disabled');
field.style.pointerEvents = 'auto';
}
});
// Focus on the first editable field
const firstField = document.getElementById('editCustomerCode');
if (firstField) {
setTimeout(() => {
firstField.focus();
firstField.select();
}, 100);
}
}, { once: true });
}
function saveRecord() {
const recordId = document.getElementById('editRecordId').value;
const formData = {
open_for_order_line: document.getElementById('editOpenForOrderLine').value,
client_order_line: document.getElementById('editClientOrderLine').value,
customer_code: document.getElementById('editCustomerCode').value,
customer_name: document.getElementById('editCustomerName').value,
article_code: document.getElementById('editArticleCode').value,
article_description: document.getElementById('editDescription').value,
quantity_requested: parseInt(document.getElementById('editQuantity').value) || 0,
delivery_date: document.getElementById('editDeliveryDate').value,
production_status: document.getElementById('editStatus').value,
machine_code: document.getElementById('editMachine').value
};
fetch(`/daily_mirror/api/tune/production_data/${recordId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
// Close modal and reload data
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
modal.hide();
alert('Record updated successfully!');
loadProductionData(currentPage);
})
.catch(error => {
console.error('Error saving record:', error);
alert('Error saving record: ' + error.message);
});
}
function deleteRecord() {
const recordId = document.getElementById('editRecordId').value;
const productionOrder = document.getElementById('editProductionOrder').value;
if (!confirm(`⚠️ Are you sure you want to delete production order "${productionOrder}"?\n\nThis action cannot be undone.`)) {
return;
}
fetch(`/daily_mirror/api/tune/production_data/${recordId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
// Close modal and reload data
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
modal.hide();
alert('Record deleted successfully!');
loadProductionData(currentPage);
})
.catch(error => {
console.error('Error deleting record:', error);
alert('Error deleting record: ' + error.message);
});
}
function clearFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('customerFilter').value = '';
loadProductionData(1);
}
function saveAllChanges() {
alert('Bulk save functionality will be implemented in a future update!');
}
function clearAllProductionOrders() {
if (!confirm('⚠️ WARNING: This will permanently delete ALL production orders from the database!\n\nAre you absolutely sure you want to continue?')) {
return;
}
// Second confirmation for safety
if (!confirm('This action CANNOT be undone! All production order data will be lost.\n\nClick OK to proceed with deletion.')) {
return;
}
const clearBtn = document.getElementById('clearAllBtn');
const originalText = clearBtn.innerHTML;
clearBtn.disabled = true;
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
fetch('/daily_mirror/clear_production_orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
// Reload the page to show empty table
loadProductionData(1);
} else {
alert('❌ Error: ' + (data.message || 'Failed to clear production orders'));
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ Error clearing production orders: ' + error.message);
})
.finally(() => {
clearBtn.disabled = false;
clearBtn.innerHTML = originalText;
});
}
// Add event listeners for real-time filtering
document.getElementById('searchInput').addEventListener('input', function() {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
loadProductionData(1);
}, 500);
});
document.getElementById('statusFilter').addEventListener('change', function() {
loadProductionData(1);
});
document.getElementById('customerFilter').addEventListener('change', function() {
loadProductionData(1);
});
document.getElementById('recordsPerPage').addEventListener('change', function() {
loadProductionData(1);
});
</script>
{% endblock %}

View File

@@ -50,6 +50,72 @@
Recommended: Use the simplified user management for easier administration
</small>
</div>
{% if session.role in ['superadmin', 'admin'] %}
<div class="card" style="margin-top: 32px;">
<h3>💾 Database Backup Management</h3>
<p><strong>Automated Backup System:</strong> Schedule and manage database backups</p>
<!-- Backup Controls -->
<div style="margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 8px;">
<h4 style="margin-top: 0;">Quick Actions</h4>
<button id="backup-now-btn" class="btn" style="background-color: #4caf50; color: white; margin-right: 10px;">
⚡ Backup Now
</button>
<button id="refresh-backups-btn" class="btn" style="background-color: #2196f3; color: white;">
🔄 Refresh List
</button>
</div>
<!-- Schedule Configuration -->
<div style="margin-top: 20px; padding: 15px; background: #f9f9f9; border-radius: 8px;">
<h4 style="margin-top: 0;">Backup Schedule</h4>
<form id="backup-schedule-form" style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div>
<label for="schedule-enabled">
<input type="checkbox" id="schedule-enabled" name="enabled"> Enable Scheduled Backups
</label>
</div>
<div>
<label for="schedule-time">Backup Time:</label>
<input type="time" id="schedule-time" name="time" value="02:00">
</div>
<div>
<label for="schedule-frequency">Frequency:</label>
<select id="schedule-frequency" name="frequency">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<div>
<label for="retention-days">Keep backups for (days):</label>
<input type="number" id="retention-days" name="retention_days" value="30" min="1" max="365">
</div>
<div style="grid-column: span 2;">
<button type="submit" class="btn" style="background-color: #ff9800; color: white;">
💾 Save Schedule
</button>
</div>
</form>
</div>
<!-- Backup List -->
<div style="margin-top: 20px;">
<h4>Available Backups</h4>
<div id="backup-list" style="max-height: 400px; overflow-y: auto;">
<p style="text-align: center; color: #999;">Loading backups...</p>
</div>
</div>
<!-- Backup Path Info -->
<div style="margin-top: 15px; padding: 10px; background: #e3f2fd; border-left: 4px solid #2196f3; border-radius: 4px;">
<strong> Backup Location:</strong> <code id="backup-path-display">/srv/quality_app/backups</code>
<br>
<small>Configure backup path in docker-compose.yml (BACKUP_PATH environment variable)</small>
</div>
</div>
{% endif %}
</div>
<!-- Popup for creating/editing a user -->
@@ -136,5 +202,162 @@ Array.from(document.getElementsByClassName('delete-user-btn')).forEach(function(
document.getElementById('close-delete-popup-btn').onclick = function() {
document.getElementById('delete-user-popup').style.display = 'none';
};
// ========================================
// Database Backup Management Functions
// ========================================
// Load backup schedule on page load
function loadBackupSchedule() {
fetch('/api/backup/schedule')
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('schedule-enabled').checked = data.schedule.enabled;
document.getElementById('schedule-time').value = data.schedule.time;
document.getElementById('schedule-frequency').value = data.schedule.frequency;
document.getElementById('retention-days').value = data.schedule.retention_days;
}
})
.catch(error => console.error('Error loading schedule:', error));
}
// Load backup list
function loadBackupList() {
const backupList = document.getElementById('backup-list');
if (!backupList) return;
backupList.innerHTML = '<p style="text-align: center; color: #999;">Loading backups...</p>';
fetch('/api/backup/list')
.then(response => response.json())
.then(data => {
if (data.success && data.backups.length > 0) {
let html = '<table style="width: 100%; border-collapse: collapse;">';
html += '<thead><tr style="background: #f0f0f0;"><th style="padding: 10px; text-align: left;">Filename</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead>';
html += '<tbody>';
data.backups.forEach(backup => {
html += `<tr style="border-bottom: 1px solid #ddd;">
<td style="padding: 10px;">${backup.filename}</td>
<td style="text-align: center;">${backup.size_mb} MB</td>
<td style="text-align: center;">${backup.created}</td>
<td style="text-align: center;">
<button onclick="downloadBackup('${backup.filename}')" class="btn" style="background: #2196f3; color: white; padding: 5px 10px; margin: 2px;">⬇️ Download</button>
<button onclick="deleteBackup('${backup.filename}')" class="btn" style="background: #f44336; color: white; padding: 5px 10px; margin: 2px;">🗑️ Delete</button>
</td>
</tr>`;
});
html += '</tbody></table>';
backupList.innerHTML = html;
} else {
backupList.innerHTML = '<p style="text-align: center; color: #999;">No backups available</p>';
}
})
.catch(error => {
console.error('Error loading backups:', error);
backupList.innerHTML = '<p style="text-align: center; color: #f44336;">Error loading backups</p>';
});
}
// Backup now button
document.getElementById('backup-now-btn')?.addEventListener('click', function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = '⏳ Creating backup...';
fetch('/api/backup/create', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message + '\nFile: ' + data.filename + '\nSize: ' + data.size);
loadBackupList();
} else {
alert('❌ ' + data.message);
}
btn.disabled = false;
btn.innerHTML = '⚡ Backup Now';
})
.catch(error => {
console.error('Error creating backup:', error);
alert('❌ Failed to create backup');
btn.disabled = false;
btn.innerHTML = '⚡ Backup Now';
});
});
// Refresh backups button
document.getElementById('refresh-backups-btn')?.addEventListener('click', function() {
loadBackupList();
});
// Save schedule form
document.getElementById('backup-schedule-form')?.addEventListener('submit', function(e) {
e.preventDefault();
const formData = {
enabled: document.getElementById('schedule-enabled').checked,
time: document.getElementById('schedule-time').value,
frequency: document.getElementById('schedule-frequency').value,
retention_days: parseInt(document.getElementById('retention-days').value)
};
fetch('/api/backup/schedule', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
} else {
alert('❌ ' + data.message);
}
})
.catch(error => {
console.error('Error saving schedule:', error);
alert('❌ Failed to save schedule');
});
});
// Download backup function
function downloadBackup(filename) {
window.location.href = `/api/backup/download/${filename}`;
}
// Delete backup function
function deleteBackup(filename) {
if (confirm(`Are you sure you want to delete backup: ${filename}?`)) {
fetch(`/api/backup/delete/${filename}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
loadBackupList();
} else {
alert('❌ ' + data.message);
}
})
.catch(error => {
console.error('Error deleting backup:', error);
alert('❌ Failed to delete backup');
});
}
}
// Load backup data on page load
if (document.getElementById('backup-list')) {
loadBackupSchedule();
loadBackupList();
}
</script>
{% endblock %}

View File

@@ -355,6 +355,100 @@
background-color: #2a2a2a !important;
color: #fff !important;
}
/* Modal fixes for accessibility */
.modal {
z-index: 1050 !important;
}
.modal-backdrop {
z-index: 1040 !important;
}
/* Theme-aware modal styling */
body.light-mode .modal-content {
background-color: #fff !important;
color: #000 !important;
}
body.dark-mode .modal-content {
background-color: #2a2a2a !important;
color: #fff !important;
border: 1px solid #444 !important;
}
body.light-mode .modal-header {
background-color: #f8f9fa !important;
border-bottom: 1px solid #dee2e6 !important;
}
body.dark-mode .modal-header {
background-color: #1e1e1e !important;
border-bottom: 1px solid #444 !important;
}
body.light-mode .modal-title {
color: #000 !important;
}
body.dark-mode .modal-title {
color: #fff !important;
}
body.light-mode .modal-body {
background-color: #fff !important;
color: #000 !important;
}
body.dark-mode .modal-body {
background-color: #2a2a2a !important;
color: #fff !important;
}
body.light-mode .modal-footer {
background-color: #f8f9fa !important;
border-top: 1px solid #dee2e6 !important;
}
body.dark-mode .modal-footer {
background-color: #1e1e1e !important;
border-top: 1px solid #444 !important;
}
/* Modal form elements */
body.light-mode .modal .form-control {
background-color: #fff !important;
color: #000 !important;
border-color: #ced4da !important;
}
body.dark-mode .modal .form-control {
background-color: #1a1a1a !important;
color: #fff !important;
border-color: #444 !important;
}
body.light-mode .modal label {
color: #000 !important;
}
body.dark-mode .modal label {
color: #ccc !important;
}
/* Ensure modal is clickable and not greyed out */
.modal.show {
display: block !important;
pointer-events: auto !important;
}
.modal-dialog {
pointer-events: auto !important;
}
.modal-content {
pointer-events: auto !important;
}
</style>
{% endblock %}
@@ -408,6 +502,10 @@
<input type="checkbox" id="module_labels" name="modules" value="labels">
<label for="module_labels">Label Management</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="module_daily_mirror" name="modules" value="daily_mirror">
<label for="module_daily_mirror">Daily Mirror (BI & Reports)</label>
</div>
</div>
<div id="accessLevelInfo" class="access-level-info" style="display: none;"></div>
</div>
@@ -454,6 +552,10 @@
<input type="checkbox" id="quick_module_labels" name="quick_modules" value="labels">
<label for="quick_module_labels">Label Management</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="quick_module_daily_mirror" name="quick_modules" value="daily_mirror">
<label for="quick_module_daily_mirror">Daily Mirror (BI & Reports)</label>
</div>
</div>
</div>
@@ -621,6 +723,10 @@
<input type="checkbox" id="edit_module_labels" name="modules" value="labels">
<label for="edit_module_labels">Label Management</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="edit_module_daily_mirror" name="modules" value="daily_mirror">
<label for="edit_module_daily_mirror">Daily Mirror (BI & Reports)</label>
</div>
</div>
<div id="editAccessLevelInfo" class="access-level-info" style="display: none;"></div>
</div>

View File

@@ -46,8 +46,10 @@ max_requests_jitter = int(os.getenv("GUNICORN_MAX_REQUESTS_JITTER", "100"))
# LOGGING CONFIGURATION
# ============================================================================
# Docker-friendly: logs to stdout/stderr by default, but allow file logging
accesslog = os.getenv("GUNICORN_ACCESS_LOG", "/srv/quality_recticel/logs/access.log")
errorlog = os.getenv("GUNICORN_ERROR_LOG", "/srv/quality_recticel/logs/error.log")
# Automatically detect the correct log path based on current directory
default_log_dir = "/srv/quality_app/logs" if "/srv/quality_app" in os.getcwd() else "/srv/quality_recticel/logs"
accesslog = os.getenv("GUNICORN_ACCESS_LOG", f"{default_log_dir}/access.log")
errorlog = os.getenv("GUNICORN_ERROR_LOG", f"{default_log_dir}/error.log")
# For pure Docker logging (12-factor app), use:
# accesslog = "-" # stdout
@@ -162,4 +164,4 @@ def worker_abort(worker):
def child_exit(server, worker):
"""Called just after a worker has been exited, in the master process."""
server.log.info("👋 Worker %s exited (exit code: %s)", worker.pid, worker.tmp.last_mtime)
server.log.info("👋 Worker %s exited", worker.pid)

View File

@@ -3,3 +3,4 @@ port=3306
database_name=trasabilitate
username=trasabilitate
password=Initial01!
backup_path=/srv/quality_app/backups

View File

@@ -2,7 +2,6 @@ Flask
Flask-SSLify
Werkzeug
gunicorn
flask-sqlalchemy
pyodbc
mariadb
reportlab

View File

@@ -1 +1 @@
394337
399048