Files
digiserver/app/templates/upload_content.html
ske087 1eb0aa3658 feat: v1.1.0 - Production-Ready Docker Deployment
🚀 Major Release: DigiServer v1.1.0 Production Deployment

## 📁 Project Restructure
- Moved all application code to app/ directory for Docker containerization
- Centralized persistent data in data/ directory with volume mounting
- Removed development artifacts and cleaned up project structure

## 🐳 Docker Integration
- Added production-ready Dockerfile with LibreOffice and poppler-utils
- Updated docker-compose.yml for production deployment
- Added .dockerignore for optimized build context
- Created automated deployment script (deploy-docker.sh)
- Added cleanup script (cleanup-docker.sh)

## 📄 Document Processing Enhancements
- Integrated LibreOffice for professional PPTX to PDF conversion
- Implemented PPTX → PDF → 4K JPG workflow for optimal quality
- Added poppler-utils for enhanced PDF processing
- Simplified PDF conversion to 300 DPI for reliability

## 🔧 File Management Improvements
- Fixed absolute path resolution for containerized deployment
- Updated all file deletion functions with proper path handling
- Enhanced bulk delete functions for players and groups
- Improved file upload workflow with consistent path management

## 🛠️ Code Quality & Stability
- Cleaned up pptx_converter.py from 442 to 86 lines
- Removed all Python cache files (__pycache__/, *.pyc)
- Updated file operations for production reliability
- Enhanced error handling and logging

## 📚 Documentation Updates
- Updated README.md with Docker deployment instructions
- Added comprehensive DEPLOYMENT.md guide
- Included production deployment best practices
- Added automated deployment workflow documentation

## 🔐 Security & Production Features
- Environment-based configuration
- Health checks and container monitoring
- Automated admin user creation
- Volume-mounted persistent data
- Production logging and error handling

##  Ready for Production
- Clean project structure optimized for Docker
- Automated deployment with ./deploy-docker.sh
- Professional document processing pipeline
- Reliable file management system
- Complete documentation and deployment guides

Access: http://localhost:8880 | Admin: admin/Initial01!
2025-08-05 18:04:02 -04:00

464 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Content</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body.dark-mode {
background-color: #121212;
color: #ffffff;
}
.card.dark-mode {
background-color: #1e1e1e;
color: #ffffff;
}
.dark-mode label, .dark-mode th, .dark-mode td {
color: #ffffff;
}
.logo {
max-height: 100px;
margin-right: 20px;
}
/* Modal styling for dark mode */
.modal-content.dark-mode {
background-color: #1e1e1e;
color: #ffffff;
}
.modal-header.dark-mode {
border-bottom: 1px solid #444;
}
.modal-footer.dark-mode {
border-top: 1px solid #444;
}
.progress-bar {
background-color: #007bff;
}
@media (max-width: 768px) {
h1 {
font-size: 1.5rem;
}
.btn {
font-size: 0.9rem;
padding: 0.5rem 1rem;
}
.card {
margin-bottom: 1rem;
}
}
</style>
</head>
<body class="{{ 'dark-mode' if theme == 'dark' else '' }}">
<div class="container py-5">
<div class="d-flex justify-content-start align-items-center mb-4">
{% if logo_exists %}
<img src="{{ url_for('static', filename='uploads/logo.png') }}" alt="Logo" class="logo">
{% endif %}
<h1 class="mb-0">Upload Content</h1>
</div>
<form id="upload-form" action="{{ url_for('upload_content') }}" method="post" enctype="multipart/form-data" onsubmit="showStatusModal()">
<input type="hidden" name="return_url" value="{{ return_url }}">
<div class="row">
<div class="col-md-6 col-12">
<div class="mb-3">
<label for="target_type" class="form-label">Target Type:</label>
<select name="target_type" id="target_type" class="form-select" required onchange="updateTargetIdOptions()">
<option value="" disabled selected>Select Target Type</option>
<option value="player" {% if target_type == 'player' %}selected{% endif %}>Player</option>
<option value="group" {% if target_type == 'group' %}selected{% endif %}>Group</option>
</select>
</div>
</div>
<div class="col-md-6 col-12">
<div class="mb-3">
<label for="target_id" class="form-label">Target ID:</label>
<select name="target_id" id="target_id" class="form-select" required>
{% if target_type == 'player' %}
<optgroup label="Players">
{% for player in players %}
<option value="{{ player.id }}" {% if target_id == player.id %}selected{% endif %}>{{ player.username }}</option>
{% endfor %}
</optgroup>
{% elif target_type == 'group' %}
<optgroup label="Groups">
{% for group in groups %}
<option value="{{ group.id }}" {% if target_id == group.id %}selected{% endif %}>{{ group.name }}</option>
{% endfor %}
</optgroup>
{% else %}
<option value="" disabled selected>Select a Target ID</option>
{% endif %}
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 col-12">
<div class="mb-3">
<label for="media_type" class="form-label">Media Type:</label>
<select name="media_type" id="media_type" class="form-select" required>
<option value="image">Image</option>
<option value="video">Video</option>
<option value="pdf">PDF</option>
<option value="ppt">PPT/PPTX</option>
</select>
</div>
</div>
<div class="col-md-6 col-12">
<div class="mb-3">
<label for="files" class="form-label">Files:</label>
<input type="file" name="files" id="files" class="form-control" multiple required onchange="handleFileChange()">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 col-12">
<div class="mb-3">
<label for="duration" class="form-label">Duration (seconds):</label>
<input type="number" name="duration" id="duration" class="form-control" required>
</div>
</div>
</div>
<div class="text-center">
<button type="submit" id="submit-button" class="btn btn-primary">Upload</button>
<a href="{{ return_url }}" class="btn btn-secondary mt-3">Back</a>
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary mt-3">Back to Dashboard</a>
</div>
</form>
<!-- Modal for Status Updates -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content {{ 'dark-mode' if theme == 'dark' else '' }}">
<div class="modal-header {{ 'dark-mode' if theme == 'dark' else '' }}">
<h5 class="modal-title" id="statusModalLabel">Processing Files</h5>
</div>
<div class="modal-body">
<p id="status-message">Uploading and processing your files. Please wait...</p>
<!-- File Processing Progress -->
<div class="mb-3">
<label class="form-label fw-bold">File Processing Progress</label>
<div class="progress" style="height: 25px;">
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<!-- System Monitoring in Modal -->
{% if system_info %}
<div class="mt-4">
<h6 class="mb-3">📊 Server Performance During Upload</h6>
<div class="row">
<!-- CPU Usage -->
<div class="col-md-4 col-12 mb-3">
<label class="form-label">CPU Usage</label>
<div class="progress" style="height: 20px;">
<div id="cpu-progress" class="progress-bar
{% if system_info.cpu_percent < 50 %}bg-success
{% elif system_info.cpu_percent < 80 %}bg-warning
{% else %}bg-danger{% endif %}"
role="progressbar"
style="width: {{ system_info.cpu_percent }}%;">
{{ system_info.cpu_percent }}%
</div>
</div>
<small class="text-muted">{{ system_info.cpu_count }} cores available</small>
</div>
<!-- Memory Usage -->
<div class="col-md-4 col-12 mb-3">
<label class="form-label">Memory Usage</label>
<div class="progress" style="height: 20px;">
<div id="memory-progress" class="progress-bar
{% if system_info.memory_percent < 60 %}bg-success
{% elif system_info.memory_percent < 85 %}bg-warning
{% else %}bg-danger{% endif %}"
role="progressbar"
style="width: {{ system_info.memory_percent }}%;">
{{ system_info.memory_percent }}%
</div>
</div>
<small class="text-muted" id="memory-text">{{ system_info.memory_used }}GB / {{ system_info.memory_total }}GB</small>
</div>
<!-- Disk Usage -->
<div class="col-md-4 col-12 mb-3">
<label class="form-label">Disk Space</label>
<div class="progress" style="height: 20px;">
<div id="disk-progress" class="progress-bar
{% if system_info.disk_percent < 70 %}bg-success
{% elif system_info.disk_percent < 90 %}bg-warning
{% else %}bg-danger{% endif %}"
role="progressbar"
style="width: {{ system_info.disk_percent }}%;">
{{ system_info.disk_percent }}%
</div>
</div>
<small class="text-muted" id="disk-text">{{ system_info.disk_free }}GB free</small>
</div>
</div>
<!-- Storage Summary -->
<div class="row mt-2">
<div class="col-md-6 col-12 text-center">
<strong>Current Media Storage:</strong>
<span class="text-primary" id="storage-size">{{ system_info.upload_folder_size }}GB</span>
</div>
<div class="col-md-6 col-12 text-center">
<strong>Last Updated:</strong>
<span class="text-muted" id="modal-last-update">Just now</span>
</div>
</div>
</div>
{% endif %}
</div>
<div class="modal-footer {{ 'dark-mode' if theme == 'dark' else '' }}">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" disabled>Close</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function showStatusModal() {
console.log("Processing popup triggered");
const statusModal = new bootstrap.Modal(document.getElementById('statusModal'));
statusModal.show();
// Update status message based on media type
const mediaType = document.getElementById('media_type').value;
const statusMessage = document.getElementById('status-message');
switch(mediaType) {
case 'image':
statusMessage.textContent = 'Uploading images...';
break;
case 'video':
statusMessage.textContent = 'Uploading and processing video. This may take a while...';
break;
case 'pdf':
statusMessage.textContent = 'Converting PDF to 4K images. This may take a while...';
break;
case 'ppt':
statusMessage.textContent = 'Converting PowerPoint to 4K images. This may take a while...';
break;
default:
statusMessage.textContent = 'Uploading and processing your files. Please wait...';
}
// Start system monitoring updates in modal
{% if system_info %}
startModalSystemMonitoring();
{% endif %}
// Simulate progress updates
const progressBar = document.getElementById('progress-bar');
let progress = 0;
const interval = setInterval(() => {
// For slow processes, increment more slowly
const increment = (mediaType === 'image') ? 20 : 5;
progress += increment;
if (progress >= 100) {
clearInterval(interval);
statusMessage.textContent = 'Files uploaded and processed successfully!';
// Stop system monitoring updates
{% if system_info %}
stopModalSystemMonitoring();
{% endif %}
// Enable the close button
document.querySelector('[data-bs-dismiss="modal"]').disabled = false;
} else {
progressBar.style.width = `${progress}%`;
progressBar.setAttribute('aria-valuenow', progress);
}
}, 500);
}
{% if system_info %}
let modalSystemInterval;
function updateModalSystemInfo() {
fetch('/api/system_info')
.then(response => response.json())
.then(data => {
if (data.error) {
console.warn('Could not fetch system info:', data.error);
return;
}
// Update CPU
const cpuProgress = document.getElementById('cpu-progress');
if (cpuProgress) {
cpuProgress.style.width = data.cpu_percent + '%';
cpuProgress.textContent = data.cpu_percent + '%';
cpuProgress.className = 'progress-bar ' +
(data.cpu_percent < 50 ? 'bg-success' :
data.cpu_percent < 80 ? 'bg-warning' : 'bg-danger');
}
// Update Memory
const memoryProgress = document.getElementById('memory-progress');
const memoryText = document.getElementById('memory-text');
if (memoryProgress) {
memoryProgress.style.width = data.memory_percent + '%';
memoryProgress.textContent = data.memory_percent + '%';
memoryProgress.className = 'progress-bar ' +
(data.memory_percent < 60 ? 'bg-success' :
data.memory_percent < 85 ? 'bg-warning' : 'bg-danger');
}
if (memoryText) {
memoryText.textContent = data.memory_used + 'GB / ' + data.memory_total + 'GB';
}
// Update Disk
const diskProgress = document.getElementById('disk-progress');
const diskText = document.getElementById('disk-text');
if (diskProgress) {
diskProgress.style.width = data.disk_percent + '%';
diskProgress.textContent = data.disk_percent + '%';
diskProgress.className = 'progress-bar ' +
(data.disk_percent < 70 ? 'bg-success' :
data.disk_percent < 90 ? 'bg-warning' : 'bg-danger');
}
if (diskText) {
diskText.textContent = data.disk_free + 'GB free';
}
// Update storage size
const storageSize = document.getElementById('storage-size');
if (storageSize) {
storageSize.textContent = data.upload_folder_size + 'GB';
}
// Update timestamp
const lastUpdate = document.getElementById('modal-last-update');
if (lastUpdate) {
lastUpdate.textContent = new Date().toLocaleTimeString();
}
})
.catch(error => {
console.warn('Modal system monitoring update failed:', error);
});
}
function startModalSystemMonitoring() {
// Update immediately
updateModalSystemInfo();
// Then update every 3 seconds for real-time monitoring during upload
modalSystemInterval = setInterval(updateModalSystemInfo, 3000);
}
function stopModalSystemMonitoring() {
if (modalSystemInterval) {
clearInterval(modalSystemInterval);
modalSystemInterval = null;
}
}
{% endif %}
function updateTargetIdOptions() {
const targetType = document.getElementById('target_type').value;
const targetIdSelect = document.getElementById('target_id');
targetIdSelect.innerHTML = ''; // Clear existing options
if (targetType === 'player') {
const players = {{ players|tojson }};
const optgroup = document.createElement('optgroup');
optgroup.label = 'Players';
players.forEach(player => {
const option = document.createElement('option');
option.value = player.id;
option.textContent = player.username;
optgroup.appendChild(option);
});
targetIdSelect.appendChild(optgroup);
} else if (targetType === 'group') {
const groups = {{ groups|tojson }};
const optgroup = document.createElement('optgroup');
optgroup.label = 'Groups';
groups.forEach(group => {
const option = document.createElement('option');
option.value = group.id;
option.textContent = group.name;
optgroup.appendChild(option);
});
targetIdSelect.appendChild(optgroup);
}
}
function handleFileChange() {
const mediaType = document.getElementById('media_type').value;
const filesInput = document.getElementById('files');
const durationInput = document.getElementById('duration');
if (mediaType === 'video' && filesInput.files.length > 0) {
const file = filesInput.files[0];
const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = function () {
window.URL.revokeObjectURL(video.src);
const duration = Math.round(video.duration);
durationInput.value = duration; // Set the duration in the input field
};
video.src = URL.createObjectURL(file);
}
}
function showStatusModal() {
console.log("Processing popup triggered");
const statusModal = new bootstrap.Modal(document.getElementById('statusModal'));
statusModal.show();
// Update status message based on media type
const mediaType = document.getElementById('media_type').value;
const statusMessage = document.getElementById('status-message');
switch(mediaType) {
case 'image':
statusMessage.textContent = 'Uploading images...';
break;
case 'video':
statusMessage.textContent = 'Uploading and processing video. This may take a while...';
break;
case 'pdf':
statusMessage.textContent = 'Converting PDF to images. This may take a while...';
break;
case 'ppt':
statusMessage.textContent = 'Converting PowerPoint to images. This may take a while...';
break;
default:
statusMessage.textContent = 'Uploading and processing your files. Please wait...';
}
// Simulate progress updates
const progressBar = document.getElementById('progress-bar');
let progress = 0;
const interval = setInterval(() => {
// For slow processes, increment more slowly
const increment = (mediaType === 'image') ? 20 : 5;
progress += increment;
if (progress >= 100) {
clearInterval(interval);
statusMessage.textContent = 'Files uploaded and processed successfully!';
// Enable the close button
document.querySelector('[data-bs-dismiss="modal"]').disabled = false;
} else {
progressBar.style.width = `${progress}%`;
progressBar.setAttribute('aria-valuenow', progress);
}
}, 500);
}
</script>
</body>
</html>