# Web Clients — Local Brokers, Host Order, and `map.clients-to-backends`

This note explains how per-broker web-client port ranges are described in the
directory, how the **host order / host index** is derived from the directory
(no `hosts.txt` file is involved), and how `ClientsToPortsGenerator` produces the
`map.clients-to-backends` file consumed by a reverse proxy.

It is written against the broker example below and the code in
`com.goldencode.p2j.main.{BrokerDiscovery, Broker, ClientsToPortsGenerator,
HostsManager, WebClientsManager}`.

## 0. Two modes

`ClientsToPortsGenerator.generate(String host)` picks one of two modes based on
whether the directory's `brokers` container has any broker nodes:

| Mode | Condition | Hosts | Host index |
|------|-----------|-------|------------|
| **Broker mode** | `brokers` container is **non-empty** | one entry per ports-restricted broker | the broker's **position in the directory order** (loopback = 1, brokers = 2, 3, …) |
| **Local mode**  | `brokers` container is **empty** | a single host | always **1** |

In **local mode** the single host is the `host` argument passed to `generate`
(the actual representer of the local server, e.g. its LAN address
`192.168.100.3`), or `localhost` when no host is supplied. **No `hosts.txt` input
is required in either mode** — the host order comes entirely from the directory
(broker mode) or from the single host argument (local mode).

> **Host order is directory-based.** The host index is *not* stored in a file and
> is *not* an attribute on any node. In broker mode it is the enumeration order
> of the broker nodes under the `brokers` container; the FWD server's loopback is
> index `1` and brokers are `2, 3, …`. Because the generator and the running
> server both derive the index from that same ordered list, their indices agree
> by construction.

## 1. The directory data (broker mode)

A broker is a node under the `brokers` container (which itself lives under
`/server/<serverId>/`, e.g. `/server/default/`):

```xml
<node class="container" name="brokers">
  <node class="broker" name="broker1">
    <node-attribute name="account" value="sbi"/>
    <node-attribute name="broker_account" value="broker1_process"/>
    <node-attribute name="host" value="192.168.100.28"/>
    <node class="container" name="portsRange">
      <node class="string" name="namePrefix">
        <node-attribute name="value" value="client"/>
      </node>
      <node class="integer" name="from">
        <node-attribute name="value" value="7449"/>
      </node>
      <node class="integer" name="to">
        <node-attribute name="value" value="7459"/>
      </node>
    </node>
  </node>
</node>
```

The `broker` object-class (see `dir_schema.xml`) is **non-leaf** and carries:

| Attribute        | Mandatory | Multiple | Meaning |
|------------------|-----------|----------|---------|
| `host`           | yes       | no       | The host name / IP address on which this broker's client (agent) runs. This is the broker's **identity for host ordering**. |
| `broker_account` | yes       | no       | The FWD process account the broker client must authenticate as to register with the server. |
| `account`        | no        | yes      | The list of FWD user accounts served by this broker. |

The child `portsRange` container holds the **web-client port range** for this
broker:

| Node                  | Meaning |
|-----------------------|---------|
| `portsRange/from`     | First web-client port (inclusive). |
| `portsRange/to`       | Last web-client port (inclusive). |
| `portsRange/namePrefix` | **Not read per broker** — see the caveat in §6. |

In the example, `broker1` runs on `192.168.100.28` and owns the web-client port
range `7449..7459` (11 ports).

### How the directory data is read

`BrokerDiscovery.readBrokerConfigs(ds)`:

1. Locates the `brokers` container via `Utils.findDirectoryNodePath(ds,
   "brokers", "container", false)`.
2. Enumerates the broker child nodes with `ds.enumerateNodes(path)` and, **in
   that enumeration order**, builds a `LinkedHashMap<String, Broker>` (broker
   name → `Broker`). **This ordered map is what defines the host order** — see
   §2.
3. For each broker node, `getBroker(...)` reads the `account` / `broker_account`
   / `host` attributes, then reads the two port-range values **relative to the
   broker node** using `ConfigItem.RELATIVE_TO_PARENT`:

   ```java
   int from = ConfigItem.<Integer>createConfigItemOfType(
                 ConfigItem.CLIENT_PORT_RANGE_FROM, Type.RELATIVE_TO_PARENT)
             .setContainerNodeSearchPath("brokers/broker1/")   // "brokers/<name>/"
             .read(0);
   // ... same for CLIENT_PORT_RANGE_TO ("portsRange/to")
   ```

   `CLIENT_PORT_RANGE_FROM`/`_TO` have node name `portsRange/from` /
   `portsRange/to`, so the resolved directory path is
   `brokers/broker1/portsRange/from` and `…/to`.

4. The resulting `Broker` reports `isPortsRestricted()` as
   `(to > from) && (from > 0)`. For `broker1` that is
   `(7459 > 7449) && (7449 > 0)` → **true**. A broker whose range is absent or
   invalid is **not** ports-restricted: it still occupies a host index (§2) but
   produces no lines in the map.

## 2. Host order — derived from the broker enumeration order

There is no host-index file and no stored index attribute. In broker mode the
host index is the **position of a broker in the ordered broker list**:

* Index **1** is reserved for the FWD server's own loopback host
  (`localhost` / `127.0.0.1`). The server is always "host 1".
* **Every** broker node consumes the next index in the order it appears under the
  `brokers` container — the first broker → `2`, the second → `3`, and so on. This
  is true even for a broker that is not ports-restricted: it still takes its slot
  so that the numbering stays aligned with the runtime, which registers one host
  per broker regardless of its port configuration (§7).

So for the example (one broker, `broker1` on `192.168.100.28`):

```
index 1 : localhost / 127.0.0.1   (the FWD server)
index 2 : 192.168.100.28          (broker1, first broker node)
```

Add a `broker2` after `broker1` and it becomes index `3`, etc. The ordering is
the directory enumeration order — the same order `BrokerDiscovery` returns and
the same "well-defined host order" the comment in `BrokerManager.registerBroker`
refers to. **To renumber a host, reorder the broker nodes** in the directory.

Each broker declares a unique `host` (mandatory, single-valued), and
`BrokerManager` rejects a second broker that resolves to a host already claimed,
so the host → index mapping is one-to-one.

> **Design decision — count every broker.** The host index advances for *every*
> broker node, including brokers that are not ports-restricted, rather than only
> for the brokers that emit lines. This is deliberate, for consistency with the
> runtime: `BrokerManager.registerBroker` registers one host per broker
> (`HostsManager.addHost`) regardless of its port configuration, so a
> non-ports-restricted broker still consumes a host slot at runtime. Counting it
> in the generator too keeps the offline indices and the runtime indices in
> lockstep. The trade-off is that the position of *all* broker nodes affects the
> numbering — adding, removing, or reordering any broker shifts the indices of
> the brokers after it.

## 3. Mode selection in `generate(String host)`

```
generate(host):
  brokers = BrokerDiscovery.readBrokerConfigs(ds)
  if (!brokers.isEmpty()):
      → generateWithBrokerRanges(brokers, fileName)   # broker mode (directory-ordered)
  else:
      → local mode: single host = host or "localhost", at index 1
```

The switch is simply **whether any broker node exists**. (Note this differs from
an earlier draft that switched on "any broker is ports-restricted"; the mode is
now decided purely by the presence of broker nodes.)

## 4. Broker mode — `generateWithBrokerRanges`

The generator walks the brokers **in directory order**, advancing the host index
for every broker (starting at `1` for the server loopback, so the first broker
host is `2`), and emits lines only for ports-restricted brokers:

```
prefix    = resolvePrefix(...)            # see §6
hostIndex = 1                             # 1 == server loopback (localhost)

for (broker : brokers.values()) {         # directory enumeration order
   hostIndex++;                           # every broker takes a slot: 2, 3, ...
   if (!broker.isPortsRestricted()) continue;   # but only restricted ones emit lines

   for (port = broker.getFrom(); port <= broker.getTo(); port++) {
      name = getPortName(prefix, broker.getFrom(), hostIndex, port);
      write(name + "    " + broker.getHost() + ":" + port);
   }
}
```

The output file is `map.clients-to-backends` by default (the user may enter
another name at the prompt). Because the index comes from the broker order, there
is no `hosts.txt` lookup, creation, or write-back.

### Port name format

`getPortName` (shared by the generator and the runtime) is:

```
«prefix»-«hostIndex»-«port - from + 1»
```

i.e. `prefix`, the **host index**, and the **1-based offset of the port within
the range**, joined by hyphens.

## 5. Local mode — single host

When `brokers` is empty, the generator uses the **global**
`webClient/portsRange` (`from` / `to`, prompting and persisting them in the
directory if absent, exactly as before) and maps a **single host at index 1**:

```
localHost = (host == null || host.isEmpty()) ? "localhost" : host
for (port = from; port <= to; port++) {
   write(getPortName(prefix, from, 1, port) + "    " + localHost + ":" + port)
}
```

`host` is the `generate(...)` argument — supplied on the command line as the
optional second parameter (see §8). Use it to name the local server by the
address a reverse proxy must reach (e.g. `192.168.100.3`); omit it to fall back
to `localhost`.

## 6. Caveat — `namePrefix` under the broker node is ignored

`resolvePrefix(...)` reads `ConfigItem.PROXY_CLIENT_NAME_PREFIX`, which is a
`Type.WEB_CLIENT` item with node name `portsRange/namePrefix` — i.e. it resolves
to the **global** `webClient/portsRange/namePrefix`, *not* the
`portsRange/namePrefix` placed under the broker node.

Consequences:

* The `<node class="string" name="namePrefix" value="client"/>` inside
  `broker1/portsRange` in the example is **not consulted** by the generator.
* The prefix comes from `webClient/portsRange/namePrefix`; if that is empty the
  generator prompts for it (interactive) and defaults to `client` if nothing is
  entered.
* Only `portsRange/from` and `portsRange/to` are read per broker
  (`BrokerDiscovery` does not read a per-broker `namePrefix`).

If a per-broker prefix is intended, it would require reading
`brokers/<name>/portsRange/namePrefix` via a `RELATIVE_TO_PARENT` item in
`BrokerDiscovery`/`generateWithBrokerRanges`; today it is a single global value.

## 7. Worked examples

### Broker mode

Directory: a single ports-restricted broker `broker1` →
`host=192.168.100.28`, `from=7449`, `to=7459`, with `prefix=client`.

`broker1` is the first broker node, so its host order is `2` (index `1` is the
server loopback). The generator emits 11 lines:

```
client-2-1     192.168.100.28:7449
client-2-2     192.168.100.28:7450
client-2-3     192.168.100.28:7451
client-2-4     192.168.100.28:7452
client-2-5     192.168.100.28:7453
client-2-6     192.168.100.28:7454
client-2-7     192.168.100.28:7455
client-2-8     192.168.100.28:7456
client-2-9     192.168.100.28:7457
client-2-10    192.168.100.28:7458
client-2-11    192.168.100.28:7459
```

The left column is the proxy **route name**; the right column is the **backend**
the reverse proxy forwards that route to. If a `broker2` were added next, it
would produce `client-3-*` lines for its own host and range.

### Local mode

No brokers configured, global `webClient/portsRange` = `7449..7459`,
`prefix=client`, and the generator invoked with host `192.168.100.3`. The single
host is index `1`:

```
client-1-1     192.168.100.3:7449
client-1-2     192.168.100.3:7450
...
client-1-11    192.168.100.3:7459
```

Invoked with no host argument, the backend column would read `localhost:74xx`.

## 8. Command line

```
java com.goldencode.p2j.main.ClientsToPortsGenerator <directory.xml> [host]
```

* `<directory.xml>` — required; the directory the brokers / webClient config is
  read from.
* `[host]` — optional; the local-server host for **local mode** (defaults to
  `localhost`). **Ignored in broker mode**, where hosts come from the broker
  nodes. There is no `hosts.txt` argument.

## 9. The runtime side — the same order, no second source of truth

The generated `map.clients-to-backends` only routes correctly if the **host
index the proxy was configured with equals the host index the running server
uses**. Both sides derive that index from the **same directory data**, so they
agree by construction:

* `HostsManager` pins the server loopback at construction: `localhost → 1`,
  `127.0.0.1 → 1`, and the host counter starts at `1`.
* As brokers register (`BrokerManager.registerBroker` → `HostsManager.addHost`),
  each host is assigned `broker.getIndex() + 1` — i.e. its **directory position**
  (`BrokerDiscovery` sets `index = i + 1`), not a running registration-time
  counter. Keying the index off the directory position is what keeps the runtime
  aligned with the generator regardless of the order in which brokers actually
  register, or whether some brokers are skipped.
* `HostsManager.getPortName(prefix, from, host, port)` looks up that host's index
  directly by the `host` literal (`hosts.get(host)`) — **no DNS resolution at
  route-name time** — and calls the same `ClientsToPortsGenerator.getPortName(...)`.
  `WebClientsManager` uses this to build the proxy web-root for a web client.

```
 directory `brokers` order ──► host index ─┬─► generator: "client-2-7"  (map.clients-to-backends)
                                           └─► runtime  : "client-2-7"  (proxy web-root)
```

* **Local mode / single-host**: the host is index `1` on both sides; nothing to
  diverge.
* **Broker mode / multiple hosts**: the index is the broker's position in the
  directory order on both sides. Reordering the broker nodes (or the order in
  which brokers are expected to register) changes the index consistently for the
  generator and the server, so the proxy map and the runtime stay aligned without
  a flat file.

### Host literal form

A broker's `host` attribute is used verbatim as its identity. The running server
keys the host index by that exact literal: `HostsManager.addHost` stores the
index under the registered `host` string, and `getPortName` looks it up by the
same string (`hosts.get(host)`) — there is **no DNS resolution at route-name
time**. The host registered (`BrokerManager.registerBroker`) and the host used to
build the route name (`WebClientsManager`) both originate from the broker's
`host` config value, so they match by construction. Keep the `host` literal in a
single, consistent form across the directory and any proxy configuration derived
from these route names — a different spelling of the same address (e.g. a DNS
name vs. its IP) is treated as a distinct host because the lookup is now purely
string-keyed.

## 10. Summary

1. `generate(host)` runs **broker mode** when the `brokers` container has any
   broker nodes, **local mode** otherwise. Neither mode reads a `hosts.txt` file.
2. **Broker mode**: each broker node declares its `host`, `broker_account`, and a
   `portsRange/{from,to}`. The **host index is the broker's position in the
   directory order** — loopback `1`, brokers `2, 3, …`, counting every broker.
   Ports-restricted brokers emit `«prefix»-«hostIndex»-«offset»  «host»:«port»`
   lines into `map.clients-to-backends`.
3. **Local mode**: a single host (the `host` argument, or `localhost`) is mapped
   at index `1` using the global `webClient/portsRange`.
4. The running server (`HostsManager`) assigns indices from the same ordered
   broker registrations (loopback pinned to `1`), so the offline-generated proxy
   map and the runtime web-roots agree by construction.
