site-agent-pro 1.0.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.
Files changed (81) hide show
  1. package/README.md +689 -0
  2. package/dist/auth/credentialStore.js +62 -0
  3. package/dist/auth/inbox.js +193 -0
  4. package/dist/auth/profile.js +379 -0
  5. package/dist/auth/runner.js +1124 -0
  6. package/dist/backend/dashboardData.js +194 -0
  7. package/dist/backend/runArtifacts.js +48 -0
  8. package/dist/backend/runRepository.js +93 -0
  9. package/dist/bin.js +2 -0
  10. package/dist/cli/backfillSiteChecks.js +143 -0
  11. package/dist/cli/run.js +309 -0
  12. package/dist/cli/trade.js +69 -0
  13. package/dist/config.js +199 -0
  14. package/dist/core/agentProfiles.js +55 -0
  15. package/dist/core/aggregateReport.js +382 -0
  16. package/dist/core/audit.js +30 -0
  17. package/dist/core/customTaskSuite.js +148 -0
  18. package/dist/core/evaluator.js +217 -0
  19. package/dist/core/executor.js +788 -0
  20. package/dist/core/fallbackReport.js +335 -0
  21. package/dist/core/formHeuristics.js +411 -0
  22. package/dist/core/gameplaySummary.js +164 -0
  23. package/dist/core/interaction.js +202 -0
  24. package/dist/core/pageState.js +201 -0
  25. package/dist/core/planner.js +1669 -0
  26. package/dist/core/processSubmissionBatch.js +204 -0
  27. package/dist/core/runAuditJob.js +170 -0
  28. package/dist/core/runner.js +2352 -0
  29. package/dist/core/siteBrief.js +107 -0
  30. package/dist/core/siteChecks.js +1526 -0
  31. package/dist/core/taskDirectives.js +279 -0
  32. package/dist/core/taskHeuristics.js +263 -0
  33. package/dist/dashboard/client.js +1256 -0
  34. package/dist/dashboard/contracts.js +95 -0
  35. package/dist/dashboard/narrative.js +277 -0
  36. package/dist/dashboard/server.js +458 -0
  37. package/dist/dashboard/theme.js +888 -0
  38. package/dist/index.js +84 -0
  39. package/dist/llm/client.js +188 -0
  40. package/dist/paystack/account.js +123 -0
  41. package/dist/paystack/client.js +100 -0
  42. package/dist/paystack/index.js +13 -0
  43. package/dist/paystack/test-paystack.js +83 -0
  44. package/dist/paystack/transfer.js +138 -0
  45. package/dist/paystack/types.js +74 -0
  46. package/dist/paystack/webhook.js +121 -0
  47. package/dist/prompts/browserAgent.js +124 -0
  48. package/dist/prompts/reviewer.js +71 -0
  49. package/dist/reporting/clickReplay.js +290 -0
  50. package/dist/reporting/html.js +930 -0
  51. package/dist/reporting/markdown.js +238 -0
  52. package/dist/reporting/template.js +1141 -0
  53. package/dist/schemas/types.js +361 -0
  54. package/dist/submissions/customTasks.js +196 -0
  55. package/dist/submissions/html.js +770 -0
  56. package/dist/submissions/model.js +56 -0
  57. package/dist/submissions/publicUrl.js +76 -0
  58. package/dist/submissions/service.js +74 -0
  59. package/dist/submissions/store.js +37 -0
  60. package/dist/submissions/types.js +65 -0
  61. package/dist/trade/engine.js +241 -0
  62. package/dist/trade/evm/erc20.js +44 -0
  63. package/dist/trade/extractor.js +148 -0
  64. package/dist/trade/policy.js +35 -0
  65. package/dist/trade/session.js +31 -0
  66. package/dist/trade/types.js +107 -0
  67. package/dist/trade/validator.js +148 -0
  68. package/dist/utils/files.js +59 -0
  69. package/dist/utils/log.js +24 -0
  70. package/dist/utils/playwrightCompat.js +14 -0
  71. package/dist/utils/time.js +3 -0
  72. package/dist/wallet/provider.js +345 -0
  73. package/dist/wallet/relay.js +129 -0
  74. package/dist/wallet/wallet.js +178 -0
  75. package/docs/01-installation.md +134 -0
  76. package/docs/02-running-your-first-audit.md +136 -0
  77. package/docs/03-configuration.md +233 -0
  78. package/docs/04-how-the-agent-thinks.md +41 -0
  79. package/docs/05-extending-personas-and-tasks.md +42 -0
  80. package/docs/06-hardening-for-production.md +92 -0
  81. package/package.json +60 -0
@@ -0,0 +1,770 @@
1
+ import { config } from "../config.js";
2
+ import { DASHBOARD_HEAD_TAGS } from "../dashboard/theme.js";
3
+ import { buildDefaultTradeRunOptions } from "../trade/policy.js";
4
+ import { TradeRunOptionsSchema } from "../trade/types.js";
5
+ import { isExpired } from "./model.js";
6
+ import { DEFAULT_SUBMISSION_TARGET_MODE } from "./publicUrl.js";
7
+ function escapeHtml(value) {
8
+ return value
9
+ .replaceAll("&", "&")
10
+ .replaceAll("<", "&lt;")
11
+ .replaceAll(">", "&gt;")
12
+ .replaceAll('"', "&quot;")
13
+ .replaceAll("'", "&#39;");
14
+ }
15
+ function formatDateTime(value) {
16
+ const parsed = new Date(value);
17
+ if (Number.isNaN(parsed.getTime())) {
18
+ return value;
19
+ }
20
+ return new Intl.DateTimeFormat(undefined, {
21
+ dateStyle: "medium",
22
+ timeStyle: "short",
23
+ timeZone: config.deviceTimezone
24
+ }).format(parsed);
25
+ }
26
+ function basePage(args) {
27
+ return `<!doctype html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="utf-8" />
31
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
32
+ <title>${escapeHtml(args.title)}</title>
33
+ ${DASHBOARD_HEAD_TAGS}
34
+ <style>
35
+ :root {
36
+ color-scheme: dark;
37
+ --bg: #0c0c10;
38
+ --surface: #13131a;
39
+ --surface2: #1a1a24;
40
+ --ink: #e8e8f0;
41
+ --muted: #87879b;
42
+ --line: rgba(255, 255, 255, 0.09);
43
+ --card: rgba(19, 19, 26, 0.96);
44
+ --accent: #00d4aa;
45
+ --teal: #00d4aa;
46
+ --gold: #f5a623;
47
+ --red: #ff5555;
48
+ --blue: #4d9fff;
49
+ --shadow: 0 28px 90px rgba(0, 0, 0, 0.34);
50
+ --font-sans: "Syne", sans-serif;
51
+ --font-mono: "IBM Plex Mono", monospace;
52
+ }
53
+
54
+ * { box-sizing: border-box; }
55
+ html, body { margin: 0; min-height: 100%; }
56
+ body {
57
+ font-family: var(--font-sans);
58
+ color: var(--ink);
59
+ background:
60
+ radial-gradient(circle at top left, rgba(0, 212, 170, 0.09), transparent 26%),
61
+ radial-gradient(circle at top right, rgba(77, 159, 255, 0.08), transparent 30%),
62
+ linear-gradient(180deg, #0c0c10 0%, #101018 100%);
63
+ }
64
+
65
+ .page {
66
+ width: min(1040px, calc(100vw - 2rem));
67
+ margin: 0 auto;
68
+ padding: 1rem 0 2.5rem;
69
+ }
70
+
71
+ .landing-shell {
72
+ width: min(1040px, calc(100vw - 2rem));
73
+ margin: 0 auto;
74
+ padding: 0.8rem 0 2.6rem;
75
+ }
76
+
77
+ .card {
78
+ background: var(--card);
79
+ border: 1px solid var(--line);
80
+ border-radius: 24px;
81
+ box-shadow: var(--shadow);
82
+ padding: 1.4rem;
83
+ margin-top: 1rem;
84
+ backdrop-filter: blur(14px);
85
+ }
86
+
87
+ .landing-shell .card {
88
+ margin-top: 0;
89
+ }
90
+
91
+ .eyebrow {
92
+ margin: 0 0 0.45rem;
93
+ color: var(--accent);
94
+ font-size: 0.72rem;
95
+ font-weight: 700;
96
+ letter-spacing: 0.18em;
97
+ text-transform: uppercase;
98
+ font-family: var(--font-mono);
99
+ }
100
+
101
+ h1, h2 {
102
+ margin: 0;
103
+ letter-spacing: -0.03em;
104
+ }
105
+
106
+ h1 { font-size: clamp(2.15rem, 4vw, 3.55rem); line-height: 0.98; }
107
+ h2 { font-size: clamp(1.45rem, 3vw, 2.1rem); line-height: 1; }
108
+
109
+ p, li, label {
110
+ color: var(--muted);
111
+ line-height: 1.6;
112
+ }
113
+
114
+ label {
115
+ display: grid;
116
+ gap: 0.5rem;
117
+ }
118
+
119
+ form {
120
+ display: grid;
121
+ gap: 1rem;
122
+ }
123
+
124
+ input, button, select, textarea {
125
+ width: 100%;
126
+ border-radius: 12px;
127
+ border: 1px solid var(--line);
128
+ padding: 0.92rem 1rem;
129
+ font: inherit;
130
+ }
131
+
132
+ input[type="checkbox"],
133
+ input[type="radio"] {
134
+ width: auto;
135
+ padding: 0;
136
+ }
137
+
138
+ input, select, textarea {
139
+ background: var(--surface2);
140
+ color: var(--ink);
141
+ }
142
+
143
+ textarea {
144
+ min-height: 4.25rem;
145
+ resize: vertical;
146
+ }
147
+
148
+ button, .button-link {
149
+ display: inline-flex;
150
+ justify-content: center;
151
+ align-items: center;
152
+ gap: 0.5rem;
153
+ background: linear-gradient(135deg, var(--accent), #2ae0bf);
154
+ color: #0c0c10;
155
+ text-decoration: none;
156
+ font-weight: 700;
157
+ cursor: pointer;
158
+ }
159
+
160
+ .hero-card {
161
+ padding: clamp(1.35rem, 2vw, 1.85rem);
162
+ display: grid;
163
+ gap: 1.3rem;
164
+ }
165
+
166
+ .hero-top {
167
+ display: grid;
168
+ grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
169
+ gap: 1.25rem;
170
+ align-items: start;
171
+ }
172
+
173
+ .hero-copy {
174
+ max-width: 34rem;
175
+ display: grid;
176
+ gap: 0.9rem;
177
+ }
178
+
179
+ .hero-card .meta,
180
+ .hero-card .hero-grid {
181
+ margin-top: 0;
182
+ }
183
+
184
+ .lead {
185
+ margin-top: 0;
186
+ font-size: 0.98rem;
187
+ max-width: 34rem;
188
+ }
189
+
190
+ .hero-grid {
191
+ display: grid;
192
+ grid-template-columns: repeat(2, minmax(0, 1fr));
193
+ gap: 0.9rem;
194
+ margin-top: 0.15rem;
195
+ }
196
+
197
+ .hero-stat {
198
+ border: 1px solid var(--line);
199
+ border-radius: 16px;
200
+ padding: 1rem 1.05rem;
201
+ background: rgba(255, 255, 255, 0.025);
202
+ }
203
+
204
+ .hero-stat strong {
205
+ display: block;
206
+ color: var(--ink);
207
+ font-size: 1.15rem;
208
+ margin-bottom: 0.28rem;
209
+ }
210
+
211
+ .hero-stat span {
212
+ color: var(--muted);
213
+ font-size: 0.9rem;
214
+ }
215
+
216
+ .launch-panel {
217
+ padding: 1.25rem;
218
+ border: 1px solid var(--line);
219
+ border-radius: 20px;
220
+ background:
221
+ linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02)),
222
+ rgba(8, 8, 14, 0.24);
223
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
224
+ }
225
+
226
+ .card-title {
227
+ font-family: var(--font-mono);
228
+ font-size: 0.78rem;
229
+ text-transform: uppercase;
230
+ letter-spacing: 0.12em;
231
+ color: var(--muted);
232
+ margin-bottom: 0.85rem;
233
+ }
234
+
235
+ .url-row {
236
+ display: flex;
237
+ gap: 0.75rem;
238
+ align-items: stretch;
239
+ }
240
+
241
+ .target-grid {
242
+ display: grid;
243
+ grid-template-columns: repeat(2, minmax(0, 1fr));
244
+ gap: 0.75rem;
245
+ }
246
+
247
+ .target-option {
248
+ position: relative;
249
+ display: block;
250
+ cursor: pointer;
251
+ }
252
+
253
+ .target-option input {
254
+ position: absolute;
255
+ opacity: 0;
256
+ pointer-events: none;
257
+ }
258
+
259
+ .target-option-body {
260
+ height: 100%;
261
+ display: grid;
262
+ gap: 0.35rem;
263
+ padding: 0.92rem 1rem;
264
+ border: 1px solid var(--line);
265
+ border-radius: 16px;
266
+ background: rgba(255, 255, 255, 0.03);
267
+ transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
268
+ }
269
+
270
+ .target-option:hover .target-option-body,
271
+ .target-option:focus-within .target-option-body {
272
+ transform: translateY(-1px);
273
+ border-color: rgba(77, 159, 255, 0.28);
274
+ }
275
+
276
+ .target-option input:checked + .target-option-body {
277
+ border-color: rgba(0, 212, 170, 0.34);
278
+ background: rgba(0, 212, 170, 0.08);
279
+ box-shadow: inset 0 0 0 1px rgba(0, 212, 170, 0.12);
280
+ }
281
+
282
+ .target-option-body strong {
283
+ color: var(--ink);
284
+ font-size: 0.94rem;
285
+ }
286
+
287
+ .target-option-body small {
288
+ color: var(--muted);
289
+ line-height: 1.5;
290
+ font-size: 0.82rem;
291
+ }
292
+
293
+ .url-input {
294
+ flex: 1 1 auto;
295
+ font-family: var(--font-mono);
296
+ }
297
+
298
+ .url-row button {
299
+ width: auto;
300
+ min-width: 210px;
301
+ white-space: nowrap;
302
+ }
303
+
304
+ .config-row {
305
+ display: flex;
306
+ flex-wrap: wrap;
307
+ gap: 0.75rem;
308
+ align-items: center;
309
+ }
310
+
311
+ .config-select {
312
+ width: auto;
313
+ min-width: 130px;
314
+ font-family: var(--font-mono);
315
+ font-size: 0.88rem;
316
+ padding: 0.72rem 0.9rem;
317
+ }
318
+
319
+ .tag {
320
+ display: inline-flex;
321
+ align-items: center;
322
+ border: 1px solid var(--line);
323
+ border-radius: 999px;
324
+ padding: 0.36rem 0.7rem;
325
+ font-family: var(--font-mono);
326
+ font-size: 0.72rem;
327
+ color: var(--muted);
328
+ background: rgba(255, 255, 255, 0.03);
329
+ }
330
+
331
+ .tag.on {
332
+ color: var(--accent);
333
+ border-color: rgba(0, 212, 170, 0.32);
334
+ background: rgba(0, 212, 170, 0.1);
335
+ }
336
+
337
+ .toggle-chip {
338
+ display: inline-flex;
339
+ align-items: center;
340
+ gap: 0.55rem;
341
+ padding: 0.66rem 0.85rem;
342
+ border-radius: 999px;
343
+ border: 1px solid var(--line);
344
+ background: rgba(255, 255, 255, 0.03);
345
+ color: var(--ink);
346
+ font-size: 0.84rem;
347
+ }
348
+
349
+ .toggle-chip input {
350
+ accent-color: var(--accent);
351
+ }
352
+
353
+ .launch-note {
354
+ margin: 0.9rem 0 0;
355
+ padding-top: 0.9rem;
356
+ border-top: 1px solid var(--line);
357
+ font-size: 0.9rem;
358
+ }
359
+
360
+ .task-intro {
361
+ margin: 0;
362
+ font-size: 0.9rem;
363
+ }
364
+
365
+ .scope-note {
366
+ margin: 0;
367
+ font-size: 0.84rem;
368
+ }
369
+
370
+ .instruction-box {
371
+ display: grid;
372
+ gap: 0.6rem;
373
+ }
374
+
375
+ .instruction-box strong {
376
+ color: var(--ink);
377
+ font-size: 0.92rem;
378
+ }
379
+
380
+ .instruction-box textarea {
381
+ min-height: 220px;
382
+ }
383
+
384
+ .file-row {
385
+ display: grid;
386
+ gap: 0.5rem;
387
+ }
388
+
389
+ .file-row input[type="file"] {
390
+ padding: 0.78rem 0.9rem;
391
+ }
392
+
393
+ .meta {
394
+ display: flex;
395
+ flex-wrap: wrap;
396
+ gap: 0.55rem;
397
+ margin-top: 1rem;
398
+ }
399
+
400
+ .meta span {
401
+ display: inline-flex;
402
+ align-items: center;
403
+ padding: 0.38rem 0.72rem;
404
+ border-radius: 999px;
405
+ background: rgba(255, 255, 255, 0.05);
406
+ border: 1px solid var(--line);
407
+ font-size: 0.78rem;
408
+ font-family: var(--font-mono);
409
+ }
410
+
411
+ .status {
412
+ display: inline-flex;
413
+ align-items: center;
414
+ padding: 0.4rem 0.78rem;
415
+ border-radius: 999px;
416
+ font-size: 0.82rem;
417
+ font-weight: 700;
418
+ text-transform: uppercase;
419
+ letter-spacing: 0.06em;
420
+ }
421
+
422
+ .status--queued, .status--running { color: var(--gold); background: rgba(169, 111, 20, 0.12); }
423
+ .status--completed { color: var(--teal); background: rgba(23, 111, 105, 0.12); }
424
+ .status--failed { color: var(--red); background: rgba(180, 35, 24, 0.12); }
425
+
426
+ .error {
427
+ margin-top: 1rem;
428
+ padding: 1rem;
429
+ border-radius: 14px;
430
+ border: 1px dashed rgba(255, 85, 85, 0.35);
431
+ background: rgba(255, 85, 85, 0.08);
432
+ color: var(--red);
433
+ }
434
+
435
+ .link-row {
436
+ display: flex;
437
+ flex-wrap: wrap;
438
+ gap: 0.75rem;
439
+ margin-top: 1rem;
440
+ }
441
+
442
+ .ghost-link {
443
+ display: inline-flex;
444
+ align-items: center;
445
+ padding: 0.82rem 1rem;
446
+ border-radius: 12px;
447
+ border: 1px solid var(--line);
448
+ background: rgba(255, 255, 255, 0.03);
449
+ text-decoration: none;
450
+ color: var(--ink);
451
+ font-weight: 700;
452
+ }
453
+
454
+ .button-link {
455
+ border-radius: 12px;
456
+ }
457
+
458
+ .mono {
459
+ font-family: var(--font-mono);
460
+ }
461
+
462
+ @media (max-width: 920px) {
463
+ .hero-top,
464
+ .hero-grid {
465
+ grid-template-columns: 1fr;
466
+ }
467
+
468
+ .target-grid {
469
+ grid-template-columns: 1fr;
470
+ }
471
+ }
472
+
473
+ @media (max-width: 720px) {
474
+ .page,
475
+ .landing-shell {
476
+ width: min(100vw - 1rem, 1120px);
477
+ padding: 1rem 0 2rem;
478
+ }
479
+
480
+ .hero-card,
481
+ .card {
482
+ padding: 1.1rem;
483
+ }
484
+
485
+ .launch-panel {
486
+ padding: 1rem;
487
+ }
488
+
489
+ .url-row {
490
+ flex-direction: column;
491
+ }
492
+
493
+ .url-row button {
494
+ width: 100%;
495
+ }
496
+
497
+ .config-row {
498
+ align-items: stretch;
499
+ }
500
+
501
+ .config-select {
502
+ width: 100%;
503
+ }
504
+ }
505
+ </style>
506
+ </head>
507
+ <body>
508
+ ${args.body}
509
+ </body>
510
+ </html>`;
511
+ }
512
+ export function renderLandingPage(args) {
513
+ const selectedAgentCount = Math.min(5, Math.max(1, Math.round(args.selectedAgentCount ?? 1)));
514
+ const selectedTradeOptions = TradeRunOptionsSchema.parse({
515
+ ...buildDefaultTradeRunOptions(),
516
+ ...(args.tradeOptions ?? {})
517
+ });
518
+ const allowPrivateTargets = Boolean(args.allowPrivateTargets);
519
+ const selectedTargetMode = allowPrivateTargets && args.selectedTargetMode === "localhost"
520
+ ? "localhost"
521
+ : DEFAULT_SUBMISSION_TARGET_MODE;
522
+ const lead = allowPrivateTargets
523
+ ? "Start with a public URL or a localhost/private dev URL and Site Agent Pro will send 1 to 5 AI visitors through the site to execute the exact tasks you provide. The output focuses on what each task attempted, what visibly happened, and whether each task succeeded or failed on desktop and mobile."
524
+ : "Start with a public URL and Site Agent Pro will send 1 to 5 AI visitors through the site to execute the exact tasks you provide. The output focuses on what each task attempted, what visibly happened, and whether each task succeeded or failed on desktop and mobile.";
525
+ const urlPlaceholder = allowPrivateTargets && selectedTargetMode === "localhost"
526
+ ? "http://localhost:3000"
527
+ : allowPrivateTargets
528
+ ? "https://example.com or http://localhost:3000"
529
+ : "https://example.com";
530
+ const launchNote = allowPrivateTargets
531
+ ? "Use the Localhost/private dev site option when you want to probe localhost, .localhost, .local, 127.0.0.1, or private LAN URLs from this machine."
532
+ : "Launch from the homepage, then use the dashboard to inspect accepted tasks, visible evidence, and saved run artifacts.";
533
+ return basePage({
534
+ title: "Site Agent Pro",
535
+ body: `
536
+ <main class="landing-shell">
537
+ <section class="card hero-card">
538
+ <p class="eyebrow">Site Agent Pro</p>
539
+ <div class="hero-top">
540
+ <div class="hero-copy">
541
+ <h1>Review your website the way a real visitor experiences it.</h1>
542
+ <p class="lead">${lead}</p>
543
+ <div class="meta">
544
+ <span>Task-driven sessions</span>
545
+ <span>Desktop and mobile notes</span>
546
+ <span>Observed clicks and states</span>
547
+ <span>Success or failure per task</span>
548
+ <span>Saved execution outputs</span>
549
+ ${allowPrivateTargets ? "<span>Localhost-ready</span>" : ""}
550
+ </div>
551
+ </div>
552
+ <div class="launch-panel">
553
+ <div class="card-title">New test</div>
554
+ ${args.error ? `<div class="error">${escapeHtml(args.error)}</div>` : ""}
555
+ <form method="post" action="/submit" enctype="multipart/form-data">
556
+ ${allowPrivateTargets
557
+ ? `
558
+ <div class="target-grid">
559
+ <label class="target-option">
560
+ <input type="radio" name="target" value="public" ${selectedTargetMode === "public" ? "checked" : ""} />
561
+ <span class="target-option-body">
562
+ <strong>Public site</strong>
563
+ <small>Use a normal internet-facing URL like https://example.com.</small>
564
+ </span>
565
+ </label>
566
+ <label class="target-option">
567
+ <input type="radio" name="target" value="localhost" ${selectedTargetMode === "localhost" ? "checked" : ""} />
568
+ <span class="target-option-body">
569
+ <strong>Localhost/private dev site</strong>
570
+ <small>Allow http://localhost, 127.0.0.1, .localhost, .local, and private LAN URLs.</small>
571
+ </span>
572
+ </label>
573
+ </div>
574
+ `
575
+ : ""}
576
+ <div class="url-row">
577
+ <input class="url-input" type="url" name="url" placeholder="${urlPlaceholder}" required value="${escapeHtml(args.submittedUrl ?? "")}" />
578
+ <button type="submit">▶ Start task run</button>
579
+ </div>
580
+ ${allowPrivateTargets
581
+ ? `<p class="scope-note">Public mode keeps the original hosted-safe validation. Localhost/private mode unlocks local dev targets when you run this dashboard on your own machine.</p>`
582
+ : ""}
583
+ <p class="task-intro">Paste the instructions in one box or upload a text or JSON file. The agent will first understand what the site appears to be for, then perform only the instructions you supplied.</p>
584
+ <label class="instruction-box">
585
+ <strong>Instructions</strong>
586
+ <textarea name="instructions" placeholder="- Find the main signup path and explain whether it is clear&#10;- Check what a new user is supposed to do first&#10;- Try the pricing flow and stop before entering private details">${escapeHtml(args.submittedInstructions ?? "")}</textarea>
587
+ </label>
588
+ <label class="file-row">
589
+ <strong>Instruction file (optional)</strong>
590
+ <input type="file" name="instructions_file" accept=".txt,.md,.json,.csv,text/plain,application/json" />
591
+ </label>
592
+ <div class="config-row">
593
+ <select class="config-select" name="agents">
594
+ ${[1, 2, 3, 4, 5]
595
+ .map((count) => `<option value="${count}" ${selectedAgentCount === count ? "selected" : ""}>${count} agent${count === 1 ? "" : "s"}</option>`)
596
+ .join("")}
597
+ </select>
598
+ <label class="toggle-chip">
599
+ <input type="checkbox" name="trade_enabled" ${selectedTradeOptions.enabled ? "checked" : ""} />
600
+ <span>Enable onchain execution</span>
601
+ </label>
602
+ <label class="toggle-chip">
603
+ <input type="checkbox" name="trade_dry_run" ${selectedTradeOptions.dryRun ? "checked" : ""} />
604
+ <span>Dry run</span>
605
+ </label>
606
+ <select class="config-select" name="trade_strategy">
607
+ ${[
608
+ ["auto", "trade: auto"],
609
+ ["deposit_only", "trade: deposit only"],
610
+ ["dapp_only", "trade: dapp only"]
611
+ ]
612
+ .map(([value, label]) => `<option value="${value}" ${selectedTradeOptions.strategy === value ? "selected" : ""}>${label}</option>`)
613
+ .join("")}
614
+ </select>
615
+ <select class="config-select" name="trade_confirmations">
616
+ ${[0, 1, 2, 3]
617
+ .map((count) => `<option value="${count}" ${selectedTradeOptions.confirmations === count ? "selected" : ""}>${count} conf${count === 1 ? "" : "s"}</option>`)
618
+ .join("")}
619
+ </select>
620
+ <span class="tag on">tabs and links</span>
621
+ <span class="tag on">mobile notes</span>
622
+ <span class="tag on">honest feedback</span>
623
+ </div>
624
+ <p class="scope-note">Wallet secrets stay in <code>.env</code>. These controls only decide whether this run may use the configured wallet and whether it should broadcast or stay in dry-run mode.</p>
625
+ </form>
626
+ <p class="launch-note">${launchNote}</p>
627
+ <div class="link-row">
628
+ <a class="ghost-link" href="/dashboard">Open dashboard</a>
629
+ </div>
630
+ </div>
631
+ </div>
632
+ <div class="hero-grid">
633
+ <div class="hero-stat">
634
+ <strong>1-5 agents</strong>
635
+ <span>Run one agent or a small task panel against the same accepted task list.</span>
636
+ </div>
637
+ <div class="hero-stat">
638
+ <strong>Real interaction notes</strong>
639
+ <span>Each output explains what each task tried, what happened, and where it stalled.</span>
640
+ </div>
641
+ <div class="hero-stat">
642
+ <strong>Task outcome focus</strong>
643
+ <span>Outputs stay anchored to accepted tasks instead of drifting into generic site commentary.</span>
644
+ </div>
645
+ <div class="hero-stat">
646
+ <strong>Task dashboard</strong>
647
+ <span>Use the dashboard after launch to inspect saved task runs and artifacts.</span>
648
+ </div>
649
+ </div>
650
+ </section>
651
+ </main>
652
+ `
653
+ });
654
+ }
655
+ export function renderSubmissionStatusPage(args) {
656
+ const { submission } = args;
657
+ const reportUrl = `${args.appBaseUrl}${submission.publicReportPath}`;
658
+ const dashboardRunUrl = submission.runId ? `/dashboard?run=${encodeURIComponent(submission.runId)}` : "/dashboard";
659
+ const htmlDownloadUrl = submission.runId ? `/api/runs/${encodeURIComponent(submission.runId)}/artifacts/report.html` : null;
660
+ const jsonDownloadUrl = submission.runId ? `/api/runs/${encodeURIComponent(submission.runId)}/artifacts/report.json` : null;
661
+ const shouldRefresh = submission.status === "queued" || submission.status === "running";
662
+ const finishedAgentCount = submission.completedAgentCount + submission.failedAgentCount;
663
+ return basePage({
664
+ title: "Site Agent Pro submission",
665
+ body: `
666
+ <main class="page">
667
+ <section class="card">
668
+ <p class="eyebrow">Submission status</p>
669
+ <h1>Your test is ${escapeHtml(submission.status)}</h1>
670
+ <p>This page refreshes while the run is active. Once it finishes, open the saved task run in the dashboard or download the task output directly.</p>
671
+ <div class="meta">
672
+ <span class="status status--${escapeHtml(submission.status)}">${escapeHtml(submission.status)}</span>
673
+ <span>${escapeHtml(submission.url)}</span>
674
+ <span>${escapeHtml(`${submission.agentCount} agent${submission.agentCount === 1 ? "" : "s"}`)}</span>
675
+ <span>${escapeHtml(`${submission.customTasks.length} accepted task${submission.customTasks.length === 1 ? "" : "s"}`)}</span>
676
+ <span>${escapeHtml(`${submission.completedAgentCount} completed`)}</span>
677
+ <span>${escapeHtml(`${submission.failedAgentCount} failed`)}</span>
678
+ <span>${escapeHtml(`${finishedAgentCount}/${submission.agentCount} finished`)}</span>
679
+ <span>Expires ${escapeHtml(formatDateTime(submission.expiresAt))}</span>
680
+ <span>${escapeHtml(config.deviceTimezone)}</span>
681
+ </div>
682
+ <div class="card" style="margin-top: 1rem; padding: 1rem;">
683
+ <h2>Accepted tasks</h2>
684
+ ${submission.customTasks.length > 0
685
+ ? `<ul>${submission.customTasks.map((task) => `<li>${escapeHtml(task)}</li>`).join("")}</ul>`
686
+ : `<p>No accepted tasks were captured for this submission.</p>`}
687
+ ${submission.instructionText
688
+ ? `<p style="margin-top: 1rem;"><strong>Instruction source</strong></p><p style="white-space: pre-wrap;">${escapeHtml(submission.instructionText)}</p>`
689
+ : ""}
690
+ ${submission.instructionFileName ? `<p class="muted" style="margin-top: 0.7rem;">Uploaded file: ${escapeHtml(submission.instructionFileName)}</p>` : ""}
691
+ </div>
692
+ ${submission.agentRuns.length > 0
693
+ ? `
694
+ <div class="card" style="margin-top: 1rem; padding: 1rem;">
695
+ <h2>Agent panel</h2>
696
+ <div class="meta">
697
+ ${submission.agentRuns
698
+ .map((agentRun) => `
699
+ <span class="status status--${escapeHtml(agentRun.status)}">${escapeHtml(agentRun.profileLabel ? `${agentRun.label}: ${agentRun.profileLabel}` : agentRun.label)} (${escapeHtml(agentRun.status)})</span>
700
+ `)
701
+ .join("")}
702
+ </div>
703
+ <div class="link-row">
704
+ ${submission.agentRuns
705
+ .map((agentRun) => {
706
+ if (!agentRun.runId) {
707
+ return "";
708
+ }
709
+ return `
710
+ <a class="ghost-link" href="/outputs/${escapeHtml(agentRun.runId)}">${escapeHtml(`${agentRun.label} output`)}</a>
711
+ <a class="ghost-link" href="/api/runs/${escapeHtml(agentRun.runId)}/artifacts/report.html">${escapeHtml(`${agentRun.label} HTML`)}</a>
712
+ <a class="ghost-link" href="/api/runs/${escapeHtml(agentRun.runId)}/artifacts/report.json">${escapeHtml(`${agentRun.label} JSON`)}</a>
713
+ `;
714
+ })
715
+ .join("")}
716
+ </div>
717
+ </div>
718
+ `
719
+ : ""}
720
+ ${submission.status === "completed"
721
+ ? `
722
+ <div class="link-row">
723
+ <a class="button-link" href="${escapeHtml(dashboardRunUrl)}">Open in dashboard</a>
724
+ <a class="ghost-link" href="/outputs/${escapeHtml(submission.runId ?? "")}">Open standalone output</a>
725
+ ${htmlDownloadUrl ? `<a class="ghost-link" href="${escapeHtml(htmlDownloadUrl)}">Download HTML output</a>` : ""}
726
+ ${jsonDownloadUrl ? `<a class="ghost-link" href="${escapeHtml(jsonDownloadUrl)}">Download JSON output</a>` : ""}
727
+ <a class="ghost-link" href="${escapeHtml(reportUrl)}">Open public output</a>
728
+ </div>
729
+ `
730
+ : ""}
731
+ ${submission.status === "failed" && submission.error
732
+ ? `<div class="error">${escapeHtml(submission.error)}</div>`
733
+ : ""}
734
+ </section>
735
+ </main>
736
+ ${shouldRefresh
737
+ ? `<script>setTimeout(() => window.location.reload(), 7000);</script>`
738
+ : ""}
739
+ `
740
+ });
741
+ }
742
+ export function renderReportUnavailablePage(args) {
743
+ return basePage({
744
+ title: args.title,
745
+ body: `
746
+ <main class="page">
747
+ <section class="card">
748
+ <p class="eyebrow">Site Agent Pro task output</p>
749
+ <h1>${escapeHtml(args.title)}</h1>
750
+ <p>${escapeHtml(args.message)}</p>
751
+ </section>
752
+ </main>
753
+ `
754
+ });
755
+ }
756
+ export function renderExpiredReportPage(submission) {
757
+ return renderReportUnavailablePage({
758
+ title: "This task output link has expired",
759
+ message: `Task outputs are available for 30 days. This link for ${submission.url} expired on ${formatDateTime(submission.expiresAt)} (${config.deviceTimezone}).`
760
+ });
761
+ }
762
+ export function canAccessPublicReport(submission) {
763
+ if (submission.status !== "completed" || !submission.runId) {
764
+ return { allowed: false, reason: "This task output is not ready yet." };
765
+ }
766
+ if (isExpired(submission.expiresAt)) {
767
+ return { allowed: false, reason: "This task output link has expired." };
768
+ }
769
+ return { allowed: true };
770
+ }