← back to drydock-dev__drydock-app

Function bodies 44 total

All specs Real LLM only Function bodies
fetchAvailableShips function · javascript · L59-L93 (35 LOC)
backend/rsi-store-worker/index.js
async function fetchAvailableShips() {
  try {
    const response = await fetch(RSI_GRAPHQL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Origin': 'https://robertsspaceindustries.com',
        'Referer': 'https://robertsspaceindustries.com/en/pledge',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
      },
      body: SHIPS_QUERY,
    });
    
    if (!response.ok) {
      console.error('RSI API error: ' + response.status);
      return null;
    }
    
    const data = await response.json();
    // Response is an array, first element has data
    const result = Array.isArray(data) ? data[0] : data;
    console.log('RSI response type:', typeof data, 'isArray:', Array.isArray(data));
    console.log('Result keys:', result ? Object.keys(result) : 'null');
    console.log('Ships count:', result?.data?.ships?.length || 0);
    if (result?.data?.ships?.l
checkUpgradeAvailability function · javascript · L96-L117 (22 LOC)
backend/rsi-store-worker/index.js
async function checkUpgradeAvailability(fromShipId) {
  try {
    const response = await fetch(RSI_GRAPHQL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
      },
      body: JSON.stringify({
        query: UPGRADES_QUERY,
        variables: { fromShipId },
      }),
    });
    
    if (!response.ok) return null;
    const data = await response.json();
    return data?.data?.ships || [];
  } catch (err) {
    console.error('Failed to check upgrade:', err);
    return null;
  }
}
sendAlertEmail function · javascript · L314-L384 (71 LOC)
backend/rsi-store-worker/index.js
async function sendAlertEmail(env, toEmail, alerts) {
  const RESEND_API_KEY = env.RESEND_API_KEY;
  if (!RESEND_API_KEY) {
    console.error('No RESEND_API_KEY configured');
    return;
  }
  
  const alertList = alerts.map(a => 
    `• ${a.fromShip} → ${a.toShip}: $${a.price} ${a.isWarbond ? '(WARBOND)' : '(Standard)'}`
  ).join('\n');
  
  const htmlAlerts = alerts.map(a => `
    <tr>
      <td style="padding:8px;border-bottom:1px solid #1e3048;color:#c8d6e5">${a.fromShip}</td>
      <td style="padding:8px;border-bottom:1px solid #1e3048;color:#c8d6e5">→</td>
      <td style="padding:8px;border-bottom:1px solid #1e3048;color:#00c8ff;font-weight:bold">${a.toShip}</td>
      <td style="padding:8px;border-bottom:1px solid #1e3048;color:#f59e0b;font-weight:bold">$${a.price}</td>
      <td style="padding:8px;border-bottom:1px solid #1e3048;color:${a.isWarbond ? '#10b981' : '#607080'}">${a.isWarbond ? 'WARBOND' : 'Standard'}</td>
    </tr>
  `).join('');
  
  try {
    await fetch('https:
collapsePhantomSteps function · javascript · L7-L42 (36 LOC)
collapse_phantoms.cjs
function collapsePhantomSteps(steps) {
  if (!steps || steps.length === 0) return steps;
  const collapsed = [];
  let i = 0;
  while (i < steps.length) {
    if (steps[i].phantom) {
      // Start of a phantom run — find the end
      let j = i;
      let totalCost = 0;
      let totalSaving = 0;
      while (j < steps.length && steps[j].phantom) {
        totalCost += steps[j].cost;
        totalSaving += (steps[j].saving || 0);
        j++;
      }
      // Collapse into one step
      collapsed.push({
        from: steps[i].from,
        to: steps[j - 1].to,
        fromMsrp: steps[i].fromMsrp,
        toMsrp: steps[j - 1].toMsrp,
        cost: totalCost,
        pledgeCost: totalCost,
        saving: totalSaving,
        phantom: true,
        standardCost: steps[j - 1].toMsrp - steps[i].fromMsrp,
        collapsedCount: j - i,
      });
      i = j;
    } else {
      collapsed.push(steps[i]);
      i++;
    }
  }
  return collapsed;
}
MinHeap class · javascript · L314-L321 (8 LOC)
src/App.jsx
class MinHeap {
  constructor() { this.h = []; }
  push(v) { this.h.push(v); this._up(this.h.length - 1); }
  pop() { const top = this.h[0], last = this.h.pop(); if (this.h.length > 0) { this.h[0] = last; this._down(0); } return top; }
  get size() { return this.h.length; }
  _up(i) { while (i > 0) { const p = (i - 1) >> 1; if (this.h[p][0] <= this.h[i][0]) break; [this.h[p], this.h[i]] = [this.h[i], this.h[p]]; i = p; } }
  _down(i) { const n = this.h.length; while (true) { let s = i, l = 2*i+1, r = 2*i+2; if (l < n && this.h[l][0] < this.h[s][0]) s = l; if (r < n && this.h[r][0] < this.h[s][0]) s = r; if (s === i) break; [this.h[s], this.h[i]] = [this.h[i], this.h[s]]; i = s; } }
}
findCheapestChains function · javascript · L324-L359 (36 LOC)
src/App.jsx
function findCheapestChains(ccus, targetShip) {
  const revEdges = {};
  const allNodes = new Set();
  ccus.forEach(c => {
    allNodes.add(c.from); allNodes.add(c.to);
    if (!revEdges[c.to]) revEdges[c.to] = [];
    revEdges[c.to].push({to:c.from,cost:c.pledge,saving:c.saving,origFrom:c.from,origTo:c.to,fromMsrp:c.fromMsrp,toMsrp:c.toMsrp});
  });
  if (!allNodes.has(targetShip)) return [];
  const dist={},prev={},prevEdge={},visited=new Set();
  dist[targetShip]=0;
  const pq=new MinHeap();
  pq.push([0,targetShip]);
  while(pq.size>0){
    const[cost,node]=pq.pop();
    if(visited.has(node))continue;
    visited.add(node);
    for(const edge of(revEdges[node]||[])){
      const nc=cost+edge.cost;
      if(dist[edge.to]===undefined||nc<dist[edge.to]){
        dist[edge.to]=nc;prev[edge.to]=node;
        prevEdge[edge.to]={from:edge.origFrom,to:edge.origTo,cost:edge.cost,saving:edge.saving,fromMsrp:edge.fromMsrp,toMsrp:edge.toMsrp};
        pq.push([nc,edge.to]);
      }
    }
  }
 
SearchableShipSelect function · javascript · L363-L412 (50 LOC)
src/App.jsx
function SearchableShipSelect({ onSelect, exclude, placeholder, restrictTo }) {
  const [query, setQuery] = useState("");
  const [open, setOpen] = useState(false);
  const excl = exclude || new Set();
  const allShips = useMemo(() =>
    Object.entries(SHIP_MSRP)
      .map(([name, msrp]) => ({ name, msrp, mfr: (SHIP_DETAILS[name] || {}).mfr || "" }))
      .sort((a, b) => b.msrp - a.msrp),
  []);
  const filtered = useMemo(() => {
    if (!query) return [];
    const q = query.toLowerCase();
    return allShips.filter(s =>
      !excl.has(s.name) &&
      (!restrictTo || restrictTo.has(s.name)) &&
      (s.name.toLowerCase().includes(q) || s.mfr.toLowerCase().includes(q))
    ).slice(0, 15);
  }, [query, allShips, excl]);

  return (
    <div className="ship-search-wrap">
      <input
        className="ship-search-input"
        placeholder={placeholder || "Type to search ships..."}
        value={query}
        onChange={e => { setQuery(e.target.value); setOpen(true); }}
        on
Repobility (the analyzer behind this table) · https://repobility.com
ShipLink function · javascript · L418-L424 (7 LOC)
src/App.jsx
function ShipLink({ name, style }) {
  return (
    <span className="ship-link" style={style} onClick={(e) => { e.stopPropagation(); _openShipModal(name); }}>
      {name}
    </span>
  );
}
ShipModal function · javascript · L426-L494 (69 LOC)
src/App.jsx
function ShipModal({ name, onClose }) {
  if (!name) return null;
  const msrp = SHIP_MSRP[name] || 0;
  const role = SHIP_ROLES[name] || "Unknown";
  const d = SHIP_DETAILS[name];
  const statusClass = d ? (d.status === "flight-ready" ? "status-flight" : d.status === "in-concept" ? "status-concept" : "status-prod") : "";
  const wbSkus = d ? d.skus.filter(s => s.t.includes("Warbond")).sort((a, b) => b.d.localeCompare(a.d)) : [];
  const bestWb = wbSkus.length > 0 ? wbSkus[0] : null;

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <div className="modal-header">
          <div>
            <div style={{ fontFamily: "'Orbitron',sans-serif", fontSize: 18, fontWeight: 700, color: "var(--text-bright)", marginBottom: 4 }}>{name}</div>
            <ShipImage name={name} size={200} role={role} style={{ marginTop: 8, borderRadius: 6 }} />
            {d && <div style={{ fontSize: 13, color: "var(--text-di
FleetVisualization function · javascript · L499-L585 (87 LOC)
src/App.jsx
function FleetVisualization() {
  const [hovered, setHovered] = useState(null);

  const ships = useMemo(() => {
    const seen = new Set();
    return SHIPS_RAW.filter(s => {
      const k = s.name + s.pledgeId;
      if (seen.has(k)) return false;
      seen.add(k);
      return true;
    }).map(s => ({
      ...s,
      realMsrp: getShipMsrp(s.name),
      role: SHIP_ROLES[s.name] || "Utility",
    })).sort((a, b) => b.realMsrp - a.realMsrp);
  }, []);

  const roles = useMemo(() => [...new Set(ships.map(s => s.role))].sort(), [ships]);

  const getSize = (msrp) => {
    if (msrp >= 900) return { w: 150, h: 72 };
    if (msrp >= 500) return { w: 130, h: 60 };
    if (msrp >= 300) return { w: 110, h: 50 };
    if (msrp >= 150) return { w: 90, h: 42 };
    if (msrp >= 50) return { w: 72, h: 34 };
    return { w: 56, h: 28 };
  };

  const hShip = hovered !== null ? ships[hovered] : null;

  return (
    <div>
      <div className="viz-legend">
        {roles.map(r => (
          <div 
FleetTab function · javascript · L587-L784 (198 LOC)
src/App.jsx
function FleetTab({search}){
  const[viewMode,setViewMode]=useState("visual");
  const[allSort,setAllSort]=useState("msrp");
  const[allDir,setAllDir]=useState(-1);

  const allShips = useMemo(() => {
    const seen = new Set();
    let ships = SHIPS_RAW.filter(s => {
      const k = s.name + s.pledgeId;
      if (seen.has(k)) return false;
      seen.add(k); return true;
    }).map(s => ({
      ...s,
      realMsrp: getShipMsrp(s.name),
      role: SHIP_ROLES[s.name] || "Utility",
      mfr: (SHIP_DETAILS[s.name] || {}).mfr || "",
      size: (SHIP_DETAILS[s.name] || {}).size || "",
    }));
    if (search) {
      const q = search.toLowerCase();
      ships = ships.filter(s => s.name.toLowerCase().includes(q) || s.packageName.toLowerCase().includes(q) || s.role.toLowerCase().includes(q) || s.mfr.toLowerCase().includes(q));
    }
    ships.sort((a, b) => {
      let av, bv;
      if (allSort === "msrp") { av = a.realMsrp; bv = b.realMsrp; }
      else if (allSort === "name") { av = a
PackageGroup function · javascript · L786-L803 (18 LOC)
src/App.jsx
function PackageGroup({name,ships,defaultOpen}){
  const[open,setOpen]=useState(defaultOpen);
  const count=ships.length;const totalMsrp=ships.reduce((s,sh)=>s+getShipMsrp(sh.name),0);
  const isDom=name==="Package - Dominus Pack - Digital";
  const displayName=name.replace("Package - ","").replace("Packs - ","");
  return(<div className="pkg-group"><div className="pkg-header"onClick={()=>setOpen(!open)}>
    <div style={{display:"flex",alignItems:"center"}}><span className={`chevron ${open?"open":""}`}>&#9654;</span><span className="pkg-title">{displayName}</span></div>
    <div className="pkg-meta"><span>{count} ship{count!==1?"s":""}</span>{isDom&&<span style={{color:"var(--green)"}}>Pack: $7,020</span>}<span>Value: {fmt(totalMsrp)}</span></div>
  </div>
  {open&&<div className="overflow-x"><table><thead><tr><th>Ship</th><th>MSRP</th><th>Ins.</th><th>Melt</th><th>Gift</th><th>Date</th></tr></thead><tbody>
    {ships.sort((a,b)=>getShipMsrp(b.name)-getShipMsrp(a.name)).map((sh,i)=><t
CCUTab function · javascript · L805-L837 (33 LOC)
src/App.jsx
function CCUTab({search}){
  const[sortKey,setSortKey]=useState("saving");const[sortDir,setSortDir]=useState(-1);
  const handleSort=(key)=>{if(sortKey===key)setSortDir(d=>d*-1);else{setSortKey(key);setSortDir(-1)}};
  const sorted=useMemo(()=>{let data=[...CCUS_RAW];if(search){const q=search.toLowerCase();data=data.filter(c=>c.from.toLowerCase().includes(q)||c.to.toLowerCase().includes(q))}data.sort((a,b)=>{let av=a[sortKey],bv=b[sortKey];if(typeof av==="string")return av.localeCompare(bv)*sortDir;return(av-bv)*sortDir});return data},[search,sortKey,sortDir]);
  const totalUnits=sorted.reduce((s,c)=>s+c.count,0);const totalPledge=sorted.reduce((s,c)=>s+c.pledge*c.count,0);const totalSaving=sorted.reduce((s,c)=>s+c.saving*c.count,0);
  const SortTh=({k,children,style})=><th className={sortKey===k?"sorted":""}onClick={()=>handleSort(k)}style={style}>{children}{sortKey===k&&<span className="sort-arrow">{sortDir===1?"▲":"▼"}</span>}</th>;
  return(<div>
    <div style={{display:"flex",gap
buildPhantomEdges function · javascript · L840-L858 (19 LOC)
src/App.jsx
function buildPhantomEdges(phantomDiscount = 0) {
  const edges = {};
  const shipNames = Object.keys(SHIP_MSRP);
  const discountMult = 1 - (phantomDiscount / 100);
  for (const from of shipNames) {
    const fromM = SHIP_MSRP[from] || 0;
    if (fromM === 0) continue;
    for (const to of shipNames) {
      if (from === to) continue;
      const toM = SHIP_MSRP[to] || 0;
      if (toM <= fromM || toM === 0 || toM - fromM > 75) continue;
      const standardCost = toM - fromM;
      const estCost = Math.max(5, Math.round(standardCost * discountMult));
      if (!edges[to]) edges[to] = [];
      edges[to].push({ to: from, cost: estCost, pledgeCost: estCost, saving: standardCost - estCost, origFrom: from, origTo: to, fromMsrp: fromM, toMsrp: toM, phantom: true, standardCost });
    }
  }
  return edges;
}
findBestChainWithInventory function · javascript · L861-L918 (58 LOC)
src/App.jsx
function findBestChainWithInventory(inventory, targetShip, validSources, usePhantoms = false, phantomEdges = null) {
  const ccuLookup = {};
  CCUS_RAW.forEach(c => { ccuLookup[`${c.from}→${c.to}`] = c; });
  const revEdges = {};

  // Add owned CCU edges — cost=0 for routing because these are already paid for (sunk cost)
  for (const [key, remaining] of Object.entries(inventory)) {
    if (remaining <= 0) continue;
    const ccu = ccuLookup[key];
    if (!ccu) continue;
    if (!revEdges[ccu.to]) revEdges[ccu.to] = [];
    revEdges[ccu.to].push({ to: ccu.from, cost: 0, pledgeCost: ccu.pledge, saving: ccu.saving, origFrom: ccu.from, origTo: ccu.to, fromMsrp: ccu.fromMsrp, toMsrp: ccu.toMsrp, phantom: false });
  }

  // Merge pre-built phantom edges (skip where owned exists)
  if (usePhantoms && phantomEdges) {
    for (const [to, edges] of Object.entries(phantomEdges)) {
      for (const edge of edges) {
        const key = `${edge.origFrom}→${edge.origTo}`;
        if (inventory[key]
About: code-quality intelligence by Repobility · https://repobility.com
computeFleetPlan function · javascript · L920-L935 (16 LOC)
src/App.jsx
function computeFleetPlan(sources, targets, usePhantoms = false, phantomEdges = null) {
  const inventory = {};
  CCUS_RAW.forEach(c => { const key = `${c.from}→${c.to}`; inventory[key] = (inventory[key] || 0) + c.count; });
  const assignments = [], unassigned = [];
  const remainingSources = new Set(sources);
  for (const target of targets) {
    const chain = findBestChainWithInventory(inventory, target, remainingSources, usePhantoms, phantomEdges);
    if (chain) {
      assignments.push(chain);
      remainingSources.delete(chain.source);
      for (const step of chain.steps) { if (!step.phantom) { const key = `${step.from}→${step.to}`; inventory[key] = (inventory[key] || 0) - 1; } }
    } else { unassigned.push(target); }
  }
  const totalCost = assignments.reduce((s, a) => s + a.totalCost, 0);
  return { assignments, unassigned, remainingInventory: inventory, totalCost };
}
computeOptimalFleetPlan function · javascript · L938-L961 (24 LOC)
src/App.jsx
function computeOptimalFleetPlan(sources, targets, usePhantoms = false, phantomDiscount = 0) {
  if (targets.length === 0 || sources.length === 0) {
    return { assignments: [], unassigned: [...targets], remainingInventory: {}, totalCost: 0, permsTried: 0, note: "" };
  }
  const phantomEdges = usePhantoms ? buildPhantomEdges(phantomDiscount) : null;
  // Smart orderings instead of full N! — covers key scenarios in O(N²) not O(N!)
  const byMsrpDesc = [...targets].sort((a, b) => (SHIP_MSRP[b] || 0) - (SHIP_MSRP[a] || 0));
  const byMsrpAsc = [...targets].sort((a, b) => (SHIP_MSRP[a] || 0) - (SHIP_MSRP[b] || 0));
  const orderings = [byMsrpDesc, byMsrpAsc];
  // Add single-target-first orderings for each target
  for (const t of targets) {
    const rest = targets.filter(x => x !== t).sort((a, b) => (SHIP_MSRP[b] || 0) - (SHIP_MSRP[a] || 0));
    orderings.push([t, ...rest]);
  }
  let bestPlan = null, bestScore = -Infinity;
  for (const ordering of orderings) {
    const plan = comput
collapsePhantomSteps function · javascript · L965-L1000 (36 LOC)
src/App.jsx
function collapsePhantomSteps(steps) {
  if (!steps || steps.length === 0) return steps;
  const collapsed = [];
  let i = 0;
  while (i < steps.length) {
    if (steps[i].phantom) {
      // Start of a phantom run — find the end
      let j = i;
      let totalCost = 0;
      let totalSaving = 0;
      while (j < steps.length && steps[j].phantom) {
        totalCost += steps[j].cost;
        totalSaving += (steps[j].saving || 0);
        j++;
      }
      // Collapse into one step
      collapsed.push({
        from: steps[i].from,
        to: steps[j - 1].to,
        fromMsrp: steps[i].fromMsrp,
        toMsrp: steps[j - 1].toMsrp,
        cost: totalCost,
        pledgeCost: totalCost,
        saving: totalSaving,
        phantom: true,
        standardCost: steps[j - 1].toMsrp - steps[i].fromMsrp,
        collapsedCount: j - i,
      });
      i = j;
    } else {
      collapsed.push(steps[i]);
      i++;
    }
  }
  return collapsed;
}
OptimizerTab function · javascript · L1002-L1292 (291 LOC)
src/App.jsx
function OptimizerTab({optMode: mode, setOptMode: setMode, optTarget: target, setOptTarget: setTarget, planTargets, setPlanTargets, planSources, setPlanSources, showPhantoms, setShowPhantoms, phantomDiscount, setPhantomDiscount}){
  const [expandedIdx, setExpandedIdx] = useState(null);
  const [expandedPlan, setExpandedPlan] = useState(null);

  const targets = useMemo(() => [...new Set(CCUS_RAW.map(c => c.to))].sort(), []);
  const chains = useMemo(() => findCheapestChains(CCUS_RAW, target), [target]);
  const ownedShips = useMemo(() => new Set(SHIPS_RAW.map(s => s.name)), []);
  const ranked = useMemo(() => {
    const owned = chains.filter(c => ownedShips.has(c.source));
    const notOwned = chains.filter(c => !ownedShips.has(c.source));
    return [...owned, ...notOwned].slice(0, 25);
  }, [chains, ownedShips]);

  const removePlanTarget = (t) => setPlanTargets(prev => prev.filter(x => x !== t));
  const toggleSource = (name) => setPlanSources(prev => {
    const next = new Set(pre
SaleEvaluatorTab function · javascript · L1295-L1538 (244 LOC)
src/App.jsx
function SaleEvaluatorTab() {
  const allShipNames = useMemo(() => {
    const names = new Set();
    Object.keys(SHIP_MSRP).forEach(n => names.add(n));
    CCUS_RAW.forEach(c => { names.add(c.from); names.add(c.to); });
    return [...names].sort();
  }, []);

  const [fromShip, setFromShip] = useState("Starlancer TAC");
  const [toShip, setToShip] = useState("Galaxy");
  const [price, setPrice] = useState("5");
  const [isWarbond, setIsWarbond] = useState(true);

  const ownedShips = useMemo(() => new Set(SHIPS_RAW.map(s => s.name)), []);

  // High-value targets: ships that are final destinations in existing chains
  const targetShips = useMemo(() => {
    const toSet = new Set(CCUS_RAW.map(c => c.to));
    const fromSet = new Set(CCUS_RAW.map(c => c.from));
    // "Terminal" targets: things that are CCU destinations but rarely sources, or high-value
    const terminals = [...toSet].filter(t => (SHIP_MSRP[t] || 0) >= 350);
    return [...new Set(terminals)].sort((a, b) => (SHIP_MSRP
App function · javascript · L1540-L1624 (85 LOC)
src/App.jsx
export default function App(){

  // ─── Dynamic data state (persisted to localStorage) ─────────────────────────
  const [shipsData, setShipsData] = useState(SHIPS_RAW);
  const [ccusData, setCcusData] = useState(CCUS_RAW);
  const hasData = shipsData.length > 0 || ccusData.length > 0;

  const handleImportShips = (ships) => {
    SHIPS_RAW = ships;
    setShipsData(ships);
    localStorage.setItem('drydock_ships', JSON.stringify(ships));
  };
  const handleImportCCUs = (ccus) => {
    CCUS_RAW = ccus;
    setCcusData(ccus);
    localStorage.setItem('drydock_ccus', JSON.stringify(ccus));
  };
  const clearData = () => {
    SHIPS_RAW = []; CCUS_RAW = [];
    setShipsData([]); setCcusData([]);
    localStorage.removeItem('drydock_ships');
    localStorage.removeItem('drydock_ccus');
  };

  const[tab,setTab]=useState(SHIPS_RAW.length > 0 ? "fleet" : "import");const[search,setSearch]=useState("");
  const[selectedShip,setSelectedShip]=useState(null);
  setShipModalOpener(setSelectedShip
apiCall function · javascript · L28-L39 (12 LOC)
src/components/CCUWatchlist.jsx
async function apiCall(path, options = {}) {
  try {
    const response = await fetch(`${API_BASE}${path}`, {
      ...options,
      headers: { 'Content-Type': 'application/json', ...options.headers },
    });
    return await response.json();
  } catch (err) {
    console.error('API error:', err);
    return { error: err.message };
  }
}
CCUWatchlist function · javascript · L42-L328 (287 LOC)
src/components/CCUWatchlist.jsx
export default function CCUWatchlist({ shipMsrp, phantomSteps }) {
  const SHIP_MSRP = shipMsrp || {};
  
  const [email, setEmail] = useState(() => localStorage.getItem('scfp_email') || '');
  const [emailSaved, setEmailSaved] = useState(!!localStorage.getItem('scfp_email'));
  const [watchlist, setWatchlist] = useState([]);
  const [alerts, setAlerts] = useState([]);
  const [storeData, setStoreData] = useState(null);
  const [lastUpdated, setLastUpdated] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [addFrom, setAddFrom] = useState('');
  const [addTo, setAddTo] = useState('');
  const [addMaxPrice, setAddMaxPrice] = useState('');
  const [backendConnected, setBackendConnected] = useState(false);
  
  // ─── Check backend connectivity ──────────────────────────────────────
  useEffect(() => {
    apiCall('/api/store/ccus').then(result => {
      if (!result.error) {
        setBackendConnected(true);
        setS
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
parseCSV function · javascript · L18-L49 (32 LOC)
src/components/CSVImport.jsx
function parseCSV(text) {
  const lines = text.trim().split('\n');
  if (lines.length < 2) return { headers: [], rows: [] };
  
  // Handle both comma and tab delimiters
  const delimiter = lines[0].includes('\t') ? '\t' : ',';
  
  const headers = lines[0].split(delimiter).map(h => h.trim().replace(/^"|"$/g, ''));
  const rows = [];
  
  for (let i = 1; i < lines.length; i++) {
    const line = lines[i].trim();
    if (!line) continue;
    
    // Handle quoted fields with commas inside
    const values = [];
    let current = '';
    let inQuotes = false;
    for (const char of line) {
      if (char === '"') { inQuotes = !inQuotes; continue; }
      if (char === delimiter && !inQuotes) { values.push(current.trim()); current = ''; continue; }
      current += char;
    }
    values.push(current.trim());
    
    const row = {};
    headers.forEach((h, idx) => { row[h] = values[idx] || ''; });
    rows.push(row);
  }
  
  return { headers, rows };
}
detectAndNormalize function · javascript · L52-L91 (40 LOC)
src/components/CSVImport.jsx
function detectAndNormalize(headers, rows) {
  const h = headers.map(x => x.toLowerCase());
  
  // Ships CSV: look for "name" + ("package" or "pledge" or "insurance")
  if (h.includes('name') && (h.includes('package') || h.includes('pledge') || h.includes('insurance') || h.includes('packagename'))) {
    const ships = rows.map(r => {
      const name = r[headers[h.indexOf('name')]] || r['Name'] || r['name'] || '';
      const packageName = r['Package'] || r['package'] || r['packageName'] || r['PackageName'] || r['pledge'] || '';
      const insurance = r['Insurance'] || r['insurance'] || r['ins'] || '';
      const customName = r['Custom Name'] || r['customName'] || r['custom_name'] || '';
      const isMeltable = ['true', 'yes', '1', 'y'].includes((r['Meltable'] || r['meltable'] || r['isMeltable'] || '').toLowerCase());
      const isGiftable = ['true', 'yes', '1', 'y'].includes((r['Giftable'] || r['giftable'] || r['isGiftable'] || '').toLowerCase());
      const pledgeId = r['Pledge
CSVImport function · javascript · L94-L232 (139 LOC)
src/components/CSVImport.jsx
export default function CSVImport({ onImportShips, onImportCCUs }) {
  const [dragOver, setDragOver] = useState(false);
  const [results, setResults] = useState([]);
  const [error, setError] = useState('');
  
  const processFile = useCallback((file) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const text = e.target.result;
        const { headers, rows } = parseCSV(text);
        const result = detectAndNormalize(headers, rows);
        
        if (result.type === 'unknown') {
          setError(`Could not detect CSV format for "${file.name}". Expected columns: name+package (ships) or from+to (CCUs).`);
          return;
        }
        
        setResults(prev => [...prev, { fileName: file.name, ...result }]);
        setError('');
        
        if (result.type === 'ships' && onImportShips) {
          onImportShips(result.data);
        } else if (result.type === 'ccus' && onImportCCUs) {
          onImportCCUs(result.data);
     
getShipDims function · javascript · L40-L44 (5 LOC)
src/components/FleetViewer3D.jsx
function getShipDims(name) {
  const d = SHIP_DIMENSIONS[name];
  if (d) return { length: d[0], beam: d[1], height: d[2] };
  return { length: 20, beam: 12, height: 5 }; // fallback
}
getShipColor function · javascript · L46-L48 (3 LOC)
src/components/FleetViewer3D.jsx
function getShipColor(role) {
  return ROLE_SHIP_COLORS[role] || 0x607080;
}
snapToGrid function · javascript · L50-L55 (6 LOC)
src/components/FleetViewer3D.jsx
function snapToGrid(x, z) {
  return {
    x: Math.round(x / CELL_SIZE) * CELL_SIZE,
    z: Math.round(z / CELL_SIZE) * CELL_SIZE,
  };
}
createShipMesh function · javascript · L58-L105 (48 LOC)
src/components/FleetViewer3D.jsx
function createShipMesh(ship, role, position) {
  const dims = getShipDims(ship.name);
  const color = getShipColor(role);
  const group = new THREE.Group();
  group.userData = { shipName: ship.name, role, dims, ship };

  // Main hull — elongated box
  const hullGeo = new THREE.BoxGeometry(dims.beam, dims.height, dims.length);
  const hullMat = new THREE.MeshPhongMaterial({
    color,
    transparent: true,
    opacity: 0.75,
    flatShading: true,
  });
  const hull = new THREE.Mesh(hullGeo, hullMat);
  hull.position.y = dims.height / 2 + 0.5;
  hull.castShadow = true;
  hull.receiveShadow = true;
  group.add(hull);

  // Wing accent — wider, thinner box for fighters/medium ships
  if (dims.length < 100 && dims.beam > 10) {
    const wingGeo = new THREE.BoxGeometry(dims.beam * 1.3, dims.height * 0.3, dims.length * 0.5);
    const wingMat = new THREE.MeshPhongMaterial({ color, transparent: true, opacity: 0.4 });
    const wing = new THREE.Mesh(wingGeo, wingMat);
    wing.position.y = 
createLabel function · javascript · L108-L139 (32 LOC)
src/components/FleetViewer3D.jsx
function createLabel(text, position, dims) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  canvas.width = 256;
  canvas.height = 64;
  
  ctx.fillStyle = 'rgba(6, 10, 16, 0.8)';
  ctx.roundRect(0, 0, 256, 64, 6);
  ctx.fill();
  
  ctx.strokeStyle = 'rgba(0, 200, 255, 0.4)';
  ctx.lineWidth = 1;
  ctx.roundRect(0, 0, 256, 64, 6);
  ctx.stroke();
  
  ctx.font = LABEL_FONT;
  ctx.fillStyle = '#e8f0f8';
  ctx.textAlign = 'center';
  ctx.fillText(text, 128, 28);
  
  ctx.font = '500 10px Rajdhani, sans-serif';
  ctx.fillStyle = '#607080';
  ctx.fillText(`${dims.length}m × ${dims.beam}m`, 128, 48);
  
  const texture = new THREE.CanvasTexture(canvas);
  const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true });
  const sprite = new THREE.Sprite(spriteMat);
  sprite.position.set(position.x, dims.height + 8, position.z);
  sprite.scale.set(24, 6, 1);
  sprite.userData = { isLabel: true };
  return sprite;
}
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
FleetViewer3D function · javascript · L142-L532 (391 LOC)
src/components/FleetViewer3D.jsx
export default function FleetViewer3D({ ships, shipRoles, shipMsrp }) {
  const containerRef = useRef(null);
  const rendererRef = useRef(null);
  const sceneRef = useRef(null);
  const cameraRef = useRef(null);
  const shipsInScene = useRef({});
  const labelsInScene = useRef({});
  const animFrameRef = useRef(null);
  const mouseRef = useRef({ isDown: false, button: -1, x: 0, y: 0, lastX: 0, lastY: 0 });
  const cameraOrbit = useRef({ theta: 0.5, phi: 0.8, radius: 400, target: new THREE.Vector3(0, 0, 0) });
  
  const [selectedShip, setSelectedShip] = useState(null);
  const [showLabels, setShowLabels] = useState(true);
  const [addShipOpen, setAddShipOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const [placedShips, setPlacedShips] = useState(() => {
    // Auto-place fleet ships in a grid
    const unique = [...new Map(ships.map(s => [s.name, s])).values()];
    const sorted = unique.sort((a, b) => {
      const da = getShipDims(a.name), db = getSh
animate function · javascript · L251-L262 (12 LOC)
src/components/FleetViewer3D.jsx
    function animate() {
      animFrameRef.current = requestAnimationFrame(animate);
      
      // Update camera from orbit params
      const o = cameraOrbit.current;
      camera.position.x = o.target.x + o.radius * Math.sin(o.phi) * Math.cos(o.theta);
      camera.position.y = o.target.y + o.radius * Math.cos(o.phi);
      camera.position.z = o.target.z + o.radius * Math.sin(o.phi) * Math.sin(o.theta);
      camera.lookAt(o.target);
      
      renderer.render(scene, camera);
    }
TierBadge function · javascript · L15-L24 (10 LOC)
src/components/GameLoopGuide.jsx
function TierBadge({ tier }) {
  const c = TIER_COLORS[tier] || TIER_COLORS.D;
  return (
    <span style={{
      display: 'inline-block', width: 22, height: 22, lineHeight: '22px',
      borderRadius: 4, textAlign: 'center', fontFamily: "'Orbitron',sans-serif",
      fontSize: 11, fontWeight: 700, background: c.bg, color: c.text,
    }}>{tier}</span>
  );
}
DifficultyBar function · javascript · L26-L42 (17 LOC)
src/components/GameLoopGuide.jsx
function DifficultyBar({ level }) {
  const d = DIFFICULTY_LABELS[level] || DIFFICULTY_LABELS[3];
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
      <div style={{ display: 'flex', gap: 2 }}>
        {[1, 2, 3, 4, 5].map(i => (
          <div key={i} style={{
            width: 16, height: 8, borderRadius: 2,
            background: i <= level ? d.color : 'var(--bg-card)',
            border: `1px solid ${i <= level ? d.color : 'var(--border)'}`,
          }} />
        ))}
      </div>
      <span style={{ fontSize: 12, color: d.color, fontWeight: 600 }}>{d.label}</span>
    </div>
  );
}
GameLoopGuide function · javascript · L44-L196 (153 LOC)
src/components/GameLoopGuide.jsx
export default function GameLoopGuide({ ownedShips }) {
  const [expandedLoop, setExpandedLoop] = useState(null);
  const [filter, setFilter] = useState('all'); // 'all' | 'covered' | 'uncovered'
  
  const owned = ownedShips || new Set();
  
  // Analyze fleet coverage per loop
  const loopAnalysis = useMemo(() => {
    return GAME_LOOPS.map(loop => {
      const allShips = Object.entries(loop.tiers).flatMap(([tier, ships]) => 
        ships.map(s => ({ name: s, tier }))
      );
      const ownedInLoop = allShips.filter(s => owned.has(s.name));
      const bestOwned = ownedInLoop.length > 0 
        ? ownedInLoop.sort((a, b) => 'SABCD'.indexOf(a.tier) - 'SABCD'.indexOf(b.tier))[0] 
        : null;
      
      return {
        ...loop,
        allShips,
        ownedInLoop,
        bestOwned,
        coverage: ownedInLoop.length > 0 ? 'covered' : 'uncovered',
      };
    });
  }, [owned]);
  
  const filtered = filter === 'all' ? loopAnalysis 
    : loopAnalysis.filter(l => l.covera
SaleDashboard function · javascript · L31-L232 (202 LOC)
src/components/SaleDashboard.jsx
export default function SaleDashboard({ chainData, shipMsrp }) {
  const SHIP_MSRP = shipMsrp || {};
  const [saleEntries, setSaleEntries] = useState([]);
  const [newFrom, setNewFrom] = useState('');
  const [newTo, setNewTo] = useState('');
  const [newPrice, setNewPrice] = useState('');
  const [newWarbond, setNewWarbond] = useState(true);
  const [eventName, setEventName] = useState('Current Sale');
  
  const addEntry = () => {
    if (!newFrom || !newTo || !newPrice) return;
    const fromMsrp = SHIP_MSRP[newFrom] || 0;
    const toMsrp = SHIP_MSRP[newTo] || 0;
    const standardCost = toMsrp - fromMsrp;
    const price = parseFloat(newPrice);
    const saving = standardCost - price;
    const savingPct = standardCost > 0 ? Math.round((saving / standardCost) * 100) : 0;
    
    // Determine verdict based on chain impact
    let verdict = 'SKIP';
    let chainImpact = null;
    
    if (chainData && chainData.assignments) {
      for (const chain of chainData.assignments) {
     
ShipBrowser function · javascript · L23-L271 (249 LOC)
src/components/ShipBrowser.jsx
export default function ShipBrowser({ shipMsrp, shipRoles, shipDetails }) {
  const MSRP = shipMsrp || {};
  const ROLES = shipRoles || {};
  const DETAILS = shipDetails || {};

  const [search, setSearch] = useState('');
  const [sortBy, setSortBy] = useState('msrp');
  const [sortDir, setSortDir] = useState(-1);
  const [roleFilter, setRoleFilter] = useState('all');
  const [mfrFilter, setMfrFilter] = useState('all');
  const [statusFilter, setStatusFilter] = useState('all');
  const [availFilter, setAvailFilter] = useState('all');
  const [viewMode, setViewMode] = useState('grid');
  const [wikiData, setWikiData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const cached = sessionStorage.getItem('drydock_wiki_ships');
    if (cached) { try { setWikiData(JSON.parse(cached)); setLoading(false); return; } catch(e) {} }
    fetch(WIKI_API).then(r => r.json()).then(d => {
      const ships = d.data || d;
      setWikiData(ships); setLoading(fa
toSlug function · javascript · L13-L19 (7 LOC)
src/components/ShipImage.jsx
function toSlug(name) {
  return name
    .toLowerCase()
    .replace(/[']/g, '')
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '');
}
Repobility (the analyzer behind this table) · https://repobility.com
loadCache function · javascript · L98-L106 (9 LOC)
src/components/ShipImage.jsx
function loadCache() {
  try {
    const stored = localStorage.getItem(CACHE_KEY);
    if (stored) {
      const parsed = JSON.parse(stored);
      Object.assign(imageCache, parsed);
    }
  } catch (e) {}
}
saveCache function · javascript · L109-L113 (5 LOC)
src/components/ShipImage.jsx
function saveCache() {
  try {
    localStorage.setItem(CACHE_KEY, JSON.stringify(imageCache));
  } catch (e) {}
}
fetchShipImage function · javascript · L116-L144 (29 LOC)
src/components/ShipImage.jsx
async function fetchShipImage(name) {
  if (imageCache[name]) return imageCache[name];
  if (imageCache[name] === null) return null; // Already tried, no image

  const slug = SLUG_OVERRIDES[name] || toSlug(name);
  
  try {
    const response = await fetch(`https://api.fleetyards.net/v1/models/${slug}`);
    if (!response.ok) {
      imageCache[name] = null;
      saveCache();
      return null;
    }
    const data = await response.json();
    const imageUrl = data.media?.storeImage?.mediumUrl || data.media?.storeImage?.url || data.media?.angledView?.mediumUrl 
      || data.media?.angledView?.smallUrl 
      || data.media?.angledView?.url 
      || data.media?.storeImage?.mediumUrl
      || data.storeImage 
      || null;
    imageCache[name] = imageUrl;
    saveCache();
    return imageUrl;
  } catch (err) {
    imageCache[name] = null;
    saveCache();
    return null;
  }
}
ShipImage function · javascript · L154-L222 (69 LOC)
src/components/ShipImage.jsx
export default function ShipImage({ name, size = 80, role, style }) {
  const [imageUrl, setImageUrl] = useState(imageCache[name] || null);
  const [loaded, setLoaded] = useState(false);
  const [failed, setFailed] = useState(imageCache[name] === null);

  useEffect(() => {
    if (imageCache[name]) {
      setImageUrl(imageCache[name]);
      return;
    }
    if (imageCache[name] === null) {
      setFailed(true);
      return;
    }

    let cancelled = false;
    fetchShipImage(name).then(url => {
      if (cancelled) return;
      if (url) setImageUrl(url);
      else setFailed(true);
    });
    return () => { cancelled = true; };
  }, [name]);

  const color = ROLE_COLORS[role] || "#607080";
  const initials = name.split(' ').map(w => w[0]).join('').slice(0, 3);

  // Fallback placeholder
  if (failed || !imageUrl) {
    return (
      <div style={{
        width: size, height: size * 0.6, borderRadius: 4,
        background: `linear-gradient(135deg, ${color}33, ${color}11)`,
  
prefetchShipImages function · javascript · L225-L236 (12 LOC)
src/components/ShipImage.jsx
export async function prefetchShipImages(shipNames) {
  const uncached = shipNames.filter(n => !(n in imageCache));
  // Fetch in batches of 5 to avoid hammering the API
  for (let i = 0; i < uncached.length; i += 5) {
    const batch = uncached.slice(i, i + 5);
    await Promise.all(batch.map(fetchShipImage));
    // Small delay between batches
    if (i + 5 < uncached.length) {
      await new Promise(r => setTimeout(r, 200));
    }
  }
}