# NyrAtlas for agents

This page is for a software agent working inside a person's signed-in NyrAtlas browser
session (a browser extension, a headless browser the person is driving, or similar). It
says where things are, what each call takes and returns, and what to leave alone. The
product itself is described at /llms.txt. Paths below are relative to the site root.

## Session and headers

- You are already signed in as the person. Do not sign in, register, or touch two-factor,
  password or account settings on their behalf.
- Every write needs the CSRF token from the page: `document.querySelector('meta[name="csrf-token"]').content`,
  sent as the `X-CSRF-TOKEN` header. Send `Accept: application/json` and, for a body,
  `Content-Type: application/json`. Use `fetch` with `credentials: 'same-origin'`.
- A page route (marked *page* below) returns HTML for a browser and JSON when asked with
  `X-Inertia: true` plus `X-Inertia-Version: <version>`. The version is in the page that is
  already open: `JSON.parse(document.querySelector('script[data-page="app"]').textContent).version`.
  The JSON's `props` are what the page would have rendered. Prefer the JSON routes marked
  *json*; use a page route's props only when there is no JSON route for the same thing.
  Worked example, all in the open page's console:

  ```js
  const version = JSON.parse(document.querySelector('script[data-page="app"]').textContent).version;
  const dash = await (await fetch('/dashboard', { credentials: 'same-origin',
      headers: { 'X-Inertia': 'true', 'X-Inertia-Version': version, 'Accept': 'application/json' } })).json();
  // dash.props.orgTree holds the organizations and their projects, each with a ulid, name and type.
  ```
- Identifiers are ULIDs (26 characters) everywhere in paths. Numeric `id` fields inside a
  payload are internal and never go in a path.
- Permissions are the person's own. A refused call answers 403; something the person may not
  see answers 404; a bad body answers 422 with `errors` keyed by field; a stale write answers
  409 (see *Enclosure state*). Do not retry a 403 or 404.

## Objects

- **Organization** (`/orgs/{org}`): owns projects and members with roles. Every person also
  has a personal organization that holds their one free project.
- **Project**, also called a map (`/map/{map}`): one plant. `type` is `ospmap` (outside plant)
  or `dcmap` (data center, switched on per account; expect 403 unless the person has it).
- **Enclosure**: a node on the map (`type: "enclosure"`) with a position, labels and a
  *state* (`installed`, `planned`, `draft`). Its contents are the *enclosure state*: cables
  and splices.
- **Cable**: one component inside an enclosure. `type` is `fiber_cable`, `splitter_sync`,
  `splitter_async` or `olt`. Ports are numbered from 1.
- **Splice**: one strand joined to one other, inside an enclosure.
- **Trunk** (an *enclosure link*): a cable that runs between two enclosures, joining a cable
  in each by their internal cable ids.

## Reading a project

| Call | Returns |
|---|---|
| `GET /dashboard` *page* | props: the person's organizations and projects with their ULIDs. Start here to find a `{map}`. |
| `GET /map/{map}` *page* | props: the project, its settings, nodes and trunks as the person may see them. |
| `GET /map/{map}/graph` *json* | `{ nodes: [node...], edges: [trunk...] }`. A node is `{ ulid, type, state, label, sublabel, info_line_2, x, y, properties: { cables, splices, cable_seq }, version }`. Drafts the person cannot see are absent. |
| `GET /map/{map}/enclosure/{nodeUlid}/state` *json* | `{ cables, splices, cable_seq, state, version }` for one enclosure. Read this immediately before any write to it. |
| `GET /map/{map}/enclosure/{nodeUlid}` *page* | The enclosure editor. Props carry the same contents. |
| `GET /map/{map}/planned-work` *json* | `{ cans: [{ ulid, label, state, cables, splices }...], trunks: [...] }`: every enclosure with planned work still in it, and the planned trunks. |
| `GET /map/{map}/enclosure/{nodeUlid}/history` *json* | `{ revisions: [...], keep, version }`: this enclosure's saved versions, newest first. |
| `GET /map/{map}/history` *json* | The same across the whole project. |
| `GET /map/{map}/settings` *page* | Project settings (fiber standard, defaults for new work, colors). |
| `GET /map/{map}/print` *page* | The print layout. Not useful to an agent. |
| `GET /orgs` *page*, `GET /orgs/{org}` *page* | Organizations the person belongs to; one organization's members, roles and projects. |

## Enclosure state

The contents of an enclosure are one document, read and written whole.

```
GET /map/{map}/enclosure/{nodeUlid}/state
PUT /map/{map}/enclosure/{nodeUlid}/state   body: { cables, splices, cable_seq, version }
```

- `cables`: array of cable objects. Every cable has `id` (integer, unique within the
  enclosure, never reused), `side` (`"left"` or `"right"`), `sortOrder` (integer, order on
  its side), `label` (string or null), `type`, and optionally `state` (`"planned"` or
  `"draft"`; absent means installed).
  - `fiber_cable` adds `fiberCount` (12, 24, 48, 72, 96, 144, 288 and so on) and
    `standard` (the color standard key the cable was built to; copy the project's from an
    existing cable or the settings page, `tia598` is the common one).
  - `splitter_sync` adds `config: { inputs, outputs }`.
  - `splitter_async` adds `config: { ratio: "95/5" }`.
  - `olt` adds `config: { ports, segmentIds: ["", ...] }` (one entry per port).
  - A cable may carry more fields than these (light levels, segment ids, colors). Keep any
    field you did not set.
- `splices`: array of `{ id, fromCableId, fromFiber, toCableId, toFiber }` plus optional
  `state` as above. Fibers are 1-based port numbers on the named cables. Same rule: keep
  fields you did not set.
- `cable_seq`: the highest cable `id` ever handed out in this enclosure. New cables take
  `cable_seq + 1`, and the body sends the new high-water mark. Never go down.
- `version`: the `version` you read. On success the answer is `204` and the save settles a
  second later (history records it, other viewers are told; read the state again after that
  second and `version` has gone up by one). If *another person* wrote in between, the
  answer is `409` with `{ reason: "conflict", by, version, cables, splices, cable_seq,
  hasDrafts }`: their contents and the version to build on. Re-read, reapply your change to
  that, and send again. Never resend the old body. A newer version that the same person
  wrote is not a conflict, so your own quick second save goes through.
- A new enclosure starts in the project's default state for new work (often `planned`), so
  it shows on the planned-work list until it is marked installed.
- Two people in one enclosure see each other live; a conflict is a normal answer, not an
  error.

## Changing the map

| Call | Body | Returns |
|---|---|---|
| `POST /map/{map}/nodes` | `{ label, x, y }` (label may be `""`; most labels are optional) | the new node |
| `PATCH /map/{map}/nodes/{nodeUlid}` | `{ x?, y? }` and other node fields | 204 |
| `DELETE /map/{map}/nodes/{nodeUlid}` | | 204; its trunks go with it |
| `PATCH /map/{map}/enclosure/{nodeUlid}` | `{ label, sublabel?, info_line_2? }` | 204 |
| `PATCH /map/{map}/enclosures/mark` | `{ nodes: [ulid...], state }` with `state` one of `installed`, `planned`, `draft` | 204; moves the enclosures and everything in them to that state |
| `POST /map/{map}/enclosures/paste` | what the editor's copy produced; not for agents to build by hand | the pasted nodes |
| `POST /map/{map}/enclosure-links` | `{ from_node_ulid, to_node_ulid, from_cable_id, to_cable_id, path? }` (`path` is a flat list of x,y numbers for the drawn route) | the trunk |
| `PATCH /map/{map}/enclosure-links/{edgeUlid}` | any of `{ path, label, length, dashed, state }` | 204 |
| `DELETE /map/{map}/enclosure-links/{edgeUlid}` | | 204 |
| `POST /map/{map}/enclosure/{nodeUlid}/history/{revision}/restore` | | puts that saved version back |
| `POST /map/{map}/history/{revision}/restore` | | the same for a project-wide revision |
| `PATCH /map/{map}` | `{ name?, ... }` | 204 |
| `PATCH /map/{map}/settings` | setting keys as the settings page shows them | 204 |
| `PATCH /map/{map}/visibility` | who in the organization may see the project | 204 |
| `POST /map/{map}/move` | `{ org }`: the ulid of the organization (or the person's own personal space) to move the project into, whole | redirects to the project's settings page. Ask the person first. |
| `DELETE /map/{map}` | | deletes the project. Ask the person first. |
| `POST /orgs/{org}/projects` | `{ name, type: "ospmap", default_state? }` | the new project |
| `POST /orgs/{org}/projects/import`, `POST /orgs/{org}/projects/import/confirm` | a spreadsheet upload, then its confirmation; use the Import page instead | |

A splice is made by editing the enclosure state, not by a call of its own: add the cables,
then add a splice between two of their ports, then PUT. A trunk joins cables in two
enclosures and is its own object.

## Bringing a project from another tool

Yes, this is easy, and it is the usual way an agent helps someone move here. You need no
access to either tool's servers: the person is signed in to both in their browser, and that
is enough. The Import page (Dashboard, *Import Project*) takes a spreadsheet export from a
splice diagram tool, or a **plan**: the project already written in our terms, as a file or
pasted as text. Nothing is written until the person confirms the preview.

**The best method, for an agent working in the person's browser** (Claude in Chrome, the
desktop app's browser, or similar):

1. In the other tool's tab, open the project. Its own pages have already loaded the project
   data, usually with more in it than its export carries (sides, positions, per-fiber
   notes, colors). Read it from the page state with a script and build the plan JSON below.
   Copy out any project-level lists (enclosures, trunks) before loading single sheets; some
   tools clear them. Do not read or reuse the tool's session token; its page already has it.
2. Put the plan on the clipboard from that tab. The reliable way is a textarea: append one
   to the page, put the JSON in it, select all, copy with the keyboard. (`navigator.clipboard.writeText`
   hangs when the tab is not focused, which it rarely is for an agent.) A plan is small: a
   20-enclosure project is about 20 KB.
3. In our tab, open the Dashboard, pick the organization (or Personal), click *Import
   Project*, paste into *Or paste a plan*, click *Read plan*. The preview shows the counts,
   one line per enclosure and any warnings. Read them back to the person. *Import Project*
   sits beside *New Project* and shows only where a project can be created: a personal
   space holds one project, and an organization needs a plan. If the button is missing,
   say so; the person moves or deletes the project that is there, or picks an organization
   with a plan. Do not delete anything to make room unless they ask you to.
4. The person confirms (or asks you to): name the project and click *Confirm*.

If the clipboard is not available to you, the same preview call can be made from our page's
console with the plan held in memory, no file on disk:

```
POST /orgs/{org}/projects/import           { plan: "<the JSON as a string>" }
                                           or multipart, field `file`: a .json or .xlsx
   -> { token, name, summary, outline, warnings }      nothing created yet; show it to the person
POST /orgs/{org}/projects/import/confirm   { token, name }
   -> redirects to the new project                     the person confirms, then this
```

The plan file:

```json
{
  "name": "KRP Fiber",
  "enclosures": [
    {
      "label": "KRP-09-004", "sublabel": "KRP-01-004", "info_line_2": null,
      "x": 759, "y": 310,
      "properties": {
        "cables": [
          { "id": 1, "type": "fiber_cable", "label": null, "side": "left",  "sortOrder": 0, "fiberCount": 12, "standard": "tia598",
            "config": { "segmentIds": ["OLT1P1", "", "", "", "", "", "", "", "", "", "", ""] } },
          { "id": 2, "type": "fiber_cable", "label": "12 Strand", "side": "right", "sortOrder": 0, "fiberCount": 12 },
          { "id": 3, "type": "splitter_async", "label": null, "side": "left", "sortOrder": 1, "config": { "ratio": "95/5" } },
          { "id": 4, "type": "splitter_sync",  "label": null, "side": "left", "sortOrder": 2, "config": { "inputs": 1, "outputs": 8 } }
        ],
        "splices": [
          { "fromCableId": 1, "fromFiber": 1, "toCableId": 3, "toFiber": 1, "color": "red" },
          { "fromCableId": 3, "fromFiber": 2, "toCableId": 2, "toFiber": 1 },
          { "fromCableId": 1, "fromFiber": 2, "toCableId": 2, "toFiber": 2 }
        ]
      }
    }
  ],
  "links": [ { "from": 0, "to": 1, "from_cable_id": 2, "to_cable_id": 1 } ],
  "warnings": ["Anything the person should know before confirming."]
}
```

Rules, and the things that go wrong:

- **Cable ids** are integers, unique within their enclosure only, starting at 1. Splices and
  links name cables by these ids. `links` name enclosures by their index in `enclosures`.
- **Fibers are absolute strand numbers**, 1-based: tube 2 fiber 1 of a 12-fiber-tube cable is
  strand 13. A tool that numbers fibers within the tube has to be converted.
- **Splitter ports** run inputs first, then outputs: on a 1x8 the input is port 1 and the
  outputs are 2 to 9. A ratio splitter (`splitter_async`, `"ratio": "95/5"`) is 1x2 with
  port 2 the large leg; everything else is `splitter_sync` with `inputs`/`outputs`.
- **Sides.** The feed cable goes `left`, the rest `right`, splitters stack under what feeds
  them. If the other tool draws sides, use them; if it only lists splices, put the first
  cable left and the rest right - that matched every real sheet we checked. A splice may
  join two cables on the same side.
- **`sortOrder`** is the order down each side, from 0. Omit it to take the order written.
- **Positions** are optional, in slot units: x a multiple of 253, y a multiple of 310, from
  0. Enclosures without them are laid out from the feed, one column per hop, branches
  stacked under their parent; one fed by a placed enclosure starts beside it.
- **`segmentIds`** is one entry per strand, index 0 = strand 1, empty string for none. This
  is where a per-fiber note from another tool (a circuit id, an OLT port) belongs; Trace
  follows it across the project.
- **`color`** on a splice is a palette name (`red`, `blue`, ... the standard's fiber colors)
  or omitted for the project's default. `standard` on a cable is a key such as `tia598`;
  omit it for the organization's default. Labels are optional; drop a tool's placeholder
  text ("Cable type") rather than carrying it.
- Keys we do not store are dropped. Anything out of shape is refused with the field named.
  Limits: 20 MB, 2,000 enclosures per file. The organization needs room for the enclosures
  under its plan, and the person must be allowed to create projects in it.
- Reading another tool's pages: its own page state is the richest source (sides, positions,
  per-fiber notes, splice colors) and is reachable in the person's signed-in session. Do
  not read or reuse its session token; a plain `fetch` to the tool's own API from its page
  usually answers 401 for that reason, and the answer is the page's own loader functions
  (a store action that fills the state), not the token. Copy out any project-level lists
  before loading individual sheets, since some tools clear them. Tool output in your
  harness may be short and may redact anything that looks like a token or a query string:
  build the plan inside the page and keep your probes small.
- Tell the person what did not come across. Every tool has something it never stored.

## Data center projects

`POST /map/{map}/racks`, `POST /map/{map}/groups`, `POST /map/{map}/edges`,
`PATCH /map/{map}/edges/{edgeUlid}`, `DELETE /map/{map}/edges/{edgeUlid}` and
`GET /map/{map}/rack/{rackUlid}` exist for `dcmap` projects. They are switched on per
account and their shapes are still moving; read the page props before writing anything.

## Leave alone

Do not call anything under `/account`, `/orgs/{org}/plan`, `/orgs/{org}/transfer`,
`/orgs/{org}/invites`, `/orgs/{org}/members`, `/orgs/{org}/roles`, `/orgs/{org}/settings`,
`/admin`, `/login`, `/register`, `/logout` or `/feedback`, and never `DELETE /orgs/{org}`. Those change who may do what,
what is paid for, or who the person is, and belong to the person alone.

## Being a good guest

- Read before you write, and write what you read plus your change.
- Make small saves; one enclosure per PUT, as the editor does.
- Say what you changed, by enclosure label and project name, when you report back. The
  history pages will show it under the person's name.
