MosswartOverlord/static/assets/RadarWindow-BK3xRVEL.js
Erik 79cf88d3f7 feat(agent): Phase 1 — chat-window AI assistant via Claude Code subprocess
Adds an in-dashboard AI assistant that answers questions about live game
state. Designed reactively (no background loops) — every user message in
the chat window or via /api/agent/ask runs one `claude -p` invocation.

Architecture:
- New host-side FastAPI service (agent/) on 127.0.0.1:8767, OUTSIDE the
  dereth-tracker Docker container because `claude` and ~/.claude
  credentials live on the host.
- nginx routes /api/agent/* to the host service.
- The same browser session cookie the tracker issues authenticates
  agent requests (shared SECRET_KEY).
- The agent shells out to `claude -p --session-id <uuid>` with
  cwd=/home/erik/MosswartOverlord. Sessions persist as JSONL on disk
  via Claude Code's built-in machinery.
- An MCP stdio server (agent/mcp_overlord.py) exposes tools to Claude:
  get_live_players, get_recent_rares, query_telemetry_db (read-only,
  parsed by sqlglot to reject DML/DDL), get_player_state, get_inventory,
  get_inventory_search, get_combat_stats, get_equipment_cantrips,
  get_quest_status, get_server_health, suitbuilder_search.
- Read-only PG role (overlord_agent_ro) is the second line of defense
  on the SQL tool — even a parser bypass can't mutate.

Frontend:
- AgentWindow.tsx — draggable chat window with localStorage-pinned
  session UUID, "New Chat" button, on-mount rehydration from
  /agent/sessions/{id}/history (parses Claude Code's JSONL).
- Wired into WindowRenderer + Sidebar (🤖 Assistant button).

Operational:
- systemd unit (overlord-agent.service) + install.sh.
- agent/README.md documents env vars, deploy flow, smoke tests.
- nginx/overlord.conf gets a new /api/agent/ location with 180s timeout.
- CLAUDE.md gets an "Overlord Assistant Mode" section briefing the
  agent on which tools to use and how to behave.

NOT YET DEPLOYED — server still needs:
1. Apply agent/sql/0001_overlord_agent_ro.sql + ALTER ROLE password
2. Add AGENT_DB_DSN to /home/erik/MosswartOverlord/.env
3. bash agent/install.sh (creates venv, installs unit, starts service)
4. sudo cp /home/erik/MosswartOverlord/nginx/overlord.conf to nginx + reload

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 20:43:59 +02:00

1 line
7.5 KiB
JavaScript

import{r as x,j as c,D as ne}from"./index-Dcc3Au5i.js";import"./react-yfL0ty4i.js";const q=300,H=.5,se={walls:{r:0,g:0,b:255},innerWalls:{r:127,g:127,b:255},rampedWalls:{r:77,g:255,b:255},floors:{r:0,g:127,b:255},stairs:{r:0,g:63,b:255}},le={walls:{r:140,g:140,b:180},innerWalls:{r:100,g:100,b:140},rampedWalls:{r:120,g:160,b:120},floors:{r:60,g:80,b:60},stairs:{r:180,g:160,b:80}};function oe(d){const g=document.createElement("canvas");g.width=10,g.height=10;const w=g.getContext("2d");w.drawImage(d,0,0,10,10);const a=w.getImageData(0,0,10,10),t=a.data;for(let p=0;p<t.length;p+=4){const _=t[p],R=t[p+1],k=t[p+2];if(_>240&&R>240&&k>240){t[p+3]=0;continue}let E=!1;for(const[L,I]of Object.entries(se))if(Math.abs(_-I.r)<15&&Math.abs(R-I.g)<15&&Math.abs(k-I.b)<15){const W=le[L];t[p]=W.r,t[p+1]=W.g,t[p+2]=W.b,E=!0;break}!E&&_<15&&R<15&&k<15&&(t[p+3]=100)}return w.putImageData(a,0,0),g}function ie(d){return d===1?Math.PI:d<-.7&&d>-.8?Math.PI/2:d>.7&&d<.8?-Math.PI/2:0}let A=null;function ce(){A||(A={},fetch("/dungeon_tiles.json").then(d=>d.json()).then(d=>{Object.entries(d).forEach(([g,w])=>{const a=new Image;a.onload=()=>{A[g]=oe(a)},a.src=w})}).catch(()=>{}))}const K={Monster:"#ff4444",Player:"#4488ff",NPC:"#44cc44",Vendor:"#44cc44",Portal:"#aa44ff",Corpse:"#ff8800",Container:"#cccc44",Door:"#888888"};function re(d){const g=(d%360+360)%360;return["N","NE","E","SE","S","SW","W","NW"][Math.round(g/45)%8]}const fe=({id:d,charName:g,zIndex:w,socket:a,radarData:t})=>{const p=x.useRef(null),_=x.useRef(H),[R,k]=x.useState(H),[E,L]=x.useState(null),I=x.useRef(null),W=x.useRef([]);x.useEffect(()=>{const n=new Image;n.src="/dereth.png",n.onload=()=>{I.current=n},ce()},[]),x.useEffect(()=>((a==null?void 0:a.readyState)===WebSocket.OPEN&&a.send(JSON.stringify({player_name:g,command:"start_radar"})),()=>{(a==null?void 0:a.readyState)===WebSocket.OPEN&&a.send(JSON.stringify({player_name:g,command:"stop_radar"}))}),[g,a]);const Q=x.useCallback(n=>{n.preventDefault();const e=n.deltaY>0?1.25:.8;_.current=Math.max(.02,Math.min(5,_.current*e)),k(_.current)},[]),D=x.useCallback(n=>{const e=p.current;if(!e)return;const o=e.getBoundingClientRect(),s=(n.clientX-o.left)*(e.width/o.width),r=(n.clientY-o.top)*(e.height/o.height);let M=null,u=20;W.current.forEach(f=>{if(f._px===void 0)return;const S=Math.sqrt((s-f._px)**2+(r-f._py)**2);S<u&&(u=S,M=f)}),L(M?M.id:null)},[]);x.useEffect(()=>{var G;const n=p.current;if(!n||!t)return;const e=n.getContext("2d");if(!e)return;const o=q,s=o/2,r=o/2,M=t.objects??[],u=t.player_ew??0,f=t.player_ns??0,S=t.player_heading??0,b=t.is_dungeon??!1,V=t.player_x??0,Z=t.player_y??0,Y=_.current,z=b?o/2/(Y*240):o/2/Y,j=S*Math.PI/180;e.clearRect(0,0,o,o),e.fillStyle="#111",e.beginPath(),e.arc(s,r,s,0,Math.PI*2),e.fill(),e.save(),e.beginPath(),e.arc(s,r,s-1,0,Math.PI*2),e.clip();const B=t.landblock??null,te=t.player_raw_z??0;if(b&&B&&((G=window.__dungeonMapCache)!=null&&G[B])){const l=window.__dungeonMapCache[B],h=Math.floor((te+3)/6)*6;e.translate(s,r),e.rotate(-(S-180)*Math.PI/180);const i=10*z,O=A&&Object.keys(A).length>0;(l.z_levels||[]).slice().sort((y,m)=>(y.z===h?1:0)-(m.z===h?1:0)).forEach(y=>{const m=y.z===h;e.globalAlpha=m?.85:.12,(y.cells||[]).forEach(T=>{const P=-(T.x-V)*z,N=(T.y-Z)*z,C=O?A[String(T.env_id)]:null;C?(e.save(),e.translate(P,N),e.rotate(ie(T.rotation)),e.drawImage(C,-i/2,-i/2,i,i),e.restore()):(e.fillStyle=m?"#3a5a3a":"#1a2a1a",e.fillRect(P-i/2,N-i/2,i,i))})}),e.globalAlpha=1,e.setTransform(1,0,0,1,0,0)}else if(!b&&I.current){const l=I.current,h=l.naturalWidth/204.2,i=(u+102.1)*h,O=(102.1-f)*h;e.globalAlpha=.4,e.save(),e.translate(s,r),e.rotate(-j);const v=Y*h*2;e.drawImage(l,i-v/2,O-v/2,v,v,-s,-r,o,o),e.restore(),e.globalAlpha=1}e.restore(),e.strokeStyle="#333",e.lineWidth=1;for(let l=1;l<=4;l++)e.beginPath(),e.arc(s,r,s/4*l,0,Math.PI*2),e.stroke();e.beginPath(),e.moveTo(s,0),e.lineTo(s,o),e.moveTo(0,r),e.lineTo(o,r),e.stroke(),e.font="bold 12px monospace",e.textAlign="center",e.textBaseline="middle",[{l:"N",a:0},{l:"E",a:Math.PI/2},{l:"S",a:Math.PI},{l:"W",a:-Math.PI/2}].forEach(({l,a:h})=>{const i=h-j;e.fillStyle=l==="N"?"#cc4444":"#888",e.fillText(l,s+Math.sin(i)*(s-12),r-Math.cos(i)*(s-12))}),e.strokeStyle="#666",e.lineWidth=1,e.beginPath(),e.moveTo(s,r),e.lineTo(s,r-s*.85),e.stroke();const $=b?Math.PI-j:j,X=Math.cos($),F=Math.sin($);M.forEach(l=>{let h,i;b&&l.raw_x!==void 0?(h=-(l.raw_x-V),i=l.raw_y-Z):(h=(l.ew??0)-u,i=(l.ns??0)-f);const O=h*X-i*F,v=b?h*F+i*X:-(h*F+i*X),y=s+O*z,m=r+v*z;if(Math.sqrt((y-s)**2+(m-r)**2)>s-4)return;l._px=y,l._py=m;const P=l.object_class??l.type??"",N=K[P]??"#888",C=l.id===E,J=C?6:P==="Monster"||P==="Player"?4:3;C&&(e.strokeStyle="#fff",e.lineWidth=2,e.beginPath(),e.arc(y,m,J+3,0,Math.PI*2),e.stroke()),e.fillStyle=N,e.beginPath(),e.arc(y,m,J,0,Math.PI*2),e.fill(),(P==="Player"||P==="Portal"||C)&&(e.fillStyle=C?"#fff":N,e.font="9px monospace",e.textAlign="left",e.fillText(l.name,y+6,m+3))}),W.current=M,e.fillStyle="#ffcc00",e.beginPath(),e.arc(s,r,5,0,Math.PI*2),e.fill(),e.strokeStyle="#fff",e.lineWidth=1,e.stroke()},[t,R,E]);const U=((t==null?void 0:t.objects)??[]).map(n=>{const e=(t==null?void 0:t.player_ew)??0,o=(t==null?void 0:t.player_ns)??0,s=(t==null?void 0:t.is_dungeon)??!1,r=(t==null?void 0:t.player_x)??0,M=(t==null?void 0:t.player_y)??0;let u,f,S;s&&n.raw_x!==void 0?(u=-(n.raw_x-r),f=n.raw_y-M,S=Math.sqrt(u*u+f*f)):(u=(n.ew??0)-e,f=(n.ns??0)-o,S=Math.sqrt(u*u+f*f)*240);const b=Math.atan2(u,f)*180/Math.PI;return{...n,dist:S,dir:re(b)}}).sort((n,e)=>n.dist-e.dist),ee=Math.round(R*240);return c.jsxs(ne,{id:d,title:`Radar: ${g}`,zIndex:w,width:360,height:560,children:[c.jsxs("div",{style:{padding:"4px 8px",display:"flex",justifyContent:"space-between",fontSize:"0.75rem",color:"#888",borderBottom:"1px solid #333",background:"#1a1a1a"},children:[c.jsxs("span",{children:["Range: ~",ee,"m"]}),c.jsx("span",{style:{fontSize:"0.65rem",color:"#555"},children:"Scroll to zoom"})]}),c.jsx("canvas",{ref:p,width:q,height:q,style:{display:"block",margin:"0 auto",borderBottom:"1px solid #333",cursor:"crosshair",flexShrink:0},onWheel:Q,onClick:D}),c.jsxs("div",{style:{flex:1,overflowY:"auto",fontSize:"0.72rem",minHeight:0},children:[c.jsxs("div",{style:{display:"flex",padding:"3px 6px",borderBottom:"1px solid #333",color:"#666",fontSize:"0.65rem",fontWeight:600},children:[c.jsx("span",{style:{width:8}}),c.jsx("span",{style:{flex:1,marginLeft:6},children:"Name"}),c.jsx("span",{style:{width:55,textAlign:"left"},children:"Type"}),c.jsx("span",{style:{width:40,textAlign:"right"},children:"Dist"}),c.jsx("span",{style:{width:24,textAlign:"center"},children:"Dir"})]}),U.length===0&&c.jsx("div",{style:{padding:12,color:"#555",textAlign:"center",fontSize:"0.7rem"},children:"Waiting for radar data..."}),U.map(n=>{const e=n.object_class??n.type??"",o=K[e]??"#888",s=n.id===E;return c.jsxs("div",{onClick:()=>L(s?null:n.id),style:{display:"flex",alignItems:"center",padding:"2px 6px",borderBottom:"1px solid #1a1a1a",cursor:"pointer",color:"#ccc",background:s?"#1a2a3a":"",borderLeft:s?"2px solid #4488ff":"2px solid transparent"},children:[c.jsx("span",{style:{width:8,height:8,borderRadius:"50%",background:o,flexShrink:0}}),c.jsx("span",{style:{flex:1,marginLeft:6,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:n.name}),c.jsx("span",{style:{width:55,color:"#888",fontSize:"0.65rem"},children:e}),c.jsx("span",{style:{width:40,textAlign:"right",fontVariantNumeric:"tabular-nums"},children:n.dist<1e3?`${Math.round(n.dist)}m`:`${(n.dist/1e3).toFixed(1)}km`}),c.jsx("span",{style:{width:24,textAlign:"center",color:"#666"},children:n.dir})]},n.id)})]})]})};export{fe as RadarWindow};