@veolab/discoverylab 1.3.1 → 1.3.3

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/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- APP_VERSION
4
- } from "./chunk-6EGBXRDK.js";
5
2
  import {
6
3
  buildAppLabNetworkProfile
7
4
  } from "./chunk-LB3RNE3O.js";
5
+ import {
6
+ APP_VERSION
7
+ } from "./chunk-6EGBXRDK.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
 
10
10
  // src/cli.ts
@@ -57,7 +57,7 @@ async function withESVPCli(action) {
57
57
  }
58
58
  }
59
59
  async function getESVPBaseResult(serverUrl) {
60
- const { getESVPConnection } = await import("./esvp-GSISVXLC.js");
60
+ const { getESVPConnection } = await import("./esvp-KVOWYW6G.js");
61
61
  const connection = await getESVPConnection(serverUrl);
62
62
  return {
63
63
  serverUrl: connection.serverUrl,
@@ -72,7 +72,7 @@ function executorToPlatform(executor) {
72
72
  var esvp = program.command("esvp").description("Access the public ESVP protocol and runtime from the CLI");
73
73
  esvp.command("status").description("Check the configured ESVP server health").option("-s, --server <url>", "ESVP base URL").action(async (options) => {
74
74
  await withESVPCli(async () => {
75
- const { getESVPHealth } = await import("./esvp-GSISVXLC.js");
75
+ const { getESVPHealth } = await import("./esvp-KVOWYW6G.js");
76
76
  return {
77
77
  ...await getESVPBaseResult(options.server),
78
78
  health: await getESVPHealth(options.server)
@@ -81,7 +81,7 @@ esvp.command("status").description("Check the configured ESVP server health").op
81
81
  });
82
82
  esvp.command("devices").description("List ESVP-visible devices").option("-s, --server <url>", "ESVP base URL").option("-p, --platform <platform>", "adb | ios-sim | maestro-ios | all", "all").action(async (options) => {
83
83
  await withESVPCli(async () => {
84
- const { listESVPDevices } = await import("./esvp-GSISVXLC.js");
84
+ const { listESVPDevices } = await import("./esvp-KVOWYW6G.js");
85
85
  return {
86
86
  ...await getESVPBaseResult(options.server),
87
87
  devices: await listESVPDevices(options.platform, options.server)
@@ -90,7 +90,7 @@ esvp.command("devices").description("List ESVP-visible devices").option("-s, --s
90
90
  });
91
91
  esvp.command("sessions").description("List public ESVP sessions").option("-s, --server <url>", "ESVP base URL").action(async (options) => {
92
92
  await withESVPCli(async () => {
93
- const { listESVPSessions } = await import("./esvp-GSISVXLC.js");
93
+ const { listESVPSessions } = await import("./esvp-KVOWYW6G.js");
94
94
  return {
95
95
  ...await getESVPBaseResult(options.server),
96
96
  ...await listESVPSessions(options.server)
@@ -99,7 +99,7 @@ esvp.command("sessions").description("List public ESVP sessions").option("-s, --
99
99
  });
100
100
  esvp.command("create").description("Create a new ESVP session").requiredOption("-e, --executor <executor>", "fake | adb | ios-sim | maestro-ios").option("-s, --server <url>", "ESVP base URL").option("-d, --device-id <id>", "Device or simulator ID").option("--meta-json <json>", "Session metadata as JSON").option("--meta-file <path>", "Path to session metadata JSON").option("--crash-clip-json <json>", "Crash clip config as JSON").option("--crash-clip-file <path>", "Path to crash clip config JSON").option("--with-network", "Auto-configure the default App Lab external-proxy profile after creating the session").action(async (options) => {
101
101
  await withESVPCli(async () => {
102
- const { createESVPSession, configureESVPNetwork } = await import("./esvp-GSISVXLC.js");
102
+ const { createESVPSession, configureESVPNetwork } = await import("./esvp-KVOWYW6G.js");
103
103
  const meta = await readJsonSource(options.metaJson, options.metaFile, "meta");
104
104
  const crashClip = await readJsonSource(options.crashClipJson, options.crashClipFile, "crash clip");
105
105
  const createResult = await createESVPSession(
@@ -142,7 +142,7 @@ esvp.command("create").description("Create a new ESVP session").requiredOption("
142
142
  });
143
143
  esvp.command("get <sessionId>").description("Get a public ESVP session summary").option("-s, --server <url>", "ESVP base URL").action(async (sessionId, options) => {
144
144
  await withESVPCli(async () => {
145
- const { getESVPSession } = await import("./esvp-GSISVXLC.js");
145
+ const { getESVPSession } = await import("./esvp-KVOWYW6G.js");
146
146
  return {
147
147
  ...await getESVPBaseResult(options.server),
148
148
  ...await getESVPSession(sessionId, options.server)
@@ -151,7 +151,7 @@ esvp.command("get <sessionId>").description("Get a public ESVP session summary")
151
151
  });
152
152
  esvp.command("inspect <sessionId>").description("Inspect a session and optionally load transcript and artifacts").option("-s, --server <url>", "ESVP base URL").option("--transcript", "Include transcript").option("--artifacts", "Include artifacts").action(async (sessionId, options) => {
153
153
  await withESVPCli(async () => {
154
- const { inspectESVPSession } = await import("./esvp-GSISVXLC.js");
154
+ const { inspectESVPSession } = await import("./esvp-KVOWYW6G.js");
155
155
  return {
156
156
  ...await getESVPBaseResult(options.server),
157
157
  ...await inspectESVPSession(
@@ -167,7 +167,7 @@ esvp.command("inspect <sessionId>").description("Inspect a session and optionall
167
167
  });
168
168
  esvp.command("transcript <sessionId>").description("Fetch the canonical session transcript").option("-s, --server <url>", "ESVP base URL").action(async (sessionId, options) => {
169
169
  await withESVPCli(async () => {
170
- const { getESVPTranscript } = await import("./esvp-GSISVXLC.js");
170
+ const { getESVPTranscript } = await import("./esvp-KVOWYW6G.js");
171
171
  return {
172
172
  ...await getESVPBaseResult(options.server),
173
173
  sessionId,
@@ -177,7 +177,7 @@ esvp.command("transcript <sessionId>").description("Fetch the canonical session
177
177
  });
178
178
  esvp.command("artifacts <sessionId>").description("List session artifacts").option("-s, --server <url>", "ESVP base URL").action(async (sessionId, options) => {
179
179
  await withESVPCli(async () => {
180
- const { listESVPArtifacts } = await import("./esvp-GSISVXLC.js");
180
+ const { listESVPArtifacts } = await import("./esvp-KVOWYW6G.js");
181
181
  return {
182
182
  ...await getESVPBaseResult(options.server),
183
183
  sessionId,
@@ -187,13 +187,13 @@ esvp.command("artifacts <sessionId>").description("List session artifacts").opti
187
187
  });
188
188
  esvp.command("artifact <sessionId> <artifactPath>").description("Read a public artifact payload").option("-s, --server <url>", "ESVP base URL").action(async (sessionId, artifactPath, options) => {
189
189
  await withESVPCli(async () => {
190
- const { getESVPArtifactContent } = await import("./esvp-GSISVXLC.js");
190
+ const { getESVPArtifactContent } = await import("./esvp-KVOWYW6G.js");
191
191
  return await getESVPArtifactContent(sessionId, artifactPath, options.server);
192
192
  });
193
193
  });
194
194
  esvp.command("actions <sessionId>").description("Run public ESVP actions inside a session").option("-s, --server <url>", "ESVP base URL").option("--actions-json <json>", "JSON array of ESVP actions").option("--actions-file <path>", "Path to a JSON file with ESVP actions").option("--finish", "Finish the session after actions").option("--capture-logcat", "Capture logcat on finish when supported").option("--checkpoint-after-each", "Set checkpointAfter on every action").option("--with-network", "Auto-configure the default App Lab external-proxy profile before running actions if not already configured").action(async (sessionId, options) => {
195
195
  await withESVPCli(async () => {
196
- const { runESVPActions, getESVPSessionNetwork, configureESVPNetwork } = await import("./esvp-GSISVXLC.js");
196
+ const { runESVPActions, getESVPSessionNetwork, configureESVPNetwork } = await import("./esvp-KVOWYW6G.js");
197
197
  const actions = await readJsonSource(options.actionsJson, options.actionsFile, "actions");
198
198
  if (!Array.isArray(actions) || actions.length === 0) {
199
199
  failCli("Provide --actions-json or --actions-file with a non-empty array of ESVP actions.");
@@ -239,7 +239,7 @@ esvp.command("actions <sessionId>").description("Run public ESVP actions inside
239
239
  });
240
240
  esvp.command("checkpoint <sessionId>").description("Capture an ESVP checkpoint").option("-s, --server <url>", "ESVP base URL").option("-l, --label <label>", "Checkpoint label").action(async (sessionId, options) => {
241
241
  await withESVPCli(async () => {
242
- const { captureESVPCheckpoint } = await import("./esvp-GSISVXLC.js");
242
+ const { captureESVPCheckpoint } = await import("./esvp-KVOWYW6G.js");
243
243
  return {
244
244
  ...await getESVPBaseResult(options.server),
245
245
  ...await captureESVPCheckpoint(
@@ -254,7 +254,7 @@ esvp.command("checkpoint <sessionId>").description("Capture an ESVP checkpoint")
254
254
  });
255
255
  esvp.command("finish <sessionId>").description("Finish an ESVP session").option("-s, --server <url>", "ESVP base URL").option("--capture-logcat", "Capture logcat on finish when supported").action(async (sessionId, options) => {
256
256
  await withESVPCli(async () => {
257
- const { finishESVPSession } = await import("./esvp-GSISVXLC.js");
257
+ const { finishESVPSession } = await import("./esvp-KVOWYW6G.js");
258
258
  return {
259
259
  ...await getESVPBaseResult(options.server),
260
260
  ...await finishESVPSession(
@@ -269,7 +269,7 @@ esvp.command("finish <sessionId>").description("Finish an ESVP session").option(
269
269
  });
270
270
  esvp.command("preflight <sessionId>").description("Run preflight/bootstrap rules on an ESVP session").option("-s, --server <url>", "ESVP base URL").option("--policy <policy>", "Preflight policy name (e.g. fresh_install)").option("--app-id <appId>", "Target app ID").option("--json <json>", "Preflight config as JSON string").option("--file <path>", "Path to preflight config JSON file").action(async (sessionId, options) => {
271
271
  await withESVPCli(async () => {
272
- const { runESVPPreflight } = await import("./esvp-GSISVXLC.js");
272
+ const { runESVPPreflight } = await import("./esvp-KVOWYW6G.js");
273
273
  const fromSource = await readJsonSource(options.json, options.file, "preflight config");
274
274
  const config = {
275
275
  ...typeof fromSource === "object" && fromSource ? fromSource : {},
@@ -284,7 +284,7 @@ esvp.command("preflight <sessionId>").description("Run preflight/bootstrap rules
284
284
  });
285
285
  esvp.command("replay-run <sessionId>").description("Replay a session to a new ESVP session").option("-s, --server <url>", "ESVP base URL").option("-e, --executor <executor>", "fake | adb | ios-sim | maestro-ios").option("-d, --device-id <id>", "Replay target device ID").option("--capture-logcat", "Capture logcat on finish when supported").option("--meta-json <json>", "Replay metadata as JSON").option("--meta-file <path>", "Path to replay metadata JSON").action(async (sessionId, options) => {
286
286
  await withESVPCli(async () => {
287
- const { replayESVPSession, getESVPReplayConsistency } = await import("./esvp-GSISVXLC.js");
287
+ const { replayESVPSession, getESVPReplayConsistency } = await import("./esvp-KVOWYW6G.js");
288
288
  const meta = await readJsonSource(options.metaJson, options.metaFile, "replay meta");
289
289
  const replay = await replayESVPSession(
290
290
  sessionId,
@@ -307,7 +307,7 @@ esvp.command("replay-run <sessionId>").description("Replay a session to a new ES
307
307
  });
308
308
  esvp.command("replay-validate <sessionId>").description("Validate whether a session supports public replay").option("-s, --server <url>", "ESVP base URL").action(async (sessionId, options) => {
309
309
  await withESVPCli(async () => {
310
- const { validateESVPReplay } = await import("./esvp-GSISVXLC.js");
310
+ const { validateESVPReplay } = await import("./esvp-KVOWYW6G.js");
311
311
  return {
312
312
  ...await getESVPBaseResult(options.server),
313
313
  ...await validateESVPReplay(sessionId, options.server)
@@ -316,7 +316,7 @@ esvp.command("replay-validate <sessionId>").description("Validate whether a sess
316
316
  });
317
317
  esvp.command("replay-consistency <sessionId>").description("Inspect replay consistency for a replay session").option("-s, --server <url>", "ESVP base URL").action(async (sessionId, options) => {
318
318
  await withESVPCli(async () => {
319
- const { getESVPReplayConsistency } = await import("./esvp-GSISVXLC.js");
319
+ const { getESVPReplayConsistency } = await import("./esvp-KVOWYW6G.js");
320
320
  return {
321
321
  ...await getESVPBaseResult(options.server),
322
322
  ...await getESVPReplayConsistency(sessionId, options.server)
@@ -325,7 +325,7 @@ esvp.command("replay-consistency <sessionId>").description("Inspect replay consi
325
325
  });
326
326
  esvp.command("network <sessionId>").description("Read the public network state for a session").option("-s, --server <url>", "ESVP base URL").action(async (sessionId, options) => {
327
327
  await withESVPCli(async () => {
328
- const { getESVPSessionNetwork } = await import("./esvp-GSISVXLC.js");
328
+ const { getESVPSessionNetwork } = await import("./esvp-KVOWYW6G.js");
329
329
  return {
330
330
  ...await getESVPBaseResult(options.server),
331
331
  ...await getESVPSessionNetwork(sessionId, options.server)
@@ -334,7 +334,7 @@ esvp.command("network <sessionId>").description("Read the public network state f
334
334
  });
335
335
  esvp.command("network-configure <sessionId>").description("Apply a public ESVP network profile").option("-s, --server <url>", "ESVP base URL").option("--json <json>", "Raw network profile JSON").option("--file <path>", "Path to network profile JSON").option("--profile <name>", "Profile name").option("--label <label>", "Profile label").option("--connectivity <state>", "online | offline | reset").action(async (sessionId, options) => {
336
336
  await withESVPCli(async () => {
337
- const { configureESVPNetwork } = await import("./esvp-GSISVXLC.js");
337
+ const { configureESVPNetwork } = await import("./esvp-KVOWYW6G.js");
338
338
  const payload = await readJsonSource(options.json, options.file, "network profile") || {};
339
339
  const merged = {
340
340
  ...typeof payload === "object" && payload ? payload : {},
@@ -350,7 +350,7 @@ esvp.command("network-configure <sessionId>").description("Apply a public ESVP n
350
350
  });
351
351
  esvp.command("network-clear <sessionId>").description("Clear the active ESVP network profile").option("-s, --server <url>", "ESVP base URL").action(async (sessionId, options) => {
352
352
  await withESVPCli(async () => {
353
- const { clearESVPNetwork } = await import("./esvp-GSISVXLC.js");
353
+ const { clearESVPNetwork } = await import("./esvp-KVOWYW6G.js");
354
354
  return {
355
355
  ...await getESVPBaseResult(options.server),
356
356
  ...await clearESVPNetwork(sessionId, options.server)
@@ -359,7 +359,7 @@ esvp.command("network-clear <sessionId>").description("Clear the active ESVP net
359
359
  });
360
360
  esvp.command("trace-attach <sessionId>").description("Attach a public network trace artifact to a session").requiredOption("--trace-kind <kind>", "Trace kind, e.g. http_trace or har").option("-s, --server <url>", "ESVP base URL").option("--json <json>", "Trace payload JSON").option("--file <path>", "Path to trace payload JSON/text").option("--label <label>", "Trace label").option("--source <source>", "Trace source").option("--request-id <id>", "Correlated request ID").option("--method <method>", "HTTP method").option("--url <url>", "Request URL").option("--status-code <code>", "HTTP status code").option("--format <format>", "Payload format label").action(async (sessionId, options) => {
361
361
  await withESVPCli(async () => {
362
- const { attachESVPNetworkTrace } = await import("./esvp-GSISVXLC.js");
362
+ const { attachESVPNetworkTrace } = await import("./esvp-KVOWYW6G.js");
363
363
  const payload = await readJsonSource(options.json, options.file, "trace payload");
364
364
  if (payload == null) {
365
365
  failCli("Provide --json or --file with the trace payload to attach.");
@@ -389,7 +389,7 @@ program.command("serve").alias("server").description("Start the DiscoveryLab web
389
389
  console.log(chalk.cyan("\n DiscoveryLab"));
390
390
  console.log(chalk.gray(" AI-powered app testing & evidence generator\n"));
391
391
  try {
392
- const { startServer } = await import("./server-FO3UVUZU.js");
392
+ const { startServer } = await import("./server-T5X6GGOO.js");
393
393
  await startServer(port);
394
394
  console.log(chalk.green(` Server running at http://localhost:${port}`));
395
395
  console.log(chalk.gray(" Press Ctrl+C to stop\n"));
@@ -501,7 +501,7 @@ program.command("mcp").description("Run as MCP server (for Claude Code integrati
501
501
  integrationTools,
502
502
  taskHubTools,
503
503
  esvpTools
504
- } = await import("./tools-OCRMOQ4U.js");
504
+ } = await import("./tools-YGM5HRIB.js");
505
505
  mcpServer.registerTools([
506
506
  ...uiTools,
507
507
  ...projectTools,
@@ -22,7 +22,8 @@ import {
22
22
  runESVPActions,
23
23
  runESVPPreflight,
24
24
  validateESVPReplay
25
- } from "./chunk-GAKEFJ5T.js";
25
+ } from "./chunk-7EDIUVIO.js";
26
+ import "./chunk-6EGBXRDK.js";
26
27
  import "./chunk-VVIOB362.js";
27
28
  import "./chunk-MLKGABMK.js";
28
29
  export {
@@ -4,9 +4,10 @@ import {
4
4
  diagnoseESVPNetworkState,
5
5
  translateMaestroActionsToESVP,
6
6
  validateMaestroRecordingWithESVP
7
- } from "./chunk-N6JJ2RGV.js";
7
+ } from "./chunk-ZLHIHMSL.js";
8
8
  import "./chunk-LB3RNE3O.js";
9
- import "./chunk-GAKEFJ5T.js";
9
+ import "./chunk-7EDIUVIO.js";
10
+ import "./chunk-6EGBXRDK.js";
10
11
  import "./chunk-SLNJEF32.js";
11
12
  import "./chunk-VVIOB362.js";
12
13
  import "./chunk-XFVDP332.js";
package/dist/index.d.ts CHANGED
@@ -1601,13 +1601,9 @@ interface ESVPResolvedConnection {
1601
1601
  /**
1602
1602
  * ESVP Public Client
1603
1603
  *
1604
- * Thin HTTP adapter for the public ESVP contract.
1605
- * This client intentionally depends only on the open contract and does not
1606
- * embed any private Entropy Lab implementation details.
1607
- *
1608
- * Connection modes:
1609
- * - remote: explicit serverUrl or ESVP_BASE_URL
1610
- * - local: embedded OSS runtime via @entropylab/esvp-local or DISCOVERYLAB_ESVP_LOCAL_MODULE
1604
+ * Thin adapter for the public ESVP contract, backed by the in-process
1605
+ * App Lab local runtime. App Lab no longer routes ESVP traffic to an external
1606
+ * localhost sidecar or remote control-plane.
1611
1607
  */
1612
1608
 
1613
1609
  type ESVPExecutor = 'fake' | 'adb' | 'ios-sim' | 'maestro-ios';
@@ -1664,6 +1660,16 @@ interface ESVPAction {
1664
1660
  checkpointAfter?: boolean;
1665
1661
  checkpointLabel?: string;
1666
1662
  }
1663
+ interface ESVPPreflightRule {
1664
+ kind: 'permission' | 'dismiss_dialog' | 'wait_for_stable' | 'clear_data' | 'set_setting';
1665
+ [key: string]: unknown;
1666
+ }
1667
+ interface ESVPPreflightConfig {
1668
+ policy?: string;
1669
+ appId?: string;
1670
+ rules?: ESVPPreflightRule[];
1671
+ [key: string]: unknown;
1672
+ }
1667
1673
  declare function getESVPBaseUrl(serverUrl?: string): string;
1668
1674
  declare function resolveESVPBaseUrl(serverUrl?: string): Promise<string>;
1669
1675
  declare function getESVPConnection(serverUrl?: string): Promise<ESVPResolvedConnection>;
@@ -1682,16 +1688,6 @@ declare function runESVPActions(sessionId: string, input: {
1682
1688
  declare function finishESVPSession(sessionId: string, input?: {
1683
1689
  captureLogcat?: boolean;
1684
1690
  }, serverUrl?: string): Promise<any>;
1685
- interface ESVPPreflightRule {
1686
- kind: 'permission' | 'dismiss_dialog' | 'wait_for_stable' | 'clear_data' | 'set_setting';
1687
- [key: string]: unknown;
1688
- }
1689
- interface ESVPPreflightConfig {
1690
- policy?: string;
1691
- appId?: string;
1692
- rules?: ESVPPreflightRule[];
1693
- [key: string]: unknown;
1694
- }
1695
1691
  declare function runESVPPreflight(sessionId: string, config: ESVPPreflightConfig, serverUrl?: string): Promise<any>;
1696
1692
  declare function inspectESVPSession(sessionId: string, input?: {
1697
1693
  includeTranscript?: boolean;
package/dist/index.html CHANGED
@@ -5602,6 +5602,12 @@
5602
5602
  width: fit-content;
5603
5603
  }
5604
5604
 
5605
+ .template-restriction-note {
5606
+ margin-bottom: 12px;
5607
+ font-size: 12px;
5608
+ color: var(--text-muted);
5609
+ }
5610
+
5605
5611
  .template-pill {
5606
5612
  position: relative;
5607
5613
  display: inline-flex;
@@ -11616,6 +11622,7 @@
11616
11622
  <span class="label">View:</span>
11617
11623
  <button class="template-pill active" data-template="raw" onclick="switchTemplate('raw')">Raw</button>
11618
11624
  </div>
11625
+ <div class="template-restriction-note" id="templateRestrictionNote" style="display:none;"></div>
11619
11626
 
11620
11627
  <!-- Raw video (default) -->
11621
11628
  <div class="video-player-container" id="rawVideoContainer">
@@ -11795,7 +11802,7 @@
11795
11802
  let templateStatus = null;
11796
11803
  let activeTemplate = 'raw';
11797
11804
  let favoriteTemplate = null;
11798
- let currentTemplateProps = null; // { title, titleLines, terminalTabs, hasNetworkData }
11805
+ let currentTemplateProps = null; // per-project template props + eligibility + mockup options
11799
11806
 
11800
11807
  // Space = play/pause active video
11801
11808
  document.addEventListener('keydown', (e) => {
@@ -11814,6 +11821,7 @@
11814
11821
 
11815
11822
  async function initTemplateUI() {
11816
11823
  try {
11824
+ currentTemplateProps = null;
11817
11825
  const [statusRes, prefRes] = await Promise.all([
11818
11826
  fetch('/api/templates/status'),
11819
11827
  fetch('/api/settings/template-preference'),
@@ -11827,37 +11835,93 @@
11827
11835
  const bar = document.getElementById('templateToggleBar');
11828
11836
  if (!bar) return;
11829
11837
 
11830
- // Build icon buttons for each template
11831
- const templateIcons = {
11832
- raw: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>',
11833
- studio: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>',
11834
- showcase: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
11835
- };
11836
- let pills = '';
11837
- pills += `<button class="template-pill active" data-template="raw" title="Raw video" tabindex="-1" onclick="switchTemplate('raw')">${templateIcons.raw}</button>`;
11838
+ await loadTemplateProps();
11839
+ renderTemplatePills();
11840
+ applyTemplateAvailabilityState();
11841
+
11842
+ if (favoriteTemplate && currentTemplateProps?.templatesAllowed !== false) {
11843
+ switchTemplate(favoriteTemplate);
11844
+ }
11845
+ } catch (e) {
11846
+ console.log('[Templates] Init error:', e);
11847
+ }
11848
+ }
11849
+
11850
+ function getTemplateIcons() {
11851
+ return {
11852
+ raw: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>',
11853
+ studio: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>',
11854
+ showcase: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
11855
+ };
11856
+ }
11857
+
11858
+ function renderTemplatePills() {
11859
+ const bar = document.getElementById('templateToggleBar');
11860
+ if (!bar || !templateStatus?.available) return;
11861
+
11862
+ const allowTemplates = currentTemplateProps?.templatesAllowed !== false;
11863
+ const templateIcons = getTemplateIcons();
11864
+ let pills = '';
11865
+ pills += `<span class="label">View:</span>`;
11866
+ pills += `<button class="template-pill ${activeTemplate === 'raw' ? 'active' : ''}" data-template="raw" title="Raw video" tabindex="-1" onclick="switchTemplate('raw')">${templateIcons.raw}</button>`;
11867
+ if (allowTemplates) {
11838
11868
  for (const t of templateStatus.templates) {
11839
11869
  const isFav = favoriteTemplate === t.id;
11840
11870
  const icon = templateIcons[t.id] || templateIcons.studio;
11841
- pills += `<button class="template-pill" data-template="${t.id}" title="${t.name}" tabindex="-1" onclick="switchTemplate('${t.id}')" ondblclick="event.stopPropagation(); toggleFavoriteTemplate('${t.id}')">
11871
+ pills += `<button class="template-pill ${activeTemplate === t.id ? 'active' : ''}" data-template="${t.id}" title="${escapeHtml(t.name)}" tabindex="-1" onclick="switchTemplate('${t.id}')" ondblclick="event.stopPropagation(); toggleFavoriteTemplate('${t.id}')">
11842
11872
  ${icon}
11843
11873
  <span class="star-dot ${isFav ? 'visible' : ''}"></span>
11844
11874
  </button>`;
11845
11875
  }
11846
- pills += `<button class="template-pill template-edit-btn" title="Edit template text" tabindex="-1" onclick="openTemplateEditModal()" style="display:none;">
11876
+ pills += `<button class="template-pill template-edit-btn" title="Edit template text" tabindex="-1" onclick="openTemplateEditModal()" style="${activeTemplate === 'raw' ? 'display:none;' : ''}">
11847
11877
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
11848
11878
  </button>`;
11849
- bar.innerHTML = pills;
11850
- bar.style.display = 'flex';
11879
+ }
11880
+ bar.innerHTML = pills;
11881
+ bar.style.display = 'flex';
11882
+ updateTemplateRestrictionNote();
11883
+ }
11851
11884
 
11852
- // Load template props for terminal content editor
11853
- loadTemplateProps();
11885
+ function updateTemplateRestrictionNote() {
11886
+ const note = document.getElementById('templateRestrictionNote');
11887
+ if (!note) return;
11888
+ const blocked = currentTemplateProps?.templatesAllowed === false;
11889
+ if (!blocked) {
11890
+ note.style.display = 'none';
11891
+ note.textContent = '';
11892
+ return;
11893
+ }
11894
+ note.textContent = currentTemplateProps?.templateRestrictionReason
11895
+ || `Templates are limited to videos up to ${currentTemplateProps?.maxTemplateDurationSeconds || 60} seconds.`;
11896
+ note.style.display = '';
11897
+ }
11854
11898
 
11855
- // Auto-apply favorite template if set
11856
- if (favoriteTemplate) {
11857
- switchTemplate(favoriteTemplate);
11858
- }
11859
- } catch (e) {
11860
- console.log('[Templates] Init error:', e);
11899
+ function setRawTemplateView() {
11900
+ activeTemplate = 'raw';
11901
+ const rawContainer = document.getElementById('rawVideoContainer');
11902
+ const templateContainer = document.getElementById('templatePreviewContainer');
11903
+ const templateVideo = document.getElementById('templateVideo');
11904
+ const downloadBtn = document.getElementById('downloadWithTemplateBtn');
11905
+ const editBtn = document.querySelector('.template-edit-btn');
11906
+ const skeleton = document.getElementById('templateSkeleton');
11907
+
11908
+ if (rawContainer) rawContainer.style.display = '';
11909
+ if (templateContainer) templateContainer.style.display = 'none';
11910
+ if (templateVideo) templateVideo.removeAttribute('src');
11911
+ if (downloadBtn) downloadBtn.style.display = 'none';
11912
+ if (editBtn) editBtn.style.display = 'none';
11913
+ if (skeleton) skeleton.style.display = 'none';
11914
+
11915
+ document.querySelectorAll('.template-pill').forEach(pill => {
11916
+ if (pill.classList.contains('template-edit-btn')) return;
11917
+ pill.classList.toggle('active', pill.dataset.template === 'raw');
11918
+ });
11919
+ }
11920
+
11921
+ function applyTemplateAvailabilityState() {
11922
+ renderTemplatePills();
11923
+ if (currentTemplateProps?.templatesAllowed === false) {
11924
+ setRawTemplateView();
11861
11925
  }
11862
11926
  }
11863
11927
 
@@ -11876,8 +11940,20 @@
11876
11940
  titleLines: data.props.titleLines || undefined,
11877
11941
  terminalTabs: data.props.terminalTabs || [],
11878
11942
  hasNetworkData: data.props.hasNetworkData || false,
11943
+ platform: data.props.platform || 'web',
11879
11944
  showcaseMode: data.props.showcaseMode || undefined,
11945
+ deviceMockup: data.props.deviceMockup || undefined,
11946
+ availableDeviceMockups: data.androidDeviceMockups || [],
11947
+ templatesAllowed: data.eligibility?.templatesAllowed !== false,
11948
+ templateRestrictionReason: data.eligibility?.reason || '',
11949
+ actualDurationSeconds: data.eligibility?.actualDurationSeconds || data.props.videoDuration || 0,
11950
+ maxTemplateDurationSeconds: data.eligibility?.maxTemplateDurationSeconds || 60,
11880
11951
  };
11952
+ applyTemplateAvailabilityState();
11953
+ } else {
11954
+ currentTemplateProps = null;
11955
+ renderTemplatePills();
11956
+ updateTemplateRestrictionNote();
11881
11957
  }
11882
11958
  } catch (e) {
11883
11959
  console.log('[Templates] Props load error:', e);
@@ -11887,6 +11963,11 @@
11887
11963
  async function switchTemplate(templateId) {
11888
11964
  // Skip if already on this template
11889
11965
  if (templateId === activeTemplate) return;
11966
+ if (templateId !== 'raw' && currentTemplateProps?.templatesAllowed === false) {
11967
+ showToast(currentTemplateProps.templateRestrictionReason || 'Templates are disabled for long recordings', 'info');
11968
+ setRawTemplateView();
11969
+ return;
11970
+ }
11890
11971
  activeTemplate = templateId;
11891
11972
  const rawContainer = document.getElementById('rawVideoContainer');
11892
11973
  const templateContainer = document.getElementById('templatePreviewContainer');
@@ -11900,10 +11981,7 @@
11900
11981
  });
11901
11982
 
11902
11983
  if (templateId === 'raw') {
11903
- if (rawContainer) rawContainer.style.display = '';
11904
- if (templateContainer) templateContainer.style.display = 'none';
11905
- if (downloadBtn) downloadBtn.style.display = 'none';
11906
- if (editBtn) editBtn.style.display = 'none';
11984
+ setRawTemplateView();
11907
11985
  return;
11908
11986
  }
11909
11987
 
@@ -12008,6 +12086,10 @@
12008
12086
  showToast('No template data loaded', 'info');
12009
12087
  return;
12010
12088
  }
12089
+ if (currentTemplateProps.templatesAllowed === false) {
12090
+ showToast(currentTemplateProps.templateRestrictionReason || 'Templates are disabled for this recording', 'info');
12091
+ return;
12092
+ }
12011
12093
  const existing = document.getElementById('templateEditModal');
12012
12094
  if (existing) existing.remove();
12013
12095
 
@@ -12027,7 +12109,9 @@
12027
12109
  #templateEditModal .te-field-full { grid-column: 1 / -1; }
12028
12110
  #templateEditModal .te-field label { display: block; font-size: 11px; font-weight: 500; color: var(--text-secondary); margin-bottom: 3px; }
12029
12111
  #templateEditModal .te-field input[type="text"] { width: 100%; padding: 7px 10px; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 12px; box-sizing: border-box; }
12112
+ #templateEditModal .te-field select { width: 100%; padding: 7px 10px; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary); font-size: 12px; box-sizing: border-box; }
12030
12113
  #templateEditModal .te-field input:focus { outline: none; border-color: var(--accent); }
12114
+ #templateEditModal .te-field select:focus { outline: none; border-color: var(--accent); }
12031
12115
  #templateEditModal .te-btn-row { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
12032
12116
  #templateEditModal .te-tab-row { border: 1px solid var(--border); border-radius: 6px; margin-bottom: 6px; overflow: hidden; }
12033
12117
  #templateEditModal .te-tab-header { display: flex; align-items: center; gap: 6px; padding: 6px 8px; cursor: pointer; background: var(--bg-elevated); }
@@ -12118,7 +12202,7 @@
12118
12202
  lines.push(words.slice(i, i + 2).join(' '));
12119
12203
  }
12120
12204
  }
12121
- fieldsArea.innerHTML = lines.map((line, i) => `
12205
+ fieldsArea.innerHTML = buildAndroidMockupFieldHtml(props) + lines.map((line, i) => `
12122
12206
  <div class="te-field te-field-full">
12123
12207
  <label>Line ${i + 1} <span style="opacity:0.5">(${fontLabels[i % fontLabels.length]})</span></label>
12124
12208
  <input type="text" class="te-title-line" data-index="${i}" value="${(line || '').replace(/"/g, '&quot;')}" autocomplete="off">
@@ -12131,6 +12215,22 @@
12131
12215
  }
12132
12216
  }
12133
12217
 
12218
+ function buildAndroidMockupFieldHtml(props) {
12219
+ if (props.platform !== 'android' || !Array.isArray(props.availableDeviceMockups) || props.availableDeviceMockups.length === 0) {
12220
+ return '';
12221
+ }
12222
+ const selected = props.deviceMockup || props.availableDeviceMockups[0]?.id || '';
12223
+ const options = props.availableDeviceMockups.map((option) => `
12224
+ <option value="${escapeHtml(option.id)}" ${option.id === selected ? 'selected' : ''}>${escapeHtml(option.label || option.id)}</option>
12225
+ `).join('');
12226
+ return `
12227
+ <div class="te-field te-field-full">
12228
+ <label>Android Mockup</label>
12229
+ <select id="teDeviceMockup">${options}</select>
12230
+ </div>
12231
+ `;
12232
+ }
12233
+
12134
12234
  function buildTerminalEditorHtml(props, existingTabs) {
12135
12235
  const tabRows = existingTabs.map((tab, i) => `
12136
12236
  <div class="te-tab-row">
@@ -12149,6 +12249,7 @@
12149
12249
  `).join('');
12150
12250
 
12151
12251
  return `
12252
+ ${buildAndroidMockupFieldHtml(props)}
12152
12253
  <div class="te-field te-field-full">
12153
12254
  <label>Title</label>
12154
12255
  <input type="text" id="teTitle" value="${(props.title || '').replace(/"/g, '&quot;')}" autocomplete="off">
@@ -12304,6 +12405,7 @@
12304
12405
  let title = '';
12305
12406
  let titleLines = undefined;
12306
12407
  let showcaseMode = undefined;
12408
+ let deviceMockup = currentTemplateProps?.deviceMockup;
12307
12409
 
12308
12410
  // Determine showcase mode from toggle
12309
12411
  if (isShowcase) {
@@ -12322,6 +12424,11 @@
12322
12424
  title = titleInput ? titleInput.value : (currentTemplateProps?.title || '');
12323
12425
  }
12324
12426
 
12427
+ const deviceMockupSelect = modal.querySelector('#teDeviceMockup');
12428
+ if (deviceMockupSelect) {
12429
+ deviceMockup = deviceMockupSelect.value;
12430
+ }
12431
+
12325
12432
  // Collect terminal tabs
12326
12433
  const tabRows = modal.querySelectorAll('.te-tab-row');
12327
12434
  const terminalTabs = Array.from(tabRows).map(row => {
@@ -12336,16 +12443,29 @@
12336
12443
  });
12337
12444
 
12338
12445
  try {
12339
- await fetch(`/api/projects/${currentProject.id}/template-content`, {
12446
+ const res = await fetch(`/api/projects/${currentProject.id}/template-content`, {
12340
12447
  method: 'PUT',
12341
12448
  headers: { 'Content-Type': 'application/json' },
12342
- body: JSON.stringify({ title, titleLines, terminalTabs, showcaseMode }),
12449
+ body: JSON.stringify({ title, titleLines, terminalTabs, showcaseMode, deviceMockup }),
12343
12450
  });
12451
+ const data = await res.json();
12452
+ if (data.error) {
12453
+ showToast(data.error, 'error');
12454
+ return;
12455
+ }
12344
12456
  showToast('Template content saved', 'success');
12345
12457
  modal.remove();
12346
12458
 
12347
12459
  // Update local props
12348
- currentTemplateProps = { title, titleLines, terminalTabs, hasNetworkData: terminalTabs.length > 0, showcaseMode };
12460
+ currentTemplateProps = {
12461
+ ...currentTemplateProps,
12462
+ title: data.content?.title || title,
12463
+ titleLines: data.content?.titleLines || titleLines,
12464
+ terminalTabs,
12465
+ hasNetworkData: terminalTabs.length > 0,
12466
+ showcaseMode: data.content?.showcaseMode || showcaseMode,
12467
+ deviceMockup: data.content?.deviceMockup || deviceMockup,
12468
+ };
12349
12469
 
12350
12470
  // Force re-render
12351
12471
  if (activeTemplate !== 'raw') {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  startServer,
4
4
  stopServer
5
- } from "./chunk-VRM42PML.js";
5
+ } from "./chunk-LXSWDEXV.js";
6
6
  import {
7
7
  analyzeTools,
8
8
  canvasTools,
@@ -15,17 +15,16 @@ import {
15
15
  templateTools,
16
16
  testingTools,
17
17
  uiTools
18
- } from "./chunk-4L76GPRC.js";
18
+ } from "./chunk-AHVBE25Y.js";
19
19
  import {
20
20
  setupTools
21
21
  } from "./chunk-FNUN7EPB.js";
22
22
  import {
23
23
  mcpServer
24
24
  } from "./chunk-XKX6NBHF.js";
25
- import "./chunk-3QRQEDWR.js";
26
- import "./chunk-6EGBXRDK.js";
25
+ import "./chunk-HGWEHWKJ.js";
27
26
  import "./chunk-FIL7IWEL.js";
28
- import "./chunk-N6JJ2RGV.js";
27
+ import "./chunk-ZLHIHMSL.js";
29
28
  import "./chunk-LB3RNE3O.js";
30
29
  import {
31
30
  attachESVPNetworkTrace,
@@ -51,7 +50,8 @@ import {
51
50
  runESVPActions,
52
51
  runESVPPreflight,
53
52
  validateESVPReplay
54
- } from "./chunk-GAKEFJ5T.js";
53
+ } from "./chunk-7EDIUVIO.js";
54
+ import "./chunk-6EGBXRDK.js";
55
55
  import "./chunk-SLNJEF32.js";
56
56
  import {
57
57
  closeDatabase,