1(() => { 2 const esc = (value) => String(value ?? '') 3 .replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>') 4 .replaceAll('"', '"').replaceAll("'", '''); 5 const money = (cents) => `$${(Number(cents || 0) / 100).toFixed(2)}`; 6 const dateText = (value) => value ? new Date(value).toLocaleString() : 'Never'; 7 const root = () => document.getElementById('admin-main-content'); 8 let projectInspectState = null; 9 10 function toast(message, tone = 'success') { 11 try { showToast(message, tone); } 12 catch { console[tone === 'error' ? 'error' : 'log'](message); } 13 } 14 15 function addStyles() { 16 if (document.getElementById('factlens-admin-api-platform-style')) return; 17 const style = document.createElement('style'); 18 style.id = 'factlens-admin-api-platform-style'; 19 style.textContent = ` 20 .fl-api-projects{margin:0 0 1.4rem}.fl-api-projects-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;margin-bottom:.8rem;flex-wrap:wrap} 21 .fl-api-projects-head h2{margin:0;font-size:1rem}.fl-api-projects-head p{margin:.25rem 0 0;color:var(--muted);font-size:.75rem} 22 .fl-api-project-summary{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:.7rem;margin-bottom:1rem} 23 .fl-api-project-stat{border:1px solid var(--border);border-radius:12px;padding:.85rem;background:rgba(255,255,255,.015)} 24 .fl-api-project-stat span{display:block;color:var(--muted);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em}.fl-api-project-stat strong{display:block;margin-top:.25rem;font-size:1.05rem} 25 .fl-api-project-user{display:grid;gap:.2rem}.fl-api-project-user small{color:var(--muted)} 26 .fl-api-project-inspect-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:1rem} 27 .fl-api-project-meta{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.7rem;margin:1rem 0}.fl-api-project-meta>div{border:1px solid var(--border);border-radius:11px;padding:.8rem} 28 .fl-api-project-meta span{display:block;color:var(--muted);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em}.fl-api-project-meta strong{display:block;margin-top:.2rem;font-size:.88rem;word-break:break-word} 29 .fl-api-project-request-row{cursor:default}.fl-api-project-request-row:hover{background:rgba(255,255,255,.025)} 30 @media(max-width:1000px){.fl-api-project-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.fl-api-project-meta{grid-template-columns:repeat(2,minmax(0,1fr))}} 31 @media(max-width:620px){.fl-api-project-summary,.fl-api-project-meta{grid-template-columns:1fr}} 32 `; 33 document.head.append(style); 34 } 35 36 async function projects() { 37 return await sbRpc('factlens_admin_api_projects'); 38 } 39 40 function projectSummary(rows) { 41 const totalRequests = rows.reduce((sum, row) => sum + Number(row.request_count || 0), 0); 42 const paid = [...new Map(rows.map((row) => [row.user_id, row])).values()].reduce((sum, row) => sum + Number(row.shared_paid_requests || 0), 0); 43 const sdk = rows.reduce((sum, row) => sum + Number(row.sdk_request_count || 0), 0); 44 const active = rows.filter((row) => row.status === 'active').length; 45 return `<div class="fl-api-project-summary"> 46 <div class="fl-api-project-stat"><span>Developer projects</span><strong>${rows.length.toLocaleString()}</strong></div> 47 <div class="fl-api-project-stat"><span>Active projects</span><strong>${active.toLocaleString()}</strong></div> 48 <div class="fl-api-project-stat"><span>API requests</span><strong>${totalRequests.toLocaleString()}</strong></div> 49 <div class="fl-api-project-stat"><span>SDK requests</span><strong>${sdk.toLocaleString()}</strong></div> 50 <div class="fl-api-project-stat"><span>Account balance</span><strong>${money(paid)}</strong></div> 51 </div>`; 52 } 53 54 function projectTable(rows) { 55 return `<div class="data-table-wrap"><div class="data-table-header"><div><div class="data-table-title">Developer projects</div><div class="muted" style="font-size:.75rem">User, credits, keys, SDK use, and durable request history.</div></div></div><div class="data-table-scroll"><table class="data-table"><thead><tr><th>
55Developer</th><th>Project</th><th>Free today</th><th>Account balance</th><th>Requests</th><th>SDK</th><th>Last active</th><th></th></tr></thead><tbody>${rows.length ? rows.map((row) => `<tr> 56 <td><div class="fl-api-project-user"><strong>${esc(row.email || 'Unknown')}</strong><small><code>${esc(row.user_id)}</code></small></div></td> 57 <td><strong>${esc(row.project_name)}</strong><small style="display:block;color:var(--muted)">${esc(row.status)} · ${Number(row.account_rate_limit_per_minute || 0)}/min · ${Number(row.key_count || 0)} key${Number(row.key_count || 0) === 1 ? '' : 's'}</small></td> 58 <td><strong>${money(row.shared_free_remaining_requests)}</strong><small style="display:block;color:var(--muted)">${money(row.shared_free_used_today_requests)} used / ${money(row.shared_free_daily_limit_requests)}</small></td> 59 <td>${money(row.shared_paid_requests)}</td> 60 <td>${Number(row.request_count || 0).toLocaleString()}<small style="display:block;color:var(--muted)">${Number(row.metered_requests || 0).toLocaleString()}¢ metered</small></td> 61 <td>${Number(row.sdk_request_count || 0).toLocaleString()}<small style="display:block;color:var(--muted)">${esc([row.last_sdk_name,row.last_sdk_version].filter(Boolean).join('/') || 'REST only')}</small></td> 62 <td>${esc(dateText(row.last_active_at))}</td> 63 <td><button type="button" class="action-btn" data-api-project-inspect="${esc(row.project_id)}">Inspect project</button></td> 64 </tr>`).join('') : '<tr><td colspan="8" class="no-data">No developer API projects.</td></tr>'}</tbody></table></div></div>`; 65 } 66 67 async function decorateManagedApiProjects() { 68 const container = root(); 69 if (!container || container.querySelector('[data-api-project-platform]')) return; 70 let rows; 71 try { rows = await projects(); } 72 catch { return; } 73 addStyles(); 74 const section = document.createElement('section'); 75 section.dataset.apiProjectPlatform = 'true'; 76 section.className = 'fl-api-projects'; 77 section.innerHTML = `${projectSummary(rows || [])}${projectTable(rows || [])}`; 78 const firstTable = container.querySelector('.data-table-wrap'); 79 if (firstTable) firstTable.insertAdjacentElement('beforebegin', section); 80 else container.append(section); 81 section.querySelectorAll('[data-api-project-inspect]').forEach((button) => button.addEventListener('click', () => inspectApiProject(button.dataset.apiProjectInspect))); 82 } 83 84 function requestFilters() { 85 const container = root(); 86 const requestId = container?.querySelector('#api-project-request-id')?.value.trim() || ''; 87 const iso = (value) => { if (!value) return null; const d = new Date(value); return Number.isNaN(d.getTime()) ? null : d.toISOString(); }; 88 return { 89 p_status: container?.querySelector('#api-project-status')?.value || null, 90 p_mode: container?.querySelector('#api-project-mode')?.value || null, 91 p_request_id: /^[0-9a-f-]{36}$/i.test(requestId) ? requestId : null, 92 p_ip: container?.querySelector('#api-project-ip')?.value.trim() || null, 93 p_origin: container?.querySelector('#api-project-origin')?.value.trim() || null, 94 p_from: iso(container?.querySelector('#api-project-from')?.value), 95 p_to: iso(container?.querySelector('#api-project-to')?.value), 96 }; 97 } 98 99 async function loadProjectRequests({ append = false } = {}) { 100 if (!projectInspectState) return; 101 const page = await sbRpc('factlens_admin_api_project_request_list', { 102 p_project_id: projectInspectState.project.project_id, 103 p_limit: 100, 104 p_before: append ? projectInspectState.before : null, 105 ...requestFilters(), 106 }); 107 projectInspectState.rows = append ? [...projectInspectState.rows, ...(page || [])] : (page || []); 108 projectInspectState.before = page?.length ? page[page.length - 1].received_at : null; 109 projectInspectState.hasMore = Array.isArray(page) && page.length === 100; 110 renderProjectRequests(); 111 } 112 113 function renderProjectRequests() { 114 const target = root()?.querySelector('#api-project-results'); 115 if (!target || !projectInspectState) return; 116 const rows = projectInspectState.rows; 117 target.innerHTML = `<div class="data-table-wrap"><div class="data-table-header"><div><div class="data-table-title">Request history</div><div class="muted" style="font-size:.75rem">
117${rows.length.toLocaleString()} loaded · retained audit history remains visible after key deletion.</div></div></div><div class="data-table-scroll"><table class="data-table"><thead><tr><th>Received</th><th>Endpoint</th><th>Key</th><th>Status</th><th>Cost</th><th>SDK</th><th>Origin / IP</th><th>Request ID</th><th></th></tr></thead><tbody>${rows.length ? rows.map((row) => `<tr class="fl-api-project-request-row"><td>${esc(dateText(row.received_at))}</td><td><strong>${esc(row.endpoint || row.mode || 'verify')}</strong><small style="display:block;color:var(--muted)">${esc(row.mode)}</small></td><td>${row.api_key_id ? `<span>${esc(row.key_label || 'Deleted key')}</span><small style="display:block;color:var(--muted)">${esc(row.key_prefix || row.api_key_id)}</small>` : '<span class="fl-admin-muted">Deleted key</span>'}</td><td>${row.http_status || 'â'}<small style="display:block;color:var(--muted)">${esc(row.status || '')}${row.error_code ? ` · ${esc(row.error_code)}` : ''}</small></td><td>${row.cost_cents == null ? 'â' : `${Number(row.cost_cents)}¢`}<small style="display:block;color:var(--muted)">${esc(row.charge_source || '')}</small></td><td>${esc([row.sdk_name,row.sdk_version].filter(Boolean).join('/') || 'REST')}</td><td>${esc(row.origin || row.referer || 'â')}<small style="display:block;color:var(--muted)">${esc(row.source_ip || 'No IP')}</small></td><td><code>${esc(row.request_id)}</code></td><td><button type="button" class="action-btn" data-api-request-inspect="${esc(row.request_id)}">Inspect</button></td></tr>`).join('') : '<tr><td colspan="9" class="no-data">No matching requests in the retained history.</td></tr>'}</tbody></table></div></div>${projectInspectState.hasMore ? '<div style="display:flex;justify-content:center;margin:1rem"><button type="button" class="action-btn" id="api-project-more">Load older requests</button></div>' : ''}`; 118 target.querySelectorAll('[data-api-request-inspect]').forEach((button) => button.addEventListener('click', () => window.inspectManagedApiRequest?.(button.dataset.apiRequestInspect))); 119 target.querySelector('#api-project-more')?.addEventListener('click', () => loadProjectRequests({ append: true }).catch((error) => toast(error.message, 'error'))); 120 } 121 122 async function inspectApiProject(projectId) { 123 const rows = await projects(); 124 const project = (rows || []).find((row) => row.project_id === projectId); 125 if (!project) return toast('Developer project not found.', 'error'); 126 projectInspectState = { project, rows: [], before: null, hasMore: false }; 127 const container = root(); 128 if (!container) return; 129 addStyles(); 130 container.innerHTML = `<div class="fl-api-project-inspect-head"><div><button type="button" class="action-btn" id="api-project-back">â FactLens API</button>
130<div class="admin-page-title" style="margin-top:1rem">${esc(project.project_name)}</div><div class="admin-page-sub">${esc(project.email)} · <code>${esc(project.user_id)}</code> · durable project-level API inspection.</div></div></div> 131 <div class="fl-api-project-meta"><div><span>Status</span><strong>${esc(project.status)}</strong></div><div><span>Free today</span><strong>${money(project.free_remaining_today_cents)} remaining / ${money(project.daily_free_cents)}</strong></div><div><span>Account balance</span><strong>${money(project.paid_balance_cents)}</strong></div><div><span>Rate limit</span><strong>${Number(project.rate_limit_per_minute || 0)}/min</strong></div><div><span>Requests</span><strong>${Number(project.request_count || 0).toLocaleString()}</strong></div><div><span>Keys</span><strong>${Number(project.key_count || 0).toLocaleString()}</strong></div><div><span>SDK requests</span><strong>${Number(project.sdk_request_count || 0).toLocaleString()}</strong></div><div><span>Last SDK</span><strong>${esc([project.last_sdk_name,project.last_sdk_version].filter(Boolean).join('/') || 'REST only')}</strong></div></div> 132 <section class="stat-card" style="margin-bottom:1rem"><div class="data-table-title" style="margin-bottom:.8rem">Filters</div><div class="fl-admin-filter-grid"><label class="fl-admin-field">Status<select id="api-project-status" class="data-table-search"><option value="">All</option><option value="completed">Completed</option><option value="failed">Failed</option><option value="started">Started</option></select></label><label class="fl-admin-field">Mode<select id="api-project-mode" class="data-table-search"><option value="">All</option><option value="text">Text</option><option value="image_post">Image / post</option><option value="audio_video">Audio / video</option><option value="search">Search</option><option value="ai">AI</option><option value="transcribe">Transcribe</option></select></label><label class="fl-admin-field">Request ID<input id="api-project-request-id" class="data-table-search" placeholder="UUID"></label><label class="fl-admin-field">IP contains<input id="api-project-ip" class="data-table-search"></label><label class="fl-admin-field">Origin contains<input id="api-project-origin" class="data-table-search"></label><label class="fl-admin-field">From<input id="api-project-from" class="data-table-search" type="datetime-local"></label><label class="fl-admin-field">To<input id="api-project-to" class="data-table-search" type="datetime-local"></label><button type="button" class="action-btn" id="api-project-apply">Apply filters</button></div></section> 133 <div id="api-project-results"><div class="no-data">Loading requestsâ¦</div></div>`; 134 container.querySelector('#api-project-back')?.addEventListener('click', async () => { projectInspectState = null; if (typeof renderManagedApi === 'function') await renderManagedApi(container); }); 135 container.querySelector('#api-project-apply')?.addEventListener('click', () => loadProjectRequests().catch((error) => toast(error.message, 'error'))); 136 await loadProjectRequests(); 137 } 138 139 function install() { 140 addStyles(); 141 if (typeof renderManagedApi === 'function' && !renderManagedApi.__apiPlatformWrapped) { 142 const original = renderManagedApi; 143 const wrapped = async function (...args) { 144 const result = await original.apply(this, args); 145 await decorateManagedApiProjects(); 146 return result; 147 }; 148 wrapped.__apiPlatformWrapped = true; 149 renderManagedApi = wrapped; 150 } 151 const title = root()?.querySelector('.admin-page-title')?.textContent || ''; 152 if (/FactLens API/i.test(title)) void decorateManagedApiProjects(); 153 } 154 155 window.inspectApiDeveloperProject = inspectApiProject; 156 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', install, { once: true }); else install(); 157})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.