UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

334 lines (302 loc) 13.3 kB
<!doctype html> <html> <head> <meta charset="utf-8"> <title>JOE Plugins — Inventory</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px} h1{margin:8px 0} .small{font-size:12px;color:#666} table{width:100%;border-collapse:collapse;margin-top:10px} th,td{border:1px solid #e1e4e8;padding:8px;text-align:left;font-size:14px} th.sortable{cursor:pointer} th.sorted-asc, th.sorted-desc{background:#eef} th .sort-ind{font-size:12px;opacity:.6;margin-left:6px} .chips{display:flex;gap:6px;flex-wrap:wrap} .chip{display:inline-block;padding:2px 8px;border-radius:12px;background:#eef;border:1px solid #ccd;font-size:12px} .chip.joe{background:#11bcd6;color:#fff;border-color:#0fa5bb} .chip.app{background:#e0ecff;border-color:#b3c7ff} .chip.async{background:#dcfce7;border-color:#16a34a} .chip.protected{background:#fee2e2;border-color:#dc2626} .badge{display:inline-block;padding:2px 6px;border-radius:10px;font-size:12px;background:#eef;border:1px solid #ccd} .badge.warn{background:#f0c040;color:#000;border-color:#e5b93b} .badge.ok{background:#0a7d00;color:#fff;border-color:#0a7d00} .badge.info{background:#e5e7eb;color:#111827;border-color:#d1d5db} .t-center{text-align:center} th{background:#f6f8fa} .status-ok{color:#0a7d00;font-size:22px} .status-bad{color:#b00020;font-size:22px} .row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:8px 0} a[target]{text-decoration:none} code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px} </style> </head> <body> <div id="mcp-nav"></div> <script src="/JsonObjectEditor/_www/mcp-nav.js"></script> <h1>Plugin Inventory</h1> <div class="small"> Overview of active JOE plugins and their async methods, with app usage. Useful for verifying that methods like <code>chatgpt.autofill</code> and <code>chatgpt.widgetStart</code> are present on this instance. </div> <div class="row" style="margin-top:12px;"> <label for="base" style="margin:0">Base URL</label> <input id="base" value="" placeholder="http://localhost:2025" style="min-width:260px" /> <button id="refresh">Refresh</button> <span id="status" class="small"></span> </div> <div class="row" style="margin-top:4px;"> <label for="appFilter" style="margin:0">Filter by App</label> <select id="appFilter"> <option value="">All</option> </select> <label for="nameFilter" style="margin:0">Filter by Plugin</label> <input id="nameFilter" placeholder="plugin name contains..." style="min-width:200px" /> </div> <table> <thead> <tr> <th class="sortable" data-key="name">Plugin <span class="sort-ind"></span></th> <th>Path</th> <th class="sortable" data-key="methodCount">Async Methods <span class="sort-ind"></span></th> <th>Methods</th> <th>Apps Using</th> <th class="sortable" data-key="protectedCount">Protected <span class="sort-ind"></span></th> <th>Notes</th> </tr> </thead> <tbody id="rows"></tbody> </table> <script> (function(){ var $ = function(id){ return document.getElementById(id); }; var base = $('base'); var refresh = $('refresh'); var status = $('status'); var rows = $('rows'); var appFilterSel = $('appFilter'); var nameFilter = $('nameFilter'); base.value = base.value || location.origin; function setStatus(msg, ok){ status.textContent = msg||''; status.className = 'small ' + (ok===true?'status-ok': ok===false?'status-bad':''); } async function fetchJSON(url, opts){ const res = await fetch(url, opts); if(!res.ok){ let body; try{ body = await res.text(); }catch(_e){ body=''; } throw new Error('HTTP '+res.status+' '+body); } return res.json(); } async function callMCP(baseUrl, method, params){ const url = baseUrl + '/mcp'; const body = { jsonrpc:'2.0', id:String(Date.now()), method:method, params:params||{} }; const resp = await fetch(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) }); if(!resp.ok){ throw new Error('HTTP '+resp.status+' '+(await resp.text())); } const j = await resp.json(); return (j && (j.result || j)) || {}; } async function load(){ try{ setStatus('Loading...', null); rows.innerHTML = ''; var baseUrl = base.value.replace(/\/$/,''); // 1) Load plugin list via plugin-utils var plugData = await fetchJSON(baseUrl + '/API/plugin/plugin-utils'); var plugins = (plugData && plugData.plugins) || {}; // 2) Load apps via MCP listApps so we can see which apps reference which plugins var appMap = {}; try{ var la = await callMCP(baseUrl, 'listApps', {}); appMap = (la && la.apps) || {}; }catch(_e){ appMap = {}; } // Build app -> plugins map and plugin -> apps usage map var pluginApps = {}; // { pluginName: [appName,...] } Object.keys(appMap || {}).forEach(function(appName){ var app = appMap[appName] || {}; var appPlugins = Array.isArray(app.plugins) ? app.plugins : []; appPlugins.forEach(function(p){ if(!p) return; pluginApps[p] = pluginApps[p] || []; if (pluginApps[p].indexOf(appName) === -1){ pluginApps[p].push(appName); } }); }); // Populate app filter options if (appFilterSel){ // Clear existing (keep "All") while(appFilterSel.options.length > 1){ appFilterSel.remove(1); } Object.keys(appMap || {}).sort().forEach(function(a){ var opt=document.createElement('option'); opt.value=a; opt.textContent=a; appFilterSel.appendChild(opt); }); } // Normalize plugin rows var data = Object.keys(plugins || {}).sort().map(function(name){ var p = plugins[name] || {}; var asyncMethods = Array.isArray(p.async) ? p.async.slice().sort() : []; var topLevelMethods = Array.isArray(p.methods) ? p.methods.slice().sort() : []; var usedBy = (pluginApps[name] || []).slice().sort(); var protectedList = Array.isArray(p.protected) ? p.protected : []; // Union of async/top-level/protected so we show everything. var methodSet = {}; asyncMethods.forEach(function(m){ if(m){ methodSet[m] = true; }}); topLevelMethods.forEach(function(m){ if(m){ methodSet[m] = true; }}); protectedList.forEach(function(m){ if(m){ methodSet[m] = true; }}); var allMethods = Object.keys(methodSet).sort(); return { name: name, path: p._pathname || '', override: !!p._override, methodsAll: allMethods, asyncMethods: asyncMethods, methodCount: asyncMethods.length, usedBy: usedBy, protected: protectedList, protectedCount: protectedList.length }; }); function getFilteredData(){ var appSel = (appFilterSel && appFilterSel.value) || ''; var nameSel = (nameFilter && nameFilter.value || '').toLowerCase().trim(); return data.filter(function(d){ if (appSel && (!d.usedBy || d.usedBy.indexOf(appSel) === -1)){ return false; } if (nameSel && d.name.toLowerCase().indexOf(nameSel) === -1){ return false; } return true; }); } var sortState = { key:'name', dir:'asc' }; function sortData(key){ if (sortState.key === key){ sortState.dir = (sortState.dir === 'asc') ? 'desc' : 'asc'; } else { sortState.key = key; sortState.dir = (key === 'methodCount' || key === 'protectedCount') ? 'desc' : 'asc'; } var dir = sortState.dir === 'asc' ? 1 : -1; var baseList = getFilteredData(); var sorted = baseList.slice().sort(function(a,b){ var av = a[key], bv = b[key]; if (typeof av === 'string' && typeof bv === 'string'){ av = av.toLowerCase(); bv = bv.toLowerCase(); } if (av > bv) return 1*dir; if (av < bv) return -1*dir; return 0; }); renderTable(sorted); updateHeaderIndicators(); } function updateHeaderIndicators(){ var ths = document.querySelectorAll('th.sortable'); Array.prototype.forEach.call(ths, function(th){ th.classList.remove('sorted-asc','sorted-desc'); var ind = th.querySelector('.sort-ind'); if (ind) ind.textContent = ''; if (th.getAttribute('data-key') === sortState.key){ th.classList.add(sortState.dir === 'asc' ? 'sorted-asc' : 'sorted-desc'); if (ind) ind.textContent = sortState.dir === 'asc' ? '▲' : '▼'; } }); } function renderTable(list){ rows.innerHTML = ''; (list||[]).forEach(function(item){ var tr = document.createElement('tr'); function td(html, cls){ var c=document.createElement('td'); if(cls){c.className=cls;} c.innerHTML = html||''; return c; } // Plugin name tr.appendChild(td('<strong>'+item.name+'</strong>')); // Path tr.appendChild(td(item.path ? ('<code>'+item.path+'</code>') : '<span class="small">n/a</span>')); // Async method count tr.appendChild(td(String(item.methodCount), 't-center')); // Methods list (all top-level + async). Color-code: // - async methods: green chip // - protected methods: red chip var methodsHTML; if (item.methodsAll && item.methodsAll.length){ methodsHTML = '<div class="chips">'+item.methodsAll.map(function(m){ var classes = ['chip']; if (item.asyncMethods && item.asyncMethods.indexOf(m) !== -1){ classes.push('async'); } if (item.protected && item.protected.indexOf(m) !== -1){ classes.push('protected'); } return '<span class="'+classes.join(' ')+'">'+m+'</span>'; }).join(' ') + '</div>'; } else { methodsHTML = '<span class="small">None</span>'; } tr.appendChild(td(methodsHTML)); // Apps using var appsHTML = item.usedBy.length ? '<div class="chips">'+item.usedBy.map(function(app){ var cls = 'chip app'; var link = '/JOE/'+app; return '<a class="'+cls+'" href="'+link+'" target="_app_'+app+'">'+app+'</a>'; }).join(' ')+'</div>' : '<span class="small">None</span>'; tr.appendChild(td(appsHTML)); // Protected list var protHTML = item.protectedCount ? '<div class="chips">'+item.protected.map(function(m){ return '<span class="chip">'+m+'</span>'; }).join(' ')+'</div>' : '<span class="small">None</span>'; tr.appendChild(td(protHTML, 't-center')); // Notes var notes = []; if (item.override){ notes.push('<span class="badge warn">override</span>'); } if (item.name === 'chatgpt'){ notes.push('<span class="badge info">AI / Responses+MCP</span>'); } tr.appendChild(td(notes.join(' ') || '<span class="small">—</span>')); rows.appendChild(tr); }); } // Attach header sort handlers once Array.prototype.forEach.call(document.querySelectorAll('th.sortable'), function(th){ th.onclick = function(){ sortData(th.getAttribute('data-key')); }; }); if (appFilterSel){ appFilterSel.onchange = function(){ sortData(sortState.key || 'name'); }; } if (nameFilter){ nameFilter.oninput = function(){ sortData(sortState.key || 'name'); }; } sortData('name'); setStatus('Loaded '+Object.keys(plugins||{}).length+' plugins', true); }catch(e){ setStatus(e.message||String(e), false); } } refresh.onclick = load; setTimeout(load, 50); })(); </script> </body> </html>