ohmyapi commited on
Commit
b8f90fe
·
1 Parent(s): 5fc7bc7

refactor: clean HF Space - remove stale admin HTML, add .gitignore

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .env
5
+ *.log
emergent2api/static/admin/config.html DELETED
@@ -1,112 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
- <title>Emergent2API - Config</title>
6
- <style>
7
- *{margin:0;padding:0;box-sizing:border-box}
8
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f5f5f5;color:#1a1a1a}
9
- nav{background:#fff;border-bottom:1px solid #e5e5e5;padding:0 24px;display:flex;align-items:center;height:56px;position:sticky;top:0;z-index:10}
10
- nav .brand{font-weight:700;font-size:18px;margin-right:32px;color:#4f46e5}
11
- nav a{text-decoration:none;color:#666;padding:16px 12px;font-size:14px;border-bottom:2px solid transparent;transition:all .2s}
12
- nav a:hover{color:#1a1a1a}nav a.active{color:#4f46e5;border-bottom-color:#4f46e5;font-weight:600}
13
- nav .spacer{flex:1}
14
- nav .logout{color:#ef4444;cursor:pointer;font-size:13px}
15
- .container{max-width:700px;margin:0 auto;padding:28px 24px}
16
- .card{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:28px;margin-bottom:20px}
17
- .card h2{font-size:17px;margin-bottom:18px;padding-bottom:10px;border-bottom:1px solid #f0f0f0}
18
- .field{margin-bottom:18px}
19
- .field label{display:block;font-size:13px;font-weight:600;color:#555;margin-bottom:6px}
20
- .field input,.field select{width:100%;padding:10px 14px;border:1px solid #ddd;border-radius:6px;font-size:14px;outline:none;transition:border .2s}
21
- .field input:focus,.field select:focus{border-color:#4f46e5}
22
- .field .hint{font-size:11px;color:#aaa;margin-top:4px}
23
- .row{display:flex;gap:10px;align-items:end}
24
- .row .field{flex:1}
25
- .btn{padding:10px 20px;border:none;border-radius:6px;font-size:14px;cursor:pointer;font-weight:500;transition:all .2s}
26
- .btn-primary{background:#4f46e5;color:#fff}.btn-primary:hover{background:#4338ca}
27
- .btn-outline{background:#fff;color:#333;border:1px solid #ddd}.btn-outline:hover{background:#f3f4f6}
28
- .actions{display:flex;gap:10px;justify-content:flex-end;margin-top:6px}
29
- .toast{position:fixed;bottom:24px;right:24px;background:#1a1a1a;color:#fff;padding:12px 20px;border-radius:8px;font-size:13px;z-index:30;opacity:0;transition:opacity .3s}
30
- .toast.show{opacity:1}
31
- </style>
32
- </head>
33
- <body>
34
- <nav>
35
- <span class="brand">Emergent2API</span>
36
- <a href="/admin/token">Token Management</a>
37
- <a href="/admin/config" class="active">Config</a>
38
- <a href="/admin/docs">Usage Docs</a>
39
- <span class="spacer"></span>
40
- <span class="logout" onclick="logout()">Logout</span>
41
- </nav>
42
- <div class="container">
43
- <div class="card">
44
- <h2>API Settings</h2>
45
- <div class="row">
46
- <div class="field"><label>API Key</label><input id="api_key" placeholder="sk-..."></div>
47
- <div style="padding-bottom:18px"><span class="btn btn-outline" onclick="genKey()">Generate</span></div>
48
- </div>
49
- <div class="field">
50
- <label>Backend</label>
51
- <select id="backend"><option value="jobs">Jobs API (no IP restriction)</option><option value="integrations">Integrations API (faster, needs US proxy)</option></select>
52
- </div>
53
- <div class="field"><label>Proxy</label><input id="proxy" placeholder="http://user:pass@host:port"><div class="hint">Required for Integrations backend; optional for Jobs</div></div>
54
- <div class="row">
55
- <div class="field"><label>Poll Interval (s)</label><input id="poll_interval" type="number" min="1"></div>
56
- <div class="field"><label>Poll Timeout (s)</label><input id="poll_timeout" type="number" min="10"></div>
57
- </div>
58
- </div>
59
- <div class="card">
60
- <h2>Admin Settings</h2>
61
- <div class="field"><label>Admin Password</label><input id="admin_password" type="password"></div>
62
- </div>
63
- <div class="actions">
64
- <span class="btn btn-outline" onclick="load()">Reset</span>
65
- <span class="btn btn-primary" onclick="save()">Save Changes</span>
66
- </div>
67
- </div>
68
- <div class="toast" id="toast"></div>
69
- <script>
70
- const API='/v1/admin';
71
- function toast(msg,dur=3000){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),dur)}
72
-
73
- async function api(path,opts={}){
74
- const r=await fetch(API+path,opts);
75
- if(r.status===401){window.location.href='/admin/login';return null}
76
- return r;
77
- }
78
-
79
- async function load(){
80
- const r=await api('/config');if(!r)return;
81
- const d=await r.json();
82
- document.getElementById('api_key').value=d.api_key||'';
83
- document.getElementById('backend').value=d.backend||'jobs';
84
- document.getElementById('proxy').value=d.proxy||'';
85
- document.getElementById('poll_interval').value=d.poll_interval||'5';
86
- document.getElementById('poll_timeout').value=d.poll_timeout||'120';
87
- document.getElementById('admin_password').value=d.admin_password||'';
88
- }
89
-
90
- async function save(){
91
- const body={
92
- api_key:document.getElementById('api_key').value,
93
- backend:document.getElementById('backend').value,
94
- proxy:document.getElementById('proxy').value,
95
- poll_interval:document.getElementById('poll_interval').value,
96
- poll_timeout:document.getElementById('poll_timeout').value,
97
- admin_password:document.getElementById('admin_password').value,
98
- };
99
- const r=await api('/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
100
- if(r){const d=await r.json();toast(`Saved: ${d.saved.join(', ')}`)}
101
- }
102
-
103
- async function genKey(){
104
- const r=await api('/config/generate-key',{method:'POST'});
105
- if(r){const d=await r.json();document.getElementById('api_key').value=d.api_key;toast('New key generated')}
106
- }
107
-
108
- async function logout(){await api('/logout',{method:'POST'});window.location.href='/admin/login'}
109
- load();
110
- </script>
111
- </body>
112
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
emergent2api/static/admin/docs.html DELETED
@@ -1,145 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
- <title>Emergent2API - Usage Docs</title>
6
- <style>
7
- *{margin:0;padding:0;box-sizing:border-box}
8
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f5f5f5;color:#1a1a1a}
9
- nav{background:#fff;border-bottom:1px solid #e5e5e5;padding:0 24px;display:flex;align-items:center;height:56px;position:sticky;top:0;z-index:10}
10
- nav .brand{font-weight:700;font-size:18px;margin-right:32px;color:#4f46e5}
11
- nav a{text-decoration:none;color:#666;padding:16px 12px;font-size:14px;border-bottom:2px solid transparent;transition:all .2s}
12
- nav a:hover{color:#1a1a1a}nav a.active{color:#4f46e5;border-bottom-color:#4f46e5;font-weight:600}
13
- nav .spacer{flex:1}
14
- nav .logout{color:#ef4444;cursor:pointer;font-size:13px}
15
- .container{max-width:800px;margin:0 auto;padding:28px 24px}
16
- .card{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:28px;margin-bottom:20px}
17
- .card h2{font-size:17px;margin-bottom:14px;color:#4f46e5}
18
- .card h3{font-size:14px;margin:16px 0 8px;color:#333}
19
- p{font-size:14px;line-height:1.6;color:#555;margin-bottom:8px}
20
- pre{background:#1a1a2e;color:#e2e8f0;padding:16px;border-radius:8px;overflow-x:auto;font-size:12px;line-height:1.5;margin:8px 0 14px}
21
- code{font-family:'SFMono-Regular',Consolas,monospace;font-size:12px}
22
- .inline-code{background:#f1f5f9;padding:2px 6px;border-radius:4px;color:#4f46e5;font-size:13px}
23
- table{width:100%;border-collapse:collapse;margin:8px 0 14px;font-size:13px}
24
- th{text-align:left;padding:8px 12px;background:#f9fafb;color:#888;font-weight:600;border-bottom:1px solid #eee}
25
- td{padding:8px 12px;border-bottom:1px solid #f3f3f3}
26
- td code{background:#f1f5f9;padding:1px 4px;border-radius:3px}
27
- </style>
28
- </head>
29
- <body>
30
- <nav>
31
- <span class="brand">Emergent2API</span>
32
- <a href="/admin/token">Token Management</a>
33
- <a href="/admin/config">Config</a>
34
- <a href="/admin/docs" class="active">Usage Docs</a>
35
- <span class="spacer"></span>
36
- <span class="logout" onclick="logout()">Logout</span>
37
- </nav>
38
- <div class="container">
39
-
40
- <div class="card">
41
- <h2>Quick Start</h2>
42
- <p>Emergent2API exposes Emergent.sh accounts as standard OpenAI and Anthropic-compatible API endpoints. Use your configured API key in the <span class="inline-code">Authorization</span> header.</p>
43
- <pre>Base URL: https://&lt;your-host&gt;/v1
44
- API Key: sk-6loA0HwMQP1mdhPvI (default, changeable in Config)</pre>
45
- </div>
46
-
47
- <div class="card">
48
- <h2>Available Models</h2>
49
- <table>
50
- <tr><th>Model ID</th><th>Description</th></tr>
51
- <tr><td><code>claude-opus-4-6</code></td><td>Claude Opus 4.6 (full, uncapped)</td></tr>
52
- <tr><td><code>claude-sonnet-4-5</code></td><td>Claude Sonnet 4.5</td></tr>
53
- <tr><td><code>claude-sonnet-4-5-thinking</code></td><td>Claude Sonnet 4.5 with extended thinking</td></tr>
54
- </table>
55
- </div>
56
-
57
- <div class="card">
58
- <h2>API Endpoints</h2>
59
- <table>
60
- <tr><th>Method</th><th>Endpoint</th><th>Format</th></tr>
61
- <tr><td>POST</td><td><code>/v1/chat/completions</code></td><td>OpenAI Chat Completions</td></tr>
62
- <tr><td>POST</td><td><code>/v1/messages</code></td><td>Anthropic Messages</td></tr>
63
- <tr><td>POST</td><td><code>/v1/responses</code></td><td>OpenAI Response API</td></tr>
64
- <tr><td>GET</td><td><code>/v1/models</code></td><td>Model list</td></tr>
65
- </table>
66
- </div>
67
-
68
- <div class="card">
69
- <h2>Examples</h2>
70
-
71
- <h3>OpenAI Chat (curl)</h3>
72
- <pre>curl -X POST https://your-host/v1/chat/completions \
73
- -H "Authorization: Bearer sk-6loA0HwMQP1mdhPvI" \
74
- -H "Content-Type: application/json" \
75
- -d '{
76
- "model": "claude-opus-4-6",
77
- "messages": [
78
- {"role": "user", "content": "Hello!"}
79
- ],
80
- "stream": true
81
- }'</pre>
82
-
83
- <h3>Anthropic Messages (curl)</h3>
84
- <pre>curl -X POST https://your-host/v1/messages \
85
- -H "x-api-key: sk-6loA0HwMQP1mdhPvI" \
86
- -H "Content-Type: application/json" \
87
- -H "anthropic-version: 2023-06-01" \
88
- -d '{
89
- "model": "claude-opus-4-6",
90
- "max_tokens": 1024,
91
- "messages": [
92
- {"role": "user", "content": "Hello!"}
93
- ]
94
- }'</pre>
95
-
96
- <h3>Python (OpenAI SDK)</h3>
97
- <pre>from openai import OpenAI
98
-
99
- client = OpenAI(
100
- api_key="sk-6loA0HwMQP1mdhPvI",
101
- base_url="https://your-host/v1"
102
- )
103
-
104
- response = client.chat.completions.create(
105
- model="claude-opus-4-6",
106
- messages=[{"role": "user", "content": "Hello!"}],
107
- stream=True
108
- )
109
- for chunk in response:
110
- print(chunk.choices[0].delta.content or "", end="")</pre>
111
- </div>
112
-
113
- <div class="card">
114
- <h2>CherryStudio Configuration</h2>
115
- <p>1. Open CherryStudio → Settings → Model Provider → Add Custom Provider</p>
116
- <p>2. Set the following:</p>
117
- <table>
118
- <tr><th>Field</th><th>Value</th></tr>
119
- <tr><td>Base URL</td><td><code>https://your-host/v1</code></td></tr>
120
- <tr><td>API Key</td><td><code>sk-6loA0HwMQP1mdhPvI</code></td></tr>
121
- <tr><td>Model</td><td><code>claude-opus-4-6</code></td></tr>
122
- </table>
123
- <p>3. Enable "Stream" mode for real-time responses.</p>
124
- </div>
125
-
126
- <div class="card">
127
- <h2>Batch Import via CLI</h2>
128
- <p>Import accounts from a zip artifact (produced by GitHub Actions):</p>
129
- <pre>curl -X POST https://your-host/v1/admin/tokens/import-zip \
130
- -H "Cookie: emergent_admin_session=YOUR_SESSION" \
131
- -H "Content-Type: application/octet-stream" \
132
- --data-binary @0316Emergent.zip</pre>
133
- <p>Or import JSONL text directly:</p>
134
- <pre>curl -X POST https://your-host/v1/admin/tokens/import \
135
- -H "Cookie: emergent_admin_session=YOUR_SESSION" \
136
- -H "Content-Type: application/json" \
137
- -d '{"jsonl": "{\"email\":\"a@b.com\",\"password\":\"p\",\"jwt\":\"j\"}\n..."}'</pre>
138
- </div>
139
-
140
- </div>
141
- <script>
142
- async function logout(){await fetch('/v1/admin/logout',{method:'POST'});window.location.href='/admin/login'}
143
- </script>
144
- </body>
145
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
emergent2api/static/admin/login.html DELETED
@@ -1,40 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
- <title>Emergent2API - Login</title>
6
- <style>
7
- *{margin:0;padding:0;box-sizing:border-box}
8
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f5f5f5;display:flex;align-items:center;justify-content:center;min-height:100vh}
9
- .card{background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,.08);padding:40px;width:380px;text-align:center}
10
- .card h1{font-size:22px;margin-bottom:6px;color:#1a1a1a}
11
- .card p{color:#888;font-size:14px;margin-bottom:28px}
12
- input{width:100%;padding:12px 16px;border:1px solid #ddd;border-radius:8px;font-size:15px;outline:none;transition:border .2s}
13
- input:focus{border-color:#4f46e5}
14
- button{width:100%;padding:12px;background:#4f46e5;color:#fff;border:none;border-radius:8px;font-size:15px;cursor:pointer;margin-top:16px;transition:background .2s}
15
- button:hover{background:#4338ca}
16
- .err{color:#ef4444;font-size:13px;margin-top:12px;display:none}
17
- </style>
18
- </head>
19
- <body>
20
- <div class="card">
21
- <h1>Emergent2API</h1>
22
- <p>Admin Panel Login</p>
23
- <input type="password" id="pwd" placeholder="Admin Password" autofocus>
24
- <button onclick="doLogin()">Login</button>
25
- <div class="err" id="err">Invalid password</div>
26
- </div>
27
- <script>
28
- const API='/v1/admin';
29
- document.getElementById('pwd').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});
30
- async function doLogin(){
31
- const pwd=document.getElementById('pwd').value;
32
- try{
33
- const r=await fetch(API+'/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pwd})});
34
- if(r.ok){window.location.href='/admin/token'}
35
- else{document.getElementById('err').style.display='block'}
36
- }catch(e){document.getElementById('err').style.display='block'}
37
- }
38
- </script>
39
- </body>
40
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
emergent2api/static/admin/token.html DELETED
@@ -1,204 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
- <title>Emergent2API - Token Management</title>
6
- <style>
7
- *{margin:0;padding:0;box-sizing:border-box}
8
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f5f5f5;color:#1a1a1a}
9
- nav{background:#fff;border-bottom:1px solid #e5e5e5;padding:0 24px;display:flex;align-items:center;height:56px;position:sticky;top:0;z-index:10}
10
- nav .brand{font-weight:700;font-size:18px;margin-right:32px;color:#4f46e5}
11
- nav a{text-decoration:none;color:#666;padding:16px 12px;font-size:14px;border-bottom:2px solid transparent;transition:all .2s}
12
- nav a:hover{color:#1a1a1a}nav a.active{color:#4f46e5;border-bottom-color:#4f46e5;font-weight:600}
13
- nav .spacer{flex:1}
14
- nav .logout{color:#ef4444;cursor:pointer;font-size:13px}
15
- .container{max-width:1200px;margin:0 auto;padding:20px 24px}
16
- .toolbar{display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;align-items:center}
17
- .toolbar select,.toolbar input{padding:8px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;background:#fff}
18
- .btn{padding:8px 16px;border:none;border-radius:6px;font-size:13px;cursor:pointer;font-weight:500;transition:all .2s}
19
- .btn-primary{background:#4f46e5;color:#fff}.btn-primary:hover{background:#4338ca}
20
- .btn-danger{background:#ef4444;color:#fff}.btn-danger:hover{background:#dc2626}
21
- .btn-warning{background:#f59e0b;color:#fff}.btn-warning:hover{background:#d97706}
22
- .btn-success{background:#10b981;color:#fff}.btn-success:hover{background:#059669}
23
- .btn-outline{background:#fff;color:#333;border:1px solid #ddd}.btn-outline:hover{background:#f3f4f6}
24
- .stats{display:flex;gap:16px;margin-bottom:16px}
25
- .stat{background:#fff;border-radius:8px;padding:16px 20px;box-shadow:0 1px 4px rgba(0,0,0,.06);flex:1}
26
- .stat .num{font-size:28px;font-weight:700;color:#4f46e5}.stat .label{font-size:12px;color:#888;margin-top:4px}
27
- table{width:100%;border-collapse:collapse;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.06)}
28
- th{text-align:left;padding:10px 14px;font-size:12px;color:#888;background:#fafafa;font-weight:600;border-bottom:1px solid #eee}
29
- td{padding:10px 14px;font-size:13px;border-bottom:1px solid #f3f3f3}
30
- tr:hover td{background:#fafafe}
31
- .badge{padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
32
- .badge-ok{background:#d1fae5;color:#059669}.badge-off{background:#fee2e2;color:#ef4444}
33
- .modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.4);z-index:20;align-items:center;justify-content:center}
34
- .modal.open{display:flex}
35
- .modal-box{background:#fff;border-radius:12px;padding:28px;width:520px;max-height:80vh;overflow-y:auto;box-shadow:0 8px 30px rgba(0,0,0,.15)}
36
- .modal-box h3{margin-bottom:14px;font-size:17px}
37
- textarea{width:100%;height:200px;padding:10px;border:1px solid #ddd;border-radius:6px;font-size:12px;font-family:monospace;resize:vertical}
38
- .modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
39
- input[type="checkbox"]{width:16px;height:16px;cursor:pointer}
40
- .toast{position:fixed;bottom:24px;right:24px;background:#1a1a1a;color:#fff;padding:12px 20px;border-radius:8px;font-size:13px;z-index:30;opacity:0;transition:opacity .3s}
41
- .toast.show{opacity:1}
42
- </style>
43
- </head>
44
- <body>
45
- <nav>
46
- <span class="brand">Emergent2API</span>
47
- <a href="/admin/token" class="active">Token Management</a>
48
- <a href="/admin/config">Config</a>
49
- <a href="/admin/docs">Usage Docs</a>
50
- <span class="spacer"></span>
51
- <span class="logout" onclick="logout()">Logout</span>
52
- </nav>
53
- <div class="container">
54
- <div class="stats">
55
- <div class="stat"><div class="num" id="s-total">-</div><div class="label">Total Tokens</div></div>
56
- <div class="stat"><div class="num" id="s-active">-</div><div class="label">Active</div></div>
57
- <div class="stat"><div class="num" id="s-inactive">-</div><div class="label">Inactive</div></div>
58
- </div>
59
- <div class="toolbar">
60
- <select id="filter" onchange="render()">
61
- <option value="all">All</option>
62
- <option value="active">Active</option>
63
- <option value="inactive">Inactive</option>
64
- </select>
65
- <span class="btn btn-outline" onclick="selectAll()">Select All</span>
66
- <span class="btn btn-outline" onclick="selectNone()">Deselect All</span>
67
- <span style="flex:1"></span>
68
- <span class="btn btn-primary" onclick="openImport()">Import</span>
69
- <span class="btn btn-outline" onclick="doExport()">Export</span>
70
- <span class="btn btn-success" onclick="batchAction('test')">Test Selected</span>
71
- <span class="btn btn-warning" onclick="batchAction('refresh')">Refresh JWT</span>
72
- <span class="btn btn-danger" onclick="batchAction('delete')">Delete Selected</span>
73
- <span class="btn btn-danger" onclick="deleteInactive()">Delete Inactive</span>
74
- </div>
75
- <table>
76
- <thead><tr>
77
- <th><input type="checkbox" id="chk-all" onchange="toggleAll(this.checked)"></th>
78
- <th>ID</th><th>Email</th><th>Balance</th><th>Status</th><th>Last Used</th><th>Created</th><th>Actions</th>
79
- </tr></thead>
80
- <tbody id="tbody"></tbody>
81
- </table>
82
- </div>
83
-
84
- <div class="modal" id="modal-import">
85
- <div class="modal-box">
86
- <h3>Import Accounts</h3>
87
- <p style="font-size:13px;color:#888;margin-bottom:10px">Paste JSONL data (one account per line: email, password, jwt required)</p>
88
- <textarea id="import-text" placeholder='{"email":"...","password":"...","jwt":"..."}'></textarea>
89
- <p style="font-size:12px;color:#888;margin-top:8px">Or upload a .zip file containing accounts.jsonl:</p>
90
- <input type="file" id="import-file" accept=".zip" style="margin-top:6px">
91
- <div class="modal-actions">
92
- <span class="btn btn-outline" onclick="closeImport()">Cancel</span>
93
- <span class="btn btn-primary" onclick="doImportText()">Import JSONL</span>
94
- <span class="btn btn-primary" onclick="doImportZip()">Import Zip</span>
95
- </div>
96
- </div>
97
- </div>
98
-
99
- <div class="toast" id="toast"></div>
100
-
101
- <script>
102
- const API='/v1/admin';
103
- let tokens=[];let selected=new Set();
104
-
105
- async function api(path,opts={}){
106
- const r=await fetch(API+path,opts);
107
- if(r.status===401){window.location.href='/admin/login';return null}
108
- return r;
109
- }
110
-
111
- function toast(msg,dur=3000){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),dur)}
112
-
113
- async function load(){
114
- const r=await api('/tokens');if(!r)return;
115
- const d=await r.json();
116
- tokens=d.tokens||[];
117
- document.getElementById('s-total').textContent=d.total;
118
- document.getElementById('s-active').textContent=d.active;
119
- document.getElementById('s-inactive').textContent=d.total-d.active;
120
- render();
121
- }
122
-
123
- function render(){
124
- const f=document.getElementById('filter').value;
125
- const tbody=document.getElementById('tbody');
126
- let rows=tokens;
127
- if(f==='active')rows=rows.filter(t=>t.is_active);
128
- else if(f==='inactive')rows=rows.filter(t=>!t.is_active);
129
- tbody.innerHTML=rows.map(t=>`<tr>
130
- <td><input type="checkbox" class="row-chk" data-id="${t.id}" ${selected.has(t.id)?'checked':''} onchange="toggleSel(${t.id},this.checked)"></td>
131
- <td>${t.id}</td>
132
- <td style="font-family:monospace;font-size:12px">${t.email}</td>
133
- <td>${t.balance!=null?'$'+t.balance.toFixed(2):'-'}</td>
134
- <td><span class="badge ${t.is_active?'badge-ok':'badge-off'}">${t.is_active?'Active':'Inactive'}</span></td>
135
- <td style="font-size:12px;color:#888">${t.last_used?new Date(t.last_used).toLocaleString():'-'}</td>
136
- <td style="font-size:12px;color:#888">${t.created_at?new Date(t.created_at).toLocaleDateString():'-'}</td>
137
- <td>
138
- <span class="btn btn-outline" style="padding:4px 8px;font-size:11px" onclick="toggleOne(${t.id})">${t.is_active?'Disable':'Enable'}</span>
139
- </td>
140
- </tr>`).join('');
141
- }
142
-
143
- function toggleSel(id,c){if(c)selected.add(id);else selected.delete(id)}
144
- function selectAll(){tokens.forEach(t=>selected.add(t.id));render()}
145
- function selectNone(){selected.clear();render()}
146
- function toggleAll(c){if(c)selectAll();else selectNone()}
147
-
148
- async function toggleOne(id){
149
- await api('/tokens/toggle',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:[id],active:!tokens.find(t=>t.id===id)?.is_active})});
150
- load();
151
- }
152
-
153
- async function batchAction(action){
154
- const ids=[...selected];
155
- if(!ids.length){toast('Select accounts first');return}
156
- if(action==='delete'&&!confirm(`Delete ${ids.length} accounts?`))return;
157
- const endpoint=`/tokens/${action}`;
158
- toast(`Processing ${ids.length} accounts...`);
159
- const r=await api(endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids})});
160
- if(r){const d=await r.json();toast(JSON.stringify(d))}
161
- selected.clear();load();
162
- }
163
-
164
- async function deleteInactive(){
165
- if(!confirm('Delete all inactive accounts?'))return;
166
- const r=await api('/tokens/delete-inactive',{method:'POST'});
167
- if(r){const d=await r.json();toast(`Deleted ${d.deleted} inactive accounts`)}
168
- load();
169
- }
170
-
171
- function openImport(){document.getElementById('modal-import').classList.add('open')}
172
- function closeImport(){document.getElementById('modal-import').classList.remove('open')}
173
-
174
- async function doImportText(){
175
- const text=document.getElementById('import-text').value.trim();
176
- if(!text){toast('Paste JSONL data first');return}
177
- const r=await api('/tokens/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({jsonl:text})});
178
- if(r){const d=await r.json();toast(`Imported ${d.imported} accounts`+(d.errors.length?`, ${d.errors.length} errors`:''));closeImport();load()}
179
- }
180
-
181
- async function doImportZip(){
182
- const f=document.getElementById('import-file').files[0];
183
- if(!f){toast('Select a zip file');return}
184
- const buf=await f.arrayBuffer();
185
- const r=await api('/tokens/import-zip',{method:'POST',headers:{'Content-Type':'application/octet-stream'},body:buf});
186
- if(r){const d=await r.json();toast(`Imported ${d.imported} accounts`);closeImport();load()}
187
- }
188
-
189
- async function doExport(){
190
- const r=await api('/tokens/export');
191
- if(!r)return;
192
- const blob=await r.blob();
193
- const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='emergent_accounts.zip';a.click();
194
- }
195
-
196
- async function logout(){
197
- await api('/logout',{method:'POST'});
198
- window.location.href='/admin/login';
199
- }
200
-
201
- load();
202
- </script>
203
- </body>
204
- </html>