PageSourceSearch

https://factlens.pro/admin-api-platform.js

js factlens.pro collected 2026-10-02 02:05:54 UTC 15,273 bytes, 157 lines download raw bytes

1(() => {
2  const esc = (value) => String(value ?? '')
3    .replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
4    .replaceAll('"', '&quot;').replaceAll("'", '&#039;');
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.