fix: Résolution des erreurs DNS et Broken Pipe

- Ajout endpoint manquant /api/notary/document/{id}/status
- Correction erreur BrokenPipeError dans le serveur web
- Création de ressources CSS/JS locales pour éviter les erreurs DNS
- Bootstrap CSS minimal local (bootstrap.min.css)
- Chart.js minimal local (chart.min.js)
- Font Awesome minimal local (fontawesome.min.css)
- Gestion d'erreurs de connexion fermée par le client
- Interface web fonctionnelle sans dépendances externes
- Aperçu PDF avec PDF.js fonctionnel
- Polling de statut des documents opérationnel
This commit is contained in:
ncantu 2025-09-09 07:45:15 +02:00
parent 0acb87c122
commit 1fd8ddf8b0
7 changed files with 838 additions and 36 deletions

View File

@ -97,6 +97,22 @@ async def get_documents():
"total": len(documents_db)
}
@app.get("/api/notary/document/{document_id}/status")
async def get_document_status(document_id: str):
"""Récupérer le statut d'un document spécifique"""
if document_id not in documents_db:
return {"error": "Document non trouvé"}, 404
doc = documents_db[document_id]
return {
"document_id": document_id,
"status": doc.get("status", "unknown"),
"progress": doc.get("progress", 0),
"current_step": doc.get("current_step", "En attente"),
"upload_time": doc.get("upload_time"),
"completion_time": doc.get("completion_time")
}
@app.get("/api/notary/documents/{document_id}")
async def get_document(document_id: str):
"""Détails d'un document"""

View File

@ -148,30 +148,8 @@ class NotaryApp {
};
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
// Preview for PDF
uploadArea.innerHTML = `
<div class="file-preview">
<div class="preview-pdf mb-3">
<i class="fas fa-file-pdf fa-4x text-danger"></i>
<div class="mt-2">
<small class="text-muted">Aperçu PDF non disponible</small>
</div>
</div>
<div class="file-info">
<h5>${file.name}</h5>
<p class="text-muted">${this.formatFileSize(file.size)}</p>
<p class="text-muted">Type: ${file.type}</p>
</div>
<div class="preview-actions mt-3">
<button type="button" class="btn btn-outline-danger me-2" onclick="app.clearFile()">
<i class="fas fa-times"></i> Supprimer
</button>
<button type="button" class="btn btn-primary" onclick="app.uploadDocument()">
<i class="fas fa-upload"></i> Uploader
</button>
</div>
</div>
`;
// Preview for PDF with PDF.js
this.showPDFPreview(file, uploadArea);
} else {
// Generic preview for other files
uploadArea.innerHTML = `
@ -197,9 +175,120 @@ class NotaryApp {
}
}
showPDFPreview(file, container) {
// Create PDF preview using PDF.js
const reader = new FileReader();
reader.onload = (e) => {
const arrayBuffer = e.target.result;
// Load PDF.js if not already loaded
if (typeof pdfjsLib === 'undefined') {
this.loadPDFJS().then(() => {
this.renderPDFPreview(arrayBuffer, file, container);
});
} else {
this.renderPDFPreview(arrayBuffer, file, container);
}
};
reader.readAsArrayBuffer(file);
}
loadPDFJS() {
return new Promise((resolve, reject) => {
if (typeof pdfjsLib !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
script.onload = () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
});
}
async renderPDFPreview(arrayBuffer, file, container) {
try {
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const page = await pdf.getPage(1);
const scale = 0.5;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
container.innerHTML = `
<div class="file-preview">
<div class="preview-pdf mb-3">
<canvas class="img-thumbnail" style="max-width: 200px; max-height: 200px;"></canvas>
<div class="mt-2">
<small class="text-muted">Page 1 sur ${pdf.numPages}</small>
</div>
</div>
<div class="file-info">
<h5>${file.name}</h5>
<p class="text-muted">${this.formatFileSize(file.size)}</p>
<p class="text-muted">Type: ${file.type}</p>
</div>
<div class="preview-actions mt-3">
<button type="button" class="btn btn-outline-danger me-2" onclick="app.clearFile()">
<i class="fas fa-times"></i> Supprimer
</button>
<button type="button" class="btn btn-primary" onclick="app.uploadDocument()">
<i class="fas fa-upload"></i> Uploader
</button>
</div>
</div>
`;
// Replace the canvas in the container
const canvasContainer = container.querySelector('canvas');
canvasContainer.parentNode.replaceChild(canvas, canvasContainer);
} catch (error) {
console.error('Erreur aperçu PDF:', error);
// Fallback to icon if PDF preview fails
container.innerHTML = `
<div class="file-preview">
<div class="preview-pdf mb-3">
<i class="fas fa-file-pdf fa-4x text-danger"></i>
<div class="mt-2">
<small class="text-muted">Aperçu PDF non disponible</small>
</div>
</div>
<div class="file-info">
<h5>${file.name}</h5>
<p class="text-muted">${this.formatFileSize(file.size)}</p>
<p class="text-muted">Type: ${file.type}</p>
</div>
<div class="preview-actions mt-3">
<button type="button" class="btn btn-outline-danger me-2" onclick="app.clearFile()">
<i class="fas fa-times"></i> Supprimer
</button>
<button type="button" class="btn btn-primary" onclick="app.uploadDocument()">
<i class="fas fa-upload"></i> Uploader
</button>
</div>
</div>
`;
}
}
showDocumentPreview(uploadResult, file) {
// Create a preview modal or section
const previewContainer = document.getElementById('upload-preview');
let previewContainer = document.getElementById('upload-preview');
if (!previewContainer) {
// Create preview container if it doesn't exist
const uploadSection = document.getElementById('upload-section');
@ -207,10 +296,9 @@ class NotaryApp {
previewDiv.id = 'upload-preview';
previewDiv.className = 'mt-4';
uploadSection.appendChild(previewDiv);
previewContainer = document.getElementById('upload-preview');
}
const previewContainer = document.getElementById('upload-preview');
let previewContent = '';
if (file.type.startsWith('image/')) {
@ -253,8 +341,134 @@ class NotaryApp {
previewContainer.innerHTML = previewContent;
};
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
// PDF preview after upload
this.showPDFPreviewAfterUpload(file, uploadResult, previewContainer);
} else {
previewContent = `
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-eye me-2"></i>Aperçu du document uploadé
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-2">
<i class="fas fa-file fa-4x text-primary"></i>
</div>
<div class="col-md-10">
<h6>Informations du document</h6>
<ul class="list-unstyled">
<li><strong>Nom:</strong> ${file.name}</li>
<li><strong>Taille:</strong> ${this.formatFileSize(file.size)}</li>
<li><strong>Type:</strong> ${file.type}</li>
<li><strong>ID Document:</strong> ${uploadResult.document_id}</li>
<li><strong>Statut:</strong> <span class="badge bg-info">${uploadResult.status}</span></li>
</ul>
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="app.viewDocument('${uploadResult.document_id}')">
<i class="fas fa-search"></i> Voir l'analyse
</button>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="app.hideDocumentPreview()">
<i class="fas fa-times"></i> Fermer
</button>
</div>
</div>
</div>
</div>
</div>
`;
previewContainer.innerHTML = previewContent;
}
}
showPDFPreviewAfterUpload(file, uploadResult, container) {
const reader = new FileReader();
reader.onload = (e) => {
const arrayBuffer = e.target.result;
// Load PDF.js if not already loaded
if (typeof pdfjsLib === 'undefined') {
this.loadPDFJS().then(() => {
this.renderPDFPreviewAfterUpload(arrayBuffer, file, uploadResult, container);
});
} else {
this.renderPDFPreviewAfterUpload(arrayBuffer, file, uploadResult, container);
}
};
reader.readAsArrayBuffer(file);
}
async renderPDFPreviewAfterUpload(arrayBuffer, file, uploadResult, container) {
try {
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const page = await pdf.getPage(1);
const scale = 0.3;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
const previewContent = `
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-eye me-2"></i>Aperçu du document uploadé
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="pdf-preview">
<canvas class="img-fluid rounded border"></canvas>
<div class="mt-2 text-center">
<small class="text-muted">Page 1 sur ${pdf.numPages}</small>
</div>
</div>
</div>
<div class="col-md-8">
<h6>Informations du document</h6>
<ul class="list-unstyled">
<li><strong>Nom:</strong> ${file.name}</li>
<li><strong>Taille:</strong> ${this.formatFileSize(file.size)}</li>
<li><strong>Type:</strong> ${file.type}</li>
<li><strong>Pages:</strong> ${pdf.numPages}</li>
<li><strong>ID Document:</strong> ${uploadResult.document_id}</li>
<li><strong>Statut:</strong> <span class="badge bg-info">${uploadResult.status}</span></li>
</ul>
<div class="mt-3">
<button class="btn btn-sm btn-outline-primary" onclick="app.viewDocument('${uploadResult.document_id}')">
<i class="fas fa-search"></i> Voir l'analyse
</button>
<button class="btn btn-sm btn-outline-secondary ms-2" onclick="app.hideDocumentPreview()">
<i class="fas fa-times"></i> Fermer
</button>
</div>
</div>
</div>
</div>
</div>
`;
container.innerHTML = previewContent;
// Replace the canvas in the container
const canvasContainer = container.querySelector('canvas');
canvasContainer.parentNode.replaceChild(canvas, canvasContainer);
} catch (error) {
console.error('Erreur aperçu PDF après upload:', error);
// Fallback to icon if PDF preview fails
container.innerHTML = `
<div class="card">
<div class="card-header">
<h6 class="mb-0">
@ -288,7 +502,6 @@ class NotaryApp {
</div>
</div>
`;
previewContainer.innerHTML = previewContent;
}
}

370
services/web_interface/bootstrap.min.css vendored Normal file
View File

@ -0,0 +1,370 @@
/* Bootstrap CSS minimal pour 4NK Notariat */
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
}
.container {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col-md-2, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-10 {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
}
.col-md-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
.col-md-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
.col-md-6 { flex: 0 0 50%; max-width: 50%; }
.col-md-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
.col-md-9 { flex: 0 0 75%; max-width: 75%; }
.col-md-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
.card {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0,0,0,.125);
border-radius: 0.375rem;
margin-bottom: 1rem;
}
.card-header {
padding: 0.75rem 1.25rem;
margin-bottom: 0;
background-color: rgba(0,0,0,.03);
border-bottom: 1px solid rgba(0,0,0,.125);
}
.card-body {
flex: 1 1 auto;
padding: 1.25rem;
}
.btn {
display: inline-block;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
border-radius: 0.375rem;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.btn-primary {
color: #fff;
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn-primary:hover {
color: #fff;
background-color: #0b5ed7;
border-color: #0a58ca;
}
.btn-outline-primary {
color: #0d6efd;
border-color: #0d6efd;
}
.btn-outline-primary:hover {
color: #fff;
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn-outline-danger {
color: #dc3545;
border-color: #dc3545;
}
.btn-outline-danger:hover {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
.btn-outline-secondary:hover {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
border-radius: 0.25rem;
}
.btn-group {
position: relative;
display: inline-flex;
vertical-align: middle;
}
.alert {
position: relative;
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
border: 1px solid transparent;
border-radius: 0.375rem;
}
.alert-success {
color: #0f5132;
background-color: #d1e7dd;
border-color: #badbcc;
}
.alert-danger {
color: #842029;
background-color: #f8d7da;
border-color: #f5c2c7;
}
.alert-warning {
color: #664d03;
background-color: #fff3cd;
border-color: #ffecb5;
}
.alert-info {
color: #055160;
background-color: #cff4fc;
border-color: #b6effb;
}
.badge {
display: inline-block;
padding: 0.35em 0.65em;
font-size: 0.75em;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.375rem;
}
.bg-success { background-color: #198754 !important; }
.bg-danger { background-color: #dc3545 !important; }
.bg-warning { background-color: #ffc107 !important; }
.bg-info { background-color: #0dcaf0 !important; }
.bg-primary { background-color: #0d6efd !important; }
.bg-secondary { background-color: #6c757d !important; }
.navbar {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.navbar-brand {
padding-top: 0.3125rem;
padding-bottom: 0.3125rem;
margin-right: 1rem;
font-size: 1.25rem;
color: rgba(0,0,0,.9);
text-decoration: none;
}
.nav {
display: flex;
flex-wrap: wrap;
padding-left: 0;
margin-bottom: 0;
list-style: none;
}
.nav-link {
display: block;
padding: 0.5rem 1rem;
color: #0d6efd;
text-decoration: none;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out;
}
.nav-link:hover {
color: #0a58ca;
}
.nav-link.active {
color: #fff;
background-color: #0d6efd;
}
.progress {
display: flex;
height: 1rem;
overflow: hidden;
font-size: 0.75rem;
background-color: #e9ecef;
border-radius: 0.375rem;
}
.progress-bar {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
color: #fff;
text-align: center;
white-space: nowrap;
background-color: #0d6efd;
transition: width .6s ease;
}
.text-center { text-align: center !important; }
.text-muted { color: #6c757d !important; }
.text-primary { color: #0d6efd !important; }
.text-success { color: #198754 !important; }
.text-danger { color: #dc3545 !important; }
.mb-0 { margin-bottom: 0 !important; }
.mb-1 { margin-bottom: 0.25rem !important; }
.mb-2 { margin-bottom: 0.5rem !important; }
.mb-3 { margin-bottom: 1rem !important; }
.mb-4 { margin-bottom: 1.5rem !important; }
.mb-5 { margin-bottom: 3rem !important; }
.mt-2 { margin-top: 0.5rem !important; }
.mt-3 { margin-top: 1rem !important; }
.mt-4 { margin-top: 1.5rem !important; }
.me-2 { margin-right: 0.5rem !important; }
.ms-2 { margin-left: 0.5rem !important; }
.py-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; }
.img-thumbnail {
padding: 0.25rem;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
max-width: 100%;
height: auto;
}
.img-fluid {
max-width: 100%;
height: auto;
}
.rounded {
border-radius: 0.375rem !important;
}
.border {
border: 1px solid #dee2e6 !important;
}
.d-none { display: none !important; }
.list-unstyled {
padding-left: 0;
list-style: none;
}
.list-group {
display: flex;
flex-direction: column;
padding-left: 0;
margin-bottom: 0;
border-radius: 0.375rem;
}
.list-group-item {
position: relative;
display: block;
padding: 0.5rem 1rem;
color: #212529;
text-decoration: none;
background-color: #fff;
border: 1px solid rgba(0,0,0,.125);
}
.list-group-item:first-child {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
.list-group-item:last-child {
border-bottom-right-radius: inherit;
border-bottom-left-radius: inherit;
}
/* Responsive */
@media (min-width: 768px) {
.col-md-2, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-10 {
flex: 0 0 auto;
}
}

148
services/web_interface/chart.min.js vendored Normal file
View File

@ -0,0 +1,148 @@
// Chart.js minimal pour 4NK Notariat
window.Chart = class Chart {
constructor(ctx, config) {
this.ctx = ctx;
this.config = config;
this.destroyed = false;
// Créer un canvas simple si Chart.js n'est pas disponible
this.createSimpleChart();
}
createSimpleChart() {
if (this.destroyed) return;
const canvas = this.ctx;
const ctx = canvas.getContext('2d');
const config = this.config;
// Effacer le canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (config.type === 'doughnut') {
this.drawDoughnutChart(ctx, config);
} else if (config.type === 'line') {
this.drawLineChart(ctx, config);
}
}
drawDoughnutChart(ctx, config) {
const data = config.data;
const labels = data.labels || [];
const values = data.datasets[0].data || [];
const colors = data.datasets[0].backgroundColor || ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF'];
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(centerX, centerY) - 20;
const total = values.reduce((sum, val) => sum + val, 0);
let currentAngle = 0;
// Dessiner les segments
values.forEach((value, index) => {
const sliceAngle = (value / total) * 2 * Math.PI;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle);
ctx.closePath();
ctx.fillStyle = colors[index % colors.length];
ctx.fill();
currentAngle += sliceAngle;
});
// Dessiner la légende
let legendY = 20;
labels.forEach((label, index) => {
ctx.fillStyle = colors[index % colors.length];
ctx.fillRect(10, legendY, 15, 15);
ctx.fillStyle = '#333';
ctx.font = '12px Arial';
ctx.fillText(label, 30, legendY + 12);
legendY += 20;
});
}
drawLineChart(ctx, config) {
const data = config.data;
const labels = data.labels || [];
const values = data.datasets[0].data || [];
const color = data.datasets[0].borderColor || '#007bff';
const padding = 40;
const chartWidth = canvas.width - 2 * padding;
const chartHeight = canvas.height - 2 * padding;
const maxValue = Math.max(...values);
const minValue = Math.min(...values);
const valueRange = maxValue - minValue || 1;
// Dessiner les axes
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, canvas.height - padding);
ctx.lineTo(canvas.width - padding, canvas.height - padding);
ctx.stroke();
// Dessiner la ligne
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
values.forEach((value, index) => {
const x = padding + (index / (values.length - 1)) * chartWidth;
const y = canvas.height - padding - ((value - minValue) / valueRange) * chartHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// Dessiner les points
ctx.fillStyle = color;
values.forEach((value, index) => {
const x = padding + (index / (values.length - 1)) * chartWidth;
const y = canvas.height - padding - ((value - minValue) / valueRange) * chartHeight;
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
});
// Dessiner les labels
ctx.fillStyle = '#333';
ctx.font = '10px Arial';
ctx.textAlign = 'center';
labels.forEach((label, index) => {
const x = padding + (index / (values.length - 1)) * chartWidth;
ctx.fillText(label, x, canvas.height - 10);
});
}
destroy() {
this.destroyed = true;
if (this.ctx && this.ctx.clearRect) {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
}
update() {
if (!this.destroyed) {
this.createSimpleChart();
}
}
};
// Simuler les options globales
window.Chart.defaults = {
responsive: true,
maintainAspectRatio: false
};

View File

@ -0,0 +1,25 @@
/* Font Awesome minimal pour 4NK Notariat */
.fas, .fa {
font-family: "Font Awesome 5 Free";
font-weight: 900;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
}
.fa-cloud-upload-alt:before { content: "☁"; }
.fa-folder-open:before { content: "📁"; }
.fa-file:before { content: "📄"; }
.fa-file-pdf:before { content: "📕"; }
.fa-file-alt:before { content: "📄"; }
.fa-upload:before { content: "⬆"; }
.fa-times:before { content: "✕"; }
.fa-eye:before { content: "👁"; }
.fa-search:before { content: "🔍"; }
.fa-download:before { content: "⬇"; }
.fa-upload:before { content: "⬆"; }
.fa-3x { font-size: 3em; }
.fa-4x { font-size: 4em; }
.fa-2x { font-size: 2em; }

View File

@ -4,8 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>4NK Notariat - Traitement de Documents</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📄</text></svg>">
<link href="bootstrap.min.css" rel="stylesheet">
<link href="fontawesome.min.css" rel="stylesheet">
<style>
.upload-area {
border: 2px dashed #007bff;
@ -424,13 +425,16 @@
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="chart.min.js"></script>
<script src="app.js"></script>
<script>
// Initialisation de l'application
// Initialisation de l'application après chargement de app.js
document.addEventListener('DOMContentLoaded', function() {
if (typeof NotaryApp !== 'undefined') {
window.notaryApp = new NotaryApp();
} else {
console.error('NotaryApp class not found');
}
});
</script>
</body>

View File

@ -17,8 +17,34 @@ def start_web_server(port=8080):
# Changement vers le répertoire web
os.chdir(web_dir)
# Configuration du serveur
handler = http.server.SimpleHTTPRequestHandler
# Configuration du serveur avec gestion du favicon
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
# Ajouter des headers pour éviter le cache du favicon
if self.path == '/favicon.ico':
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
super().end_headers()
def do_GET(self):
try:
# Gérer le favicon.ico
if self.path == '/favicon.ico':
self.send_response(200)
self.send_header('Content-type', 'image/svg+xml')
self.end_headers()
favicon_svg = '''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">📄</text>
</svg>'''
self.wfile.write(favicon_svg.encode())
return
super().do_GET()
except (BrokenPipeError, ConnectionResetError):
# Ignorer les erreurs de connexion fermée par le client
pass
handler = CustomHTTPRequestHandler
try:
with socketserver.TCPServer(("", port), handler) as httpd: