Spaces:
Running
Running
nacho commited on
Commit ·
3cb96d8
1
Parent(s): f249601
feat: account detail modal — click eye icon to see full info + screenshots per account
Browse files- .deepseek/pastes/paste-2026-05-18-124817-6d59a6f3.md +0 -0
- main.py +39 -0
- static/index.html +65 -1
.deepseek/pastes/paste-2026-05-18-124817-6d59a6f3.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
main.py
CHANGED
|
@@ -443,6 +443,45 @@ async def login_account(request: Request, admin_key: str = Header(...)):
|
|
| 443 |
return {"ok": True, "message": "Login task started"}
|
| 444 |
|
| 445 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
@app.post("/admin/verify")
|
| 447 |
async def admin_verify(request: Request):
|
| 448 |
"""Verify admin key for panel login."""
|
|
|
|
| 443 |
return {"ok": True, "message": "Login task started"}
|
| 444 |
|
| 445 |
|
| 446 |
+
@app.get("/admin/accounts/{email:path}")
|
| 447 |
+
async def get_account_detail(email: str, admin_key: str = Header(...)):
|
| 448 |
+
"""Get detailed info for a single account, including related screenshots."""
|
| 449 |
+
if admin_key != config.server.admin_key:
|
| 450 |
+
raise HTTPException(status_code=401, detail="Invalid admin key")
|
| 451 |
+
|
| 452 |
+
if email not in manager.accounts:
|
| 453 |
+
raise HTTPException(status_code=404, detail="Account not found")
|
| 454 |
+
|
| 455 |
+
a = manager.accounts[email]
|
| 456 |
+
detail = {
|
| 457 |
+
"email": a.email, "name": a.name, "proxy": a.proxy,
|
| 458 |
+
"in_use": a.in_use, "logged_in": a.logged_in,
|
| 459 |
+
"is_muted": a.is_muted, "muted_until": a.muted_until,
|
| 460 |
+
"error_count": a.error_count, "last_error": a.last_error,
|
| 461 |
+
"last_used": a.last_used,
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
safe = email.replace("@", "_at_").replace("+", "_plus_")
|
| 465 |
+
screenshots = []
|
| 466 |
+
if SCREENSHOT_DIR.exists():
|
| 467 |
+
for f in sorted(SCREENSHOT_DIR.glob(f"*{safe}*.png"), key=lambda p: p.stat().st_mtime, reverse=True):
|
| 468 |
+
txt = f.with_suffix(".txt")
|
| 469 |
+
err = ""
|
| 470 |
+
if txt.exists():
|
| 471 |
+
try:
|
| 472 |
+
err = txt.read_text(encoding="utf-8").strip()
|
| 473 |
+
except Exception:
|
| 474 |
+
pass
|
| 475 |
+
screenshots.append({
|
| 476 |
+
"name": f.name, "url": f"/static/screenshots/{f.name}",
|
| 477 |
+
"size_kb": round(f.stat().st_size / 1024, 1),
|
| 478 |
+
"time": time.strftime("%m-%d %H:%M", time.localtime(f.stat().st_mtime)),
|
| 479 |
+
"error": err,
|
| 480 |
+
})
|
| 481 |
+
detail["screenshots"] = screenshots
|
| 482 |
+
return detail
|
| 483 |
+
|
| 484 |
+
|
| 485 |
@app.post("/admin/verify")
|
| 486 |
async def admin_verify(request: Request):
|
| 487 |
"""Verify admin key for panel login."""
|
static/index.html
CHANGED
|
@@ -238,6 +238,22 @@ html.light-mode .log-viewer{background:rgba(0,0,0,.04);color:#475569}
|
|
| 238 |
|
| 239 |
@media(max-width:639px){.hide-xs{display:none!important}}
|
| 240 |
@media(max-width:1023px){.hide-md{display:none!important}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
</style>
|
| 242 |
</head>
|
| 243 |
<body>
|
|
@@ -397,6 +413,17 @@ html.light-mode .log-viewer{background:rgba(0,0,0,.04);color:#475569}
|
|
| 397 |
</div>
|
| 398 |
</div>
|
| 399 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
</main>
|
| 401 |
</div>
|
| 402 |
|
|
@@ -592,7 +619,7 @@ async function loadAccounts(){
|
|
| 592 |
<td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
|
| 593 |
<td class="hide-xs">${a.is_muted?`<span class="badge badge-warn" title="${a.muted_until||''}">禁言</span>`:'<span class="badge badge-idle">正常</span>'}</td>
|
| 594 |
<td class="hide-xs">${a.error_count>0?`<span class="badge badge-off" title="${(a.last_error||'').replace(/"/g,'"').replace(/'/g,"'")}">${a.error_count}</span>`:'—'}</td>
|
| 595 |
-
<td><button class="btn btn-sm" onclick="doLoginAccount('${a.email}')">${a.logged_in?'重连':'登录'}</button></td>
|
| 596 |
</tr>`;
|
| 597 |
}
|
| 598 |
document.getElementById('tbl').innerHTML=r||'<tr><td colspan="7" style="text-align:center;padding:20px;color:var(--text-muted)">暂无账号</td></tr>';
|
|
@@ -712,6 +739,43 @@ async function setLevel(lvl){
|
|
| 712 |
}catch(e){toast(e.message,0)}
|
| 713 |
}
|
| 714 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 715 |
async function loadScreenshots(){
|
| 716 |
try{
|
| 717 |
const d=await api('/admin/screenshots',{headers:{'admin-key':getAdminKey()}});
|
|
|
|
| 238 |
|
| 239 |
@media(max-width:639px){.hide-xs{display:none!important}}
|
| 240 |
@media(max-width:1023px){.hide-md{display:none!important}}
|
| 241 |
+
|
| 242 |
+
/* detail modal */
|
| 243 |
+
.detail-overlay{position:fixed;inset:0;z-index:250;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .25s;padding:20px}
|
| 244 |
+
.detail-overlay.open{opacity:1;pointer-events:auto}
|
| 245 |
+
.detail-box{background:var(--surface-solid);border:1px solid var(--border);border-radius:var(--radius);max-width:640px;width:100%;max-height:85vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.5)}
|
| 246 |
+
.detail-header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--surface-solid);z-index:1}
|
| 247 |
+
.detail-header h2{font-size:15px;font-weight:600}
|
| 248 |
+
.detail-body{padding:16px 18px}
|
| 249 |
+
.detail-body .kv{display:grid;grid-template-columns:120px 1fr;gap:6px 12px;font-size:12px;margin-bottom:10px}
|
| 250 |
+
.detail-body .kv .k{color:var(--text-dim);font-weight:500}
|
| 251 |
+
.detail-body .kv .v{color:var(--text);word-break:break-all}
|
| 252 |
+
.detail-body .kv .v.err{color:var(--red)}
|
| 253 |
+
.detail-body h3{font-size:13px;font-weight:600;margin:16px 0 8px;color:var(--text)}
|
| 254 |
+
.detail-body .ss-thumbs{display:flex;flex-wrap:wrap;gap:6px}
|
| 255 |
+
.detail-body .ss-thumbs a{display:inline-block;padding:4px 8px;background:rgba(0,0,0,.2);border:1px solid var(--border);border-radius:6px;color:var(--text);text-decoration:none;font-size:10px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 256 |
+
.detail-body .ss-thumbs a:hover{border-color:var(--accent)}
|
| 257 |
</style>
|
| 258 |
</head>
|
| 259 |
<body>
|
|
|
|
| 413 |
</div>
|
| 414 |
</div>
|
| 415 |
|
| 416 |
+
<!-- Account Detail Modal -->
|
| 417 |
+
<div class="detail-overlay" id="detailOverlay" onclick="closeDetail(event)">
|
| 418 |
+
<div class="detail-box" onclick="event.stopPropagation()">
|
| 419 |
+
<div class="detail-header">
|
| 420 |
+
<h2 id="detailTitle">账号详情</h2>
|
| 421 |
+
<button class="btn btn-sm" onclick="closeDetail()">✕</button>
|
| 422 |
+
</div>
|
| 423 |
+
<div class="detail-body" id="detailBody">加载中…</div>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
|
| 427 |
</main>
|
| 428 |
</div>
|
| 429 |
|
|
|
|
| 619 |
<td><span class="badge ${a.in_use?'badge-on':'badge-idle'}">${a.in_use?'使用中':'空闲'}</span></td>
|
| 620 |
<td class="hide-xs">${a.is_muted?`<span class="badge badge-warn" title="${a.muted_until||''}">禁言</span>`:'<span class="badge badge-idle">正常</span>'}</td>
|
| 621 |
<td class="hide-xs">${a.error_count>0?`<span class="badge badge-off" title="${(a.last_error||'').replace(/"/g,'"').replace(/'/g,"'")}">${a.error_count}</span>`:'—'}</td>
|
| 622 |
+
<td><button class="btn btn-sm" onclick="doLoginAccount('${a.email}')">${a.logged_in?'重连':'登录'}</button> <button class="btn btn-sm" onclick="showDetail('${a.email}')" title="详情">👁️</button></td>
|
| 623 |
</tr>`;
|
| 624 |
}
|
| 625 |
document.getElementById('tbl').innerHTML=r||'<tr><td colspan="7" style="text-align:center;padding:20px;color:var(--text-muted)">暂无账号</td></tr>';
|
|
|
|
| 739 |
}catch(e){toast(e.message,0)}
|
| 740 |
}
|
| 741 |
|
| 742 |
+
async function showDetail(email){
|
| 743 |
+
const overlay=document.getElementById('detailOverlay');
|
| 744 |
+
const body=document.getElementById('detailBody');
|
| 745 |
+
const title=document.getElementById('detailTitle');
|
| 746 |
+
overlay.classList.add('open');
|
| 747 |
+
title.textContent='加载中…';
|
| 748 |
+
body.innerHTML='<span style="color:var(--text-dim)">加载中…</span>';
|
| 749 |
+
try{
|
| 750 |
+
const d=await api('/admin/accounts/'+encodeURIComponent(email),{headers:{'admin-key':getAdminKey()}});
|
| 751 |
+
title.textContent=d.email;
|
| 752 |
+
const mutedTag=d.is_muted?`<span class="badge badge-warn">禁言至 ${d.muted_until||'?'}</span>`:'<span class="badge badge-idle">正常</span>';
|
| 753 |
+
body.innerHTML=`
|
| 754 |
+
<div class="kv">
|
| 755 |
+
<span class="k">邮箱</span><span class="v">${d.email}</span>
|
| 756 |
+
<span class="k">备注</span><span class="v">${d.name||'—'}</span>
|
| 757 |
+
<span class="k">代理</span><span class="v">${d.proxy||'—'}</span>
|
| 758 |
+
<span class="k">状态</span><span class="v"><span class="badge ${d.logged_in?'badge-on':'badge-off'}">${d.logged_in?'在线':'离线'}</span> <span class="badge ${d.in_use?'badge-on':'badge-idle'}">${d.in_use?'使用中':'空闲'}</span></span>
|
| 759 |
+
<span class="k">禁言</span><span class="v">${mutedTag}</span>
|
| 760 |
+
<span class="k">错误次数</span><span class="v">${d.error_count}</span>
|
| 761 |
+
<span class="k">最后错误</span><span class="v err">${(d.last_error||'—').replace(/</g,'<')}</span>
|
| 762 |
+
<span class="k">最后使用</span><span class="v">${d.last_used?new Date(d.last_used*1000).toLocaleString():'—'}</span>
|
| 763 |
+
</div>
|
| 764 |
+
<h3>📸 相关截图 (${d.screenshots.length})</h3>
|
| 765 |
+
<div class="ss-thumbs">${d.screenshots.length?d.screenshots.map(s=>`<a href="${s.url}" target="_blank" title="${s.error||s.name}">🖼️ ${s.name.substring(0,30)} · ${s.time}</a>`).join(''):'<span style="color:var(--text-muted);font-size:11px">暂无</span>'}</div>`;
|
| 766 |
+
}catch(e){
|
| 767 |
+
body.innerHTML=`<span style="color:var(--red)">加载失败: ${e.message}</span>`;
|
| 768 |
+
}
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
function closeDetail(e){
|
| 772 |
+
if(e&&e.target!==document.getElementById('detailOverlay'))return;
|
| 773 |
+
document.getElementById('detailOverlay').classList.remove('open');
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
// ESC to close
|
| 777 |
+
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeDetail()});
|
| 778 |
+
|
| 779 |
async function loadScreenshots(){
|
| 780 |
try{
|
| 781 |
const d=await api('/admin/screenshots',{headers:{'admin-key':getAdminKey()}});
|