oettaM123 commited on
Commit
2e860af
·
1 Parent(s): a9474c7

Speed up live lookup and keep optimize actions visible

Browse files
Files changed (3) hide show
  1. app.py +33 -10
  2. static/index.html +45 -16
  3. 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
- rank_map = {
8728
- _sm_live_username_key(username): by_key.get(_sm_live_username_key(username))
8729
- for username in unique
8730
- if by_key.get(_sm_live_username_key(username)) is not None
8731
- }
8732
- _sm_attach_level_ranks_unlocked(rank_map, unique)
 
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 {"results": _sm_live_lookup_bulk(usernames), "token_warnings": []}
 
 
 
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 after lookup',true);
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&&currentName!==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: