@legenki/print2medusa 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +202 -2
- package/.medusa/server/src/admin/index.mjs +205 -5
- package/.medusa/server/src/api/admin/printful/health/route.js +47 -0
- package/.medusa/server/src/api/admin/printful/history/route.js +51 -0
- package/.medusa/server/src/api/admin/printful/statistics/route.js +59 -0
- package/.medusa/server/src/api/admin/printful/sync/clear/route.js +93 -0
- package/.medusa/server/src/modules/printful/service.js +39 -1
- package/.medusa/server/src/utils/clear-sync.js +91 -0
- package/.medusa/server/src/utils/printful-client.js +23 -1
- package/.medusa/server/src/utils/webhook-health.js +73 -0
- package/README.md +30 -0
- package/package.json +1 -1
|
@@ -307,6 +307,191 @@ const PrintfulSyncWidget = () => {
|
|
|
307
307
|
adminSdk.defineWidgetConfig({
|
|
308
308
|
zone: "product.list.before"
|
|
309
309
|
});
|
|
310
|
+
const STATUS_COLOUR = {
|
|
311
|
+
success: "green",
|
|
312
|
+
running: "orange",
|
|
313
|
+
partial: "orange",
|
|
314
|
+
failed: "red"
|
|
315
|
+
};
|
|
316
|
+
const looksStuck = (row, staleMinutes) => {
|
|
317
|
+
if (!row || row.status !== "running") {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
const beat = row.heartbeat_at ?? row.started_at;
|
|
321
|
+
if (!beat) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
return Date.now() - new Date(beat).getTime() > staleMinutes * 6e4;
|
|
325
|
+
};
|
|
326
|
+
const minutesSince = (iso) => {
|
|
327
|
+
if (!iso) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return Math.floor((Date.now() - new Date(iso).getTime()) / 6e4);
|
|
331
|
+
};
|
|
332
|
+
const PrintfulPage = () => {
|
|
333
|
+
var _a, _b, _c;
|
|
334
|
+
const [syncs, setSyncs] = react.useState([]);
|
|
335
|
+
const [health, setHealth] = react.useState(null);
|
|
336
|
+
const [stats, setStats] = react.useState(null);
|
|
337
|
+
const [loading, setLoading] = react.useState(true);
|
|
338
|
+
const [clearing, setClearing] = react.useState(false);
|
|
339
|
+
const load = react.useCallback(async () => {
|
|
340
|
+
var _a2;
|
|
341
|
+
const [h, w, s] = await Promise.allSettled([
|
|
342
|
+
fetch("/admin/printful/history", { credentials: "include" }),
|
|
343
|
+
fetch("/admin/printful/health", { credentials: "include" }),
|
|
344
|
+
fetch("/admin/printful/statistics", { credentials: "include" })
|
|
345
|
+
]);
|
|
346
|
+
if (h.status === "fulfilled" && h.value.ok) {
|
|
347
|
+
const body = await h.value.json();
|
|
348
|
+
setSyncs(body.syncs ?? []);
|
|
349
|
+
}
|
|
350
|
+
if (w.status === "fulfilled" && w.value.ok) {
|
|
351
|
+
setHealth(await w.value.json());
|
|
352
|
+
}
|
|
353
|
+
if (s.status === "fulfilled" && s.value.ok) {
|
|
354
|
+
const body = await s.value.json();
|
|
355
|
+
setStats(((_a2 = body.statistics) == null ? void 0 : _a2[0]) ?? null);
|
|
356
|
+
}
|
|
357
|
+
setLoading(false);
|
|
358
|
+
}, []);
|
|
359
|
+
react.useEffect(() => {
|
|
360
|
+
void load();
|
|
361
|
+
}, [load]);
|
|
362
|
+
const latest = syncs[0];
|
|
363
|
+
const running = (latest == null ? void 0 : latest.status) === "running";
|
|
364
|
+
react.useEffect(() => {
|
|
365
|
+
if (!running) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const t = setInterval(() => void load(), 3e3);
|
|
369
|
+
return () => clearInterval(t);
|
|
370
|
+
}, [running, load]);
|
|
371
|
+
const clearStuck = async () => {
|
|
372
|
+
setClearing(true);
|
|
373
|
+
try {
|
|
374
|
+
const res = await fetch("/admin/printful/sync/clear", {
|
|
375
|
+
method: "POST",
|
|
376
|
+
credentials: "include",
|
|
377
|
+
headers: { "content-type": "application/json" },
|
|
378
|
+
body: JSON.stringify({ confirm: "clear-stuck-sync" })
|
|
379
|
+
});
|
|
380
|
+
const body = await res.json().catch(() => ({}));
|
|
381
|
+
if (res.ok) {
|
|
382
|
+
ui.toast.success("Cleared the stuck sync", {
|
|
383
|
+
description: body.error_message
|
|
384
|
+
});
|
|
385
|
+
} else if (res.status === 409) {
|
|
386
|
+
ui.toast.info("The sync is still alive", {
|
|
387
|
+
description: "It checked in while you were looking. Nothing cleared."
|
|
388
|
+
});
|
|
389
|
+
} else {
|
|
390
|
+
ui.toast.error("Could not clear the sync", { description: body.message });
|
|
391
|
+
}
|
|
392
|
+
await load();
|
|
393
|
+
} catch (err) {
|
|
394
|
+
ui.toast.error("Could not clear the sync", {
|
|
395
|
+
description: err instanceof Error ? err.message : String(err)
|
|
396
|
+
});
|
|
397
|
+
} finally {
|
|
398
|
+
setClearing(false);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
const stuck = looksStuck(latest, (health == null ? void 0 : health.window) ?? 60);
|
|
402
|
+
const silentFor = minutesSince((latest == null ? void 0 : latest.heartbeat_at) ?? (latest == null ? void 0 : latest.started_at));
|
|
403
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
404
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
405
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Printful" }),
|
|
406
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
407
|
+
ui.Button,
|
|
408
|
+
{
|
|
409
|
+
size: "small",
|
|
410
|
+
variant: "secondary",
|
|
411
|
+
onClick: () => void load(),
|
|
412
|
+
disabled: loading,
|
|
413
|
+
children: "Refresh"
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
] }),
|
|
417
|
+
stuck && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
418
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "This sync looks stuck" }),
|
|
419
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
420
|
+
"It has not checked in",
|
|
421
|
+
silentFor != null ? ` for ${silentFor} minutes` : "",
|
|
422
|
+
". Until it is cleared or reclaimed, no other sync can start."
|
|
423
|
+
] }),
|
|
424
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt, { children: [
|
|
425
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "danger", disabled: clearing, children: "Clear it" }) }),
|
|
426
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
427
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
428
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Clear the stuck sync?" }),
|
|
429
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: "This marks the run as failed so a new sync can start. If it turns out to be alive, nothing is cleared. Products already imported are not touched." })
|
|
430
|
+
] }),
|
|
431
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
432
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { children: "Cancel" }),
|
|
433
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Action, { onClick: () => void clearStuck(), children: "Clear" })
|
|
434
|
+
] })
|
|
435
|
+
] })
|
|
436
|
+
] })
|
|
437
|
+
] }),
|
|
438
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
439
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Sales" }),
|
|
440
|
+
stats ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
441
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
442
|
+
"Paid orders: ",
|
|
443
|
+
((_a = stats.total_paid_orders) == null ? void 0 : _a.value) ?? "—"
|
|
444
|
+
] }),
|
|
445
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
446
|
+
"Profit: ",
|
|
447
|
+
((_b = stats.profit) == null ? void 0 : _b.value) ?? "—",
|
|
448
|
+
" ",
|
|
449
|
+
((_c = stats.currency) == null ? void 0 : _c.toUpperCase()) ?? ""
|
|
450
|
+
] })
|
|
451
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "Printful reported no statistics." })
|
|
452
|
+
] }),
|
|
453
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
454
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Webhooks" }),
|
|
455
|
+
health && !health.ever_received && // The state a misconfigured webhook leaves behind, and the reason
|
|
456
|
+
// this panel exists: a store can look fine and never hear back.
|
|
457
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: health.secret_set ? "A secret is configured, but nothing has arrived yet." : "No webhook secret is configured." }),
|
|
458
|
+
health && health.ever_received && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
459
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
460
|
+
"Last event:",
|
|
461
|
+
" ",
|
|
462
|
+
health.last_event_at ? new Date(health.last_event_at).toLocaleString() : "—"
|
|
463
|
+
] }),
|
|
464
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
465
|
+
health.processed,
|
|
466
|
+
" processed · ",
|
|
467
|
+
health.deferred,
|
|
468
|
+
" waiting ·",
|
|
469
|
+
" ",
|
|
470
|
+
health.failed,
|
|
471
|
+
" failed"
|
|
472
|
+
] })
|
|
473
|
+
] })
|
|
474
|
+
] }),
|
|
475
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
476
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Recent syncs" }),
|
|
477
|
+
syncs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No syncs yet." }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
|
|
478
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
479
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Started" }),
|
|
480
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
|
|
481
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Result" })
|
|
482
|
+
] }) }),
|
|
483
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: syncs.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
484
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: row.started_at ? new Date(row.started_at).toLocaleString() : "—" }),
|
|
485
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: STATUS_COLOUR[row.status] ?? "grey", children: row.status }) }),
|
|
486
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: row.status === "running" ? `${row.products_processed ?? 0} of ${row.products_total ?? "?"}` : `${row.products_created ?? 0} created · ${row.products_updated ?? 0} updated · ${row.products_failed ?? 0} failed` })
|
|
487
|
+
] }, row.id)) })
|
|
488
|
+
] })
|
|
489
|
+
] })
|
|
490
|
+
] });
|
|
491
|
+
};
|
|
492
|
+
const config = adminSdk.defineRouteConfig({
|
|
493
|
+
label: "Printful"
|
|
494
|
+
});
|
|
310
495
|
const i18nTranslations0 = {};
|
|
311
496
|
const widgetModule = { widgets: [
|
|
312
497
|
{
|
|
@@ -321,10 +506,25 @@ const widgetModule = { widgets: [
|
|
|
321
506
|
}
|
|
322
507
|
] };
|
|
323
508
|
const routeModule = {
|
|
324
|
-
routes: [
|
|
509
|
+
routes: [
|
|
510
|
+
{
|
|
511
|
+
Component: PrintfulPage,
|
|
512
|
+
path: "/printful",
|
|
513
|
+
handle: { label: config.label, translationNs: config.translationNs }
|
|
514
|
+
}
|
|
515
|
+
]
|
|
325
516
|
};
|
|
326
517
|
const menuItemModule = {
|
|
327
|
-
menuItems: [
|
|
518
|
+
menuItems: [
|
|
519
|
+
{
|
|
520
|
+
label: config.label,
|
|
521
|
+
icon: void 0,
|
|
522
|
+
path: "/printful",
|
|
523
|
+
nested: void 0,
|
|
524
|
+
rank: void 0,
|
|
525
|
+
translationNs: void 0
|
|
526
|
+
}
|
|
527
|
+
]
|
|
328
528
|
};
|
|
329
529
|
const formModule = { customFields: {} };
|
|
330
530
|
const displayModule = {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { defineWidgetConfig } from "@medusajs/admin-sdk";
|
|
3
|
-
import { Container, Heading, Badge, Text, Button, toast } from "@medusajs/ui";
|
|
2
|
+
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
+
import { Container, Heading, Badge, Text, Button, toast, Prompt, Table } from "@medusajs/ui";
|
|
4
4
|
import { defaultCurrencies } from "@medusajs/framework/utils";
|
|
5
|
-
import { useState, useEffect } from "react";
|
|
5
|
+
import { useState, useEffect, useCallback } from "react";
|
|
6
6
|
function minorUnitFactor(currencyCode) {
|
|
7
7
|
return isZeroDecimalCurrency(currencyCode) ? 1 : 100;
|
|
8
8
|
}
|
|
@@ -306,6 +306,191 @@ const PrintfulSyncWidget = () => {
|
|
|
306
306
|
defineWidgetConfig({
|
|
307
307
|
zone: "product.list.before"
|
|
308
308
|
});
|
|
309
|
+
const STATUS_COLOUR = {
|
|
310
|
+
success: "green",
|
|
311
|
+
running: "orange",
|
|
312
|
+
partial: "orange",
|
|
313
|
+
failed: "red"
|
|
314
|
+
};
|
|
315
|
+
const looksStuck = (row, staleMinutes) => {
|
|
316
|
+
if (!row || row.status !== "running") {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
const beat = row.heartbeat_at ?? row.started_at;
|
|
320
|
+
if (!beat) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
return Date.now() - new Date(beat).getTime() > staleMinutes * 6e4;
|
|
324
|
+
};
|
|
325
|
+
const minutesSince = (iso) => {
|
|
326
|
+
if (!iso) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return Math.floor((Date.now() - new Date(iso).getTime()) / 6e4);
|
|
330
|
+
};
|
|
331
|
+
const PrintfulPage = () => {
|
|
332
|
+
var _a, _b, _c;
|
|
333
|
+
const [syncs, setSyncs] = useState([]);
|
|
334
|
+
const [health, setHealth] = useState(null);
|
|
335
|
+
const [stats, setStats] = useState(null);
|
|
336
|
+
const [loading, setLoading] = useState(true);
|
|
337
|
+
const [clearing, setClearing] = useState(false);
|
|
338
|
+
const load = useCallback(async () => {
|
|
339
|
+
var _a2;
|
|
340
|
+
const [h, w, s] = await Promise.allSettled([
|
|
341
|
+
fetch("/admin/printful/history", { credentials: "include" }),
|
|
342
|
+
fetch("/admin/printful/health", { credentials: "include" }),
|
|
343
|
+
fetch("/admin/printful/statistics", { credentials: "include" })
|
|
344
|
+
]);
|
|
345
|
+
if (h.status === "fulfilled" && h.value.ok) {
|
|
346
|
+
const body = await h.value.json();
|
|
347
|
+
setSyncs(body.syncs ?? []);
|
|
348
|
+
}
|
|
349
|
+
if (w.status === "fulfilled" && w.value.ok) {
|
|
350
|
+
setHealth(await w.value.json());
|
|
351
|
+
}
|
|
352
|
+
if (s.status === "fulfilled" && s.value.ok) {
|
|
353
|
+
const body = await s.value.json();
|
|
354
|
+
setStats(((_a2 = body.statistics) == null ? void 0 : _a2[0]) ?? null);
|
|
355
|
+
}
|
|
356
|
+
setLoading(false);
|
|
357
|
+
}, []);
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
void load();
|
|
360
|
+
}, [load]);
|
|
361
|
+
const latest = syncs[0];
|
|
362
|
+
const running = (latest == null ? void 0 : latest.status) === "running";
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (!running) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const t = setInterval(() => void load(), 3e3);
|
|
368
|
+
return () => clearInterval(t);
|
|
369
|
+
}, [running, load]);
|
|
370
|
+
const clearStuck = async () => {
|
|
371
|
+
setClearing(true);
|
|
372
|
+
try {
|
|
373
|
+
const res = await fetch("/admin/printful/sync/clear", {
|
|
374
|
+
method: "POST",
|
|
375
|
+
credentials: "include",
|
|
376
|
+
headers: { "content-type": "application/json" },
|
|
377
|
+
body: JSON.stringify({ confirm: "clear-stuck-sync" })
|
|
378
|
+
});
|
|
379
|
+
const body = await res.json().catch(() => ({}));
|
|
380
|
+
if (res.ok) {
|
|
381
|
+
toast.success("Cleared the stuck sync", {
|
|
382
|
+
description: body.error_message
|
|
383
|
+
});
|
|
384
|
+
} else if (res.status === 409) {
|
|
385
|
+
toast.info("The sync is still alive", {
|
|
386
|
+
description: "It checked in while you were looking. Nothing cleared."
|
|
387
|
+
});
|
|
388
|
+
} else {
|
|
389
|
+
toast.error("Could not clear the sync", { description: body.message });
|
|
390
|
+
}
|
|
391
|
+
await load();
|
|
392
|
+
} catch (err) {
|
|
393
|
+
toast.error("Could not clear the sync", {
|
|
394
|
+
description: err instanceof Error ? err.message : String(err)
|
|
395
|
+
});
|
|
396
|
+
} finally {
|
|
397
|
+
setClearing(false);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const stuck = looksStuck(latest, (health == null ? void 0 : health.window) ?? 60);
|
|
401
|
+
const silentFor = minutesSince((latest == null ? void 0 : latest.heartbeat_at) ?? (latest == null ? void 0 : latest.started_at));
|
|
402
|
+
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
403
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
404
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Printful" }),
|
|
405
|
+
/* @__PURE__ */ jsx(
|
|
406
|
+
Button,
|
|
407
|
+
{
|
|
408
|
+
size: "small",
|
|
409
|
+
variant: "secondary",
|
|
410
|
+
onClick: () => void load(),
|
|
411
|
+
disabled: loading,
|
|
412
|
+
children: "Refresh"
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
] }),
|
|
416
|
+
stuck && /* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
417
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "This sync looks stuck" }),
|
|
418
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
419
|
+
"It has not checked in",
|
|
420
|
+
silentFor != null ? ` for ${silentFor} minutes` : "",
|
|
421
|
+
". Until it is cleared or reclaimed, no other sync can start."
|
|
422
|
+
] }),
|
|
423
|
+
/* @__PURE__ */ jsxs(Prompt, { children: [
|
|
424
|
+
/* @__PURE__ */ jsx(Prompt.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { size: "small", variant: "danger", disabled: clearing, children: "Clear it" }) }),
|
|
425
|
+
/* @__PURE__ */ jsxs(Prompt.Content, { children: [
|
|
426
|
+
/* @__PURE__ */ jsxs(Prompt.Header, { children: [
|
|
427
|
+
/* @__PURE__ */ jsx(Prompt.Title, { children: "Clear the stuck sync?" }),
|
|
428
|
+
/* @__PURE__ */ jsx(Prompt.Description, { children: "This marks the run as failed so a new sync can start. If it turns out to be alive, nothing is cleared. Products already imported are not touched." })
|
|
429
|
+
] }),
|
|
430
|
+
/* @__PURE__ */ jsxs(Prompt.Footer, { children: [
|
|
431
|
+
/* @__PURE__ */ jsx(Prompt.Cancel, { children: "Cancel" }),
|
|
432
|
+
/* @__PURE__ */ jsx(Prompt.Action, { onClick: () => void clearStuck(), children: "Clear" })
|
|
433
|
+
] })
|
|
434
|
+
] })
|
|
435
|
+
] })
|
|
436
|
+
] }),
|
|
437
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
438
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Sales" }),
|
|
439
|
+
stats ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
440
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
441
|
+
"Paid orders: ",
|
|
442
|
+
((_a = stats.total_paid_orders) == null ? void 0 : _a.value) ?? "—"
|
|
443
|
+
] }),
|
|
444
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
445
|
+
"Profit: ",
|
|
446
|
+
((_b = stats.profit) == null ? void 0 : _b.value) ?? "—",
|
|
447
|
+
" ",
|
|
448
|
+
((_c = stats.currency) == null ? void 0 : _c.toUpperCase()) ?? ""
|
|
449
|
+
] })
|
|
450
|
+
] }) : /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "Printful reported no statistics." })
|
|
451
|
+
] }),
|
|
452
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
453
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Webhooks" }),
|
|
454
|
+
health && !health.ever_received && // The state a misconfigured webhook leaves behind, and the reason
|
|
455
|
+
// this panel exists: a store can look fine and never hear back.
|
|
456
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: health.secret_set ? "A secret is configured, but nothing has arrived yet." : "No webhook secret is configured." }),
|
|
457
|
+
health && health.ever_received && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
458
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
459
|
+
"Last event:",
|
|
460
|
+
" ",
|
|
461
|
+
health.last_event_at ? new Date(health.last_event_at).toLocaleString() : "—"
|
|
462
|
+
] }),
|
|
463
|
+
/* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
464
|
+
health.processed,
|
|
465
|
+
" processed · ",
|
|
466
|
+
health.deferred,
|
|
467
|
+
" waiting ·",
|
|
468
|
+
" ",
|
|
469
|
+
health.failed,
|
|
470
|
+
" failed"
|
|
471
|
+
] })
|
|
472
|
+
] })
|
|
473
|
+
] }),
|
|
474
|
+
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
|
|
475
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Recent syncs" }),
|
|
476
|
+
syncs.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No syncs yet." }) : /* @__PURE__ */ jsxs(Table, { children: [
|
|
477
|
+
/* @__PURE__ */ jsx(Table.Header, { children: /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
478
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Started" }),
|
|
479
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Status" }),
|
|
480
|
+
/* @__PURE__ */ jsx(Table.HeaderCell, { children: "Result" })
|
|
481
|
+
] }) }),
|
|
482
|
+
/* @__PURE__ */ jsx(Table.Body, { children: syncs.map((row) => /* @__PURE__ */ jsxs(Table.Row, { children: [
|
|
483
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: row.started_at ? new Date(row.started_at).toLocaleString() : "—" }),
|
|
484
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Badge, { color: STATUS_COLOUR[row.status] ?? "grey", children: row.status }) }),
|
|
485
|
+
/* @__PURE__ */ jsx(Table.Cell, { children: row.status === "running" ? `${row.products_processed ?? 0} of ${row.products_total ?? "?"}` : `${row.products_created ?? 0} created · ${row.products_updated ?? 0} updated · ${row.products_failed ?? 0} failed` })
|
|
486
|
+
] }, row.id)) })
|
|
487
|
+
] })
|
|
488
|
+
] })
|
|
489
|
+
] });
|
|
490
|
+
};
|
|
491
|
+
const config = defineRouteConfig({
|
|
492
|
+
label: "Printful"
|
|
493
|
+
});
|
|
309
494
|
const i18nTranslations0 = {};
|
|
310
495
|
const widgetModule = { widgets: [
|
|
311
496
|
{
|
|
@@ -320,10 +505,25 @@ const widgetModule = { widgets: [
|
|
|
320
505
|
}
|
|
321
506
|
] };
|
|
322
507
|
const routeModule = {
|
|
323
|
-
routes: [
|
|
508
|
+
routes: [
|
|
509
|
+
{
|
|
510
|
+
Component: PrintfulPage,
|
|
511
|
+
path: "/printful",
|
|
512
|
+
handle: { label: config.label, translationNs: config.translationNs }
|
|
513
|
+
}
|
|
514
|
+
]
|
|
324
515
|
};
|
|
325
516
|
const menuItemModule = {
|
|
326
|
-
menuItems: [
|
|
517
|
+
menuItems: [
|
|
518
|
+
{
|
|
519
|
+
label: config.label,
|
|
520
|
+
icon: void 0,
|
|
521
|
+
path: "/printful",
|
|
522
|
+
nested: void 0,
|
|
523
|
+
rank: void 0,
|
|
524
|
+
translationNs: void 0
|
|
525
|
+
}
|
|
526
|
+
]
|
|
327
527
|
};
|
|
328
528
|
const formModule = { customFields: {} };
|
|
329
529
|
const displayModule = {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
const printful_1 = require("../../../../modules/printful");
|
|
5
|
+
const webhook_health_1 = require("../../../../utils/webhook-health");
|
|
6
|
+
/**
|
|
7
|
+
* Rows the summary is computed over: the most recent slice of the event table.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately a window rather than the whole table. This is a health panel —
|
|
10
|
+
* "are deliveries arriving and are they landing" — and the answer lives in
|
|
11
|
+
* recent traffic. Reading every row ever received would grow unbounded with
|
|
12
|
+
* store age and make the page slower the longer the plugin has been installed,
|
|
13
|
+
* for an answer that gets less useful the further back it reaches.
|
|
14
|
+
*
|
|
15
|
+
* The counts are therefore explicitly counts within this window, and the
|
|
16
|
+
* response says so via `window`, so the page cannot present them as all-time
|
|
17
|
+
* totals.
|
|
18
|
+
*/
|
|
19
|
+
const HEALTH_WINDOW = 200;
|
|
20
|
+
/**
|
|
21
|
+
* Webhook delivery health.
|
|
22
|
+
*
|
|
23
|
+
* This route does NOT call Printful. `client.getWebhookConfig()` exists and
|
|
24
|
+
* would report the registered URL authoritatively, but a panel whose entire
|
|
25
|
+
* job is to tell the owner whether Printful is reaching them must not go dark
|
|
26
|
+
* when Printful is down — an outage would turn the one screen you check
|
|
27
|
+
* during an outage into an error. Everything here is answered from local rows
|
|
28
|
+
* and plugin options, so it renders during a Printful outage and, in fact,
|
|
29
|
+
* shows exactly the symptom (no recent events) that an outage produces.
|
|
30
|
+
*
|
|
31
|
+
* `/admin/printful/webhook` still exposes the live config for the moment an
|
|
32
|
+
* admin actually wants to inspect or overwrite the registration, and that
|
|
33
|
+
* route is the right place for a network call because the admin asked for it.
|
|
34
|
+
*/
|
|
35
|
+
async function GET(req, res) {
|
|
36
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
37
|
+
const options = await printful.getOptions();
|
|
38
|
+
const rows = (await printful.listPrintfulWebhookEvents({}, { take: HEALTH_WINDOW, order: { created_at: "DESC" } }));
|
|
39
|
+
const summary = (0, webhook_health_1.summarizeWebhookHealth)(rows, {
|
|
40
|
+
secret_set: Boolean(options.webhookSecret),
|
|
41
|
+
});
|
|
42
|
+
res.status(200).json({
|
|
43
|
+
...summary,
|
|
44
|
+
window: HEALTH_WINDOW,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL2hlYWx0aC9yb3V0ZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQXNDQSxrQkFpQkM7QUF0REQsMkRBQThEO0FBRTlELHFFQUd5QztBQUV6Qzs7Ozs7Ozs7Ozs7O0dBWUc7QUFDSCxNQUFNLGFBQWEsR0FBRyxHQUFHLENBQUE7QUFFekI7Ozs7Ozs7Ozs7Ozs7O0dBY0c7QUFDSSxLQUFLLFVBQVUsR0FBRyxDQUFDLEdBQWtCLEVBQUUsR0FBbUI7SUFDL0QsTUFBTSxRQUFRLEdBQTBCLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLDBCQUFlLENBQUMsQ0FBQTtJQUMxRSxNQUFNLE9BQU8sR0FBRyxNQUFNLFFBQVEsQ0FBQyxVQUFVLEVBQUUsQ0FBQTtJQUUzQyxNQUFNLElBQUksR0FBRyxDQUFDLE1BQU0sUUFBUSxDQUFDLHlCQUF5QixDQUNwRCxFQUFFLEVBQ0YsRUFBRSxJQUFJLEVBQUUsYUFBYSxFQUFFLEtBQUssRUFBRSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsRUFBRSxDQUN2RCxDQUF1QixDQUFBO0lBRXhCLE1BQU0sT0FBTyxHQUFHLElBQUEsdUNBQXNCLEVBQUMsSUFBSSxFQUFFO1FBQzNDLFVBQVUsRUFBRSxPQUFPLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQztLQUMzQyxDQUFDLENBQUE7SUFFRixHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztRQUNuQixHQUFHLE9BQU87UUFDVixNQUFNLEVBQUUsYUFBYTtLQUN0QixDQUFDLENBQUE7QUFDSixDQUFDIn0=
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
const printful_1 = require("../../../../modules/printful");
|
|
5
|
+
/** Rows per page. Enough to cover a week of hourly syncs on one screen. */
|
|
6
|
+
const DEFAULT_LIMIT = 20;
|
|
7
|
+
/**
|
|
8
|
+
* Hard ceiling on `limit`. The admin page never asks for more than the
|
|
9
|
+
* default; this only stops a hand-crafted `?limit=100000` from paging the
|
|
10
|
+
* whole table into memory to render a list nobody reads.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_LIMIT = 100;
|
|
13
|
+
function parsePositiveInt(value, fallback, max) {
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
return Math.min(Math.floor(parsed), max);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Recent sync runs, newest first.
|
|
22
|
+
*
|
|
23
|
+
* Ordered by `started_at DESC` to match `getLatestSyncLog`, so the first row
|
|
24
|
+
* here is always the same run the status widget calls "latest". Ordering by
|
|
25
|
+
* `finished_at` instead would sort a still-running sync (finished_at null) to
|
|
26
|
+
* an arbitrary end of the list.
|
|
27
|
+
*/
|
|
28
|
+
async function GET(req, res) {
|
|
29
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
30
|
+
const limit = parsePositiveInt(req.query.limit, DEFAULT_LIMIT, MAX_LIMIT);
|
|
31
|
+
const offset = Math.max(0, Number(req.query.offset) || 0);
|
|
32
|
+
const logs = await printful.listPrintfulSyncLogs({}, { take: limit, skip: offset, order: { started_at: "DESC" } });
|
|
33
|
+
res.status(200).json({
|
|
34
|
+
syncs: logs.map((log) => ({
|
|
35
|
+
id: log.id,
|
|
36
|
+
status: log.status,
|
|
37
|
+
started_at: log.started_at,
|
|
38
|
+
finished_at: log.finished_at,
|
|
39
|
+
heartbeat_at: log.heartbeat_at,
|
|
40
|
+
products_created: log.products_created,
|
|
41
|
+
products_updated: log.products_updated,
|
|
42
|
+
products_failed: log.products_failed,
|
|
43
|
+
products_processed: log.products_processed,
|
|
44
|
+
products_total: log.products_total,
|
|
45
|
+
error_message: log.error_message,
|
|
46
|
+
})),
|
|
47
|
+
limit,
|
|
48
|
+
offset,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL2hpc3Rvcnkvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFrQ0Esa0JBNEJDO0FBN0RELDJEQUE4RDtBQUc5RCwyRUFBMkU7QUFDM0UsTUFBTSxhQUFhLEdBQUcsRUFBRSxDQUFBO0FBRXhCOzs7O0dBSUc7QUFDSCxNQUFNLFNBQVMsR0FBRyxHQUFHLENBQUE7QUFFckIsU0FBUyxnQkFBZ0IsQ0FDdkIsS0FBYyxFQUNkLFFBQWdCLEVBQ2hCLEdBQVc7SUFFWCxNQUFNLE1BQU0sR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUE7SUFDNUIsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLElBQUksTUFBTSxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQzVDLE9BQU8sUUFBUSxDQUFBO0lBQ2pCLENBQUM7SUFDRCxPQUFPLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQTtBQUMxQyxDQUFDO0FBRUQ7Ozs7Ozs7R0FPRztBQUNJLEtBQUssVUFBVSxHQUFHLENBQUMsR0FBa0IsRUFBRSxHQUFtQjtJQUMvRCxNQUFNLFFBQVEsR0FBMEIsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsMEJBQWUsQ0FBQyxDQUFBO0lBRTFFLE1BQU0sS0FBSyxHQUFHLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsS0FBSyxFQUFFLGFBQWEsRUFBRSxTQUFTLENBQUMsQ0FBQTtJQUN6RSxNQUFNLE1BQU0sR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxNQUFNLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQTtJQUV6RCxNQUFNLElBQUksR0FBRyxNQUFNLFFBQVEsQ0FBQyxvQkFBb0IsQ0FDOUMsRUFBRSxFQUNGLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsRUFBRSxDQUM3RCxDQUFBO0lBRUQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDbkIsS0FBSyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDeEIsRUFBRSxFQUFFLEdBQUcsQ0FBQyxFQUFFO1lBQ1YsTUFBTSxFQUFFLEdBQUcsQ0FBQyxNQUFNO1lBQ2xCLFVBQVUsRUFBRSxHQUFHLENBQUMsVUFBVTtZQUMxQixXQUFXLEVBQUUsR0FBRyxDQUFDLFdBQVc7WUFDNUIsWUFBWSxFQUFFLEdBQUcsQ0FBQyxZQUFZO1lBQzlCLGdCQUFnQixFQUFFLEdBQUcsQ0FBQyxnQkFBZ0I7WUFDdEMsZ0JBQWdCLEVBQUUsR0FBRyxDQUFDLGdCQUFnQjtZQUN0QyxlQUFlLEVBQUUsR0FBRyxDQUFDLGVBQWU7WUFDcEMsa0JBQWtCLEVBQUUsR0FBRyxDQUFDLGtCQUFrQjtZQUMxQyxjQUFjLEVBQUUsR0FBRyxDQUFDLGNBQWM7WUFDbEMsYUFBYSxFQUFFLEdBQUcsQ0FBQyxhQUFhO1NBQ2pDLENBQUMsQ0FBQztRQUNILEtBQUs7UUFDTCxNQUFNO0tBQ1AsQ0FBQyxDQUFBO0FBQ0osQ0FBQyJ9
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GET = GET;
|
|
4
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
5
|
+
const printful_1 = require("../../../../modules/printful");
|
|
6
|
+
/** Days of history when the caller does not ask for a range. */
|
|
7
|
+
const DEFAULT_DAYS = 30;
|
|
8
|
+
/**
|
|
9
|
+
* Reports the admin page renders. Asking for only these keeps the response
|
|
10
|
+
* small; Printful also offers per-product and per-variant breakdowns, which
|
|
11
|
+
* nothing displays yet and which would be paid for on every page load.
|
|
12
|
+
*/
|
|
13
|
+
const REPORT_TYPES = "profit,total_paid_orders,average_fulfillment_time";
|
|
14
|
+
const isoDate = (d) => d.toISOString().slice(0, 10);
|
|
15
|
+
/**
|
|
16
|
+
* Sales statistics for the admin page.
|
|
17
|
+
*
|
|
18
|
+
* Unlike `/health`, this one genuinely has to call Printful — the figures do
|
|
19
|
+
* not exist locally. So the failure is handled rather than propagated: an
|
|
20
|
+
* outage answers `200` with an empty list and a `reason`, not a `500`. The
|
|
21
|
+
* page fetches three panels independently and a thrown error here would take
|
|
22
|
+
* out the one screen an owner opens *because* something is wrong.
|
|
23
|
+
*/
|
|
24
|
+
async function GET(req, res) {
|
|
25
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
26
|
+
const logger = req.scope.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
27
|
+
const query = req.query;
|
|
28
|
+
const now = new Date();
|
|
29
|
+
const from = new Date(now.getTime() - DEFAULT_DAYS * 24 * 60 * 60 * 1000);
|
|
30
|
+
const dateFrom = typeof query.date_from === "string" && query.date_from
|
|
31
|
+
? query.date_from
|
|
32
|
+
: isoDate(from);
|
|
33
|
+
const dateTo = typeof query.date_to === "string" && query.date_to
|
|
34
|
+
? query.date_to
|
|
35
|
+
: isoDate(now);
|
|
36
|
+
try {
|
|
37
|
+
const client = await printful.getClient();
|
|
38
|
+
const statistics = await client.getStatistics({
|
|
39
|
+
dateFrom,
|
|
40
|
+
dateTo,
|
|
41
|
+
reportTypes: REPORT_TYPES,
|
|
42
|
+
});
|
|
43
|
+
res.status(200).json({ statistics, date_from: dateFrom, date_to: dateTo });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
47
|
+
logger.error(`Printful statistics unavailable: ${message}`);
|
|
48
|
+
// 200 with an empty list, not an error status: the page treats this as
|
|
49
|
+
// "nothing to show" and keeps rendering sync history and webhook health,
|
|
50
|
+
// which are exactly what an operator needs during a Printful outage.
|
|
51
|
+
res.status(200).json({
|
|
52
|
+
statistics: [],
|
|
53
|
+
date_from: dateFrom,
|
|
54
|
+
date_to: dateTo,
|
|
55
|
+
reason: "printful_unreachable",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL3N0YXRpc3RpY3Mvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUEwQkEsa0JBd0NDO0FBakVELHFEQUFxRTtBQUNyRSwyREFBOEQ7QUFHOUQsZ0VBQWdFO0FBQ2hFLE1BQU0sWUFBWSxHQUFHLEVBQUUsQ0FBQTtBQUV2Qjs7OztHQUlHO0FBQ0gsTUFBTSxZQUFZLEdBQUcsbURBQW1ELENBQUE7QUFFeEUsTUFBTSxPQUFPLEdBQUcsQ0FBQyxDQUFPLEVBQVUsRUFBRSxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFBO0FBRWpFOzs7Ozs7OztHQVFHO0FBQ0ksS0FBSyxVQUFVLEdBQUcsQ0FBQyxHQUFrQixFQUFFLEdBQW1CO0lBQy9ELE1BQU0sUUFBUSxHQUEwQixHQUFHLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQywwQkFBZSxDQUFDLENBQUE7SUFDMUUsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsaUNBQXlCLENBQUMsTUFBTSxDQUFDLENBQUE7SUFFbEUsTUFBTSxLQUFLLEdBQUcsR0FBRyxDQUFDLEtBQWdDLENBQUE7SUFDbEQsTUFBTSxHQUFHLEdBQUcsSUFBSSxJQUFJLEVBQUUsQ0FBQTtJQUN0QixNQUFNLElBQUksR0FBRyxJQUFJLElBQUksQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLEdBQUcsWUFBWSxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFBO0lBRXpFLE1BQU0sUUFBUSxHQUNaLE9BQU8sS0FBSyxDQUFDLFNBQVMsS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLFNBQVM7UUFDcEQsQ0FBQyxDQUFDLEtBQUssQ0FBQyxTQUFTO1FBQ2pCLENBQUMsQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUE7SUFDbkIsTUFBTSxNQUFNLEdBQ1YsT0FBTyxLQUFLLENBQUMsT0FBTyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsT0FBTztRQUNoRCxDQUFDLENBQUMsS0FBSyxDQUFDLE9BQU87UUFDZixDQUFDLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFBO0lBRWxCLElBQUksQ0FBQztRQUNILE1BQU0sTUFBTSxHQUFHLE1BQU0sUUFBUSxDQUFDLFNBQVMsRUFBRSxDQUFBO1FBQ3pDLE1BQU0sVUFBVSxHQUFHLE1BQU0sTUFBTSxDQUFDLGFBQWEsQ0FBQztZQUM1QyxRQUFRO1lBQ1IsTUFBTTtZQUNOLFdBQVcsRUFBRSxZQUFZO1NBQzFCLENBQUMsQ0FBQTtRQUVGLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsVUFBVSxFQUFFLFNBQVMsRUFBRSxRQUFRLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUE7SUFDNUUsQ0FBQztJQUFDLE9BQU8sR0FBRyxFQUFFLENBQUM7UUFDYixNQUFNLE9BQU8sR0FBRyxHQUFHLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUE7UUFDaEUsTUFBTSxDQUFDLEtBQUssQ0FBQyxvQ0FBb0MsT0FBTyxFQUFFLENBQUMsQ0FBQTtRQUUzRCx1RUFBdUU7UUFDdkUseUVBQXlFO1FBQ3pFLHFFQUFxRTtRQUNyRSxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztZQUNuQixVQUFVLEVBQUUsRUFBRTtZQUNkLFNBQVMsRUFBRSxRQUFRO1lBQ25CLE9BQU8sRUFBRSxNQUFNO1lBQ2YsTUFBTSxFQUFFLHNCQUFzQjtTQUMvQixDQUFDLENBQUE7SUFDSixDQUFDO0FBQ0gsQ0FBQyJ9
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.POST = POST;
|
|
4
|
+
const printful_1 = require("../../../../../modules/printful");
|
|
5
|
+
const clear_sync_1 = require("../../../../../utils/clear-sync");
|
|
6
|
+
/**
|
|
7
|
+
* Clear a sync that is stuck in `running`.
|
|
8
|
+
*
|
|
9
|
+
* Nested under `sync/` rather than living at its own top-level path because it
|
|
10
|
+
* acts on exactly the resource `POST /admin/printful/sync` claims — the two
|
|
11
|
+
* belong together, and the 409 body from that route is what sends an operator
|
|
12
|
+
* here. POST, not DELETE: this does not remove the row, it transitions it to a
|
|
13
|
+
* terminal state and writes an audit marker.
|
|
14
|
+
*
|
|
15
|
+
* This closes the limit recorded in 0.4.0: after a crash the log stays
|
|
16
|
+
* `running` and nothing reclaims it until the next sync attempt, so with the
|
|
17
|
+
* default `syncStaleMinutes: 60` the catalog is blocked for up to an hour with
|
|
18
|
+
* no way to intervene.
|
|
19
|
+
*
|
|
20
|
+
* It deliberately does not just call `reapStaleSyncLogs(0)`. That would clear
|
|
21
|
+
* any running sync unconditionally, including a healthy one mid-catalog, and
|
|
22
|
+
* would stamp the row with the same `stale_running` marker the timeout reaper
|
|
23
|
+
* uses — making a human intervention indistinguishable from an automatic
|
|
24
|
+
* recovery in the audit trail. `planSyncClear` owns both decisions.
|
|
25
|
+
*/
|
|
26
|
+
async function POST(req, res) {
|
|
27
|
+
const printful = req.scope.resolve(printful_1.PRINTFUL_MODULE);
|
|
28
|
+
const options = await printful.getOptions();
|
|
29
|
+
const body = (req.body ?? {});
|
|
30
|
+
const confirmation = typeof body.confirm === "string" ? body.confirm : null;
|
|
31
|
+
const running = await printful.getRunningSyncLog();
|
|
32
|
+
const decision = (0, clear_sync_1.planSyncClear)({
|
|
33
|
+
log: running,
|
|
34
|
+
confirmation,
|
|
35
|
+
staleMinutes: options.syncStaleMinutes ?? 60,
|
|
36
|
+
});
|
|
37
|
+
if (!decision.allowed) {
|
|
38
|
+
if (decision.reason === "nothing_running") {
|
|
39
|
+
// Not an error. The page asked to clear a stuck sync and there is no
|
|
40
|
+
// sync to clear, which is the state the operator wanted.
|
|
41
|
+
res.status(200).json({
|
|
42
|
+
cleared: false,
|
|
43
|
+
reason: "nothing_running",
|
|
44
|
+
message: "No Printful sync is running",
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (decision.reason === "unconfirmed") {
|
|
49
|
+
res.status(400).json({
|
|
50
|
+
cleared: false,
|
|
51
|
+
reason: "unconfirmed",
|
|
52
|
+
message: `Clearing a sync is destructive. Send { "confirm": "${clear_sync_1.CLEAR_SYNC_CONFIRMATION}" } to proceed.`,
|
|
53
|
+
sync_id: decision.sync_id,
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// `alive`: the heartbeat says this sync is still running. 409 matches the
|
|
58
|
+
// 409 that POST /admin/printful/sync returns for the same condition — a
|
|
59
|
+
// sync is holding the claim and the caller cannot have it.
|
|
60
|
+
res.status(409).json({
|
|
61
|
+
cleared: false,
|
|
62
|
+
reason: "alive",
|
|
63
|
+
message: "This sync is still sending heartbeats. Clearing it would mark a live sync as failed while it keeps writing.",
|
|
64
|
+
sync_id: decision.sync_id,
|
|
65
|
+
silent_minutes: decision.silent_minutes,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Conditional on the heartbeat we just read. If the sync emitted one more
|
|
70
|
+
// between that read and this write, it was never dead and the update matches
|
|
71
|
+
// nothing — see clearStuckSyncLog.
|
|
72
|
+
const cleared = await printful.clearStuckSyncLog({
|
|
73
|
+
id: decision.sync_id,
|
|
74
|
+
observedHeartbeatAt: running?.heartbeat_at ?? null,
|
|
75
|
+
errorMessage: decision.error_message,
|
|
76
|
+
});
|
|
77
|
+
if (!cleared) {
|
|
78
|
+
res.status(409).json({
|
|
79
|
+
cleared: false,
|
|
80
|
+
reason: "alive",
|
|
81
|
+
message: "This sync sent a heartbeat while the request was being handled, so it is still alive and was left running.",
|
|
82
|
+
sync_id: decision.sync_id,
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
res.status(200).json({
|
|
87
|
+
cleared: true,
|
|
88
|
+
sync_id: decision.sync_id,
|
|
89
|
+
error_message: decision.error_message,
|
|
90
|
+
silent_minutes: decision.silent_minutes,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3ByaW50ZnVsL3N5bmMvY2xlYXIvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUE0QkEsb0JBNkVDO0FBeEdELDhEQUFpRTtBQUVqRSxnRUFHd0M7QUFFeEM7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FtQkc7QUFDSSxLQUFLLFVBQVUsSUFBSSxDQUFDLEdBQWtCLEVBQUUsR0FBbUI7SUFDaEUsTUFBTSxRQUFRLEdBQTBCLEdBQUcsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLDBCQUFlLENBQUMsQ0FBQTtJQUMxRSxNQUFNLE9BQU8sR0FBRyxNQUFNLFFBQVEsQ0FBQyxVQUFVLEVBQUUsQ0FBQTtJQUUzQyxNQUFNLElBQUksR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLElBQUksRUFBRSxDQUEwQixDQUFBO0lBQ3RELE1BQU0sWUFBWSxHQUFHLE9BQU8sSUFBSSxDQUFDLE9BQU8sS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQTtJQUUzRSxNQUFNLE9BQU8sR0FBRyxNQUFNLFFBQVEsQ0FBQyxpQkFBaUIsRUFBRSxDQUFBO0lBRWxELE1BQU0sUUFBUSxHQUFHLElBQUEsMEJBQWEsRUFBQztRQUM3QixHQUFHLEVBQUUsT0FBTztRQUNaLFlBQVk7UUFDWixZQUFZLEVBQUUsT0FBTyxDQUFDLGdCQUFnQixJQUFJLEVBQUU7S0FDN0MsQ0FBQyxDQUFBO0lBRUYsSUFBSSxDQUFDLFFBQVEsQ0FBQyxPQUFPLEVBQUUsQ0FBQztRQUN0QixJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssaUJBQWlCLEVBQUUsQ0FBQztZQUMxQyxxRUFBcUU7WUFDckUseURBQXlEO1lBQ3pELEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDO2dCQUNuQixPQUFPLEVBQUUsS0FBSztnQkFDZCxNQUFNLEVBQUUsaUJBQWlCO2dCQUN6QixPQUFPLEVBQUUsNkJBQTZCO2FBQ3ZDLENBQUMsQ0FBQTtZQUNGLE9BQU07UUFDUixDQUFDO1FBRUQsSUFBSSxRQUFRLENBQUMsTUFBTSxLQUFLLGFBQWEsRUFBRSxDQUFDO1lBQ3RDLEdBQUcsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxDQUFDO2dCQUNuQixPQUFPLEVBQUUsS0FBSztnQkFDZCxNQUFNLEVBQUUsYUFBYTtnQkFDckIsT0FBTyxFQUFFLHNEQUFzRCxvQ0FBdUIsaUJBQWlCO2dCQUN2RyxPQUFPLEVBQUUsUUFBUSxDQUFDLE9BQU87YUFDMUIsQ0FBQyxDQUFBO1lBQ0YsT0FBTTtRQUNSLENBQUM7UUFFRCwwRUFBMEU7UUFDMUUsd0VBQXdFO1FBQ3hFLDJEQUEyRDtRQUMzRCxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQztZQUNuQixPQUFPLEVBQUUsS0FBSztZQUNkLE1BQU0sRUFBRSxPQUFPO1lBQ2YsT0FBTyxFQUNMLDZHQUE2RztZQUMvRyxPQUFPLEVBQUUsUUFBUSxDQUFDLE9BQU87WUFDekIsY0FBYyxFQUFFLFFBQVEsQ0FBQyxjQUFjO1NBQ3hDLENBQUMsQ0FBQTtRQUNGLE9BQU07SUFDUixDQUFDO0lBRUQsMEVBQTBFO0lBQzFFLDZFQUE2RTtJQUM3RSxtQ0FBbUM7SUFDbkMsTUFBTSxPQUFPLEdBQUcsTUFBTSxRQUFRLENBQUMsaUJBQWlCLENBQUM7UUFDL0MsRUFBRSxFQUFFLFFBQVEsQ0FBQyxPQUFPO1FBQ3BCLG1CQUFtQixFQUFFLE9BQU8sRUFBRSxZQUFZLElBQUksSUFBSTtRQUNsRCxZQUFZLEVBQUUsUUFBUSxDQUFDLGFBQWE7S0FDckMsQ0FBQyxDQUFBO0lBRUYsSUFBSSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ2IsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7WUFDbkIsT0FBTyxFQUFFLEtBQUs7WUFDZCxNQUFNLEVBQUUsT0FBTztZQUNmLE9BQU8sRUFDTCw0R0FBNEc7WUFDOUcsT0FBTyxFQUFFLFFBQVEsQ0FBQyxPQUFPO1NBQzFCLENBQUMsQ0FBQTtRQUNGLE9BQU07SUFDUixDQUFDO0lBRUQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUM7UUFDbkIsT0FBTyxFQUFFLElBQUk7UUFDYixPQUFPLEVBQUUsUUFBUSxDQUFDLE9BQU87UUFDekIsYUFBYSxFQUFFLFFBQVEsQ0FBQyxhQUFhO1FBQ3JDLGNBQWMsRUFBRSxRQUFRLENBQUMsY0FBYztLQUN4QyxDQUFDLENBQUE7QUFDSixDQUFDIn0=
|
|
@@ -299,6 +299,44 @@ class PrintfulModuleService extends (0, utils_1.MedusaService)({
|
|
|
299
299
|
const [log] = await this.listPrintfulSyncLogs({ status: "running" });
|
|
300
300
|
return log ?? null;
|
|
301
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Clear a specific stuck sync on an operator's instruction.
|
|
304
|
+
*
|
|
305
|
+
* Separate from `reapStaleSyncLogs` on purpose. That one is the timeout
|
|
306
|
+
* reaper: it selects its own victims by age and stamps them `stale_running`.
|
|
307
|
+
* This one clears exactly the row the operator was looking at and records a
|
|
308
|
+
* marker naming them as the cause, so the two are distinguishable in the log
|
|
309
|
+
* long after the fact.
|
|
310
|
+
*
|
|
311
|
+
* The `heartbeat_at` predicate makes the write conditional on the row still
|
|
312
|
+
* being as stale as the caller observed. Between the route's read and this
|
|
313
|
+
* write, a sync everyone believed dead can emit one more heartbeat; without
|
|
314
|
+
* this predicate that row would be marked `failed` underneath a live
|
|
315
|
+
* process. With it, the late heartbeat moves `heartbeat_at` past the
|
|
316
|
+
* observed value, the update matches nothing, and the caller is told the
|
|
317
|
+
* sync is alive after all.
|
|
318
|
+
*
|
|
319
|
+
* Returns true when a row was actually cleared.
|
|
320
|
+
*/
|
|
321
|
+
async clearStuckSyncLog(input) {
|
|
322
|
+
const stillStale = await this.listPrintfulSyncLogs({
|
|
323
|
+
id: input.id,
|
|
324
|
+
status: "running",
|
|
325
|
+
heartbeat_at: input.observedHeartbeatAt
|
|
326
|
+
? { $lte: input.observedHeartbeatAt }
|
|
327
|
+
: null,
|
|
328
|
+
});
|
|
329
|
+
if (stillStale.length === 0) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
await this.updatePrintfulSyncLogs({
|
|
333
|
+
id: input.id,
|
|
334
|
+
status: "failed",
|
|
335
|
+
error_message: input.errorMessage,
|
|
336
|
+
finished_at: new Date(),
|
|
337
|
+
});
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
302
340
|
/** Refresh the claim so it is not reaped, and report progress. */
|
|
303
341
|
async heartbeatSyncLog(id, progress = {}) {
|
|
304
342
|
return this.updatePrintfulSyncLogs({
|
|
@@ -309,4 +347,4 @@ class PrintfulModuleService extends (0, utils_1.MedusaService)({
|
|
|
309
347
|
}
|
|
310
348
|
}
|
|
311
349
|
exports.default = PrintfulModuleService;
|
|
312
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
350
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL3ByaW50ZnVsL3NlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBZ0RBLDRDQUlDO0FBYUQsZ0NBR0M7QUFpQkQsOENBMkJDO0FBaEhELHFEQUF5RDtBQUN6RCxtQ0FBbUM7QUFDbkMsaUVBQTREO0FBRTVELHVGQUE0RDtBQUM1RCwyRkFBZ0U7QUFDaEUsbUZBQXdEO0FBQ3hELDJGQUFnRTtBQUNoRSw2RkFBa0U7QUEyQmxFOzs7Ozs7R0FNRztBQUNVLFFBQUEseUJBQXlCLEdBQUcsU0FBUyxDQUFBO0FBRWxELGdFQUFnRTtBQUNuRCxRQUFBLG9CQUFvQixHQUFHLEVBQUUsQ0FBQTtBQUV0QyxrRUFBa0U7QUFDbEUsU0FBZ0IsZ0JBQWdCLENBQUMsUUFBZ0I7SUFDL0MsTUFBTSxZQUFZLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUE7SUFDbEMsTUFBTSxTQUFTLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFBO0lBQ3BDLE9BQU8sSUFBSSxDQUFDLEdBQUcsQ0FBQyxZQUFZLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsUUFBUSxDQUFDLEVBQUUsU0FBUyxDQUFDLENBQUE7QUFDbEUsQ0FBQztBQUVEOzs7Ozs7Ozs7O0dBVUc7QUFDSCxTQUFnQixVQUFVLENBQUMsZUFBdUI7SUFDaEQsTUFBTSxNQUFNLEdBQUcsSUFBQSxtQkFBVSxFQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsZUFBZSxDQUFDLENBQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQTtJQUM1RSxPQUFPLE1BQU0sQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLENBQUE7QUFDOUIsQ0FBQztBQUVEOzs7Ozs7Ozs7Ozs7OztHQWNHO0FBQ0gsU0FBZ0IsaUJBQWlCLENBQUMsR0FB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
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLEARED_BY_OPERATOR = exports.CLEAR_SYNC_CONFIRMATION = void 0;
|
|
4
|
+
exports.planSyncClear = planSyncClear;
|
|
5
|
+
/**
|
|
6
|
+
* Exact phrase the caller must send to clear a stuck sync.
|
|
7
|
+
*
|
|
8
|
+
* Clearing is destructive, so it must not be reachable by an empty POST. A
|
|
9
|
+
* typed phrase makes the operator's intent explicit and unambiguous in the
|
|
10
|
+
* request itself — a boolean `force: true` is too easy to send by accident
|
|
11
|
+
* from a curl line or a mis-wired button.
|
|
12
|
+
*/
|
|
13
|
+
exports.CLEAR_SYNC_CONFIRMATION = "clear-stuck-sync";
|
|
14
|
+
/**
|
|
15
|
+
* Audit marker written to `error_message` when a human clears a sync.
|
|
16
|
+
*
|
|
17
|
+
* `reapStaleSyncLogs` writes the bare string "stale_running" when the timeout
|
|
18
|
+
* reclaims a row on its own. These two must never be confused: one says the
|
|
19
|
+
* system recovered by itself after `syncStaleMinutes`, the other says a person
|
|
20
|
+
* decided the sync was dead and intervened. Anyone reading this log six months
|
|
21
|
+
* from now needs to be able to tell those apart.
|
|
22
|
+
*/
|
|
23
|
+
exports.CLEARED_BY_OPERATOR = "cleared_by_operator";
|
|
24
|
+
function toDate(value) {
|
|
25
|
+
if (value == null) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
29
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Decide whether a running sync may be cleared by hand, and what to record.
|
|
33
|
+
*
|
|
34
|
+
* This exists instead of simply exposing `reapStaleSyncLogs(0)`, which would
|
|
35
|
+
* clear ANY running sync unconditionally. That call has no notion of whether
|
|
36
|
+
* the sync is alive: a healthy run mid-catalog would be marked `failed` while
|
|
37
|
+
* its process keeps writing, which both corrupts the log and frees the claim
|
|
38
|
+
* so a second sync can start against the same catalog.
|
|
39
|
+
*
|
|
40
|
+
* Two guards, deliberately layered:
|
|
41
|
+
*
|
|
42
|
+
* 1. The caller must send the confirmation phrase. Checked FIRST, so an
|
|
43
|
+
* unconfirmed request against a live sync is told it is unconfirmed rather
|
|
44
|
+
* than being told it is alive — the latter would wrongly imply that
|
|
45
|
+
* confirming alone would have been enough.
|
|
46
|
+
* 2. The heartbeat must actually be stale. Confirmation proves intent, not
|
|
47
|
+
* that the operator is right. An operator staring at a widget cannot see
|
|
48
|
+
* the heartbeat; the server can, and refuses on their behalf.
|
|
49
|
+
*
|
|
50
|
+
* The stale window is clamped to a minimum of one minute so that passing 0 —
|
|
51
|
+
* the exact foot-gun this function replaces — cannot make a live sync
|
|
52
|
+
* clearable.
|
|
53
|
+
*
|
|
54
|
+
* A row with no heartbeat at all falls back to `started_at`. A crash between
|
|
55
|
+
* the claim insert and the first heartbeat leaves `heartbeat_at` null, and
|
|
56
|
+
* such a row must not be permanently unclearable; but a claim made seconds ago
|
|
57
|
+
* is far more likely to be a sync starting up than a dead one.
|
|
58
|
+
*/
|
|
59
|
+
function planSyncClear(input) {
|
|
60
|
+
const { log, confirmation, staleMinutes } = input;
|
|
61
|
+
const now = input.now ?? new Date();
|
|
62
|
+
if (!log || log.status !== "running") {
|
|
63
|
+
return { allowed: false, reason: "nothing_running" };
|
|
64
|
+
}
|
|
65
|
+
if (confirmation !== exports.CLEAR_SYNC_CONFIRMATION) {
|
|
66
|
+
return { allowed: false, reason: "unconfirmed", sync_id: log.id };
|
|
67
|
+
}
|
|
68
|
+
// Never trust a zero or negative window: that is precisely the call this
|
|
69
|
+
// function exists to prevent.
|
|
70
|
+
const window = Math.max(1, Number.isFinite(staleMinutes) ? staleMinutes : 1);
|
|
71
|
+
const lastSign = toDate(log.heartbeat_at) ?? toDate(log.started_at);
|
|
72
|
+
const silentMs = lastSign ? now.getTime() - lastSign.getTime() : Infinity;
|
|
73
|
+
const silentMinutes = Number.isFinite(silentMs)
|
|
74
|
+
? Math.max(0, Math.floor(silentMs / 60_000))
|
|
75
|
+
: Number.MAX_SAFE_INTEGER;
|
|
76
|
+
if (silentMinutes < window) {
|
|
77
|
+
return {
|
|
78
|
+
allowed: false,
|
|
79
|
+
reason: "alive",
|
|
80
|
+
sync_id: log.id,
|
|
81
|
+
silent_minutes: silentMinutes,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
allowed: true,
|
|
86
|
+
sync_id: log.id,
|
|
87
|
+
error_message: `${exports.CLEARED_BY_OPERATOR}: no heartbeat for ${silentMinutes}m`,
|
|
88
|
+
silent_minutes: silentMinutes,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xlYXItc3luYy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy91dGlscy9jbGVhci1zeW5jLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQTJGQSxzQ0EyQ0M7QUF0SUQ7Ozs7Ozs7R0FPRztBQUNVLFFBQUEsdUJBQXVCLEdBQUcsa0JBQWtCLENBQUE7QUFFekQ7Ozs7Ozs7O0dBUUc7QUFDVSxRQUFBLG1CQUFtQixHQUFHLHFCQUFxQixDQUFBO0FBb0N4RCxTQUFTLE1BQU0sQ0FBQyxLQUF1QztJQUNyRCxJQUFJLEtBQUssSUFBSSxJQUFJLEVBQUUsQ0FBQztRQUNsQixPQUFPLElBQUksQ0FBQTtJQUNiLENBQUM7SUFDRCxNQUFNLElBQUksR0FBRyxLQUFLLFlBQVksSUFBSSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFBO0lBQzVELE9BQU8sTUFBTSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUE7QUFDbkQsQ0FBQztBQUVEOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0EyQkc7QUFDSCxTQUFnQixhQUFhLENBQUMsS0FNN0I7SUFDQyxNQUFNLEVBQUUsR0FBRyxFQUFFLFlBQVksRUFBRSxZQUFZLEVBQUUsR0FBRyxLQUFLLENBQUE7SUFDakQsTUFBTSxHQUFHLEdBQUcsS0FBSyxDQUFDLEdBQUcsSUFBSSxJQUFJLElBQUksRUFBRSxDQUFBO0lBRW5DLElBQUksQ0FBQyxHQUFHLElBQUksR0FBRyxDQUFDLE1BQU0sS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUNyQyxPQUFPLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsaUJBQWlCLEVBQUUsQ0FBQTtJQUN0RCxDQUFDO0lBRUQsSUFBSSxZQUFZLEtBQUssK0JBQXVCLEVBQUUsQ0FBQztRQUM3QyxPQUFPLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsYUFBYSxFQUFFLE9BQU8sRUFBRSxHQUFHLENBQUMsRUFBRSxFQUFFLENBQUE7SUFDbkUsQ0FBQztJQUVELHlFQUF5RTtJQUN6RSw4QkFBOEI7SUFDOUIsTUFBTSxNQUFNLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsTUFBTSxDQUFDLFFBQVEsQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQTtJQUU1RSxNQUFNLFFBQVEsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLFlBQVksQ0FBQyxJQUFJLE1BQU0sQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLENBQUE7SUFDbkUsTUFBTSxRQUFRLEdBQUcsUUFBUSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLEdBQUcsUUFBUSxDQUFDLE9BQU8sRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUE7SUFDekUsTUFBTSxhQUFhLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUM7UUFDN0MsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsUUFBUSxHQUFHLE1BQU0sQ0FBQyxDQUFDO1FBQzVDLENBQUMsQ0FBQyxNQUFNLENBQUMsZ0JBQWdCLENBQUE7SUFFM0IsSUFBSSxhQUFhLEdBQUcsTUFBTSxFQUFFLENBQUM7UUFDM0IsT0FBTztZQUNMLE9BQU8sRUFBRSxLQUFLO1lBQ2QsTUFBTSxFQUFFLE9BQU87WUFDZixPQUFPLEVBQUUsR0FBRyxDQUFDLEVBQUU7WUFDZixjQUFjLEVBQUUsYUFBYTtTQUM5QixDQUFBO0lBQ0gsQ0FBQztJQUVELE9BQU87UUFDTCxPQUFPLEVBQUUsSUFBSTtRQUNiLE9BQU8sRUFBRSxHQUFHLENBQUMsRUFBRTtRQUNmLGFBQWEsRUFBRSxHQUFHLDJCQUFtQixzQkFBc0IsYUFBYSxHQUFHO1FBQzNFLGNBQWMsRUFBRSxhQUFhO0tBQzlCLENBQUE7QUFDSCxDQUFDIn0=
|
|
@@ -107,6 +107,28 @@ class PrintfulClient {
|
|
|
107
107
|
});
|
|
108
108
|
return data.result ?? { url: null, types: [] };
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Sales, costs and profit for a date range.
|
|
112
|
+
*
|
|
113
|
+
* Returns the per-store list, empty when Printful reports none — the admin
|
|
114
|
+
* page maps over this, and a missing key must not crash the one screen whose
|
|
115
|
+
* job is telling the owner what is going on.
|
|
116
|
+
*
|
|
117
|
+
* `currency` is omitted rather than defaulted when absent: Printful then
|
|
118
|
+
* reports in the store's own currency, which is a different request from
|
|
119
|
+
* asking for a specific one.
|
|
120
|
+
*/
|
|
121
|
+
async getStatistics(params) {
|
|
122
|
+
const search = new URLSearchParams();
|
|
123
|
+
search.set("date_from", params.dateFrom);
|
|
124
|
+
search.set("date_to", params.dateTo);
|
|
125
|
+
search.set("report_types", params.reportTypes);
|
|
126
|
+
if (params.currency) {
|
|
127
|
+
search.set("currency", params.currency);
|
|
128
|
+
}
|
|
129
|
+
const data = await this.request(`/reports/statistics?${search.toString()}`);
|
|
130
|
+
return data.result?.store_statistics ?? [];
|
|
131
|
+
}
|
|
110
132
|
/**
|
|
111
133
|
* Quote shipping for a cart. Returns every method Printful offers for that
|
|
112
134
|
* destination in one response — the caller picks the one it needs.
|
|
@@ -208,4 +230,4 @@ class PrintfulClient {
|
|
|
208
230
|
}
|
|
209
231
|
}
|
|
210
232
|
exports.PrintfulClient = PrintfulClient;
|
|
211
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
233
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJpbnRmdWwtY2xpZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL3ByaW50ZnVsLWNsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxxQ0FBZ0U7QUFvQ2hFLE1BQU0sZ0JBQWdCLEdBQUcsMEJBQTBCLENBQUE7QUFFbkQsTUFBYSxjQUFjO0lBU3pCLFlBQVksT0FBOEI7UUFDeEMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUN0QixNQUFNLElBQUksNEJBQW1CLENBQUMsK0JBQStCLENBQUMsQ0FBQTtRQUNoRSxDQUFDO1FBRUQsSUFBSSxDQUFDLFFBQVEsR0FBRyxPQUFPLENBQUMsUUFBUSxDQUFBO1FBQ2hDLElBQUksQ0FBQyxPQUFPLEdBQUcsT0FBTyxDQUFDLE9BQU8sQ0FBQTtRQUM5QixJQUFJLENBQUMsT0FBTyxHQUFHLENBQUMsT0FBTyxDQUFDLE9BQU8sSUFBSSxnQkFBZ0IsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUE7UUFDdkUsSUFBSSxDQUFDLFNBQVMsR0FBRyxPQUFPLENBQUMsU0FBUyxJQUFJLEtBQUssQ0FBQTtRQUMzQyxJQUFJLENBQUMsVUFBVSxHQUFHLE9BQU8sQ0FBQyxVQUFVLElBQUksQ0FBQyxDQUFBO1FBQ3pDLElBQUksQ0FBQyxXQUFXLEdBQUcsT0FBTyxDQUFDLFdBQVcsSUFBSSxHQUFHLENBQUE7UUFDN0MsSUFBSSxDQUFDLFNBQVM7WUFDWixNQUFNLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFBO0lBQ3JFLENBQUM7SUFFRCxLQUFLLENBQUMsZ0JBQWdCLENBQUMsU0FBaUMsRUFBRTtRQUl4RCxNQUFNLE1BQU0sR0FBRyxJQUFJLGVBQWUsRUFBRSxD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
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summarizeWebhookHealth = summarizeWebhookHealth;
|
|
4
|
+
function toDate(value) {
|
|
5
|
+
if (value == null) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
9
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Summarise the webhook event table for the admin page.
|
|
13
|
+
*
|
|
14
|
+
* This is intentionally derived from LOCAL rows only. The obvious alternative
|
|
15
|
+
* is to call `client.getWebhookConfig()` and report Printful's registered URL
|
|
16
|
+
* verbatim, and this route deliberately does not: a health panel whose job is
|
|
17
|
+
* to tell the owner whether Printful is reaching them must not itself go dark
|
|
18
|
+
* when Printful is down. `/admin/printful/webhook` already exposes the live
|
|
19
|
+
* config for the moment an admin actually wants to inspect or change it.
|
|
20
|
+
*
|
|
21
|
+
* The cost of that choice is that `registered` is evidence-based rather than
|
|
22
|
+
* authoritative. A configured secret proves the owner intended to register a
|
|
23
|
+
* webhook; it does not prove the URL was ever pushed to Printful. So
|
|
24
|
+
* `registered` requires a secret AND a delivered event — the one thing that
|
|
25
|
+
* genuinely proves the round trip works. A store that has a secret but no
|
|
26
|
+
* events reads as "configured, nothing received yet", which is the honest
|
|
27
|
+
* statement and is exactly the state a misconfigured webhook leaves behind.
|
|
28
|
+
*/
|
|
29
|
+
function summarizeWebhookHealth(rows, config) {
|
|
30
|
+
let received = 0;
|
|
31
|
+
let deferred = 0;
|
|
32
|
+
let failed = 0;
|
|
33
|
+
let processed = 0;
|
|
34
|
+
let lastEventAt = null;
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
switch (row.status) {
|
|
37
|
+
case "received":
|
|
38
|
+
received += 1;
|
|
39
|
+
break;
|
|
40
|
+
case "deferred":
|
|
41
|
+
deferred += 1;
|
|
42
|
+
break;
|
|
43
|
+
case "failed":
|
|
44
|
+
failed += 1;
|
|
45
|
+
break;
|
|
46
|
+
case "processed":
|
|
47
|
+
processed += 1;
|
|
48
|
+
break;
|
|
49
|
+
// A status added by a later release counts toward `total` only. Guessing
|
|
50
|
+
// it is a failure would show the owner an alarm that is not real.
|
|
51
|
+
default:
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const createdAt = toDate(row.created_at);
|
|
55
|
+
if (createdAt && (!lastEventAt || createdAt > lastEventAt)) {
|
|
56
|
+
lastEventAt = createdAt;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
total: rows.length,
|
|
61
|
+
received,
|
|
62
|
+
deferred,
|
|
63
|
+
failed,
|
|
64
|
+
processed,
|
|
65
|
+
last_event_at: lastEventAt,
|
|
66
|
+
// Based on row count, not on the timestamp: a row with an unreadable
|
|
67
|
+
// created_at still proves something arrived.
|
|
68
|
+
ever_received: rows.length > 0,
|
|
69
|
+
secret_set: config.secret_set,
|
|
70
|
+
registered: config.secret_set && rows.length > 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2ViaG9vay1oZWFsdGguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvdXRpbHMvd2ViaG9vay1oZWFsdGgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUE4REEsd0RBaURDO0FBM0VELFNBQVMsTUFBTSxDQUFDLEtBQXVDO0lBQ3JELElBQUksS0FBSyxJQUFJLElBQUksRUFBRSxDQUFDO1FBQ2xCLE9BQU8sSUFBSSxDQUFBO0lBQ2IsQ0FBQztJQUNELE1BQU0sSUFBSSxHQUFHLEtBQUssWUFBWSxJQUFJLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUE7SUFDNUQsT0FBTyxNQUFNLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQTtBQUNuRCxDQUFDO0FBRUQ7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBaUJHO0FBQ0gsU0FBZ0Isc0JBQXNCLENBQ3BDLElBQXFDLEVBQ3JDLE1BQStCO0lBRS9CLElBQUksUUFBUSxHQUFHLENBQUMsQ0FBQTtJQUNoQixJQUFJLFFBQVEsR0FBRyxDQUFDLENBQUE7SUFDaEIsSUFBSSxNQUFNLEdBQUcsQ0FBQyxDQUFBO0lBQ2QsSUFBSSxTQUFTLEdBQUcsQ0FBQyxDQUFBO0lBQ2pCLElBQUksV0FBVyxHQUFnQixJQUFJLENBQUE7SUFFbkMsS0FBSyxNQUFNLEdBQUcsSUFBSSxJQUFJLEVBQUUsQ0FBQztRQUN2QixRQUFRLEdBQUcsQ0FBQyxNQUFNLEVBQUUsQ0FBQztZQUNuQixLQUFLLFVBQVU7Z0JBQ2IsUUFBUSxJQUFJLENBQUMsQ0FBQTtnQkFDYixNQUFLO1lBQ1AsS0FBSyxVQUFVO2dCQUNiLFFBQVEsSUFBSSxDQUFDLENBQUE7Z0JBQ2IsTUFBSztZQUNQLEtBQUssUUFBUTtnQkFDWCxNQUFNLElBQUksQ0FBQyxDQUFBO2dCQUNYLE1BQUs7WUFDUCxLQUFLLFdBQVc7Z0JBQ2QsU0FBUyxJQUFJLENBQUMsQ0FBQTtnQkFDZCxNQUFLO1lBQ1AseUVBQXlFO1lBQ3pFLGtFQUFrRTtZQUNsRTtnQkFDRSxNQUFLO1FBQ1QsQ0FBQztRQUVELE1BQU0sU0FBUyxHQUFHLE1BQU0sQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLENBQUE7UUFDeEMsSUFBSSxTQUFTLElBQUksQ0FBQyxDQUFDLFdBQVcsSUFBSSxTQUFTLEdBQUcsV0FBVyxDQUFDLEVBQUUsQ0FBQztZQUMzRCxXQUFXLEdBQUcsU0FBUyxDQUFBO1FBQ3pCLENBQUM7SUFDSCxDQUFDO0lBRUQsT0FBTztRQUNMLEtBQUssRUFBRSxJQUFJLENBQUMsTUFBTTtRQUNsQixRQUFRO1FBQ1IsUUFBUTtRQUNSLE1BQU07UUFDTixTQUFTO1FBQ1QsYUFBYSxFQUFFLFdBQVc7UUFDMUIscUVBQXFFO1FBQ3JFLDZDQUE2QztRQUM3QyxhQUFhLEVBQUUsSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDO1FBQzlCLFVBQVUsRUFBRSxNQUFNLENBQUMsVUFBVTtRQUM3QixVQUFVLEVBQUUsTUFBTSxDQUFDLFVBQVUsSUFBSSxJQUFJLENBQUMsTUFBTSxHQUFHLENBQUM7S0FDakQsQ0FBQTtBQUNILENBQUMifQ==
|
package/README.md
CHANGED
|
@@ -96,6 +96,7 @@ See `examples/basic-store/` for a fuller snippet.
|
|
|
96
96
|
| Orders | On `payment.captured` → creates Printful order with **`sync_variant_id`** |
|
|
97
97
|
| Fulfillment provider | Select Printful shipping option in Admin locations |
|
|
98
98
|
| Status | `GET /admin/printful/status` + product list widget |
|
|
99
|
+
| Admin page | **Printful** in the sidebar: sales, sync history, webhook health, stuck-sync recovery |
|
|
99
100
|
| Shipment tracking | Printful webhooks → Medusa fulfillment + shipment per parcel, with tracking |
|
|
100
101
|
| Order visibility | Printful status and per-parcel tracking on the Admin order page |
|
|
101
102
|
|
|
@@ -320,6 +321,35 @@ so they cannot disagree; the individual `printful_cost_*` fee keys are refreshed
|
|
|
320
321
|
per-key and a fee absent from a later response keeps its previous value, so a
|
|
321
322
|
breakdown need not sum to the total.
|
|
322
323
|
|
|
324
|
+
## The Printful admin page
|
|
325
|
+
|
|
326
|
+
A **Printful** section in the sidebar, gathering what the widgets could not:
|
|
327
|
+
|
|
328
|
+
- **Sales** — paid orders and profit from Printful's own reporting
|
|
329
|
+
- **Webhooks** — when the last event arrived, how many are waiting or failed
|
|
330
|
+
- **Recent syncs** — status, counters, and live progress while one runs
|
|
331
|
+
- **Stuck sync recovery** — see below
|
|
332
|
+
|
|
333
|
+
Two of these behave differently on purpose. **Webhook health never calls
|
|
334
|
+
Printful**: a panel whose job is reporting whether Printful reaches you must
|
|
335
|
+
still render when Printful is down, so it answers from local rows. **Sales does
|
|
336
|
+
call Printful**, because the figures do not exist locally — and an outage
|
|
337
|
+
answers with an empty panel rather than blanking the page.
|
|
338
|
+
|
|
339
|
+
### When a sync gets stuck
|
|
340
|
+
|
|
341
|
+
If Medusa dies mid-sync, the run stays `running` and nothing else can start
|
|
342
|
+
until it is reclaimed — up to `syncStaleMinutes`, one hour by default. The page
|
|
343
|
+
notices and offers to clear it.
|
|
344
|
+
|
|
345
|
+
Clearing is deliberately awkward, because it is destructive. The request must
|
|
346
|
+
carry a typed confirmation, and the **server** re-checks the heartbeat: if the
|
|
347
|
+
sync is actually alive, it refuses and tells you so. A wrong guess costs a
|
|
348
|
+
message, never a killed sync.
|
|
349
|
+
|
|
350
|
+
A run cleared by a person is recorded as `cleared_by_operator` with how long it
|
|
351
|
+
had been silent, so it is never confused with one the timeout reaped.
|
|
352
|
+
|
|
323
353
|
## Admin usage
|
|
324
354
|
|
|
325
355
|
1. Create products in Printful (Store Products).
|