feat: viewer local upload, Bambu collapse, planning done/fail, stats waste, portal link in jobs, sidebar collapsible, settings layout fix
Deploy via Portainer / deploy (push) Successful in 0s

This commit is contained in:
Jo
2026-07-07 09:16:24 +02:00
parent 7b65c4b2e2
commit b22e5e0fb4
9 changed files with 502 additions and 78 deletions
+70 -17
View File
@@ -56,12 +56,22 @@
<span class="font-monospace">{{ job.source_file.split('/')[-1] }}</span>
{% set ext = job.source_file.rsplit('.',1)[-1].lower() %}
{% if ext in ['stl','3mf','step','stp','obj'] %}
<button class="btn btn-xs btn-outline-secondary py-0 px-1 ms-1"
style="font-size:.7rem"
data-bs-toggle="modal" data-bs-target="#viewerModal"
data-file="{{ job.source_file }}" data-ext="{{ ext }}">
<i class="bi bi-box me-1"></i>Voir
</button>
{% set is_nc_path = job.source_file.startswith('/') %}
{% if is_nc_path %}
<button class="btn btn-xs btn-outline-secondary py-0 px-1 ms-1"
style="font-size:.7rem"
data-bs-toggle="modal" data-bs-target="#viewerModal"
data-file="{{ job.source_file }}" data-ext="{{ ext }}" data-src="nextcloud">
<i class="bi bi-box me-1"></i>Voir
</button>
{% else %}
<button class="btn btn-xs btn-outline-secondary py-0 px-1 ms-1"
style="font-size:.7rem"
data-bs-toggle="modal" data-bs-target="#viewerModal"
data-file="" data-ext="{{ ext }}" data-src="upload">
<i class="bi bi-upload me-1"></i>Charger pour voir
</button>
{% endif %}
{% endif %}
{% else %}—{% endif %}
</td></tr>
@@ -246,7 +256,12 @@
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
</div>
<div class="modal-body p-0 position-relative" style="height:75vh">
<div id="viewerUploadZone" class="p-4 text-center" style="display:none">
<i class="bi bi-cloud-upload fs-1 text-white-50 d-block mb-3"></i>
<p class="text-white-50 mb-3">Le fichier source n'est pas sur Nextcloud.<br>Chargez-le manuellement pour le visualiser.</p>
<input type="file" id="viewerLocalFile" class="form-control form-control-sm mx-auto" style="max-width:300px" accept=".stl,.3mf,.obj">
</div>
<div class="modal-body p-0 position-relative" style="height:75vh" id="viewerCanvasWrap">
<canvas id="viewerCanvas" style="width:100%;height:100%;display:block"></canvas>
<div id="viewerLoader" class="position-absolute top-50 start-50 translate-middle text-white text-center">
<div class="spinner-border mb-2"></div><br>Chargement du modèle…
@@ -332,14 +347,20 @@ function fitCamera() {
controls.update();
}
function loadModel(path, ext) {
const uploadZone = document.getElementById('viewerUploadZone');
const canvasWrap = document.getElementById('viewerCanvasWrap');
const localFileIn = document.getElementById('viewerLocalFile');
function makeMat() {
return new THREE.MeshPhongMaterial({ color: 0xff6b35, specular: 0x333333, shininess: 40, side: THREE.DoubleSide });
}
function loadFromUrl(url, ext) {
loader.style.display = '';
errDiv.style.display = 'none';
if (modelMesh) { scene.remove(modelMesh); modelMesh = null; }
const url = '/api/nextcloud/file?path=' + encodeURIComponent(path);
const mat = new THREE.MeshPhongMaterial({ color: 0xff6b35, specular: 0x333333, shininess: 40, side: THREE.DoubleSide });
const mat = makeMat();
const onError = (e) => {
loader.style.display = 'none';
errDiv.style.display = '';
@@ -355,7 +376,6 @@ function loadModel(path, ext) {
loader.style.display = 'none';
fitCamera();
}, undefined, onError);
} else if (ext === '3mf') {
new ThreeMFLoader().load(url, obj => {
obj.traverse(child => {
@@ -366,7 +386,6 @@ function loadModel(path, ext) {
loader.style.display = 'none';
fitCamera();
}, undefined, onError);
} else {
loader.style.display = 'none';
errDiv.style.display = '';
@@ -374,19 +393,53 @@ function loadModel(path, ext) {
}
}
function showUploadZone(ext) {
uploadZone.style.display = '';
canvasWrap.style.display = 'none';
loader.style.display = 'none';
// Reset file input
localFileIn.value = '';
localFileIn.onchange = (ev) => {
const file = ev.target.files[0];
if (!file) return;
const detectedExt = file.name.split('.').pop().toLowerCase();
const objectUrl = URL.createObjectURL(file);
title.textContent = file.name;
uploadZone.style.display = 'none';
canvasWrap.style.display = '';
initRenderer();
loadFromUrl(objectUrl, detectedExt);
// Revoke the blob URL after loading
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
};
}
// Ouvrir la modale
modal.addEventListener('show.bs.modal', e => {
const btn = e.relatedTarget;
const path = btn.dataset.file;
const ext = btn.dataset.ext;
title.textContent = path.split('/').pop();
initRenderer();
loadModel(path, ext);
const ext = (btn.dataset.ext || '').toLowerCase();
const src = btn.dataset.src;
if (src === 'upload') {
title.textContent = 'Charger un fichier local';
uploadZone.style.display = '';
canvasWrap.style.display = 'none';
showUploadZone(ext);
} else {
title.textContent = path.split('/').pop();
uploadZone.style.display = 'none';
canvasWrap.style.display = '';
initRenderer();
loadFromUrl('/api/nextcloud/file?path=' + encodeURIComponent(path), ext);
}
});
// Fermer → libérer
modal.addEventListener('hidden.bs.modal', () => {
if (modelMesh) { scene.remove(modelMesh); modelMesh = null; }
uploadZone.style.display = 'none';
canvasWrap.style.display = '';
});
// Wireframe toggle