diff --git a/src/server/server.ts b/src/server/server.ts index 84e1e53..91a5420 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1306,6 +1306,89 @@ export const server = createServer( res.end(JSON.stringify({ ok: false, error: "raw_disabled" })); return; } + // Additional safety: for the official server require a specific staff role + const OFFICIAL_SERVER_ID = String( + process.env.OFFICIAL_SERVER_ID || "1316592320954630144" + ); + const DEV_ROLE_ID = String( + process.env.DEV_ROLE_ID || "1424252340659163268" + ); + const userId = sessionApi?.user?.id + ? String(sessionApi.user.id) + : null; + if (!userId) { + res.writeHead( + 403, + applySecurityHeadersForRequest(req, { + "Content-Type": "application/json", + }) + ); + res.end(JSON.stringify({ ok: false, error: "no_user" })); + return; + } + if (String(guildId) === OFFICIAL_SERVER_ID) { + // require that the member has DEV_ROLE_ID in the guild + const botToken = + process.env.DISCORD_BOT_TOKEN ?? process.env.TOKEN; + if (!botToken) { + res.writeHead( + 403, + applySecurityHeadersForRequest(req, { + "Content-Type": "application/json", + }) + ); + res.end(JSON.stringify({ ok: false, error: "no_bot_token" })); + return; + } + try { + const memRes = await fetch( + `https://discord.com/api/guilds/${encodeURIComponent( + String(guildId) + )}/members/${encodeURIComponent(String(userId))}`, + { headers: { Authorization: `Bot ${botToken}` } } + ); + if (!memRes.ok) { + res.writeHead( + 403, + applySecurityHeadersForRequest(req, { + "Content-Type": "application/json", + }) + ); + res.end( + JSON.stringify({ + ok: false, + error: "member_fetch_failed", + }) + ); + return; + } + const memJson = await memRes.json(); + const roles = Array.isArray(memJson.roles) + ? memJson.roles.map(String) + : []; + if (!roles.includes(DEV_ROLE_ID)) { + res.writeHead( + 403, + applySecurityHeadersForRequest(req, { + "Content-Type": "application/json", + }) + ); + res.end( + JSON.stringify({ ok: false, error: "insufficient_role" }) + ); + return; + } + } catch (err) { + res.writeHead( + 500, + applySecurityHeadersForRequest(req, { + "Content-Type": "application/json", + }) + ); + res.end(JSON.stringify({ ok: false, error: String(err) })); + return; + } + } try { const it = await prisma.economyItem.findUnique({ where: { id: String(itemId) }, diff --git a/src/server/views/partials/dashboard/dashboard_items.ejs b/src/server/views/partials/dashboard/dashboard_items.ejs index 875b462..0e2bac9 100644 --- a/src/server/views/partials/dashboard/dashboard_items.ejs +++ b/src/server/views/partials/dashboard/dashboard_items.ejs @@ -50,12 +50,158 @@
- - + +
+
+ + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + +
+
+

Tool

+
+ + + + +
+
+
+

Breakable / Durability

+
+ + + + +
+
+
+

Consumable / Usable

+
+ + + + +
+
+ + +
+
+
+

Damage / Stats / Bonuses

+
+ + + + +
+
+ + + + + + +
+
+ + +
+
+
+

Requirements

+
+ + + + + + +
+
+
+

Economy

+
+ + + + +
+
+
+

Chest / Drops

+
+ + +
+
+ +
+
+ + + + +
+
+
+
+ + +
+
- - + +
+
+ + + + +
+
+ + +
+
@@ -84,8 +230,44 @@ modal.classList.add('hidden'); modal.classList.remove('flex'); form.reset(); - document.getElementById('fieldProps').value = '{}'; - document.getElementById('fieldMetadata').value = '{}'; + // reset structured props/meta + document.getElementById('propCraftable').checked = false; + document.getElementById('propRecipeKey').value = ''; + document.getElementById('propEquipable').checked = false; + document.getElementById('propSlot').value = ''; + document.getElementById('propAttack').value = ''; + document.getElementById('propDefense').value = ''; + document.getElementById('propDurability').value = ''; + document.getElementById('propMaxDurability').value = ''; + document.getElementById('propCustomJson').value = '{}'; + // new fields + document.getElementById('propToolType').value = ''; + document.getElementById('propToolTier').value = ''; + document.getElementById('propBreakable').checked = false; + document.getElementById('propDurabilityPerUse').value = ''; + document.getElementById('propUsable').checked = false; + document.getElementById('propPurgeAllEffects').checked = false; + document.getElementById('propHealAmount').value = ''; + document.getElementById('propDamage').value = ''; + document.getElementById('propDamageBonus').value = ''; + document.getElementById('statAttack').value = ''; + document.getElementById('statHp').value = ''; + document.getElementById('statDefense').value = ''; + document.getElementById('statXpReward').value = ''; + document.getElementById('reqToolRequired').checked = false; + document.getElementById('reqToolType').value = ''; + document.getElementById('reqMinTier').value = ''; + document.getElementById('propSellPrice').value = ''; + document.getElementById('propBuyPrice').value = ''; + document.getElementById('propChestEnabled').checked = false; + // rewards UI reset + document.getElementById('rewardsList').innerHTML = ''; + document.getElementById('newRewardType').value = 'coins'; + document.getElementById('newRewardAmount').value = ''; + document.getElementById('newRewardItemKey').value = ''; + document.getElementById('metaRarity').value = 'common'; + document.getElementById('metaWeight').value = ''; + document.getElementById('metaCustomJson').value = '{}'; document.getElementById('itemId').value = ''; } @@ -103,6 +285,107 @@ } } + // Rewards UI helpers + function renderRewardsList(rewards) { + const container = document.getElementById('rewardsList'); + container.innerHTML = ''; + if (!Array.isArray(rewards) || rewards.length === 0) return; + rewards.forEach((r, idx) => { + const row = document.createElement('div'); + row.className = 'flex items-center gap-2'; + if (r.type === 'coins' || typeof r.coins !== 'undefined') { + const amt = r.coins || r.amount || 0; + row.innerHTML = `
Coins: ${amt}
`; + } else if (r.type === 'item' || r.itemKey) { + const key = r.itemKey || (r.items && r.items[0] && r.items[0].key) || ''; + const qty = r.qty || (r.items && r.items[0] && r.items[0].quantity) || r.quantity || 1; + row.innerHTML = `
Item: ${escapeHtml(key)} x${qty}
`; + } else { + row.innerHTML = `
${escapeHtml(JSON.stringify(r))}
`; + } + const del = document.createElement('button'); + del.className = 'px-2 py-1 bg-red-600 text-white rounded text-sm ml-2'; + del.textContent = 'Eliminar'; + del.addEventListener('click', ()=>{ + const list = getCurrentRewards(); + list.splice(idx,1); + renderRewardsList(list); + setCurrentRewards(list); + renderTagChips(); + }); + row.appendChild(del); + container.appendChild(row); + }); + } + + function getCurrentRewards() { + try { + const el = document.getElementById('rewardsList'); + const nodes = Array.from(el.children || []); + // store rewards in dataset for simple persistence + const raw = el.dataset.rewards; + return raw ? JSON.parse(raw) : []; + } catch (e) { return []; } + } + function setCurrentRewards(arr) { document.getElementById('rewardsList').dataset.rewards = JSON.stringify(arr || []); } + + document.getElementById('addRewardBtn').addEventListener('click', ()=>{ + const type = document.getElementById('newRewardType').value; + const amt = Number(document.getElementById('newRewardAmount').value) || 0; + const key = document.getElementById('newRewardItemKey').value.trim(); + const list = getCurrentRewards(); + if (type === 'coins') { + list.push({ coins: amt }); + } else { + if (!key) return alert('item.key requerido'); + list.push({ items: [{ key, quantity: amt || 1 }] }); + } + setCurrentRewards(list); + renderRewardsList(list); + // clear input + document.getElementById('newRewardAmount').value = ''; + document.getElementById('newRewardItemKey').value = ''; + }); + + // Tag chips UI + const tagsChipsContainer = document.createElement('div'); + tagsChipsContainer.id = 'tagsChips'; + tagsChipsContainer.className = 'mt-2 flex flex-wrap gap-2'; + const tagsField = document.getElementById('fieldTags'); + tagsField.parentNode.appendChild(tagsChipsContainer); + + function renderTagChips() { + const base = tagsField.value.split(',').map(s=>s.trim()).filter(Boolean); + const auto = []; + // build preview from current UI selections + if (document.getElementById('propCraftable').checked) auto.push('craftable'); + if (document.getElementById('propToolType').value.trim()) auto.push('tool'); + if (document.getElementById('propDamage').value.trim() || document.getElementById('propAttack').value.trim()) auto.push('weapon'); + if (document.getElementById('propUsable').checked) auto.push('consumable'); + if (document.getElementById('propEquipable').checked) auto.push('equipable'); + if (document.getElementById('propChestEnabled').checked) auto.push('chest'); + if (document.getElementById('propBreakable').checked) auto.push('breakable'); + if (document.getElementById('propSellPrice').value.trim()) auto.push('sellable'); + if (document.getElementById('propBuyPrice').value.trim()) auto.push('buyable'); + const merged = Array.from(new Set([...base, ...auto])); + tagsChipsContainer.innerHTML = ''; + merged.forEach(t=>{ + const chip = document.createElement('span'); + chip.className = 'px-2 py-1 rounded bg-white/6 text-sm text-white'; + chip.textContent = t; + tagsChipsContainer.appendChild(chip); + }); + } + + // update chips when some inputs change + ['propCraftable','propToolType','propDamage','propAttack','propUsable','propEquipable','propChestEnabled','propBreakable','propSellPrice','propBuyPrice','fieldTags'].forEach(id=>{ + const el = document.getElementById(id); + if (!el) return; + el.addEventListener('input', renderTagChips); + el.addEventListener('change', renderTagChips); + }); + + function renderList(items) { if (!Array.isArray(items) || items.length === 0) { list.innerHTML = '
No hay items definidos.
'; @@ -148,9 +431,43 @@ document.getElementById('fieldDescription').value = it.description || ''; document.getElementById('fieldTags').value = (Array.isArray(it.tags) ? it.tags.join(',') : ''); document.getElementById('fieldMaxPer').value = it.maxPerInventory || ''; - // For security we do NOT expose stored props/metadata via the API. User must paste JSON manually if needed. - document.getElementById('fieldProps').value = '{ /* props are hidden for security; paste JSON to replace */ }'; - document.getElementById('fieldMetadata').value = '{ /* metadata hidden for security */ }'; + // For security we do NOT expose stored props/metadata via the API. UI fields will be empty/default and admin can attempt raw load. + document.getElementById('propCraftable').checked = false; + document.getElementById('propRecipeKey').value = ''; + document.getElementById('propEquipable').checked = false; + document.getElementById('propSlot').value = ''; + document.getElementById('propAttack').value = ''; + document.getElementById('propDefense').value = ''; + document.getElementById('propDurability').value = ''; + document.getElementById('propMaxDurability').value = ''; + document.getElementById('propCustomJson').value = '{}'; + document.getElementById('metaRarity').value = 'common'; + document.getElementById('metaWeight').value = ''; + document.getElementById('metaCustomJson').value = '{}'; + // new fields defaults on edit + document.getElementById('propToolType').value = ''; + document.getElementById('propToolTier').value = ''; + document.getElementById('propBreakable').checked = false; + document.getElementById('propDurabilityPerUse').value = ''; + document.getElementById('propUsable').checked = false; + document.getElementById('propPurgeAllEffects').checked = false; + document.getElementById('propHealAmount').value = ''; + document.getElementById('propDamage').value = ''; + document.getElementById('propDamageBonus').value = ''; + document.getElementById('statAttack').value = ''; + document.getElementById('statHp').value = ''; + document.getElementById('statDefense').value = ''; + document.getElementById('statXpReward').value = ''; + document.getElementById('reqToolRequired').checked = false; + document.getElementById('reqToolType').value = ''; + document.getElementById('reqMinTier').value = ''; + document.getElementById('propSellPrice').value = ''; + document.getElementById('propBuyPrice').value = ''; + document.getElementById('propChestEnabled').checked = false; + document.getElementById('propDropsJson').value = '{}'; + // populate rewards if we have a cached representation in the item (not exposed by default) + document.getElementById('rewardsList').dataset.rewards = JSON.stringify([]); + renderRewardsList([]); modalTitle.textContent = 'Editar item'; showModal(true); } catch (err) { @@ -158,6 +475,27 @@ } } + // Try to load raw props for the currently opened item (admin-only; server may reject) + async function tryLoadRawProps(itemId) { + const msgEl = document.getElementById('loadRawMsg'); + msgEl.textContent = ''; + try { + const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items/${encodeURIComponent(itemId)}?raw=1`, { headers: { 'Accept':'application/json' } }); + if (!res.ok) { + // server might reject raw access; silent friendly message + msgEl.textContent = 'raw no permitido'; + return null; + } + const j = await res.json(); + if (!j || !j.ok) { msgEl.textContent = 'no data'; return null; } + msgEl.textContent = 'raw cargado'; + return j.item || null; + } catch (e) { + msgEl.textContent = 'error'; + return null; + } + } + async function onDelete(e) { const id = e.currentTarget.getAttribute('data-id'); if (!confirm('Eliminar item?')) return; @@ -177,20 +515,88 @@ ev.preventDefault(); const id = document.getElementById('itemId').value; // validate JSON fields before sending - const propsText = document.getElementById('fieldProps').value.trim(); - const metaText = document.getElementById('fieldMetadata').value.trim(); - let parsedProps = null; - let parsedMeta = null; - try { - parsedProps = propsText ? JSON.parse(propsText) : null; - } catch (e) { - return alert('JSON inválido en Props: ' + (e && e.message ? e.message : String(e))); + // Build props object from UI + let parsedProps = {}; + if (document.getElementById('propCraftable').checked) { + parsedProps.craftable = true; + const rk = document.getElementById('propRecipeKey').value.trim(); if (rk) parsedProps.recipe = { key: rk }; + } + if (document.getElementById('propEquipable').checked) { + parsedProps.equipable = true; + const slot = document.getElementById('propSlot').value; if (slot) parsedProps.slot = slot; + } + // tool + const ttype = document.getElementById('propToolType').value.trim(); + const ttier = document.getElementById('propToolTier').value.trim(); + if (ttype) parsedProps.tool = Object.assign({}, parsedProps.tool || {}, { type: ttype }); + if (ttier) parsedProps.tool = Object.assign({}, parsedProps.tool || {}, { tier: Number(ttier) }); + // breakable + if (document.getElementById('propBreakable').checked) { + parsedProps.breakable = parsedProps.breakable || {}; + parsedProps.breakable.enabled = true; + const dpu = document.getElementById('propDurabilityPerUse').value.trim(); if (dpu) parsedProps.breakable.durabilityPerUse = Number(dpu); + } + const attack = document.getElementById('propAttack').value.trim(); if (attack) parsedProps.attack = Number(attack); + const defense = document.getElementById('propDefense').value.trim(); if (defense) parsedProps.defense = Number(defense); + const durability = document.getElementById('propDurability').value.trim(); if (durability) parsedProps.durability = Number(durability); + const maxDur = document.getElementById('propMaxDurability').value.trim(); if (maxDur) parsedProps.maxDurability = Number(maxDur); + // usable + if (document.getElementById('propUsable').checked) { + parsedProps.usable = true; + if (document.getElementById('propPurgeAllEffects').checked) parsedProps.purgeAllEffects = true; + const heal = document.getElementById('propHealAmount').value.trim(); if (heal) parsedProps.heal = Number(heal); + } + // damage/bonuses + const dmg = document.getElementById('propDamage').value.trim(); if (dmg) parsedProps.damage = Number(dmg); + const dmgBonus = document.getElementById('propDamageBonus').value.trim(); if (dmgBonus) parsedProps.damageBonus = Number(dmgBonus); + // stats + const sAtk = document.getElementById('statAttack').value.trim(); if (sAtk) parsedProps.stats = parsedProps.stats || {}, parsedProps.stats.attack = Number(sAtk); + const sHp = document.getElementById('statHp').value.trim(); if (sHp) parsedProps.stats = parsedProps.stats || {}, parsedProps.stats.hp = Number(sHp); + const sDef = document.getElementById('statDefense').value.trim(); if (sDef) parsedProps.stats = parsedProps.stats || {}, parsedProps.stats.defense = Number(sDef); + const sXp = document.getElementById('statXpReward').value.trim(); if (sXp) parsedProps.stats = parsedProps.stats || {}, parsedProps.stats.xpReward = Number(sXp); + // requirements + if (document.getElementById('reqToolRequired').checked) { + parsedProps.requirements = parsedProps.requirements || {}; + parsedProps.requirements.tool = { required: true }; + const rtype = document.getElementById('reqToolType').value.trim(); if (rtype) parsedProps.requirements.tool.toolType = rtype; + const rmin = document.getElementById('reqMinTier').value.trim(); if (rmin) parsedProps.requirements.tool.minTier = Number(rmin); + } + // economy + const sell = document.getElementById('propSellPrice').value.trim(); if (sell) parsedProps.sellPrice = Number(sell); + const buy = document.getElementById('propBuyPrice').value.trim(); if (buy) parsedProps.buyPrice = Number(buy); + // chest/drops + if (document.getElementById('propChestEnabled').checked) { + parsedProps.chest = parsedProps.chest || {}; + parsedProps.chest.enabled = true; } try { - parsedMeta = metaText ? JSON.parse(metaText) : null; - } catch (e) { - return alert('JSON inválido en Metadata: ' + (e && e.message ? e.message : String(e))); - } + // prefer structured rewards list + const rawRewards = document.getElementById('rewardsList').dataset.rewards; + const rewards = rawRewards ? JSON.parse(rawRewards) : null; + if (Array.isArray(rewards)) { + parsedProps.chest = Object.assign(parsedProps.chest || {}, { rewards }); + } else { + // fallback parse old textarea if present + const el = document.getElementById('propDropsJson'); + if (el) { + const drops = JSON.parse(el.value || '{}'); + if (drops && typeof drops === 'object') parsedProps.chest = Object.assign(parsedProps.chest || {}, { rewards: drops.rewards || drops }); + } + } + } catch (e) { return alert('JSON inválido en Drops/Chest: ' + (e && e.message ? e.message : String(e))); } + // merge custom JSON + try { + const custom = JSON.parse(document.getElementById('propCustomJson').value || '{}'); + if (custom && typeof custom === 'object') Object.assign(parsedProps, custom); + } catch (e) { return alert('JSON inválido en Props personalizado: ' + (e && e.message ? e.message : String(e))); } + + // Build metadata + let parsedMeta = { rarity: document.getElementById('metaRarity').value }; + const w = document.getElementById('metaWeight').value.trim(); if (w) parsedMeta.weight = Number(w); + try { + const customM = JSON.parse(document.getElementById('metaCustomJson').value || '{}'); + if (customM && typeof customM === 'object') Object.assign(parsedMeta, customM); + } catch (e) { return alert('JSON inválido en Metadata personalizado: ' + (e && e.message ? e.message : String(e))); } const payload = { key: document.getElementById('fieldKey').value.trim(), @@ -200,9 +606,26 @@ description: document.getElementById('fieldDescription').value.trim(), tags: document.getElementById('fieldTags').value.split(',').map(s=>s.trim()).filter(Boolean), maxPerInventory: Number(document.getElementById('fieldMaxPer').value) || null, - props: parsedProps, - metadata: parsedMeta, + props: Object.keys(parsedProps).length ? parsedProps : null, + metadata: Object.keys(parsedMeta).length ? parsedMeta : null, }; + // Auto-generate tags from props UI + try { + const autoTags = new Set(payload.tags); + if (parsedProps.craftable) autoTags.add('craftable'); + if (parsedProps.tool) autoTags.add('tool'); + if (parsedProps.damage || parsedProps.attack || parsedProps.stats) autoTags.add('weapon'); + if (parsedProps.usable) autoTags.add('consumable'); + if (parsedProps.equipable) autoTags.add('equipable'); + if (parsedProps.chest) autoTags.add('chest'); + if (parsedProps.breakable) autoTags.add('breakable'); + if (parsedProps.sellPrice) autoTags.add('sellable'); + if (parsedProps.buyPrice) autoTags.add('buyable'); + // merge back to payload.tags as array + payload.tags = Array.from(autoTags).filter(Boolean); + } catch (e) { + // ignore tagging failures + } try { if (id) { const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(payload) }); @@ -218,6 +641,71 @@ } }); + // load raw props button handler + document.getElementById('loadRawPropsBtn').addEventListener('click', async ()=>{ + const id = document.getElementById('itemId').value; + if (!id) return alert('Abra un item primero para cargar props raw'); + const raw = await tryLoadRawProps(id); + if (!raw) return; + try { + if (raw.props && typeof raw.props === 'object') { + // map known props to UI + document.getElementById('propCraftable').checked = !!raw.props.craftable; + document.getElementById('propRecipeKey').value = raw.props.recipe && raw.props.recipe.key ? raw.props.recipe.key : ''; + document.getElementById('propEquipable').checked = !!raw.props.equipable; + document.getElementById('propSlot').value = raw.props.slot || ''; + document.getElementById('propAttack').value = raw.props.attack || ''; + document.getElementById('propDefense').value = raw.props.defense || ''; + document.getElementById('propDurability').value = raw.props.durability || ''; + document.getElementById('propMaxDurability').value = raw.props.maxDurability || ''; + // tool + document.getElementById('propToolType').value = (raw.props.tool && raw.props.tool.type) ? raw.props.tool.type : ''; + document.getElementById('propToolTier').value = (raw.props.tool && typeof raw.props.tool.tier !== 'undefined') ? String(raw.props.tool.tier) : ''; + // breakable + document.getElementById('propBreakable').checked = !!(raw.props.breakable && raw.props.breakable.enabled); + document.getElementById('propDurabilityPerUse').value = (raw.props.breakable && raw.props.breakable.durabilityPerUse) ? String(raw.props.breakable.durabilityPerUse) : ''; + // usable + document.getElementById('propUsable').checked = !!raw.props.usable; + document.getElementById('propPurgeAllEffects').checked = !!raw.props.purgeAllEffects; + document.getElementById('propHealAmount').value = raw.props.heal || ''; + // damage/bonuses + document.getElementById('propDamage').value = raw.props.damage || ''; + document.getElementById('propDamageBonus').value = raw.props.damageBonus || ''; + // stats + document.getElementById('statAttack').value = (raw.props.stats && raw.props.stats.attack) ? String(raw.props.stats.attack) : ''; + document.getElementById('statHp').value = (raw.props.stats && raw.props.stats.hp) ? String(raw.props.stats.hp) : ''; + document.getElementById('statDefense').value = (raw.props.stats && raw.props.stats.defense) ? String(raw.props.stats.defense) : ''; + document.getElementById('statXpReward').value = (raw.props.stats && raw.props.stats.xpReward) ? String(raw.props.stats.xpReward) : ''; + // requirements + document.getElementById('reqToolRequired').checked = !!(raw.props.requirements && raw.props.requirements.tool && raw.props.requirements.tool.required); + document.getElementById('reqToolType').value = (raw.props.requirements && raw.props.requirements.tool && raw.props.requirements.tool.toolType) ? raw.props.requirements.tool.toolType : ''; + document.getElementById('reqMinTier').value = (raw.props.requirements && raw.props.requirements.tool && typeof raw.props.requirements.tool.minTier !== 'undefined') ? String(raw.props.requirements.tool.minTier) : ''; + // economy + document.getElementById('propSellPrice').value = raw.props.sellPrice || ''; + document.getElementById('propBuyPrice').value = raw.props.buyPrice || ''; + // chest/drops + document.getElementById('propChestEnabled').checked = !!(raw.props.chest && raw.props.chest.enabled); + // map rewards into UI + const rewards = raw.props.chest && raw.props.chest.rewards ? raw.props.chest.rewards : []; + document.getElementById('rewardsList').dataset.rewards = JSON.stringify(rewards || []); + renderRewardsList(rewards || []); + // leave extra fields in custom JSON + const copy = Object.assign({}, raw.props); + delete copy.craftable; delete copy.recipe; delete copy.equipable; delete copy.slot; delete copy.attack; delete copy.defense; delete copy.durability; delete copy.maxDurability; + document.getElementById('propCustomJson').value = JSON.stringify(copy, null, 2); + } + if (raw.metadata && typeof raw.metadata === 'object') { + document.getElementById('metaRarity').value = raw.metadata.rarity || 'common'; + document.getElementById('metaWeight').value = raw.metadata.weight || ''; + const copyM = Object.assign({}, raw.metadata); + delete copyM.rarity; delete copyM.weight; + document.getElementById('metaCustomJson').value = JSON.stringify(copyM, null, 2); + } + } catch (e) { + alert('Error al mapear raw props: ' + (e && e.message ? e.message : String(e))); + } + }); + // Initial load fetchItems(); })();