feat: Enhance error logging for PDF preview failures and SQL timeouts

- Implemented a new method `ShapeTableNames` to gather entity and navigation table names, ensuring no blanks or duplicates are included.
- Updated `FdsMfrClient` to handle cases where schema queries return no tables, logging appropriate warnings and preventing misleading errors.
- Introduced `PdfPreviewAsync` in `IntranetController` to manage PDF preview rendering, capturing detailed logs for failures and notifying users with readable error messages.
- Added comprehensive unit tests for error logging and table name shaping to ensure robustness and correctness.
- Documented decision to persist user action failures to the `fds__admin_logdebug` table, enhancing support diagnostics.
This commit is contained in:
Stefan
2026-09-16 16:35:04 +02:00
parent 38e0d55b09
commit fce83c528f
19 changed files with 609 additions and 56 deletions
+31 -8
View File
@@ -116,6 +116,29 @@ $inv.eM = (r, re, opt) => {
}
return $('#topbar').ocmsmenu(m); /* empty Array => empty menu */
};
/* ── PDF preview failure handling (shared by every image-preview call) ────────
A preview render can legitimately take longer than the 30s postXT default (Spire
rasterises every page), and postXT swallows a timeout silently — the user would
sit in front of a frozen editor with no message. So previews get a longer timeout
and one handler that always tells the user something: the server's description
header when it sent one (the backend logs the failure and puts the reason there),
a timeout hint when the request timed out, the generic text otherwise. */
$inv.pdfPreviewTimeout = 120; /* seconds */
$inv.pdfPreviewFailed = function (jqXHR, textStatus) {
if ((jqXHR || {}).internalCode === 11110) { $ocms.login.dlg(); return; }
let desc = ((jqXHR || {}).internalText || '').trim();
if (desc === '' && textStatus === 'timeout') { desc = $ict.pvto; }
if (desc === '' && (jqXHR || {}).status === 410) { desc = $ict.pvgone; }
alert(($ict.pvf || $t.f1) + (desc !== '' ? '\n' + desc : ''));
};
/* postXT never calls `error` for a timeout (it returns early), so the timeout is only
observable from `complete`; this builds the complete handler for a preview call. */
$inv.pdfPreviewComplete = function (layout) {
return (jqXHR, textStatus) => {
if (layout) { layout.rC('freeze'); }
if (textStatus === 'timeout') { $inv.pdfPreviewFailed(jqXHR, textStatus); }
};
};
/* ── Backend-authoritative draft editing controller (ADR 0006/0007) ───────────
The server holds the truth for an invoice draft in an in-memory session; this
object seeds it, sends single edits as deltas, and renders the totals footer +
@@ -352,7 +375,7 @@ $inv.d = {
if ($fis.ValidateEmail(email) === false) { if (bool(confirm($ict.ivE + $ict.ivEc), false) === false) { return; } }
l.aC('freeze');
$ocms.postXT({
url: $ocms.url('inv/dpreview'), data: { token: t }, success: (response) => {
url: $ocms.url('inv/dpreview'), data: { token: t }, timeout: $inv.pdfPreviewTimeout, success: (response) => {
l.rC('freeze');
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), total = response.total;
if (total > 10) { $$.dc('note warn', c).text($ict.tpe); }
@@ -377,7 +400,7 @@ $inv.d = {
},
cancel: function (e) { if (confirm($ict.cdI)) { $inv.d.close(); $inv.rReload(); } }
});
}, error: () => { l.rC('freeze'); alert($ict.eis); }
}, error: $inv.pdfPreviewFailed, complete: $inv.pdfPreviewComplete(l)
});
},
/* Zwischenspeichern: flush the cache to the DB (no re-upload); stay in the editor. */
@@ -540,7 +563,7 @@ $inv.rd = {
if ($fis.ValidateEmail(email) === false) { if (bool(confirm($ict.ivE + $ict.ivEc), false) === false) { return; } }
l.aC('freeze');
$ocms.postXT({
url: $ocms.url('rem/dpreview'), data: { token: t }, success: (response) => {
url: $ocms.url('rem/dpreview'), data: { token: t }, timeout: $inv.pdfPreviewTimeout, success: (response) => {
l.rC('freeze');
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88);
$.each(response.img || [], (ii, img) => { $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); });
@@ -563,7 +586,7 @@ $inv.rd = {
},
cancel: function (e) { if (confirm($ict.cdI)) { $inv.rd.close(); $inv.rReload(); } }
});
}, error: () => { l.rC('freeze'); alert($t.f1); }
}, error: $inv.pdfPreviewFailed, complete: $inv.pdfPreviewComplete(l)
});
},
/* Zwischenspeichern: flush the cache to the DB (no re-upload); stay in the editor. */
@@ -1540,7 +1563,7 @@ $inv.disp = (id, typ) => {
}
if (u !== '') {
$ocms.postXT({
url: $ocms.url(u), data: { id: id || '', typ: 'img' }, success: (response) => {
url: $ocms.url(u), data: { id: id || '', typ: 'img' }, timeout: $inv.pdfPreviewTimeout, success: (response) => {
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = response.id;
$.each(response.img || [], function (ii, img) {
$$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px'));
@@ -1548,13 +1571,13 @@ $inv.disp = (id, typ) => {
$ocms.dlg(c, {
size: [vhr, Math.round(vw() * 0.88)], zindex_min: 50, form: false, exclusive:false
});
}
}, error: $inv.pdfPreviewFailed, complete: $inv.pdfPreviewComplete()
});
}
};
$inv.jdbn = function (ev) {
$ocms.postXT({
url: $ocms.url('inv/rdocn'), data: { name: ev.data.id || '', typ: 'img' }, success: (response) => {
url: $ocms.url('inv/rdocn'), data: { name: ev.data.id || '', typ: 'img' }, timeout: $inv.pdfPreviewTimeout, success: (response) => {
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = response.id;
$.each(response.img || [], function (ii, img) {
$$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px'));
@@ -1562,7 +1585,7 @@ $inv.jdbn = function (ev) {
$ocms.dlg(c, {
size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false
});
}
}, error: $inv.pdfPreviewFailed, complete: $inv.pdfPreviewComplete()
});
};
$inv.sp13b = () => {