feat: implementar 12 mejoras, tests, docs y optimizaciones

- Fase A: license templates, search history, cost estimator
- Fase B: import URL, bulk ZIP, batch download
- Fase C: comparison mode, mesh validation, measurement tool
- Fase D: cross-section clipping, overhang heatmap, layer animation
- Refactor Pydantic/SQLAlchemy warnings
- 24 tests pytest
- README actualizado
- WebP thumbnails, lazy loading, cache headers
This commit is contained in:
Consultoria AS
2026-04-27 09:14:58 +00:00
commit 14b307110d
31 changed files with 5386 additions and 0 deletions

59
static/js/api.js Normal file
View File

@@ -0,0 +1,59 @@
const API_BASE = '/api';
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(20px)';
toast.style.transition = 'all 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 4000);
}
async function apiGet(path) {
const res = await fetch(API_BASE + path);
if (!res.ok) {
const text = await res.text();
throw new Error(text || `HTTP ${res.status}`);
}
return res.json();
}
async function apiDelete(path) {
const res = await fetch(API_BASE + path, { method: 'DELETE' });
if (!res.ok) {
const text = await res.text();
throw new Error(text || `HTTP ${res.status}`);
}
return res.json();
}
async function apiPostForm(path, formData) {
const res = await fetch(API_BASE + path, {
method: 'POST',
body: formData,
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `HTTP ${res.status}`);
}
return res.json();
}
async function apiPut(path, data) {
const res = await fetch(API_BASE + path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `HTTP ${res.status}`);
}
return res.json();
}