Fix inventory window never refreshing live (per-character version)
The inventoryVersion counter in useLiveData was a single global value that bumped on every inventory_delta for any character. With 60+ active chars all generating deltas, the global counter advanced multiple times per second. InventoryWindow's debounce effect watched this global counter, so every bump reset its 2-second fetch timer. Since bumps arrived faster than 2s, the fetch never fired — the window appeared frozen until the user closed and reopened it (which triggered the initial-fetch effect). Fix: make inventoryVersions a Map<string, number> keyed by character name. Each inventory_delta now only bumps its own character's counter, so an open window's debounce correctly fires 2s after its character's last delta, ignoring unrelated traffic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7dc5996820
commit
d26f1f725c
18 changed files with 176 additions and 9 deletions
|
|
@ -66,7 +66,7 @@ export const MapLayout: React.FC<Props> = ({ data }) => {
|
|||
selectedPlayer={selectedPlayer}
|
||||
/>
|
||||
<WindowRenderer characters={data.characters} chatMessages={data.chatMessages}
|
||||
nearbyObjects={data.nearbyObjects} inventoryVersion={data.inventoryVersion}
|
||||
nearbyObjects={data.nearbyObjects} inventoryVersions={data.inventoryVersions}
|
||||
equipmentCantrips={data.equipmentCantrips} characterStats={data.characterStats}
|
||||
socket={data.socketRef.current} />
|
||||
<RareNotification recentRares={data.recentRares} />
|
||||
|
|
|
|||
|
|
@ -17,13 +17,15 @@ interface Props {
|
|||
characters: Map<string, CharacterState>;
|
||||
chatMessages: Map<string, Array<{ text: string; color?: number; timestamp: string }>>;
|
||||
nearbyObjects: Map<string, any>;
|
||||
inventoryVersion: number;
|
||||
/** Per-character inventory counters. InventoryWindow watches only its
|
||||
* own character's value so unrelated deltas don't reset its debounce. */
|
||||
inventoryVersions: Map<string, number>;
|
||||
equipmentCantrips: Map<string, any>;
|
||||
characterStats: Map<string, any>;
|
||||
socket: WebSocket | null;
|
||||
}
|
||||
|
||||
export const WindowRenderer: React.FC<Props> = React.memo(({ characters, chatMessages, nearbyObjects, inventoryVersion, equipmentCantrips, characterStats, socket }) => {
|
||||
export const WindowRenderer: React.FC<Props> = React.memo(({ characters, chatMessages, nearbyObjects, inventoryVersions, equipmentCantrips, characterStats, socket }) => {
|
||||
const { windows } = useWindowManager();
|
||||
|
||||
return (
|
||||
|
|
@ -44,7 +46,7 @@ export const WindowRenderer: React.FC<Props> = React.memo(({ characters, chatMes
|
|||
liveStats={characterStats.get(charName)} />;
|
||||
case 'inv':
|
||||
return <InventoryWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex}
|
||||
inventoryVersion={inventoryVersion} equipmentCantrips={equipmentCantrips.get(charName)} />;
|
||||
inventoryVersion={inventoryVersions.get(charName) ?? 0} equipmentCantrips={equipmentCantrips.get(charName)} />;
|
||||
case 'radar':
|
||||
return <RadarWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex}
|
||||
socket={socket} radarData={nearbyObjects.get(charName) ?? null} />;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ export interface DashboardState {
|
|||
recentRares: RareMessage[];
|
||||
chatMessages: Map<string, Array<{ text: string; color?: number; timestamp: string }>>;
|
||||
nearbyObjects: Map<string, any>;
|
||||
inventoryVersion: number;
|
||||
/** Per-character inventory version counter — bumps when that character
|
||||
* receives an inventory_delta. Open windows watch only their own
|
||||
* character's counter so deltas for unrelated chars don't reset their
|
||||
* debounce timer. */
|
||||
inventoryVersions: Map<string, number>;
|
||||
equipmentCantrips: Map<string, any>;
|
||||
characterStats: Map<string, any>;
|
||||
deathAlerts: Array<{ character_name: string; vitae: number; timestamp: string }>;
|
||||
|
|
@ -29,7 +33,7 @@ export function useLiveData(): DashboardState {
|
|||
const [recentRares, setRecentRares] = useState<RareMessage[]>([]);
|
||||
const chatMessagesRef = useRef(new Map<string, Array<{ text: string; color?: number; timestamp: string }>>());
|
||||
const [chatVersion, setChatVersion] = useState(0);
|
||||
const [inventoryVersion, setInventoryVersion] = useState(0);
|
||||
const [inventoryVersions, setInventoryVersions] = useState<Map<string, number>>(new Map());
|
||||
const equipmentCantripRef = useRef(new Map<string, any>());
|
||||
const [equipCantripVersion, setEquipCantripVersion] = useState(0);
|
||||
const characterStatsRef = useRef(new Map<string, any>());
|
||||
|
|
@ -72,8 +76,17 @@ export function useLiveData(): DashboardState {
|
|||
setRecentRares(prev => [r, ...prev].slice(0, 50));
|
||||
} else if (msg.type === 'inventory_delta') {
|
||||
const d = msg as unknown as { character_name: string };
|
||||
// Bump inventory version so open inventory windows can re-fetch
|
||||
setInventoryVersion(v => v + 1);
|
||||
// Bump ONLY this character's inventory version so an open window for
|
||||
// that character re-fetches. Deltas for other characters don't touch
|
||||
// it, which keeps the 2s debounce in InventoryWindow from being reset
|
||||
// forever by unrelated chatter.
|
||||
if (d.character_name) {
|
||||
setInventoryVersions(prev => {
|
||||
const next = new Map(prev);
|
||||
next.set(d.character_name, (next.get(d.character_name) ?? 0) + 1);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
} else if (msg.type === 'character_stats') {
|
||||
// Store full character stats for CharacterWindow live updates
|
||||
const cs = msg as unknown as { character_name: string };
|
||||
|
|
@ -200,5 +213,5 @@ export function useLiveData(): DashboardState {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const characterStats = useMemo(() => characterStatsRef.current, [charStatsVersion]);
|
||||
|
||||
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, nearbyObjects, inventoryVersion, equipmentCantrips, characterStats, deathAlerts, socketRef };
|
||||
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, nearbyObjects, inventoryVersions, equipmentCantrips, characterStats, deathAlerts, socketRef };
|
||||
}
|
||||
|
|
|
|||
1
static/_build/assets/CharacterWindow-r4JBxCcd.js
Normal file
1
static/_build/assets/CharacterWindow-r4JBxCcd.js
Normal file
File diff suppressed because one or more lines are too long
1
static/_build/assets/CombatPickerWindow-Dn19Owcx.js
Normal file
1
static/_build/assets/CombatPickerWindow-Dn19Owcx.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{u as c,j as r,D as d}from"./index-BipVD-xb.js";import"./react-yfL0ty4i.js";const p=({id:n,zIndex:i,characters:a})=>{const{openWindow:s}=c(),e=Array.from(a.keys()).sort();return r.jsx(d,{id:n,title:"Combat Stats — Select Character",zIndex:i,width:300,height:400,children:r.jsx("div",{style:{flex:1,overflowY:"auto",padding:6},children:e.length===0?r.jsx("div",{style:{padding:12,color:"#666",textAlign:"center",fontSize:"0.8rem"},children:"No characters online"}):e.map(o=>r.jsx("div",{style:{padding:"5px 8px",cursor:"pointer",borderBottom:"1px solid #222",color:"#ccc",fontSize:"0.82rem"},onMouseEnter:t=>t.currentTarget.style.background="#2a2a2a",onMouseLeave:t=>t.currentTarget.style.background="",onClick:()=>s(`combat-${o}`,`Combat: ${o}`,o),children:o},o))})})};export{p as CombatPickerWindow};
|
||||
1
static/_build/assets/CombatStatsWindow-Cp6gbZMF.js
Normal file
1
static/_build/assets/CombatStatsWindow-Cp6gbZMF.js
Normal file
File diff suppressed because one or more lines are too long
1
static/_build/assets/InventoryWindow-BakfUY1F.js
Normal file
1
static/_build/assets/InventoryWindow-BakfUY1F.js
Normal file
File diff suppressed because one or more lines are too long
1
static/_build/assets/IssuesWindow-CrYOTY1n.js
Normal file
1
static/_build/assets/IssuesWindow-CrYOTY1n.js
Normal file
File diff suppressed because one or more lines are too long
1
static/_build/assets/PlayerDashboardWindow-fofpF1Et.js
Normal file
1
static/_build/assets/PlayerDashboardWindow-fofpF1Et.js
Normal file
File diff suppressed because one or more lines are too long
1
static/_build/assets/QuestStatusWindow-YiUHZSeT.js
Normal file
1
static/_build/assets/QuestStatusWindow-YiUHZSeT.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{r as c,j as t,D as u,a as f}from"./index-BipVD-xb.js";import"./react-yfL0ty4i.js";const j=({id:p,zIndex:x})=>{const[s,h]=c.useState(null);c.useEffect(()=>{const e=async()=>{try{h(await f("/quest-status"))}catch{}};e();const o=setInterval(e,3e4);return()=>clearInterval(o)},[]);const i=s?Object.keys(s.quest_data).sort():[],l=new Set;if(s)for(const e of Object.values(s.quest_data))for(const o of Object.keys(e))l.add(o);const n=Array.from(l).sort();return t.jsx(u,{id:p,title:"Quest Status",zIndex:x,width:780,height:500,children:t.jsx("div",{style:{flex:1,overflow:"auto",fontSize:"0.72rem"},children:s?i.length===0?t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"No quest data available"}):t.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[t.jsx("thead",{children:t.jsxs("tr",{style:{position:"sticky",top:0,background:"#1a1a1a",zIndex:1},children:[t.jsx("th",{style:{textAlign:"left",padding:"4px 8px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.65rem",fontWeight:600,minWidth:140},children:"Character"}),n.map(e=>t.jsx("th",{style:{textAlign:"center",padding:"4px 6px",borderBottom:"1px solid #444",color:"#888",fontSize:"0.6rem",fontWeight:600,maxWidth:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:e,children:e.replace(" Timer","").replace(" Pickup","")},e))]})}),t.jsx("tbody",{children:i.map(e=>{const o=s.quest_data[e]||{};return t.jsxs("tr",{style:{borderBottom:"1px solid #222"},children:[t.jsx("td",{style:{padding:"3px 8px",color:"#ccc",fontWeight:500,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:160},children:e}),n.map(d=>{const r=o[d],a=r==="READY";return t.jsx("td",{style:{textAlign:"center",padding:"3px 6px",color:a?"#4c4":r?"#ca0":"#333",fontWeight:a?600:400,fontSize:a?"0.7rem":"0.68rem"},children:r||"—"},d)})]},e)})})]}):t.jsx("div",{style:{padding:20,color:"#666",textAlign:"center"},children:"Loading quest data..."})})})};export{j as QuestStatusWindow};
|
||||
1
static/_build/assets/RadarWindow-CqeTYRoC.js
Normal file
1
static/_build/assets/RadarWindow-CqeTYRoC.js
Normal file
File diff suppressed because one or more lines are too long
1
static/_build/assets/StatsWindow-B26MeIH_.js
Normal file
1
static/_build/assets/StatsWindow-B26MeIH_.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{r as o,j as t,D as d}from"./index-BipVD-xb.js";import"./react-yfL0ty4i.js";const c=[{title:"Kills per Hour",id:1},{title:"Memory (MB)",id:2},{title:"CPU (%)",id:3},{title:"Mem Handles",id:4}],m=[{label:"1H",value:"now-1h"},{label:"6H",value:"now-6h"},{label:"24H",value:"now-24h"},{label:"7D",value:"now-7d"}],v=({id:s,charName:a,zIndex:i})=>{const[l,r]=o.useState("now-24h"),n=e=>`/grafana/d-solo/dereth-tracker/dereth-tracker-dashboard?panelId=${e}&var-character=${encodeURIComponent(a)}&from=${l}&to=now&theme=light`;return t.jsxs(d,{id:s,title:`Stats: ${a}`,zIndex:i,width:750,height:480,children:[t.jsx("div",{className:"ml-stats-controls",children:m.map(e=>t.jsx("button",{className:`ml-stats-range-btn ${l===e.value?"active":""}`,onClick:()=>r(e.value),children:e.label},e.value))}),t.jsx("div",{className:"ml-stats-grid",children:c.map(e=>t.jsx("div",{className:"ml-stats-panel",children:t.jsx("iframe",{src:n(e.id),width:"100%",height:"100%",frameBorder:"0",title:e.title})},e.id))})]})};export{v as StatsWindow};
|
||||
1
static/_build/assets/VitalSharingWindow-DDYw2SOf.js
Normal file
1
static/_build/assets/VitalSharingWindow-DDYw2SOf.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{r as n,j as t,D as x,a as m}from"./index-BipVD-xb.js";import"./react-yfL0ty4i.js";const v=({id:o,zIndex:c})=>{const[a,d]=n.useState([]);n.useEffect(()=>{const e=async()=>{try{const s=await m("/vital-sharing/peers");d(s.peers??[])}catch{}};e();const l=setInterval(e,5e3);return()=>clearInterval(l)},[]);const h=(e,l)=>l>0?Math.min(100,e/l*100):0;return t.jsx(x,{id:o,title:"Vital Sharing Network",zIndex:c,width:520,height:450,children:t.jsx("div",{style:{flex:1,overflowY:"auto",padding:6,fontSize:"0.75rem"},children:a.length===0?t.jsx("div",{style:{padding:16,color:"#666",textAlign:"center"},children:"No vital-sharing peers connected"}):a.map(e=>{var l,s,r;return t.jsxs("div",{style:{padding:"6px 8px",marginBottom:4,background:"#1f1f1f",borderRadius:3,border:"1px solid #333"},children:[t.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:3},children:[t.jsx("span",{style:{color:e.plugin_connected?"#4c4":"#a33",fontSize:"0.8rem"},children:"●"}),t.jsx("strong",{style:{flex:1},children:e.character_name}),e.subscribed&&t.jsx("span",{style:{color:"#6bf",fontSize:"0.65rem"},children:"[subscribed]"})]}),t.jsxs("div",{style:{color:"#666",fontSize:"0.68rem",marginBottom:3},children:["tags: ",((l=e.tags)==null?void 0:l.join(", "))||"none"]}),e.vitals&&e.vitals.max_health>0&&t.jsx("div",{style:{display:"flex",flexDirection:"column",gap:2},children:[{label:"HP",cur:e.vitals.current_health,max:e.vitals.max_health,bg:"#330000",fill:"#c44"},{label:"STA",cur:e.vitals.current_stamina,max:e.vitals.max_stamina,bg:"#331a00",fill:"#ca0"},{label:"MANA",cur:e.vitals.current_mana,max:e.vitals.max_mana,bg:"#001433",fill:"#48f"}].map(i=>t.jsxs("div",{style:{display:"flex",alignItems:"center",gap:4},children:[t.jsx("span",{style:{width:32,color:"#888",fontSize:"0.65rem"},children:i.label}),t.jsx("div",{style:{flex:1,height:6,background:i.bg,borderRadius:3,overflow:"hidden"},children:t.jsx("div",{style:{width:`${h(i.cur,i.max)}%`,height:"100%",background:i.fill,borderRadius:3}})}),t.jsxs("span",{style:{width:60,textAlign:"right",fontSize:"0.65rem",color:"#888"},children:[i.cur,"/",i.max]})]},i.label))}),e.position&&t.jsxs("div",{style:{color:"#555",fontSize:"0.65rem",marginTop:2},children:[(s=e.position.ns)==null?void 0:s.toFixed(1),"N, ",(r=e.position.ew)==null?void 0:r.toFixed(1),"E"]})]},e.character_name)})})})};export{v as VitalSharingWindow};
|
||||
34
static/_build/assets/index-BipVD-xb.js
Normal file
34
static/_build/assets/index-BipVD-xb.js
Normal file
File diff suppressed because one or more lines are too long
1
static/_build/assets/index-BsAcOCNp.css
Normal file
1
static/_build/assets/index-BsAcOCNp.css
Normal file
File diff suppressed because one or more lines are too long
17
static/_build/assets/react-yfL0ty4i.js
vendored
Normal file
17
static/_build/assets/react-yfL0ty4i.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
static/_build/index.html
Normal file
18
static/_build/index.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mosswart Overlord v2</title>
|
||||
<link rel="icon" type="image/png" href="/icons/7735.png" />
|
||||
<link rel="preload" as="image" href="/dereth.png" />
|
||||
<link rel="preload" as="image" href="/icons/0600127E.png" />
|
||||
<link rel="preload" as="fetch" href="/dungeon_tiles.json" crossorigin="anonymous" />
|
||||
<script type="module" crossorigin src="/assets/index-BipVD-xb.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/react-yfL0ty4i.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BsAcOCNp.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
72
static/_build/sw.js
Normal file
72
static/_build/sw.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Service worker for MosswartOverlord v2 — caches static assets for instant repeat loads
|
||||
const CACHE_NAME = 'mo-v2-cache-v1';
|
||||
const STATIC_ASSETS = [
|
||||
'/dereth.png',
|
||||
'/dereth_highres.png',
|
||||
'/prismatic-taper-icon.png',
|
||||
'/icons/0600127E.png',
|
||||
'/icons/06000133.png',
|
||||
'/icons/06001080.png',
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(keys =>
|
||||
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
|
||||
)
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// Cache icon images on first fetch
|
||||
if (url.pathname.startsWith('/icons/') && event.request.method === 'GET') {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then(cached => {
|
||||
if (cached) return cached;
|
||||
return fetch(event.request).then(response => {
|
||||
if (response.ok) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
|
||||
}
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache dungeon_tiles.json (large, rarely changes)
|
||||
if (url.pathname === '/dungeon_tiles.json') {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then(cached => {
|
||||
if (cached) return cached;
|
||||
return fetch(event.request).then(response => {
|
||||
if (response.ok) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
|
||||
}
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache static assets (map images etc)
|
||||
if (STATIC_ASSETS.some(a => url.pathname === a)) {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then(cached => cached || fetch(event.request))
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue