Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Changelog

## [Unreleased]

### Changed

- **The README and `package.json` now say Node.js 18 or newer, not 16.** They said 16 or newer, but the build step runs esbuild, which asks for Node 18, and the CI runs 18 and 20: they now say what is true.

## [2.12.0] — 2026-10-29

The Verify run looks at virtual machines, a VM's state is the colour of its icon, and Undo and Redo keep what you did.

### Added

- **The Verify run looks at virtual machines.** It asks each VM's IPv4 addresses and, with the signals it already collects for devices (the sweep's ARP, the routers' ARP and ND, the switches' MAC tables), writes what the network says next to the state you declared: "seen on the network", "no answer" (which does not prove it is off) or "no longer on the local network" (the address does not even answer ARP). When the network contradicts you the VM gets a yellow mark in its sheet and in the host's list; VMs stay out of the red categories, a stopped VM is not an anomaly.
- **"Follow VM state" in the Automations menu, off by default.** The Verify run sets *running* on a VM that answers and *stopped* on one that has left the local network, only where the state is empty or the run wrote it; a state you set by hand is never touched, and the batch is one Undo step.

### Changed

- **A VM's state is the colour of the icon next to its name, and a small on/off switch changes it.** Green is running, red stopped, yellow attention (the network contradicts you) and grey not specified, with the switch half-way until you move it.

### Fixed

- **Opening a project no longer drops its LAG markers.** The load-time check that strips the marker from a cable to a passive element read the project that was open before, so every end looked inactive and the markers of every project were dropped; it now reads the project being opened.
- **The "Routed by" list on a VLAN no longer offers "Custom…".** It wrote a placeholder as the gateway device, and the card then said the device no longer exists.
- **Panning, zooming or switching rack tab no longer breaks Undo and Redo, and Redo no longer overwrites a change made after an undo.** The view is not part of what Ctrl+Z restores, and a field edited or an automatic update that landed in between is kept.
- **Nothing is recorded when nothing happened.** The monitor's silent rounds and a drag that ends where it started, outside the plan or with a refused VM import no longer leave an Undo step, and Undo and Redo do less work.
- **A refused cable no longer deletes the automatic cables on its ports, and an accepted one can be undone.**
- **Ctrl+Z works after Enter in the rack height and date fields, and undoing a JSON import brings back the background and the history from before it.**
- **"Is this machine already a VM?" is one question, asked with the whole row.** The Verify adoption, the LLDP/CDP neighbour, the drop onto a host and the crawl each looked at part of it: a VM known by its address or name came back as a second tile, and a host behind a virtual gateway was taken for the gateway. A name alone no longer discards a physical machine, and importing rows that are all VMs no longer lights Undo or Save.
- **"Is this device polled over SNMP?" is one question.** About a dozen places decided it and two disagreed: a driver written in capitals (`SNMP-V2C`) counted as no SNMP in the sidebar, the Sync, the Verify and the Security view while the API listed it as managed, and an address of only spaces made the API fall back to the device's IP.
- **The AI assistant no longer calls a documented MAC "invented"** when it is written the Cisco way or without separators.
- **A host found over TCP is no longer recorded as found by ping.** The sweep reports `tcp:<port>`, which matched no method.
- **The tooltips on the title row of the Properties panel are no longer cut in half.** They opened above their element, past the top edge of the panel; they now open below.
- **The VM card is tidier.** A new VM opens with all its sections, the back button no longer sticks out of a narrow panel, the closed-section summaries read right, and the "silent to this key" badge says "SNMP: no response" to a screen reader.
- **The expand-all and collapse-all arrows of every properties panel are real buttons with an accessible name.**

### Security

- **The four vulnerabilities `npm audit` reported in the production dependencies are gone.** Denials of service in the request-body parser, the query-string parser and, through the rate limiter, the IP address library. The lockfile — what `npm ci` and the Docker image install — resolves the patched releases and catches up with the versions the tests ran on (net-snmp 3.26 → 3.29). Run `npm ci` and restart the server.
- **The backup the user store leaves beside `users.json` is no longer one `git add -A` away from being published.** It holds the administrators' earlier password hashes; `.gitignore` covered `users.json` but not its neighbours, and now covers them.

## [2.11.19] — 2026-10-23

The dossier lives inside Export PDF, the catalogue update works again, and the manuals describe the interface as it is.

### Changed

- **The handover dossier is part of Export PDF, not a menu entry of its own.** The Export menu loses "Handoff dossier"; the Export PDF dialog opens with a locked, always-on row for it — the cover page and the change history — and every section below it starts unticked, so the PDF carries exactly what you tick on top of that frame. "Deselect all" can now really empty the list. The downloaded file is always named `<project>-dossier.pdf`.
- **The README is shorter and its screenshots show the current interface.** The release notes live in this file.

### Fixed

- **The hardware catalogue update no longer stops when the source library grows.** The check refused any update in which the number of excluded models moved by more than 500; when the NetBox library added about six thousand camera models, the update — and "Check for updates" with it — failed for good while the catalogue in use was almost unchanged. It now counts the models it already knew that change fate and ignores new ones. The limit is 500 and `--max-runtime-change=N` raises it by hand (`--max-exclusion-delta` is gone). The catalogue in use moves from 5,296 to 5,502 models.
- **The manuals describe the toolbar and the status bar as they are.** Both still drew a toolbar without Multi-site, import/export and History, and said the centre of the status bar suggests the next step; it has not since August. Both PDFs are rebuilt.

## [2.11.18] — 2026-10-17

A machine you documented stays documented, Ctrl+Z does what it says, and the VM card closes like the host panel.

### Added

- **The VM card has the host panel's arrows and summaries.** Expand all and collapse all sit in the header; a closed section says on the right what it holds — role and OS; vCPU, RAM and disk; hostname; address and MAC; the SNMP outcome; owner and criticality, or "empty" in grey when nothing is declared — and the card remembers which sections you closed instead of reopening them at the next field you write. On a narrow panel the header's buttons wrap under the title instead of running off the edge.

### Fixed

- **Ctrl+Z undoes one operation at a time, and Ctrl+Y gets you back.** On a freshly loaded project the first operation could not be undone, the first Ctrl+Z skipped one, and redo never reached the last state — undo then redo lost the last operation for good. Ctrl+Shift+Z never worked on a real keyboard, and Ctrl+Z was blocked inside text fields, where it now undoes the typing. Moving a tile, a rack icon or a room is one undoable step. Edits made in the panel fields are not recorded yet.
- **A machine already documented as a VM is no longer "New".** Discover, the topology crawl and the Verify adoption looked only at devices and their ports, so a VM came back as a new tile next to itself and the IPAM audit reported one IP on two devices. Discover now says "Already present", with the VM and its host in the tooltip; the crawl and the Verify adoption have no such column, so they skip those rows and say how many in the result line ("N already documented as a VM"). Dropping a tile whose MAC (or IP, with no contradicting MAC) is already a VM of any host is refused, with a notice that names the other one; a different MAC on the same IP is another machine and stays a real conflict.

### Changed

- **The "silent to this key" badge on a Discover row reads "SNMP"**, with the muted speaker and the same tooltip. The filter above the table keeps its name.

## [2.11.17] — 2026-10-16

A neighbour is created as what it says it is, a hostile id can no longer run code, and the discovery tells you when it could not look.

### Added

- **A neighbour seen over LLDP or CDP is created as what it announces.** The poll now reads the platform and the capabilities each neighbour advertises; an access point or a phone lands on the floor plan, a server or a router in the rack, and only a neighbour that says nothing stays a switch (still a guess you confirm). The same device seen over LLDP on one side and CDP on the other is one node, not two with the same IP, and the Sync summary no longer lists as "not in the document" the neighbours it has just created.
- **The discovery says when it could not look.** Without `ping` on the server every host looked dead and "0 found" read as an empty network; the scan now returns the missing tool (`ping`, ARP table) and the page writes it in red. The ARP table is also read without `arp` (from `/proc/net/arp`, then `ip neigh`).
- **You are told what the server corrected.** An IP that was not an address or an id that was not a name, repaired on save, is reported in a notice instead of being found out at the next reload.

### Fixed

- **A device with SNMP shows its name, not its model.** Twelve "Catalyst 2960" in a column did not say which was which; the name the administrator gave it (sysName or DNS) wins when it is readable, the model stays first where the hostname is a generic default. `dc-01`, `db-01` or `ad-02` are names, not hexadecimal blobs: only an identifier-sized run of hex digits (a UUID, a MAC) is dropped for the IP.
- **An old Catalyst is a switch.** The description of a Catalyst 2950 says "C2950 Software", with L2+L3 services, and it was classified as a router; the 29xx/35xx/38xx/45xx/9200-9600 model codes and the 2950 object id are now recognised (not the 36xx/37xx routers, not the 9800 controller).
- **Exports carry the project name.** Titles and file names said "InfraNet Pro" and "infranet" whatever project was open.
- **No "aggregate removed" notice for an aggregate that was not there.** An empty member list on a passive port counted as one removed, so the notice came back at every open.
- **A name read over SNMP is no longer labelled "resolved by DNS".**

### Security

- **An identifier or a number can no longer run code.** `rack.id`, `link.id`, `integration.port`/`timeout`, a port count, a cable length and a VLAN override were written into the pages without escaping: a project from a file, an editor or the API could run script in the browser of whoever opened it, viewers and the admin included. Ids are names now (letters, digits and `_ . : @ + # -`): the client repairs a loaded project and its references, the server repairs what it is sent, and the numbers of the model come back numbers on load. A real-browser fuzz puts a payload in every numeric field and every id, and checks both the load and the output.
- **A burst of logins can no longer add up CPU.** A password check is ~340 ms of CPU in a single process; a hundred at once stalled everything for half a minute, and the per-IP limit does not see them. At most two run at once and eight wait (`INFRANET_LOGIN_CONCURRENCY`, `INFRANET_LOGIN_QUEUE`); beyond that login answers 503 with `Retry-After` at once.
- **The AI assistant checks what it is given.** The endpoint must be an http(s) address with no user or password in it (the key has its own field); the path is appended before the query (Azure OpenAI); the key is a valid header and an error from the provider that echoes it is redacted before it is shown; only the admin sees the endpoint; a chat forwards at most 40 turns and 60,000 characters, and the facts sent from the browser have a cap.
- **Files with credentials are readable by their owner only.** Projects (they hold the devices' SNMP credentials), their backups and history, `users.json` and `api-tokens.json` were `0644`. They are `0600` now, existing files are tightened at startup, and `INFRANET_FILE_MODE` widens it to the group for a backup job (never to others).

### Changed

- **The README no longer promises a forced password change on first login.** The server prints the random `admin` password once and never asked for a change; the text now says where to change it (user menu, **Change password**).
- **Behind a reverse proxy, set `INFRANET_TRUST_PROXY=1`:** without it every user arrives from the proxy's address and the per-IP login limit counts everyone together. The README now says so, and documents `INFRANET_LOGIN_IP_MAX`.

## [2.11.16] — 2026-10-16

A value says where it comes from, and a model you typed is no longer called a swapped device.

### Added

- **A value says where it comes from, and what you type is locked.** What the Sync or a scan copies in — brand, model, serial number, firmware, MAC, a port's description and address — now reads "Measured", and your own write turns it back to "Declared". Typing a model, serial number or firmware closes its padlock, so the Verify stops calling it a replaced device on every scan (a locked serial no longer flags a real swap: open the padlock to bring the check back).

### Fixed

- **What the switch says is no longer lost or overruled.** A slow agent no longer costs the poll its hardware identity (the ENTITY-MIB is read first). "Free but active" uses the read link state, a port nobody read is no longer "consistent" and has its own tab in "Ports to check", and a cable drawn on a port read as down no longer lights it green.
- **The Dashboard and the bar count devices.** "Answering over SNMP" no longer lists the devices without SNMP and says in small grey how many are not observed; "Devices" counts devices (33, not 36 with two jacks and a patch panel); the bar's figures are 20% larger.
- **The portable export lists the right fields:** the firmware you typed now leaves with the project; seven measures and the copy marks no longer do.

### Security

- **A hostile value can no longer reach a page through the IP field or a translated text, and a login no longer stalls the server.** The IP field holds only an address and an address has its own attempt limit. Restart the server once.

## [2.11.15] — 2026-10-15

The map and the rack can be used with a finger on a tablet, and the History says what the Sync really read.

### Added

- **The map and the rack can be used with a finger on a tablet.** Two fingers zoom and pan (the point under them stays put); a device, a room or a rack icon is grabbed by pressing it for a moment, so zooming never moves anything and a finger that slides first just scrolls the view. A tap selects, the panel dividers can be dragged, and a tap on the empty map deselects only when it really is a tap. Sizes grow for a finger only on touch screens; a mouse sees no difference. Tested on Chrome with emulated touch, not yet on a real tablet or an iPad.

### Fixed

- **The History says what a Sync read, and keeps the lines of the automatic rounds.** A line read "18/18 devices · all reachable" in a project of 47: the 18 were the only devices with SNMP, and the Sync polls no others. It now reads "18/18 SNMP devices read · no read errors · 25 without SNMP, not read". The lines of the automatic monitoring reached the disk only when the project was saved, and nobody saves the hourly round, so the Changes tab stopped at the last save while the Checks tab went on: a run's line is now written at once. Lines already written keep their text. Restart the server once: it gains a history route.
- **An SNMP error message no longer leaves in an exported project.** The text of the last failed read was the one SNMP-status field without a class, so it travelled in the portable file while its sibling status was stripped, and it can name addresses of your network. It is a measure now, like the status.

### Changed

- **`proxy-addr` 2.0.7 → 2.0.8**, an Express dependency (Dependabot).

## [2.11.14] — 2026-10-06

A device that never answers no longer freezes the monitoring, and the port descriptions get a report.

### Added

- **A "Port descriptions" report** counts the ports whose description follows the "jack | text" format and says whether the jack exists in the project. It opens from the *Cables* row of the Dashboard and exports a CSV; a project that documents no jacks is told so instead of every jack being called unknown.

### Fixed

- **A device that never answers no longer freezes the monitoring.** A device read, and the Check's reachability sweep, had no deadline on the browser side: one request that never came back kept the Check marked as running, so the Check — and with it the full monitoring — never started again, without a word. Each now gives up after a deadline above the server's own worst case, the device is marked in error, the round goes on, and nothing is declared absent for lack of a look. On a real lab with one silent device the round finishes, 17 of 18 read.

- **The scheduled monitoring keeps running in the background, and a running round is visible.** A round skipped a hidden tab, and one that could not start was lost: the "Auto" badge counted down to "Auto 0s" for a whole interval (an hour with the full check). Now it runs in a hidden tab too (the tab still has to stay open), a blocked round stays due and runs as soon as the field is free, the badge shows a spinner while it runs, and Check, "Refresh now" and the right-click on *Topology* say that a read is in progress instead of returning silently. The *Topology* button also no longer goes back to normal in the middle of a refresh.

### Changed

- **The text a device reports for a port now becomes its Description by itself,** on every read. A Description you typed is never overwritten, and a read that brings no text never erases one.

## [2.11.13] — 2026-10-02

A damaged file is read from its backup, an address is sent as it was checked, and an SNMPv3 name is never guessed.

### Fixed

- **A damaged state file is read from its backup, and never copied over it.** A corrupted `organization.json` came back empty without anyone trying the `.bak`, and the first save copied the broken file over the only good copy. Four more stores had the habit — the AI and DCIM configurations, the API tokens, the skin index — and users and projects already read their backup: now all seven share one reader. A file that is absent stays absent, and a `.json` that no longer parses is never promoted to `.bak`.

- **An address is checked and sent as the same string.** `ping.exe` reads `010.8.8.8` as 8.8.8.8, while the project reads it as 10.8.8.8, so a documented `10.10.010.5` was pinged as another host and the Verification spoke about that one. Every door that turns an address into traffic now reads it with one definition and sends the canonical form. Multicast, broadcast and `0.0.0.0/8` are never targets, and a documented IP that is not valid is *not verified* instead of "no answer".

- **An SNMPv3 device set up with AES-256 or SHA-2 can be queried, and an unknown protocol name is an error.** The driver turned a name it did not know into SHA-1 or AES-128 in silence, and all you saw was a timeout. One list now feeds the driver and both property panels: the SHA-2 family and both AES-256 variants are selectable, and an unknown name is refused with an error that names the field. Saved projects are unaffected.

### Security

- **The Content-Security-Policy no longer allows `eval`.** `'unsafe-eval'` was there for libraries that compile at runtime; the frontend has none, and the only code that needed it was dead. The policy the server sends is now tested on every kind of response.

### Changed

- **Two more pieces of the link-inference engine are pure, tested modules** — the Direct Connection Theorem and the leaf-endpoint decision. Behaviour is unchanged, checked against the old code on 20,000 and 30,000 random inputs; the test suite now lives in one folder with unique names.

## [2.11.12] — 2026-10-01

An address is compared by what it is, not by how it is written — and a register that asks for an owner now has one.

### Fixed

- **A NAT64 local-use address is no longer called public.** An address from `64:ff9b:1::/48` (RFC 8215) came out as a global address, so one sitting among a device's public IPs was described as reachable from the internet while it is not routable at all. IANA's registry marks that whole block as not globally reachable, and that is how it is declared now. The IPv4 embedded in such an address is deliberately **not** decoded: RFC 8215 leaves the prefix length to the operator, so `127.0.0.1` is written one way under a /48 and another under a /96 — there is no single address inside to read. The well-known prefix `64:ff9b::/96` is a different case and stays global, which is what the registry says of it.

- **An octet is decimal digits, in every reader.** The IPv4 parser existed in five places and three of them accepted forms no resolver does: `0x7f.0.0.1` was read as 127.0.0.1, `1e2.0.0.1` as 100.0.0.1, `12abc.1.1.1` as 12.1.1.1, an empty octet as zero. Two of the three decide something: which of two ARP rows sharing a MAC stays active and which is marked Inactive, and the order of the discovery frontier — which settles deduplication and who is recorded as having discovered a device, from a neighbour address written by whatever is on the other end of the cable. All of them now use one definition, and an address that cannot be read sorts last instead of first.

- **A link-local IPv6 address is recognised across the whole `fe80::/10`**, not only in its first /64, and there is now a test that would have caught the difference.

- **The check on an AI answer compares addresses, not text.** Three things followed from one validator being stricter than the rest of the project: an address of yours written with leading zeros (`192.168.001.005` — the same address as `192.168.1.5`) was neither cited nor flagged; an **invented** address written the same way passed the no-invention check with nothing said about it; and a device whose address is the beginning of another's (`10.0.0.1` inside `10.0.0.100`) raised the wrong chip, so the jump landed on the wrong node.

- **The manual explains an empty VLAN column.** After a Sync the VLAN can be empty on ports that plainly have one: not a failed reading, but a reading that says the device never declared it — some switches leave the standard PVID at 1 whatever the port really carries. The chapter also says why a `1` from a device that knows no other VLAN is not a measurement, and what to do instead.

### Added

- **The asset register carries the tenant.** ISO 27001 A.5.9 asks an asset register for identity, owner and location; the owner imported from NetBox was visible only in the Properties panel. It now has a column in the PDF, which appears only when at least one device has one — on an installation that never imported from NetBox it would be a column of dashes. The column is headed *Tenant* in both languages, not *Owner*: in a register that word means the A.5.9 owner, which is a different field.

- **A security policy** (`SECURITY.md`): how to report a vulnerability privately, which versions get fixes, and the two premises that decide whether a finding is a vulnerability or a deployment choice.

### Security

- `ip-address` 10.4.0 → 10.7.2, closing three advisories. The two that describe a class this project also has — an address classifier disagreeing with the network — were read as classes and fixed here too, above.

## [2.11.11] — 2026-09-29

One piece of evidence, one vote: the scan stops counting the same clue twice, and says when it had nothing to go on.

### Fixed

- **The same clue no longer casts two ballots.** Two signals were capped on one door and walked in through another. The vendor derived from the MAC is capped at 45 points, well below any measured signal — but the same name also went into the text the vendor→type rules read, where no cap applies: 123 points for an OUI whose owner makes switches, 207 for one whose owner makes routers. And two rules read the same operating system out of the same text, summing to 125. Measured on rows a real scan produces with no SNMP answer: a printer whose page reads *HP LaserJet Pro M404* came out `router`, and on a BSD firewall the word *FreeBSD* outvoted the word *pfSense*. A vendor name is identity, not function; an operating system says what a device is made of, not what it does. Each votes once now — and with the MAC as the only signal the class is still assigned, at honest confidence.

- **An OID that names an SNMP agent no longer names a device.** `1.3.6.1.4.1.12325.` claimed *firewall, 95*; FreeBSD's own `snmpd.config` sets it as the identifier of the SNMP implementation, so every FreeBSD-family host sends it and pfSense reports the same value as plain FreeBSD. A device that says nothing else about itself now comes out unnamed at low confidence instead of `firewall` at 99. Manufacturer identifiers, such as Fortinet's and Palo Alto's, are unaffected.

- **A scan row speaks only for itself.** The Discover dialog identified each row by its position and looked that up in the results of the moment, so when the list changed between two renders a choice made on one row landed on another device — on a second scan, and on *split* of a folded row, which happens inside a single scan with one click. Each row now carries its own key.

- **A type you picked by hand is not a suspect.** Any different type of the same family raised *possible replacement* on every scan, so whoever mapped their network most carefully got the most noise. A hand-picked type is a decision about the device, not a measurement of it; vendor and hostname differences still raise the flag.

- **The three fields the Management panel writes are declared** in the project schema, so the gate that catches a new field before it leaves with an export is green again instead of red out of habit.

### Added

- **A scan that gets no SNMP answer at all now says so.** The summary hid every counter worth zero, including the SNMP one — which is what the rest of the scan depends on: types, ports, topology. It is always visible now, and when hosts are alive and not one answered, the scan offers the two possibilities without picking between them: the community is not the right one, or SNMP is not enabled on these devices. No individual host is marked — calling one silent without the authority to say so would be noise on the only signal that matters.

## [2.11.10] — 2026-09-24

The measurement that was missing exactly where the previous release had stopped guessing — and the two lists that now say how they know.

### Fixed

- **A Cisco `Port-channel` brings its members with it.** 2.11.9 stopped inferring a LAG from two parallel cables and asked for a measurement instead — and on Cisco that measurement never arrived. *Is this interface an aggregator?* was asked in three places in the SNMP driver and answered differently: two demanded `ifType=161` (ieee8023adLag), the third also accepted `53` (propVirtual) when the name was a bundle. IOS declares `Port-channel` as 53, so the strictest answer won: the aggregator was listed and its members kept no membership — a LAG with no members, which downstream is no LAG at all, so the aggregation vanished from the document. One predicate now answers for all three, and membership is read from `ifStackTable`, which those switches publish and which was there all along; 802.3ad, CISCO-PAGP-MIB and CISCO-LAG-MIB answer nothing on those images. `53` on its own is not enough — a Cisco SVI is `53` too — so the name decides it, through the project's single vendor-neutral recogniser. Members inherit the trunk and its carried VLANs from the bundle again, which on Cisco is where the configuration lives.

- **The Dashboard's LAG list knows who its ends are.** They were read out of the group key — a plate, not a fact — which worked only for the two key shapes the app writes itself: a group created by hand rendered with an **empty name**, six rows out of sixteen on the test bench. The ends now come from the member ports, the one place they are a fact, and a group left with no ports is labelled as such instead of having an end guessed for it, because it is not a LAG but a leftover.

- **A LAG group nobody joins is no longer created.** The inferred group exists to carry a measured bundle onto a peer whose SNMP says nothing about aggregation. It was registered before checking that a port would actually adopt it, so once both ends reported their own measured group the LAG list kept an empty shell — remade at every AutoLink run, and therefore impossible to delete by hand. It is created only where a port will adopt it, and a shell left behind by an earlier run is removed.

- **The cable list and its own row counted two different populations.** The row counted every cable and the detail listed only the inferred ones, so a project with 40 cables opened a drill-down of 15.

### Changed

- **Both Dashboard lists split by how we know.** LAG groups into measured · declared · derived — the third bucket did not exist, and a group made by hand was counted among the inferred ones. Cables into those three plus the two only a Verify can produce: **contradicted**, where the network denies the cable, and **not read** — an inference whose evidence evaporated. The words are the ones the certainty pills use everywhere else, taken from the single notation rather than written again, so a tab and a pill can never disagree; the tabs follow the notation's own reading order, and the two verdict tabs are absent, not empty, until a Verify has run.

- **Three sentences that were not true.** The README promised that a Cisco `Port-channel` was *fully supported* while its members were being dropped; a second line still described the pre-2.11.9 rule, where duplicate links between two devices became a LAG on their own; and a test comment blamed the vIOS images for not publishing membership at all. They do publish it — measured on the bench, read-only.

## [2.11.9] — 2026-09-24

Two things the app used to state without measuring them, and a gate for the numbers a release forgets.

### Fixed

- **Two parallel cables are no longer called a LAG.** Two LLDP/CDP adjacencies between the same pair of devices were enough to write an inferred aggregation — a group on the ports, `isTrunk` forced true, carried VLANs filled in — three document fields born from a coincidence. LLDP says nothing about aggregation, and two parallel cables are equally the picture of spanning-tree redundancy. A LAG is now declared only where one end reports it (`ifStackTable`, IEEE 802.3ad, AttachedAggID) or a person did; elsewhere the cables stay two, and a group an earlier run left behind is removed.

- **A port description deleted on the switch now disappears from InfraNet too.** The SNMP reader closed every interface with `alias: f.alias || ''`, so *not read* and *read empty* became one empty string, and the client kept the previous text whenever it saw it. Two fallbacks in a row made the description immortal — in the port panel, the PDF report and the LLDP neighbour matching. The three cases are now kept apart, and a description the latest poll did not confirm is dropped, by the rule that already applies to a port's measured VLAN.

### Changed

- **A release gate instead of a list to remember** (`npm run release -- check`): the version in its four places, a CHANGELOG section that actually lists something, and the declared test count measured against what the suite prints — the one number no test can check without counting itself.

- **Dependabot gets one narrow rule**: scheduled version-update pull requests stay off, and Express majors are filtered — Express 5 is a decision with its own testing pass, not a dependency bump. Security updates keep arriving.

## [2.11.8] — 2026-09-16

A strategy for the one layer nothing can measure, and a spring clean of the code.

### Added

- **The passive layer gets a method, written down.** A patch panel answers no query: which wall socket a cable comes from is the one thing SNMP will never see. But the run inside the wall does not change, so the socket can be **derived** from the switch port, and the devices that never move — printers, access points, phones — anchor the rack patching even where cords were attached with no rule at all. The convention that makes it work costs one command per port: put the **panel port** in the switch port description, not who is plugged in. Who is plugged in the app measures on its own; the socket nobody can. The manual carries the full strategy (chapter *Cables, connections and wireless*), and the README a summary. ⚠️ InfraNet reads that description and offers it when you type your own, but does not yet compare it with the document — this is a working convention, not an automatic check.

### Changed

- **Dead weight out of the code.** Seventeen dead imports left in `server.js` by the split into routes, a front-panel layout table nobody reads that still travelled in the browser bundle, unused locals and parameters, and unnecessary escapes in regular expressions: 83 lint warnings down to 5, and the five that stay are deliberate. No behaviour changes.

### Fixed

- **Errors keep their cause.** Three places rethrew a library failure as a fresh message, dropping the original: they now pass `{ cause }`, so what really failed travels with the message.
- **A test case that was not what it looked like.** `test/backup-ref.test.js` used `'C:\backup\admin@corp\sw1.cfg'`, which in JavaScript is not a Windows path — `\b` is a control character — so the case never tested what its author meant. It does now.

## [2.11.7] — 2026-09-15

A security patch, and a README that stopped promising more than the code does.

### Fixed

- **SNMP credentials had two ways out.** The *JSON backup* download fell back to the raw project when the redaction script had not loaded — community strings and SNMPv3 passphrases included, in the very file people attach and share. It now refuses, and says why. The VM panel showed the community in a plain text field, where a screenshot attached to an issue would publish it; it is masked now, like the device panel's. Every other way out — REST API, Ansible inventory, AI context, PDF dossier, portable export, viewer role, server logs — was checked and already guarded. The new test asks the schema which fields are secret, so a future one is covered on its own.
- **Seven sentences the code had already proved wrong.** The architecture notes still listed as open a ping bug fixed in July. The top of the README claimed the product knows when a community is wrong, which SNMPv2c makes impossible: what it flags is a device that should answer and stays quiet. *Known limitations* described a VLAN limit that does not exist and hid the one that does — a Cisco trunk read from the VTP MIB stops at VLAN 1023 — pointed at a database backend that was never built, and offered LLDP as the cure for a write that never happens.

### Changed

- **External discovery and monitoring engines are a possible secondary evidence source**, not a closed door: optional, not a priority, never a runtime dependency. Integrations with vendor or platform APIs can be built on request.
- `js-yaml` 4.3.1 → 4.3.2 — development only, pulled in by ESLint.

## [2.11.6] — 2026-09-12

The release that taught the tool to say what it cannot see. The assistant declares the part of
the graph it was never given, a device that stays quiet to the community in use stops being filed
as one without SNMP, and the checks you run finally add up to a reading: is this network getting
better known, or are we only looking at it?

### Added

- **A trend on the verification timeline.** The *Checks* tab opened on a list of snapshots; it now opens on their reading — where you are, as the last check split into consistent, mismatched and not-verifiable devices, and where you are going, as the shape of the series. It **refuses a verdict** when the share it could not look at has grown, never compares a manual check against an automatic poll, breaks the series if the estate changes size, and below three samples says so: the confirmed share matures with use.
- **«Silent to this key».** With SNMPv2c a wrong community raises no error — the agent drops the request and the probe times out — so silence and *no SNMP* are indistinguishable from outside, and a whole estate gets filed as unmanaged after knocking with the wrong key. Discover says it now and counts them, but only where something authoritative expected an answer: a neighbour that declared the device over LLDP/CDP, or the project documenting it with an SNMP driver. Such a neighbour, when it stayed quiet, used not to appear at all.

### Changed

- **The assistant's context tells the truth about itself.** Cutting a list is honest; cutting a relation is not — one missing link makes two connected halves look separate, and from the inside you cannot tell. Topology, ports, PDU outlets, VMs and SSIDs now carry how much was shown and how much there was, and the prompt forbids answering past that line. Passive cabling comes out summarised by type while its names stay in the topology, where a path reads hop by hop: 56% off the context, at the cost of one question in twenty.

### Fixed

- **One number, one definition.** Free ports had two answers in the same context — a collector stopped counting at its cap, so a fully cabled switch reported 88 free in one block and 0 in another, and the prompt sent the assistant to the broken one. The verification tab counted divergences with a sum of its own, and the timeline silently dropped a bucket the report had been emitting all along.
- **The anti-invention check was accusing correct answers.** Addresses from the *not verifiable* category reached the model but not the digest that validates what it cites, so the assistant correctly reported an address the app had handed it and collected a *reference not found* warning beside a right answer.
- **A measured number that was never about the product.** `npm run check` counted the files on the disk it ran on — 548 on a developer machine, 520 in a clean clone — so the README quoted whichever of the two had been measured last. A source of the product is now what git tracks: 519 everywhere, with a gate that fails if the count ever reaches for an untracked file again. The Verify's categories were also declared as 6 in one place and 7 in another: there are 9.

## [2.11.5] — 2026-09-07

A maintenance release. Saving stopped holding the server still, three surfaces no probe had
ever touched were driven against deliberately hostile input, and one number on the Overview
stopped answering a question nobody had asked.

### Changed

- **Saving no longer holds the server still.** The write left the single thread, behind one queue per project — while a document went to disk, every other request waited for it. The version check moved inside that queue together with the write, and so did the choice of a new project's number: otherwise two saves, or two imports in flight, would both pass and one of them would disappear.
- **Text a person types has a shape and a ceiling** — project names, token labels, skin names, site fields: control characters removed and a 200-character cap, applied where each is written instead of in four separate places. Names arriving from a DCIM go through the same rule.

### Fixed

- **The Overview counted who had not failed, not who answered.** A device configured for SNMP but never queried read as one that answers, so a project nobody had polled reported *all reachable* in green. The three states are told apart now, and the row's label says what its number says.
- **A misbehaving DCIM can no longer hang an import** — a page whose *next* pointed at itself made the client fetch forever, and a two-megabyte device name travelled through untouched.
- **A report whose list is not a list** answers *400* naming the field, instead of failing halfway through a PDF.
- **Cables outlived the ports they were on.** Reducing a device's port count left links attached to ports that no longer exist; they are found and named, with SFP numbering respected so a real uplink is never cut.
- **Two sessions can no longer overwrite each other's organisation** — the same version check the projects have, with limits on sites and links; a panel skin has a size limit; an API token can be given an expiry; and Windows name discovery ignores answers that did not come from the host it asked.
- **The REST API v1 answers the right question** for a path it does not serve — *404* for a read, *405* with `Allow` for a write — and a project id that is not a number is refused before it becomes a file name.
- **The floor-plan grid no longer flashes** when a project that has it switched off is opened, and label export speaks both languages.

## [2.11.4] — 2026-09-07

A security release. Two smoke passes went over the product — the code, the app driven
live, then the surfaces the first pass had not touched. Everything short enough to fix
safely was fixed, each finding reproduced first and each covered by a test. Nothing here
changes what the app documents; what changes is what it refuses to do.

### Added

- **Anyone can change their own password** — `POST /api/auth/password`, viewers included, who had no way to. It asks for the current one, keeps the session that made the change and expires the account's others.
- **Every secret can live on the data volume** — `INFRANET_API_TOKENS_FILE`, `INFRANET_AI_CONFIG_FILE`, `INFRANET_DCIM_CONFIG_FILE`, `INFRANET_SESSION_SECRET_FILE` — and `INFRANET_AI_ENDPOINT` pins the AI endpoint, so a key set by the operator cannot be redirected from the interface.

### Changed

- **The Docker image runs as a non-root user** and keeps the secrets out of its layers, on the `/data` volume.
- **Signing in is stricter**: the session id is regenerated on login, a new password needs eight characters, and throttling counts per address *and* account — behind one proxy, ten failures used to lock out everyone.
- **The topology crawl stays in internal address space** unless you declare otherwise, with depth and device count capped; a VLAN id outside 1–4094 is refused instead of quietly clamped; and every `/api/*` answer carries `Cache-Control: no-store`.

### Fixed

- **A management URL could carry credentials out of the app.** With a `javascript:` scheme it ran in the app's own origin when opened; written without a scheme (`admin:pw@10.0.0.1`) it travelled with its password into the REST DTOs, the Ansible inventory and the context sent to the AI provider.
- **Revoking a session survives the next login** — a demoted administrator who left a tab open used to get their rights back simply by signing in elsewhere.
- **The export stopped being a way in.** It could read files off the server through an image reference, hang forever on a malformed PNG, and freeze the single thread the server has for nineteen seconds on one long cell.
- **Work no longer disappears quietly**: a malformed save rewrote a project as empty and answered *200 OK*; undo and redo did not light «Save»; deleting a rack left its HA partner pointing at something gone; closing the tab said nothing; and an import from discovery went unrecorded in the document's journal.
- **Values are checked before they are used** — a subnet prefix outside its range, a `projectId` that is not a positive integer, a non-string username (a 500, now a 400), a snapshot id before it becomes a path, and the saved DCIM token, which now travels only to the instance it was saved for.
- **Three strings that had never reached either language** now follow the chosen one, and `<html lang>` follows it too.

## [2.11.3] — 2026-09-04

One question — *how much do I trust this?* — used to be answered in seven vocabularies.
It is one alphabet now, on the five screens where the question is actually asked. No engine
was merged and no number moved: what changed is the word you read.

### Changed

- **Six signs, learnt once, read everywhere.** *Measured*, *Declared*, *Derived*, *Contradicted*, *Not declared*, *No reading* — on a cable, a panel field, a discovered host, a floor-plan tile and the Dashboard. The two absences are drawn identically on purpose: the word says who has to move next, you or a probe.
- **A cable's «Status» row went from five badges to two plus a detail.** Three of the five answered that one question in three incompatible notations, one of them a percentage that contradicted the other two. What a link *is* (access, trunk, LAG member) and how far it is trusted are separate questions now, with the protocol and score in a quiet chip behind the grade.
- **The property panel says who wrote each inventory value** — a mark per field and one legend at the top, instead of a paragraph explaining it field by field.
- **Discovery grades a device by its signals, never by its score.** The score is an additive vote that reaches «high» with no SNMP and no LLDP at all, so a well-summed pile of weak clues used to read as a measurement. *Measured* now means something authoritative spoke, and a barely-recognised device is no longer painted red.
- **The floor plan and the racks say in words what they used to say only in colour.** A red halo, a greyed-out tile and a grey ring were the last place in the app with no words at all.
- **The Dashboard speaks the alphabet it gave its words to.** It is where the six words came from, and the last screen still spelling them its own way — a row could show the new pill beside the old tag, *Measured* against *measured*. Every badge there is one pill now, and the renderer asks the engine which tags are certainties instead of holding a list of its own. `declaredNet` had been carried out of the alphabet by a fork made for Italian gender agreement.
- **A grade's colour, and a tag's, exist in exactly one place.** Tint, border and dot derive from one ink, retiring three hand-written colour tables, a fifth amber, two off-token literals and a colour token repeated on every tag.

### Fixed

- **The guard that derives the Dashboard vocabulary read only one of its two birth sites** — `prov:` on rows, `tag:` on list items — which is how a word could sit outside the alphabet with every gate green. It reads both now, and the words of the second site that answer a *different* question (no gateway, unverifiable, stale…) are declared as non-certainties instead of left unmapped.

### Removed

- **The cable's proof-state badge**, the sixth vocabulary for the same question, with its colour table, its CSS class, its seven translation keys and `badgeInk()` — a helper that existed only to pick legible text for those solid backgrounds. The `cableProof` engine is untouched: what changed is the alphabet its result is read in.
- **The discovery row's «switch · port» badge**, which named the access port a device was found on. The row was crowded and this was the least load-bearing thing on it; the FDB correlation behind it still runs.

## [2.11.2] — 2026-09-01

Post-release polish: the Overview reads the same in every section, wireless discovery
explains itself, and a transitive dependency advisory is cleared.

### Changed

- **All six Overview sections give their verdict the same way, and «I don't know» is now grey everywhere.** There used to be two verdicts — a sentence in the Summary columns, a hand-built number in the other three lenses. They are one component now: the number where the answer is a number, the sentence otherwise. A section with nothing to judge shows grey and says what is missing, instead of a green that was never earned or a red that promised a diagnosis nobody made — a never-synced project no longer reads as a fault.
- **Wireless discovery says why it found nothing, instead of three silent zeros.** A Sync that made no wireless associations used to report `0 / 0 / 0` and no reason, so an access point that answers no SNMP looked broken. Each of the five gates that can skip a device is now named — no clients read, no declared SSID, client not yet in the document, not an endpoint, rejected pair, manual link — and a wired-only network stays silent, as it should.

### Fixed

- **An OpenWrt access point was not recognised as wireless.** On Linux a radio in AP mode reports itself as Ethernet, so recognition falls back to the interface name — and the current OpenWrt name `phy0-ap0` (and the `wlx` name of a USB adapter) did not match. Both match now, with the reverse pinned too: no wired interface may be read as a radio.

### Security

- **`qs` pinned to 6.16.0**, clearing two advisories carried transitively under Express. Neither was reachable here — the `arrayLimit` bypass needs `comma: true`, which the app never sets, and bodies are parsed only by `express.json()` — but the fix is a one-line `overrides` that leaves Express and body-parser untouched.

## [2.11.1] — 2026-09-01

Faster where you feel it, and a few things the app used to state without having measured them.

### Changed

- **The project list is paid once per save instead of once per read.** Every call re-read and re-parsed every project file on disk, synchronously, so nobody else was served meanwhile. It now costs about 1 ms instead of 68 on a large installation, and a save makes the list re-read only the project that was saved.
- **A full canvas render reuses the DOM instead of recreating it.** 55 ms at 500 devices and 92 at 1000, where it was 119 and 203; the floor-only render no longer grows with the project at all, and dragging a device no longer produces a single long frame.
- **Selecting a device no longer rebuilds the whole canvas.** A click cost 122 ms at 500 devices and 205 at 1000; it now costs the same at either size and repaints only what depends on the selection.
- **Two hot lookups are no longer paid on every render.** Resolving a port to its device is remembered, and the rack-unit height no longer forces a whole-document style recalculation on each redraw.
- **Save is loud when there *is* something to save.** With the document clean it used to be the strongest button on the toolbar, and unsaved work showed as a faint amber outline. The emphasis is the other way round now, and the button no longer changes width between the two states.
- **Notifications stay until you dismiss them.** They used to share one box and fade on a timer, so a second message overwrote a first nobody had read yet. Each is its own card now, stacked in a column you empty yourself.
- **The status badges no longer shove the toolbar.** The four of them sit in one bay after the search field: with all four lit, the action buttons move by zero pixels instead of 267.
- **The toolbar reads as groups instead of one long row.** Thirteen dividers became the five that mark what the bar actually does, Delete is no longer painted red at rest, and History gets its word back.
- **Cable badges are readable.** Four of the twelve wrote white text on a background that could not carry it — the worst, at 2.03:1, was the one warning you *not* to trust that cable. The ink is now derived from each background instead of fixed.
- **A media converter on the floor plan is labelled «Converter».** In Italian its old label was also the word for *medium* recognition confidence and *medium* VM criticality: one word for a device type and for two unrelated scales.

### Fixed

- **A port's VLAN that could not be read was written down as VLAN 1.** Zero is not a value a PVID can take — the standard uses it to mean *no VLAN* — so a zero there was a decoding failure, and writing 1 over it made a failure look like a measurement. On a device that switches VLANs that invented 1 could outrank a network documented by hand. It stays absent now.
- **A save in flight can no longer be overwritten by another one.** The temporary file used during a save was named after the process rather than after the write, so every save of the same project inside one server shared a single filename.
- **An import can now say which networks a link makes reachable — where NetBox actually says so.** Two coherence checks needed that answer and had never once run. A tunnel and a circuit carry nothing about the networks they serve, so the field stays empty there deliberately rather than being guessed; the one chain that does exist — an L2 service on a VLAN, and the prefixes declared on it — is read.
- **Extreme switches were showing the generic icon.** A word boundary does not fall inside `ExtremeXOS`, so the pattern that recognises network operating systems never matched it.

## [2.11.0] — 2026-08-31

A project documents one building; this release documents how the buildings talk to each other — the sites, the WAN lines each one buys, the tunnels between them, as a map and as a form in the same place, with the declared model checking itself and a WAN chapter in the handover dossier.

### Added

- **Sites and links: the multi-site layer, as a map and as a form.** A panel beside the project picker holds the organisation — its sites, their WAN lines, the tunnels between them. Clicking a site walks down into that site's own project, and the sub-header path leads back up. Writing it by hand is the primary path, not a fallback; reading is open to everyone and writing is admin-only, because documenting is not administering.
- **Each site's box says how much is inside it** — devices and racks, counted server-side inside the parse the project list already does. `null`, never `0`, when a project has no readable state: a project whose contents we do not know is not an empty one.
- **The device holding each end of a link, picked from the site's project or typed.** WAN-edge kinds are suggested first and nothing is filtered out — somebody terminates a tunnel on a NAS. What you pick shows as linked and says when it stops resolving; what you type stays a declaration.
- **A site can take its networks from its project**, in one button. It takes the *declared* rows, never the /24s inferred from device addresses, and it only ever adds.
- **The declared model checks itself.** `lib/inter-site-audit.js` reports a tunnel carrying a network no site claims, the same subnet at two sites, an end pointing at a site that does not exist, a spoke touching no hub, a link declaring a WAN line that belongs to neither of its sites, a link that never says which line carries it, an implausible MTU or bandwidth, and an address declared *public* that is not one — RFC1918, a carrier's `100.64`, a loopback, classified off the IANA special-purpose registry rather than a vendor list. Inconsistencies and gaps stay in separate lists, because a missing line and a false one are not the same thing, and every check that could not run leaves its name and its reason instead of passing for a clean bill of health.
- **The map is drawn by a pure, deterministic module.** Same input, same coordinates, every time — which is what lets the browser, the PDF and the coming draw.io export draw the same map. It zooms and pans with the floor plan's own controls; parallel links fan apart instead of overlapping; a link pointing at a site that does not exist is reported as undrawable with the missing name.
- **The organisation is stored, and the server decides what is well-formed.** `data/organization.json` holds one organisation per installation — not a copy inside each project. `GET` returns it with its audit; `PUT` is admin-only, re-normalises the body and answers with what was actually written plus a count of what was refused.
- **The WAN lines of a site can be read from NetBox.** Circuits become uplinks or inter-site links depending on where their two ends land; a line whose far end is a carrier's provider network stays an uplink and the cloud is said rather than modelled. The bandwidth comes from `port_speed` on the site-side termination, in kbps converted to Mbps. The cable leaving the termination identifies the WAN port — device and interface — so the line can be recognised.
- **What links two sites is read too.** NetBox keeps L2 services and tunnels in `vpn/`, which nothing was opening: VPLS, IPsec, GRE and the rest now arrive alongside the circuits. Their kind is *translated* from NetBox's own closed vocabularies rather than guessed from free text, and anything with no counterpart enters as `other` carrying NetBox's label.
- **After an import, InfraNet offers to register the site on the map** — it offers, because joining the organisation is a company-level declaration. A project already registered says so; a namesake with no project is linked rather than duplicated; a project born from more than one NetBox site is refused with the reason.
- **The dossier gains the WAN chapter.** The map redrawn for print — vector, with its own colours, so it does not come out black or in the generator's theme — and one recovery card per line and per link: who sells it, the circuit id you dictate on the phone, the port bandwidth, the SLA reference, the WAN interface with when it was read, the public addresses as a list, the device holding each end, and the networks the link makes reachable, which on an IPsec **are** the encryption domain. The header counts the chapter's own holes instead of hiding them.
- **A floor plan gains a second level of containment: the storey.** Drawn like a room, not modelled as a device, so a two-storey building stops being two projects or one flat drawing.
- **A value can carry how we know it.** `lib/provenance.js`: `declared` (a person wrote it, and it never ages), `measured` (read from a device at a stated instant) or `derived`. A bare value is deliberately not promoted to `declared`, and a measurement with an unreadable timestamp stays `undated` rather than being stamped with now.
- **Every field of a project has a written class** — `document`, `measure`, `derived`, `private` or `secret` — and therefore a decided fate in an export. A field a person can type in the UI stays a `document`: erring that way adds one field to an export the user made themselves, erring the other way deletes somebody's work.

### Fixed

- **An edit made while a save was in flight disappeared, and the interface said «saved».** The unsaved dot now stays on: that edit could not have been inside a request that had already left. With autosave on, the last edit of the day could stay in memory and never reach the disk.
- **Two sessions saving the same project both got 200 OK, and one afternoon vanished.** The project now carries a version: a save that would overwrite somebody else's work is refused with who wrote and when, and offers to overwrite. Whoever sends no version keeps the old behaviour, so imports, scripts and tests need not learn a protocol.
- **A project served from its backup arrived without saying so.** When a project file cannot be read the store falls back to the last valid copy — older content — and in the same instant the version marker answers «I cannot tell», which lets the save through: the older version then became the project. The browser now warns when the project opens, before it can be saved over, and says which of the two failures happened. A project whose file broke also stays *in the list*, rebuilt from that same copy, instead of disappearing while its content sits intact beside it.
- **Credentials were blanked from a hand-written list, in two copies** — so a fourth secret field would have gone out in the clear with the suite still green. The container is now classified field by field, and both consumers ask that classification for the list instead of keeping one each.
- **A project export carried measurements by construction of a blocklist** that had to be remembered; it now follows the field classification.
- **Running the headless test suite could overwrite your organisation.** The end-to-end harness redirected every store to a temporary directory except that one.
- **Deleting one of two identical networks deleted both.**
- **Every port MAC was lost when importing from NetBox 4.2 or newer**, which moved MAC addresses into objects of their own; and a trunk's native VLAN now survives the import.
- **The import knew which port the WAN line lands on and threw the answer away** — the first question of a line that is down.
- **Two sites sharing one id both went in, and one of them vanished from the map.**
- **On a hub-and-spoke map the sites sat on top of each other** and the links disappeared underneath.
- **A bandwidth no line can have reached the recovery card**: `0`, `-100` and anything else that is not a measurement.
- **Which project opens at start-up was a coin toss** — `updated_at` is truncated to the second, and two projects saved in the same second left the order to the filesystem.
- **The floor plan grid ended and you could walk off it**; a storey could be drawn but not moved or deleted; and the properties panel called a storey a room.
- **«Sites and links» jumped back to the top of the list on every change**, so editing the eighth site meant scrolling back to it each time.
- **«Save» has its word back at 1920**, where most screens are: it had been collapsed to its icon by a measurement taken on the other side of that width.
- **The sub-header said «No project» while a project was open**, and a toolbar button's height depended on whether its label happened to be visible.
- **The import result printed a raw `{n}` at the reader**, and had no singular: «1 linee», «1 servizi» and four more like them.

### Changed

- **A link between two sites answers two questions, not one.** «MPLS» and «IPsec» sat in one dropdown, and they are not alternatives — an IPsec runs *over* an MPLS every day. The model now carries what it runs on (`transport`) and what runs on top (`tunnel`), the technologies went from five to twelve, and the words are the ones a carrier's contract uses rather than ours: *technology* and *topology*, *overlay* and *underlay* explained where they are used.
- **A WAN line's bandwidth field is the port's, not the contract's** — in the panel, in the dossier and in the import, which reads `port_speed` from the site-side termination and never the circuit's `commit_rate`. They are different numbers: on the reference archive one line sells 100 Mbps over a 1000 Mbps port. The field arrives empty more often as a result, and the panel counts and names the lines it left empty.
- **A WAN line carries public addresses, plural**, because one was false as soon as there is a routed block, an IPv6 alongside it, or an HA pair; both ends of a link name their device and their site rather than «first» and «second»; and a link says who sells it and under which circuit id whatever its kind.
- **The coherence panel states the fact and stops adding what follows from it**, says each thing once instead of repeating its heading on every row, and «I could not check» stopped meaning «there was nothing to check».
- **The recovery card became a checklist rather than an inventory**: an IPsec link carries the two proposals you actually retype and a pointer to where the key lives — never the key.
- **The Provider field suggests the providers already in the document** and still takes anything typed, matched on identity rather than on spelling.
- **The map's chips say who sells a link**, a site's colour says whether a finding names it, and clicking a link's badge opens the row that describes it.
- **The «add» buttons moved into the tab bar**, one per tab, so adding the twelfth site no longer means scrolling past eleven; Save sits next to the project bar; and the multi-site button says «Multisito» instead of being an icon with a tooltip.
- **The NetBox import stops flattening the location hierarchy into a name**, and the box reporting a WAN read went from 389 pixels of prose to three lines and two drawers.
- **The overview stopped declaring it was not looking at the WAN**, which had stopped being true.
- **Two fields left the multi-site model** rather than staying as free text nobody filled, and the panel's lists are comma-separated like the rest of the app.

### Removed

- **The «next step» nudge is gone from the sub-header.** It sat in the middle of the bar on every screen, permanently, telling you what to do next.

## [2.10.2] — 2026-08-27

### Fixed

- **The port table stopped writing «1» into the VLAN field of a port nobody had declared.** `_effPortVlan()` always answers — with no source it falls to the site native, which is the right answer for the *cable* and not for the *port* — and writing it inside an editable input turned it into a statement. The properties panel already knew this and left the field empty with the number as a placeholder, so the two disagreed about the same port at the same instant. The test is now `hasPortVlan()` in `app-util.js`, twin of the `hasPortStatus()` that was already there for the identical problem on port state, and both renderers ask it.
- **The L3 report stopped calling «declared» the VLANs a project is merely born with.** A new project seeds `vlanColors` with 10/20/30/40/99 as a palette, and every one of them earned a row reading «declared VLAN, no network» — declared by nobody. A colour is not an act; a name is, and so is having chosen who routes it. ⚠️ The palette itself stays whole, so a prefix citing VLAN 40 keeps its colour: what narrows is who earns a row of their own. It is the same warning `ipam-audit` already carried — it compares against what was declared and never against `vlanColors` — which had not reached here.
- **A model recognised in a scan could take the wrong vendor's model when its designation collapsed to a short number.** Stripping the leading vendor code off a model core left short shared numbers that met across vendors — a TP-Link «TL-SG1008D» resolved to «D-Link DES-1008D», a D-Link «DES-1024D» to «Fortinet FortiSwitch 1024D», both reducing to «1008d»/«1024d». A false exact on the SOHO long tail is the very kind of invention the recogniser must not make. A core now carries an EXACT only when it stays specific — it keeps a separator («2960-24TC-L») or has at least two letters («SRX300», «GS308E»); a core collapsed to a short number is left to the family step, which needs two or more variants and can never assert a single wrong model. Vendor-neutral, a rule on the shape of the core and never on the brand, and it subsumes the earlier bare-number guard. Found by a broad SOHO sweep across vendors.

### Added

- **A cable whose device carries an IP in a different VLAN than its port now says so.** The colour follows the endpoint's declared-IP VLAN while the port shows its own; instead of the two disagreeing in silence, a sober warning (`ip-vlan-mismatch`) names both numbers. The verdict is decided once in `link-vlan-color` and only reported by the cable validator, like `vlan-ends-disagree`.
- **A trackpad pinch now zooms the floor and the rack in Safari.** Chrome and Firefox deliver the pinch as a `ctrl`+wheel event the zoom already caught; Safari sends its own `gesture*` stream with a cumulative scale, and nothing was listening. The scale ratio now drives the same `zoomFloor`/`zoomRack`, with the point under the fingers held fixed.
- **Clicking a cable in the dashboard now lights up its path on the floor.** The «Cables» list opened one endpoint of the row; a cable row now carries its link id and, on click, leaves the Panoramica and traces the whole physical route (through patch panels) across the floor and the rack, with the «Physical path» panel open — reusing the same highlight the topology view and the port popup already use.
- **The dashboard’s subnet, gateway, VLAN and LLDP/CDP neighbour rows are clickable too.** A subnet or gateway opens its network detail in «Networks»; a VLAN opens its card in «VLAN»; an LLDP/CDP neighbour that matches a documented cable lights up that cable’s path. Each one leaves the Panoramica for the place where the item is edited or shown — the same move the cable rows already make.
- **A device that answers on more than one NIC is imported as one box, not one per address.** A multi-homed server or NAS shows up in a scan as several rows — one per responding IP, each with its own MAC — and each used to become its own node. A pre-pass (`lib/host-merge.js`) now folds those rows into one, on *authoritative* keys only: the device's own IP table, its serial number, its SNMP engine-id, its mDNS UUID — never a shared name, a MAC or a model id, which identical or neighbouring boxes also carry. The fold is shown (a «+N NIC» badge that names the key) and reversible (a «split» control), and a hard veto keeps two boxes apart the instant their serials disagree.
- **Each interface now shows the IP it owns.** The port panel always had a per-interface address field — a router has one address per interface — but nothing filled it. A Sync now takes the address the device publishes for each interface (from its own IP table, matched to the interface by index) and writes it on the matching port, fill-if-empty so a hand-typed address is never overwritten. It is also how a folded second NIC resurfaces: the box declares the interface, and its port carries the IP on the next Sync.
- **A model string measured in a scan can now be matched to the catalogue, with a confidence it never overstates.** The device-type resolver (`lib/device-catalog.js`) only matched exact keys and only ran in the DCIM import; it gains a fuzzy `model-core` stage and a separate `confidence` field. An order code or an ENTITY-MIB `entPhysicalModelName` resolves `exact`; a bare family root like `2960` resolves `family` — several variants, carrying no entry, so ports and rack height can never be auto-filled from a guess. A part-number index (the order code many devices report verbatim — `WS-C2960-24TC-L`, `J9776A`, `ISR4331/K9`) is tried first; a bare number is barred from `exact` (no «Nexus 9000» → «ION 9000»), and version strings, unmatched part numbers, OS banners and a virtual verdict all decline. Opt-in (`{fuzzy:true}`), so the DCIM import stays exact-only and byte-identical. A golden trap-table is the oracle.
- **A scan now recognises the device model and proposes it, without ever applying it on its own.** Each discovered row is resolved against the catalogue — from its `entPhysicalModelName`, else its `sysDescr`, else its mDNS model (the resolver self-filters banners, so feeding `sysDescr` is safe: a Zyxel reports «GS1900-24» there, not in ENTITY-MIB, measured on a real /24) — and a chip in the results names the match, `exact` or `family`. On import the recognised model rides onto the node as a *measured proposal* (`modelMatch`), never as the editable `model` field and never touching the ports; the properties panel shows an «Adopt» banner, and only an explicit click applies the full template — the same manual choice, one step shorter. Vendor-neutral, no per-brand rule.

### Changed

- **The neutral cable colour is kept a step lighter than the inactive-port grey** (`#6b7d99` → `#a6aab1`), so a neutral cable (multi-VLAN trunk or routed) crossing a port at rest stays visible instead of blending with `--inactive-color`. One definition — floor, rack, topology, PDF and draw.io all follow.
- **The port panel’s VLAN section reads at one size.** Its explanatory sub-texts — the trunk summary, the VLAN-name tag, the range hint, the «N VLANs configured» line — now use the hint size like every other hint (secondary weight is carried by colour, not a smaller body), and the carried-VLAN field matches the native-VLAN field instead of a smaller monospace box.
- **The cable diagnostics now read in one voice, and the trunk carried-VLAN warning is legible again.** When one trunk end carried no extra VLAN the warning printed a bare «—» as the subject of the sentence («— only on one side»); each side is now named after a label, so the placeholder reads as «none». The Italian dropped the untranslated «Allowed VLAN» / «allowed-list» for the app’s own «VLAN trasportate» (carried VLANs), and the three VLAN-mismatch titles — access, native and carried — now share the «not aligned» register their own fix line already used («Align…»). Two smaller alignments came with it: «Max speed» to match the field label, and «permanent link» for the TIA-568 term.

## [2.10.1] — 2026-08-23

### Fixed

- **A VLAN nobody declared is left absent, not written down as 1**, and the measurement no longer outlives the reading that produced it. On the bench a Cisco vIOS publishes neither table that carries an access port's VLAN, and all nine of its ports came out on VLAN 1.
- **A port bundled into an aggregate inherits what the aggregate declares.** The lookup used the logical id where the index was the interface index — `Po1` is id 1 at index 10 — so the block had never once run.
- **An interface that calls itself virtual is not a port you can cable.** A Cisco wireless controller publishes «Virtual Interface» beside its real port, and was documented with two.
- **A management interface written out in full is the one announced in short.** «Management Port» in the interface table is «mgmt» over LLDP, and the cable was lost without a word.
- **The native VLAN of a Cisco trunk is read instead of assumed.** It lives in a third column nothing was asking for, so every trunk read VLAN 1 — right by coincidence until someone sets `switchport trunk native vlan 99`.
- **A cable's colour asked the wrong question, in eight places, of the wrong device.** It painted the *native* VLAN, which on a trunk is one among several; an access cable now takes the one VLAN that applies and a trunk takes none, the authority is whoever *switches* the VLAN rather than whoever is typed «active», and one module decides for the rack, the topology, the floor plan, the PDF and both exports — which had already drifted into painting the same cable two colours.
- **A cable whose two ends name different VLANs no longer picks one and asserts it.** It went out as «VLAN 20, set by hand», the same hex as a cable whose ends agree, in the one state where the drawing was certain and the network was broken.
- **A switch declared in a stack could make the whole properties panel disappear.** The guard accepted two shapes and the panel read only one, so on a node the guard had just approved the read threw and took everything with it.
- **A device panel stopped filling itself in, and a camera stopped specifying itself.** Ninety-eight editable fields opened pre-filled with values nobody had declared and nothing had measured; clearing one wrote the default straight back, so «I don't know» was not expressible.
- **A device missing from the network had its red ring painted over by the device below it**, leaving a single horizontal line that reads as a rendering artefact.
- **A port declared faulty was exported grey, in all three exports.** Two functions shared the name `normalizeStatus` on `window`; the port one is `normalizePortStatus` now. ⚠️ On screen the red was correct — it was wrong only in the file you take into the machine room.
- **A cable whose two ends contradict each other is a finding, not just a grey line.** Neutral is also what a trunk and a routed link look like, so the one case where the network is genuinely broken was the only one without a name.
- **A VLAN the document carries and the plan never names is said out loud.** ⚠️ Not compared against `vlanColors`, which fills itself with every VLAN ever *seen* — the check would have been green on every project ever.
- **A hierarchy you wrote by hand can say it is one.** A `/16` with its `/24`s inside it was accused of overlapping its own subnets on every opening of the report, and the accusation was impossible to close.
- **A cable that does not exist no longer reports itself as carrying VLAN 1.** ⚠️ That literal 1 was also a second definition of the site native, which on a site declaring 99 still said 1.
- **Every VLAN on every Cisco switch was named «1».** The VTP fallback read column `.2` of `vtpVlanTable` — the state — instead of `.4`, the name, and the overview called the result a fabric in disagreement: five VLANs «in conflict» on the bench, four born from one digit of an OID.
- **The routed entry in the VLAN legend is a badge like the others, and it filters.** It read «instradato» at four times the width of a VLAN badge, and it was inert.
- **The strongest proof state of a cable is called «Strong», not «Fresh».** The badge names how strong the adjacency is, not how recent it is.
- **A MAC the network places at a documented device's address is no longer accused of being undocumented.** Infrastructure exposes no device MAC over SNMP, so the accusation was true in form, wrong in substance, and impossible to close.
- **A host the crawl only saw in an ARP table is asked who it is before being described.** Three pieces of bench infrastructure were each typed as a PC, one UDP round-trip away with credentials the crawl already had.
- **A disagreement about a VLAN name says who disagrees, and stops inventing ones that are not there.** It printed «DATA ↔ DATA» in the very case the check exists for; and names now compare through a case-folding key, since `default` and `Default` are one name written by two vendors.

### Added

- **A port can be declared L3, so a hand-drawn project stops claiming a router-to-router cable switches.** «Routed» was measurable and not sayable, and that cable fell to the floor and was painted VLAN 1 — not a missing answer but a wrong one. It is the third value of port mode rather than a switch beside it, and it can name which declared network it routes, which gives the device's SVI section the port column it was missing.
- **A routed link is told apart from one whose VLAN we simply cannot read**, and «routes» is measured as «is not a port of the bridge» rather than «owns an address», which any host does. ⚠️ Asymmetric on purpose: being in `dot1dBasePortIfIndex` is a veto, being absent proves nothing — a vIOS publishes it for two ports out of eight.
- **A cable that switches always has a VLAN, so it always has a colour** — every bridge port has a PVID, and where nobody configured one it is 1. The neutral is now a colour of its own and means exactly two things: a multi-VLAN trunk, or a routed link.
- **The cable panel says what the cable is and why** — «VLAN 99», «trunk — 4 VLANs, none prevails», «routed link» — with the source in plain words, and a trunk's carried VLANs as coloured pills.
- **A trunk whose declared VLANs no longer match the measured ones says so**, and the two ends are compared on what they carry rather than on the native alone — the most common cause of «it works for some VLANs and not others».
- **A router-on-a-stick stops being invisible.** A dot1Q sub-interface was discarded as «not physical», taking the VLAN and the address with it — on the bench the management address of a CSR1000v sits there, so the interface InfraNet was talking through did not exist in the document.
- **An address that lives outside every declared network is something the app says out loud**, judged per address family so a plan with no IPv6 accuses nobody.
- **«I could not check» stopped looking exactly like «there is nothing wrong».** Two fallbacks returned the shape of a clean network, and an audit that says nothing is believed.
- **A bundle is checked for how many members it has and where they live.** A group with one member aggregates nothing, and members on two devices only bundle if those devices are one logical switch.
- **A LAG's VLAN can be declared once, on the bundle**, instead of on every member with a coherence warning for the one you forgot.
- **The handover dossier prints what a trunk carries** instead of its native VLAN, which is the least informative of them.
- **The declared operating status carries the padlock the other hand-set fields already had.** It was set invisibly and could not be cleared.
- **The rack's zoom percentage became the way back.** Past a certain zoom the chassis falls outside its window and the only way back was the minus button, 10% at a time.

### Changed

- **Everything about a cable's VLAN lives in one collapsible section**, with the port mode moved up among the badges that answer «what is this cable». A trunk used to state its VLANs four times over; the pills carry it alone now.
- **A port has three states, and «idle» is no longer one of them** — one word had been doing four different jobs, and nothing read it to decide anything. The device's own word survives as `operWait`, printed untranslated.
- **A port with no link for three verifies is amber, not dark grey.** A `shutdown` is a decision and stays quiet; no link is an ambiguous symptom somebody has to go and look at. ⚠️ A deeper `#d29922` than the generic warning amber, written in three places since the exports cannot read a CSS token, with a test comparing them.
- **The status badges are bigger and stay on one line**, verified across all 1,680 combinations the row can produce.
- **Every info box in the properties panel states its rule in one line, at one size**, with the rest in its tooltip. They had carried five hand-written sizes, four of them off the type scale, and one box was written in Italian inside the renderer. A character budget guards them now.
- **The LAG section speaks the panel's language, and the proof badge's shape left the code that builds it.**

## [2.10.0] — 2026-08-20

### Added

- **NetBox locations become rooms on the floor plan**, with their racks and devices inside them. ⚠️ NetBox has no geometry — that the room exists is measured, where it sits and how big it is is our choice — and what has no location stays **outside** the rooms rather than in one nobody declared.
- **Hosting virtual machines is a capability, not a type.** Storage, NAS and servers carry the same VM section a hypervisor has, and the import stops rewriting a device's type to make room for them.
- **A declared life-cycle status** — planned through out of service. It changes how silence is read: a planned device that stays quiet is expected, and one declared out of service that *answers* is flagged.
- **Imported objects keep their DCIM identity** — ports, patch-panel slots and racks carry the id of the object they came from, so renaming a rack upstream is a rename and not a delete followed by an arrival. ⚠️ The reference never lives inside the InfraNet id, because ids get renumbered.
- **Two devices that announce each other are drawn as adjacent even when the port is unknown**, with `?` where the port would be rather than one picked at random. And a neighbour that produces no cable now says why — the silence used to look exactly like a device with no neighbours at all.

### Fixed

- **An LLDP neighbour is read from the subtype it declares, not from the length of its value.** «Six bytes means a MAC» lost the addresses written out in full and invented others out of port names — and the encoding is the *answering* agent's choice, not the advertiser's.
- **The local port of a neighbour is read, not assumed to be an ifIndex.** On the bench Arista, `Management1` is LLDP port 97 and interface 999001, so the cable was dropped without a word.
- **A port a neighbour identifies by MAC resolves to that port** instead of falling back to the first free one and being marked inferred.
- **A room follows the cursor while you drag it, and the grid you see is the grid you land on.** The live update was skipped in silence for rooms; and the grid was drawn at 40px while things snapped at 20.
- **The assistant no longer says every switch is full.** It counted port *records*, which only exist once a port is documented — 291 free ports per the Overview, 0 per the assistant, on all fourteen bench switches.
- **A PDU stops having two outlet counts.** «Apply model» wrote one level above `node.spec`, so the rack grid and the Overview disagreed by design.
- **The public API counts a device as SNMP-managed only if it can be polled.** Picking a driver was enough, so a device with no address landed in the `snmp_managed` Ansible group with nowhere to connect.
- **The Dashboard stops declaring a full network empty.** Loading a project reset the view flag while the page stayed put, so the Overview bailed out of every render and kept the empty screen drawn before the project arrived.

## [2.9.2] — 2026-08-18
**Power gets documented the way it actually fails: by group, not by socket.**

### Added

- **Outlet groups on UPSs and strips.** A group answers the question a UPS is bought for — what stays on when the power goes, and what can be shed to last longer. Two axes, measured across APC, Eaton, Vertiv, CyberPower, Ubiquiti and others: whether the group can be switched on its own, and whether the battery holds it. The rack repeats each group as a colour band above its outlets, over the fill that shows state; the handover dossier prints them. **Declared, not measured**: RFC 1628 has no outlet groups, every brand keeps them in a private MIB.
- **A UPS from the catalogue arrives with its outlets.** The slim catalogue kept only network ports, so 53 UPS models carried none. They now come across with name and socket type, and the group is read from the name where the maker wrote it — 56% of UPS outlets, measured. What the name does not say stays blank: position is not evidence. Applying a model never costs you work: what you wrote on an outlet survives, and groups you declared are not renamed.
- **`--from-canonical` rebuilds the slim catalogue without the network**, reprojecting it from the canonical file on disk and leaving the manifest and diff untouched — the source did not change, only the projection.
- **The rack tells a port shut on purpose from one merely idle.** InfraNet read only whether a port has link, never whether someone turned it off. The two off-states now share a monochrome scale — near-black for shut by hand, dark grey for no link across three verifies — and the PDF and draw.io export print the same colours. A cable you drew over a shut port gets a "Port shut" badge: your word and the switch contradict each other. A status you declared keeps your colour, and the reading expires when the switch stops confirming it.
- **The Dashboard says out loud that it never looks at port access control.** 802.1X, MAB and port-security were missing from the list of dimensions the summary does not judge, with a "Security" lens right above it — and there silence reads as absolution. Declaring it beats reading half of it: "authorized" alone cannot tell an authenticated supplicant from a port forced open.
- **Wi-Fi from a DCIM arrives as Wi-Fi.** An 802.11 interface is an antenna, not a port: radios now come in as radios with band, standard and their BSSs, up to the eight the model allows, and the ninth is declared rather than dropped. The channel enters only when comparable — a wide channel names a block, not the primary 20 MHz one — and WPA2 goes in as the common case with a row saying so.
- **Virtual machines land on the host that runs them.** A cluster with exactly one imported member is unambiguous, so the VM goes there and the reading is declared as a reading; a cluster with two or more says nothing, so the VM stays out with the reason. Adapters bring MAC, VLAN and addresses, and megabytes become gigabytes out loud rather than inside a division.
- **IPAM roles can fill the four VLAN declarations that read them** — management, voice, guest, native — matched once per role, not once per VLAN. "Access - Wireless" is not the guest network: a rule reading the word "wireless" would have declared the whole corporate network a guest network.
- **Compare with the DCIM, without writing anything.** Rows show the document value and the DCIM value side by side. A device you added by hand is never reported as "gone from the DCIM" — that would accuse you of your own work — only declared fields are compared, and "different" is not "wrong": with no per-field timestamp nobody can say who changed it.

### Changed

- **One site, one project.** Step one is the site list, and an imported project records the slice of the DCIM it was born from — measured from the devices that really came in. The comparison re-reads exactly that slice: against a single-site project, comparing everything reported 181 new devices, all true and all useless. The instance address is deliberately not written into the document, which gets exported and passed around.
- **The import preview says each thing once.** Four ways of stating the same numbers, and two attention signals whose counts disagreed — the banner counted devices, the row counts decisions. Now the estimate is the only statement of numbers, losses join it as chips that jump to the row explaining them, and rows whose default is already right collapse to one line.
- **The manuals no longer cut a box in half at a page change.** 9 split blocks in Italian and 8 in English, measured on the rendered PDFs, now zero — at the cost of one page.

### Fixed

- **A UPS's outlets stopped being thrown away on import.** Nothing in the model was missing: six copies of `type === 'pdu'` across import, panels, rack and PDF kept them out. They now ask one predicate. A UPS you documented by hand keeps its front panel until it really has outlets. **The ATS stays out on purpose**: its two inputs are the point, and outlets without them would imply the redundancy had been captured.
- **Nine kinds of device could not come in from a DCIM, and one came in wrong.** Cameras, phones, PCs, displays, projectors, door controllers, sensors, tablets and KVMs had no entry, so eight different terminals produced eight copies of one generic — the *rack* generic, even with no rack, and the name-guessing rule filed a "KVM Switch" under switches.
- **Renaming a radio changes the labels that name it.** The name lived on the device and nothing that composes a label ever read it: six independent derivations each sliced the internal port id, so a radio printed "Pradio2" whatever you called it. One function answers now — including the names the DCIM import had already brought in and never shown.
- **A shut port, and a reading that never arrived, stop counting as evidence about the cable.** Both fed the down-streak that promotes an inferred cable to ghost and darkens a port. A decision is not an observation, and an unanswered poll is not a measurement: link state is now measured into its own field with three states — link, no link, **not measured** — and `unknown(4)`, which the RFC defines as "the agent cannot determine", no longer comes out amber.
- **"Replaced device" is no longer claimed from a reading that never happened.** A walk cut short left only the low ENTITY-MIB indices and the survivor was crowned, overwriting a good measurement and then being compared as if fresh. A measured identity now has three states — reconfirmed, **last known** (kept and dated), never measured — and whoever accuses asks the same rule and stays quiet unless it was reconfirmed.
- **A container prefix no longer counts as overlapping the networks inside it.** An IPAM plan is hierarchical on purpose: on a seven-network plan of the usual shape, eight conflicts, none real. Same for one space declared twice in two VRFs. Both are recognised and counted separately rather than silently dropped.
- **An address the import had worked out now reaches the port it belongs to.** The address is the socket's, not the device's. A router that used to arrive with one address now arrives with five, and what cannot fit — a second address, an IPv6, an interface with no port — is counted and declared.
- **"Next free IP" stops offering an address the plan has already booked.** IPAM reservations sit on no device but are not free either: they now come in, land on the network rather than on a device, and keep their own share of the occupancy bar.
- **A count taken across the whole archive no longer reads as a count of what you are importing.** Importing a site with six unattached addresses announced a hundred and eighty-six. Narrowing it properly costs one call per network; until that is worth paying, the sentence says what it actually counted.
- **Three fixes to one row of the import panel.** A raw `{total}` placeholder printed as-is, because a closed allow-list of context keys had never heard of it; an example dumped eighteen networks instead of three; and a row speaking for a whole reading counted itself as a single case — "One IP address stays out" over a hundred and eighty of them.
- **A tooltip showed a raw translation key.** "Trunk symmetry" was declared by the engine with no wording in either language, and the it/en parity gate could not see it: the key was absent from both, so the symmetry was perfect while the hole was too. A structural test now checks that every declared dimension carries its label and tooltip in both languages.

## [2.9.1] — 2026-08-14
**Three answers that could be wrong are now right.** A patch release: no new capability, only correctness in places where the app was stating something it did not know.

### Fixed
- **A declared network is recognised as declared, whatever its shape.** The Verify and the Overview each read a CIDR with a reader of their own, both IPv4-only: an IPv6 network you had declared never earned its badge, and "declared if it sits in a subnet of /24 or wider" was a threshold from when every row was a /24 — a /22 or a /25 was judged against 24 instead of its own size. One reader answers now (`lib/cidr.js`), a row is declared when a declared network contains it and is no narrower, and the list counts the networks with no VLAN too.
- **A sighting with no date no longer counts as one from this morning.** Confidence fades with age, but only when the record carried a date; without one it never aged and scored like something seen yesterday. An unknown age now weighs as much as a month of silence and is shown as "Undated" rather than hidden — it is not called stale, and no age is invented: not knowing is the finding.
- **The AI chat being admin-only is recorded as a decision**, not a default waiting to be relaxed: opening it to viewers would take a rate limit and a spending budget first, not the removal of the gate.

## [2.9.0] — 2026-08-14
**A network is a thing of its own, and every address is checked — not just the first IPv4.** The subnet stops being a field of the VLAN: a network is first-class, its VLAN optional, the shape every real IPAM uses. Project format goes to schema 2, migrated in place on open.

### Added
- **A "Networks" panel** — every declared network in one list, sorted by address, overlaps spelled out. Add several at once; edit an address in place, delete one row, or clear the plan (both "clear all" actions ask first).
- **A VLAN can carry more than one network** (dual-stack, or a second range on the same SVI), and **a network can have no VLAN at all** — on a real NetBox, most of the plan.
- **IPv6 can be declared**, not just measured: a bare address normalises to its /64, and a /64 shows the addresses actually seen instead of a meaningless percentage.
- **`prefixes` in the REST inventory**, with VLAN (`null` when none), name, gateway, DNS and provenance.

### Changed
- **"Networks" comes before "VLAN"**: the addressing plan is the document, a VLAN is a label a network may carry. The VLAN card shows its networks and takes you to them; it no longer writes them.
- **The two gateways are named**: "Gateway (IP)" is the address, on the network; "Routed by" is the device, on the VLAN.
- **The L3 map is one row per network, not per VLAN**, and wider so device names are not cut.
- Addresses, CIDRs and MACs use the interface font with tabular figures; code and logs stay monospace.

### Fixed
- **Presence no longer depends on how a MAC is written**: a dash-separated or Cisco dotted address stopped matching and the device went grey forever. The five MAC normalisers became one, so a dotted MAC is also recognised as virtual or randomised.
- **A declared network's real mask decides, instead of an assumed /24.** That assumption was written in five places and decided three verdicts: a device nobody probed reported **absent**, a router invented inside a /22, dead hosts resurrected as discovery candidates.
- **No IPv6 gateway was ever checked**, and an IPv6 network read as empty: the L3 report and the overlap audit now walk every declared network, both families, comparing addresses by identity rather than by spelling.
- **An address written on a port counts everywhere** — occupancy, "next free" and the L3 map had all ignored it.
- **The assistant is told the whole documentation, not a third of it**: 23 of 30 documented parameters never reached the model, which answers only from what it is given. Secrets are unaffected — every block is an explicit allowlist.
- **Topology and cabling**: a cable inferred twice is removed even when the real one is corroborated; the crawl no longer overwrites a pinned name; a trunk set by hand stays a trunk.
- **The DCIM import stops throwing prefixes away** — 51 of 90 on a real NetBox were VLAN-less — and declares what stays out.
- **Smaller correctness fixes**: a link-local is not a duplicate (RFC 4007); an empty VLAN key no longer becomes "VLAN 0"; SNMP no longer stamps "VLAN 1" on a port it never measured.

### Security
- **The AI chat is admin-only**: it spends the admin's key and sends the project's topology to an external provider.
- **A colour from an imported project can no longer run as code**: a `vlanColors` value that is not a plain `#rrggbb` is snapped to neutral grey on load.

## [2.8.2] — 2026-08-12
### Added
- **An address on the port**: a router answers on one address per interface, and each one can now be documented where it lives.
- **Draggable columns in the Discover table**, with the Name column no longer repeating the IP that already has its own.

### Changed
- **Retention ceilings raised** where they were cutting history short (change journal, discovery sightings, verification timeline).
- **The change journal moves out of the project file**, joining presence and sightings in `history/<id>/`.
- The PDU loses its "Orientation" field, and its outlet chips are easier to read.

### Fixed
- **A device that will not answer SNMP no longer looks like a device that is gone**: an authentication failure is not an absence.
- The import preview stops announcing a limit that does not exist.

## [2.8.1] — 2026-08-12
### Added
- **Declared by the DCIM** — a read-only block in Properties showing what NetBox says, kept apart from what you declared.

### Changed
- **Presence lives in exactly one place**, and discovery sightings move out of the project file too: up to 96% smaller on a small project.
- **Fields nobody reads are gone** from the project file.
- **Portable exports carry the document only** — no presence, no sightings, no derived VLANs.

### Fixed
- **Looking at a project no longer changes it.**
- The browser tab has an icon.

## [2.8.0] — 2026-08-11
**Sync with NetBox, and a hardware catalogue that speaks its language.** Import is free; write-back is a paid module.

### Added
- **DCIM/IPAM import from NetBox** (free): base URL + API token stored server-side, progressive preview, selection by site/role/tag, explicit reconciliation. Devices, interfaces, cables, VLANs, prefixes, IP addresses and racks come across.
- **Imported racks are placed on the floor plan**; a cabinet with devices on both faces becomes two InfraNet racks (front / rear).
- **Patch-panel cabling**: front/rear-port terminations become a native pass-through chain, so a run through a panel stays one run.
- **A virtual chassis is imported as a stack**, a declared platform picks the Ansible network OS, and a device description is kept as a note.
- **PDU model**: up to 48 passive outlets inside the rack frame, an editable outlet list with what each one feeds, NetBox outlet status mapped to active/inactive/faulty, and management/console/auxiliary ports told apart.
- **Power chapter in the PDF report and the handover dossier** — one PDU per block, one row per outlet.
- **Vendor-neutral hardware catalogue** from the CC0 source, projected into a neutral port-slot model (management, copper, SFP/SFP+, QSFP…).
- **Portable, versioned project JSON**: projects carry a schema version; exports declare their format.
- **Write-back to NetBox** as a **paid module**: dry-run diff, create-or-PATCH by natural key.

### Changed
- **The import's reconciliation step is a list of decisions, not a list of warnings**: each row says what will happen, what it costs and how to change it. The separate manual-reconciliation panel is gone, absorbed into the same list; changing a decision no longer re-reads the whole DCIM.
- **The import declares what it leaves behind** instead of dropping it quietly, and "not in service" is a decision you make rather than a filter applied for you.
- **Device notes travel with their device** in the handover dossier, instead of a separate chapter to cross-reference.
- **High-density SFP rendering** for devices with large optic blocks.

### Fixed
- **A prefix with no VLAN is no longer documented as "VLAN 0"** — the first-class network of 2.9.0 starts here.
- **A device you switch off turns red, and stays red after a reload**: a measurement is not an edit, and it is kept beside the history rather than inside the document. A stale SNMP success no longer hides a proven absence, and an SNMP failure is drawn as a fault, not as a disappearance.
- **Verify, SNMP poll and topology stay responsive on large networks**: the port→node resolver went from scanning every node per port to constant time (4.4 s to milliseconds on 500 devices).
- **Port identifiers containing hyphens are resolved safely** across correlation, topology, drift and SNMP.
- **Manual brand edits stay manual-first**; discovery and SNMP no longer replace a brand you typed.
- **A PDU outlet set to "inactive" by hand stays inactive**, and a PDU in Ethernet management mode always shows its cable-able port.
- **The default "trunk + access" topology view shows all rack cables**, with the same emphasis as the exclusive states.
- Deleting a project also removes its timeline and snapshots; the import preview no longer multiplies the VLANs it reports.

## [2.7.3] — 2026-08-09
### Changed
- **A hand-typed device name is manual-first**: Discovery no longer renames what you named.
- **A retype is proposed, not applied**, on a device that is already documented.
- **A hand-set port count is manual-first**: SNMP proposes a different count instead of raising it silently.

### Fixed
- Drift's IP-change no longer overwrites a hand-pinned SNMP host.
- **Rack devices show honest presence**: absence is drawn where it was measured.

## [2.7.1] — 2026-08-06
### Changed
- **One "Automatic monitoring" replaces two schedulers** — a single timer with two depths, instead of an SNMP poll and a Verify going their own way.

## [2.7.0] — 2026-08-06
**The project can look after itself, and you can go back.**

### Added
- **Autosave** (opt-in, off by default): after a real change the project saves itself.
- **Scheduled Verify** (off by default): a silent documentation check on a timer.
- **A verification timeline**: each Verify appends a dated row, so drift has a history instead of only a last-known state.
- **Restorable snapshots and a unified History panel**: full-state snapshots you can go back to.

### Fixed
- The header stays on one row when the runtime badges appear.
- The "restore point created" feedback is visible from inside the History panel.

## [2.6.2] — 2026-08-05
### Added
- **The Security lens flags a wireless client whose IP VLAN is not its SSID's VLAN.**

### Fixed
- The VLAN filter matches the cable's real carried VLANs, and follows a wireless device to the VLAN of its SSID.

## [2.6.1] — 2026-08-05
### Changed
- The header buttons are evenly spaced and the search bar takes the free space; Verify sits centred between its dividers.

### Fixed
- On short screens the Dashboard columns scroll their own tiles, and an open drill-down list stays readable.

## [2.6.0] — 2026-08-05
**A cable says how well it is known.**

### Added
- **Proof-state for cables** from a pure engine: fresh or weak for an inference, ghost for one no longer corroborated, declared for one you drew by hand — a silent manual cable is *declared*, never a ghost.
- **A Truth Score on the Dashboard**: how many cables are ghost, inferred, declared.
- **A proof-state badge** beside the LLDP/CDP/MAC provenance, in the cable properties and the list.
- **Per-port miscabling detection**: the neighbour a port announces is compared with the cable you declared, and a mismatch is shown rather than silently trusted.

### Changed
- Inferred cables render dashed, ghosts attenuated.
- The drift "ghost cable" counts only inferred cables: a manual cable on a long-down port is a device down, not a phantom cable.

## [2.5.2] — 2026-08-04
### Added
- **Discover associates the scanned subnet with a VLAN**, so the same subnet is not typed twice.
- **A reveal toggle on every masked password field** — SNMP communities, the AI key, DCIM tokens.

### Changed
- The VLAN filter badge moves into the sub-header; cable tooltips join each device with its port on one line.

## [2.5.1](https://github.com/muttley1973/infranetpro/releases/tag/v2.5.1) — 2026-08-02
### Fixed
- Cable rendering ignores property-panel controls that reuse a port ID, so traces no longer start from the wrong place.
- Custom manual property values survive a re-render.

## [2.5.0](https://github.com/muttley1973/infranetpro/releases/tag/v2.5.0) — 2026-08-02
**The Overview becomes the Dashboard, and the report menu is absorbed into it.**

### Added
- The **Live reading** and **Management VLAN** tiles open a list, like every other tile.
- A **wireless VLAN coherence** verdict in the Security lens.

### Changed
- **The "Reports" menu is absorbed into the Dashboard**: free ports and the L3 map are drill-downs of the tiles that already state their numbers.
- **Free ports headlines real switch capacity** — the port where a new device actually goes — with the rest counted apart.
- The **Gateway** tile opens the full subnet→gateway map; the **Default community** tile names which known default a device answers to.
- The toolbar spells out its actions, and the freshness chip shows the age of the last SNMP read, colour-coded.

## [2.4.0](https://github.com/muttley1973/infranetpro/releases/tag/v2.4.0) — 2026-07-31
### Added
- Two legend pills **cycle through three states** (wired/wireless, trunk/access) instead of two.
- **Cables the active filter picked are drawn thicker**, whichever way it is filtering.
- **Every fan-out line can be clicked**, not only those of the open rack.
- **A passive hop can be declared occupied**: on a patch panel or outlet, *active* means a cable is in it.

### Changed
- **The README is a landing page**; language flags are SVG files, because no Windows font renders the flag emoji.
- **The release badge reads GitHub** instead of a hand-typed number.
- **Wall outlets are born `WA-01`**, matching the ones already on the wall.

### Fixed
- The breadcrumb says which view you are in; ENDPOINT hides VoIP phones too.
- A trunk no longer borrows the look of the cable you are tracing: thickness says *trunk*, glow says *you are following this one*.
- The toolbar stops wrapping at full screen.

## [2.3.1](https://github.com/muttley1973/infranetpro/releases/tag/v2.3.1) — 2026-07-31
**One classifier, and no word outranking a measurement.**

### Added
- A shared **classifier corpus** (53 rows) and two goldens, run through *both* engines: the browser and the server can no longer disagree in silence.
- **Emulation platforms are a class** (GNS3, containerlab, EVE-NG…), not the bench they were first written on.
- The browser reads the **sysObjectID** for types it had no regex for.
- The Overview opens **rack space and free fibre** as lists, and free ports gain a "by speed" tab.

### Fixed
- **A word no longer outranks a measurement**: a description saying `ios` scored higher than a device that answered.
- **Brands stop deciding types**: `apc`, `raritan`, `sony`, `cyberpower`, `liebert`/`vertiv` all make more than one kind of thing.
- **"switch" is no longer the answer for the unrecognised**, and a vSRX is no longer a router because `srx` appears inside its name.
- A Synology is recognised by its product line; on managed gear the MAC comes from the interfaces, where it lives.
- A patch panel with nothing filled in no longer declares a specification.

### Changed
- Inference rules built on an identity string (HP factory hostname, Juniper names, phone-vendor list) lose weight to rules built on measurement.
- **"Detected identity" moves to the top of every property panel**; IP and MAC share one row.

### Repository
- ⚠️ **The commit history was rewritten and force-pushed** (English messages, no bot trailers).

## [2.3.0](https://github.com/muttley1973/infranetpro/releases/tag/v2.3.0) — 2026-07-30
**A sixth lens — Health — and a long pass on numbers that were stating more than they knew.**

### Added
- **Health lens**: what problems are there right now, composed from telemetry already collected.
- **Warranty and end-of-life dates** per device, feeding a Lifecycle row in Recoverability.
- The dossier opens with the **Overview page** and closes with **"What I'm not looking at"**, naming the dimensions it does *not* judge.
- **Fibre runs are validated** against their optical class: reach depends on the optic, not on the metre.

### Fixed
- **A zero that was never measured is no longer printed as a fact.** Across the Overview, the PDF and the dossier: "0 free addresses" with no valid subnet, "0 VLANs named" on a project with no VLANs, "0 fibre free" where no fibre was declared, "0 suspect ports · all coherent" on a project never verified, a rack that never declared its height getting 42U written into the document, a counter never supplied printing a dash instead of a zero. Where an engine fails, the answer is "not assessed", not a green tick.
- **A negative PoE headroom is a debt, not a margin.**
- **A measurement no longer overwrites what you set by hand**: a VM's power state, a LAG created by hand, a LAG renamed.
- **Ages are visible**: the SNMP indicator and per-device lamps turn amber past six hours, and a summary row that forgets its provenance no longer claims a human wrote it.
- The validators speak English too, and layout no longer scrolls sideways or overlaps at narrow widths.

### Changed
- A MAC's vendor comes from the **refreshable registry**, not a hand-written table; VMware's OID is no longer hardcoded.
- **"Add to map" no longer arrives with every candidate pre-ticked.**
- When SNMP finds more interfaces than the declared port count, the drawing shows them as proposed.

### Security
- **The backup pointer no longer reaches the PDF with credentials inside it**, and the same check now runs on the server, on save and on import — it lived only in the browser.
- **The SNMP community is masked** like the v3 passphrases already were, on devices and on virtual machines.

## [2.2.0](https://github.com/muttley1973/infranetpro/releases/tag/v2.2.0) — 2026-07-29
**Two new questions: can you bring it back up, and how exposed is management?**

### Added
- A **Recoverability (DR) lens** — if it falls tonight, can you restore it? — and a matching **DR section in the PDF**, one row per managed device.
- A **Security & Services lens**: encrypted SNMP versus clear text, default communities, management exposure.
- **VLAN names read over SNMP**, reconciled against what you declared.
- **IPAM conflicts, Gateway and Redundancy rows** in Conformance: the same IP on two devices, a declared subnet with no gateway, how many devices declare an HA twin.
- **Wireless association discovery** over SNMP, with an opt-in "AP mode" for devices that broadcast without being access points.
- **Hardware identity drift** as a seventh Verify category: a changed serial or model means the box was replaced.
- **A configuration-backup pointer** per device — where the backup lives, never the config itself.
- **Temporal confidence**: the discovery sightings already collected become a score.
- **Operating-system logos** in the device and VM panels.

### Changed
- **The declared addressing plan is law**: free addresses are measured against the network you declared.
- **One primary action, "Verify"**: the separate Sync button retired, the Verify overlay replaced by a result that stays in the Overview.
- **Conformance stops being green when the data is old** — past seven days a verdict is suspended, not assumed.
- A backup counts as fresh within 30 days, matching the device panel.

### Fixed
- Two documented devices sharing a MAC (VRRP/HSRP) no longer collide in the drift keys.
- The "Copy" button for an API token works over plain HTTP, where the clipboard API is not available.

## [2.1.0](https://github.com/muttley1973/infranetpro/releases/tag/v2.1.0) — 2026-07-24
**The Overview: is the document complete, does it still match reality, and what is coming next.**

### Added
- **The Overview view** — three columns and a health-dot verdict per column, red reserved for a project never synced.
- **Device labels say what a thing is**, not just where it answers.
- **IPv6 as a first-class address**, read from the device and from neighbour discovery.
- **An OS hint from the ping TTL** already captured by the sweep — low weight, never decisive.
- **vNIC ports and a dedicated VM card**: a VM can declare several virtual cards and be polled over SNMP like a device.
- **Honest presence on the floor**: red when confirmed absent, grey when not verifiable.
- **Eight new classifiable types** (ATS, NVR, PBX, VPN concentrator, console server, projector…).

### Fixed
- **A blind Check no longer reports "documentation aligned"**: observing nothing is not a clean bill of health.
- **Looking is not editing** — switching view no longer marks the project unsaved.
- **An absent field no longer states an invented default** (18 identity selects, cable types, and more).
- **"Configured for SNMP" is not "responding"**, and measurements are stamped with the time they were taken.
- **PoE classes were read off-by-one**, inflating every budget.
- **The AI context stopped scanning the whole project for every port**: 4.5 s to 5 ms on a large network.

### Security
- SNMP secrets are **redacted for read-only viewers**; the development auth-bypass fails closed.

## [2.0.5](https://github.com/muttley1973/infranetpro/releases/tag/v2.0.5) — 2026-07-14
### Added
- **"Apply model"**: search a real switch or router model and apply it — port count, layout and optics — in one click, from a **catalogue of ~4,100 models across 52 vendors** built from public-domain data.
- **draw.io rack export**: a native, editable diagram, one page per rack, one edge per cable, one layer per VLAN.

### Fixed
- **Fibre ports no longer render as phantom copper** (~553 of 4,070 catalogue models were affected).
- **The two floor-plan renderers share one builder** — they had drifted, so a partial redraw disagreed with a full one.
- Defaults that asserted things nobody set are gone (NAS RAID level, diskless-server storage…).
- **Discovery robustness**: an oversized HTTP body no longer hangs the sweep; an aggregate interface name no longer resolves to a same-numbered physical port.

### Security
- **The panel-skin importer's stored XSS is closed.** Secrets are owner-only and written atomically; the login timing leak is equalised.

## [2.0.4](https://github.com/muttley1973/infranetpro/releases/tag/v2.0.4) — 2026-07-10
### Added
- **Direct NBSTAT over UDP** names Windows hosts in about 40 ms, instead of shelling out per host.
- **Stealth mode shuffles both sweep phases**: a sequential sweep is itself a signature.

### Changed
- The Verify report stops repeating itself, and the PDF asset register lists IT assets rather than structural cabling.

## [2.0.2](https://github.com/muttley1973/infranetpro/releases/tag/v2.0.2) – [2.0.3](https://github.com/muttley1973/infranetpro/releases/tag/v2.0.3) — 2026-07-08
### Added
- **Closed-port devices identify themselves**: an opt-in multicast pass (mDNS, SSDP, ONVIF).
- **Off-segment discovery via SNMP ARP**, and **DHCP leases as a discovery source** with macsuck locating each MAC on its switch.
- A **"Project networks"** section in the Verify report, derived from the documented devices.
- The full **IANA PEN registry** (~66k organisations), so a new vendor resolves without a code change.
- A **bilingual PDF report** and an **audit-ready asset register**.

### Changed
- **One classifier, for real**: the duplicate legacy implementation was removed.
- **The MAC vendor stops deciding the device type** — the OUI drops to the identity tier.
- SNMP interfaces are matched to ports **by `ifName`, not by position**, so a hand-cabled port keeps its cable.
- A WLAN controller no longer counts as a Wi-Fi radio device: it has no radios.

### Fixed
- **Sync no longer stalls on a slow SNMP device**: a deadline returns partial data instead of hanging.
- **The presence audit no longer greys an unobserved subnet** in a multi-fabric project.
- **Performance**: the topology overlay batches its work — 724 ms to 7 ms on a large map.

### Security
- The AI key file is written owner-only, and a dead route that leaked an absolute path is gone.