pa_encoder 0.1.0 → 0.2.0

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.
@@ -0,0 +1,368 @@
1
+ function qp(name) {
2
+ return new URL(location.href).searchParams.get(name);
3
+ }
4
+
5
+ const $ = (id) => document.getElementById(id);
6
+
7
+ const entryEl = $("entry");
8
+ const modeEl = $("mode");
9
+ const canvasEl = $("canvas");
10
+ const fpsEl = $("fps");
11
+ const exporterEl = $("exporter");
12
+
13
+ const policyEl = $("policy");
14
+ const concurrencyEl = $("concurrency");
15
+ const maxQueueEl = $("maxQueue");
16
+ const durationEl = $("duration");
17
+
18
+ const framesEl = $("frames");
19
+ const warmupEl = $("warmup");
20
+
21
+ const detailsLive = $("detailsLive");
22
+ const detailsFrame = $("detailsFrame");
23
+
24
+ const btnReload = $("btnReload");
25
+ const btnStart = $("btnStart");
26
+ const btnStop = $("btnStop");
27
+
28
+ const btnHide = $("btnHide");
29
+ const btnCompact = $("btnCompact");
30
+ const btnDock = $("btnDock");
31
+
32
+ const btnPassthrough = $("btnPassthrough");
33
+ const btnFocus = $("btnFocus");
34
+
35
+ const logEl = $("log");
36
+ const preview = $("preview");
37
+
38
+ let running = false;
39
+ let stopTimer = null;
40
+
41
+ const docks = ["dock-br", "dock-bl", "dock-tr", "dock-tl"];
42
+ let dockIndex = 0;
43
+
44
+ let passthrough = false;
45
+
46
+ function appendLog(line) {
47
+ const s = String(line);
48
+ const needsNL = logEl.textContent && !logEl.textContent.endsWith("\n");
49
+ logEl.textContent += (needsNL ? "\n" : "") + s;
50
+ logEl.scrollTop = logEl.scrollHeight;
51
+ }
52
+
53
+ function setRunning(v) {
54
+ running = v;
55
+ btnStart.disabled = v;
56
+ btnStop.disabled = !v;
57
+
58
+ // recording starts -> enable passthrough so canvas can receive interaction
59
+ if (v) {
60
+ setPassthrough(true, { silent: true });
61
+ // best-effort focus request
62
+ postToPreview("pa_focus_canvas", {
63
+ canvasSelector: selectedCanvasSelector(),
64
+ });
65
+ }
66
+ }
67
+
68
+ function buildPreviewUrl(entry) {
69
+ const e = entry || "/src/main.js";
70
+ return `/__pa_encoder__/ui/preview.html?entry=${encodeURIComponent(e)}`;
71
+ }
72
+
73
+ function buildPreviewAutostartUrl(entry, payload) {
74
+ const e = entry || "/src/main.js";
75
+ const json = JSON.stringify(payload);
76
+ const b64 = btoa(json);
77
+ return (
78
+ `/__pa_encoder__/ui/preview.html?entry=${encodeURIComponent(e)}` +
79
+ `&autostart=1&payload=${encodeURIComponent(b64)}`
80
+ );
81
+ }
82
+
83
+ function reloadPreview() {
84
+ const entry = entryEl.value.trim() || "/src/main.js";
85
+ preview.src = buildPreviewUrl(entry);
86
+ appendLog(`reloading preview (entry=${entry})`);
87
+ }
88
+
89
+ function postToPreview(type, payload = {}) {
90
+ const w = preview.contentWindow;
91
+ if (!w) return false;
92
+ w.postMessage({ type, payload }, "*");
93
+ return true;
94
+ }
95
+
96
+ function fillCanvasDropdown(canvases) {
97
+ const prev = canvasEl.value;
98
+
99
+ canvasEl.innerHTML = "";
100
+ const opt0 = document.createElement("option");
101
+ opt0.value = "";
102
+ opt0.textContent = canvases.length ? "(auto)" : "(no canvas found)";
103
+ canvasEl.appendChild(opt0);
104
+
105
+ for (const c of canvases) {
106
+ const opt = document.createElement("option");
107
+ opt.value = c.selector;
108
+ opt.textContent = c.label || c.selector;
109
+ canvasEl.appendChild(opt);
110
+ }
111
+
112
+ const hasPrev = Array.from(canvasEl.options).some((o) => o.value === prev);
113
+ if (hasPrev) canvasEl.value = prev;
114
+ }
115
+
116
+ function selectedCanvasSelector() {
117
+ return canvasEl.value || "canvas";
118
+ }
119
+
120
+ function exporterPayload() {
121
+ const mode = exporterEl.value; // zip|fs|best
122
+ const zipName = "frames.zip";
123
+ return { exporter: { mode, prefer: "fs", zipName } };
124
+ }
125
+
126
+ function setDockByIndex(i) {
127
+ for (const d of docks) document.body.classList.remove(d);
128
+ dockIndex = ((i % docks.length) + docks.length) % docks.length;
129
+ document.body.classList.add(docks[dockIndex]);
130
+ }
131
+
132
+ function cycleDock() {
133
+ setDockByIndex(dockIndex + 1);
134
+ appendLog(`dock: ${docks[dockIndex]}`);
135
+ }
136
+
137
+ function setPassthrough(on, { silent = false } = {}) {
138
+ passthrough = !!on;
139
+ document.body.classList.toggle("passthrough", passthrough);
140
+ btnPassthrough.textContent = `Passthrough: ${passthrough ? "On" : "Off"}`;
141
+ if (!silent) appendLog(`passthrough: ${passthrough ? "on" : "off"}`);
142
+ }
143
+
144
+ function focusSketch() {
145
+ // Best-effort:
146
+ // - Enable passthrough so user can click canvas
147
+ // - Ask preview to focus the canvas
148
+ setPassthrough(true);
149
+ postToPreview("pa_focus_canvas", {
150
+ canvasSelector: selectedCanvasSelector(),
151
+ });
152
+ appendLog("focus sketch: click the canvas if focus is not acquired");
153
+ }
154
+
155
+ function syncModeUI() {
156
+ const isFrame = modeEl.value === "frame";
157
+ detailsFrame.style.display = isFrame ? "block" : "none";
158
+ detailsLive.style.display = isFrame ? "none" : "block";
159
+ if (isFrame) detailsFrame.open = true;
160
+ else detailsLive.open = true;
161
+ }
162
+
163
+ function currentPayload() {
164
+ const fps = Number(fpsEl.value || "60");
165
+ const kind = modeEl.value === "frame" ? "frame" : "live";
166
+ const canvasSelector = selectedCanvasSelector();
167
+
168
+ if (kind === "frame") {
169
+ const frames = Math.max(1, Math.floor(Number(framesEl.value || "300")));
170
+ const warmup = Math.max(0, Math.floor(Number(warmupEl.value || "0")));
171
+ return {
172
+ kind: "frame",
173
+ fps,
174
+ frames,
175
+ warmup,
176
+ canvasSelector,
177
+ ...exporterPayload(),
178
+ };
179
+ }
180
+
181
+ const duration = Math.max(1, Number(durationEl.value || "10"));
182
+ return {
183
+ kind: "live",
184
+ fps,
185
+ duration,
186
+ canvasSelector,
187
+ policy: policyEl.value,
188
+ concurrency: Math.max(1, Math.floor(Number(concurrencyEl.value || "2"))),
189
+ maxQueue: Math.max(0, Math.floor(Number(maxQueueEl.value || "8"))),
190
+ ...exporterPayload(),
191
+ };
192
+ }
193
+
194
+ function start() {
195
+ if (running) return;
196
+
197
+ const payload = currentPayload();
198
+ const entry = entryEl.value.trim() || "/src/main.js";
199
+
200
+ setRunning(true);
201
+ appendLog(
202
+ `start: ${payload.kind} fps=${payload.fps} canvas=${payload.canvasSelector}`
203
+ );
204
+
205
+ // start recording -> prefer passthrough + focus canvas
206
+ setPassthrough(true, { silent: true });
207
+
208
+ if (payload.kind === "frame") {
209
+ // Frame mode: reload preview with autostart so virtual-time hooks apply before sketch starts
210
+ preview.src = buildPreviewAutostartUrl(entry, payload);
211
+ return;
212
+ }
213
+
214
+ const ok = postToPreview("pa_start_frame_capture", payload);
215
+ if (!ok) {
216
+ setRunning(false);
217
+ appendLog("ERROR: preview iframe not ready");
218
+ return;
219
+ }
220
+
221
+ clearTimeout(stopTimer);
222
+ stopTimer = setTimeout(() => stop(), payload.duration * 1000);
223
+ }
224
+
225
+ function stop() {
226
+ if (!running) return;
227
+
228
+ clearTimeout(stopTimer);
229
+ stopTimer = null;
230
+
231
+ postToPreview("pa_stop", {});
232
+ appendLog("stop requested...");
233
+ }
234
+
235
+ /* UI events */
236
+ btnReload.addEventListener("click", () => reloadPreview());
237
+ btnStart.addEventListener("click", () => start());
238
+ btnStop.addEventListener("click", () => stop());
239
+
240
+ btnHide.addEventListener("click", () =>
241
+ document.body.classList.toggle("hidden")
242
+ );
243
+ btnCompact.addEventListener("click", () =>
244
+ document.body.classList.toggle("compact")
245
+ );
246
+ btnDock.addEventListener("click", () => cycleDock());
247
+
248
+ btnPassthrough.addEventListener("click", () => setPassthrough(!passthrough));
249
+ btnFocus.addEventListener("click", () => focusSketch());
250
+
251
+ modeEl.addEventListener("change", () => syncModeUI());
252
+
253
+ /**
254
+ * Hotkeys behavior:
255
+ * - Only active when NOT recording (running === false).
256
+ * - While recording, UI does not call preventDefault or consume keys.
257
+ * This minimizes interference with live keyboard interaction in the sketch.
258
+ */
259
+ window.addEventListener("keydown", (e) => {
260
+ if (running) return; // do not intercept any keys while recording
261
+
262
+ const active = document.activeElement?.tagName?.toLowerCase() || "";
263
+ const isTyping =
264
+ active === "input" || active === "textarea" || active === "select";
265
+ if (isTyping) return;
266
+
267
+ const k = (e.key || "").toLowerCase();
268
+
269
+ if (k === "h") {
270
+ e.preventDefault();
271
+ document.body.classList.toggle("hidden");
272
+ return;
273
+ }
274
+
275
+ if (k === "p") {
276
+ e.preventDefault();
277
+ setPassthrough(!passthrough);
278
+ return;
279
+ }
280
+
281
+ if (k === "c") {
282
+ e.preventDefault();
283
+ document.body.classList.toggle("compact");
284
+ return;
285
+ }
286
+
287
+ if (k === "d") {
288
+ e.preventDefault();
289
+ cycleDock();
290
+ return;
291
+ }
292
+
293
+ // Space starts recording when idle; during recording we do not bind Space to stop.
294
+ if (e.code === "Space") {
295
+ e.preventDefault();
296
+ start();
297
+ return;
298
+ }
299
+ });
300
+
301
+ /* Messages from preview */
302
+ window.addEventListener("message", (ev) => {
303
+ const msg = ev.data;
304
+ if (!msg || typeof msg !== "object") return;
305
+
306
+ if (msg.type === "pa_preview_ready") {
307
+ appendLog(`preview ready (entry=${msg.entry})`);
308
+ return;
309
+ }
310
+
311
+ if (msg.type === "pa_preview_canvas_list") {
312
+ const canvases = Array.isArray(msg.canvases) ? msg.canvases : [];
313
+ fillCanvasDropdown(canvases);
314
+ return;
315
+ }
316
+
317
+ if (msg.type === "pa_status") {
318
+ const status = msg.status || "";
319
+ const message = msg.message || "";
320
+ appendLog(message ? `${status}: ${message}` : String(status));
321
+
322
+ if (status === "idle") {
323
+ setRunning(false);
324
+ // optionally return UI control after recording
325
+ // (do not force passthrough off; user may want to keep interacting)
326
+ btnStart.focus?.();
327
+ }
328
+ if (status === "running") {
329
+ setRunning(true);
330
+ }
331
+ return;
332
+ }
333
+
334
+ if (msg.type === "pa_progress") {
335
+ const done = Number(msg.done ?? 0);
336
+ const total = Number(msg.total ?? 0);
337
+ appendLog(`progress: ${done}/${total}`);
338
+ return;
339
+ }
340
+
341
+ if (msg.type === "pa_stats") {
342
+ const safe = { ...msg };
343
+ delete safe.type;
344
+ appendLog(`stats: ${JSON.stringify(safe)}`);
345
+ return;
346
+ }
347
+
348
+ if (msg.type === "pa_error") {
349
+ appendLog(`ERROR: ${msg.message || "unknown error"}`);
350
+ setRunning(false);
351
+ }
352
+ });
353
+
354
+ /* init */
355
+ (function init() {
356
+ setDockByIndex(0);
357
+ setPassthrough(false, { silent: true });
358
+
359
+ const entry = qp("entry") || "/src/main.js";
360
+ entryEl.value = entry;
361
+
362
+ setRunning(false);
363
+ syncModeUI();
364
+
365
+ fillCanvasDropdown([]);
366
+ appendLog("loading preview...");
367
+ preview.src = buildPreviewUrl(entry);
368
+ })();
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>pa_encoder preview</title>
7
+ <style>
8
+ html,
9
+ body {
10
+ margin: 0;
11
+ padding: 0;
12
+ background: #111;
13
+ overflow: hidden;
14
+ }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <script type="module" src="/__pa_encoder__/ui/preview.js"></script>
19
+ </body>
20
+ </html>
@@ -0,0 +1,322 @@
1
+ import {
2
+ virtualTimeCaptureFromStart,
3
+ startLiveCapture,
4
+ createBestExporter,
5
+ createZipExporter,
6
+ createFsExporter,
7
+ } from "/__pa_encoder__/lib/index.js";
8
+
9
+ function getQueryParam(name) {
10
+ return new URL(location.href).searchParams.get(name);
11
+ }
12
+
13
+ function post(type, data = {}) {
14
+ try {
15
+ parent.postMessage({ type, ...data }, "*");
16
+ } catch {}
17
+ }
18
+
19
+ async function importEntry(entry) {
20
+ await import(entry);
21
+ }
22
+
23
+ function listCanvases() {
24
+ return Array.from(document.querySelectorAll("canvas")).filter(
25
+ (c) => c instanceof HTMLCanvasElement
26
+ );
27
+ }
28
+
29
+ function buildCanvasList() {
30
+ const cs = listCanvases();
31
+ return cs.map((c, i) => {
32
+ const selector = `canvas:nth-of-type(${i + 1})`;
33
+ return {
34
+ index: i,
35
+ selector,
36
+ id: c.id || "",
37
+ className: String(c.className || ""),
38
+ width: c.width,
39
+ height: c.height,
40
+ clientWidth: c.clientWidth,
41
+ clientHeight: c.clientHeight,
42
+ label: c.id ? `#${c.id}` : selector,
43
+ };
44
+ });
45
+ }
46
+
47
+ function sendCanvasList() {
48
+ const canvases = buildCanvasList();
49
+ post("pa_preview_canvas_list", { count: canvases.length, canvases });
50
+ }
51
+
52
+ function canvasToBlob(canvas, type = "image/png") {
53
+ return new Promise((resolve, reject) => {
54
+ canvas.toBlob((b) => {
55
+ if (b) resolve(b);
56
+ else reject(new Error("canvas.toBlob() returned null"));
57
+ }, type);
58
+ });
59
+ }
60
+
61
+ function ensureCanvasFocusable(canvas) {
62
+ try {
63
+ if (canvas && canvas.tabIndex < 0) canvas.tabIndex = 0;
64
+ } catch {}
65
+ try {
66
+ canvas.style.outline = "none";
67
+ } catch {}
68
+ }
69
+
70
+ function focusCanvasBestEffort(canvas) {
71
+ if (!(canvas instanceof HTMLCanvasElement)) return;
72
+ ensureCanvasFocusable(canvas);
73
+ try {
74
+ canvas.focus({ preventScroll: true });
75
+ } catch {
76
+ try {
77
+ canvas.focus();
78
+ } catch {}
79
+ }
80
+ }
81
+
82
+ async function createExporterFromPayload(p = {}) {
83
+ const mode = p.exporter?.mode || "best";
84
+ const prefer = p.exporter?.prefer || "fs";
85
+ const zipName = p.exporter?.zipName || "frames.zip";
86
+
87
+ if (mode === "zip") return await createZipExporter({ zipName });
88
+ if (mode === "fs") return await createFsExporter();
89
+ return await createBestExporter({ prefer, zip: { zipName } });
90
+ }
91
+
92
+ let running = false;
93
+ let stopFlag = false;
94
+
95
+ window.addEventListener("message", async (ev) => {
96
+ const msg = ev.data;
97
+ if (!msg || typeof msg !== "object") return;
98
+
99
+ if (msg.type === "pa_stop") {
100
+ stopFlag = true;
101
+ return;
102
+ }
103
+
104
+ if (msg.type === "pa_focus_canvas") {
105
+ const sel = msg.payload?.canvasSelector || "canvas";
106
+ const c = document.querySelector(sel) || document.querySelector("canvas");
107
+ if (c instanceof HTMLCanvasElement) focusCanvasBestEffort(c);
108
+ return;
109
+ }
110
+
111
+ if (msg.type !== "pa_start_frame_capture") return;
112
+ if (running) return;
113
+
114
+ // live는 message로 시작 가능
115
+ const p = msg.payload || {};
116
+ if ((p.kind || "live") === "frame") {
117
+ post("pa_error", {
118
+ message: "Frame mode requires iframe reload autostart.",
119
+ });
120
+ return;
121
+ }
122
+
123
+ await runLive(p);
124
+ });
125
+
126
+ async function runFrameAutostart(payload, entry) {
127
+ if (running) return;
128
+ running = true;
129
+ stopFlag = false;
130
+
131
+ const p = payload || {};
132
+ const canvasSelector = p.canvasSelector || "canvas";
133
+
134
+ post("pa_status", { status: "running", message: "capture starting..." });
135
+
136
+ let exporter = null;
137
+ let didFinalize = false;
138
+ let firstCanvasFocused = false;
139
+
140
+ try {
141
+ exporter = await createExporterFromPayload(p);
142
+
143
+ const fps = Number(p.fps ?? 60);
144
+ const frames = Number(p.frames ?? 300);
145
+ const warmup = Number(p.warmup ?? 0);
146
+ const totalSteps = warmup + frames;
147
+
148
+ let written = 0;
149
+ post("pa_progress", { done: 0, total: frames });
150
+
151
+ try {
152
+ await virtualTimeCaptureFromStart({
153
+ fps,
154
+ canvasSelector,
155
+ canvasWaitFrames: Number(p.canvasWaitFrames ?? 600),
156
+
157
+ hookDateNow: true,
158
+ hookPerformanceNow: true,
159
+ hookTimers: true,
160
+
161
+ frameCount: totalSteps,
162
+
163
+ start: async () => {
164
+ await importEntry(entry);
165
+ // after import, try focus if canvas already exists
166
+ const c =
167
+ document.querySelector(canvasSelector) ||
168
+ document.querySelector("canvas");
169
+ if (c instanceof HTMLCanvasElement) {
170
+ focusCanvasBestEffort(c);
171
+ firstCanvasFocused = true;
172
+ }
173
+ },
174
+
175
+ onFrame: async (canvas, i) => {
176
+ if (!firstCanvasFocused && canvas instanceof HTMLCanvasElement) {
177
+ focusCanvasBestEffort(canvas);
178
+ firstCanvasFocused = true;
179
+ }
180
+
181
+ if (stopFlag) throw new Error("stopped");
182
+ if (i < warmup) return;
183
+
184
+ const frameIndex = i - warmup;
185
+ const blob = await canvasToBlob(canvas, "image/png");
186
+ await exporter.write(frameIndex, blob);
187
+
188
+ written++;
189
+ post("pa_progress", { done: written, total: frames });
190
+ post("pa_stats", { written, captured: i + 1 });
191
+ },
192
+ });
193
+ } catch (e) {
194
+ if ((e?.message ?? "") !== "stopped") throw e;
195
+ post("pa_status", {
196
+ status: "running",
197
+ message: "finalizing (stopped)...",
198
+ });
199
+ }
200
+
201
+ if (exporter && !didFinalize) {
202
+ await exporter.finalize();
203
+ didFinalize = true;
204
+ }
205
+
206
+ post("pa_status", {
207
+ status: "idle",
208
+ message: stopFlag ? "stopped (finalized)" : "frame capture finished",
209
+ });
210
+ } catch (e) {
211
+ post("pa_error", { message: e?.message ?? String(e) });
212
+ post("pa_status", { status: "idle" });
213
+ } finally {
214
+ if (exporter && !didFinalize) {
215
+ try {
216
+ await exporter.finalize();
217
+ } catch {}
218
+ }
219
+ running = false;
220
+ stopFlag = false;
221
+ }
222
+ }
223
+
224
+ async function runLive(p) {
225
+ running = true;
226
+ stopFlag = false;
227
+
228
+ const canvasSelector = p.canvasSelector || "canvas";
229
+ post("pa_status", { status: "running", message: "capture starting..." });
230
+
231
+ let exporter = null;
232
+ let didFinalize = false;
233
+
234
+ try {
235
+ exporter = await createExporterFromPayload(p);
236
+
237
+ const canvas =
238
+ document.querySelector(canvasSelector) ||
239
+ document.querySelector("canvas");
240
+ if (!(canvas instanceof HTMLCanvasElement)) {
241
+ throw new Error(`Canvas not found: ${canvasSelector}`);
242
+ }
243
+
244
+ // Try to focus canvas so keyboard interaction goes to sketch
245
+ focusCanvasBestEffort(canvas);
246
+
247
+ const ac = new AbortController();
248
+ const stopWatcher = setInterval(() => {
249
+ if (stopFlag && !ac.signal.aborted) ac.abort(new Error("stopped"));
250
+ }, 50);
251
+
252
+ try {
253
+ const { stop } = await startLiveCapture({
254
+ canvas,
255
+ exporter,
256
+ fps: Number(p.fps ?? 30),
257
+ concurrency: Number(p.concurrency ?? 2),
258
+ maxQueue: Number(p.maxQueue ?? 8),
259
+ policy: p.policy ?? "drop",
260
+ signal: ac.signal,
261
+ onProgress: (stats) => post("pa_stats", stats),
262
+ });
263
+
264
+ post("pa_status", { status: "running", message: "capturing..." });
265
+
266
+ await new Promise((_, reject) => {
267
+ ac.signal.addEventListener("abort", () => reject(ac.signal.reason));
268
+ }).catch(async (e) => {
269
+ await stop();
270
+ didFinalize = true;
271
+ throw e;
272
+ });
273
+ } finally {
274
+ clearInterval(stopWatcher);
275
+ }
276
+ } catch (e) {
277
+ if ((e?.message ?? "") === "stopped") {
278
+ post("pa_status", { status: "idle", message: "stopped" });
279
+ } else {
280
+ post("pa_error", { message: e?.message ?? String(e) });
281
+ post("pa_status", { status: "idle" });
282
+ }
283
+ } finally {
284
+ if (exporter && !didFinalize) {
285
+ try {
286
+ await exporter.finalize();
287
+ } catch {}
288
+ }
289
+ running = false;
290
+ stopFlag = false;
291
+ }
292
+ }
293
+
294
+ (function init() {
295
+ const entry = getQueryParam("entry") || "/src/main.js";
296
+ post("pa_preview_ready", { entry });
297
+
298
+ sendCanvasList();
299
+ const mo = new MutationObserver(() => sendCanvasList());
300
+ mo.observe(document.documentElement, { childList: true, subtree: true });
301
+ setTimeout(sendCanvasList, 250);
302
+ setTimeout(sendCanvasList, 1000);
303
+
304
+ const autostart = getQueryParam("autostart") === "1";
305
+ const payloadB64 = getQueryParam("payload");
306
+
307
+ if (autostart && payloadB64) {
308
+ try {
309
+ const json = atob(payloadB64);
310
+ const payload = JSON.parse(json);
311
+ // IMPORTANT: do not import entry here; runFrameAutostart does it under virtual time
312
+ runFrameAutostart(payload, entry);
313
+ } catch (e) {
314
+ post("pa_error", { message: e?.message ?? String(e) });
315
+ }
316
+ } else {
317
+ importEntry(entry).catch((e) => {
318
+ console.error("[pa_encoder preview] entry import failed:", entry, e);
319
+ post("pa_error", { message: e?.message ?? String(e) });
320
+ });
321
+ }
322
+ })();