> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiroleague.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Network topology

> Local and Remote setups

export const DiagramViewer = ({children, title = "Diagram"}) => {
  const dialogRef = useRef(null);
  const viewportRef = useRef(null);
  const contentRef = useRef(null);
  const [isOpen, setIsOpen] = useState(false);
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({
    x: 0,
    y: 0
  });
  const zoomRef = useRef(1);
  const panRef = useRef({
    x: 0,
    y: 0
  });
  const dragRef = useRef(null);
  const open = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(true);
  };
  const close = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(false);
  };
  const resetView = () => {
    fitDiagramToViewport();
    setPan({
      x: 0,
      y: 0
    });
  };
  const setView = (nextZoom, nextPan) => {
    zoomRef.current = nextZoom;
    panRef.current = nextPan;
    setZoom(nextZoom);
    setPan(nextPan);
  };
  const MaximizeIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5" />
    </svg>;
  const ResetIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
      <path d="M3 3v5h5" />
    </svg>;
  const CloseIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M18 6 6 18M6 6l12 12" />
    </svg>;
  const fitDiagramToViewport = () => {
    const viewport = viewportRef.current;
    const content = contentRef.current;
    const svg = content?.querySelector(".mermaid svg[viewBox]");
    if (!viewport || !svg) return;
    const viewBox = svg.getAttribute("viewBox")?.split(/\s+/).map(Number);
    if (!viewBox || viewBox.length !== 4 || !viewBox[2] || !viewBox[3]) return;
    const [, , width, height] = viewBox;
    svg.style.width = `${width}px`;
    svg.style.maxWidth = "none";
    const rect = viewport.getBoundingClientRect();
    const nextZoom = Math.min(2.2, Math.max(0.75, Number((Math.min((rect.width - 96) / width, (rect.height - 96) / height) * 0.98).toFixed(3))));
    setView(nextZoom, {
      x: 0,
      y: 0
    });
  };
  const handleWheel = event => {
    if (!isOpen) return;
    event.preventDefault();
    event.stopPropagation();
    const viewport = viewportRef.current;
    if (!viewport) return;
    const rect = viewport.getBoundingClientRect();
    const pointerX = event.clientX - rect.left - rect.width / 2;
    const pointerY = event.clientY - rect.top - rect.height / 2;
    const currentZoom = zoomRef.current;
    const currentPan = panRef.current;
    const nextZoom = Math.max(0.25, Math.min(5, Number((currentZoom * (event.deltaY < 0 ? 1.12 : 0.88)).toFixed(3))));
    const ratio = nextZoom / currentZoom;
    setView(nextZoom, {
      x: pointerX - (pointerX - currentPan.x) * ratio,
      y: pointerY - (pointerY - currentPan.y) * ratio
    });
  };
  const onPointerDown = event => {
    if (!isOpen || event.button !== 0) return;
    event.preventDefault();
    dragRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      pan: panRef.current
    };
    try {
      event.currentTarget.setPointerCapture?.(event.pointerId);
    } catch {}
  };
  const onPointerMove = event => {
    const drag = dragRef.current;
    if (!isOpen || !drag || drag.pointerId !== event.pointerId) return;
    event.preventDefault();
    const nextPan = {
      x: drag.pan.x + event.clientX - drag.startX,
      y: drag.pan.y + event.clientY - drag.startY
    };
    panRef.current = nextPan;
    setPan(nextPan);
  };
  const endDrag = event => {
    if (dragRef.current?.pointerId === event.pointerId) {
      dragRef.current = null;
    }
  };
  useEffect(() => {
    const dialog = dialogRef.current;
    if (isOpen && dialog && !dialog.open) {
      dialog.showModal();
      requestAnimationFrame(() => {
        requestAnimationFrame(fitDiagramToViewport);
      });
    }
    if (!isOpen && dialog?.open) {
      dialog.close();
    }
  }, [isOpen]);
  useEffect(() => {
    if (!isOpen) return;
    const onKeyDown = event => {
      if (event.key === "Escape") close();
    };
    const onWheel = event => {
      event.preventDefault();
      event.stopPropagation();
      if (viewportRef.current?.contains(event.target)) {
        handleWheel(event);
      }
    };
    const previousOverflow = document.body.style.overflow;
    const previousHtmlOverflow = document.documentElement.style.overflow;
    document.body.style.overflow = "hidden";
    document.documentElement.style.overflow = "hidden";
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("wheel", onWheel, {
      capture: true,
      passive: false
    });
    return () => {
      document.body.style.overflow = previousOverflow;
      document.documentElement.style.overflow = previousHtmlOverflow;
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("wheel", onWheel, {
        capture: true
      });
    };
  }, [isOpen]);
  const toolbar = <div className="diagram-viewer__bar">
      <span className="diagram-viewer__title">{title}</span>
      <div className="diagram-viewer__actions">
        {isOpen && <span className="diagram-viewer__zoom" aria-live="polite">
            {Math.round(zoom * 100)}%
          </span>}
        {isOpen && <button className="diagram-viewer__button" type="button" onClick={resetView} aria-label="Reset diagram">
            <ResetIcon />
          </button>}
        <button className="diagram-viewer__button" type="button" onClick={isOpen ? close : open} aria-label={isOpen ? "Close" : "Maximize"}>
          {isOpen ? <CloseIcon /> : <MaximizeIcon />}
        </button>
      </div>
    </div>;
  const diagram = <div ref={viewportRef} className="diagram-viewer__viewport" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={endDrag} onPointerCancel={endDrag}>
      <div className="diagram-viewer__stage" style={{
    transform: isOpen ? `translate(${pan.x}px, ${pan.y}px)` : undefined
  }}>
        <div className="diagram-viewer__centerer">
          <div ref={contentRef} className="diagram-viewer__content" style={{
    transform: isOpen ? `scale(${zoom})` : "none"
  }}>
            {children}
          </div>
        </div>
      </div>
    </div>;
  return <>
      {!isOpen && <div className="diagram-viewer not-prose">
          {toolbar}
          {diagram}
        </div>}

      <dialog ref={dialogRef} className="diagram-viewer-dialog" onWheel={event => {
    if (event.target === event.currentTarget) {
      event.preventDefault();
      event.stopPropagation();
    }
  }} onCancel={event => {
    event.preventDefault();
    close();
  }} onClose={() => setIsOpen(false)}>
        {isOpen && <div className="diagram-viewer diagram-viewer--open not-prose">
            {toolbar}
            {diagram}
          </div>}
      </dialog>
    </>;
};

Hiro League Architecture offers users the flexibility to run different setups.

1. Local Only: Everything lives on the same machine - Hiro Server, Hiro Gate, Control Room.
2. Local and Network: Allows access to Hiro Server on the local network only.
3. Remote: Secure online access to Hiro Server from the internet.

***

## Local / Network Setup

To run a local only setup:

* Install and configure both **Hiro Server** and **Hiro Gate** on the same machine.
* Configure **Hiro Gate** to listen on the localhost address.
* Run **Control Room** or **Hiro Web** app on the same machine.

To run on the Local Network:

* Install and configure both **Hiro Server** and **Hiro Gate** on the same machine (recommended).
* If you have a reason to install **Hiro Gate** on a different machine on the local network, you can do so.
* Configure **Hiro Gate** to listen on the local network address.
* Run **Control Room** on the same machine.
* Run any Hiro Node app on the local network.

<DiagramViewer title="Local / Network topology">
  ```mermaid actions={false} theme={null}
  ---
  config:
    layout: dagre
  ---
  flowchart LR
   subgraph LAN_ONLY["Local / network only"]
      direction LR
   subgraph Instance["Instance"]
          Gateway["Gateway"]
    end

   subgraph HiroGate["**Hiro Gate**"]
      direction TB
        Instance
    end
   subgraph HiroServer["Hiro Server"]
      direction TB
          Workspace
    end
   subgraph Workspace["Workspace"]
          direction LR
          AIAgents["Hiro <br> Agents"]
          ControlRoom["Control<br>Room"]
    end
   end

   subgraph Nodes["**Nodes**"]
      direction TB
          Mobile["`**Hiro Mobile**<br>iOS / Android`"]
          Web["Hiro Web"]
          Desktop["`**Hiro Desktop**<br>Win / Mac / *nix`"]
    end

   subgraph ThirdPartyApps["Third party<br>apps"]
      direction TB
          WhatsApp["WhatsApp"]
          Telegram["Telegram"]
    end
      Mobile -- WS --> Gateway
      Web -- WS --> Gateway
      Desktop -- WS --> Gateway
      Gateway <-- Device Plugin --> Workspace
      HiroServer <-- WP Plugin --> WhatsApp
      HiroServer <-- TG Plugin --> Telegram

      style LAN_ONLY stroke-dasharray: 5 5,fill:transparent
      style ThirdPartyApps stroke-dasharray: 5 5,fill:transparent
      style Nodes stroke-dasharray: 5 5,fill:transparent
  ```
</DiagramViewer>

***

## Remote / Internet Setup

To run a remote setup:

* Install and configure **Hiro Server** on your **Root Node** - your desktop, macMini, etc...
* Install and configure **Hiro Gate** on a VPS.
* Configure **Hiro Gate** to listen on the public internet address.
* Run **Control Room** on your **Root Node**.
* Run any Hiro Node app on the internet.

<DiagramViewer title="Remote / Internet topology">
  ```mermaid actions={false} theme={null}
  ---
  config:
    layout: dagre
  ---
  flowchart LR
   subgraph VPS["**VPS/Cloud**"]
      direction LR
   subgraph Instance["Instance"]
          Gateway["Gateway"]
    end

   subgraph HiroGate["**Hiro Gate**"]
      direction TB
        Instance
    end
   end

   subgraph Local["**Local/Desktop**"]
      direction LR
   subgraph HiroServer["Hiro Server"]
      direction TB
          Workspace
    end
   subgraph Workspace["Workspace"]
          direction LR
          AIAgents["Hiro <br> Agents"]
          ControlRoom["Control<br>Room"]
    end
   end

   subgraph Nodes["**Internet**"]
      direction TB
          Mobile["`**Hiro Mobile**<br>iOS / Android`"]
          Web["Hiro Web"]
          Desktop["`**Hiro Desktop**<br>Win / Mac / *nix`"]
    end

   subgraph ThirdPartyApps["Third party<br>apps"]
      direction TB
          WhatsApp["WhatsApp"]
          Telegram["Telegram"]
    end
      Mobile -- WS --> Gateway
      Web -- WS --> Gateway
      Desktop -- WS --> Gateway
      Gateway <-- Device Plugin --> Workspace
      HiroServer <-- WP Plugin --> WhatsApp
      HiroServer <-- TG Plugin --> Telegram

      style VPS stroke-dasharray: 5 5,fill:transparent
      style Local stroke-dasharray: 5 5,fill:transparent
      style ThirdPartyApps stroke-dasharray: 5 5,fill:transparent
      style Nodes stroke-dasharray: 5 5,fill:transparent
  ```
</DiagramViewer>

## VPN Setup

Hiro League may work with VPN, when configured as a Local/Network only setup. It needs to be tested.
