emblem-vault-sdk 2.6.0 → 2.7.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.
@@ -123,15 +123,24 @@
123
123
  <h1>Metadata Comparison Tool</h1>
124
124
 
125
125
  <div class="section">
126
- <h2>Select Collection</h2>
127
- <label for="collectionSelect">Choose a collection to compare:</label>
128
- <select id="collectionSelect" onchange="loadComparison()" style="padding: 10px; font-size: 16px; margin-left: 10px;">
129
- <option value="">Loading collections...</option>
130
- </select>
126
+ <h2>Select Source</h2>
127
+ <div style="margin-bottom: 15px;">
128
+ <label for="collectionSelect">Choose a collection to compare:</label>
129
+ <select id="collectionSelect" onchange="loadComparison('collection')" style="padding: 10px; font-size: 16px; margin-left: 10px;">
130
+ <option value="">Loading collections...</option>
131
+ </select>
132
+ </div>
133
+ <div>
134
+ <label for="traitSelect">Choose a trait file to compare:</label>
135
+ <select id="traitSelect" onchange="loadComparison('trait')" style="padding: 10px; font-size: 16px; margin-left: 10px;">
136
+ <option value="">Loading traits...</option>
137
+ </select>
138
+ </div>
131
139
  </div>
132
140
 
133
141
  <div id="status" class="status">Select a collection to begin...</div>
134
142
 
143
+ <div id="traitVerification" class="section" style="display:none;"></div>
135
144
  <div id="summary" class="summary" style="display:none;"></div>
136
145
 
137
146
  <div id="fakeRaresSection" style="display:none;"></div>
@@ -162,6 +171,190 @@
162
171
  return props;
163
172
  }
164
173
 
174
+ // Map trait data to Rare Pepe metadata format
175
+ function mapTraitToMetadataFormat(traitItem, projectName) {
176
+ // Detect which format we're dealing with
177
+ const hasTokenId = traitItem.hasOwnProperty('tokenId');
178
+ const hasAsset = traitItem.hasOwnProperty('asset');
179
+
180
+ let mappedData = {
181
+ projectProtocol: "Counterparty"
182
+ };
183
+
184
+ // Map project names to their proper display names and sites
185
+ const projectMappings = {
186
+ 'Age_of_Chains': { name: 'Age of Chains', site: 'https://www.ageofchains.com/' },
187
+ 'Age_of_Rust': { name: 'Age of Rust', site: 'https://www.ageofrustgame.com/' },
188
+ 'Bitgirls': { name: 'Bitgirls', site: 'https://bitcorns.com/' },
189
+ 'Force_of_Will': { name: 'Force of Will', site: 'https://forceofwill.cards/' },
190
+ 'Oasis_Mining': { name: 'Oasis Mining', site: 'https://www.oasismining.com/' },
191
+ 'Sarutobi': { name: 'Sarutobi Island', site: 'https://mandelduck.com/' },
192
+ 'Spells_of_Genesis': { name: 'Spells of Genesis', site: 'https://spellsofgenesis.com/' },
193
+ 'bitcorn': { name: 'Bitcorn Crops', site: 'https://bitcorns.com/' },
194
+ 'memorychain': { name: 'Memorychain', site: 'https://pepecash.cards/' }
195
+ };
196
+
197
+ const projectInfo = projectMappings[projectName.replace('.json', '')] || { name: projectName, site: '' };
198
+ mappedData.projectName = projectInfo.name;
199
+ if (projectInfo.site) mappedData.projectSite = projectInfo.site;
200
+
201
+ if (hasTokenId) {
202
+ // Format 1: Age of Chains style (tokenId, assetId, supply, burns)
203
+ const assetName = traitItem.tokenId;
204
+
205
+ mappedData.burned = parseInt(traitItem.burns) || 0;
206
+ mappedData.divisible = 0; // Most NFTs are not divisible
207
+ mappedData.quantity = parseInt(traitItem.supply) || 0;
208
+ mappedData.remaining = mappedData.quantity - mappedData.burned;
209
+
210
+ // Store original data in raw
211
+ mappedData.raw = {
212
+ ...traitItem,
213
+ supply: parseInt(traitItem.supply) || 0,
214
+ name: assetName
215
+ };
216
+
217
+ // Generate image URL based on project
218
+ const projectFolder = projectName.replace('.json', '').toLowerCase().replace(/_/g, '-');
219
+ mappedData.image = `https://raw.githubusercontent.com/EmblemCompany/vaultImages/master/collection/${projectFolder}/${assetName}.jpg`;
220
+
221
+ } else if (hasAsset) {
222
+ // Format 2: Bitcorn style (asset, series, card_no, quantity, artist_name)
223
+ const assetName = traitItem.asset;
224
+
225
+ mappedData.burned = 0; // No burn info in this format
226
+ mappedData.divisible = traitItem.divisible === true ? 1 : 0;
227
+ mappedData.order = parseInt(traitItem.card_no) || 0;
228
+ mappedData.quantity = parseInt(traitItem.quantity) || 0;
229
+ mappedData.series = parseInt(traitItem.series) || 1;
230
+ mappedData.remaining = mappedData.quantity;
231
+
232
+ // Store original data in raw with artist info
233
+ mappedData.raw = {
234
+ ...traitItem,
235
+ name: assetName,
236
+ supply: parseInt(traitItem.quantity) || 0,
237
+ serie: parseInt(traitItem.series) || 1,
238
+ card: parseInt(traitItem.card_no) || 0,
239
+ artist: {
240
+ name: traitItem.artist_name || 'Unknown',
241
+ slug: (traitItem.artist_name || 'Unknown').toLowerCase().replace(/\s+/g, '-')
242
+ }
243
+ };
244
+
245
+ // Use provided image URL or generate one
246
+ if (traitItem.img_url) {
247
+ mappedData.image = traitItem.img_url;
248
+ } else {
249
+ const projectFolder = projectName.replace('.json', '').toLowerCase().replace(/_/g, '-');
250
+ mappedData.image = `https://raw.githubusercontent.com/EmblemCompany/vaultImages/master/collection/${projectFolder}/${assetName}.jpg`;
251
+ }
252
+ }
253
+
254
+ return mappedData;
255
+ }
256
+
257
+ function verifyTraitCoverage(traitData, sdk, site) {
258
+ const verification = {
259
+ foundInBoth: [],
260
+ foundInSDKOnly: [],
261
+ foundInSiteOnly: [],
262
+ foundInNone: [],
263
+ propertyMatches: [],
264
+ propertyMismatches: [],
265
+ missingProperties: [],
266
+ total: 0
267
+ };
268
+
269
+ Object.keys(traitData).forEach(key => {
270
+ verification.total++;
271
+ const traitItem = traitData[key];
272
+ const sdkItem = sdk[key];
273
+ const siteItem = site[key];
274
+ const inSDK = sdk.hasOwnProperty(key);
275
+ const inSite = site.hasOwnProperty(key);
276
+
277
+ const itemAnalysis = {
278
+ key: key,
279
+ inSDK: inSDK,
280
+ inSite: inSite,
281
+ traitProps: Object.keys(traitItem).sort(),
282
+ sdkProps: sdkItem ? Object.keys(sdkItem).sort() : [],
283
+ siteProps: siteItem ? Object.keys(siteItem).sort() : [],
284
+ propertyComparison: {}
285
+ };
286
+
287
+ if (inSDK && inSite) {
288
+ verification.foundInBoth.push(key);
289
+
290
+ // Compare properties between trait, SDK, and Site
291
+ const allProps = new Set([
292
+ ...itemAnalysis.traitProps,
293
+ ...itemAnalysis.sdkProps,
294
+ ...itemAnalysis.siteProps
295
+ ]);
296
+
297
+ let hasPropertyIssues = false;
298
+ const propertyIssues = {
299
+ key: key,
300
+ missingInSDK: [],
301
+ missingInSite: [],
302
+ valueMismatches: []
303
+ };
304
+
305
+ allProps.forEach(prop => {
306
+ const inTrait = traitItem.hasOwnProperty(prop);
307
+ const inSDKProp = sdkItem && sdkItem.hasOwnProperty(prop);
308
+ const inSiteProp = siteItem && siteItem.hasOwnProperty(prop);
309
+
310
+ // Check if trait property exists in metadata
311
+ if (inTrait) {
312
+ if (!inSDKProp) {
313
+ propertyIssues.missingInSDK.push(prop);
314
+ hasPropertyIssues = true;
315
+ }
316
+ if (!inSiteProp) {
317
+ propertyIssues.missingInSite.push(prop);
318
+ hasPropertyIssues = true;
319
+ }
320
+
321
+ // Check if values match when property exists in all three
322
+ if (inSDKProp && inSiteProp) {
323
+ const traitValue = JSON.stringify(traitItem[prop]);
324
+ const sdkValue = JSON.stringify(sdkItem[prop]);
325
+ const siteValue = JSON.stringify(siteItem[prop]);
326
+
327
+ if (traitValue !== sdkValue || traitValue !== siteValue || sdkValue !== siteValue) {
328
+ propertyIssues.valueMismatches.push({
329
+ prop: prop,
330
+ traitValue: traitItem[prop],
331
+ sdkValue: sdkItem[prop],
332
+ siteValue: siteItem[prop]
333
+ });
334
+ hasPropertyIssues = true;
335
+ }
336
+ }
337
+ }
338
+ });
339
+
340
+ if (hasPropertyIssues) {
341
+ verification.propertyMismatches.push(propertyIssues);
342
+ } else {
343
+ verification.propertyMatches.push(key);
344
+ }
345
+
346
+ } else if (inSDK && !inSite) {
347
+ verification.foundInSDKOnly.push(key);
348
+ } else if (!inSDK && inSite) {
349
+ verification.foundInSiteOnly.push(key);
350
+ } else {
351
+ verification.foundInNone.push(key);
352
+ }
353
+ });
354
+
355
+ return verification;
356
+ }
357
+
165
358
  function compareMetadata(sdk, site, fakeRares) {
166
359
  const results = {
167
360
  onlyInSDK: [],
@@ -1854,6 +2047,8 @@
1854
2047
  }
1855
2048
 
1856
2049
  let missingItemsGlobal = [];
2050
+ let isCurrentSourceTrait = false;
2051
+ let currentTraitData = null;
1857
2052
 
1858
2053
  async function addMissingItemsToBoth() {
1859
2054
  const button = document.getElementById('addMissingItemsBtn');
@@ -1955,6 +2150,100 @@
1955
2150
  }
1956
2151
  }
1957
2152
 
2153
+ async function applyTraitMappings() {
2154
+ const statusDiv = document.getElementById('applyMappingStatus');
2155
+
2156
+ if (!isCurrentSourceTrait || !currentTraitData) {
2157
+ statusDiv.style.display = 'block';
2158
+ statusDiv.className = 'status error';
2159
+ statusDiv.textContent = 'No trait data loaded. Please select a trait file first.';
2160
+ return;
2161
+ }
2162
+
2163
+ statusDiv.style.display = 'block';
2164
+ statusDiv.className = 'status';
2165
+ statusDiv.textContent = 'Applying trait mappings to existing metadata items...';
2166
+
2167
+ try {
2168
+ // Map trait items to the correct format - only for items that exist in metadata
2169
+ const mappedItems = {};
2170
+ let foundCount = 0;
2171
+ let notFoundCount = 0;
2172
+
2173
+ Object.keys(currentTraitData).forEach(key => {
2174
+ // Only process if the item exists in at least one metadata file
2175
+ if (sdkMetadataGlobal[key] || siteMetadataGlobal[key]) {
2176
+ const traitItem = currentTraitData[key];
2177
+ const mappedData = mapTraitToMetadataFormat(traitItem, currentCollection);
2178
+
2179
+ // Merge with existing metadata if present
2180
+ const existingSDK = sdkMetadataGlobal[key] || {};
2181
+ const existingSite = siteMetadataGlobal[key] || {};
2182
+
2183
+ mappedItems[key] = {
2184
+ ...existingSDK, // Preserve existing properties
2185
+ ...mappedData, // Apply mapped properties (overwrites conflicts)
2186
+ // Ensure raw data includes both old and new
2187
+ raw: {
2188
+ ...existingSDK.raw,
2189
+ ...mappedData.raw
2190
+ }
2191
+ };
2192
+ foundCount++;
2193
+ } else {
2194
+ notFoundCount++;
2195
+ console.log(`Skipping ${key} - not found in existing metadata`);
2196
+ }
2197
+ });
2198
+
2199
+ if (foundCount === 0) {
2200
+ statusDiv.className = 'status error';
2201
+ statusDiv.textContent = 'No matching items found in metadata to update.';
2202
+ return;
2203
+ }
2204
+
2205
+ statusDiv.textContent = `Updating ${foundCount} existing items (skipping ${notFoundCount} not found)...`;
2206
+
2207
+ // Send update request to server
2208
+ const response = await fetch('/apply-trait-mappings', {
2209
+ method: 'POST',
2210
+ headers: {
2211
+ 'Content-Type': 'application/json'
2212
+ },
2213
+ body: JSON.stringify({ items: mappedItems })
2214
+ });
2215
+
2216
+ const results = await response.json();
2217
+
2218
+ if (!response.ok) {
2219
+ throw new Error(results.error || 'Failed to apply trait mappings');
2220
+ }
2221
+
2222
+ statusDiv.className = 'status success';
2223
+ statusDiv.innerHTML = `
2224
+ <strong>Trait mappings applied to existing items!</strong><br>
2225
+ <div style="margin: 10px 0;">
2226
+ <span style="color: #27ae60;">✓ SDK Updated: ${results.sdkUpdated || 0} items</span> |
2227
+ <span style="color: #27ae60;">✓ Site Updated: ${results.siteUpdated || 0} items</span>
2228
+ </div>
2229
+ <div style="margin: 5px 0; color: #666;">
2230
+ <span>Processed: ${foundCount} items found in metadata</span><br>
2231
+ <span>Skipped: ${notFoundCount} items not in metadata</span>
2232
+ </div>
2233
+ <p style="color: #666; font-size: 12px; margin-top: 10px;">
2234
+ Only existing metadata items were updated with trait properties.
2235
+ Properties have been mapped to match the Rare Pepe format.
2236
+ Reload the page to see the updated metadata comparison.
2237
+ </p>
2238
+ `;
2239
+
2240
+ } catch (error) {
2241
+ statusDiv.className = 'status error';
2242
+ statusDiv.textContent = `Error: ${error.message}`;
2243
+ console.error('Failed to apply trait mappings:', error);
2244
+ }
2245
+ }
2246
+
1958
2247
  async function copyMissingImages() {
1959
2248
  const button = document.getElementById('copyImagesBtn');
1960
2249
  const statusDiv = document.getElementById('copyStatus');
@@ -1966,18 +2255,44 @@
1966
2255
  statusDiv.textContent = `Preparing to copy ${missingItemsGlobal.length} images...`;
1967
2256
 
1968
2257
  try {
1969
- const items = missingItemsGlobal.map(item => ({
1970
- name: item.name,
1971
- image: item.data.image,
1972
- serie: item.data.serie
1973
- }));
2258
+ let items;
2259
+ let collectionName = currentCollection;
2260
+
2261
+ if (isCurrentSourceTrait) {
2262
+ // For trait files, we need to get the project name from SDK metadata
2263
+ // and handle the different data structure
2264
+ items = missingItemsGlobal.map(item => {
2265
+ // Try to get project info from SDK metadata if the item exists there
2266
+ const sdkItem = sdkMetadataGlobal[item.name];
2267
+ let projectName = 'Unknown';
2268
+
2269
+ if (sdkItem && sdkItem.projectName) {
2270
+ projectName = sdkItem.projectName;
2271
+ // Convert project name to folder format (e.g., "Age of Chains" -> "age-of-chains")
2272
+ collectionName = projectName.toLowerCase().replace(/\s+/g, '-') + '.json';
2273
+ }
2274
+
2275
+ return {
2276
+ name: item.name,
2277
+ image: item.data.img_url || item.data.image || `${item.name}.jpg`,
2278
+ serie: item.data.series || item.data.serie || 1
2279
+ };
2280
+ });
2281
+ } else {
2282
+ // Original logic for collection files
2283
+ items = missingItemsGlobal.map(item => ({
2284
+ name: item.name,
2285
+ image: item.data.image,
2286
+ serie: item.data.serie
2287
+ }));
2288
+ }
1974
2289
 
1975
2290
  const response = await fetch('/copy-images', {
1976
2291
  method: 'POST',
1977
2292
  headers: {
1978
2293
  'Content-Type': 'application/json'
1979
2294
  },
1980
- body: JSON.stringify({ items, collection: currentCollection })
2295
+ body: JSON.stringify({ items, collection: collectionName })
1981
2296
  });
1982
2297
 
1983
2298
  const results = await response.json();
@@ -2063,6 +2378,191 @@
2063
2378
  }
2064
2379
  }
2065
2380
 
2381
+ function displayTraitVerification(verification, traitFileName) {
2382
+ const section = document.getElementById('traitVerification');
2383
+
2384
+ if (!verification) {
2385
+ section.style.display = 'none';
2386
+ return;
2387
+ }
2388
+
2389
+ section.style.display = 'block';
2390
+
2391
+ const percentInBoth = ((verification.foundInBoth.length / verification.total) * 100).toFixed(1);
2392
+ const percentInSDKOnly = ((verification.foundInSDKOnly.length / verification.total) * 100).toFixed(1);
2393
+ const percentInSiteOnly = ((verification.foundInSiteOnly.length / verification.total) * 100).toFixed(1);
2394
+ const percentInNone = ((verification.foundInNone.length / verification.total) * 100).toFixed(1);
2395
+
2396
+ let html = `
2397
+ <h2>Trait Verification Report for ${traitFileName}</h2>
2398
+ <div class="summary" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0;">
2399
+ <div style="background: #d4edda; padding: 15px; border-radius: 8px;">
2400
+ <div style="font-size: 24px; font-weight: bold; color: #155724;">${verification.foundInBoth.length}</div>
2401
+ <div style="color: #155724;">Found in Both (${percentInBoth}%)</div>
2402
+ </div>
2403
+ <div style="background: #cce5ff; padding: 15px; border-radius: 8px;">
2404
+ <div style="font-size: 24px; font-weight: bold; color: #004085;">${verification.foundInSDKOnly.length}</div>
2405
+ <div style="color: #004085;">SDK Only (${percentInSDKOnly}%)</div>
2406
+ </div>
2407
+ <div style="background: #fff3cd; padding: 15px; border-radius: 8px;">
2408
+ <div style="font-size: 24px; font-weight: bold; color: #856404;">${verification.foundInSiteOnly.length}</div>
2409
+ <div style="color: #856404;">Site Only (${percentInSiteOnly}%)</div>
2410
+ </div>
2411
+ <div style="background: #f8d7da; padding: 15px; border-radius: 8px;">
2412
+ <div style="font-size: 24px; font-weight: bold; color: #721c24;">${verification.foundInNone.length}</div>
2413
+ <div style="color: #721c24;">Not Found (${percentInNone}%)</div>
2414
+ </div>
2415
+ </div>
2416
+ <div style="font-weight: bold; margin: 10px 0;">Total Trait Items: ${verification.total}</div>
2417
+ `;
2418
+
2419
+ // Add property analysis for items found in both
2420
+ if (verification.propertyMatches.length > 0 || verification.propertyMismatches.length > 0) {
2421
+ html += `
2422
+ <div style="margin: 20px 0; padding: 15px; background: #f0f8ff; border-radius: 8px;">
2423
+ <h3 style="color: #004085;">Property Analysis for Items in Both Metadata Files</h3>
2424
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 10px 0;">
2425
+ <div style="background: #d4edda; padding: 10px; border-radius: 4px;">
2426
+ <strong style="color: #155724;">Perfect Matches: ${verification.propertyMatches.length}</strong>
2427
+ <div style="color: #155724; font-size: 14px;">All properties and values match</div>
2428
+ </div>
2429
+ <div style="background: #fff3cd; padding: 10px; border-radius: 4px;">
2430
+ <strong style="color: #856404;">Property Issues: ${verification.propertyMismatches.length}</strong>
2431
+ <div style="color: #856404; font-size: 14px;">Missing properties or value mismatches</div>
2432
+ </div>
2433
+ </div>
2434
+ <button onclick="applyTraitMappings()" style="margin-top: 10px; padding: 10px 20px; background: #5e72e4; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold;">
2435
+ Apply Trait Mappings to Metadata
2436
+ </button>
2437
+ <div id="applyMappingStatus" style="display:none; margin-top: 10px;"></div>
2438
+ <p style="color: #666; margin: 10px 0; font-size: 12px;">
2439
+ This will update the SDK and Site metadata with properly mapped trait properties following the Rare Pepe format,
2440
+ ensuring Series, Card, Total Supply, and Artist traits will work correctly.
2441
+ </p>
2442
+ </div>
2443
+ `;
2444
+ }
2445
+
2446
+ // Show property mismatches in detail
2447
+ if (verification.propertyMismatches.length > 0) {
2448
+ html += `
2449
+ <details style="margin: 20px 0; padding: 15px; background: #fff3cd; border-radius: 8px;">
2450
+ <summary style="cursor: pointer; font-weight: bold; color: #856404;">
2451
+ Property Issues Detail (${verification.propertyMismatches.length} items)
2452
+ </summary>
2453
+ <div style="margin-top: 15px;">
2454
+ `;
2455
+
2456
+ verification.propertyMismatches.forEach(issue => {
2457
+ html += `
2458
+ <div style="margin: 10px 0; padding: 10px; background: white; border-radius: 4px; border-left: 4px solid #ff6b6b;">
2459
+ <strong style="color: #2c3e50;">${issue.key}</strong>
2460
+ `;
2461
+
2462
+ if (issue.missingInSDK.length > 0) {
2463
+ html += `
2464
+ <div style="margin: 5px 0;">
2465
+ <span style="color: #e74c3c;">Missing in SDK:</span>
2466
+ ${issue.missingInSDK.map(p => `<code style="background: #f8f9fa; padding: 2px 4px; margin: 0 2px;">${p}</code>`).join(', ')}
2467
+ </div>
2468
+ `;
2469
+ }
2470
+
2471
+ if (issue.missingInSite.length > 0) {
2472
+ html += `
2473
+ <div style="margin: 5px 0;">
2474
+ <span style="color: #e74c3c;">Missing in Site:</span>
2475
+ ${issue.missingInSite.map(p => `<code style="background: #f8f9fa; padding: 2px 4px; margin: 0 2px;">${p}</code>`).join(', ')}
2476
+ </div>
2477
+ `;
2478
+ }
2479
+
2480
+ if (issue.valueMismatches.length > 0) {
2481
+ html += `
2482
+ <div style="margin: 5px 0;">
2483
+ <span style="color: #e67e22;">Value Mismatches:</span>
2484
+ <table style="width: 100%; margin-top: 5px; border-collapse: collapse;">
2485
+ <thead>
2486
+ <tr style="background: #f8f9fa;">
2487
+ <th style="padding: 5px; border: 1px solid #dee2e6; text-align: left;">Property</th>
2488
+ <th style="padding: 5px; border: 1px solid #dee2e6; text-align: left;">Trait Value</th>
2489
+ <th style="padding: 5px; border: 1px solid #dee2e6; text-align: left;">SDK Value</th>
2490
+ <th style="padding: 5px; border: 1px solid #dee2e6; text-align: left;">Site Value</th>
2491
+ </tr>
2492
+ </thead>
2493
+ <tbody>
2494
+ `;
2495
+ issue.valueMismatches.forEach(mismatch => {
2496
+ html += `
2497
+ <tr>
2498
+ <td style="padding: 5px; border: 1px solid #dee2e6; font-family: monospace;">${mismatch.prop}</td>
2499
+ <td style="padding: 5px; border: 1px solid #dee2e6; font-family: monospace; font-size: 12px;">${JSON.stringify(mismatch.traitValue)}</td>
2500
+ <td style="padding: 5px; border: 1px solid #dee2e6; font-family: monospace; font-size: 12px;">${JSON.stringify(mismatch.sdkValue)}</td>
2501
+ <td style="padding: 5px; border: 1px solid #dee2e6; font-family: monospace; font-size: 12px;">${JSON.stringify(mismatch.siteValue)}</td>
2502
+ </tr>
2503
+ `;
2504
+ });
2505
+ html += `
2506
+ </tbody>
2507
+ </table>
2508
+ </div>
2509
+ `;
2510
+ }
2511
+
2512
+ html += `</div>`;
2513
+ });
2514
+
2515
+ html += `
2516
+ </div>
2517
+ </details>
2518
+ `;
2519
+ }
2520
+
2521
+ // Show details for items not found
2522
+ if (verification.foundInNone.length > 0) {
2523
+ html += `
2524
+ <details style="margin: 20px 0;">
2525
+ <summary style="cursor: pointer; font-weight: bold; color: #721c24;">
2526
+ Items Not Found in Either Metadata (${verification.foundInNone.length})
2527
+ </summary>
2528
+ <div style="margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px;">
2529
+ ${verification.foundInNone.map(item => `<span style="display: inline-block; margin: 3px; padding: 5px 10px; background: white; border: 1px solid #dee2e6; border-radius: 3px; font-family: monospace;">${item}</span>`).join('')}
2530
+ </div>
2531
+ </details>
2532
+ `;
2533
+ }
2534
+
2535
+ // Show details for items only in SDK
2536
+ if (verification.foundInSDKOnly.length > 0) {
2537
+ html += `
2538
+ <details style="margin: 20px 0;">
2539
+ <summary style="cursor: pointer; font-weight: bold; color: #004085;">
2540
+ Items Only in SDK Metadata (${verification.foundInSDKOnly.length})
2541
+ </summary>
2542
+ <div style="margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px;">
2543
+ ${verification.foundInSDKOnly.map(item => `<span style="display: inline-block; margin: 3px; padding: 5px 10px; background: white; border: 1px solid #dee2e6; border-radius: 3px; font-family: monospace;">${item}</span>`).join('')}
2544
+ </div>
2545
+ </details>
2546
+ `;
2547
+ }
2548
+
2549
+ // Show details for items only in Site
2550
+ if (verification.foundInSiteOnly.length > 0) {
2551
+ html += `
2552
+ <details style="margin: 20px 0;">
2553
+ <summary style="cursor: pointer; font-weight: bold; color: #856404;">
2554
+ Items Only in Site Metadata (${verification.foundInSiteOnly.length})
2555
+ </summary>
2556
+ <div style="margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px;">
2557
+ ${verification.foundInSiteOnly.map(item => `<span style="display: inline-block; margin: 3px; padding: 5px 10px; background: white; border: 1px solid #dee2e6; border-radius: 3px; font-family: monospace;">${item}</span>`).join('')}
2558
+ </div>
2559
+ </details>
2560
+ `;
2561
+ }
2562
+
2563
+ section.innerHTML = html;
2564
+ }
2565
+
2066
2566
  function displayMissingFromBoth(missing, collectionName = 'collection') {
2067
2567
  const section = document.getElementById('fakeRaresSection');
2068
2568
 
@@ -2080,10 +2580,17 @@
2080
2580
  <p style="color: #666; margin: 10px 0;">These items exist in ${collectionName} but are not present in either sdk-metadata.json or emblem-site-metadata.json</p>
2081
2581
  <button id="addMissingItemsBtn" onclick="addMissingItemsToBoth()" style="padding: 10px 20px; background: #8e44ad; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; margin: 10px 5px 10px 0;">
2082
2582
  Add Missing Items to Both SDK & Site (${missing.length})
2083
- </button>
2583
+ </button>`;
2584
+
2585
+ // Only show Copy Images button for collection files, not trait files
2586
+ if (!isCurrentSourceTrait) {
2587
+ html += `
2084
2588
  <button id="copyImagesBtn" onclick="copyMissingImages()" style="padding: 10px 20px; background: #27ae60; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; margin: 10px 0;">
2085
2589
  Copy Missing Images (${missing.length})
2086
- </button>
2590
+ </button>`;
2591
+ }
2592
+
2593
+ html += `
2087
2594
  <div id="addMissingItemsStatus" style="display:none;"></div>
2088
2595
  <div id="copyStatus" style="display:none;"></div>
2089
2596
  <p style="color: #666; margin: 10px 0; font-size: 12px;">The "Add Missing Items" button will create entries in both SDK and Site metadata using the template structure with ${collectionName} data in the 'raw' property.</p>
@@ -2128,14 +2635,51 @@
2128
2635
  }
2129
2636
  }
2130
2637
 
2131
- async function loadComparison() {
2132
- const select = document.getElementById('collectionSelect');
2133
- const selectedCollection = select.value;
2638
+ async function loadTraits() {
2639
+ try {
2640
+ const response = await fetch('/list-traits');
2641
+ const availableTraits = await response.json();
2642
+
2643
+ const select = document.getElementById('traitSelect');
2644
+ select.innerHTML = '<option value="">-- Select a trait file --</option>';
2134
2645
 
2135
- if (!selectedCollection) {
2136
- document.getElementById('status').textContent = 'Select a collection to begin...';
2646
+ availableTraits.forEach(trait => {
2647
+ const option = document.createElement('option');
2648
+ option.value = trait.filename;
2649
+ option.textContent = trait.name;
2650
+ select.appendChild(option);
2651
+ });
2652
+ } catch (error) {
2653
+ document.getElementById('status').textContent = `Error loading traits: ${error.message}`;
2654
+ document.getElementById('status').className = 'status error';
2655
+ }
2656
+ }
2657
+
2658
+ async function loadComparison(sourceType) {
2659
+ let selectedFile = '';
2660
+ let isTraitSource = false;
2661
+
2662
+ if (sourceType === 'trait') {
2663
+ const traitSelect = document.getElementById('traitSelect');
2664
+ selectedFile = traitSelect.value;
2665
+ isTraitSource = true;
2666
+ isCurrentSourceTrait = true; // Set global flag
2667
+ // Clear collection dropdown
2668
+ document.getElementById('collectionSelect').value = '';
2669
+ } else {
2670
+ const collectionSelect = document.getElementById('collectionSelect');
2671
+ selectedFile = collectionSelect.value;
2672
+ isCurrentSourceTrait = false; // Set global flag
2673
+ // Clear trait dropdown
2674
+ document.getElementById('traitSelect').value = '';
2675
+ }
2676
+
2677
+ if (!selectedFile) {
2678
+ document.getElementById('status').textContent = 'Select a collection or trait file to begin...';
2137
2679
  document.getElementById('status').className = 'status';
2138
2680
  // Clear all sections
2681
+ document.getElementById('traitVerification').style.display = 'none';
2682
+ document.getElementById('traitVerification').innerHTML = '';
2139
2683
  document.getElementById('summary').style.display = 'none';
2140
2684
  document.getElementById('summary').innerHTML = '';
2141
2685
  document.getElementById('fakeRaresSection').style.display = 'none';
@@ -2146,10 +2690,12 @@
2146
2690
  return;
2147
2691
  }
2148
2692
 
2149
- currentCollection = selectedCollection;
2693
+ currentCollection = selectedFile;
2150
2694
  const statusDiv = document.getElementById('status');
2151
2695
 
2152
2696
  // Clear all previous results before loading new collection
2697
+ document.getElementById('traitVerification').style.display = 'none';
2698
+ document.getElementById('traitVerification').innerHTML = '';
2153
2699
  document.getElementById('summary').style.display = 'none';
2154
2700
  document.getElementById('summary').innerHTML = '';
2155
2701
  document.getElementById('fakeRaresSection').style.display = 'none';
@@ -2166,11 +2712,36 @@
2166
2712
  statusDiv.textContent = 'Loading emblem-site-metadata.json...';
2167
2713
  siteMetadataGlobal = await loadJSON('./emblem-site-metadata.json');
2168
2714
 
2169
- statusDiv.textContent = `Loading ${selectedCollection}...`;
2170
- const incomingArray = await loadJSON(`./incoming/${selectedCollection}`);
2171
- incomingDataGlobal = convertFakeRaresToObject(incomingArray);
2715
+ statusDiv.textContent = `Loading ${selectedFile}...`;
2716
+ let incomingArray;
2717
+ if (isTraitSource) {
2718
+ incomingArray = await loadJSON(`./traits/${selectedFile}`);
2719
+ // Convert traits format to standard format using asset or tokenId as key
2720
+ const convertedData = {};
2721
+ incomingArray.forEach(item => {
2722
+ const key = item.asset || item.tokenId;
2723
+ if (key) {
2724
+ convertedData[key] = item;
2725
+ }
2726
+ });
2727
+ incomingDataGlobal = convertedData;
2728
+ currentTraitData = convertedData; // Save for mapping function
2729
+ } else {
2730
+ incomingArray = await loadJSON(`./incoming/${selectedFile}`);
2731
+ incomingDataGlobal = convertFakeRaresToObject(incomingArray);
2732
+ }
2172
2733
 
2173
2734
  statusDiv.textContent = 'Comparing metadata...';
2735
+
2736
+ // If this is a trait source, run verification
2737
+ if (isTraitSource) {
2738
+ const verification = verifyTraitCoverage(incomingDataGlobal, sdkMetadataGlobal, siteMetadataGlobal);
2739
+ displayTraitVerification(verification, selectedFile);
2740
+ } else {
2741
+ // Hide trait verification for collection sources
2742
+ document.getElementById('traitVerification').style.display = 'none';
2743
+ }
2744
+
2174
2745
  const results = compareMetadata(sdkMetadataGlobal, siteMetadataGlobal, incomingDataGlobal);
2175
2746
  const missingFromBoth = findMissingFromBoth(incomingDataGlobal, sdkMetadataGlobal, siteMetadataGlobal);
2176
2747
 
@@ -2179,11 +2750,17 @@
2179
2750
  const siteRedundant = findRedundantOldImages(siteMetadataGlobal);
2180
2751
 
2181
2752
  statusDiv.className = 'status success';
2182
- statusDiv.textContent = `Comparison complete! Found ${missingFromBoth.length} items missing from both SDK and Site. Found ${sdkRedundant.length + siteRedundant.length} redundant oldImage properties.`;
2183
2753
 
2184
- displayMissingFromBoth(missingFromBoth, selectedCollection);
2754
+ if (isTraitSource) {
2755
+ const verification = verifyTraitCoverage(incomingDataGlobal, sdkMetadataGlobal, siteMetadataGlobal);
2756
+ statusDiv.textContent = `Trait verification complete! ${verification.foundInBoth.length} items found in both, ${verification.foundInNone.length} missing from both metadata files.`;
2757
+ } else {
2758
+ statusDiv.textContent = `Comparison complete! Found ${missingFromBoth.length} items missing from both SDK and Site. Found ${sdkRedundant.length + siteRedundant.length} redundant oldImage properties.`;
2759
+ }
2760
+
2761
+ displayMissingFromBoth(missingFromBoth, selectedFile);
2185
2762
  displayRedundantOldImages(sdkRedundant, siteRedundant);
2186
- displayResults(results, selectedCollection);
2763
+ displayResults(results, selectedFile);
2187
2764
  } catch (error) {
2188
2765
  statusDiv.className = 'status error';
2189
2766
  statusDiv.textContent = `Error: ${error.message}`;
@@ -2193,6 +2770,7 @@
2193
2770
 
2194
2771
  // Auto-run when page loads
2195
2772
  loadCollections();
2773
+ loadTraits();
2196
2774
  </script>
2197
2775
  </body>
2198
2776
  </html>