This commit is contained in:
@@ -242,10 +242,15 @@ router.get('/admin/models/:id/edit', requireAdmin, (req: Request, res: Response)
|
|||||||
const model = q<Model>(`SELECT * FROM models WHERE id = ?`).get(req.params.id)
|
const model = q<Model>(`SELECT * FROM models WHERE id = ?`).get(req.params.id)
|
||||||
if (!model) { res.redirect('/admin'); return }
|
if (!model) { res.redirect('/admin'); return }
|
||||||
|
|
||||||
const categories = q<Category>(`SELECT * FROM categories ORDER BY sort_order, name`).all()
|
const categories = q<Category>(`SELECT * FROM categories ORDER BY sort_order, name`).all()
|
||||||
const pdfs = q<ModelPdf>(`SELECT * FROM model_pdfs WHERE model_id = ? ORDER BY sort_order`).all(model.id)
|
const pdfs = q<ModelPdf>(`SELECT * FROM model_pdfs WHERE model_id = ? ORDER BY sort_order`).all(model.id)
|
||||||
|
const absModelPath = path.join(UPLOADS_DIR, model.file_path)
|
||||||
|
const hasGeometry = (model.file_type === 'step' || model.file_type === 'stp')
|
||||||
|
? fs.existsSync(geometryOutputPath(absModelPath))
|
||||||
|
: true
|
||||||
|
|
||||||
res.render('admin/edit', {
|
res.render('admin/edit', {
|
||||||
model, categories, pdfs, error: null,
|
model, categories, pdfs, error: null, hasGeometry,
|
||||||
baseUrl: process.env.BASE_URL ?? `http://localhost:${process.env.PORT ?? 3000}`,
|
baseUrl: process.env.BASE_URL ?? `http://localhost:${process.env.PORT ?? 3000}`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -277,6 +282,44 @@ router.post('/admin/pdfs/:id/delete', requireAdmin, (req: Request, res: Response
|
|||||||
res.redirect(`/admin/models/${pdf.model_id}/edit`)
|
res.redirect(`/admin/models/${pdf.model_id}/edit`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---- Reconvert STEP/STP geometry -----------------------------------------
|
||||||
|
|
||||||
|
router.post('/admin/models/:id/reconvert', requireAdmin, async (req: Request, res: Response) => {
|
||||||
|
const model = q<Model>(`SELECT * FROM models WHERE id = ?`).get(req.params.id)
|
||||||
|
if (!model) { res.status(404).json({ error: 'Model not found' }); return }
|
||||||
|
|
||||||
|
if (model.file_type !== 'step' && model.file_type !== 'stp') {
|
||||||
|
res.status(400).json({ error: 'Only STEP/STP models require geometry conversion' }); return
|
||||||
|
}
|
||||||
|
|
||||||
|
const absModelPath = path.join(UPLOADS_DIR, model.file_path)
|
||||||
|
if (!fs.existsSync(absModelPath)) {
|
||||||
|
res.status(404).json({ error: 'Source model file not found on disk' }); return
|
||||||
|
}
|
||||||
|
|
||||||
|
const geoOutPath = geometryOutputPath(absModelPath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await convertStepFile(absModelPath, geoOutPath)
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: `Conversion failed: ${(err as Error).message}` }); return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate thumbnail from new geometry (non-fatal)
|
||||||
|
try {
|
||||||
|
const geo = JSON.parse(fs.readFileSync(geoOutPath, 'utf8')) as GeometryFile
|
||||||
|
const tris = geometryToTriangles(geo)
|
||||||
|
const thumbPath = thumbnailOutputPath(UPLOADS_DIR, model.id)
|
||||||
|
await renderThumbnail(tris, thumbPath)
|
||||||
|
const thumbRel = path.relative(UPLOADS_DIR, thumbPath).replace(/\\/g, '/')
|
||||||
|
db.prepare(`UPDATE models SET thumbnail_path = ? WHERE id = ?`).run(thumbRel, model.id)
|
||||||
|
} catch (thumbErr) {
|
||||||
|
console.error('[thumbnail] reconvert thumbnail failed:', (thumbErr as Error).message)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
// ---- JSON list -----------------------------------------------------------
|
// ---- JSON list -----------------------------------------------------------
|
||||||
|
|
||||||
router.get('/api/admin/models', requireAdmin, (_req: Request, res: Response) => {
|
router.get('/api/admin/models', requireAdmin, (_req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ let _init: Promise<Awaited<ReturnType<typeof occtimport>>> | null = null
|
|||||||
|
|
||||||
async function getOcct() {
|
async function getOcct() {
|
||||||
if (_occt) return _occt
|
if (_occt) return _occt
|
||||||
if (!_init) _init = occtimport().then(m => { _occt = m; return m })
|
if (!_init) {
|
||||||
|
_init = occtimport().then(m => { _occt = m; return m })
|
||||||
|
// Reset on failure so the next upload attempt retries rather than replaying a cached rejection
|
||||||
|
_init.catch(() => { _init = null })
|
||||||
|
}
|
||||||
return _init
|
return _init
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Geometry status (STEP/STP only) -->
|
||||||
|
<% if (model.file_type === 'step' || model.file_type === 'stp') { %>
|
||||||
|
<div class="bg-surface-900 border border-gray-800 rounded-2xl p-6 mb-6">
|
||||||
|
<h2 class="text-sm font-semibold text-white mb-3">3D Geometry Processing</h2>
|
||||||
|
<% if (hasGeometry) { %>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-green-400">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
Geometry processed successfully
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-2 text-sm text-amber-400">
|
||||||
|
<svg class="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>
|
||||||
|
Geometry processing failed — model cannot be viewed
|
||||||
|
</div>
|
||||||
|
<button type="button" id="reconvert-btn"
|
||||||
|
class="shrink-0 bg-surface-700 hover:bg-surface-600 border border-gray-700 text-sm text-gray-300 rounded-lg px-4 py-2 transition-colors">
|
||||||
|
Retry Processing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre id="reconvert-error" class="mt-3 text-xs text-red-400 whitespace-pre-wrap break-all hidden"></pre>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<!-- Attached PDFs -->
|
<!-- Attached PDFs -->
|
||||||
<div class="bg-surface-900 border border-gray-800 rounded-2xl p-6 mb-6">
|
<div class="bg-surface-900 border border-gray-800 rounded-2xl p-6 mb-6">
|
||||||
<h2 class="text-sm font-semibold text-white mb-4">Shop Diagrams / PDFs</h2>
|
<h2 class="text-sm font-semibold text-white mb-4">Shop Diagrams / PDFs</h2>
|
||||||
@@ -115,5 +140,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/admin.js"></script>
|
<script src="/admin.js"></script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const btn = document.getElementById('reconvert-btn')
|
||||||
|
if (!btn) return
|
||||||
|
const errEl = document.getElementById('reconvert-error')
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
btn.disabled = true
|
||||||
|
btn.textContent = 'Processing…'
|
||||||
|
errEl.classList.add('hidden')
|
||||||
|
try {
|
||||||
|
const res = await fetch('/admin/models/<%= model.id %>/reconvert', { method: 'POST' })
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.ok) {
|
||||||
|
window.location.reload()
|
||||||
|
} else {
|
||||||
|
errEl.textContent = data.error ?? 'Unknown error'
|
||||||
|
errEl.classList.remove('hidden')
|
||||||
|
btn.disabled = false
|
||||||
|
btn.textContent = 'Retry Processing'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errEl.textContent = String(err)
|
||||||
|
errEl.classList.remove('hidden')
|
||||||
|
btn.disabled = false
|
||||||
|
btn.textContent = 'Retry Processing'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user