Speed up live lookup and keep optimize actions visible
Browse files- app.py +33 -10
- static/index.html +45 -16
- tests/test_market_optimizer_logic.py +29 -2
app.py
CHANGED
|
@@ -8617,7 +8617,7 @@ def _sm_cached_leaderboard_row_for_username(username: str):
|
|
| 8617 |
return dict(row)
|
| 8618 |
return None
|
| 8619 |
|
| 8620 |
-
def _sm_live_lookup_bulk(usernames: list[str]):
|
| 8621 |
unique = []
|
| 8622 |
original_by_key = {}
|
| 8623 |
for username in usernames:
|
|
@@ -8724,12 +8724,13 @@ def _sm_live_lookup_bulk(usernames: list[str]):
|
|
| 8724 |
by_key[key] = output_result
|
| 8725 |
if result.get("ok"):
|
| 8726 |
_SM_LOOKUP_CACHE[key] = {"stamp": time.monotonic(), "result": dict(result)}
|
| 8727 |
-
|
| 8728 |
-
|
| 8729 |
-
|
| 8730 |
-
|
| 8731 |
-
|
| 8732 |
-
|
|
|
|
| 8733 |
try:
|
| 8734 |
_sm_leaderboard_upsert_lookup_results(by_key.values(), source="lookup")
|
| 8735 |
except Exception as exc:
|
|
@@ -9156,12 +9157,30 @@ def _discord_slot_failover(exc, slot: int):
|
|
| 9156 |
|
| 9157 |
@app.get("/live-lookup")
|
| 9158 |
@app.get("/discord-lookup")
|
| 9159 |
-
def discord_lookup(username: str):
|
| 9160 |
-
result = _sm_live_lookup_bulk([username]).get(username)
|
| 9161 |
if not result or not result.get("ok"):
|
| 9162 |
raise HTTPException(502, (result or {}).get("error") or f"No live stats found for '{username}'.")
|
| 9163 |
return result
|
| 9164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9165 |
# ─────────────────────────────────────────────────────────────────
|
| 9166 |
# /get-tokens — gibt nur Status ans Frontend, keine Secrets
|
| 9167 |
# ─────────────────────────────────────────────────────────────────
|
|
@@ -9178,6 +9197,7 @@ def get_tokens():
|
|
| 9178 |
|
| 9179 |
class BulkLookupRequest(BaseModel):
|
| 9180 |
usernames: list[str]
|
|
|
|
| 9181 |
|
| 9182 |
class ClanLookupRequest(BaseModel):
|
| 9183 |
clan_name: str
|
|
@@ -9214,7 +9234,10 @@ def discord_lookup_bulk(data: BulkLookupRequest):
|
|
| 9214 |
usernames = [str(u).strip() for u in (data.usernames or []) if str(u).strip()]
|
| 9215 |
if not usernames:
|
| 9216 |
return {"results": {}}
|
| 9217 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 9218 |
|
| 9219 |
@app.get("/leaderboard")
|
| 9220 |
def leaderboard(
|
|
|
|
| 8617 |
return dict(row)
|
| 8618 |
return None
|
| 8619 |
|
| 8620 |
+
def _sm_live_lookup_bulk(usernames: list[str], *, include_ranks: bool = True):
|
| 8621 |
unique = []
|
| 8622 |
original_by_key = {}
|
| 8623 |
for username in usernames:
|
|
|
|
| 8724 |
by_key[key] = output_result
|
| 8725 |
if result.get("ok"):
|
| 8726 |
_SM_LOOKUP_CACHE[key] = {"stamp": time.monotonic(), "result": dict(result)}
|
| 8727 |
+
if include_ranks:
|
| 8728 |
+
rank_map = {
|
| 8729 |
+
_sm_live_username_key(username): by_key.get(_sm_live_username_key(username))
|
| 8730 |
+
for username in unique
|
| 8731 |
+
if by_key.get(_sm_live_username_key(username)) is not None
|
| 8732 |
+
}
|
| 8733 |
+
_sm_attach_level_ranks_unlocked(rank_map, unique)
|
| 8734 |
try:
|
| 8735 |
_sm_leaderboard_upsert_lookup_results(by_key.values(), source="lookup")
|
| 8736 |
except Exception as exc:
|
|
|
|
| 9157 |
|
| 9158 |
@app.get("/live-lookup")
|
| 9159 |
@app.get("/discord-lookup")
|
| 9160 |
+
def discord_lookup(username: str, include_rank: bool = True):
|
| 9161 |
+
result = _sm_live_lookup_bulk([username], include_ranks=include_rank).get(username)
|
| 9162 |
if not result or not result.get("ok"):
|
| 9163 |
raise HTTPException(502, (result or {}).get("error") or f"No live stats found for '{username}'.")
|
| 9164 |
return result
|
| 9165 |
|
| 9166 |
+
|
| 9167 |
+
@app.get("/live-rank")
|
| 9168 |
+
def live_rank(username: str):
|
| 9169 |
+
clean = str(username or "").strip()
|
| 9170 |
+
if not clean:
|
| 9171 |
+
raise HTTPException(400, "Username is required.")
|
| 9172 |
+
result = _sm_live_lookup_bulk([clean], include_ranks=False).get(clean)
|
| 9173 |
+
if not result or not result.get("ok"):
|
| 9174 |
+
raise HTTPException(502, (result or {}).get("error") or f"No live stats found for '{clean}'.")
|
| 9175 |
+
|
| 9176 |
+
rank_map = {_sm_live_username_key(clean): result}
|
| 9177 |
+
_sm_attach_level_ranks_unlocked(rank_map, [clean])
|
| 9178 |
+
try:
|
| 9179 |
+
_sm_leaderboard_upsert_lookup_results([result], source="lookup")
|
| 9180 |
+
except Exception as exc:
|
| 9181 |
+
print(f"[SwordMasters leaderboard rank upsert failed] {type(exc).__name__}: {str(exc)[:240]}")
|
| 9182 |
+
return result
|
| 9183 |
+
|
| 9184 |
# ─────────────────────────────────────────────────────────────────
|
| 9185 |
# /get-tokens — gibt nur Status ans Frontend, keine Secrets
|
| 9186 |
# ─────────────────────────────────────────────────────────────────
|
|
|
|
| 9197 |
|
| 9198 |
class BulkLookupRequest(BaseModel):
|
| 9199 |
usernames: list[str]
|
| 9200 |
+
include_rank: bool = True
|
| 9201 |
|
| 9202 |
class ClanLookupRequest(BaseModel):
|
| 9203 |
clan_name: str
|
|
|
|
| 9234 |
usernames = [str(u).strip() for u in (data.usernames or []) if str(u).strip()]
|
| 9235 |
if not usernames:
|
| 9236 |
return {"results": {}}
|
| 9237 |
+
return {
|
| 9238 |
+
"results": _sm_live_lookup_bulk(usernames, include_ranks=data.include_rank),
|
| 9239 |
+
"token_warnings": [],
|
| 9240 |
+
}
|
| 9241 |
|
| 9242 |
@app.get("/leaderboard")
|
| 9243 |
def leaderboard(
|
static/index.html
CHANGED
|
@@ -184,7 +184,7 @@ input::placeholder{color:var(--text3);}
|
|
| 184 |
.market-panel.market-collapsed .market-body{display:none;}
|
| 185 |
.market-panel.market-collapsed{max-width:none;margin-left:0;}
|
| 186 |
.market-panel.market-collapsed .collapse-btn{transform:rotate(-90deg);}
|
| 187 |
-
.opt-action-bar{display:grid;grid-template-columns:minmax(0,1.7fr) auto minmax(130px,.8fr);gap:8px;margin-bottom:12px;align-items:stretch;}
|
| 188 |
.opt-action-bar .btn{justify-content:center;border-radius:7px;min-height:44px;}
|
| 189 |
.opt-action-bar .mini-toggle{justify-content:center;min-height:44px;}
|
| 190 |
.opt-plan-shell{display:flex;flex-direction:column;gap:12px;}
|
|
@@ -449,6 +449,7 @@ input::placeholder{color:var(--text3);}
|
|
| 449 |
.trade-cta{width:100%;justify-content:center;margin-top:12px;padding:10px;font-size:12px;border-radius:5px;}
|
| 450 |
@media(max-width:1180px){.opt-plan-head,.opt-plan-actions{grid-template-columns:1fr}.opt-summary-grid{grid-template-columns:repeat(2,minmax(135px,1fr));}.opt-room-head{grid-template-columns:1fr}.opt-room-stats{grid-template-columns:repeat(3,minmax(70px,1fr));}.opt-market-grid{grid-template-columns:repeat(2,minmax(100px,1fr));}}
|
| 451 |
@media(max-width:650px){.account-add-row,.account-base-grid,.opt-action-bar,.opt-rec-actions,.trade-lab,.trade-actions,.opt-summary-grid,.opt-room-stats,.opt-account-grid,.opt-market-grid,.opt-market-columns{grid-template-columns:1fr}.opt-plan-head,.opt-plan-actions{grid-template-columns:1fr}.opt-gear-row{grid-template-columns:1fr}.opt-pets{grid-template-columns:1fr}.opt-account-stats{grid-template-columns:repeat(2,minmax(0,1fr));}.opt-market-card .market-head{grid-template-columns:1fr auto}.opt-market-brief{grid-column:1 / -1;justify-content:flex-start}.opt-market-meta{grid-column:1 / -1;text-align:left}.opt-period-control{grid-template-columns:auto 1fr 1fr;}.opt-market-row{grid-template-columns:42px minmax(0,1fr)}.opt-market-side{grid-column:2 / -1;align-items:flex-start}.opt-market-price{text-align:left}.opt-section-head{align-items:flex-start;flex-direction:column}.opt-section-head>span{text-align:left}}
|
|
|
|
| 452 |
.account-lookup-panel{overflow:visible;}
|
| 453 |
.ai-avatar-card{width:94px;height:94px;margin:7px auto 3px;padding:3px;overflow:visible;border:1px solid rgba(0,200,255,.24);background:#657b91;border-radius:3px;display:flex;align-items:center;justify-content:center;box-shadow:inset 0 0 14px rgba(0,0,0,.18);}
|
| 454 |
.ai-avatar-card canvas{width:86px!important;height:86px!important;display:block;image-rendering:auto;filter:drop-shadow(0 4px 7px rgba(0,0,0,.42));}
|
|
@@ -996,6 +997,15 @@ input::placeholder{color:var(--text3);}
|
|
| 996 |
</div>
|
| 997 |
</div>
|
| 998 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 999 |
<!-- ACCOUNTS -->
|
| 1000 |
<div class="panel opt-accounts-panel" id="panel-accounts">
|
| 1001 |
<div class="ph"><h2>Accounts</h2>
|
|
@@ -1027,15 +1037,6 @@ input::placeholder{color:var(--text3);}
|
|
| 1027 |
</div>
|
| 1028 |
</div>
|
| 1029 |
|
| 1030 |
-
<div class="opt-action-bar">
|
| 1031 |
-
<button class="btn btn-p" id="run-btn" style="font-size:14px;padding:11px" onclick="runOpt()">OPTIMIZE</button>
|
| 1032 |
-
<label class="mini-toggle" title="Disable market buy/sell search and only optimize current setup power and rooms">
|
| 1033 |
-
<input type="checkbox" id="use-market-optimum" checked>
|
| 1034 |
-
MARKET
|
| 1035 |
-
</label>
|
| 1036 |
-
<button class="btn btn-s" id="check-btn" style="font-size:13px;padding:11px" onclick="runCheck()">CHECK SETUP</button>
|
| 1037 |
-
</div>
|
| 1038 |
-
|
| 1039 |
</div><!-- end left -->
|
| 1040 |
|
| 1041 |
<div><!-- RIGHT COLUMN -->
|
|
@@ -3263,7 +3264,7 @@ async function upgradeLoadAccount(){
|
|
| 3263 |
if(!name){if(status){status.style.color='var(--red)';status.textContent='Enter an account name first.';}return;}
|
| 3264 |
if(status){status.style.color='var(--accent)';status.textContent=`Loading ${name} inventory...`;}
|
| 3265 |
try{
|
| 3266 |
-
const resp=await fetch(`${base()}/live-lookup-bulk`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({usernames:[name]})});
|
| 3267 |
if(!resp.ok){const e=await resp.json().catch(()=>{});throw new Error(e?.detail||`HTTP ${resp.status}`);}
|
| 3268 |
const bulk=await resp.json();
|
| 3269 |
const d=bulk.results?.[name]||Object.values(bulk.results||{})[0];
|
|
@@ -4078,7 +4079,7 @@ async function discordLookup(id){
|
|
| 4078 |
if(!username){if(statusEl){statusEl.style.color='var(--red)';statusEl.textContent='⚠ Enter name first';setTimeout(()=>statusEl.textContent='',3000);}return;}
|
| 4079 |
if(statusEl){statusEl.style.color='var(--accent)';statusEl.textContent='⟳ Loading...';}
|
| 4080 |
try{
|
| 4081 |
-
const resp=await fetch(`${base()}/live-lookup?username=${encodeURIComponent(username)}`);
|
| 4082 |
if(!resp.ok){const e=await resp.json().catch(()=>{});throw new Error(e?.detail||`HTTP ${resp.status}`);}
|
| 4083 |
const data=await resp.json();
|
| 4084 |
const applied=applyDiscordAccountData(card,data);
|
|
@@ -4108,7 +4109,7 @@ async function refreshAllAccounts(){
|
|
| 4108 |
const entries=toRefresh.map(card=>({id:parseInt(card.dataset.id),name:card.querySelector('.iname').value.trim()}));
|
| 4109 |
entries.forEach(entry=>setAccountStatus(entry.id,'loading','updating'));
|
| 4110 |
try{
|
| 4111 |
-
const resp=await fetch(`${base()}/live-lookup-bulk`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({usernames:entries.map(e=>e.name)})});
|
| 4112 |
if(!resp.ok){const e=await resp.json().catch(()=>{});throw new Error(e?.detail||`HTTP ${resp.status}`);}
|
| 4113 |
const data=await resp.json();
|
| 4114 |
let done=0,okCount=0,failCount=0,changedCount=0;
|
|
@@ -5822,6 +5823,7 @@ function _dynamicResultView(data,roomKey,tab='opt'){
|
|
| 5822 |
|
| 5823 |
// Stored live data for the details panel
|
| 5824 |
let _aiDiscordData = null;
|
|
|
|
| 5825 |
|
| 5826 |
function _detailChip(label,active=false){
|
| 5827 |
return `<span class="detail-chip${active?' active':''}">${_escHtml(label)}</span>`;
|
|
@@ -6965,7 +6967,7 @@ function updateAiProfileSummary({dmgHit=null,dps=null}={}){
|
|
| 6965 |
const level=document.getElementById('ai-level')?.value||_aiDiscordData?.level||'';
|
| 6966 |
set('ai-profile-level',level?`Lv ${Number(level).toLocaleString('en-US')}`:'-');
|
| 6967 |
const rank=_levelRankDetail(_aiDiscordData||{});
|
| 6968 |
-
set('ai-profile-rank',rank||'rank
|
| 6969 |
set('ai-profile-dmg',Number.isFinite(dmgHit)?fmtAI(dmgHit):'-');
|
| 6970 |
set('ai-profile-dps',Number.isFinite(dps)?`${fmtAI(dps)}/s`:'DPS after setup');
|
| 6971 |
tip('ai-profile-power-card','Power',powStr?powVal:NaN,powStr);
|
|
@@ -7219,13 +7221,39 @@ function aiLookupNameChanged(){
|
|
| 7219 |
}
|
| 7220 |
}
|
| 7221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7222 |
async function aiLive(options={}){
|
| 7223 |
const name=document.getElementById('ai-name').value.trim();if(!name)return;
|
| 7224 |
const st=document.getElementById('ai-status');
|
| 7225 |
const expectedPlayerId=String(options?.expectedPlayerId||'').trim();
|
|
|
|
| 7226 |
st.style.color='var(--accent)';st.textContent='⟳ Loading...';
|
| 7227 |
try{
|
| 7228 |
-
const resp=await fetch(`${base()}/live-lookup-bulk`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({usernames:[name]})});
|
| 7229 |
if(!resp.ok){const e=await resp.json().catch(()=>{});throw new Error(e?.detail||`HTTP ${resp.status}`);}
|
| 7230 |
const bulk=await resp.json();
|
| 7231 |
const d=bulk.results[name];
|
|
@@ -7264,7 +7292,7 @@ async function aiLive(options={}){
|
|
| 7264 |
if(d[key]!==undefined)document.getElementById(id).value=d[key];
|
| 7265 |
});
|
| 7266 |
// Store full live data for details panel (hp, gold, kills, etc.)
|
| 7267 |
-
_aiDiscordData={username:loadedName||name,player_id:d.player_id,level_rank_page:d.level_rank_page,level_rank_page_position:d.level_rank_page_position,gold:d.gold,gold_raw:d.gold_raw,hp:d.hp,hp_raw:d.hp_raw,level_rank:d.level_rank,level_rank_total_players:d.level_rank_total_players,level_rank_top_percent:d.level_rank_top_percent,level_rank_percentile_min_level:d.level_rank_percentile_min_level,pvp_kills:d.pvp_kills,pvp_loot:d.pvp_loot,register_date:d.register_date,level:d.level,experience:d.experience,experience_raw:d.experience_raw,total_experience:d.total_experience,total_power:d.total_power,enemies:d.killed_enemies||{},inventory:d.inventory||null,appearance:d.appearance||null};
|
| 7268 |
_renderSavedPlayers();
|
| 7269 |
if(expectedPlayerId&&_upsertSavedPlayerFromLive(_aiDiscordData,expectedPlayerId)){
|
| 7270 |
_savedPlayerStatus(`Verified saved player ${_aiDiscordData.username||name}.`,'var(--green)');
|
|
@@ -7274,6 +7302,7 @@ async function aiLive(options={}){
|
|
| 7274 |
st.style.color='var(--green)';
|
| 7275 |
st.textContent=d.lookup_alias&&loadedName!==name?`✓ Loaded as ${loadedName}`:'✓ Loaded';
|
| 7276 |
setTimeout(()=>st.textContent='',4000);
|
|
|
|
| 7277 |
}catch(e){st.style.color='var(--red)';st.textContent=`✗ ${e.message}`;setTimeout(()=>st.textContent='',6000);}
|
| 7278 |
}
|
| 7279 |
|
|
|
|
| 184 |
.market-panel.market-collapsed .market-body{display:none;}
|
| 185 |
.market-panel.market-collapsed{max-width:none;margin-left:0;}
|
| 186 |
.market-panel.market-collapsed .collapse-btn{transform:rotate(-90deg);}
|
| 187 |
+
.opt-action-bar{position:sticky;top:8px;z-index:30;display:grid;grid-template-columns:minmax(0,1.7fr) auto minmax(130px,.8fr);gap:8px;margin-bottom:12px;padding:8px;border:1px solid rgba(0,200,255,.28);border-radius:9px;background:rgba(8,11,16,.94);box-shadow:0 10px 32px rgba(0,0,0,.52),0 0 20px rgba(0,200,255,.08);backdrop-filter:blur(10px);align-items:stretch;}
|
| 188 |
.opt-action-bar .btn{justify-content:center;border-radius:7px;min-height:44px;}
|
| 189 |
.opt-action-bar .mini-toggle{justify-content:center;min-height:44px;}
|
| 190 |
.opt-plan-shell{display:flex;flex-direction:column;gap:12px;}
|
|
|
|
| 449 |
.trade-cta{width:100%;justify-content:center;margin-top:12px;padding:10px;font-size:12px;border-radius:5px;}
|
| 450 |
@media(max-width:1180px){.opt-plan-head,.opt-plan-actions{grid-template-columns:1fr}.opt-summary-grid{grid-template-columns:repeat(2,minmax(135px,1fr));}.opt-room-head{grid-template-columns:1fr}.opt-room-stats{grid-template-columns:repeat(3,minmax(70px,1fr));}.opt-market-grid{grid-template-columns:repeat(2,minmax(100px,1fr));}}
|
| 451 |
@media(max-width:650px){.account-add-row,.account-base-grid,.opt-action-bar,.opt-rec-actions,.trade-lab,.trade-actions,.opt-summary-grid,.opt-room-stats,.opt-account-grid,.opt-market-grid,.opt-market-columns{grid-template-columns:1fr}.opt-plan-head,.opt-plan-actions{grid-template-columns:1fr}.opt-gear-row{grid-template-columns:1fr}.opt-pets{grid-template-columns:1fr}.opt-account-stats{grid-template-columns:repeat(2,minmax(0,1fr));}.opt-market-card .market-head{grid-template-columns:1fr auto}.opt-market-brief{grid-column:1 / -1;justify-content:flex-start}.opt-market-meta{grid-column:1 / -1;text-align:left}.opt-period-control{grid-template-columns:auto 1fr 1fr;}.opt-market-row{grid-template-columns:42px minmax(0,1fr)}.opt-market-side{grid-column:2 / -1;align-items:flex-start}.opt-market-price{text-align:left}.opt-section-head{align-items:flex-start;flex-direction:column}.opt-section-head>span{text-align:left}}
|
| 452 |
+
@media(max-width:650px){.opt-action-bar{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.opt-action-bar #run-btn{grid-column:1 / -1}}
|
| 453 |
.account-lookup-panel{overflow:visible;}
|
| 454 |
.ai-avatar-card{width:94px;height:94px;margin:7px auto 3px;padding:3px;overflow:visible;border:1px solid rgba(0,200,255,.24);background:#657b91;border-radius:3px;display:flex;align-items:center;justify-content:center;box-shadow:inset 0 0 14px rgba(0,0,0,.18);}
|
| 455 |
.ai-avatar-card canvas{width:86px!important;height:86px!important;display:block;image-rendering:auto;filter:drop-shadow(0 4px 7px rgba(0,0,0,.42));}
|
|
|
|
| 997 |
</div>
|
| 998 |
</div>
|
| 999 |
|
| 1000 |
+
<div class="opt-action-bar">
|
| 1001 |
+
<button class="btn btn-p" id="run-btn" style="font-size:14px;padding:11px" onclick="runOpt()">OPTIMIZE</button>
|
| 1002 |
+
<label class="mini-toggle" title="Disable market buy/sell search and only optimize current setup power and rooms">
|
| 1003 |
+
<input type="checkbox" id="use-market-optimum" checked>
|
| 1004 |
+
MARKET
|
| 1005 |
+
</label>
|
| 1006 |
+
<button class="btn btn-s" id="check-btn" style="font-size:13px;padding:11px" onclick="runCheck()">CHECK SETUP</button>
|
| 1007 |
+
</div>
|
| 1008 |
+
|
| 1009 |
<!-- ACCOUNTS -->
|
| 1010 |
<div class="panel opt-accounts-panel" id="panel-accounts">
|
| 1011 |
<div class="ph"><h2>Accounts</h2>
|
|
|
|
| 1037 |
</div>
|
| 1038 |
</div>
|
| 1039 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1040 |
</div><!-- end left -->
|
| 1041 |
|
| 1042 |
<div><!-- RIGHT COLUMN -->
|
|
|
|
| 3264 |
if(!name){if(status){status.style.color='var(--red)';status.textContent='Enter an account name first.';}return;}
|
| 3265 |
if(status){status.style.color='var(--accent)';status.textContent=`Loading ${name} inventory...`;}
|
| 3266 |
try{
|
| 3267 |
+
const resp=await fetch(`${base()}/live-lookup-bulk`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({usernames:[name],include_rank:false})});
|
| 3268 |
if(!resp.ok){const e=await resp.json().catch(()=>{});throw new Error(e?.detail||`HTTP ${resp.status}`);}
|
| 3269 |
const bulk=await resp.json();
|
| 3270 |
const d=bulk.results?.[name]||Object.values(bulk.results||{})[0];
|
|
|
|
| 4079 |
if(!username){if(statusEl){statusEl.style.color='var(--red)';statusEl.textContent='⚠ Enter name first';setTimeout(()=>statusEl.textContent='',3000);}return;}
|
| 4080 |
if(statusEl){statusEl.style.color='var(--accent)';statusEl.textContent='⟳ Loading...';}
|
| 4081 |
try{
|
| 4082 |
+
const resp=await fetch(`${base()}/live-lookup?username=${encodeURIComponent(username)}&include_rank=false`);
|
| 4083 |
if(!resp.ok){const e=await resp.json().catch(()=>{});throw new Error(e?.detail||`HTTP ${resp.status}`);}
|
| 4084 |
const data=await resp.json();
|
| 4085 |
const applied=applyDiscordAccountData(card,data);
|
|
|
|
| 4109 |
const entries=toRefresh.map(card=>({id:parseInt(card.dataset.id),name:card.querySelector('.iname').value.trim()}));
|
| 4110 |
entries.forEach(entry=>setAccountStatus(entry.id,'loading','updating'));
|
| 4111 |
try{
|
| 4112 |
+
const resp=await fetch(`${base()}/live-lookup-bulk`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({usernames:entries.map(e=>e.name),include_rank:false})});
|
| 4113 |
if(!resp.ok){const e=await resp.json().catch(()=>{});throw new Error(e?.detail||`HTTP ${resp.status}`);}
|
| 4114 |
const data=await resp.json();
|
| 4115 |
let done=0,okCount=0,failCount=0,changedCount=0;
|
|
|
|
| 5823 |
|
| 5824 |
// Stored live data for the details panel
|
| 5825 |
let _aiDiscordData = null;
|
| 5826 |
+
let _aiRankLookupSeq = 0;
|
| 5827 |
|
| 5828 |
function _detailChip(label,active=false){
|
| 5829 |
return `<span class="detail-chip${active?' active':''}">${_escHtml(label)}</span>`;
|
|
|
|
| 6967 |
const level=document.getElementById('ai-level')?.value||_aiDiscordData?.level||'';
|
| 6968 |
set('ai-profile-level',level?`Lv ${Number(level).toLocaleString('en-US')}`:'-');
|
| 6969 |
const rank=_levelRankDetail(_aiDiscordData||{});
|
| 6970 |
+
set('ai-profile-rank',rank||(_aiDiscordData?.rank_loading?'rank loading...':'rank unavailable'),true);
|
| 6971 |
set('ai-profile-dmg',Number.isFinite(dmgHit)?fmtAI(dmgHit):'-');
|
| 6972 |
set('ai-profile-dps',Number.isFinite(dps)?`${fmtAI(dps)}/s`:'DPS after setup');
|
| 6973 |
tip('ai-profile-power-card','Power',powStr?powVal:NaN,powStr);
|
|
|
|
| 7221 |
}
|
| 7222 |
}
|
| 7223 |
|
| 7224 |
+
async function _loadAiRank(username,lookupSeq){
|
| 7225 |
+
try{
|
| 7226 |
+
const resp=await fetch(`${base()}/live-rank?username=${encodeURIComponent(username)}`);
|
| 7227 |
+
const rankData=await resp.json().catch(()=>({}));
|
| 7228 |
+
if(!resp.ok)throw new Error(rankData?.detail||`HTTP ${resp.status}`);
|
| 7229 |
+
if(lookupSeq!==_aiRankLookupSeq||!_aiDiscordData)return;
|
| 7230 |
+
const currentName=String(_aiDiscordData.username||'').trim().toLowerCase();
|
| 7231 |
+
const returnedName=String(rankData.username||username).trim().toLowerCase();
|
| 7232 |
+
if(currentName&&returnedName&¤tName!==returnedName)return;
|
| 7233 |
+
[
|
| 7234 |
+
'player_id','level_rank','level_rank_page','level_rank_page_position',
|
| 7235 |
+
'level_rank_total_players','level_rank_top_percent','level_rank_percentile_min_level'
|
| 7236 |
+
].forEach(key=>{
|
| 7237 |
+
if(rankData[key]!==undefined&&rankData[key]!==null)_aiDiscordData[key]=rankData[key];
|
| 7238 |
+
});
|
| 7239 |
+
_aiDiscordData.rank_loading=false;
|
| 7240 |
+
_renderSavedPlayers();
|
| 7241 |
+
aiCalc();
|
| 7242 |
+
}catch(e){
|
| 7243 |
+
if(lookupSeq!==_aiRankLookupSeq||!_aiDiscordData)return;
|
| 7244 |
+
_aiDiscordData.rank_loading=false;
|
| 7245 |
+
aiCalc();
|
| 7246 |
+
}
|
| 7247 |
+
}
|
| 7248 |
+
|
| 7249 |
async function aiLive(options={}){
|
| 7250 |
const name=document.getElementById('ai-name').value.trim();if(!name)return;
|
| 7251 |
const st=document.getElementById('ai-status');
|
| 7252 |
const expectedPlayerId=String(options?.expectedPlayerId||'').trim();
|
| 7253 |
+
const lookupSeq=++_aiRankLookupSeq;
|
| 7254 |
st.style.color='var(--accent)';st.textContent='⟳ Loading...';
|
| 7255 |
try{
|
| 7256 |
+
const resp=await fetch(`${base()}/live-lookup-bulk`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({usernames:[name],include_rank:!!expectedPlayerId})});
|
| 7257 |
if(!resp.ok){const e=await resp.json().catch(()=>{});throw new Error(e?.detail||`HTTP ${resp.status}`);}
|
| 7258 |
const bulk=await resp.json();
|
| 7259 |
const d=bulk.results[name];
|
|
|
|
| 7292 |
if(d[key]!==undefined)document.getElementById(id).value=d[key];
|
| 7293 |
});
|
| 7294 |
// Store full live data for details panel (hp, gold, kills, etc.)
|
| 7295 |
+
_aiDiscordData={username:loadedName||name,player_id:d.player_id,level_rank_page:d.level_rank_page,level_rank_page_position:d.level_rank_page_position,gold:d.gold,gold_raw:d.gold_raw,hp:d.hp,hp_raw:d.hp_raw,level_rank:d.level_rank,level_rank_total_players:d.level_rank_total_players,level_rank_top_percent:d.level_rank_top_percent,level_rank_percentile_min_level:d.level_rank_percentile_min_level,pvp_kills:d.pvp_kills,pvp_loot:d.pvp_loot,register_date:d.register_date,level:d.level,experience:d.experience,experience_raw:d.experience_raw,total_experience:d.total_experience,total_power:d.total_power,enemies:d.killed_enemies||{},inventory:d.inventory||null,appearance:d.appearance||null,rank_loading:!expectedPlayerId};
|
| 7296 |
_renderSavedPlayers();
|
| 7297 |
if(expectedPlayerId&&_upsertSavedPlayerFromLive(_aiDiscordData,expectedPlayerId)){
|
| 7298 |
_savedPlayerStatus(`Verified saved player ${_aiDiscordData.username||name}.`,'var(--green)');
|
|
|
|
| 7302 |
st.style.color='var(--green)';
|
| 7303 |
st.textContent=d.lookup_alias&&loadedName!==name?`✓ Loaded as ${loadedName}`:'✓ Loaded';
|
| 7304 |
setTimeout(()=>st.textContent='',4000);
|
| 7305 |
+
if(!expectedPlayerId)_loadAiRank(loadedName||name,lookupSeq);
|
| 7306 |
}catch(e){st.style.color='var(--red)';st.textContent=`✗ ${e.message}`;setTimeout(()=>st.textContent='',6000);}
|
| 7307 |
}
|
| 7308 |
|
tests/test_market_optimizer_logic.py
CHANGED
|
@@ -1948,6 +1948,15 @@ class MarketOptimizerLogicTests(unittest.TestCase):
|
|
| 1948 |
self.assertIn("panel.classList.remove('panel-collapsed')", block)
|
| 1949 |
self.assertIn("localStorage.setItem('panel_panel-accounts','0')", block)
|
| 1950 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1951 |
def test_live_power_ui_keeps_raw_value_for_calculations_until_manual_edit(self):
|
| 1952 |
html = (Path(__file__).resolve().parents[1] / "static" / "index.html").read_text(encoding="utf-8")
|
| 1953 |
self.assertIn('class="ipowraw"', html)
|
|
@@ -2761,7 +2770,7 @@ class MarketOptimizerLogicTests(unittest.TestCase):
|
|
| 2761 |
original_lookup = self.app._sm_live_lookup_bulk
|
| 2762 |
usernames = [f"Player{i}" for i in range(10)]
|
| 2763 |
try:
|
| 2764 |
-
self.app._sm_live_lookup_bulk = lambda names: {
|
| 2765 |
username: {"username": username, "ok": True, "power": str(index)}
|
| 2766 |
for index, username in enumerate(names)
|
| 2767 |
}
|
|
@@ -2775,6 +2784,24 @@ class MarketOptimizerLogicTests(unittest.TestCase):
|
|
| 2775 |
self.assertTrue(result["results"][username]["ok"])
|
| 2776 |
self.assertEqual(result["results"][username]["power"], str(index))
|
| 2777 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2778 |
def test_live_bulk_accepts_50_accounts_and_rejects_51(self):
|
| 2779 |
original_fetch = self.app._sm_fetch_live_inventories
|
| 2780 |
original_cache = self.app._SM_LOOKUP_CACHE
|
|
@@ -2806,7 +2833,7 @@ class MarketOptimizerLogicTests(unittest.TestCase):
|
|
| 2806 |
def test_live_single_lookup_surfaces_short_public_error(self):
|
| 2807 |
original_lookup = self.app._sm_live_lookup_bulk
|
| 2808 |
try:
|
| 2809 |
-
self.app._sm_live_lookup_bulk = lambda names: {
|
| 2810 |
names[0]: {"username": names[0], "ok": False, "error": "Live stats unavailable."}
|
| 2811 |
}
|
| 2812 |
with self.assertRaises(self.app.HTTPException) as ctx:
|
|
|
|
| 1948 |
self.assertIn("panel.classList.remove('panel-collapsed')", block)
|
| 1949 |
self.assertIn("localStorage.setItem('panel_panel-accounts','0')", block)
|
| 1950 |
|
| 1951 |
+
def test_optimizer_actions_are_visible_before_the_account_list(self):
|
| 1952 |
+
html = (Path(__file__).resolve().parents[1] / "static" / "index.html").read_text(encoding="utf-8")
|
| 1953 |
+
self.assertLess(
|
| 1954 |
+
html.index('<div class="opt-action-bar">'),
|
| 1955 |
+
html.index('id="panel-accounts"'),
|
| 1956 |
+
)
|
| 1957 |
+
self.assertIn(".opt-action-bar{position:sticky;top:8px", html)
|
| 1958 |
+
self.assertEqual(html.count('id="run-btn"'), 1)
|
| 1959 |
+
|
| 1960 |
def test_live_power_ui_keeps_raw_value_for_calculations_until_manual_edit(self):
|
| 1961 |
html = (Path(__file__).resolve().parents[1] / "static" / "index.html").read_text(encoding="utf-8")
|
| 1962 |
self.assertIn('class="ipowraw"', html)
|
|
|
|
| 2770 |
original_lookup = self.app._sm_live_lookup_bulk
|
| 2771 |
usernames = [f"Player{i}" for i in range(10)]
|
| 2772 |
try:
|
| 2773 |
+
self.app._sm_live_lookup_bulk = lambda names, **kwargs: {
|
| 2774 |
username: {"username": username, "ok": True, "power": str(index)}
|
| 2775 |
for index, username in enumerate(names)
|
| 2776 |
}
|
|
|
|
| 2784 |
self.assertTrue(result["results"][username]["ok"])
|
| 2785 |
self.assertEqual(result["results"][username]["power"], str(index))
|
| 2786 |
|
| 2787 |
+
def test_live_bulk_can_skip_slow_rank_enrichment(self):
|
| 2788 |
+
original_lookup = self.app._sm_live_lookup_bulk
|
| 2789 |
+
captured = {}
|
| 2790 |
+
try:
|
| 2791 |
+
def fake_lookup(names, **kwargs):
|
| 2792 |
+
captured.update(kwargs)
|
| 2793 |
+
return {name: {"username": name, "ok": True} for name in names}
|
| 2794 |
+
|
| 2795 |
+
self.app._sm_live_lookup_bulk = fake_lookup
|
| 2796 |
+
result = self.app.discord_lookup_bulk(
|
| 2797 |
+
self.app.BulkLookupRequest(usernames=["Player"], include_rank=False)
|
| 2798 |
+
)
|
| 2799 |
+
finally:
|
| 2800 |
+
self.app._sm_live_lookup_bulk = original_lookup
|
| 2801 |
+
|
| 2802 |
+
self.assertTrue(result["results"]["Player"]["ok"])
|
| 2803 |
+
self.assertFalse(captured["include_ranks"])
|
| 2804 |
+
|
| 2805 |
def test_live_bulk_accepts_50_accounts_and_rejects_51(self):
|
| 2806 |
original_fetch = self.app._sm_fetch_live_inventories
|
| 2807 |
original_cache = self.app._SM_LOOKUP_CACHE
|
|
|
|
| 2833 |
def test_live_single_lookup_surfaces_short_public_error(self):
|
| 2834 |
original_lookup = self.app._sm_live_lookup_bulk
|
| 2835 |
try:
|
| 2836 |
+
self.app._sm_live_lookup_bulk = lambda names, **kwargs: {
|
| 2837 |
names[0]: {"username": names[0], "ok": False, "error": "Live stats unavailable."}
|
| 2838 |
}
|
| 2839 |
with self.assertRaises(self.app.HTTPException) as ctx:
|