updated documentation folder
This commit is contained in:
@@ -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
74
README.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
205
documentation/BACKUP_SYSTEM.md
Normal file
205
documentation/BACKUP_SYSTEM.md
Normal 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
148
documentation/README.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
770
logs/error.log
770
logs/error.log
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
431
py_app/app/database_backup.py
Normal file
431
py_app/app/database_backup.py
Normal 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)}'
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
447
py_app/app/templates/daily_mirror.html
Normal file
447
py_app/app/templates/daily_mirror.html
Normal 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 %}
|
||||
760
py_app/app/templates/daily_mirror_build_database.html
Normal file
760
py_app/app/templates/daily_mirror_build_database.html
Normal 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 %}
|
||||
449
py_app/app/templates/daily_mirror_history.html
Normal file
449
py_app/app/templates/daily_mirror_history.html
Normal 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 %}
|
||||
262
py_app/app/templates/daily_mirror_main.html
Normal file
262
py_app/app/templates/daily_mirror_main.html
Normal 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 %}
|
||||
549
py_app/app/templates/daily_mirror_tune_delivery.html
Normal file
549
py_app/app/templates/daily_mirror_tune_delivery.html
Normal 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 %}
|
||||
753
py_app/app/templates/daily_mirror_tune_orders.html
Normal file
753
py_app/app/templates/daily_mirror_tune_orders.html
Normal 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 %}
|
||||
683
py_app/app/templates/daily_mirror_tune_production.html
Normal file
683
py_app/app/templates/daily_mirror_tune_production.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -3,3 +3,4 @@ port=3306
|
||||
database_name=trasabilitate
|
||||
username=trasabilitate
|
||||
password=Initial01!
|
||||
backup_path=/srv/quality_app/backups
|
||||
|
||||
@@ -2,7 +2,6 @@ Flask
|
||||
Flask-SSLify
|
||||
Werkzeug
|
||||
gunicorn
|
||||
flask-sqlalchemy
|
||||
pyodbc
|
||||
mariadb
|
||||
reportlab
|
||||
|
||||
@@ -1 +1 @@
|
||||
394337
|
||||
399048
|
||||
|
||||
Reference in New Issue
Block a user