@ucptools/validator 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 (121) hide show
  1. package/CLAUDE.md +109 -0
  2. package/CONTRIBUTING.md +113 -0
  3. package/LICENSE +21 -0
  4. package/README.md +203 -0
  5. package/api/analyze-feed.js +140 -0
  6. package/api/badge.js +185 -0
  7. package/api/benchmark.js +177 -0
  8. package/api/directory-stats.ts +29 -0
  9. package/api/directory.ts +73 -0
  10. package/api/generate-compliance.js +143 -0
  11. package/api/generate-schema.js +457 -0
  12. package/api/generate.js +132 -0
  13. package/api/security-scan.js +133 -0
  14. package/api/simulate.js +187 -0
  15. package/api/tsconfig.json +10 -0
  16. package/api/validate.js +1351 -0
  17. package/apify-actor/.actor/actor.json +68 -0
  18. package/apify-actor/.actor/input_schema.json +32 -0
  19. package/apify-actor/APIFY-STORE-LISTING.md +412 -0
  20. package/apify-actor/Dockerfile +8 -0
  21. package/apify-actor/README.md +166 -0
  22. package/apify-actor/main.ts +111 -0
  23. package/apify-actor/package.json +17 -0
  24. package/apify-actor/src/main.js +199 -0
  25. package/docs/BRAND-IDENTITY.md +238 -0
  26. package/docs/BRAND-STYLE-GUIDE.md +356 -0
  27. package/drizzle/0000_black_king_cobra.sql +39 -0
  28. package/drizzle/meta/0000_snapshot.json +309 -0
  29. package/drizzle/meta/_journal.json +13 -0
  30. package/drizzle.config.ts +10 -0
  31. package/examples/full-profile.json +70 -0
  32. package/examples/minimal-profile.json +23 -0
  33. package/package.json +69 -0
  34. package/public/.well-known/ucp +25 -0
  35. package/public/android-chrome-192x192.png +0 -0
  36. package/public/android-chrome-512x512.png +0 -0
  37. package/public/apple-touch-icon.png +0 -0
  38. package/public/brand.css +321 -0
  39. package/public/directory.html +701 -0
  40. package/public/favicon-16x16.png +0 -0
  41. package/public/favicon-32x32.png +0 -0
  42. package/public/favicon.ico +0 -0
  43. package/public/guides/bigcommerce.html +743 -0
  44. package/public/guides/fastucp.html +838 -0
  45. package/public/guides/magento.html +779 -0
  46. package/public/guides/shopify.html +726 -0
  47. package/public/guides/squarespace.html +749 -0
  48. package/public/guides/wix.html +747 -0
  49. package/public/guides/woocommerce.html +733 -0
  50. package/public/index.html +3835 -0
  51. package/public/learn.html +396 -0
  52. package/public/logo.jpeg +0 -0
  53. package/public/og-image-icon.png +0 -0
  54. package/public/og-image.png +0 -0
  55. package/public/robots.txt +6 -0
  56. package/public/site.webmanifest +31 -0
  57. package/public/sitemap.xml +69 -0
  58. package/public/social/linkedin-banner-1128x191.png +0 -0
  59. package/public/social/temp.PNG +0 -0
  60. package/public/social/x-header-1500x500.png +0 -0
  61. package/public/verify.html +410 -0
  62. package/scripts/generate-favicons.js +44 -0
  63. package/scripts/generate-ico.js +23 -0
  64. package/scripts/generate-og-image.js +45 -0
  65. package/scripts/reset-db.ts +77 -0
  66. package/scripts/seed-db.ts +71 -0
  67. package/scripts/setup-benchmark-db.js +70 -0
  68. package/src/api/server.ts +266 -0
  69. package/src/cli/index.ts +302 -0
  70. package/src/compliance/compliance-generator.ts +452 -0
  71. package/src/compliance/index.ts +28 -0
  72. package/src/compliance/templates.ts +338 -0
  73. package/src/compliance/types.ts +170 -0
  74. package/src/db/index.ts +28 -0
  75. package/src/db/schema.ts +84 -0
  76. package/src/feed-analyzer/feed-analyzer.ts +726 -0
  77. package/src/feed-analyzer/index.ts +34 -0
  78. package/src/feed-analyzer/types.ts +354 -0
  79. package/src/generator/index.ts +7 -0
  80. package/src/generator/key-generator.ts +124 -0
  81. package/src/generator/profile-builder.ts +402 -0
  82. package/src/hosting/artifacts-generator.ts +679 -0
  83. package/src/hosting/index.ts +6 -0
  84. package/src/index.ts +105 -0
  85. package/src/security/index.ts +15 -0
  86. package/src/security/security-scanner.ts +604 -0
  87. package/src/security/types.ts +55 -0
  88. package/src/services/directory.ts +434 -0
  89. package/src/simulator/agent-simulator.ts +941 -0
  90. package/src/simulator/index.ts +7 -0
  91. package/src/simulator/types.ts +170 -0
  92. package/src/types/generator.ts +140 -0
  93. package/src/types/index.ts +7 -0
  94. package/src/types/ucp-profile.ts +140 -0
  95. package/src/types/validation.ts +89 -0
  96. package/src/validator/index.ts +194 -0
  97. package/src/validator/network-validator.ts +417 -0
  98. package/src/validator/rules-validator.ts +297 -0
  99. package/src/validator/sdk-validator.ts +330 -0
  100. package/src/validator/structural-validator.ts +476 -0
  101. package/tests/fixtures/non-compliant-profile.json +25 -0
  102. package/tests/fixtures/official-sample-profile.json +75 -0
  103. package/tests/integration/benchmark.test.ts +207 -0
  104. package/tests/integration/database.test.ts +163 -0
  105. package/tests/integration/directory-api.test.ts +268 -0
  106. package/tests/integration/simulate-api.test.ts +230 -0
  107. package/tests/integration/validate-api.test.ts +269 -0
  108. package/tests/setup.ts +15 -0
  109. package/tests/unit/agent-simulator.test.ts +575 -0
  110. package/tests/unit/compliance-generator.test.ts +374 -0
  111. package/tests/unit/directory-service.test.ts +272 -0
  112. package/tests/unit/feed-analyzer.test.ts +517 -0
  113. package/tests/unit/lint-suggestions.test.ts +423 -0
  114. package/tests/unit/official-samples.test.ts +211 -0
  115. package/tests/unit/pdf-report.test.ts +390 -0
  116. package/tests/unit/sdk-validator.test.ts +531 -0
  117. package/tests/unit/security-scanner.test.ts +410 -0
  118. package/tests/unit/validation.test.ts +390 -0
  119. package/tsconfig.json +20 -0
  120. package/vercel.json +34 -0
  121. package/vitest.config.ts +22 -0
@@ -0,0 +1,3835 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <!-- Google Analytics -->
6
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-J5JSHV7H1E"></script>
7
+ <script>
8
+ window.dataLayer = window.dataLayer || [];
9
+ function gtag() { dataLayer.push(arguments); }
10
+ gtag('js', new Date());
11
+ gtag('config', 'G-J5JSHV7H1E');
12
+ </script>
13
+ <!-- Ahrefs Analytics -->
14
+ <script src="https://analytics.ahrefs.com/analytics.js" data-key="w/KDij6w81HzA8oXaw60JA" async></script>
15
+ <!-- jsPDF for PDF report generation -->
16
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
17
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
18
+ <meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
+ <title>Universal Commerce Protocol (UCP) Validator & AI Commerce Readiness Checker | UCP.tools</title>
21
+ <meta name="description"
22
+ content="The leading Universal Commerce Protocol (UCP) validator. Check if your e-commerce store is ready for AI shopping agents like Google AI Mode, ChatGPT Shopping, and Microsoft Copilot. Validate UCP profiles, Schema.org markup, and Jan 2026 compliance.">
23
+ <meta name="keywords" content="Universal Commerce Protocol, UCP, AI Commerce, AI Shopping Agents, Google AI Mode, ChatGPT Shopping, Copilot Checkout, Schema.org, E-commerce, UCP Validator, AI Readiness, Google Shopping, Merchant Center">
24
+ <meta name="author" content="UCP.tools">
25
+ <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
26
+ <link rel="canonical" href="https://ucptools.dev/">
27
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
28
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
29
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
30
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
31
+ <link rel="manifest" href="/site.webmanifest">
32
+ <meta name="theme-color" content="#2E86AB">
33
+ <meta name="google-site-verification" content="Ra49U5gMiADsSr9byqzFnI-GJVA4Gj0_i38nJewkhaU" />
34
+
35
+ <!-- Open Graph / Facebook -->
36
+ <meta property="og:type" content="website">
37
+ <meta property="og:url" content="https://ucptools.dev/">
38
+ <meta property="og:title" content="Universal Commerce Protocol (UCP) Validator | AI Commerce Readiness Checker">
39
+ <meta property="og:description"
40
+ content="The leading UCP validator for e-commerce. Check AI shopping agent readiness for Google AI Mode, ChatGPT, and Copilot. Validate UCP profiles and Schema.org compliance.">
41
+ <meta property="og:image" content="https://ucptools.dev/og-image.png">
42
+ <meta property="og:image:width" content="1200">
43
+ <meta property="og:image:height" content="630">
44
+ <meta property="og:site_name" content="UCP.tools">
45
+ <meta property="og:locale" content="en_US">
46
+
47
+ <!-- Twitter Card -->
48
+ <meta name="twitter:card" content="summary_large_image">
49
+ <meta name="twitter:url" content="https://ucptools.dev/">
50
+ <meta name="twitter:title" content="Universal Commerce Protocol (UCP) Validator | AI Commerce Readiness">
51
+ <meta name="twitter:description" content="Check if your store is ready for AI shopping agents. Validate UCP profiles for Google AI Mode, ChatGPT Shopping, and Copilot.">
52
+ <meta name="twitter:image" content="https://ucptools.dev/og-image.png">
53
+
54
+ <!-- JSON-LD Structured Data -->
55
+ <script type="application/ld+json">
56
+ {
57
+ "@context": "https://schema.org",
58
+ "@graph": [
59
+ {
60
+ "@type": "WebSite",
61
+ "@id": "https://ucptools.dev/#website",
62
+ "url": "https://ucptools.dev/",
63
+ "name": "UCP.tools",
64
+ "description": "Universal Commerce Protocol (UCP) Validator and AI Commerce Readiness Checker",
65
+ "publisher": {"@id": "https://ucptools.dev/#organization"},
66
+ "potentialAction": {
67
+ "@type": "SearchAction",
68
+ "target": {
69
+ "@type": "EntryPoint",
70
+ "urlTemplate": "https://ucptools.dev/?domain={search_term_string}"
71
+ },
72
+ "query-input": "required name=search_term_string"
73
+ }
74
+ },
75
+ {
76
+ "@type": "Organization",
77
+ "@id": "https://ucptools.dev/#organization",
78
+ "name": "UCP.tools",
79
+ "url": "https://ucptools.dev/",
80
+ "logo": {
81
+ "@type": "ImageObject",
82
+ "url": "https://ucptools.dev/logo.jpeg",
83
+ "width": 512,
84
+ "height": 512
85
+ },
86
+ "sameAs": []
87
+ },
88
+ {
89
+ "@type": "SoftwareApplication",
90
+ "@id": "https://ucptools.dev/#software",
91
+ "name": "UCP.tools - Universal Commerce Protocol Validator",
92
+ "applicationCategory": "DeveloperApplication",
93
+ "operatingSystem": "Web",
94
+ "offers": {
95
+ "@type": "Offer",
96
+ "price": "0",
97
+ "priceCurrency": "USD"
98
+ },
99
+ "description": "Free Universal Commerce Protocol (UCP) validator and AI Commerce readiness checker for e-commerce stores. Validates UCP profiles, Schema.org markup, and Google AI Mode compatibility.",
100
+ "featureList": [
101
+ "UCP Profile Validation",
102
+ "Schema.org Markup Validation",
103
+ "AI Shopping Agent Readiness Score",
104
+ "Google AI Mode Compatibility Check",
105
+ "Jan 2026 Compliance Verification",
106
+ "PDF Report Generation",
107
+ "UCP Profile Generator"
108
+ ],
109
+ "screenshot": "https://ucptools.dev/og-image.png",
110
+ "provider": {"@id": "https://ucptools.dev/#organization"}
111
+ },
112
+ {
113
+ "@type": "WebPage",
114
+ "@id": "https://ucptools.dev/#webpage",
115
+ "url": "https://ucptools.dev/",
116
+ "name": "Universal Commerce Protocol (UCP) Validator & AI Commerce Readiness Checker",
117
+ "description": "The leading UCP validator. Check if your e-commerce store is ready for AI shopping agents like Google AI Mode, ChatGPT, and Copilot.",
118
+ "isPartOf": {"@id": "https://ucptools.dev/#website"},
119
+ "about": {"@id": "https://ucptools.dev/#software"},
120
+ "primaryImageOfPage": {
121
+ "@type": "ImageObject",
122
+ "url": "https://ucptools.dev/og-image.png"
123
+ }
124
+ }
125
+ ]
126
+ }
127
+ </script>
128
+ <style>
129
+ /* UCP.tools Brand Design System */
130
+ :root {
131
+ /* Primary Colors (from logo gradient) */
132
+ --brand-blue: #2E86AB;
133
+ --brand-teal: #36B5A2;
134
+ --brand-green: #47C97A;
135
+ --brand-gradient: linear-gradient(135deg, #2E86AB 0%, #36B5A2 50%, #47C97A 100%);
136
+ --brand-gradient-hover: linear-gradient(135deg, #267593 0%, #2EA18F 50%, #3BB86B 100%);
137
+
138
+ /* Secondary Colors */
139
+ --color-dark: #1A2B3C;
140
+ --color-medium: #5A6978;
141
+ --color-light: #94A3B8;
142
+ --color-border: #E2E8F0;
143
+ --color-background: #F8FAFC;
144
+ --color-card: #FFFFFF;
145
+
146
+ /* Semantic Colors */
147
+ --color-success: #47C97A;
148
+ --color-warning: #F59E0B;
149
+ --color-error: #EF4444;
150
+ --color-info: #2E86AB;
151
+
152
+ /* Grade Colors */
153
+ --grade-a-bg: #DCFCE7;
154
+ --grade-a-text: #16A34A;
155
+ --grade-b-bg: #DBEAFE;
156
+ --grade-b-text: #2563EB;
157
+ --grade-c-bg: #FEF9C3;
158
+ --grade-c-text: #CA8A04;
159
+ --grade-d-bg: #FED7AA;
160
+ --grade-d-text: #EA580C;
161
+ --grade-f-bg: #FEE2E2;
162
+ --grade-f-text: #DC2626;
163
+ }
164
+
165
+ * {
166
+ box-sizing: border-box;
167
+ margin: 0;
168
+ padding: 0;
169
+ }
170
+
171
+ body {
172
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
173
+ background: var(--color-background);
174
+ color: var(--color-dark);
175
+ line-height: 1.6;
176
+ }
177
+
178
+ .container {
179
+ max-width: 1200px;
180
+ margin: 0 auto;
181
+ padding: 0 20px;
182
+ }
183
+
184
+ /* Header */
185
+ header {
186
+ background: var(--color-card);
187
+ border-bottom: 1px solid var(--color-border);
188
+ padding: 16px 0;
189
+ position: sticky;
190
+ top: 0;
191
+ z-index: 1000;
192
+ }
193
+
194
+ .header-inner {
195
+ display: flex;
196
+ justify-content: space-between;
197
+ align-items: center;
198
+ gap: 24px;
199
+ }
200
+
201
+ .header-nav {
202
+ display: flex;
203
+ align-items: center;
204
+ flex: 1;
205
+ justify-content: center;
206
+ }
207
+
208
+ .logo {
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 12px;
212
+ font-size: 24px;
213
+ font-weight: 700;
214
+ text-decoration: none;
215
+ }
216
+
217
+ .logo-icon {
218
+ width: 40px;
219
+ height: 40px;
220
+ border-radius: 8px;
221
+ }
222
+
223
+ .logo-text {
224
+ background: var(--brand-gradient);
225
+ -webkit-background-clip: text;
226
+ -webkit-text-fill-color: transparent;
227
+ background-clip: text;
228
+ }
229
+
230
+ .logo-suffix {
231
+ font-weight: 400;
232
+ -webkit-text-fill-color: var(--color-medium);
233
+ }
234
+
235
+ nav {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 8px;
239
+ }
240
+
241
+ nav>a {
242
+ padding: 8px 12px;
243
+ color: var(--color-medium);
244
+ text-decoration: none;
245
+ transition: color 0.2s;
246
+ border-radius: 6px;
247
+ }
248
+
249
+ nav>a:hover {
250
+ color: var(--brand-blue);
251
+ background: rgba(46, 134, 171, 0.08);
252
+ }
253
+
254
+ /* Dropdown Menu */
255
+ .dropdown {
256
+ position: relative;
257
+ display: inline-block;
258
+ }
259
+
260
+ .dropdown-toggle {
261
+ color: var(--color-medium);
262
+ text-decoration: none;
263
+ cursor: pointer;
264
+ transition: all 0.2s;
265
+ background: none;
266
+ border: none;
267
+ font-size: inherit;
268
+ font-family: inherit;
269
+ padding: 8px 12px;
270
+ border-radius: 6px;
271
+ }
272
+
273
+ .dropdown-toggle:hover,
274
+ .dropdown:hover .dropdown-toggle {
275
+ color: var(--brand-blue);
276
+ background: rgba(46, 134, 171, 0.08);
277
+ }
278
+
279
+ .dropdown-toggle::after {
280
+ content: '▼';
281
+ font-size: 0.6em;
282
+ margin-left: 4px;
283
+ vertical-align: middle;
284
+ }
285
+
286
+ .dropdown-menu {
287
+ display: none;
288
+ position: absolute;
289
+ top: 100%;
290
+ left: 0;
291
+ background: var(--color-card);
292
+ border: 1px solid var(--color-border);
293
+ border-radius: 8px;
294
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
295
+ min-width: 180px;
296
+ z-index: 1001;
297
+ padding: 8px 0;
298
+ margin-top: 8px;
299
+ }
300
+
301
+ .dropdown:hover .dropdown-menu {
302
+ display: block;
303
+ }
304
+
305
+ .dropdown-menu a {
306
+ display: block;
307
+ padding: 10px 16px;
308
+ color: var(--color-medium);
309
+ text-decoration: none;
310
+ margin-left: 0;
311
+ transition: background 0.2s, color 0.2s;
312
+ }
313
+
314
+ .dropdown-menu a:hover {
315
+ background: rgba(46, 134, 171, 0.08);
316
+ color: var(--brand-blue);
317
+ }
318
+
319
+ .dropdown-divider {
320
+ height: 1px;
321
+ background: var(--color-border);
322
+ margin: 8px 0;
323
+ }
324
+
325
+ .ph-badge {
326
+ display: flex;
327
+ align-items: center;
328
+ flex-shrink: 0;
329
+ transition: transform 0.2s ease, opacity 0.2s ease;
330
+ }
331
+
332
+ .ph-badge:hover {
333
+ transform: translateY(-2px);
334
+ opacity: 0.9;
335
+ }
336
+
337
+ .ph-badge img {
338
+ height: 40px;
339
+ width: auto;
340
+ }
341
+
342
+ /* Hero */
343
+ .hero {
344
+ text-align: center;
345
+ padding: 60px 20px;
346
+ background: linear-gradient(135deg, rgba(46, 134, 171, 0.1) 0%, rgba(54, 181, 162, 0.05) 50%, rgba(71, 201, 122, 0.1) 100%);
347
+ }
348
+
349
+ .hero h1 {
350
+ font-size: 42px;
351
+ margin-bottom: 16px;
352
+ color: var(--color-dark);
353
+ }
354
+
355
+ .hero h1 .gradient-text {
356
+ background: var(--brand-gradient);
357
+ -webkit-background-clip: text;
358
+ -webkit-text-fill-color: transparent;
359
+ background-clip: text;
360
+ }
361
+
362
+ .hero p {
363
+ font-size: 18px;
364
+ color: var(--color-medium);
365
+ max-width: 600px;
366
+ margin: 0 auto 32px;
367
+ }
368
+
369
+ /* Tabs */
370
+ .tabs {
371
+ display: flex;
372
+ justify-content: center;
373
+ gap: 8px;
374
+ margin-bottom: 40px;
375
+ }
376
+
377
+ .tab {
378
+ padding: 12px 24px;
379
+ background: var(--color-card);
380
+ border: 2px solid var(--color-border);
381
+ border-radius: 8px;
382
+ cursor: pointer;
383
+ font-weight: 500;
384
+ transition: all 0.2s;
385
+ }
386
+
387
+ .tab:hover {
388
+ border-color: var(--brand-blue);
389
+ }
390
+
391
+ .tab.active {
392
+ background: var(--brand-gradient);
393
+ color: white;
394
+ border-color: transparent;
395
+ }
396
+
397
+ /* Cards */
398
+ .card {
399
+ background: var(--color-card);
400
+ border-radius: 12px;
401
+ padding: 32px;
402
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
403
+ border: 1px solid var(--color-border);
404
+ max-width: 700px;
405
+ margin: 0 auto 40px;
406
+ }
407
+
408
+ /* Forms */
409
+ .form-group {
410
+ margin-bottom: 20px;
411
+ }
412
+
413
+ label {
414
+ display: block;
415
+ font-weight: 500;
416
+ margin-bottom: 8px;
417
+ color: var(--color-dark);
418
+ }
419
+
420
+ input,
421
+ select,
422
+ textarea {
423
+ width: 100%;
424
+ padding: 12px 16px;
425
+ border: 2px solid var(--color-border);
426
+ border-radius: 8px;
427
+ font-size: 16px;
428
+ font-family: inherit;
429
+ transition: border-color 0.2s, box-shadow 0.2s;
430
+ }
431
+
432
+ input:focus,
433
+ select:focus,
434
+ textarea:focus {
435
+ outline: none;
436
+ border-color: var(--brand-blue);
437
+ box-shadow: 0 0 0 3px rgba(46, 134, 171, 0.1);
438
+ }
439
+
440
+ /* Buttons */
441
+ .btn {
442
+ display: inline-block;
443
+ padding: 14px 28px;
444
+ background: var(--brand-gradient);
445
+ color: white;
446
+ border: none;
447
+ border-radius: 8px;
448
+ font-size: 16px;
449
+ font-weight: 600;
450
+ cursor: pointer;
451
+ transition: transform 0.2s, box-shadow 0.2s;
452
+ text-decoration: none;
453
+ }
454
+
455
+ .btn:hover {
456
+ transform: translateY(-1px);
457
+ box-shadow: 0 4px 12px rgba(46, 134, 171, 0.4);
458
+ }
459
+
460
+ .btn:active {
461
+ transform: translateY(0);
462
+ }
463
+
464
+ .btn:disabled {
465
+ opacity: 0.6;
466
+ cursor: not-allowed;
467
+ transform: none;
468
+ }
469
+
470
+ .btn-secondary {
471
+ background: var(--color-card);
472
+ color: var(--color-dark);
473
+ border: 2px solid var(--color-border);
474
+ }
475
+
476
+ .btn-secondary:hover {
477
+ background: var(--color-background);
478
+ border-color: var(--brand-blue);
479
+ color: var(--brand-blue);
480
+ box-shadow: none;
481
+ transform: none;
482
+ }
483
+
484
+ /* Results */
485
+ .result {
486
+ margin-top: 24px;
487
+ padding: 20px;
488
+ border-radius: 8px;
489
+ display: none;
490
+ }
491
+
492
+ .result.show {
493
+ display: block;
494
+ }
495
+
496
+ .result.success {
497
+ background: #dcfce7;
498
+ border: 1px solid #86efac;
499
+ }
500
+
501
+ .result.error {
502
+ background: #fee2e2;
503
+ border: 1px solid #fca5a5;
504
+ }
505
+
506
+ /* Score Badges */
507
+ .score-badge {
508
+ display: inline-flex;
509
+ align-items: center;
510
+ gap: 8px;
511
+ padding: 8px 16px;
512
+ border-radius: 20px;
513
+ font-weight: 700;
514
+ font-size: 24px;
515
+ }
516
+
517
+ .score-badge.A {
518
+ background: var(--grade-a-bg);
519
+ color: var(--grade-a-text);
520
+ }
521
+
522
+ .score-badge.B {
523
+ background: var(--grade-b-bg);
524
+ color: var(--grade-b-text);
525
+ }
526
+
527
+ .score-badge.C {
528
+ background: var(--grade-c-bg);
529
+ color: var(--grade-c-text);
530
+ }
531
+
532
+ .score-badge.D {
533
+ background: var(--grade-d-bg);
534
+ color: var(--grade-d-text);
535
+ }
536
+
537
+ .score-badge.F {
538
+ background: var(--grade-f-bg);
539
+ color: var(--grade-f-text);
540
+ }
541
+
542
+ /* Issues */
543
+ .issues-list {
544
+ margin-top: 16px;
545
+ }
546
+
547
+ .issue {
548
+ padding: 12px;
549
+ margin-bottom: 8px;
550
+ border-radius: 6px;
551
+ font-size: 14px;
552
+ }
553
+
554
+ .issue.error {
555
+ background: #fef2f2;
556
+ border-left: 3px solid var(--color-error);
557
+ }
558
+
559
+ .issue.warn {
560
+ background: #fefce8;
561
+ border-left: 3px solid var(--color-warning);
562
+ }
563
+
564
+ .issue.info {
565
+ background: rgba(46, 134, 171, 0.1);
566
+ border-left: 3px solid var(--color-info);
567
+ }
568
+
569
+ .issue code {
570
+ background: rgba(0, 0, 0, 0.05);
571
+ padding: 2px 6px;
572
+ border-radius: 4px;
573
+ }
574
+
575
+ /* Fix Cards - Expandable Action Items */
576
+ .fix-card {
577
+ background: var(--color-card);
578
+ border: 1px solid var(--color-border);
579
+ border-radius: 10px;
580
+ margin-bottom: 12px;
581
+ overflow: hidden;
582
+ transition: box-shadow 0.2s, border-color 0.2s;
583
+ }
584
+
585
+ .fix-card:hover {
586
+ border-color: var(--brand-blue);
587
+ box-shadow: 0 2px 8px rgba(46, 134, 171, 0.1);
588
+ }
589
+
590
+ .fix-card.critical { border-left: 4px solid var(--color-error); }
591
+ .fix-card.warning { border-left: 4px solid var(--color-warning); }
592
+ .fix-card.info { border-left: 4px solid var(--color-info); }
593
+
594
+ .fix-card-header {
595
+ display: flex;
596
+ align-items: center;
597
+ gap: 12px;
598
+ padding: 14px 16px;
599
+ cursor: pointer;
600
+ user-select: none;
601
+ }
602
+
603
+ .fix-card-priority {
604
+ flex-shrink: 0;
605
+ }
606
+
607
+ .fix-number {
608
+ display: flex;
609
+ align-items: center;
610
+ justify-content: center;
611
+ width: 28px;
612
+ height: 28px;
613
+ border-radius: 50%;
614
+ background: var(--color-background);
615
+ color: var(--color-dark);
616
+ font-weight: 700;
617
+ font-size: 13px;
618
+ }
619
+
620
+ .fix-card.critical .fix-number { background: #fef2f2; color: #DC2626; }
621
+ .fix-card.warning .fix-number { background: #fefce8; color: #CA8A04; }
622
+ .fix-card.info .fix-number { background: #f0f9ff; color: #0369a1; }
623
+
624
+ .fix-card-content {
625
+ flex: 1;
626
+ min-width: 0;
627
+ }
628
+
629
+ .fix-card-title {
630
+ font-weight: 600;
631
+ font-size: 14px;
632
+ color: var(--color-dark);
633
+ margin-bottom: 4px;
634
+ }
635
+
636
+ .fix-card-meta {
637
+ display: flex;
638
+ gap: 8px;
639
+ flex-wrap: wrap;
640
+ }
641
+
642
+ .fix-severity-tag {
643
+ font-size: 10px;
644
+ font-weight: 600;
645
+ text-transform: uppercase;
646
+ padding: 2px 8px;
647
+ border-radius: 4px;
648
+ }
649
+
650
+ .fix-severity-tag.critical { background: #fef2f2; color: #DC2626; }
651
+ .fix-severity-tag.warning { background: #fefce8; color: #CA8A04; }
652
+ .fix-severity-tag.info { background: #f0f9ff; color: #0369a1; }
653
+
654
+ .fix-code-tag {
655
+ font-size: 10px;
656
+ font-family: monospace;
657
+ padding: 2px 8px;
658
+ background: var(--color-background);
659
+ color: var(--color-medium);
660
+ border-radius: 4px;
661
+ }
662
+
663
+ .fix-card-chevron {
664
+ flex-shrink: 0;
665
+ font-size: 14px;
666
+ color: var(--color-medium);
667
+ transition: transform 0.2s;
668
+ }
669
+
670
+ .fix-card.expanded .fix-card-chevron {
671
+ transform: rotate(180deg);
672
+ }
673
+
674
+ .fix-card-body {
675
+ display: none;
676
+ padding: 0 16px 16px;
677
+ border-top: 1px solid var(--color-border);
678
+ background: var(--color-background);
679
+ }
680
+
681
+ .fix-card.expanded .fix-card-body {
682
+ display: block;
683
+ }
684
+
685
+ .fix-card-impact {
686
+ padding: 12px 0;
687
+ font-size: 13px;
688
+ color: var(--color-medium);
689
+ font-style: italic;
690
+ }
691
+
692
+ .fix-card-instruction {
693
+ font-size: 14px;
694
+ color: var(--color-dark);
695
+ line-height: 1.6;
696
+ padding: 12px;
697
+ background: var(--color-card);
698
+ border-radius: 8px;
699
+ border: 1px solid var(--color-border);
700
+ }
701
+
702
+ .fix-code-block {
703
+ margin-top: 12px;
704
+ border-radius: 8px;
705
+ overflow: hidden;
706
+ border: 1px solid #334155;
707
+ }
708
+
709
+ .fix-code-header {
710
+ display: flex;
711
+ justify-content: space-between;
712
+ align-items: center;
713
+ padding: 8px 12px;
714
+ background: #334155;
715
+ font-size: 12px;
716
+ color: #94a3b8;
717
+ }
718
+
719
+ .fix-copy-btn {
720
+ background: rgba(255, 255, 255, 0.1);
721
+ border: none;
722
+ color: #94a3b8;
723
+ padding: 4px 10px;
724
+ border-radius: 4px;
725
+ cursor: pointer;
726
+ font-size: 11px;
727
+ transition: all 0.2s;
728
+ }
729
+
730
+ .fix-copy-btn:hover {
731
+ background: rgba(255, 255, 255, 0.2);
732
+ color: #fff;
733
+ }
734
+
735
+ .fix-copy-btn.copied {
736
+ background: #16a34a;
737
+ color: #fff;
738
+ }
739
+
740
+ .fix-code-block pre {
741
+ margin: 0;
742
+ padding: 12px;
743
+ background: #1e293b;
744
+ font-size: 12px;
745
+ line-height: 1.5;
746
+ overflow-x: auto;
747
+ }
748
+
749
+ .fix-code-block code {
750
+ color: #e2e8f0;
751
+ font-family: 'Fira Code', 'Consolas', monospace;
752
+ }
753
+
754
+ .fix-card-links {
755
+ display: flex;
756
+ gap: 8px;
757
+ margin-top: 12px;
758
+ flex-wrap: wrap;
759
+ }
760
+
761
+ .fix-link {
762
+ display: inline-flex;
763
+ align-items: center;
764
+ gap: 4px;
765
+ font-size: 12px;
766
+ color: var(--brand-blue);
767
+ text-decoration: none;
768
+ padding: 6px 12px;
769
+ background: var(--color-card);
770
+ border: 1px solid var(--color-border);
771
+ border-radius: 6px;
772
+ transition: all 0.2s;
773
+ }
774
+
775
+ .fix-link:hover {
776
+ background: rgba(46, 134, 171, 0.1);
777
+ border-color: var(--brand-blue);
778
+ }
779
+
780
+ /* Enhanced Results UI */
781
+ .result-header {
782
+ display: flex;
783
+ align-items: center;
784
+ gap: 20px;
785
+ margin-bottom: 24px;
786
+ padding-bottom: 20px;
787
+ border-bottom: 2px solid var(--color-border);
788
+ }
789
+
790
+ .result-grade-circle {
791
+ width: 90px;
792
+ height: 90px;
793
+ border-radius: 50%;
794
+ display: flex;
795
+ align-items: center;
796
+ justify-content: center;
797
+ font-size: 42px;
798
+ font-weight: 800;
799
+ flex-shrink: 0;
800
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
801
+ }
802
+
803
+ .result-grade-circle.grade-A { background: linear-gradient(135deg, #22c55e, #16a34a); color: white; }
804
+ .result-grade-circle.grade-B { background: linear-gradient(135deg, #3b82f6, #2563eb); color: white; }
805
+ .result-grade-circle.grade-C { background: linear-gradient(135deg, #eab308, #ca8a04); color: white; }
806
+ .result-grade-circle.grade-D { background: linear-gradient(135deg, #f97316, #ea580c); color: white; }
807
+ .result-grade-circle.grade-F { background: linear-gradient(135deg, #ef4444, #dc2626); color: white; }
808
+
809
+ .result-score-info {
810
+ flex: 1;
811
+ }
812
+
813
+ .result-score-info h3 {
814
+ font-size: 28px;
815
+ margin-bottom: 4px;
816
+ color: var(--color-dark);
817
+ }
818
+
819
+ .result-score-info .score-value {
820
+ font-size: 18px;
821
+ font-weight: 600;
822
+ color: var(--color-medium);
823
+ }
824
+
825
+ .result-badges {
826
+ display: flex;
827
+ gap: 8px;
828
+ flex-wrap: wrap;
829
+ margin-top: 8px;
830
+ }
831
+
832
+ .result-badge {
833
+ display: inline-flex;
834
+ align-items: center;
835
+ gap: 6px;
836
+ padding: 6px 12px;
837
+ border-radius: 20px;
838
+ font-size: 13px;
839
+ font-weight: 600;
840
+ }
841
+
842
+ .result-badge.success { background: #dcfce7; color: #16a34a; }
843
+ .result-badge.warning { background: #fef9c3; color: #ca8a04; }
844
+ .result-badge.error { background: #fee2e2; color: #dc2626; }
845
+ .result-badge.info { background: #dbeafe; color: #2563eb; }
846
+
847
+ .result-section {
848
+ background: var(--color-card);
849
+ border: 1px solid var(--color-border);
850
+ border-radius: 12px;
851
+ padding: 20px;
852
+ margin-bottom: 16px;
853
+ transition: box-shadow 0.2s;
854
+ }
855
+
856
+ .result-section:hover {
857
+ box-shadow: 0 4px 12px rgba(0,0,0,0.05);
858
+ }
859
+
860
+ .result-section-header {
861
+ display: flex;
862
+ align-items: center;
863
+ gap: 12px;
864
+ margin-bottom: 12px;
865
+ }
866
+
867
+ .result-section-icon {
868
+ width: 40px;
869
+ height: 40px;
870
+ border-radius: 10px;
871
+ display: flex;
872
+ align-items: center;
873
+ justify-content: center;
874
+ font-size: 20px;
875
+ flex-shrink: 0;
876
+ }
877
+
878
+ .result-section-icon.ucp { background: rgba(46, 134, 171, 0.15); }
879
+ .result-section-icon.schema { background: rgba(71, 201, 122, 0.15); }
880
+ .result-section-icon.product { background: rgba(245, 158, 11, 0.15); }
881
+ .result-section-icon.shipping { background: rgba(59, 130, 246, 0.15); }
882
+ .result-section-icon.compliance { background: rgba(139, 92, 246, 0.15); }
883
+
884
+ .result-section-title {
885
+ flex: 1;
886
+ }
887
+
888
+ .result-section-title h4 {
889
+ margin: 0;
890
+ font-size: 16px;
891
+ color: var(--color-dark);
892
+ }
893
+
894
+ .result-section-title p {
895
+ margin: 4px 0 0;
896
+ font-size: 13px;
897
+ color: var(--color-medium);
898
+ }
899
+
900
+ .result-section-status {
901
+ padding: 6px 12px;
902
+ border-radius: 8px;
903
+ font-size: 13px;
904
+ font-weight: 600;
905
+ }
906
+
907
+ .result-section-status.pass { background: #dcfce7; color: #16a34a; }
908
+ .result-section-status.warn { background: #fef9c3; color: #ca8a04; }
909
+ .result-section-status.fail { background: #fee2e2; color: #dc2626; }
910
+
911
+ .result-stats-row {
912
+ display: flex;
913
+ gap: 24px;
914
+ margin-top: 12px;
915
+ padding-top: 12px;
916
+ border-top: 1px solid var(--color-border);
917
+ }
918
+
919
+ .result-stat {
920
+ text-align: center;
921
+ }
922
+
923
+ .result-stat-value {
924
+ font-size: 24px;
925
+ font-weight: 700;
926
+ color: var(--color-dark);
927
+ }
928
+
929
+ .result-stat-label {
930
+ font-size: 12px;
931
+ color: var(--color-medium);
932
+ }
933
+
934
+ .benchmark-card {
935
+ background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(168,85,247,0.08));
936
+ border: 1px solid rgba(99,102,241,0.2);
937
+ border-radius: 12px;
938
+ padding: 20px;
939
+ margin-bottom: 20px;
940
+ }
941
+
942
+ .benchmark-card h4 {
943
+ margin: 0 0 12px;
944
+ font-size: 14px;
945
+ color: var(--color-medium);
946
+ text-transform: uppercase;
947
+ letter-spacing: 0.5px;
948
+ }
949
+
950
+ .benchmark-main {
951
+ display: flex;
952
+ align-items: baseline;
953
+ gap: 8px;
954
+ margin-bottom: 8px;
955
+ }
956
+
957
+ .benchmark-percentile {
958
+ font-size: 36px;
959
+ font-weight: 800;
960
+ color: #6366f1;
961
+ }
962
+
963
+ .benchmark-label {
964
+ font-size: 14px;
965
+ color: var(--color-medium);
966
+ }
967
+
968
+ .benchmark-details {
969
+ font-size: 13px;
970
+ color: var(--color-medium);
971
+ }
972
+
973
+ .action-cards {
974
+ display: grid;
975
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
976
+ gap: 16px;
977
+ margin-top: 24px;
978
+ }
979
+
980
+ .action-card {
981
+ border-radius: 12px;
982
+ padding: 24px;
983
+ text-align: center;
984
+ border: 1px solid var(--color-border);
985
+ }
986
+
987
+ .action-card.badge-card {
988
+ background: linear-gradient(135deg, rgba(46,134,171,0.08), rgba(71,201,122,0.08));
989
+ border-color: rgba(46,134,171,0.2);
990
+ }
991
+
992
+ .action-card.report-card {
993
+ background: linear-gradient(135deg, rgba(139,92,246,0.08), rgba(236,72,153,0.08));
994
+ border-color: rgba(139,92,246,0.2);
995
+ }
996
+
997
+ .action-card h4 {
998
+ display: flex;
999
+ align-items: center;
1000
+ justify-content: center;
1001
+ gap: 8px;
1002
+ margin-bottom: 12px;
1003
+ font-size: 16px;
1004
+ }
1005
+
1006
+ .action-card p {
1007
+ font-size: 13px;
1008
+ color: var(--color-medium);
1009
+ margin-bottom: 16px;
1010
+ }
1011
+
1012
+ /* Code Block */
1013
+ pre {
1014
+ background: var(--color-dark);
1015
+ color: #e2e8f0;
1016
+ padding: 20px;
1017
+ border-radius: 8px;
1018
+ overflow-x: auto;
1019
+ font-size: 13px;
1020
+ line-height: 1.5;
1021
+ text-align: left;
1022
+ white-space: pre-wrap;
1023
+ word-break: break-word;
1024
+ }
1025
+
1026
+ /* Pricing Section */
1027
+ .pricing {
1028
+ padding: 60px 20px;
1029
+ background: var(--color-card);
1030
+ }
1031
+
1032
+ .pricing h2 {
1033
+ text-align: center;
1034
+ margin-bottom: 40px;
1035
+ color: var(--color-dark);
1036
+ }
1037
+
1038
+ .pricing-grid {
1039
+ display: grid;
1040
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1041
+ gap: 24px;
1042
+ max-width: 900px;
1043
+ margin: 0 auto;
1044
+ }
1045
+
1046
+ .pricing-card {
1047
+ border: 2px solid var(--color-border);
1048
+ border-radius: 12px;
1049
+ padding: 32px;
1050
+ text-align: center;
1051
+ background: var(--color-card);
1052
+ }
1053
+
1054
+ .pricing-card.featured {
1055
+ border-color: var(--brand-blue);
1056
+ position: relative;
1057
+ }
1058
+
1059
+ .pricing-card.featured::before {
1060
+ content: 'Most Popular';
1061
+ position: absolute;
1062
+ top: -12px;
1063
+ left: 50%;
1064
+ transform: translateX(-50%);
1065
+ background: var(--brand-gradient);
1066
+ color: white;
1067
+ padding: 4px 16px;
1068
+ border-radius: 20px;
1069
+ font-size: 12px;
1070
+ font-weight: 600;
1071
+ }
1072
+
1073
+ .pricing-card h3 {
1074
+ margin-bottom: 8px;
1075
+ color: var(--color-dark);
1076
+ }
1077
+
1078
+ .price {
1079
+ font-size: 48px;
1080
+ font-weight: 700;
1081
+ background: var(--brand-gradient);
1082
+ -webkit-background-clip: text;
1083
+ -webkit-text-fill-color: transparent;
1084
+ background-clip: text;
1085
+ }
1086
+
1087
+ .price span {
1088
+ font-size: 16px;
1089
+ -webkit-text-fill-color: var(--color-medium);
1090
+ font-weight: 400;
1091
+ }
1092
+
1093
+ .pricing-card ul {
1094
+ list-style: none;
1095
+ margin: 24px 0;
1096
+ text-align: left;
1097
+ }
1098
+
1099
+ .pricing-card li {
1100
+ padding: 8px 0;
1101
+ border-bottom: 1px solid var(--color-border);
1102
+ color: var(--color-dark);
1103
+ }
1104
+
1105
+ .pricing-card li::before {
1106
+ content: '\2713';
1107
+ color: var(--color-success);
1108
+ margin-right: 8px;
1109
+ font-weight: bold;
1110
+ }
1111
+
1112
+ /* Footer */
1113
+ footer {
1114
+ padding: 40px 20px;
1115
+ text-align: center;
1116
+ color: var(--color-medium);
1117
+ border-top: 1px solid var(--color-border);
1118
+ background: var(--color-card);
1119
+ }
1120
+
1121
+ footer a {
1122
+ color: var(--brand-blue);
1123
+ text-decoration: none;
1124
+ }
1125
+
1126
+ footer a:hover {
1127
+ color: var(--brand-teal);
1128
+ }
1129
+
1130
+ /* Utilities */
1131
+ .hidden {
1132
+ display: none !important;
1133
+ }
1134
+
1135
+ .text-muted {
1136
+ color: var(--color-medium);
1137
+ }
1138
+
1139
+ .link-brand {
1140
+ color: var(--brand-blue);
1141
+ text-decoration: none;
1142
+ }
1143
+
1144
+ .link-brand:hover {
1145
+ color: var(--brand-teal);
1146
+ }
1147
+
1148
+ /* Responsive */
1149
+ @media (max-width: 900px) {
1150
+ .header-inner {
1151
+ flex-wrap: wrap;
1152
+ }
1153
+
1154
+ .header-nav {
1155
+ order: 3;
1156
+ width: 100%;
1157
+ margin-top: 12px;
1158
+ }
1159
+
1160
+ .ph-badge {
1161
+ order: 2;
1162
+ }
1163
+ }
1164
+
1165
+ @media (max-width: 600px) {
1166
+ .hero h1 {
1167
+ font-size: 28px;
1168
+ }
1169
+
1170
+ .tabs {
1171
+ flex-direction: column;
1172
+ }
1173
+
1174
+ .header-inner {
1175
+ flex-direction: column;
1176
+ gap: 16px;
1177
+ }
1178
+
1179
+ .header-nav {
1180
+ order: 2;
1181
+ margin-top: 0;
1182
+ }
1183
+
1184
+ .ph-badge {
1185
+ order: 3;
1186
+ }
1187
+
1188
+ nav {
1189
+ display: flex;
1190
+ flex-wrap: wrap;
1191
+ justify-content: center;
1192
+ gap: 4px;
1193
+ }
1194
+
1195
+ nav>a,
1196
+ .dropdown-toggle {
1197
+ padding: 6px 10px;
1198
+ font-size: 14px;
1199
+ }
1200
+ }
1201
+
1202
+ /* ========================================
1203
+ NEW REPORT UI - Complete Rebuild
1204
+ ======================================== */
1205
+
1206
+ /* Report Header - Executive Summary */
1207
+ .report-header {
1208
+ display: flex;
1209
+ align-items: center;
1210
+ gap: 24px;
1211
+ padding: 28px;
1212
+ background: var(--color-card);
1213
+ border-radius: 16px;
1214
+ border: 1px solid var(--color-border);
1215
+ margin-bottom: 20px;
1216
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
1217
+ }
1218
+
1219
+ .report-grade {
1220
+ width: 80px;
1221
+ height: 80px;
1222
+ border-radius: 50%;
1223
+ display: flex;
1224
+ align-items: center;
1225
+ justify-content: center;
1226
+ font-size: 40px;
1227
+ font-weight: 800;
1228
+ color: white;
1229
+ flex-shrink: 0;
1230
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1231
+ }
1232
+
1233
+ .report-grade.grade-A { background: linear-gradient(135deg, #22c55e, #16a34a); }
1234
+ .report-grade.grade-B { background: linear-gradient(135deg, #3b82f6, #2563eb); }
1235
+ .report-grade.grade-C { background: linear-gradient(135deg, #eab308, #ca8a04); }
1236
+ .report-grade.grade-D { background: linear-gradient(135deg, #f97316, #ea580c); }
1237
+ .report-grade.grade-F { background: linear-gradient(135deg, #ef4444, #dc2626); }
1238
+
1239
+ .report-score-info {
1240
+ flex: 1;
1241
+ }
1242
+
1243
+ .report-score {
1244
+ font-size: 42px;
1245
+ font-weight: 800;
1246
+ color: var(--color-dark);
1247
+ line-height: 1;
1248
+ }
1249
+
1250
+ .report-score span {
1251
+ font-size: 18px;
1252
+ font-weight: 500;
1253
+ color: var(--color-medium);
1254
+ }
1255
+
1256
+ .report-label {
1257
+ font-size: 16px;
1258
+ font-weight: 600;
1259
+ margin-top: 4px;
1260
+ }
1261
+
1262
+ .report-benchmark {
1263
+ text-align: right;
1264
+ padding: 16px 20px;
1265
+ background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(168,85,247,0.08));
1266
+ border-radius: 12px;
1267
+ border: 1px solid rgba(99,102,241,0.15);
1268
+ }
1269
+
1270
+ .report-benchmark-value {
1271
+ font-size: 28px;
1272
+ font-weight: 800;
1273
+ color: #6366f1;
1274
+ }
1275
+
1276
+ .report-benchmark-label {
1277
+ font-size: 12px;
1278
+ color: var(--color-medium);
1279
+ }
1280
+
1281
+ /* Quick Stats Row */
1282
+ .report-stats {
1283
+ display: grid;
1284
+ grid-template-columns: repeat(4, 1fr);
1285
+ gap: 12px;
1286
+ margin-bottom: 20px;
1287
+ }
1288
+
1289
+ .report-stat {
1290
+ display: flex;
1291
+ align-items: center;
1292
+ gap: 12px;
1293
+ padding: 16px;
1294
+ background: var(--color-card);
1295
+ border-radius: 12px;
1296
+ border: 1px solid var(--color-border);
1297
+ transition: all 0.2s;
1298
+ }
1299
+
1300
+ .report-stat:hover {
1301
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
1302
+ }
1303
+
1304
+ .report-stat.pass { border-left: 4px solid #22c55e; }
1305
+ .report-stat.warn { border-left: 4px solid #eab308; }
1306
+ .report-stat.fail { border-left: 4px solid #ef4444; }
1307
+
1308
+ .report-stat-icon {
1309
+ width: 36px;
1310
+ height: 36px;
1311
+ border-radius: 50%;
1312
+ display: flex;
1313
+ align-items: center;
1314
+ justify-content: center;
1315
+ font-size: 16px;
1316
+ font-weight: 700;
1317
+ flex-shrink: 0;
1318
+ }
1319
+
1320
+ .report-stat.pass .report-stat-icon { background: #dcfce7; color: #16a34a; }
1321
+ .report-stat.warn .report-stat-icon { background: #fef9c3; color: #ca8a04; }
1322
+ .report-stat.fail .report-stat-icon { background: #fee2e2; color: #dc2626; }
1323
+
1324
+ .report-stat-text {
1325
+ font-size: 14px;
1326
+ font-weight: 600;
1327
+ color: var(--color-dark);
1328
+ }
1329
+
1330
+ /* Collapsible Report Sections */
1331
+ .report-section {
1332
+ background: var(--color-card);
1333
+ border: 1px solid var(--color-border);
1334
+ border-radius: 12px;
1335
+ margin-bottom: 12px;
1336
+ overflow: hidden;
1337
+ transition: box-shadow 0.2s;
1338
+ }
1339
+
1340
+ .report-section:hover {
1341
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1342
+ }
1343
+
1344
+ .report-section-header {
1345
+ display: flex;
1346
+ align-items: center;
1347
+ gap: 14px;
1348
+ padding: 16px 20px;
1349
+ cursor: pointer;
1350
+ user-select: none;
1351
+ transition: background 0.2s;
1352
+ }
1353
+
1354
+ .report-section-header:hover {
1355
+ background: var(--color-background);
1356
+ }
1357
+
1358
+ .report-section-status {
1359
+ width: 32px;
1360
+ height: 32px;
1361
+ border-radius: 8px;
1362
+ display: flex;
1363
+ align-items: center;
1364
+ justify-content: center;
1365
+ font-size: 14px;
1366
+ font-weight: 700;
1367
+ flex-shrink: 0;
1368
+ }
1369
+
1370
+ .report-section-status.pass { background: #dcfce7; color: #16a34a; }
1371
+ .report-section-status.warn { background: #fef9c3; color: #ca8a04; }
1372
+ .report-section-status.fail { background: #fee2e2; color: #dc2626; }
1373
+
1374
+ .report-section-title {
1375
+ flex: 1;
1376
+ min-width: 0;
1377
+ }
1378
+
1379
+ .report-section-title strong {
1380
+ display: block;
1381
+ font-size: 15px;
1382
+ color: var(--color-dark);
1383
+ }
1384
+
1385
+ .report-section-title span {
1386
+ font-size: 13px;
1387
+ color: var(--color-medium);
1388
+ }
1389
+
1390
+ .report-section-toggle {
1391
+ font-size: 12px;
1392
+ color: var(--color-light);
1393
+ transition: transform 0.2s;
1394
+ }
1395
+
1396
+ .report-section.collapsed .report-section-toggle {
1397
+ transform: rotate(-90deg);
1398
+ }
1399
+
1400
+ .report-section-body {
1401
+ padding: 0 20px 20px;
1402
+ border-top: 1px solid var(--color-border);
1403
+ background: var(--color-background);
1404
+ }
1405
+
1406
+ .report-section.collapsed .report-section-body {
1407
+ display: none;
1408
+ }
1409
+
1410
+ .report-schema-stats {
1411
+ display: flex;
1412
+ gap: 24px;
1413
+ padding: 16px 0;
1414
+ margin-bottom: 12px;
1415
+ border-bottom: 1px solid var(--color-border);
1416
+ font-size: 14px;
1417
+ color: var(--color-medium);
1418
+ }
1419
+
1420
+ .report-schema-stats strong {
1421
+ color: var(--color-dark);
1422
+ font-weight: 700;
1423
+ }
1424
+
1425
+ .report-empty-state {
1426
+ text-align: center;
1427
+ padding: 32px 20px;
1428
+ }
1429
+
1430
+ .report-empty-state p {
1431
+ margin-bottom: 16px;
1432
+ color: var(--color-medium);
1433
+ }
1434
+
1435
+ /* Issue Items */
1436
+ .issue-item {
1437
+ background: var(--color-card);
1438
+ border: 1px solid var(--color-border);
1439
+ border-radius: 10px;
1440
+ margin-top: 12px;
1441
+ overflow: hidden;
1442
+ transition: border-color 0.2s, box-shadow 0.2s;
1443
+ }
1444
+
1445
+ .issue-item:hover {
1446
+ border-color: var(--brand-blue);
1447
+ }
1448
+
1449
+ .issue-item-header {
1450
+ display: flex;
1451
+ align-items: center;
1452
+ gap: 10px;
1453
+ padding: 14px 16px;
1454
+ cursor: pointer;
1455
+ flex-wrap: wrap;
1456
+ }
1457
+
1458
+ .issue-severity-dot {
1459
+ width: 10px;
1460
+ height: 10px;
1461
+ border-radius: 50%;
1462
+ flex-shrink: 0;
1463
+ }
1464
+
1465
+ .issue-severity-dot.error { background: #ef4444; }
1466
+ .issue-severity-dot.warn { background: #eab308; }
1467
+ .issue-severity-dot.info { background: #3b82f6; }
1468
+
1469
+ .issue-code {
1470
+ font-family: 'Fira Code', 'Consolas', monospace;
1471
+ font-size: 11px;
1472
+ padding: 3px 8px;
1473
+ background: var(--color-background);
1474
+ color: var(--color-medium);
1475
+ border-radius: 4px;
1476
+ flex-shrink: 0;
1477
+ }
1478
+
1479
+ .issue-message {
1480
+ flex: 1;
1481
+ font-size: 14px;
1482
+ color: var(--color-dark);
1483
+ min-width: 0;
1484
+ }
1485
+
1486
+ .issue-has-fix {
1487
+ font-size: 12px;
1488
+ color: var(--brand-blue);
1489
+ font-weight: 600;
1490
+ flex-shrink: 0;
1491
+ }
1492
+
1493
+ .issue-context {
1494
+ padding: 4px 16px 8px;
1495
+ font-size: 12px;
1496
+ color: var(--brand-blue);
1497
+ font-weight: 600;
1498
+ }
1499
+
1500
+ .issue-hint {
1501
+ padding: 0 16px 14px;
1502
+ font-size: 13px;
1503
+ color: var(--color-medium);
1504
+ font-style: italic;
1505
+ }
1506
+
1507
+ .report-ucp-summary {
1508
+ padding: 16px;
1509
+ margin-bottom: 12px;
1510
+ background: rgba(46, 134, 171, 0.08);
1511
+ border-radius: 8px;
1512
+ border-left: 4px solid var(--brand-blue);
1513
+ }
1514
+
1515
+ .report-ucp-summary p {
1516
+ margin: 0;
1517
+ font-size: 14px;
1518
+ color: var(--color-dark);
1519
+ }
1520
+
1521
+ /* Issue Fix Panel (Inline Expandable) */
1522
+ .issue-fix-panel {
1523
+ display: none;
1524
+ padding: 16px;
1525
+ background: linear-gradient(to bottom, rgba(46,134,171,0.04), rgba(46,134,171,0.08));
1526
+ border-top: 1px solid var(--color-border);
1527
+ }
1528
+
1529
+ .issue-item.expanded .issue-fix-panel {
1530
+ display: block;
1531
+ }
1532
+
1533
+ .issue-fix-title {
1534
+ font-size: 14px;
1535
+ font-weight: 600;
1536
+ color: var(--color-dark);
1537
+ margin-bottom: 8px;
1538
+ }
1539
+
1540
+ .issue-fix-description {
1541
+ font-size: 14px;
1542
+ color: var(--color-dark);
1543
+ line-height: 1.6;
1544
+ margin-bottom: 12px;
1545
+ }
1546
+
1547
+ .issue-fix-code {
1548
+ border-radius: 8px;
1549
+ overflow: hidden;
1550
+ border: 1px solid #334155;
1551
+ margin-bottom: 12px;
1552
+ }
1553
+
1554
+ .issue-fix-code-header {
1555
+ display: flex;
1556
+ justify-content: space-between;
1557
+ align-items: center;
1558
+ padding: 8px 12px;
1559
+ background: #334155;
1560
+ font-size: 12px;
1561
+ color: #94a3b8;
1562
+ }
1563
+
1564
+ .issue-copy-btn {
1565
+ background: rgba(255, 255, 255, 0.1);
1566
+ border: none;
1567
+ color: #94a3b8;
1568
+ padding: 4px 10px;
1569
+ border-radius: 4px;
1570
+ cursor: pointer;
1571
+ font-size: 11px;
1572
+ transition: all 0.2s;
1573
+ }
1574
+
1575
+ .issue-copy-btn:hover {
1576
+ background: rgba(255, 255, 255, 0.2);
1577
+ color: #fff;
1578
+ }
1579
+
1580
+ .issue-copy-btn.copied {
1581
+ background: #16a34a;
1582
+ color: #fff;
1583
+ }
1584
+
1585
+ .issue-fix-code pre {
1586
+ margin: 0;
1587
+ padding: 12px;
1588
+ background: #1e293b;
1589
+ font-size: 12px;
1590
+ line-height: 1.5;
1591
+ overflow-x: auto;
1592
+ }
1593
+
1594
+ .issue-fix-code code {
1595
+ color: #e2e8f0;
1596
+ font-family: 'Fira Code', 'Consolas', monospace;
1597
+ }
1598
+
1599
+ .issue-fix-links {
1600
+ display: flex;
1601
+ gap: 8px;
1602
+ flex-wrap: wrap;
1603
+ }
1604
+
1605
+ .issue-fix-links a {
1606
+ display: inline-flex;
1607
+ align-items: center;
1608
+ gap: 4px;
1609
+ font-size: 12px;
1610
+ color: var(--brand-blue);
1611
+ text-decoration: none;
1612
+ padding: 6px 12px;
1613
+ background: var(--color-card);
1614
+ border: 1px solid var(--color-border);
1615
+ border-radius: 6px;
1616
+ transition: all 0.2s;
1617
+ }
1618
+
1619
+ .issue-fix-links a:hover {
1620
+ background: rgba(46, 134, 171, 0.1);
1621
+ border-color: var(--brand-blue);
1622
+ }
1623
+
1624
+ /* Report Actions */
1625
+ .report-actions {
1626
+ display: flex;
1627
+ gap: 12px;
1628
+ flex-wrap: wrap;
1629
+ padding: 24px;
1630
+ background: var(--color-card);
1631
+ border-radius: 12px;
1632
+ border: 1px solid var(--color-border);
1633
+ margin-top: 20px;
1634
+ }
1635
+
1636
+ /* Responsive for Report UI */
1637
+ @media (max-width: 768px) {
1638
+ .report-header {
1639
+ flex-direction: column;
1640
+ text-align: center;
1641
+ gap: 16px;
1642
+ }
1643
+
1644
+ .report-benchmark {
1645
+ text-align: center;
1646
+ }
1647
+
1648
+ .report-stats {
1649
+ grid-template-columns: repeat(2, 1fr);
1650
+ }
1651
+
1652
+ .report-actions {
1653
+ justify-content: center;
1654
+ }
1655
+
1656
+ .issue-item-header {
1657
+ flex-wrap: wrap;
1658
+ }
1659
+
1660
+ .issue-message {
1661
+ width: 100%;
1662
+ order: 3;
1663
+ margin-top: 8px;
1664
+ }
1665
+ }
1666
+
1667
+ @media (max-width: 480px) {
1668
+ .report-stats {
1669
+ grid-template-columns: 1fr;
1670
+ }
1671
+
1672
+ .report-actions {
1673
+ flex-direction: column;
1674
+ }
1675
+
1676
+ .report-actions .btn {
1677
+ width: 100%;
1678
+ text-align: center;
1679
+ }
1680
+ }
1681
+ </style>
1682
+ </head>
1683
+
1684
+ <body>
1685
+ <header>
1686
+ <div class="container header-inner">
1687
+ <a href="/" class="logo">
1688
+ <img src="/logo.jpeg" alt="UCP.tools logo" class="logo-icon">
1689
+ <span class="logo-text">UCP<span class="logo-suffix">.tools</span></span>
1690
+ </a>
1691
+ <div class="header-nav">
1692
+ <nav>
1693
+ <div class="dropdown">
1694
+ <span class="dropdown-toggle">Tools</span>
1695
+ <div class="dropdown-menu">
1696
+ <a href="#validate" onclick="switchTab('validate'); closeDropdowns(); return false;">🔍 Validator</a>
1697
+ <a href="#simulate" onclick="switchTab('simulate'); closeDropdowns(); return false;">🤖 AI Simulator</a>
1698
+ <a href="#generate" onclick="switchTab('generate'); closeDropdowns(); return false;">📝 Generator</a>
1699
+ <a href="#schema" onclick="switchTab('schema'); closeDropdowns(); return false;">📊 Schema Builder</a>
1700
+ </div>
1701
+ </div>
1702
+ <a href="/learn">Learn</a>
1703
+ <div class="dropdown">
1704
+ <span class="dropdown-toggle">Guides</span>
1705
+ <div class="dropdown-menu">
1706
+ <a href="/guides/fastucp">⚡ FastUCP</a>
1707
+ <div class="dropdown-divider"></div>
1708
+ <a href="/guides/shopify">🛒 Shopify</a>
1709
+ <a href="/guides/woocommerce">🔌 WooCommerce</a>
1710
+ <a href="/guides/magento">🧱 Magento</a>
1711
+ <a href="/guides/wix">✨ Wix</a>
1712
+ <a href="/guides/bigcommerce">📦 BigCommerce</a>
1713
+ <a href="/guides/squarespace">◼️ Squarespace</a>
1714
+ </div>
1715
+ </div>
1716
+ <a href="/directory">Directory</a>
1717
+ <a href="https://ucp.dev/specification/overview/" target="_blank">Docs ↗</a>
1718
+ </nav>
1719
+ </div>
1720
+ <a href="https://www.producthunt.com/products/ucp-tools?embed=true&amp;utm_source=badge-featured&amp;utm_medium=badge&amp;utm_campaign=badge-ucp-tools"
1721
+ target="_blank" rel="noopener noreferrer" class="ph-badge">
1722
+ <img
1723
+ src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1063017&amp;theme=light&amp;t=1768475714634"
1724
+ alt="Featured on Product Hunt" />
1725
+ </a>
1726
+ </div>
1727
+ </header>
1728
+
1729
+ <section class="hero">
1730
+ <div class="container">
1731
+ <h1>AI Commerce <span class="gradient-text">Readiness</span></h1>
1732
+ <p>Check if your store is ready for ChatGPT, Google AI Mode, and Copilot checkout. Validates UCP profile +
1733
+ Schema.org requirements.</p>
1734
+
1735
+ <div class="tabs">
1736
+ <button class="tab active" data-tab="validate">Check Readiness</button>
1737
+ <button class="tab" data-tab="simulate">🤖 AI Agent Test</button>
1738
+ <button class="tab" data-tab="security">🔒 Security Scan</button>
1739
+ <button class="tab" data-tab="feed">📦 Feed Analyzer</button>
1740
+ <button class="tab" data-tab="compliance">📋 GDPR Generator</button>
1741
+ <button class="tab" data-tab="generate">Generate UCP</button>
1742
+ <button class="tab" data-tab="schema">Schema Snippets</button>
1743
+ </div>
1744
+
1745
+ <!-- Validate Tab -->
1746
+ <div class="card" id="validate-panel">
1747
+ <h3 style="margin-bottom: 20px;">Check AI Commerce Readiness</h3>
1748
+ <form id="validate-form">
1749
+ <div class="form-group">
1750
+ <label for="domain">Your Domain</label>
1751
+ <input type="text" id="domain" placeholder="yourstore.com" required>
1752
+ <small class="text-muted">We'll check UCP profile + Schema.org (MerchantReturnPolicy,
1753
+ shippingDetails)</small>
1754
+ </div>
1755
+ <button type="submit" class="btn" id="validate-btn">Check AI Readiness</button>
1756
+ </form>
1757
+
1758
+ <div class="result" id="validate-result"></div>
1759
+ </div>
1760
+
1761
+ <!-- Generate Tab -->
1762
+ <div class="card hidden" id="generate-panel">
1763
+ <h3 style="margin-bottom: 20px;">Generate UCP Profile</h3>
1764
+ <form id="generate-form">
1765
+ <div class="form-group">
1766
+ <label for="gen-domain">Your Domain</label>
1767
+ <input type="text" id="gen-domain" placeholder="yourstore.com" required>
1768
+ </div>
1769
+ <div class="form-group">
1770
+ <label for="gen-endpoint">REST API Endpoint</label>
1771
+ <input type="url" id="gen-endpoint" placeholder="https://api.yourstore.com/ucp/v1" required>
1772
+ </div>
1773
+ <div class="form-group">
1774
+ <label>Capabilities</label>
1775
+ <div style="display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;">
1776
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
1777
+ <input type="checkbox" id="cap-checkout" checked disabled> Checkout (required)
1778
+ </label>
1779
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
1780
+ <input type="checkbox" id="cap-order"> Order
1781
+ </label>
1782
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
1783
+ <input type="checkbox" id="cap-fulfillment"> Fulfillment
1784
+ </label>
1785
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
1786
+ <input type="checkbox" id="cap-discount"> Discount
1787
+ </label>
1788
+ </div>
1789
+ </div>
1790
+ <button type="submit" class="btn" id="generate-btn">Generate Profile</button>
1791
+ </form>
1792
+
1793
+ <div class="result" id="generate-result"></div>
1794
+ </div>
1795
+
1796
+ <!-- Schema Snippets Tab -->
1797
+ <div class="card hidden" id="schema-panel">
1798
+ <h3 style="margin-bottom: 20px;">Generate Schema.org Snippets</h3>
1799
+ <p style="margin-bottom: 24px; color: var(--color-medium);">
1800
+ Generate ready-to-use JSON-LD snippets for AI Commerce requirements (Jan 2026).
1801
+ </p>
1802
+
1803
+ <div class="form-group">
1804
+ <label>Return Policy Template</label>
1805
+ <select id="return-template">
1806
+ <option value="30-day-free">30-Day Free Returns (Recommended)</option>
1807
+ <option value="14-day-free">14-Day Free Returns</option>
1808
+ <option value="30-day-paid">30-Day Returns (Customer Pays Shipping)</option>
1809
+ <option value="no-returns">No Returns / Final Sale</option>
1810
+ <option value="custom">Custom...</option>
1811
+ </select>
1812
+ </div>
1813
+
1814
+ <div id="custom-return" class="hidden"
1815
+ style="background: var(--color-background); padding: 16px; border-radius: 8px; margin-bottom: 20px;">
1816
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
1817
+ <div class="form-group" style="margin-bottom: 0;">
1818
+ <label style="font-size: 13px;">Return Window (days)</label>
1819
+ <input type="number" id="return-days" value="30" min="0" max="365">
1820
+ </div>
1821
+ <div class="form-group" style="margin-bottom: 0;">
1822
+ <label style="font-size: 13px;">Country Code</label>
1823
+ <input type="text" id="return-country" value="US" maxlength="2">
1824
+ </div>
1825
+ </div>
1826
+ <div class="form-group" style="margin-top: 12px; margin-bottom: 0;">
1827
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal; font-size: 13px;">
1828
+ <input type="checkbox" id="free-returns" checked> Free returns (merchant pays shipping)
1829
+ </label>
1830
+ </div>
1831
+ </div>
1832
+
1833
+ <div class="form-group">
1834
+ <label>Shipping Template</label>
1835
+ <select id="shipping-template">
1836
+ <option value="us-standard">US Standard ($5.99, 3-7 days)</option>
1837
+ <option value="us-free">US Free Shipping (5-10 days)</option>
1838
+ <option value="us-express">US Express ($14.99, 1-2 days)</option>
1839
+ <option value="international">International ($19.99, 7-21 days)</option>
1840
+ <option value="custom">Custom...</option>
1841
+ </select>
1842
+ </div>
1843
+
1844
+ <div id="custom-shipping" class="hidden"
1845
+ style="background: var(--color-background); padding: 16px; border-radius: 8px; margin-bottom: 20px;">
1846
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px;">
1847
+ <div class="form-group" style="margin-bottom: 0;">
1848
+ <label style="font-size: 13px;">Rate</label>
1849
+ <input type="number" id="shipping-rate" value="5.99" step="0.01" min="0">
1850
+ </div>
1851
+ <div class="form-group" style="margin-bottom: 0;">
1852
+ <label style="font-size: 13px;">Currency</label>
1853
+ <input type="text" id="shipping-currency" value="USD" maxlength="3">
1854
+ </div>
1855
+ <div class="form-group" style="margin-bottom: 0;">
1856
+ <label style="font-size: 13px;">Country</label>
1857
+ <input type="text" id="shipping-country" value="US" maxlength="2">
1858
+ </div>
1859
+ </div>
1860
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px;">
1861
+ <div class="form-group" style="margin-bottom: 0;">
1862
+ <label style="font-size: 13px;">Transit Time (min-max days)</label>
1863
+ <div style="display: flex; gap: 8px;">
1864
+ <input type="number" id="transit-min" value="3" min="0" style="width: 50%;">
1865
+ <input type="number" id="transit-max" value="7" min="0" style="width: 50%;">
1866
+ </div>
1867
+ </div>
1868
+ <div class="form-group" style="margin-bottom: 0;">
1869
+ <label style="font-size: 13px;">Handling Time (min-max days)</label>
1870
+ <div style="display: flex; gap: 8px;">
1871
+ <input type="number" id="handling-min" value="1" min="0" style="width: 50%;">
1872
+ <input type="number" id="handling-max" value="2" min="0" style="width: 50%;">
1873
+ </div>
1874
+ </div>
1875
+ </div>
1876
+ </div>
1877
+
1878
+ <button class="btn" id="generate-schema-btn" onclick="generateSchema()">Generate Snippets</button>
1879
+
1880
+ <div class="result" id="schema-result"></div>
1881
+ </div>
1882
+
1883
+ <!-- AI Agent Simulation Tab -->
1884
+ <div class="card hidden" id="simulate-panel">
1885
+ <h3 style="margin-bottom: 8px;">🤖 AI Agent Simulation Test</h3>
1886
+ <p style="margin-bottom: 20px; color: var(--color-medium); font-size: 14px;">
1887
+ Test how AI shopping agents (ChatGPT, Google AI, Copilot) would actually interact with your UCP
1888
+ implementation.
1889
+ Goes beyond spec compliance to prove real-world functionality.
1890
+ </p>
1891
+ <form id="simulate-form">
1892
+ <div class="form-group">
1893
+ <label for="simulate-domain">Your Domain</label>
1894
+ <input type="text" id="simulate-domain" placeholder="yourstore.com" required>
1895
+ <small class="text-muted">We'll simulate an AI agent discovering and interacting with your UCP
1896
+ profile</small>
1897
+ </div>
1898
+ <button type="submit" class="btn" id="simulate-btn">🚀 Run AI Agent Simulation</button>
1899
+ </form>
1900
+
1901
+ <div class="result" id="simulate-result"></div>
1902
+ </div>
1903
+
1904
+ <!-- Security Scanner Tab -->
1905
+ <div class="card hidden" id="security-panel">
1906
+ <h3 style="margin-bottom: 8px;">🔒 Security Posture Scanner</h3>
1907
+ <p style="margin-bottom: 20px; color: var(--color-medium); font-size: 14px;">
1908
+ Scan your UCP endpoint for common security misconfigurations. Exposing REST endpoints for AI agents
1909
+ creates new attack surfaces - make sure you're protected.
1910
+ </p>
1911
+ <form id="security-form">
1912
+ <div class="form-group">
1913
+ <label for="security-domain">Your Domain</label>
1914
+ <input type="text" id="security-domain" placeholder="yourstore.com" required>
1915
+ <small class="text-muted">We'll check HTTPS, security headers, rate limiting, CORS, and more</small>
1916
+ </div>
1917
+ <button type="submit" class="btn" id="security-btn">🔍 Run Security Scan</button>
1918
+ </form>
1919
+
1920
+ <div class="result" id="security-result"></div>
1921
+ </div>
1922
+
1923
+ <!-- Feed Analyzer Tab -->
1924
+ <div class="card hidden" id="feed-panel">
1925
+ <h3 style="margin-bottom: 8px;">📦 Product Feed Quality Analyzer</h3>
1926
+ <p style="margin-bottom: 20px; color: var(--color-medium); font-size: 14px;">
1927
+ Deep analysis of your product data quality for AI agent visibility.
1928
+ AI shopping agents evaluate product feed completeness, identifiers, images, and pricing to decide what to recommend.
1929
+ </p>
1930
+
1931
+ <form id="feed-form">
1932
+ <div class="form-group">
1933
+ <label for="feed-url">Product Page or Catalog URL</label>
1934
+ <input type="url" id="feed-url" placeholder="https://yourstore.com/products" required>
1935
+ <small class="text-muted">We'll scan for Schema.org Product markup and analyze data quality</small>
1936
+ </div>
1937
+
1938
+ <div class="form-group">
1939
+ <label for="feed-max-products">Max Products to Analyze</label>
1940
+ <select id="feed-max-products">
1941
+ <option value="10">10 products (quick scan)</option>
1942
+ <option value="25">25 products</option>
1943
+ <option value="50" selected>50 products (recommended)</option>
1944
+ <option value="100">100 products (thorough)</option>
1945
+ </select>
1946
+ </div>
1947
+
1948
+ <button type="submit" class="btn" id="feed-btn">🔍 Analyze Product Feed</button>
1949
+ </form>
1950
+
1951
+ <div class="result" id="feed-result"></div>
1952
+ </div>
1953
+
1954
+ <!-- GDPR Compliance Generator Tab -->
1955
+ <div class="card hidden" id="compliance-panel">
1956
+ <h3 style="margin-bottom: 8px;">📋 GDPR/Privacy Compliance Generator</h3>
1957
+ <p style="margin-bottom: 20px; color: var(--color-medium); font-size: 14px;">
1958
+ Generate privacy policy addendums and consent language for AI-powered shopping.
1959
+ When purchases happen via AI agents, traditional cookie consent doesn't apply - you need specific disclosures.
1960
+ </p>
1961
+
1962
+ <div style="background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 12px; margin-bottom: 20px; font-size: 13px;">
1963
+ <strong>⚠️ Disclaimer:</strong> This generates templates for informational purposes only and does NOT constitute legal advice.
1964
+ Please have a qualified legal professional review before use.
1965
+ </div>
1966
+
1967
+ <form id="compliance-form">
1968
+ <div class="form-group">
1969
+ <label for="compliance-company">Company Name *</label>
1970
+ <input type="text" id="compliance-company" placeholder="Your Company Name" required>
1971
+ </div>
1972
+
1973
+ <div class="form-group">
1974
+ <label for="compliance-email">Privacy Contact Email</label>
1975
+ <input type="email" id="compliance-email" placeholder="privacy@yourcompany.com">
1976
+ </div>
1977
+
1978
+ <div class="form-group">
1979
+ <label for="compliance-dpo">Data Protection Officer Email (optional)</label>
1980
+ <input type="email" id="compliance-dpo" placeholder="dpo@yourcompany.com">
1981
+ </div>
1982
+
1983
+ <div class="form-group">
1984
+ <label>Compliance Regions *</label>
1985
+ <div style="display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;">
1986
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
1987
+ <input type="checkbox" id="region-eu" checked> EU (GDPR)
1988
+ </label>
1989
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
1990
+ <input type="checkbox" id="region-uk"> UK (UK GDPR)
1991
+ </label>
1992
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
1993
+ <input type="checkbox" id="region-california"> California (CCPA/CPRA)
1994
+ </label>
1995
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
1996
+ <input type="checkbox" id="region-global"> Global
1997
+ </label>
1998
+ </div>
1999
+ </div>
2000
+
2001
+ <div class="form-group">
2002
+ <label>AI Platforms You're Using *</label>
2003
+ <div style="display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;">
2004
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
2005
+ <input type="checkbox" id="platform-openai" checked> ChatGPT (OpenAI)
2006
+ </label>
2007
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
2008
+ <input type="checkbox" id="platform-google" checked> Google AI Mode
2009
+ </label>
2010
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
2011
+ <input type="checkbox" id="platform-microsoft"> Microsoft Copilot
2012
+ </label>
2013
+ </div>
2014
+ </div>
2015
+
2016
+ <div class="form-group">
2017
+ <label for="compliance-basis">Lawful Basis for Processing *</label>
2018
+ <select id="compliance-basis" required>
2019
+ <option value="contract" selected>Contract Performance (Recommended for orders)</option>
2020
+ <option value="consent">Explicit Consent</option>
2021
+ <option value="legitimate">Legitimate Interests</option>
2022
+ <option value="legal">Legal Obligation</option>
2023
+ </select>
2024
+ <small class="text-muted" id="basis-description">Processing is necessary to fulfill customer orders</small>
2025
+ </div>
2026
+
2027
+ <div class="form-group">
2028
+ <label>Optional Sections</label>
2029
+ <div style="display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;">
2030
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
2031
+ <input type="checkbox" id="include-marketing"> Marketing consent language
2032
+ </label>
2033
+ <label style="display: flex; align-items: center; gap: 8px; font-weight: normal;">
2034
+ <input type="checkbox" id="include-retention"> Data retention policy
2035
+ </label>
2036
+ </div>
2037
+ </div>
2038
+
2039
+ <button type="submit" class="btn" id="compliance-btn">📄 Generate Compliance Documents</button>
2040
+ </form>
2041
+
2042
+ <div class="result" id="compliance-result"></div>
2043
+ </div>
2044
+ </div>
2045
+ </section>
2046
+
2047
+ <section class="pricing" id="pricing">
2048
+ <div class="container">
2049
+ <h2>Free & Open</h2>
2050
+ <div class="pricing-grid" style="max-width: 650px;">
2051
+ <div class="pricing-card">
2052
+ <h3>Free</h3>
2053
+ <div class="price">$0</div>
2054
+ <ul>
2055
+ <li>Unlimited validations</li>
2056
+ <li>Profile generation</li>
2057
+ <li>Download JSON file</li>
2058
+ <li>A-F grading & recommendations</li>
2059
+ </ul>
2060
+ <a href="#validate" class="btn btn-secondary">Start Validating</a>
2061
+ </div>
2062
+ <div class="pricing-card" style="border-color: var(--brand-teal);">
2063
+ <h3>Support the Project</h3>
2064
+ <div class="price" style="font-size: 36px;">&#9829;</div>
2065
+ <ul>
2066
+ <li>Help keep this tool free</li>
2067
+ <li>Support independent development</li>
2068
+ <li>Get a warm fuzzy feeling</li>
2069
+ <li>Karma points (unverified)</li>
2070
+ </ul>
2071
+ <a href="https://buymeacoffee.com/ucptools" target="_blank" class="btn">Buy Me a Coffee</a>
2072
+ </div>
2073
+ </div>
2074
+ </div>
2075
+ </section>
2076
+
2077
+ <!-- Opt-out Info Section -->
2078
+ <section id="optout"
2079
+ style="padding: 40px 20px; background: var(--color-background); border-top: 1px solid var(--color-border);">
2080
+ <div class="container" style="max-width: 800px;">
2081
+ <h2 style="margin-bottom: 20px; text-align: center;">How to Opt Out of AI Commerce</h2>
2082
+ <div
2083
+ style="background: var(--color-card); border: 1px solid var(--color-border); border-radius: 12px; padding: 24px;">
2084
+ <p style="margin-bottom: 16px;">Shopify's Agentic Storefronts are <strong>enabled by default</strong> starting
2085
+ January 26, 2026. If you want to disable AI agent access to your store:</p>
2086
+
2087
+ <h4 style="margin: 16px 0 8px; color: var(--brand-blue);">Shopify Stores:</h4>
2088
+ <ol style="margin-left: 20px; margin-bottom: 16px;">
2089
+ <li>Go to <strong>Sales Channels</strong> in your Shopify admin</li>
2090
+ <li>Find <strong>Agentic Storefronts</strong> or <strong>Shop</strong> channel</li>
2091
+ <li>Click <strong>Settings</strong> and disable the channel</li>
2092
+ <li>Remove products from the Shopify Global Catalog</li>
2093
+ </ol>
2094
+
2095
+ <h4 style="margin: 16px 0 8px; color: var(--brand-blue);">Block Specific AI Agents:</h4>
2096
+ <ul style="margin-left: 20px; margin-bottom: 16px;">
2097
+ <li><strong>Amazon Buy for Me:</strong> Use Cloudflare security rules to block Amazon's bot</li>
2098
+ <li><strong>robots.txt:</strong> Add <code>User-agent: GPTBot</code> and <code>Disallow: /</code> to block
2099
+ ChatGPT</li>
2100
+ <li><strong>Google AI:</strong> Currently no opt-out for Google AI Mode with UCP</li>
2101
+ </ul>
2102
+
2103
+ <div
2104
+ style="background: #fef9c3; border-left: 4px solid #CA8A04; padding: 12px 16px; border-radius: 0 8px 8px 0;">
2105
+ <strong style="color: #92400E;">Warning:</strong> Opting out means AI shopping agents won't be able to
2106
+ complete purchases on your store. This may reduce discoverability as AI-powered shopping grows.
2107
+ </div>
2108
+ </div>
2109
+ </div>
2110
+ </section>
2111
+
2112
+ <footer>
2113
+ <div class="container">
2114
+ <p><strong>UCP.tools</strong> &bull; <a href="https://github.com/Universal-Commerce-Protocol/ucp">UCP Spec</a>
2115
+ &bull; <a href="#optout">Opt-Out Guide</a> &bull; <a href="mailto:hello@ucptools.dev">Contact</a></p>
2116
+ <p style="margin-top: 8px; font-size: 14px;">Independent project. Not affiliated with Google or Shopify. UCP is an
2117
+ open protocol.</p>
2118
+ </div>
2119
+ </footer>
2120
+
2121
+ <script>
2122
+ // Switch tab function (called from nav and tab buttons)
2123
+ function switchTab(tabName) {
2124
+ // Update tab buttons
2125
+ document.querySelectorAll('.tab').forEach(t => {
2126
+ t.classList.toggle('active', t.dataset.tab === tabName);
2127
+ });
2128
+ // Show/hide panels
2129
+ document.getElementById('validate-panel').classList.toggle('hidden', tabName !== 'validate');
2130
+ document.getElementById('simulate-panel').classList.toggle('hidden', tabName !== 'simulate');
2131
+ document.getElementById('security-panel').classList.toggle('hidden', tabName !== 'security');
2132
+ document.getElementById('feed-panel').classList.toggle('hidden', tabName !== 'feed');
2133
+ document.getElementById('compliance-panel').classList.toggle('hidden', tabName !== 'compliance');
2134
+ document.getElementById('generate-panel').classList.toggle('hidden', tabName !== 'generate');
2135
+ document.getElementById('schema-panel').classList.toggle('hidden', tabName !== 'schema');
2136
+ // Scroll to hero section
2137
+ document.querySelector('.hero').scrollIntoView({ behavior: 'smooth' });
2138
+ }
2139
+
2140
+ // Schema template toggles
2141
+ document.getElementById('return-template').addEventListener('change', (e) => {
2142
+ document.getElementById('custom-return').classList.toggle('hidden', e.target.value !== 'custom');
2143
+ });
2144
+ document.getElementById('shipping-template').addEventListener('change', (e) => {
2145
+ document.getElementById('custom-shipping').classList.toggle('hidden', e.target.value !== 'custom');
2146
+ });
2147
+
2148
+ // Generate Schema.org snippets
2149
+ async function generateSchema() {
2150
+ const btn = document.getElementById('generate-schema-btn');
2151
+ const result = document.getElementById('schema-result');
2152
+
2153
+ const returnTemplate = document.getElementById('return-template').value;
2154
+ const shippingTemplate = document.getElementById('shipping-template').value;
2155
+
2156
+ btn.disabled = true;
2157
+ btn.textContent = 'Generating...';
2158
+
2159
+ try {
2160
+ let options = {
2161
+ returnPolicy: returnTemplate !== 'custom' ? returnTemplate : undefined,
2162
+ shipping: shippingTemplate !== 'custom' ? shippingTemplate : undefined,
2163
+ };
2164
+
2165
+ if (returnTemplate === 'custom') {
2166
+ options.customReturn = {
2167
+ returnDays: parseInt(document.getElementById('return-days').value),
2168
+ country: document.getElementById('return-country').value,
2169
+ freeReturns: document.getElementById('free-returns').checked,
2170
+ };
2171
+ }
2172
+
2173
+ if (shippingTemplate === 'custom') {
2174
+ options.customShipping = {
2175
+ rate: parseFloat(document.getElementById('shipping-rate').value),
2176
+ currency: document.getElementById('shipping-currency').value,
2177
+ country: document.getElementById('shipping-country').value,
2178
+ transitMin: parseInt(document.getElementById('transit-min').value),
2179
+ transitMax: parseInt(document.getElementById('transit-max').value),
2180
+ handlingMin: parseInt(document.getElementById('handling-min').value),
2181
+ handlingMax: parseInt(document.getElementById('handling-max').value),
2182
+ };
2183
+ }
2184
+
2185
+ const res = await fetch('/api/generate-schema', {
2186
+ method: 'POST',
2187
+ headers: { 'Content-Type': 'application/json' },
2188
+ body: JSON.stringify({ type: 'complete', options })
2189
+ });
2190
+ const data = await res.json();
2191
+
2192
+ if (!data.success) {
2193
+ throw new Error(data.error || 'Failed to generate schema');
2194
+ }
2195
+
2196
+ window.generatedSchemaCode = data.embedCode;
2197
+
2198
+ result.className = 'result show success';
2199
+ result.innerHTML = `
2200
+ <p style="margin-bottom: 16px;">\u2713 Schema snippets generated!</p>
2201
+ <p style="font-size: 13px; margin-bottom: 12px; color: var(--color-medium);">Copy this code and paste it into your HTML &lt;head&gt; section:</p>
2202
+ <pre style="font-size: 12px;">${escapeHtml(data.embedCode)}</pre>
2203
+ <div style="margin-top: 16px; display: flex; gap: 12px; flex-wrap: wrap;">
2204
+ <button class="btn" onclick="copySchemaCode()">Copy to Clipboard</button>
2205
+ <button class="btn btn-secondary" onclick="downloadSchemaCode()">Download HTML</button>
2206
+ </div>
2207
+ <div style="margin-top: 20px; padding: 16px; background: rgba(46,134,171,0.1); border-radius: 8px;">
2208
+ <h4 style="margin-bottom: 8px; font-size: 14px;">Next Steps:</h4>
2209
+ <ol style="margin: 0; padding-left: 20px; font-size: 13px; color: var(--color-dark);">
2210
+ <li>Paste this code in your HTML &lt;head&gt;</li>
2211
+ <li>For product pages, add Product schema with @id references</li>
2212
+ <li>Validate at <a href="https://validator.schema.org/" target="_blank" class="link-brand">validator.schema.org</a></li>
2213
+ <li>Re-check your AI Readiness score!</li>
2214
+ </ol>
2215
+ </div>
2216
+ `;
2217
+ } catch (err) {
2218
+ result.className = 'result show error';
2219
+ result.innerHTML = `<p>Error: ${err.message}</p>`;
2220
+ }
2221
+
2222
+ btn.disabled = false;
2223
+ btn.textContent = 'Generate Snippets';
2224
+ }
2225
+
2226
+ function copySchemaCode() {
2227
+ navigator.clipboard.writeText(window.generatedSchemaCode);
2228
+ alert('Copied to clipboard!');
2229
+ }
2230
+
2231
+ function downloadSchemaCode() {
2232
+ const blob = new Blob([window.generatedSchemaCode], { type: 'text/html' });
2233
+ const url = URL.createObjectURL(blob);
2234
+ const a = document.createElement('a');
2235
+ a.href = url;
2236
+ a.download = 'schema-snippets.html';
2237
+ a.click();
2238
+ }
2239
+
2240
+ // Tab button click handlers
2241
+ document.querySelectorAll('.tab').forEach(tab => {
2242
+ tab.addEventListener('click', () => {
2243
+ switchTab(tab.dataset.tab);
2244
+ });
2245
+ });
2246
+
2247
+ // Validate form
2248
+ document.getElementById('validate-form').addEventListener('submit', async (e) => {
2249
+ e.preventDefault();
2250
+ const btn = document.getElementById('validate-btn');
2251
+ const result = document.getElementById('validate-result');
2252
+ const domain = document.getElementById('domain').value.replace(/^https?:\/\//, '').replace(/\/$/, '');
2253
+
2254
+ btn.disabled = true;
2255
+ btn.textContent = 'Checking AI Readiness...';
2256
+ result.className = 'result';
2257
+ result.innerHTML = '';
2258
+
2259
+ try {
2260
+ const res = await fetch('/api/validate', {
2261
+ method: 'POST',
2262
+ headers: { 'Content-Type': 'application/json' },
2263
+ body: JSON.stringify({ domain })
2264
+ });
2265
+ const data = await res.json();
2266
+
2267
+ // Use new AI readiness score
2268
+ const score = data.ai_readiness?.score ?? 0;
2269
+ const grade = data.ai_readiness?.grade ?? 'F';
2270
+ const readinessLabel = data.ai_readiness?.label ?? 'Not Ready';
2271
+
2272
+ // Separate issues by category
2273
+ const ucpIssues = data.ucp?.issues || [];
2274
+ const schemaIssues = data.schema?.issues || [];
2275
+ const ucpErrors = ucpIssues.filter(i => i.severity === 'error').length;
2276
+ const ucpWarnings = ucpIssues.filter(i => i.severity === 'warn').length;
2277
+ const schemaErrors = schemaIssues.filter(i => i.severity === 'error').length;
2278
+ const schemaWarnings = schemaIssues.filter(i => i.severity === 'warn').length;
2279
+
2280
+ const levelColors = {
2281
+ 'ready': '#16A34A',
2282
+ 'partial': '#CA8A04',
2283
+ 'limited': '#EA580C',
2284
+ 'not_ready': '#DC2626'
2285
+ };
2286
+ const levelColor = levelColors[data.ai_readiness?.level] || '#DC2626';
2287
+
2288
+ result.className = 'result show';
2289
+ result.style.background = '#fff';
2290
+ result.style.border = 'none';
2291
+ result.style.borderRadius = '0';
2292
+ result.style.boxShadow = 'none';
2293
+ result.style.padding = '0';
2294
+
2295
+ const productCompleteness = data.product_quality?.completeness || 0;
2296
+ const productIssues = data.product_quality?.issues || [];
2297
+ const shippingIssues = data.shipping?.issues || [];
2298
+
2299
+ // Combine all issues and create fix lookup
2300
+ const allIssues = [...ucpIssues, ...schemaIssues, ...productIssues, ...shippingIssues];
2301
+ const suggestions = data.lint_suggestions || [];
2302
+
2303
+ // Create issue code to fix mapping from API
2304
+ const fixLookup = {};
2305
+ suggestions.forEach((s, idx) => {
2306
+ if (s.code) fixLookup[s.code] = { ...s, idx };
2307
+ });
2308
+
2309
+ // Default fix guidance for common UCP issues
2310
+ const defaultFixes = {
2311
+ 'UCP_INVALID_VERSION': {
2312
+ title: 'Fix UCP Version Format',
2313
+ fix: 'The version field must use YYYY-MM-DD format (e.g., "2025-01-15"). This indicates when your UCP profile was last updated.',
2314
+ code_snippet: '"version": "2025-01-15"'
2315
+ },
2316
+ 'UCP_INVALID_SERVICE': {
2317
+ title: 'Complete Service Definition',
2318
+ fix: 'Each service in your UCP profile must include: name, version (YYYY-MM-DD format), and spec (URL to OpenAPI specification).',
2319
+ code_snippet: `{
2320
+ "name": "checkout",
2321
+ "version": "2025-01-15",
2322
+ "spec": "https://yoursite.com/api/checkout/openapi.json"
2323
+ }`
2324
+ },
2325
+ 'UCP_NO_TRANSPORT': {
2326
+ title: 'Add Transport Binding',
2327
+ fix: 'Services need transport bindings to tell AI agents how to communicate. Add at least one transport (usually REST with a base URL).',
2328
+ code_snippet: `{
2329
+ "name": "checkout",
2330
+ "version": "2025-01-15",
2331
+ "spec": "https://yoursite.com/api/checkout/openapi.json",
2332
+ "transport": [{
2333
+ "type": "REST",
2334
+ "base_url": "https://yoursite.com/api/checkout"
2335
+ }]
2336
+ }`
2337
+ },
2338
+ 'UCP_INVALID_CAP': {
2339
+ title: 'Fix Capability Definition',
2340
+ fix: 'Each capability must have: name (what it does), version (YYYY-MM-DD), spec (OpenAPI URL), and schema (JSON Schema URL).',
2341
+ code_snippet: `{
2342
+ "name": "product-search",
2343
+ "version": "2025-01-15",
2344
+ "spec": "https://yoursite.com/api/search/openapi.json",
2345
+ "schema": "https://yoursite.com/api/search/schema.json"
2346
+ }`
2347
+ },
2348
+ 'UCP_MISSING_SERVICES': {
2349
+ title: 'Add Required Services',
2350
+ fix: 'UCP profiles should define services that AI agents can use. Common services include: checkout, products, orders, and cart.',
2351
+ doc_link: 'https://ucp.dev/specification/services/'
2352
+ },
2353
+ 'UCP_MISSING_CAPABILITIES': {
2354
+ title: 'Add Capabilities',
2355
+ fix: 'Capabilities tell AI agents what your store can do. Define capabilities like product-search, cart-management, etc.',
2356
+ doc_link: 'https://ucp.dev/specification/capabilities/'
2357
+ }
2358
+ };
2359
+
2360
+ // Enhanced issue message with context
2361
+ const enhanceIssue = (issue) => {
2362
+ let enhanced = { ...issue };
2363
+
2364
+ // Extract context from message (e.g., "Service \"checkout\" missing version")
2365
+ const serviceMatch = issue.message.match(/Service "([^"]+)"/i);
2366
+ const capMatch = issue.message.match(/Capability "([^"]+)"/i);
2367
+
2368
+ if (serviceMatch) {
2369
+ enhanced.context = `Service: ${serviceMatch[1]}`;
2370
+ } else if (capMatch) {
2371
+ enhanced.context = `Capability: ${capMatch[1]}`;
2372
+ }
2373
+
2374
+ // Improve cryptic messages
2375
+ if (issue.code === 'UCP_INVALID_CAP' && issue.message === 'Missing name') {
2376
+ enhanced.message = 'Capability is missing required "name" field';
2377
+ enhanced.hint = 'Each capability needs a name that describes what it does (e.g., "product-search", "cart-checkout")';
2378
+ }
2379
+ if (issue.code === 'UCP_INVALID_CAP' && issue.message === 'Missing version') {
2380
+ enhanced.message = 'Capability is missing required "version" field';
2381
+ enhanced.hint = 'Add version in YYYY-MM-DD format (e.g., "2025-01-15")';
2382
+ }
2383
+ if (issue.code === 'UCP_INVALID_CAP' && issue.message === 'Missing spec') {
2384
+ enhanced.message = 'Capability is missing required "spec" field';
2385
+ enhanced.hint = 'Add URL to your OpenAPI specification file';
2386
+ }
2387
+ if (issue.code === 'UCP_INVALID_CAP' && issue.message === 'Missing schema') {
2388
+ enhanced.message = 'Capability is missing required "schema" field';
2389
+ enhanced.hint = 'Add URL to your JSON Schema file';
2390
+ }
2391
+
2392
+ return enhanced;
2393
+ };
2394
+
2395
+ // Group similar issues for cleaner display
2396
+ const groupIssues = (issues) => {
2397
+ const groups = {};
2398
+ const standalone = [];
2399
+
2400
+ issues.forEach(issue => {
2401
+ // Group UCP_INVALID_CAP issues by message type
2402
+ if (issue.code === 'UCP_INVALID_CAP') {
2403
+ const key = `${issue.code}:${issue.message}`;
2404
+ if (!groups[key]) {
2405
+ groups[key] = { ...issue, count: 0, items: [] };
2406
+ }
2407
+ groups[key].count++;
2408
+ groups[key].items.push(issue);
2409
+ } else {
2410
+ standalone.push(issue);
2411
+ }
2412
+ });
2413
+
2414
+ // Convert groups to array and add to standalone
2415
+ Object.values(groups).forEach(group => {
2416
+ if (group.count > 1) {
2417
+ group.message = `${group.message} (${group.count} capabilities)`;
2418
+ group.isGrouped = true;
2419
+ }
2420
+ standalone.push(group);
2421
+ });
2422
+
2423
+ return standalone;
2424
+ };
2425
+
2426
+ // Helper to render issue with inline fix
2427
+ const renderIssue = (issue, idx) => {
2428
+ const enhanced = enhanceIssue(issue);
2429
+ const fix = fixLookup[issue.code] || defaultFixes[issue.code];
2430
+ const hasSpecificFix = !!fix;
2431
+
2432
+ return `
2433
+ <div class="issue-item has-fix" data-issue-idx="${idx}">
2434
+ <div class="issue-item-header">
2435
+ <span class="issue-severity-dot ${enhanced.severity}"></span>
2436
+ <span class="issue-code">${enhanced.code}</span>
2437
+ <span class="issue-message">${enhanced.message}</span>
2438
+ <span class="issue-has-fix">${hasSpecificFix ? 'Fix' : 'Help'} &#8594;</span>
2439
+ </div>
2440
+ ${enhanced.context ? '<div class="issue-context">' + enhanced.context + '</div>' : ''}
2441
+ ${enhanced.hint ? '<div class="issue-hint">' + enhanced.hint + '</div>' : ''}
2442
+ ${hasSpecificFix ? `
2443
+ <div class="issue-fix-panel">
2444
+ <div class="issue-fix-title">${fix.title}</div>
2445
+ <div class="issue-fix-description">${fix.fix}</div>
2446
+ ${fix.code_snippet ? `
2447
+ <div class="issue-fix-code">
2448
+ <div class="issue-fix-code-header">
2449
+ <span>Example Code</span>
2450
+ <button class="issue-copy-btn" data-snippet="${idx}">Copy</button>
2451
+ </div>
2452
+ <pre><code>${escapeHtml(fix.code_snippet)}</code></pre>
2453
+ </div>
2454
+ ` : ''}
2455
+ <div class="issue-fix-links">
2456
+ ${fix.doc_link ? '<a href="' + fix.doc_link + '" target="_blank">Documentation</a>' : ''}
2457
+ ${fix.generator_link ? '<a href="' + fix.generator_link + '">Generator</a>' : ''}
2458
+ <a href="#generate" onclick="switchTab(\'generate\'); return false;">Use Generator</a>
2459
+ </div>
2460
+ </div>
2461
+ ` : `
2462
+ <div class="issue-fix-panel">
2463
+ <div class="issue-fix-title">How to Fix</div>
2464
+ <div class="issue-fix-description">Use our UCP Generator tool to create a valid UCP profile with all required fields properly configured.</div>
2465
+ <div class="issue-fix-links">
2466
+ <a href="#generate" onclick="switchTab('generate'); return false;">Open Generator</a>
2467
+ <a href="https://ucp.dev/specification/overview/" target="_blank">UCP Specification</a>
2468
+ </div>
2469
+ </div>
2470
+ `}
2471
+ </div>
2472
+ `;
2473
+ };
2474
+
2475
+ // Process UCP issues - group similar ones
2476
+ const processedUcpIssues = groupIssues(ucpIssues);
2477
+
2478
+ result.innerHTML = `
2479
+ <!-- Score Header -->
2480
+ <div class="report-header">
2481
+ <div class="report-grade grade-${grade}">${grade}</div>
2482
+ <div class="report-score-info">
2483
+ <div class="report-score">${score}<span>/100</span></div>
2484
+ <div class="report-label" style="color: ${levelColor}">${readinessLabel}</div>
2485
+ </div>
2486
+ ${data.benchmark ? `
2487
+ <div class="report-benchmark">
2488
+ <div class="report-benchmark-value">${data.benchmark.percentile}%</div>
2489
+ <div class="report-benchmark-label">better than ${data.benchmark.total_sites_analyzed} sites</div>
2490
+ </div>
2491
+ ` : ''}
2492
+ </div>
2493
+
2494
+ <!-- Quick Stats -->
2495
+ <div class="report-stats">
2496
+ <div class="report-stat ${data.ucp?.found ? 'pass' : 'fail'}">
2497
+ <div class="report-stat-icon">${data.ucp?.found ? '&#10003;' : '&#10005;'}</div>
2498
+ <div class="report-stat-text">UCP Profile</div>
2499
+ </div>
2500
+ <div class="report-stat ${(data.schema?.stats?.returnPolicies || 0) > 0 ? 'pass' : 'fail'}">
2501
+ <div class="report-stat-icon">${(data.schema?.stats?.returnPolicies || 0) > 0 ? '&#10003;' : '&#10005;'}</div>
2502
+ <div class="report-stat-text">Return Policy</div>
2503
+ </div>
2504
+ <div class="report-stat ${(data.schema?.stats?.products || 0) > 0 ? 'pass' : 'warn'}">
2505
+ <div class="report-stat-icon">${data.schema?.stats?.products || 0}</div>
2506
+ <div class="report-stat-text">Products</div>
2507
+ </div>
2508
+ <div class="report-stat ${productCompleteness >= 80 ? 'pass' : productCompleteness >= 50 ? 'warn' : 'fail'}">
2509
+ <div class="report-stat-icon">${productCompleteness}%</div>
2510
+ <div class="report-stat-text">Data Quality</div>
2511
+ </div>
2512
+ </div>
2513
+
2514
+ <!-- Issues Sections -->
2515
+ <div id="report-issues">
2516
+ ${ucpIssues.length > 0 || !data.ucp?.found ? `
2517
+ <div class="report-section ${ucpIssues.length === 0 && data.ucp?.found ? 'collapsed' : ''}" data-section="ucp">
2518
+ <div class="report-section-header">
2519
+ <div class="report-section-status ${data.ucp?.found ? (ucpErrors === 0 ? 'pass' : 'fail') : 'fail'}">
2520
+ ${data.ucp?.found ? (ucpErrors === 0 ? '&#10003;' : processedUcpIssues.length) : '&#10005;'}
2521
+ </div>
2522
+ <div class="report-section-title">
2523
+ <strong>UCP Profile</strong>
2524
+ <span>${data.ucp?.found ? (ucpIssues.length === 0 ? 'Valid' : processedUcpIssues.length + ' issues to fix') : 'Not found - create /.well-known/ucp'}</span>
2525
+ </div>
2526
+ <div class="report-section-toggle">&#9662;</div>
2527
+ </div>
2528
+ <div class="report-section-body">
2529
+ ${data.ucp?.found && processedUcpIssues.length > 0 ? `
2530
+ <div class="report-ucp-summary">
2531
+ <p>Your UCP profile was found but has configuration issues. Click each issue to see how to fix it.</p>
2532
+ </div>
2533
+ ` : ''}
2534
+ ${processedUcpIssues.length > 0 ? processedUcpIssues.map((issue, idx) => renderIssue(issue, 'ucp-' + idx)).join('') : `
2535
+ <div class="report-empty-state">
2536
+ <p>Create a UCP profile to enable AI shopping agents on your store.</p>
2537
+ <a href="#generate" onclick="switchTab('generate'); return false;" class="btn btn-secondary">Generate UCP Profile</a>
2538
+ </div>
2539
+ `}
2540
+ </div>
2541
+ </div>
2542
+ ` : ''}
2543
+
2544
+ ${schemaIssues.length > 0 || (data.schema?.stats?.products === 0) ? `
2545
+ <div class="report-section" data-section="schema">
2546
+ <div class="report-section-header">
2547
+ <div class="report-section-status ${schemaErrors === 0 ? (schemaWarnings === 0 ? 'pass' : 'warn') : 'fail'}">
2548
+ ${schemaErrors === 0 ? (schemaWarnings === 0 ? '&#10003;' : schemaWarnings) : schemaIssues.length}
2549
+ </div>
2550
+ <div class="report-section-title">
2551
+ <strong>Schema.org Markup</strong>
2552
+ <span>${schemaIssues.length === 0 ? 'Valid' : schemaIssues.length + ' issues to fix'} &bull; Jan 2026 deadline</span>
2553
+ </div>
2554
+ <div class="report-section-toggle">&#9662;</div>
2555
+ </div>
2556
+ <div class="report-section-body">
2557
+ <div class="report-schema-stats">
2558
+ <div><strong>${data.schema?.stats?.products || 0}</strong> Products</div>
2559
+ <div><strong>${data.schema?.stats?.returnPolicies || 0}</strong> Return Policies</div>
2560
+ <div><strong>${data.schema?.stats?.orgs || 0}</strong> Organizations</div>
2561
+ </div>
2562
+ ${schemaIssues.length > 0 ? schemaIssues.map((issue, idx) => renderIssue(issue, 'schema-' + idx)).join('') : ''}
2563
+ </div>
2564
+ </div>
2565
+ ` : ''}
2566
+
2567
+ ${productIssues.length > 0 ? `
2568
+ <div class="report-section collapsed" data-section="product">
2569
+ <div class="report-section-header">
2570
+ <div class="report-section-status ${productCompleteness >= 80 ? 'pass' : 'warn'}">
2571
+ ${productIssues.length}
2572
+ </div>
2573
+ <div class="report-section-title">
2574
+ <strong>Product Data</strong>
2575
+ <span>${productCompleteness}% complete &bull; ${productIssues.length} suggestions</span>
2576
+ </div>
2577
+ <div class="report-section-toggle">&#9662;</div>
2578
+ </div>
2579
+ <div class="report-section-body">
2580
+ ${productIssues.map((issue, idx) => renderIssue(issue, 'product-' + idx)).join('')}
2581
+ </div>
2582
+ </div>
2583
+ ` : ''}
2584
+
2585
+ ${shippingIssues.length > 0 ? `
2586
+ <div class="report-section collapsed" data-section="shipping">
2587
+ <div class="report-section-header">
2588
+ <div class="report-section-status warn">
2589
+ ${shippingIssues.length}
2590
+ </div>
2591
+ <div class="report-section-title">
2592
+ <strong>Shipping Details</strong>
2593
+ <span>${shippingIssues.length} issues</span>
2594
+ </div>
2595
+ <div class="report-section-toggle">&#9662;</div>
2596
+ </div>
2597
+ <div class="report-section-body">
2598
+ ${shippingIssues.map((issue, idx) => renderIssue(issue, 'shipping-' + idx)).join('')}
2599
+ </div>
2600
+ </div>
2601
+ ` : ''}
2602
+ </div>
2603
+
2604
+ <!-- Actions -->
2605
+ <div class="report-actions">
2606
+ <button class="btn" onclick="downloadPdfReport()">&#128196; Download PDF Report</button>
2607
+ <a href="/verify?domain=${encodeURIComponent(domain)}" target="_blank" class="btn btn-secondary">&#128279; Share Results</a>
2608
+ ${score >= 50 ? '<button class="btn btn-secondary" onclick="copyBadgeCode(\'' + domain + '\')">&#127942; Get Badge</button>' : ''}
2609
+ </div>
2610
+ `;
2611
+
2612
+ // Store validation data for report generation
2613
+ window.lastValidatedDomain = domain;
2614
+ window.lastValidationData = data;
2615
+ window.lintSuggestions = data.lint_suggestions || [];
2616
+
2617
+ // Setup event delegation for report sections and issues
2618
+ const reportIssues = document.getElementById('report-issues');
2619
+ if (reportIssues) {
2620
+ reportIssues.addEventListener('click', function(e) {
2621
+ // Handle report section header click (expand/collapse section)
2622
+ const sectionHeader = e.target.closest('.report-section-header');
2623
+ if (sectionHeader) {
2624
+ const section = sectionHeader.closest('.report-section');
2625
+ if (section) {
2626
+ section.classList.toggle('collapsed');
2627
+ }
2628
+ return;
2629
+ }
2630
+
2631
+ // Handle issue item header click (expand/collapse fix panel)
2632
+ const issueHeader = e.target.closest('.issue-item-header');
2633
+ if (issueHeader) {
2634
+ const issueItem = issueHeader.closest('.issue-item');
2635
+ if (issueItem) {
2636
+ // Only toggle if this issue has a fix panel
2637
+ const fixPanel = issueItem.querySelector('.issue-fix-panel');
2638
+ if (fixPanel) {
2639
+ issueItem.classList.toggle('expanded');
2640
+ }
2641
+ }
2642
+ return;
2643
+ }
2644
+
2645
+ // Handle copy button click
2646
+ const copyBtn = e.target.closest('.issue-copy-btn');
2647
+ if (copyBtn) {
2648
+ e.stopPropagation();
2649
+ const pre = copyBtn.closest('.issue-fix-code').querySelector('code');
2650
+ if (pre) {
2651
+ navigator.clipboard.writeText(pre.textContent).then(() => {
2652
+ copyBtn.textContent = 'Copied!';
2653
+ copyBtn.classList.add('copied');
2654
+ setTimeout(() => {
2655
+ copyBtn.textContent = 'Copy';
2656
+ copyBtn.classList.remove('copied');
2657
+ }, 2000);
2658
+ });
2659
+ }
2660
+ return;
2661
+ }
2662
+ });
2663
+ }
2664
+ } catch (err) {
2665
+ result.className = 'result show error';
2666
+ result.innerHTML = `<p>Error: ${err.message}</p>`;
2667
+ }
2668
+
2669
+ btn.disabled = false;
2670
+ btn.textContent = 'Check AI Readiness';
2671
+ });
2672
+
2673
+ function copyBadgeCode(domain) {
2674
+ const code = '<a href="https://ucptools.dev/verify?domain=' + encodeURIComponent(domain) + '"><img src="https://ucptools.dev/api/badge?domain=' + encodeURIComponent(domain) + '" alt="AI Commerce Ready"></a>';
2675
+ navigator.clipboard.writeText(code);
2676
+ alert('Badge code copied to clipboard!');
2677
+ }
2678
+
2679
+ // PDF Report Generation (Issue #7)
2680
+ function downloadPdfReport() {
2681
+ const { jsPDF } = window.jspdf;
2682
+ const doc = new jsPDF('p', 'mm', 'a4');
2683
+ const data = window.lastValidationData;
2684
+ const domain = window.lastValidatedDomain;
2685
+
2686
+ if (!data || !domain) {
2687
+ alert('No validation data available. Please run a validation first.');
2688
+ return;
2689
+ }
2690
+
2691
+ const pageWidth = doc.internal.pageSize.getWidth();
2692
+ const pageHeight = doc.internal.pageSize.getHeight();
2693
+ const margin = 20;
2694
+ const contentWidth = pageWidth - (margin * 2);
2695
+
2696
+ const score = data.ai_readiness?.score ?? 0;
2697
+ const grade = data.ai_readiness?.grade ?? 'F';
2698
+ const readinessLabel = data.ai_readiness?.label ?? 'Not Ready';
2699
+
2700
+ // Brand Colors
2701
+ const colors = {
2702
+ brandBlue: [46, 134, 171],
2703
+ brandTeal: [54, 181, 162],
2704
+ brandGreen: [71, 201, 122],
2705
+ dark: [26, 43, 60],
2706
+ medium: [90, 105, 120],
2707
+ light: [148, 163, 184],
2708
+ border: [226, 232, 240],
2709
+ background: [248, 250, 252],
2710
+ white: [255, 255, 255],
2711
+ gradeA: [22, 163, 74],
2712
+ gradeB: [37, 99, 235],
2713
+ gradeC: [202, 138, 4],
2714
+ gradeD: [234, 88, 12],
2715
+ gradeF: [220, 38, 38],
2716
+ error: [220, 38, 38],
2717
+ warning: [202, 138, 4],
2718
+ success: [22, 163, 74],
2719
+ info: [37, 99, 235]
2720
+ };
2721
+
2722
+ const gradeColorMap = { A: colors.gradeA, B: colors.gradeB, C: colors.gradeC, D: colors.gradeD, F: colors.gradeF };
2723
+ const gradeColor = gradeColorMap[grade] || colors.gradeF;
2724
+
2725
+ // Helper functions
2726
+ const setColor = (color, type = 'text') => {
2727
+ if (type === 'text') doc.setTextColor(...color);
2728
+ else if (type === 'fill') doc.setFillColor(...color);
2729
+ else if (type === 'draw') doc.setDrawColor(...color);
2730
+ };
2731
+
2732
+ const drawRoundedRect = (x, y, w, h, r, fillColor, strokeColor = null) => {
2733
+ setColor(fillColor, 'fill');
2734
+ doc.roundedRect(x, y, w, h, r, r, strokeColor ? 'FD' : 'F');
2735
+ if (strokeColor) {
2736
+ setColor(strokeColor, 'draw');
2737
+ doc.roundedRect(x, y, w, h, r, r, 'S');
2738
+ }
2739
+ };
2740
+
2741
+ const addText = (text, x, y, options = {}) => {
2742
+ const { size = 10, style = 'normal', color = colors.dark, align = 'left', maxWidth = null } = options;
2743
+ doc.setFontSize(size);
2744
+ doc.setFont('helvetica', style);
2745
+ setColor(color);
2746
+ if (maxWidth) {
2747
+ doc.text(text, x, y, { maxWidth, align });
2748
+ } else {
2749
+ doc.text(text, x, y, { align });
2750
+ }
2751
+ };
2752
+
2753
+ let y = 0;
2754
+
2755
+ // ============ HEADER ============
2756
+ // Gradient header background
2757
+ const headerHeight = 55;
2758
+ // Draw gradient effect with multiple rectangles
2759
+ for (let i = 0; i < headerHeight; i++) {
2760
+ const ratio = i / headerHeight;
2761
+ const r = Math.round(colors.brandBlue[0] + (colors.brandTeal[0] - colors.brandBlue[0]) * ratio);
2762
+ const g = Math.round(colors.brandBlue[1] + (colors.brandTeal[1] - colors.brandBlue[1]) * ratio);
2763
+ const b = Math.round(colors.brandBlue[2] + (colors.brandTeal[2] - colors.brandBlue[2]) * ratio);
2764
+ doc.setFillColor(r, g, b);
2765
+ doc.rect(0, i, pageWidth, 1, 'F');
2766
+ }
2767
+
2768
+ // Logo and title
2769
+ addText('UCP.tools', margin, 22, { size: 28, style: 'bold', color: colors.white });
2770
+ addText('AI Commerce Readiness Report', margin, 32, { size: 12, color: [255, 255, 255, 200] });
2771
+
2772
+ // Report date and ID
2773
+ const dateStr = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
2774
+ const timeStr = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
2775
+ addText(dateStr, pageWidth - margin, 22, { size: 10, color: colors.white, align: 'right' });
2776
+ addText(timeStr, pageWidth - margin, 30, { size: 9, color: [255, 255, 255, 180], align: 'right' });
2777
+
2778
+ // Domain badge in header
2779
+ const domainText = domain.length > 35 ? domain.substring(0, 35) + '...' : domain;
2780
+ addText(domainText, margin, 46, { size: 14, style: 'bold', color: colors.white });
2781
+
2782
+ y = headerHeight + 15;
2783
+
2784
+ // ============ EXECUTIVE SUMMARY CARD ============
2785
+ const summaryCardHeight = 65;
2786
+ drawRoundedRect(margin, y, contentWidth, summaryCardHeight, 4, colors.white);
2787
+ doc.setDrawColor(...colors.border);
2788
+ doc.setLineWidth(0.5);
2789
+ doc.roundedRect(margin, y, contentWidth, summaryCardHeight, 4, 4, 'S');
2790
+
2791
+ // Grade circle with ring indicator
2792
+ const gradeCircleX = margin + 30;
2793
+ const gradeCircleY = y + summaryCardHeight / 2;
2794
+ const gradeCircleR = 22;
2795
+ const ringR = 25;
2796
+
2797
+ // Draw outer ring (score indicator)
2798
+ doc.setDrawColor(...colors.border);
2799
+ doc.setLineWidth(3);
2800
+ doc.circle(gradeCircleX, gradeCircleY, ringR, 'S');
2801
+
2802
+ // Draw score arc (colored portion)
2803
+ const scoreAngle = (score / 100) * 360;
2804
+ doc.setDrawColor(...gradeColor);
2805
+ doc.setLineWidth(3);
2806
+ // Approximate arc with multiple small lines for the score portion
2807
+ for (let angle = -90; angle < -90 + scoreAngle; angle += 5) {
2808
+ const rad = (angle * Math.PI) / 180;
2809
+ const nextRad = ((angle + 5) * Math.PI) / 180;
2810
+ const x1 = gradeCircleX + ringR * Math.cos(rad);
2811
+ const y1 = gradeCircleY + ringR * Math.sin(rad);
2812
+ const x2 = gradeCircleX + ringR * Math.cos(nextRad);
2813
+ const y2 = gradeCircleY + ringR * Math.sin(nextRad);
2814
+ doc.line(x1, y1, x2, y2);
2815
+ }
2816
+
2817
+ // Draw grade circle with color
2818
+ setColor(gradeColor, 'fill');
2819
+ doc.circle(gradeCircleX, gradeCircleY, gradeCircleR, 'F');
2820
+
2821
+ // Grade letter
2822
+ addText(grade, gradeCircleX, gradeCircleY + 8, { size: 36, style: 'bold', color: colors.white, align: 'center' });
2823
+
2824
+ // Score and label
2825
+ const scoreX = margin + 70;
2826
+ addText(`${score}/100`, scoreX, y + 20, { size: 32, style: 'bold', color: colors.dark });
2827
+ addText(readinessLabel, scoreX, y + 32, { size: 14, style: 'bold', color: gradeColor });
2828
+ addText('AI Commerce Readiness Score', scoreX, y + 42, { size: 10, color: colors.medium });
2829
+
2830
+ // Benchmark info on the right
2831
+ if (data.benchmark) {
2832
+ const benchX = pageWidth - margin - 55;
2833
+ drawRoundedRect(benchX - 5, y + 10, 60, 45, 3, colors.background);
2834
+ addText('BENCHMARK', benchX + 25, y + 22, { size: 8, style: 'bold', color: colors.medium, align: 'center' });
2835
+ addText(`Top ${100 - data.benchmark.percentile}%`, benchX + 25, y + 35, { size: 16, style: 'bold', color: colors.brandBlue, align: 'center' });
2836
+ addText(`of ${data.benchmark.total_sites_analyzed.toLocaleString()} sites`, benchX + 25, y + 45, { size: 8, color: colors.medium, align: 'center' });
2837
+ }
2838
+
2839
+ y += summaryCardHeight + 12;
2840
+
2841
+ // ============ STATUS OVERVIEW (4 columns) ============
2842
+ const colWidth = (contentWidth - 15) / 4;
2843
+ const statusHeight = 50;
2844
+
2845
+ // UCP Status
2846
+ const ucpFound = data.ucp?.found ?? false;
2847
+ const ucpIssues = data.ucp?.issues || [];
2848
+ const ucpErrors = ucpIssues.filter(i => i.severity === 'error').length;
2849
+ const ucpWarnings = ucpIssues.filter(i => i.severity === 'warn').length;
2850
+
2851
+ drawRoundedRect(margin, y, colWidth, statusHeight, 3, ucpFound ? [240, 253, 244] : [254, 242, 242]);
2852
+ addText('UCP Profile', margin + 8, y + 12, { size: 9, style: 'bold', color: colors.medium });
2853
+ addText(ucpFound ? '✓ Found' : '✗ Not Found', margin + 8, y + 24, {
2854
+ size: 14, style: 'bold', color: ucpFound ? colors.success : colors.error
2855
+ });
2856
+ addText(`${ucpErrors} errors · ${ucpWarnings} warnings`, margin + 8, y + 36, { size: 8, color: colors.medium });
2857
+ if (data.ucp?.version) {
2858
+ addText(`v${data.ucp.version}`, margin + colWidth - 8, y + 12, { size: 8, color: colors.medium, align: 'right' });
2859
+ }
2860
+
2861
+ // Schema Status
2862
+ const schemaX = margin + colWidth + 5;
2863
+ const schemaStats = data.schema?.stats || {};
2864
+ const hasSchema = schemaStats.products > 0 || schemaStats.returnPolicies > 0;
2865
+ const schemaIssues = data.schema?.issues || [];
2866
+ const schemaErrors = schemaIssues.filter(i => i.severity === 'error').length;
2867
+ const schemaWarnings = schemaIssues.filter(i => i.severity === 'warn').length;
2868
+
2869
+ drawRoundedRect(schemaX, y, colWidth, statusHeight, 3, hasSchema ? [240, 249, 255] : [254, 242, 242]);
2870
+ addText('Schema.org', schemaX + 6, y + 12, { size: 8, style: 'bold', color: colors.medium });
2871
+ addText(`${schemaStats.products || 0} Products`, schemaX + 6, y + 24, { size: 12, style: 'bold', color: colors.brandBlue });
2872
+ addText(`${schemaStats.returnPolicies || 0} Returns`, schemaX + 6, y + 36, { size: 7, color: colors.medium });
2873
+
2874
+ // Product Quality Status
2875
+ const productX = margin + (colWidth * 2) + 10;
2876
+ const productCompleteness = data.product_quality?.completeness || 0;
2877
+ const productColor = productCompleteness >= 80 ? [240, 253, 244] : productCompleteness >= 60 ? [255, 251, 235] : [254, 242, 242];
2878
+ drawRoundedRect(productX, y, colWidth, statusHeight, 3, productColor);
2879
+ addText('Data Quality', productX + 6, y + 12, { size: 8, style: 'bold', color: colors.medium });
2880
+ addText(`${productCompleteness}%`, productX + 6, y + 24, { size: 12, style: 'bold', color: productCompleteness >= 80 ? colors.success : productCompleteness >= 60 ? colors.warning : colors.error });
2881
+ addText('Completeness', productX + 6, y + 36, { size: 7, color: colors.medium });
2882
+
2883
+ // Compliance Status
2884
+ const compX = margin + (colWidth * 3) + 15;
2885
+ const isCompliant = schemaStats.returnPolicies > 0 && ucpFound;
2886
+ drawRoundedRect(compX, y, colWidth, statusHeight, 3, isCompliant ? [240, 253, 244] : [255, 251, 235]);
2887
+ addText('Jan 2026', compX + 6, y + 12, { size: 8, style: 'bold', color: colors.medium });
2888
+ addText(isCompliant ? 'Ready' : 'Action Req.', compX + 6, y + 24, {
2889
+ size: 12, style: 'bold', color: isCompliant ? colors.success : colors.warning
2890
+ });
2891
+ const missingItems = [];
2892
+ if (!ucpFound) missingItems.push('UCP');
2893
+ if (schemaStats.returnPolicies === 0) missingItems.push('Returns');
2894
+ addText(isCompliant ? 'All met' : missingItems.join(', '), compX + 6, y + 36, { size: 7, color: colors.medium });
2895
+
2896
+ y += statusHeight + 15;
2897
+
2898
+ // ============ ISSUES & FIXES (Combined View) ============
2899
+ const allIssues = [...(data.ucp?.issues || []), ...(data.schema?.issues || []), ...(data.product_quality?.issues || [])];
2900
+ const suggestions = data.lint_suggestions || [];
2901
+
2902
+ // Create a mapping of issue codes to their fixes
2903
+ const issueCodeToFix = {};
2904
+ suggestions.forEach(s => {
2905
+ if (s.code) {
2906
+ // Map the suggestion code back to issue codes it addresses
2907
+ const relatedCodes = [];
2908
+ if (s.code.includes('VERSION')) relatedCodes.push('UCP_INVALID_VERSION');
2909
+ if (s.code.includes('RETURN') || s.code === 'SCHEMA_NO_RETURN') relatedCodes.push('SCHEMA_NO_RETURN');
2910
+ if (s.code.includes('SHIPPING') || s.code === 'SCHEMA_NO_SHIPPING') relatedCodes.push('SCHEMA_NO_SHIPPING');
2911
+ if (s.code.includes('TRANSPORT') || s.code === 'UCP_NO_TRANSPORT') relatedCodes.push('UCP_NO_TRANSPORT');
2912
+ if (s.code.includes('SERVICE') || s.code === 'UCP_INVALID_SERVICE') relatedCodes.push('UCP_INVALID_SERVICE');
2913
+ if (s.code.includes('CAP') || s.code === 'UCP_INVALID_CAP') relatedCodes.push('UCP_INVALID_CAP');
2914
+ if (s.code.includes('PRODUCT') || s.code === 'SCHEMA_NO_PRODUCT') relatedCodes.push('SCHEMA_NO_PRODUCT');
2915
+ if (s.code.includes('CHECKOUT')) relatedCodes.push('UCP_NO_CHECKOUT');
2916
+ relatedCodes.forEach(rc => { issueCodeToFix[rc] = s; });
2917
+ // Also direct mapping
2918
+ issueCodeToFix[s.code] = s;
2919
+ }
2920
+ });
2921
+
2922
+ if (allIssues.length > 0) {
2923
+ addText(`Issues Found (${allIssues.length} total)`, margin, y, { size: 14, style: 'bold', color: colors.dark });
2924
+ y += 8;
2925
+
2926
+ // Show ALL issues in a compact table format
2927
+ const tableData = allIssues.map(issue => {
2928
+ const severity = issue.severity === 'error' ? 'ERROR' : 'WARNING';
2929
+ const message = issue.message.length > 50 ? issue.message.substring(0, 50) + '...' : issue.message;
2930
+ // Find related fix
2931
+ const fix = issueCodeToFix[issue.code];
2932
+ const fixRef = fix ? fix.title.substring(0, 25) + (fix.title.length > 25 ? '...' : '') : '-';
2933
+ return [severity, issue.code || '-', message, fixRef];
2934
+ });
2935
+
2936
+ doc.autoTable({
2937
+ startY: y,
2938
+ head: [['Severity', 'Code', 'Issue', 'Fix']],
2939
+ body: tableData,
2940
+ margin: { left: margin, right: margin },
2941
+ styles: {
2942
+ fontSize: 7,
2943
+ cellPadding: 2,
2944
+ lineColor: colors.border,
2945
+ lineWidth: 0.1
2946
+ },
2947
+ headStyles: {
2948
+ fillColor: colors.dark,
2949
+ textColor: colors.white,
2950
+ fontStyle: 'bold',
2951
+ fontSize: 7
2952
+ },
2953
+ columnStyles: {
2954
+ 0: { cellWidth: 18, fontStyle: 'bold' },
2955
+ 1: { cellWidth: 32, fontSize: 6 },
2956
+ 2: { cellWidth: 'auto' },
2957
+ 3: { cellWidth: 35, fontSize: 6, textColor: colors.brandBlue }
2958
+ },
2959
+ alternateRowStyles: { fillColor: colors.background },
2960
+ didParseCell: function (cellData) {
2961
+ if (cellData.section === 'body' && cellData.column.index === 0) {
2962
+ if (cellData.cell.raw === 'ERROR') {
2963
+ cellData.cell.styles.textColor = colors.error;
2964
+ } else {
2965
+ cellData.cell.styles.textColor = colors.warning;
2966
+ }
2967
+ }
2968
+ // Highlight fix column
2969
+ if (cellData.section === 'body' && cellData.column.index === 3 && cellData.cell.raw !== '-') {
2970
+ cellData.cell.styles.textColor = colors.brandBlue;
2971
+ cellData.cell.styles.fontStyle = 'bold';
2972
+ }
2973
+ }
2974
+ });
2975
+
2976
+ y = doc.lastAutoTable.finalY + 12;
2977
+ }
2978
+
2979
+ // ============ HOW TO FIX (Detailed Actions) ============
2980
+ if (suggestions.length > 0 && y < pageHeight - 60) {
2981
+ addText('How to Fix', margin, y, { size: 14, style: 'bold', color: colors.dark });
2982
+ y += 3;
2983
+ addText('Each fix addresses one or more issues listed above', margin, y + 5, { size: 8, color: colors.medium });
2984
+ y += 12;
2985
+
2986
+ const maxSuggestions = Math.min(suggestions.length, y < 200 ? 8 : 5);
2987
+ suggestions.slice(0, maxSuggestions).forEach((suggestion, idx) => {
2988
+ if (y > pageHeight - 35) return;
2989
+
2990
+ const severityColor = suggestion.severity === 'critical' ? colors.error :
2991
+ suggestion.severity === 'warning' ? colors.warning : colors.info;
2992
+
2993
+ // Priority badge with number
2994
+ drawRoundedRect(margin, y, 16, 6, 2, severityColor);
2995
+ addText(`${idx + 1}`, margin + 8, y + 4.5, { size: 7, style: 'bold', color: colors.white, align: 'center' });
2996
+
2997
+ // Issue code badge (shows which issue this fixes)
2998
+ const codeText = suggestion.code ? suggestion.code : '';
2999
+ if (codeText) {
3000
+ const codeWidth = Math.min(codeText.length * 1.8 + 6, 40);
3001
+ drawRoundedRect(margin + 18, y, codeWidth, 6, 2, colors.background);
3002
+ addText(codeText, margin + 18 + codeWidth/2, y + 4.5, { size: 6, color: colors.medium, align: 'center' });
3003
+ }
3004
+
3005
+ // Title
3006
+ addText(suggestion.title, margin + (codeText ? 62 : 22), y + 4.5, { size: 9, style: 'bold', color: colors.dark });
3007
+ y += 8;
3008
+
3009
+ // Impact/description
3010
+ const impactText = suggestion.impact || suggestion.fix || '';
3011
+ if (impactText) {
3012
+ addText(impactText.substring(0, 90) + (impactText.length > 90 ? '...' : ''), margin + 18, y, { size: 7, color: colors.medium, maxWidth: contentWidth - 20 });
3013
+ y += 7;
3014
+ }
3015
+ y += 2;
3016
+ });
3017
+
3018
+ if (suggestions.length > maxSuggestions) {
3019
+ addText(`+ ${suggestions.length - maxSuggestions} more fixes - see full report at ucptools.dev`, margin, y, { size: 7, color: colors.medium });
3020
+ y += 8;
3021
+ }
3022
+ }
3023
+
3024
+ // ============ FOOTER ============
3025
+ const footerY = pageHeight - 20;
3026
+
3027
+ // Footer line
3028
+ doc.setDrawColor(...colors.border);
3029
+ doc.setLineWidth(0.5);
3030
+ doc.line(margin, footerY - 5, pageWidth - margin, footerY - 5);
3031
+
3032
+ // Footer content
3033
+ addText('Generated by UCP.tools', margin, footerY, { size: 8, color: colors.medium });
3034
+ addText('ucptools.dev', margin, footerY + 5, { size: 8, style: 'bold', color: colors.brandBlue });
3035
+
3036
+ // QR code substitute - call to action
3037
+ drawRoundedRect(pageWidth - margin - 55, footerY - 8, 55, 18, 2, colors.background);
3038
+ addText('Validate your store:', pageWidth - margin - 52, footerY - 1, { size: 7, color: colors.medium });
3039
+ addText('ucptools.dev', pageWidth - margin - 52, footerY + 6, { size: 9, style: 'bold', color: colors.brandBlue });
3040
+
3041
+ // Page number
3042
+ addText('Page 1 of 1', pageWidth / 2, footerY + 5, { size: 8, color: colors.light, align: 'center' });
3043
+
3044
+ // Download
3045
+ doc.save(`ucp-report-${domain.replace(/[^a-zA-Z0-9]/g, '-')}.pdf`);
3046
+
3047
+ // Track download
3048
+ if (typeof gtag === 'function') {
3049
+ gtag('event', 'download_report', { domain: domain, score: score, grade: grade });
3050
+ }
3051
+ }
3052
+
3053
+ // Close dropdowns helper
3054
+ function closeDropdowns() {
3055
+ // Dropdowns auto-close via CSS :hover, but this helps on touch devices
3056
+ }
3057
+
3058
+ // Generate form
3059
+ document.getElementById('generate-form').addEventListener('submit', async (e) => {
3060
+ e.preventDefault();
3061
+ const btn = document.getElementById('generate-btn');
3062
+ const result = document.getElementById('generate-result');
3063
+
3064
+ const domain = document.getElementById('gen-domain').value.replace(/^https?:\/\//, '').replace(/\/$/, '');
3065
+ const endpoint = document.getElementById('gen-endpoint').value;
3066
+ const capabilities = {
3067
+ checkout: true,
3068
+ order: document.getElementById('cap-order').checked,
3069
+ fulfillment: document.getElementById('cap-fulfillment').checked,
3070
+ discount: document.getElementById('cap-discount').checked,
3071
+ };
3072
+
3073
+ btn.disabled = true;
3074
+ btn.textContent = 'Generating...';
3075
+
3076
+ try {
3077
+ const res = await fetch('/api/generate', {
3078
+ method: 'POST',
3079
+ headers: { 'Content-Type': 'application/json' },
3080
+ body: JSON.stringify({ domain, endpoint, capabilities })
3081
+ });
3082
+ const data = await res.json();
3083
+
3084
+ const profileJson = JSON.stringify(data.profile, null, 2);
3085
+
3086
+ result.className = 'result show success';
3087
+ result.innerHTML = `
3088
+ <p style="margin-bottom: 16px;">\u2713 Profile generated! Copy the JSON below:</p>
3089
+ <pre>${escapeHtml(profileJson)}</pre>
3090
+ <div style="margin-top: 16px; display: flex; gap: 12px;">
3091
+ <button class="btn" onclick="downloadProfile('${domain}')">Download JSON</button>
3092
+ <button class="btn btn-secondary" onclick="copyProfile()">Copy to Clipboard</button>
3093
+ </div>
3094
+ <p style="margin-top: 16px; font-size: 14px;" class="text-muted">
3095
+ Deploy to <code>https://${domain}/.well-known/ucp</code> &bull;
3096
+ <a href="#" onclick="showInstructions(); return false;" class="link-brand">See setup instructions \u2192</a>
3097
+ </p>
3098
+ `;
3099
+
3100
+ window.generatedProfile = profileJson;
3101
+ } catch (err) {
3102
+ result.className = 'result show error';
3103
+ result.innerHTML = `<p>Error: ${err.message}</p>`;
3104
+ }
3105
+
3106
+ btn.disabled = false;
3107
+ btn.textContent = 'Generate Profile';
3108
+ });
3109
+
3110
+ function escapeHtml(str) {
3111
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
3112
+ }
3113
+
3114
+ function downloadProfile(domain) {
3115
+ const blob = new Blob([window.generatedProfile], { type: 'application/json' });
3116
+ const url = URL.createObjectURL(blob);
3117
+ const a = document.createElement('a');
3118
+ a.href = url;
3119
+ a.download = 'ucp.json';
3120
+ a.click();
3121
+ }
3122
+
3123
+ function copyProfile() {
3124
+ navigator.clipboard.writeText(window.generatedProfile);
3125
+ alert('Copied to clipboard!');
3126
+ }
3127
+
3128
+ function showInstructions() {
3129
+ alert('Setup instructions:\n\n1. Upload ucp.json to your server\n2. Configure your web server to serve it at /.well-known/ucp\n3. Set Content-Type: application/json\n4. Add CORS headers: Access-Control-Allow-Origin: *\n\nSee our documentation for platform-specific guides.');
3130
+ }
3131
+
3132
+ // AI Agent Simulation form
3133
+ document.getElementById('simulate-form').addEventListener('submit', async (e) => {
3134
+ e.preventDefault();
3135
+ const btn = document.getElementById('simulate-btn');
3136
+ const result = document.getElementById('simulate-result');
3137
+ const domain = document.getElementById('simulate-domain').value.replace(/^https?:\/\//, '').replace(/\/$/, '');
3138
+
3139
+ btn.disabled = true;
3140
+ btn.textContent = '🔄 Running Simulation...';
3141
+ result.className = 'result';
3142
+ result.innerHTML = '';
3143
+
3144
+ try {
3145
+ const res = await fetch('/api/simulate', {
3146
+ method: 'POST',
3147
+ headers: { 'Content-Type': 'application/json' },
3148
+ body: JSON.stringify({ domain })
3149
+ });
3150
+ const data = await res.json();
3151
+
3152
+ if (data.error) {
3153
+ throw new Error(data.message || data.error);
3154
+ }
3155
+
3156
+ const score = data.score || 0;
3157
+ const grade = data.grade || 'F';
3158
+
3159
+ // Grade colors
3160
+ const gradeColors = {
3161
+ 'A': { bg: '#dcfce7', border: '#86efac', text: '#16A34A' },
3162
+ 'B': { bg: '#dbeafe', border: '#93c5fd', text: '#2563EB' },
3163
+ 'C': { bg: '#fef9c3', border: '#fde047', text: '#CA8A04' },
3164
+ 'D': { bg: '#fed7aa', border: '#fdba74', text: '#EA580C' },
3165
+ 'F': { bg: '#fee2e2', border: '#fca5a5', text: '#DC2626' },
3166
+ };
3167
+ const colors = gradeColors[grade] || gradeColors['F'];
3168
+
3169
+ result.className = 'result show';
3170
+ result.style.background = colors.bg;
3171
+ result.style.border = '1px solid ' + colors.border;
3172
+
3173
+ // Build step results HTML
3174
+ const renderSteps = (steps) => {
3175
+ if (!steps || steps.length === 0) return '';
3176
+ return steps.map(s => {
3177
+ const icons = { passed: '✅', failed: '❌', warning: '⚠️', skipped: '⏭️' };
3178
+ const icon = icons[s.status] || '•';
3179
+ const bgColors = { passed: 'rgba(22,163,74,0.1)', failed: 'rgba(220,38,38,0.1)', warning: 'rgba(202,138,4,0.1)', skipped: 'rgba(100,116,139,0.1)' };
3180
+ return `<div style="padding: 8px 12px; margin: 4px 0; border-radius: 6px; background: ${bgColors[s.status] || 'transparent'}; font-size: 13px;">
3181
+ <span style="margin-right: 8px;">${icon}</span>
3182
+ <strong>${s.step}</strong>: ${s.message}
3183
+ ${s.details ? `<br><small style="color: #666; margin-left: 28px;">${s.details}</small>` : ''}
3184
+ </div>`;
3185
+ }).join('');
3186
+ };
3187
+
3188
+ result.innerHTML = `
3189
+ <!-- Score Header -->
3190
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px;">
3191
+ <div>
3192
+ <span class="score-badge ${grade}">${grade} ${score}/100</span>
3193
+ <span style="margin-left: 12px; font-weight: 600; color: ${colors.text};">AI Agent ${data.ok ? 'Ready' : 'Not Ready'}</span>
3194
+ </div>
3195
+ <span style="font-size: 13px; color: #666;">Completed in ${data.duration_ms}ms</span>
3196
+ </div>
3197
+
3198
+ <!-- Summary Stats -->
3199
+ <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px;">
3200
+ <div style="text-align: center; padding: 12px; background: rgba(22,163,74,0.1); border-radius: 8px;">
3201
+ <div style="font-size: 24px; font-weight: 700; color: #16A34A;">${data.summary?.passed || 0}</div>
3202
+ <div style="font-size: 12px; color: #666;">Passed</div>
3203
+ </div>
3204
+ <div style="text-align: center; padding: 12px; background: rgba(220,38,38,0.1); border-radius: 8px;">
3205
+ <div style="font-size: 24px; font-weight: 700; color: #DC2626;">${data.summary?.failed || 0}</div>
3206
+ <div style="font-size: 12px; color: #666;">Failed</div>
3207
+ </div>
3208
+ <div style="text-align: center; padding: 12px; background: rgba(202,138,4,0.1); border-radius: 8px;">
3209
+ <div style="font-size: 24px; font-weight: 700; color: #CA8A04;">${data.summary?.warnings || 0}</div>
3210
+ <div style="font-size: 12px; color: #666;">Warnings</div>
3211
+ </div>
3212
+ <div style="text-align: center; padding: 12px; background: rgba(100,116,139,0.1); border-radius: 8px;">
3213
+ <div style="font-size: 24px; font-weight: 700; color: #64748B;">${data.summary?.skipped || 0}</div>
3214
+ <div style="font-size: 12px; color: #666;">Skipped</div>
3215
+ </div>
3216
+ </div>
3217
+
3218
+ <!-- Discovery Flow -->
3219
+ <div style="margin-bottom: 16px; padding: 16px; background: rgba(46,134,171,0.1); border-radius: 8px;">
3220
+ <h4 style="margin-bottom: 12px; color: #2E86AB; display: flex; align-items: center; gap: 8px;">
3221
+ <span>🔍 Discovery Flow</span>
3222
+ ${data.discovery?.success ? '<span style="font-size: 12px; padding: 2px 8px; background: #dcfce7; color: #16A34A; border-radius: 12px;">Success</span>' : '<span style="font-size: 12px; padding: 2px 8px; background: #fee2e2; color: #DC2626; border-radius: 12px;">Failed</span>'}
3223
+ </h4>
3224
+ ${data.discovery?.profile_url ? `<p style="margin-bottom: 8px; font-size: 13px;">Profile: <code>${data.discovery.profile_url}</code></p>` : ''}
3225
+ ${data.discovery?.services?.length > 0 ? `<p style="margin-bottom: 8px; font-size: 13px;">Services: ${data.discovery.services.join(', ')}</p>` : ''}
3226
+ ${data.discovery?.capabilities?.length > 0 ? `<p style="margin-bottom: 8px; font-size: 13px;">Capabilities: ${data.discovery.capabilities.join(', ')}</p>` : ''}
3227
+ ${data.discovery?.transports?.length > 0 ? `<p style="margin-bottom: 8px; font-size: 13px;">Transports: ${data.discovery.transports.join(', ')}</p>` : ''}
3228
+ <details style="margin-top: 8px;">
3229
+ <summary style="cursor: pointer; font-size: 13px; color: #2E86AB;">View step details</summary>
3230
+ <div style="margin-top: 8px;">${renderSteps(data.discovery?.steps)}</div>
3231
+ </details>
3232
+ </div>
3233
+
3234
+ <!-- REST API Test -->
3235
+ ${data.rest_api ? `
3236
+ <div style="margin-bottom: 16px; padding: 16px; background: rgba(99,102,241,0.1); border-radius: 8px;">
3237
+ <h4 style="margin-bottom: 12px; color: #6366F1; display: flex; align-items: center; gap: 8px;">
3238
+ <span>🌐 REST API Test</span>
3239
+ ${data.rest_api.success ? '<span style="font-size: 12px; padding: 2px 8px; background: #dcfce7; color: #16A34A; border-radius: 12px;">Success</span>' : '<span style="font-size: 12px; padding: 2px 8px; background: #fee2e2; color: #DC2626; border-radius: 12px;">Issues Found</span>'}
3240
+ </h4>
3241
+ <p style="font-size: 13px;">Schema: ${data.rest_api.schema_loaded ? '✅ Loaded' : '❌ Not loaded'} | Endpoint: ${data.rest_api.endpoint_accessible ? '✅ Accessible' : '❌ Not accessible'}</p>
3242
+ <details style="margin-top: 8px;">
3243
+ <summary style="cursor: pointer; font-size: 13px; color: #6366F1;">View step details</summary>
3244
+ <div style="margin-top: 8px;">${renderSteps(data.rest_api?.steps)}</div>
3245
+ </details>
3246
+ </div>
3247
+ ` : ''}
3248
+
3249
+ <!-- Checkout Flow -->
3250
+ ${data.checkout ? `
3251
+ <div style="margin-bottom: 16px; padding: 16px; background: rgba(71,201,122,0.1); border-radius: 8px;">
3252
+ <h4 style="margin-bottom: 12px; color: #36B5A2; display: flex; align-items: center; gap: 8px;">
3253
+ <span>🛒 Checkout Flow</span>
3254
+ ${data.checkout.success ? '<span style="font-size: 12px; padding: 2px 8px; background: #dcfce7; color: #16A34A; border-radius: 12px;">Ready</span>' : '<span style="font-size: 12px; padding: 2px 8px; background: #fef9c3; color: #CA8A04; border-radius: 12px;">Incomplete</span>'}
3255
+ </h4>
3256
+ <div style="display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; margin-bottom: 8px;">
3257
+ <span>${data.checkout.can_create_checkout ? '✅' : '❌'} Checkout</span>
3258
+ <span>${data.checkout.order_flow_supported ? '✅' : '⚪'} Order Tracking</span>
3259
+ <span>${data.checkout.fulfillment_supported ? '✅' : '⚪'} Fulfillment</span>
3260
+ </div>
3261
+ <details style="margin-top: 8px;">
3262
+ <summary style="cursor: pointer; font-size: 13px; color: #36B5A2;">View step details</summary>
3263
+ <div style="margin-top: 8px;">${renderSteps(data.checkout?.steps)}</div>
3264
+ </details>
3265
+ </div>
3266
+ ` : ''}
3267
+
3268
+ <!-- Payment Readiness -->
3269
+ ${data.payment ? `
3270
+ <div style="margin-bottom: 16px; padding: 16px; background: rgba(245,158,11,0.1); border-radius: 8px;">
3271
+ <h4 style="margin-bottom: 12px; color: #D97706; display: flex; align-items: center; gap: 8px;">
3272
+ <span>💳 Payment Readiness</span>
3273
+ ${data.payment.success ? '<span style="font-size: 12px; padding: 2px 8px; background: #dcfce7; color: #16A34A; border-radius: 12px;">Configured</span>' : '<span style="font-size: 12px; padding: 2px 8px; background: #fef9c3; color: #CA8A04; border-radius: 12px;">Needs Config</span>'}
3274
+ </h4>
3275
+ <div style="display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; margin-bottom: 8px;">
3276
+ <span>${data.payment.handlers_found > 0 ? '✅' : '❌'} Payment Handlers (${data.payment.handlers_found})</span>
3277
+ <span>${data.payment.signing_key_valid ? '✅' : '⚪'} Webhook Signing Keys</span>
3278
+ </div>
3279
+ <details style="margin-top: 8px;">
3280
+ <summary style="cursor: pointer; font-size: 13px; color: #D97706;">View step details</summary>
3281
+ <div style="margin-top: 8px;">${renderSteps(data.payment?.steps)}</div>
3282
+ </details>
3283
+ </div>
3284
+ ` : ''}
3285
+
3286
+ <!-- Recommendations -->
3287
+ ${data.recommendations && data.recommendations.length > 0 ? `
3288
+ <div style="padding: 16px; background: linear-gradient(135deg, rgba(46,134,171,0.1), rgba(71,201,122,0.1)); border-radius: 8px;">
3289
+ <h4 style="margin-bottom: 12px; color: var(--color-dark);">💡 Recommendations</h4>
3290
+ <ul style="margin: 0; padding-left: 20px; font-size: 14px; color: var(--color-dark);">
3291
+ ${data.recommendations.map(r => `<li style="margin-bottom: 8px;">${r}</li>`).join('')}
3292
+ </ul>
3293
+ </div>
3294
+ ` : ''}
3295
+ `;
3296
+
3297
+ } catch (err) {
3298
+ result.className = 'result show error';
3299
+ result.innerHTML = `<p>❌ Simulation failed: ${err.message}</p><p style="font-size: 13px; margin-top: 8px;">Make sure the domain has a valid UCP profile at /.well-known/ucp</p>`;
3300
+ }
3301
+
3302
+ btn.disabled = false;
3303
+ btn.textContent = '🚀 Run AI Agent Simulation';
3304
+ });
3305
+
3306
+ // Security Scanner form
3307
+ document.getElementById('security-form').addEventListener('submit', async (e) => {
3308
+ e.preventDefault();
3309
+ const btn = document.getElementById('security-btn');
3310
+ const result = document.getElementById('security-result');
3311
+ const domain = document.getElementById('security-domain').value.replace(/^https?:\/\//, '').replace(/\/$/, '');
3312
+
3313
+ btn.disabled = true;
3314
+ btn.textContent = '🔄 Running Security Scan...';
3315
+ result.className = 'result';
3316
+ result.innerHTML = '';
3317
+
3318
+ try {
3319
+ const res = await fetch('/api/security-scan', {
3320
+ method: 'POST',
3321
+ headers: { 'Content-Type': 'application/json' },
3322
+ body: JSON.stringify({ domain })
3323
+ });
3324
+ const data = await res.json();
3325
+
3326
+ if (data.error) {
3327
+ throw new Error(data.message || data.error);
3328
+ }
3329
+
3330
+ const score = data.score || 0;
3331
+ const grade = data.grade || 'F';
3332
+
3333
+ // Grade colors
3334
+ const gradeColors = {
3335
+ 'A': { bg: '#dcfce7', border: '#86efac', text: '#16A34A' },
3336
+ 'B': { bg: '#dbeafe', border: '#93c5fd', text: '#2563EB' },
3337
+ 'C': { bg: '#fef9c3', border: '#fde047', text: '#CA8A04' },
3338
+ 'D': { bg: '#fed7aa', border: '#fdba74', text: '#EA580C' },
3339
+ 'F': { bg: '#fee2e2', border: '#fca5a5', text: '#DC2626' },
3340
+ };
3341
+ const colors = gradeColors[grade] || gradeColors['F'];
3342
+
3343
+ // Severity colors
3344
+ const severityColors = {
3345
+ 'critical': { bg: 'rgba(127,29,29,0.1)', text: '#7F1D1D', icon: '🔴' },
3346
+ 'high': { bg: 'rgba(220,38,38,0.1)', text: '#DC2626', icon: '🟠' },
3347
+ 'medium': { bg: 'rgba(202,138,4,0.1)', text: '#CA8A04', icon: '🟡' },
3348
+ 'low': { bg: 'rgba(37,99,235,0.1)', text: '#2563EB', icon: '🔵' },
3349
+ 'info': { bg: 'rgba(100,116,139,0.1)', text: '#64748B', icon: 'ℹ️' },
3350
+ };
3351
+
3352
+ // Status icons
3353
+ const statusIcons = { pass: '✅', fail: '❌', warn: '⚠️', skip: '⏭️' };
3354
+
3355
+ result.className = 'result show';
3356
+ result.style.background = colors.bg;
3357
+ result.style.border = '1px solid ' + colors.border;
3358
+
3359
+ // Render security checks
3360
+ const renderChecks = (checks) => {
3361
+ if (!checks || checks.length === 0) return '<p>No checks performed</p>';
3362
+ return checks.map(c => {
3363
+ const sevColors = severityColors[c.severity] || severityColors['info'];
3364
+ const statusBg = c.status === 'pass' ? 'rgba(22,163,74,0.1)' :
3365
+ c.status === 'fail' ? 'rgba(220,38,38,0.1)' :
3366
+ c.status === 'warn' ? 'rgba(202,138,4,0.1)' :
3367
+ 'rgba(100,116,139,0.1)';
3368
+ return `
3369
+ <div style="padding: 12px 16px; margin: 8px 0; border-radius: 8px; background: ${statusBg}; border-left: 4px solid ${sevColors.text};">
3370
+ <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;">
3371
+ <div style="display: flex; align-items: center; gap: 8px;">
3372
+ <span style="font-size: 16px;">${statusIcons[c.status] || '•'}</span>
3373
+ <strong style="color: var(--color-dark);">${c.name}</strong>
3374
+ <span style="font-size: 11px; padding: 2px 6px; background: ${sevColors.bg}; color: ${sevColors.text}; border-radius: 4px; text-transform: uppercase;">${c.severity}</span>
3375
+ </div>
3376
+ </div>
3377
+ <p style="margin: 8px 0 0 28px; font-size: 13px; color: var(--color-medium);">${c.details || c.description}</p>
3378
+ ${c.recommendation ? `<p style="margin: 4px 0 0 28px; font-size: 12px; color: ${sevColors.text};"><strong>Fix:</strong> ${c.recommendation}</p>` : ''}
3379
+ </div>
3380
+ `;
3381
+ }).join('');
3382
+ };
3383
+
3384
+ result.innerHTML = `
3385
+ <!-- Score Header -->
3386
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px;">
3387
+ <div>
3388
+ <span class="score-badge ${grade}">${grade} ${score}/100</span>
3389
+ <span style="margin-left: 12px; font-weight: 600; color: ${colors.text};">Security ${data.ok ? 'Good' : 'Needs Attention'}</span>
3390
+ </div>
3391
+ <span style="font-size: 13px; color: #666;">Scanned: ${data.endpoint}</span>
3392
+ </div>
3393
+
3394
+ <!-- Summary Stats -->
3395
+ <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px;">
3396
+ <div style="text-align: center; padding: 12px; background: rgba(22,163,74,0.1); border-radius: 8px;">
3397
+ <div style="font-size: 24px; font-weight: 700; color: #16A34A;">${data.summary?.passed || 0}</div>
3398
+ <div style="font-size: 12px; color: #666;">Passed</div>
3399
+ </div>
3400
+ <div style="text-align: center; padding: 12px; background: rgba(220,38,38,0.1); border-radius: 8px;">
3401
+ <div style="font-size: 24px; font-weight: 700; color: #DC2626;">${data.summary?.failed || 0}</div>
3402
+ <div style="font-size: 12px; color: #666;">Failed</div>
3403
+ </div>
3404
+ <div style="text-align: center; padding: 12px; background: rgba(202,138,4,0.1); border-radius: 8px;">
3405
+ <div style="font-size: 24px; font-weight: 700; color: #CA8A04;">${data.summary?.warnings || 0}</div>
3406
+ <div style="font-size: 12px; color: #666;">Warnings</div>
3407
+ </div>
3408
+ <div style="text-align: center; padding: 12px; background: rgba(100,116,139,0.1); border-radius: 8px;">
3409
+ <div style="font-size: 24px; font-weight: 700; color: #64748B;">${data.summary?.skipped || 0}</div>
3410
+ <div style="font-size: 12px; color: #666;">Skipped</div>
3411
+ </div>
3412
+ </div>
3413
+
3414
+ <!-- Critical Issues -->
3415
+ ${data.critical_issues && data.critical_issues.length > 0 ? `
3416
+ <div style="margin-bottom: 20px; padding: 16px; background: rgba(220,38,38,0.15); border-radius: 8px; border: 1px solid rgba(220,38,38,0.3);">
3417
+ <h4 style="margin-bottom: 12px; color: #DC2626; display: flex; align-items: center; gap: 8px;">
3418
+ <span>🚨 Critical Issues (${data.critical_issues.length})</span>
3419
+ </h4>
3420
+ <p style="font-size: 13px; margin-bottom: 12px; color: #7F1D1D;">These issues should be addressed immediately:</p>
3421
+ ${renderChecks(data.critical_issues)}
3422
+ </div>
3423
+ ` : ''}
3424
+
3425
+ <!-- All Checks -->
3426
+ <div style="margin-bottom: 16px;">
3427
+ <h4 style="margin-bottom: 12px; color: var(--color-dark);">🔍 All Security Checks</h4>
3428
+ ${renderChecks(data.checks)}
3429
+ </div>
3430
+
3431
+ <!-- Recommendations -->
3432
+ <div style="padding: 16px; background: linear-gradient(135deg, rgba(46,134,171,0.1), rgba(71,201,122,0.1)); border-radius: 8px;">
3433
+ <h4 style="margin-bottom: 12px; color: var(--color-dark);">💡 What This Means</h4>
3434
+ <ul style="margin: 0; padding-left: 20px; font-size: 14px; color: var(--color-dark);">
3435
+ <li style="margin-bottom: 8px;">UCP exposes your checkout to AI agents - security is critical</li>
3436
+ <li style="margin-bottom: 8px;">Implement rate limiting to prevent abuse and DoS attacks</li>
3437
+ <li style="margin-bottom: 8px;">HTTPS and security headers protect against common attacks</li>
3438
+ <li style="margin-bottom: 8px;">CORS should be configured to allow AI agent access</li>
3439
+ </ul>
3440
+ </div>
3441
+ `;
3442
+
3443
+ } catch (err) {
3444
+ result.className = 'result show error';
3445
+ result.innerHTML = `<p>❌ Security scan failed: ${err.message}</p><p style="font-size: 13px; margin-top: 8px;">Make sure the domain is accessible.</p>`;
3446
+ }
3447
+
3448
+ btn.disabled = false;
3449
+ btn.textContent = '🔍 Run Security Scan';
3450
+ });
3451
+
3452
+ // Feed Analyzer form
3453
+ document.getElementById('feed-form').addEventListener('submit', async (e) => {
3454
+ e.preventDefault();
3455
+ const btn = document.getElementById('feed-btn');
3456
+ const result = document.getElementById('feed-result');
3457
+ const url = document.getElementById('feed-url').value.trim();
3458
+ const maxProducts = parseInt(document.getElementById('feed-max-products').value);
3459
+
3460
+ btn.disabled = true;
3461
+ btn.textContent = '🔄 Analyzing Feed...';
3462
+ result.className = 'result';
3463
+ result.innerHTML = '';
3464
+
3465
+ try {
3466
+ const res = await fetch('/api/analyze-feed', {
3467
+ method: 'POST',
3468
+ headers: { 'Content-Type': 'application/json' },
3469
+ body: JSON.stringify({ url, maxProducts, includeProductDetails: true })
3470
+ });
3471
+ const data = await res.json();
3472
+
3473
+ if (data.error) {
3474
+ throw new Error(data.message || data.error);
3475
+ }
3476
+
3477
+ const score = data.overallScore || 0;
3478
+ const visibilityScore = data.agentVisibilityScore || 0;
3479
+ const grade = data.grade || 'F';
3480
+
3481
+ // Grade colors
3482
+ const gradeColors = {
3483
+ 'A': { bg: '#dcfce7', border: '#86efac', text: '#16A34A' },
3484
+ 'B': { bg: '#dbeafe', border: '#93c5fd', text: '#2563EB' },
3485
+ 'C': { bg: '#fef9c3', border: '#fde047', text: '#CA8A04' },
3486
+ 'D': { bg: '#fed7aa', border: '#fdba74', text: '#EA580C' },
3487
+ 'F': { bg: '#fee2e2', border: '#fca5a5', text: '#DC2626' },
3488
+ };
3489
+ const colors = gradeColors[grade] || gradeColors['F'];
3490
+
3491
+ // Priority colors
3492
+ const priorityColors = {
3493
+ 'high': { bg: 'rgba(220,38,38,0.1)', text: '#DC2626', icon: '🔴' },
3494
+ 'medium': { bg: 'rgba(202,138,4,0.1)', text: '#CA8A04', icon: '🟡' },
3495
+ 'low': { bg: 'rgba(37,99,235,0.1)', text: '#2563EB', icon: '🔵' },
3496
+ };
3497
+
3498
+ result.className = 'result show';
3499
+ result.style.background = colors.bg;
3500
+ result.style.border = '1px solid ' + colors.border;
3501
+
3502
+ // Calculate percentages for summary
3503
+ const total = data.summary?.totalProducts || 0;
3504
+ const pct = (val) => total > 0 ? Math.round((val / total) * 100) : 0;
3505
+
3506
+ // Render category score bar
3507
+ const renderCategoryBar = (name, score, color) => `
3508
+ <div style="margin-bottom: 8px;">
3509
+ <div style="display: flex; justify-content: space-between; margin-bottom: 2px; font-size: 12px;">
3510
+ <span>${name}</span>
3511
+ <span style="font-weight: 600;">${score}%</span>
3512
+ </div>
3513
+ <div style="height: 8px; background: rgba(0,0,0,0.1); border-radius: 4px; overflow: hidden;">
3514
+ <div style="height: 100%; width: ${score}%; background: ${color}; border-radius: 4px;"></div>
3515
+ </div>
3516
+ </div>
3517
+ `;
3518
+
3519
+ // Render recommendations
3520
+ const renderRecommendations = (recs) => {
3521
+ if (!recs || recs.length === 0) return '<p style="color: #666; font-size: 13px;">No recommendations - your feed looks great!</p>';
3522
+ return recs.map(r => {
3523
+ const pColors = priorityColors[r.priority] || priorityColors['medium'];
3524
+ return `
3525
+ <div style="padding: 12px 16px; margin: 8px 0; border-radius: 8px; background: ${pColors.bg}; border-left: 4px solid ${pColors.text};">
3526
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
3527
+ <span>${pColors.icon}</span>
3528
+ <strong style="color: var(--color-dark);">${r.title}</strong>
3529
+ <span style="font-size: 11px; padding: 2px 6px; background: rgba(0,0,0,0.1); border-radius: 4px;">${r.affectedCount} products</span>
3530
+ </div>
3531
+ <p style="margin: 0 0 0 28px; font-size: 13px; color: var(--color-medium);">${r.description}</p>
3532
+ <p style="margin: 4px 0 0 28px; font-size: 12px; color: ${pColors.text};"><strong>Impact:</strong> ${r.impact}</p>
3533
+ </div>
3534
+ `;
3535
+ }).join('');
3536
+ };
3537
+
3538
+ result.innerHTML = `
3539
+ <!-- Score Header -->
3540
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px;">
3541
+ <div>
3542
+ <span class="score-badge ${grade}">${grade} ${score}/100</span>
3543
+ <span style="margin-left: 12px; font-weight: 600; color: ${colors.text};">Feed Quality</span>
3544
+ </div>
3545
+ <div style="text-align: right;">
3546
+ <div style="font-size: 13px; color: #666;">Analyzed: ${data.productsAnalyzed}/${data.productsFound} products</div>
3547
+ </div>
3548
+ </div>
3549
+
3550
+ ${data.warning ? `
3551
+ <div style="padding: 12px 16px; margin-bottom: 20px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; font-size: 13px;">
3552
+ <strong>⚠️ ${data.warning}:</strong> ${data.message}
3553
+ </div>
3554
+ ` : ''}
3555
+
3556
+ <!-- AI Visibility Score -->
3557
+ <div style="padding: 20px; margin-bottom: 20px; background: linear-gradient(135deg, rgba(46,134,171,0.15), rgba(71,201,122,0.15)); border-radius: 12px; text-align: center;">
3558
+ <div style="font-size: 14px; color: #666; margin-bottom: 8px;">AI Agent Visibility Score</div>
3559
+ <div style="font-size: 48px; font-weight: 700; color: ${visibilityScore >= 80 ? '#16A34A' : visibilityScore >= 60 ? '#CA8A04' : '#DC2626'};">${visibilityScore}%</div>
3560
+ <div style="font-size: 13px; color: #666; margin-top: 8px;">
3561
+ ${visibilityScore >= 80 ? '✅ Excellent - AI agents can easily discover and recommend your products' :
3562
+ visibilityScore >= 60 ? '⚠️ Fair - Some products may not be visible to AI shopping agents' :
3563
+ '❌ Low - Many products are invisible to AI shopping agents'}
3564
+ </div>
3565
+ </div>
3566
+
3567
+ <!-- Summary Stats -->
3568
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 12px; margin-bottom: 20px;">
3569
+ <div style="text-align: center; padding: 12px; background: rgba(22,163,74,0.1); border-radius: 8px;">
3570
+ <div style="font-size: 20px; font-weight: 700; color: #16A34A;">${pct(data.summary?.withName)}%</div>
3571
+ <div style="font-size: 11px; color: #666;">Have Names</div>
3572
+ </div>
3573
+ <div style="text-align: center; padding: 12px; background: rgba(37,99,235,0.1); border-radius: 8px;">
3574
+ <div style="font-size: 20px; font-weight: 700; color: #2563EB;">${pct(data.summary?.withPrice)}%</div>
3575
+ <div style="font-size: 11px; color: #666;">Have Prices</div>
3576
+ </div>
3577
+ <div style="text-align: center; padding: 12px; background: rgba(147,51,234,0.1); border-radius: 8px;">
3578
+ <div style="font-size: 20px; font-weight: 700; color: #9333EA;">${pct(data.summary?.withImages)}%</div>
3579
+ <div style="font-size: 11px; color: #666;">Have Images</div>
3580
+ </div>
3581
+ <div style="text-align: center; padding: 12px; background: rgba(234,88,12,0.1); border-radius: 8px;">
3582
+ <div style="font-size: 20px; font-weight: 700; color: #EA580C;">${pct(data.summary?.withGtin)}%</div>
3583
+ <div style="font-size: 11px; color: #666;">Have GTIN</div>
3584
+ </div>
3585
+ <div style="text-align: center; padding: 12px; background: rgba(20,184,166,0.1); border-radius: 8px;">
3586
+ <div style="font-size: 20px; font-weight: 700; color: #14B8A6;">${pct(data.summary?.withDescription)}%</div>
3587
+ <div style="font-size: 11px; color: #666;">Have Desc</div>
3588
+ </div>
3589
+ </div>
3590
+
3591
+ <!-- Category Scores -->
3592
+ <div style="margin-bottom: 20px; padding: 16px; background: rgba(255,255,255,0.5); border-radius: 8px;">
3593
+ <h4 style="margin-bottom: 12px; color: var(--color-dark);">📊 Category Breakdown</h4>
3594
+ ${renderCategoryBar('Completeness', data.categoryScores?.completeness || 0, '#2E86AB')}
3595
+ ${renderCategoryBar('Pricing', data.categoryScores?.pricing || 0, '#16A34A')}
3596
+ ${renderCategoryBar('Images', data.categoryScores?.images || 0, '#9333EA')}
3597
+ ${renderCategoryBar('Identifiers (SKU/GTIN)', data.categoryScores?.identifiers || 0, '#EA580C')}
3598
+ ${renderCategoryBar('Descriptions', data.categoryScores?.descriptions || 0, '#14B8A6')}
3599
+ ${renderCategoryBar('Availability', data.categoryScores?.availability || 0, '#64748B')}
3600
+ </div>
3601
+
3602
+ <!-- Recommendations -->
3603
+ <div style="margin-bottom: 16px;">
3604
+ <h4 style="margin-bottom: 12px; color: var(--color-dark);">💡 Recommendations</h4>
3605
+ ${renderRecommendations(data.recommendations)}
3606
+ </div>
3607
+
3608
+ <!-- Info Box -->
3609
+ <div style="padding: 16px; background: linear-gradient(135deg, rgba(46,134,171,0.1), rgba(71,201,122,0.1)); border-radius: 8px;">
3610
+ <h4 style="margin-bottom: 12px; color: var(--color-dark);">🤖 Why This Matters</h4>
3611
+ <ul style="margin: 0; padding-left: 20px; font-size: 14px; color: var(--color-dark);">
3612
+ <li style="margin-bottom: 8px;">AI shopping agents use Schema.org Product data to understand your catalog</li>
3613
+ <li style="margin-bottom: 8px;">Missing prices mean AI agents can't complete purchases</li>
3614
+ <li style="margin-bottom: 8px;">GTINs enable cross-platform product matching for better visibility</li>
3615
+ <li style="margin-bottom: 8px;">Rich descriptions help AI agents recommend the right products</li>
3616
+ </ul>
3617
+ </div>
3618
+ `;
3619
+
3620
+ } catch (err) {
3621
+ result.className = 'result show error';
3622
+ result.innerHTML = `<p>❌ Feed analysis failed: ${err.message}</p><p style="font-size: 13px; margin-top: 8px;">Make sure the URL is accessible and contains Schema.org Product markup.</p>`;
3623
+ }
3624
+
3625
+ btn.disabled = false;
3626
+ btn.textContent = '🔍 Analyze Product Feed';
3627
+ });
3628
+
3629
+ // Lawful basis description updates
3630
+ const basisDescriptions = {
3631
+ 'contract': 'Processing is necessary to fulfill customer orders',
3632
+ 'consent': 'Customer explicitly consents to data processing',
3633
+ 'legitimate': 'Processing serves legitimate business interests',
3634
+ 'legal': 'Processing is required by law',
3635
+ };
3636
+ document.getElementById('compliance-basis').addEventListener('change', (e) => {
3637
+ document.getElementById('basis-description').textContent = basisDescriptions[e.target.value] || '';
3638
+ });
3639
+
3640
+ // Compliance Generator form
3641
+ document.getElementById('compliance-form').addEventListener('submit', async (e) => {
3642
+ e.preventDefault();
3643
+ const btn = document.getElementById('compliance-btn');
3644
+ const result = document.getElementById('compliance-result');
3645
+
3646
+ // Gather form data
3647
+ const companyName = document.getElementById('compliance-company').value.trim();
3648
+ const companyEmail = document.getElementById('compliance-email').value.trim();
3649
+ const dpoEmail = document.getElementById('compliance-dpo').value.trim();
3650
+ const lawfulBasis = document.getElementById('compliance-basis').value;
3651
+
3652
+ // Gather regions
3653
+ const regions = [];
3654
+ if (document.getElementById('region-eu').checked) regions.push('eu');
3655
+ if (document.getElementById('region-uk').checked) regions.push('uk');
3656
+ if (document.getElementById('region-california').checked) regions.push('california');
3657
+ if (document.getElementById('region-global').checked) regions.push('global');
3658
+
3659
+ // Gather platforms
3660
+ const platforms = [];
3661
+ if (document.getElementById('platform-openai').checked) platforms.push('openai');
3662
+ if (document.getElementById('platform-google').checked) platforms.push('google');
3663
+ if (document.getElementById('platform-microsoft').checked) platforms.push('microsoft');
3664
+
3665
+ // Options
3666
+ const includeMarketingConsent = document.getElementById('include-marketing').checked;
3667
+ const includeDataRetention = document.getElementById('include-retention').checked;
3668
+
3669
+ // Validate
3670
+ if (!companyName) {
3671
+ result.className = 'result show error';
3672
+ result.innerHTML = '<p>Please enter your company name</p>';
3673
+ return;
3674
+ }
3675
+ if (regions.length === 0) {
3676
+ result.className = 'result show error';
3677
+ result.innerHTML = '<p>Please select at least one compliance region</p>';
3678
+ return;
3679
+ }
3680
+ if (platforms.length === 0) {
3681
+ result.className = 'result show error';
3682
+ result.innerHTML = '<p>Please select at least one AI platform</p>';
3683
+ return;
3684
+ }
3685
+
3686
+ btn.disabled = true;
3687
+ btn.textContent = '🔄 Generating...';
3688
+ result.className = 'result';
3689
+ result.innerHTML = '';
3690
+
3691
+ try {
3692
+ const res = await fetch('/api/generate-compliance', {
3693
+ method: 'POST',
3694
+ headers: { 'Content-Type': 'application/json' },
3695
+ body: JSON.stringify({
3696
+ companyName,
3697
+ companyEmail: companyEmail || undefined,
3698
+ dpoEmail: dpoEmail || undefined,
3699
+ regions,
3700
+ platforms,
3701
+ lawfulBasis,
3702
+ includeMarketingConsent,
3703
+ includeDataRetention,
3704
+ retentionPeriodYears: 7,
3705
+ })
3706
+ });
3707
+ const data = await res.json();
3708
+
3709
+ if (data.error) {
3710
+ throw new Error(data.message || data.error);
3711
+ }
3712
+
3713
+ // Store for download
3714
+ window.complianceData = data;
3715
+
3716
+ result.className = 'result show success';
3717
+ result.innerHTML = `
3718
+ <div style="margin-bottom: 20px;">
3719
+ <h4 style="margin-bottom: 12px; color: var(--brand-teal);">✅ Compliance Documents Generated</h4>
3720
+ <p style="font-size: 13px; color: var(--color-medium);">
3721
+ Generated for: ${companyName} | Regions: ${regions.join(', ').toUpperCase()} | Basis: ${lawfulBasis}
3722
+ </p>
3723
+ </div>
3724
+
3725
+ <!-- Download Buttons -->
3726
+ <div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 24px;">
3727
+ <button class="btn" onclick="downloadComplianceHtml()">📥 Download HTML</button>
3728
+ <button class="btn btn-secondary" onclick="downloadComplianceTxt()">📄 Download Text</button>
3729
+ <button class="btn btn-secondary" onclick="copyComplianceSnippet('aiCommerceSection')">📋 Copy AI Commerce Section</button>
3730
+ </div>
3731
+
3732
+ <!-- Preview Sections -->
3733
+ <div style="background: var(--color-background); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
3734
+ <h5 style="margin-bottom: 12px; cursor: pointer;" onclick="toggleSection('preview-privacy')">
3735
+ 📄 Privacy Policy Addendum <span style="color: var(--color-light);">▼</span>
3736
+ </h5>
3737
+ <div id="preview-privacy" style="display: none;">
3738
+ <pre style="font-size: 12px; white-space: pre-wrap; max-height: 300px; overflow-y: auto; background: white; padding: 12px; border-radius: 4px;">${escapeHtml(data.snippets.aiCommerceSection)}</pre>
3739
+ <button class="btn btn-secondary" style="margin-top: 8px;" onclick="copyComplianceSnippet('aiCommerceSection')">Copy</button>
3740
+ </div>
3741
+ </div>
3742
+
3743
+ <div style="background: var(--color-background); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
3744
+ <h5 style="margin-bottom: 12px; cursor: pointer;" onclick="toggleSection('preview-processors')">
3745
+ 🔗 Data Processor Disclosures <span style="color: var(--color-light);">▼</span>
3746
+ </h5>
3747
+ <div id="preview-processors" style="display: none;">
3748
+ <pre style="font-size: 12px; white-space: pre-wrap; max-height: 300px; overflow-y: auto; background: white; padding: 12px; border-radius: 4px;">${escapeHtml(data.snippets.processorDisclosures)}</pre>
3749
+ <button class="btn btn-secondary" style="margin-top: 8px;" onclick="copyComplianceSnippet('processorDisclosures')">Copy</button>
3750
+ </div>
3751
+ </div>
3752
+
3753
+ <div style="background: var(--color-background); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
3754
+ <h5 style="margin-bottom: 12px; cursor: pointer;" onclick="toggleSection('preview-consent')">
3755
+ ✍️ Consent Language <span style="color: var(--color-light);">▼</span>
3756
+ </h5>
3757
+ <div id="preview-consent" style="display: none;">
3758
+ <pre style="font-size: 12px; white-space: pre-wrap; max-height: 300px; overflow-y: auto; background: white; padding: 12px; border-radius: 4px;">${escapeHtml(data.snippets.consentLanguage)}</pre>
3759
+ <button class="btn btn-secondary" style="margin-top: 8px;" onclick="copyComplianceSnippet('consentLanguage')">Copy</button>
3760
+ </div>
3761
+ </div>
3762
+
3763
+ <div style="background: var(--color-background); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
3764
+ <h5 style="margin-bottom: 12px; cursor: pointer;" onclick="toggleSection('preview-rights')">
3765
+ ⚖️ Data Subject Rights <span style="color: var(--color-light);">▼</span>
3766
+ </h5>
3767
+ <div id="preview-rights" style="display: none;">
3768
+ <pre style="font-size: 12px; white-space: pre-wrap; max-height: 300px; overflow-y: auto; background: white; padding: 12px; border-radius: 4px;">${escapeHtml(data.snippets.dataSubjectRights)}</pre>
3769
+ <button class="btn btn-secondary" style="margin-top: 8px;" onclick="copyComplianceSnippet('dataSubjectRights')">Copy</button>
3770
+ </div>
3771
+ </div>
3772
+
3773
+ <!-- Disclaimer -->
3774
+ <div style="margin-top: 20px; padding: 16px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px;">
3775
+ <h5 style="margin-bottom: 8px;">⚠️ Important</h5>
3776
+ <ul style="margin: 0; padding-left: 20px; font-size: 13px;">
3777
+ <li>This is a template - have it reviewed by a legal professional</li>
3778
+ <li>Privacy laws vary by jurisdiction and change frequently</li>
3779
+ <li>Your specific circumstances may require additional provisions</li>
3780
+ </ul>
3781
+ </div>
3782
+ `;
3783
+
3784
+ } catch (err) {
3785
+ result.className = 'result show error';
3786
+ result.innerHTML = '<p>❌ Generation failed: ' + err.message + '</p>';
3787
+ }
3788
+
3789
+ btn.disabled = false;
3790
+ btn.textContent = '📄 Generate Compliance Documents';
3791
+ });
3792
+
3793
+ // Toggle preview sections
3794
+ function toggleSection(id) {
3795
+ const el = document.getElementById(id);
3796
+ if (el) {
3797
+ el.style.display = el.style.display === 'none' ? 'block' : 'none';
3798
+ }
3799
+ }
3800
+
3801
+ // Download compliance HTML
3802
+ function downloadComplianceHtml() {
3803
+ if (!window.complianceData) return;
3804
+ const blob = new Blob([window.complianceData.embedHtml], { type: 'text/html' });
3805
+ const url = URL.createObjectURL(blob);
3806
+ const a = document.createElement('a');
3807
+ a.href = url;
3808
+ a.download = 'ai-commerce-privacy-addendum.html';
3809
+ a.click();
3810
+ URL.revokeObjectURL(url);
3811
+ }
3812
+
3813
+ // Download compliance text
3814
+ function downloadComplianceTxt() {
3815
+ if (!window.complianceData) return;
3816
+ const blob = new Blob([window.complianceData.plainText], { type: 'text/plain' });
3817
+ const url = URL.createObjectURL(blob);
3818
+ const a = document.createElement('a');
3819
+ a.href = url;
3820
+ a.download = 'ai-commerce-privacy-addendum.txt';
3821
+ a.click();
3822
+ URL.revokeObjectURL(url);
3823
+ }
3824
+
3825
+ // Copy compliance snippet
3826
+ function copyComplianceSnippet(key) {
3827
+ if (!window.complianceData || !window.complianceData.snippets[key]) return;
3828
+ navigator.clipboard.writeText(window.complianceData.snippets[key]).then(() => {
3829
+ alert('Copied to clipboard!');
3830
+ });
3831
+ }
3832
+ </script>
3833
+ </body>
3834
+
3835
+ </html>