Embed .docx as HTML iframe with download fallback

On upload, mammoth converts the doc to a styled HTML page saved alongside
the .docx. The item body gets an <iframe> pointing to the HTML so students
see the document inline. A download link below gives access to the original.
Falls back to a plain download link if HTML conversion fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-30 08:58:02 -04:00
1 parent 5de4cfa6cf
commit eff8cf5e60
2 files changed
+25 -6

No files matched your search

+6 -4
View File
@@ -620,13 +620,15 @@ $('#import-go-btn').addEventListener('click', async () => {
method: 'POST', method: 'POST',
body: { base64, originalName: file.name }, body: { base64, originalName: file.name },
}); });
const link = `<a href="${result.url}" target="_blank">📄 ${result.originalName}</a>`; const embed = result.htmlUrl
? `<iframe src="${result.htmlUrl}" style="width:100%;height:600px;border:1px solid #ccc;border-radius:4px;"></iframe>\n<p><a href="${result.url}" download="${result.originalName}">⬇️ Download ${result.originalName}</a></p>`
: `<a href="${result.url}" target="_blank">📄 ${result.originalName}</a>`;
if (type === 'assignment') { if (type === 'assignment') {
$('#ie-body-assignment').value = link; $('#ie-body-assignment').value = embed;
} else { } else {
$('#ie-body-lesson').innerHTML = link; $('#ie-body-lesson').innerHTML = embed;
} }
$('#import-status').textContent = '✅ File uploaded. Link inserted into body.'; $('#import-status').textContent = '✅ File uploaded. Document embedded in body.';
} catch (err) { } catch (err) {
$('#import-status').textContent = `❌ ${err.message}`; $('#import-status').textContent = `❌ ${err.message}`;
} finally { } finally {
+19 -2
View File
@@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { readFile, writeFile } from 'node:fs/promises'; import { readFile, writeFile } from 'node:fs/promises';
import { createHash, randomUUID } from 'node:crypto'; import { createHash, randomUUID } from 'node:crypto';
import mammoth from 'mammoth';
import { db, sha256 } from './db.js'; import { db, sha256 } from './db.js';
const ROOT = dirname(fileURLToPath(import.meta.url)); const ROOT = dirname(fileURLToPath(import.meta.url));
@@ -1539,14 +1540,30 @@ app.post('/api/admin/upload-attachment', requirePin, async (req, res) => {
catch (err) { return res.status(400).json({ error: 'Invalid base64' }); } catch (err) { return res.status(400).json({ error: 'Invalid base64' }); }
const ext = extname(originalName).toLowerCase() || '.docx'; const ext = extname(originalName).toLowerCase() || '.docx';
const filename = `${randomUUID()}${ext}`; const uuid = randomUUID();
const filename = `${uuid}${ext}`;
try { try {
await writeFile(join(ATTACHMENTS_DIR, filename), buffer); await writeFile(join(ATTACHMENTS_DIR, filename), buffer);
} catch (err) { } catch (err) {
return res.status(500).json({ error: `Could not save file: ${err.message}` }); return res.status(500).json({ error: `Could not save file: ${err.message}` });
} }
return res.json({ url: `/api/attachments/${filename}`, originalName }); // Convert to HTML so it can be shown in an iframe
let htmlUrl = null;
try {
const { value: bodyHtml } = await mammoth.convertToHtml({ buffer });
const htmlFilename = `${uuid}.html`;
const page = `<!doctype html><html><head><meta charset="utf-8">
<style>body{font-family:sans-serif;max-width:800px;margin:2rem auto;padding:0 1rem;line-height:1.6}
img{max-width:100%}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ccc;padding:.4em .6em}</style>
</head><body>${bodyHtml}</body></html>`;
await writeFile(join(ATTACHMENTS_DIR, htmlFilename), page);
htmlUrl = `/api/attachments/${htmlFilename}`;
} catch (_) {
// HTML preview generation failed — download link still works
}
return res.json({ url: `/api/attachments/${filename}`, htmlUrl, originalName });
}); });
app.get('/health', (req, res) => res.json({ ok: true })); app.get('/health', (req, res) => res.json({ ok: true }));