/** * The UGC Server page: counts, a gallery and a list of what the UGC server made of players' models and modular builds * (from the database), its live status, and a viewer comparing a model's generated mesh, its LXFML and its icon, now * and before it was made again. Everything from the UGC server comes through the dashboard (/api/ugc/server/status, * /api/ugc/files/..., /api/ugc/mesh/...), so the browser never has to reach the UGC server itself. */ (function () { 'use strict'; var kind = 'model', state = '', search = '', page = 0, view = 'gallery', canManage = false, publicUrl = ''; var nifViewer = null, lxfmlViewer = null, current = null; var STATES = { pending: ['Waiting', 'secondary'], done: ['Made', 'success'], failed: ['Failed', 'danger'], empty: ['Empty', 'light'] }; var PAGE_SIZE = { gallery: 48, list: 50 }; try { view = localStorage.getItem('ugcView') === 'list' ? 'list' : 'gallery'; } catch (e) { /* storage blocked */ } function $(id) { return document.getElementById(id); } function fileUrl(itemKind, id, name) { return '/api/ugc/files/' + itemKind + '/' + encodeURIComponent(id) + '/' + name; } function mb(bytes) { return (bytes / 1048576).toFixed(0) + ' MB'; } function countCard(title, c) { return '
' + esc(title) + '
' + fmt.badge(c.done + ' made', 'success') + ' ' + fmt.badge(c.pending + ' waiting', 'secondary') + ' ' + fmt.badge(c.failed + ' failed', c.failed ? 'danger' : 'secondary') + (c.empty ? ' ' + fmt.badge(c.empty + ' empty', 'light') : '') + '
'; } function owner(i) { return i.characterName ? '' + esc(i.characterName) + '' : '' + esc(i.characterId) + ''; } function iconImg(i, size) { if (i.state !== 'done') return '' + esc((STATES[i.state] || [i.state])[0]) + ''; return ''; } function tile(i) { var s = STATES[i.state] || [i.state, 'secondary']; return '
' + iconImg(i, 128) + '
' + (i.characterName ? esc(i.characterName) : '' + esc(i.characterId) + '') + '
' + '
' + esc(i.id) + '
' + fmt.badge(s[0], s[1]) + extraBadges(i) + '
'; } // Cars and rockets: how many builds share this icon; waiting models: when their quiet period after a save ends function extraBadges(i) { var out = ''; if (i.sharedBy > 1) out += ' ' + fmt.badge('shared by ' + i.sharedBy, 'info'); if (i.state === 'pending' && i.processAfter > Date.now() / 1000) out += ' after ' + esc(fmt.unix(i.processAfter)) + ''; return out; } function row(i) { var s = STATES[i.state] || [i.state, 'secondary']; var details = (kind === 'modular' ? '' + esc(i.modules) + '' : '') + extraBadges(i); if (i.error) details += '
' + esc(i.error) + '
'; var buttons = '' + (canManage ? '' : ''); return '' + iconImg(i, 48) + '' + esc(i.id) + '' + owner(i) + '' + fmt.badge(s[0], s[1]) + (i.attempts ? ' ' + esc(i.attempts) + ' attempt' + (i.attempts === 1 ? '' : 's') + '' : '') + '' + '' + (i.processedAt ? esc(fmt.unix(i.processedAt)) : '') + '' + details + '' + buttons + ''; } var items = []; function load() { var size = PAGE_SIZE[view]; api.get('/api/ugc?kind=' + kind + '&state=' + state + '&page=' + page + '&size=' + size + '&search=' + encodeURIComponent(search)).then(function (d) { if (!d.success) return; items = d.items; canManage = d.canManage; publicUrl = (d.ugcPublicUrl || '').replace(/\/+$/, ''); $('manageButtons').classList.toggle('d-none', !canManage); $('cacheCard').classList.toggle('d-none', !canManage); $('counts').innerHTML = countCard('Models', d.counts.model) + countCard('Cars and rockets', d.counts.modular); $('gallery').classList.toggle('d-none', view !== 'gallery'); $('listCard').classList.toggle('d-none', view !== 'list'); if (view === 'gallery') $('gallery').innerHTML = d.items.map(tile).join('') || '
Nothing here.
'; else $('rows').innerHTML = d.items.map(row).join('') || 'Nothing here.'; $('prevPage').disabled = page === 0; $('nextPage').disabled = !d.more; }).catch(function () {}); } function meter(label, value, limit, text) { var share = limit > 0 ? Math.min(100, value / limit * 100) : 0; var colour = share > 90 ? 'danger' : share > 70 ? 'warning' : 'success'; return '
' + esc(label) + '' + text + '
' + (limit > 0 ? '
' + '
' : '') + '
'; } function loadStatus() { var box = $('serverStatus'); api.get('/api/ugc/server/status').then(function (d) { if (!d.success) { box.innerHTML = fmt.badge('Unreachable', 'warning') + ' The dashboard can\'t reach the UGC server at ' + esc(d.url || '') + (d.error ? ' (' + esc(d.error) + ')' : '') + '. Is enable_ugc_server on, and ugc_internal_url (dashboard settings) right?'; return; } var s = d.status, u = s.usage || {}, l = s.limits || {}, last = s.recent && s.recent[0]; var cores = u.cores || 1, machinePercent = (u.cpuPercent || 0) / cores; var badges = fmt.badge('Online', 'success') + (u.throttled ? ' ' + fmt.badge('Throttled', 'warning') : '') + (u.paused ? ' ' + fmt.badge('Paused (' + esc(l.pauseHours) + ')', 'info') : ''); box.innerHTML = '
' + badges + ' ' + esc(s.workers) + ' workers, ' + esc(s.active) + ' working, ' + esc(s.queued) + ' queued. ' + 'Since it started: ' + esc(s.made) + ' made, ' + esc(s.failed) + ' failed attempts. Files: ' + esc(mb(s.storedBytes)) + (s.maxStorageBytes ? ' of ' + esc(mb(s.maxStorageBytes)) : '') + (s.evicted ? ', ' + esc(s.evicted) + ' deleted to save space' : '') + '.
' + '
' + meter('CPU', machinePercent, l.maxCpus ? l.maxCpus / cores * 100 : 0, machinePercent.toFixed(0) + '% of ' + cores + ' cores' + (l.maxCpus ? ' (limit ' + (l.maxCpus / cores * 100).toFixed(0) + '%)' : ', no limit') + (u.throttledMs ? ', paused ' + (u.throttledMs / 1000).toFixed(0) + ' s to stay under it' : '')) + meter('Memory', u.residentBytes || 0, 0, esc(mb(u.residentBytes || 0)) + ' in use') + meter('Jobs\' memory', s.jobMemoryBytes || 0, l.maxMemoryBytes || 0, esc(mb(s.jobMemoryBytes || 0)) + ' estimated' + (l.maxMemoryBytes ? ' of ' + esc(mb(l.maxMemoryBytes)) : ', no limit') + (s.memoryWaits ? ', ' + esc(s.memoryWaits) + ' waits' : '')) + '
' + (l.nice ? '
Workers run at priority ' + esc(l.nice) + '.
' : '') + (last ? '
Last: ' + esc(last.kind) + ' ' + esc(last.id) + (last.ok ? ' made in ' + esc(last.ms) + ' ms' : ' failed') + (last.message ? ' (' + esc(last.message) + ')' : '') + '
' : ''); }).catch(function () { box.innerHTML = fmt.badge('Unknown', 'secondary') + ' Couldn\'t ask the dashboard for the UGC server\'s status.'; }); } function remake(body, question) { if (question && !confirm(question)) return; body.kind = kind; api.post('/api/ugc/reprocess', body).then(function (d) { if (!d.success) { toast(d.error || 'Failed', 'danger'); return; } toast(d.message, 'success'); load(); }); } // ---- the viewer ---- function fetchJson(url) { return fetch(url, { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); } function statsTable(now, before) { if (!now) return '
No stats for this model (made before the UGC server wrote them).
'; var lods = now.lods || [], old = before && before.lods || []; var diff = function (a, b) { if (b === undefined || b === null || a === b) return ''; var d = a - b; return ' (' + (d > 0 ? '+' : '') + d + ')'; }; var rows = lods.map(function (l, i) { var o = old[i] || {}; var removed = l.opaqueBefore ? (100 - l.opaqueAfter / l.opaqueBefore * 100).toFixed(0) + '%' : ''; return 'LOD ' + esc(l.lod) + '' + esc(l.near) + ' - ' + esc(l.far) + '' + esc(l.opaqueBefore) + '' + esc(l.opaqueAfter) + diff(l.opaqueAfter, o.opaqueAfter) + '' + esc(removed) + '' + esc(l.transparent) + '' + esc(l.vertices) + diff(l.vertices, o.vertices) + '' + esc(l.shapes) + ''; }).join(''); var ms = now.ms || {}, set = now.settings || {}; return '
' + '' + rows + '
DetailDrawn from - toOpaque trianglesAfter removing hidden facesRemovedTransparent trianglesVerticesShapes
' + '
' + esc(now.bricks) + ' bricks' + (now.missingDesigns ? ', no geometry for ' + esc(now.missingDesigns.join(', ')) : '') + '. Took ' + esc(ms.total) + ' ms (build ' + esc(ms.build) + ', hidden faces ' + esc(ms.hiddenSurfaces) + ', occlusion ' + esc(ms.ambientOcclusion) + ', icon ' + esc(ms.icon) + ')' + (before && before.ms ? ', before ' + esc(before.ms.total) + ' ms' : '') + '. Colors: ' + esc(set.palette) + ', variation ' + esc(set.colorVariation) + '%' + (set.ao ? ', occlusion ' + esc(set.aoSamples) + ' rays to ' + esc(set.aoDistance) : ', no occlusion') + '.
'; } function loadMesh(reframe) { if (!current || !nifViewer) return; var lod = $('lodSelect').value || '0', version = $('versionSelect').value, ao = $('aoSwitch').checked ? '1' : '0'; var stats = $('nifStats'); stats.textContent = 'Loading…'; nifViewer.load('/api/ugc/mesh/' + encodeURIComponent(current.id) + '?lod=' + lod + '&version=' + version + '&ao=' + ao, reframe).then(function (r) { var lodStats = current.stats && current.stats.lods && current.stats.lods[+lod]; stats.textContent = r.triangles.toLocaleString() + ' triangles, ' + r.vertices.toLocaleString() + ' vertices in ' + r.shapes + ' shape' + (r.shapes === 1 ? '' : 's') + (lodStats && version === 'current' ? ' (' + lodStats.opaqueBefore.toLocaleString() + ' opaque triangles before hidden faces were removed)' : ''); }).catch(function (e) { stats.textContent = 'Could not load the mesh: ' + e.message; }); } // ---- the open item in the address bar (?item=&kind=model|modular), so it can be shared and Back closes it ---- var pushedItem = false; function urlItem() { var params = new URLSearchParams(location.search), id = params.get('item'); if (!id || !/^\d{1,20}$/.test(id)) return null; return { id: id, kind: params.get('kind') === 'modular' ? 'modular' : 'model' }; } function setKind(value) { if (value === kind) return; kind = value; page = 0; $('kindButtons').querySelectorAll('[data-kind]').forEach(function (b) { b.classList.toggle('active', b.dataset.kind === kind); }); load(); } // Opens the item the address names, or closes the open one when it names none function openFromUrl() { var wanted = urlItem(); if (!wanted) { if (current) bootstrap.Modal.getOrCreateInstance($('previewModal')).hide(); return; } setKind(wanted.kind); if (current && current.id === wanted.id && current.kind === wanted.kind) return; var known = items.find(function (i) { return i.id === wanted.id; }); if (known) return preview(wanted.id, known); // Not on the page of the list that is showing: look it up by its id api.get('/api/ugc?kind=' + wanted.kind + '&search=' + wanted.id + '&size=10').then(function (d) { canManage = d.canManage; publicUrl = (d.ugcPublicUrl || '').replace(/\/+$/, ''); preview(wanted.id, (d.items || []).find(function (i) { return i.id === wanted.id; })); }).catch(function () { preview(wanted.id); }); } function preview(id, found) { var item = found || items.find(function (i) { return i.id === id; }) || { id: id, state: 'done' }; current = { id: id, kind: kind, stats: null }; var shown = urlItem(); if (!shown || shown.id !== id || shown.kind !== kind) { history.pushState(null, '', location.pathname + '?item=' + encodeURIComponent(id) + '&kind=' + kind); pushedItem = true; } $('previewTitle').textContent = (kind === 'model' ? 'Model ' : 'Car or rocket ') + id + (item.characterName ? ' by ' + item.characterName : ''); $('previewIcon').src = fileUrl(kind, id, 'icon.png'); $('previousIconBox').classList.add('d-none'); var previous = $('previousIcon'); previous.onload = function () { $('previousIconBox').classList.remove('d-none'); }; previous.src = fileUrl(kind, id, 'previous.icon.png'); var links = ''; if (kind === 'model') { links += 'Download the mesh (.nif)
' + 'Download the LXFML
'; } if (publicUrl) links += 'Open on the UGC server'; if (canManage) { links += '
' + '' + '
'; if (item.sharedBy > 1) links += '
This icon is shared by ' + esc(item.sharedBy) + ' builds of the same modules.
'; } if (item.error) links += '
' + esc(item.error) + '
'; $('previewLinks').innerHTML = links; var model = kind === 'model'; ['nifColumn', 'lxfmlColumn', 'meshControls'].forEach(function (x) { $(x).classList.toggle('d-none', !model); }); $('statsBox').innerHTML = model ? '' : '
Modules: ' + esc(item.modules || '') + ' (combination ' + esc(item.combination || '') + '). The game client puts cars and rockets together itself; only the icon is made, once per combination of modules.
'; $('framingCard').classList.toggle('d-none', !canManage); if (canManage) openFraming(item, model); bootstrap.Modal.getOrCreateInstance($('previewModal')).show(); if (!model) return; Promise.all([fetchJson(fileUrl('model', id, 'stats.json')), fetchJson(fileUrl('model', id, 'previous.stats.json'))]).then(function (s) { if (!current || current.id !== id) return; current.stats = s[0]; current.statsLoaded = true; $('statsBox').innerHTML = statsTable(s[0], s[1]); var lods = (s[0] && s[0].lods) || [{ lod: 0 }]; $('lodSelect').innerHTML = lods.map(function (l, i) { return ''; }).join(''); $('versionSelect').querySelector('[value=previous]').disabled = !s[1]; $('versionSelect').value = 'current'; loadMesh(true); }); var nifBox = $('nifViewer'), lxfmlBox = $('lxfmlViewer'); import('/js/ugc-viewer.js').then(function (module) { if (!nifViewer) { nifViewer = module.createNifViewer(nifBox); nifViewer.setWireframe($('wireframeSwitch').checked); nifViewer.setVertexColors($('colorsSwitch').checked); } if (current && current.id === id && current.statsLoaded) loadMesh(true); }).catch(function (e) { nifBox.textContent = 'The 3D view could not load: ' + e.message; }); lxfmlBox.textContent = 'Loading…'; import('/js/lddviewer.js').then(function (module) { if (lxfmlViewer && lxfmlViewer.dispose) lxfmlViewer.dispose(); lxfmlBox.textContent = ''; lxfmlViewer = module.createViewer(lxfmlBox, {}); return lxfmlViewer.load([{ id: id, name: 'Model ' + id, position: [0, 0, 0], rotation: [0, 0, 0, 1], url: '/api/ugc/' + id + '/lxfml' }], 1); }).catch(function (e) { lxfmlBox.textContent = 'The 3D view could not load: ' + e.message; }); } $('kindButtons').addEventListener('click', function (e) { var button = e.target.closest('[data-kind]'); if (button) setKind(button.dataset.kind); }); $('viewButtons').addEventListener('click', function (e) { var button = e.target.closest('[data-view]'); if (!button) return; view = button.dataset.view; page = 0; try { localStorage.setItem('ugcView', view); } catch (err) { /* storage blocked */ } this.querySelectorAll('[data-view]').forEach(function (b) { b.classList.toggle('active', b === button); }); load(); }); $('viewButtons').querySelectorAll('[data-view]').forEach(function (b) { b.classList.toggle('active', b.dataset.view === view); }); $('stateFilter').addEventListener('change', function () { state = this.value; page = 0; load(); }); var searchTimer = null; $('search').addEventListener('input', function () { var value = this.value.trim(); clearTimeout(searchTimer); searchTimer = setTimeout(function () { search = value; page = 0; load(); }, 300); }); $('prevPage').addEventListener('click', function () { if (page > 0) { page--; load(); } }); $('nextPage').addEventListener('click', function () { page++; load(); }); $('retryFailed').addEventListener('click', function () { remake({ failedOnly: true }); }); $('remakeAll').addEventListener('click', function () { remake({}, 'Make every ' + (kind === 'model' ? 'model' : 'car and rocket') + ' again? This can take a long time.'); }); function onItemClick(e) { var remakeButton = e.target.closest('[data-remake]'), previewButton = e.target.closest('[data-preview]'); if (remakeButton) remake({ id: remakeButton.dataset.remake }); else if (previewButton) preview(previewButton.dataset.preview); } $('rows').addEventListener('click', onItemClick); $('gallery').addEventListener('click', onItemClick); $('gallery').addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onItemClick(e); } }); $('previewLinks').addEventListener('click', function (e) { var button = e.target.closest('[data-remake-open]'); if (button) remake({ id: button.dataset.remakeOpen }); }); $('lodSelect').addEventListener('change', function () { loadMesh(false); }); $('versionSelect').addEventListener('change', function () { loadMesh(false); }); $('aoSwitch').addEventListener('change', function () { loadMesh(false); }); $('wireframeSwitch').addEventListener('change', function () { if (nifViewer) nifViewer.setWireframe(this.checked); }); $('colorsSwitch').addEventListener('change', function () { if (nifViewer) nifViewer.setVertexColors(this.checked); }); $('reframeButton').addEventListener('click', function () { if (nifViewer) nifViewer.frame(); }); $('previewModal').addEventListener('hidden.bs.modal', function () { current = null; if (lxfmlViewer && lxfmlViewer.dispose) lxfmlViewer.dispose(); lxfmlViewer = null; // Closed here rather than with Back: take the item out of the address too if (urlItem()) { if (pushedItem) history.back(); else history.replaceState(null, '', location.pathname); } pushedItem = false; }); window.addEventListener('popstate', function () { pushedItem = false; openFromUrl(); }); // ---- deleting and purging stored files ---- function showDeleteResult(box, d) { if (!d.success) { toast(d.error || 'Failed', 'danger'); return; } var text = d.deleted + ' deleted (' + mb(d.bytes || 0) + ' freed). ' + (d.notes || []).join(' '); if (box) box.textContent = text; toast(text, 'success'); load(); } $('previewLinks').addEventListener('click', function (e) { var button = e.target.closest('[data-delete-open]'); if (!button || !current) return; var after = button.dataset.deleteOpen; if (!confirm('Delete the files made for ' + current.id + (after === 'gone' ? ' and leave them deleted?' : ' and make them again?'))) return; api.post('/api/ugc/cache/delete', { kind: current.kind, id: current.id, after: after }).then(function (d) { showDeleteResult(null, d); }); }); function purge(all) { var body = { kind: kind, state: $('purgeState').value, owner: $('purgeOwner').value.trim(), olderThanDays: +$('purgeOlder').value || 0, unusedDays: +$('purgeUnused').value || 0, after: $('purgeAfter').value }; if (all) { var typed = prompt('This deletes every stored ' + (kind === 'model' ? 'model' : 'car and rocket') + ' file. Type PURGE ALL to go ahead.'); if (typed !== 'PURGE ALL') return; body = { kind: kind, all: true, confirm: typed, after: $('purgeAfter').value }; } else if (!body.state && !body.owner && !body.olderThanDays && !body.unusedDays) { toast('Pick a filter first, or purge all', 'warning'); return; } else if (!confirm('Delete the stored files of every ' + (kind === 'model' ? 'model' : 'car and rocket') + ' matching the filter?')) { return; } $('purgeResult').textContent = 'Deleting…'; api.post('/api/ugc/cache/purge', body).then(function (d) { showDeleteResult($('purgeResult'), d); }); } $('purgeButton').addEventListener('click', function () { purge(false); }); $('purgeAllButton').addEventListener('click', function () { purge(true); }); // ---- icon framing and light, for any item; the controls come from the server's list of parameters ---- var PARAMS = [], framingItem = null, framingKind = '', previewTimer = null, previewUrl = null; function stepDigits(step) { return step >= 1 ? 0 : 2; } function buildControls(params) { PARAMS = params; $('framingControls').innerHTML = params.map(function (p) { return '
'; }).join(''); } function framingValues() { var out = {}; PARAMS.forEach(function (p) { out[p.key] = parseFloat($('icon_' + p.key).value); }); return out; } function setValues(values) { if (!values) return; PARAMS.forEach(function (p) { if (values[p.key] === undefined || values[p.key] === null) return; $('icon_' + p.key).value = values[p.key]; $('icon_' + p.key + '_value').textContent = (+values[p.key]).toFixed(stepDigits(p.step)) + (p.unit === 'degrees' ? '\u00b0' : ''); }); } function itemQuery() { return framingItem.model ? 'kind=model&id=' + encodeURIComponent(framingItem.id) : 'kind=modular&modules=' + encodeURIComponent(framingItem.modules || ''); } function itemBody(extra) { var body = framingItem.model ? { kind: 'model', id: framingItem.id } : { kind: 'modular', modules: framingItem.modules }; for (var k in extra) body[k] = extra[k]; return body; } function loadValues() { return api.get('/api/ugc/icon/settings?' + itemQuery()).then(function (d) { if (!d.success) { $('framingState').textContent = d.error || 'Failed'; return; } framingKind = d.kind; setValues(d.settings); setValues(d.preset); setValues(d.own); $('framingState').textContent = d.own ? 'This item has its own values.' : d.preset ? 'The kind\'s preset.' : 'The default settings.'; renderPreview(); }); } function openFraming(item, model) { framingItem = { id: item.id, modules: item.modules, model: model }; var start = PARAMS.length ? Promise.resolve() : api.get('/api/ugc/icon/params').then(function (d) { if (!d.success) return; buildControls(d.params); window.ugcIconKinds = d.kinds; }); start.then(loadValues).then(function () { var label = (window.ugcIconKinds || []).find(function (k) { return k.kind === framingKind; }); $('framingKind').textContent = label ? label.label : framingKind; }); } function renderPreview() { if (!framingItem) return; var img = $('framingPreview'); img.style.opacity = 0.5; fetch('/api/ugc/icon/preview', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify(itemBody({ values: framingValues() })) }).then(function (r) { if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'HTTP ' + r.status); }); return r.blob(); }).then(function (blob) { if (previewUrl) URL.revokeObjectURL(previewUrl); previewUrl = URL.createObjectURL(blob); img.src = previewUrl; img.style.opacity = 1; }).catch(function (e) { img.style.opacity = 1; $('framingState').textContent = 'No preview: ' + e.message; }); } function saved(d) { toast(d.success ? d.message : (d.error || 'Failed'), d.success ? 'success' : 'danger'); } $('framingControls').addEventListener('input', function (e) { var p = PARAMS.find(function (x) { return 'icon_' + x.key === e.target.id; }); if (p) $('icon_' + p.key + '_value').textContent = (+e.target.value).toFixed(stepDigits(p.step)) + (p.unit === 'degrees' ? '\u00b0' : ''); clearTimeout(previewTimer); previewTimer = setTimeout(renderPreview, 350); }); $('framingPreviewButton').addEventListener('click', renderPreview); $('framingReset').addEventListener('click', function () { if (!framingItem || !confirm('Remove this item\'s own values and go back to its kind\'s?')) return; api.post('/api/ugc/icon/save', itemBody({ scope: 'item', values: null })).then(function (d) { saved(d); loadValues(); }); }); $('framingSaveType').addEventListener('click', function () { if (!confirm('Use these values for every icon of this kind? Icons already made keep theirs until they are drawn again.')) return; api.post('/api/ugc/icon/save', { scope: 'kind', kind: framingKind, values: framingValues() }).then(saved); }); $('framingSaveCombo').addEventListener('click', function () { if (!framingItem) return; api.post('/api/ugc/icon/save', itemBody({ scope: 'item', values: framingValues() })).then(function (d) { saved(d); if (d.success) $('framingState').textContent = 'This item has its own values.'; }); }); $('framingRegenerate').addEventListener('click', function () { if (!confirm('Draw every stored icon of this kind again with the saved values? (Only icons are drawn.)')) return; api.post('/api/ugc/icon/regenerate', { kind: framingKind }).then(function (d) { toast(d.success ? d.queued + ' icon' + (d.queued === 1 ? '' : 's') + ' queued' : (d.error || 'Failed'), d.success ? 'success' : 'danger'); }); }); if (window.Live) Live.on('ugc', load); var linked = urlItem(); if (linked) kind = linked.kind; $('kindButtons').querySelectorAll('[data-kind]').forEach(function (b) { b.classList.toggle('active', b.dataset.kind === kind); }); load(); if (linked) openFromUrl(); loadStatus(); setInterval(loadStatus, 10000); })();