Skip to content

Release v0.12.0 - #29

Merged
gitkodak merged 5 commits into
mainfrom
release/v0.12.0
Aug 14, 2026
Merged

Release v0.12.0#29
gitkodak merged 5 commits into
mainfrom
release/v0.12.0

Conversation

@gitkodak

Copy link
Copy Markdown
Owner

Cuts 0.12.0.

Added

A config file and UNIFI_MAP_* environment variables for preferences.
~/.config/unifi-map/config.toml (honouring XDG_CONFIG_HOME, or
$UNIFI_MAP_CONFIG) plus a matching variable for theme, layout, icons,
formats, overrides and the three directories.

Both mechanisms rather than one. A file suits somebody whose taste differs from
the defaults; environment variables are what make the tool configurable in a
container without mounting anything. Precedence is flag, then environment, then
config file, then default, with environment above file so a config baked into
an image can be overridden per deployment.

--obfuscate and --force are excluded on purpose: one asserts the output is
safe to share and the other overwrites files, and neither should be answerable
by ambient state invisible at the call site.

Every render now reports which settings it did not get from the command line
and where each came from.

Deprecated

UNIFI_CACHE_DIR, UNIFI_ASSET_CACHE and UNIFI_OUT_DIR are renamed to the
UNIFI_MAP_* forms. UNIFI_* is the console's namespace and these were never
console settings. The old names still work and warn.

Fixed

Three defects, all found by external review of this branch before merge:

  • A malformed config file produced a traceback instead of Configuration error: ... and exit 2, for every command including shape.
  • formats = [] in a config file passed validation, did the full topology and
    artwork work, printed Full map: with nothing under it, wrote no files and
    exited 0.
  • SECURITY.md claimed that resolving Graphviz to an absolute path guards
    against the file at that path being swapped. It does not; it only removes the
    second PATH lookup.

Plus the API key no longer appears in ExporterConfig's repr (KAN-198),
defensive rather than a fix for a demonstrated leak.

Notes

make demo-images was deliberately not run: the committed screenshots were
generated by Graphviz 15.1.0 and only 2.43.0 is available here, so regenerating
would downgrade all eight PNGs and both HTML viewers. Nothing in this release
changes how a map is drawn.

make check passes at 721 tests. Every new guard was mutation-tested.

sakodak and others added 5 commits August 14, 2026 14:32
Everything about how a map is drawn had to be typed on every run, and so
did --overrides. This adds ~/.config/unifi-map/config.toml (honouring
XDG_CONFIG_HOME, or $UNIFI_MAP_CONFIG) and a matching UNIFI_MAP_* variable
for each of theme, layout, icons, formats, overrides and the three
directories.

Both mechanisms rather than one. A file suits somebody whose taste differs
from the defaults; environment variables are what make the tool
configurable in a container without mounting anything. Precedence is flag,
then environment, then config file, then default: environment above file so
a config baked into an image can be overridden per deployment.

--obfuscate and --force are deliberately excluded. One asserts that the
output is safe to share and the other overwrites files, and neither should
be answerable by ambient state invisible at the call site.

Two things this turned up. The render flags had real argparse defaults
rather than SUPPRESS, so nothing from the environment could ever have
applied to them; they now suppress with their defaults in GLOBAL_DEFAULTS,
which needed no change to the doc generator since it already falls back
there. And a value injected this way never passes argparse's own choices
check, so _validate_injected checks it against the same constants the
arguments are declared with and names the source in the error.

Every render now reports which settings it did not get from the command
line and where each came from. Supporting three sources is what makes "it
looks different on your machine" possible, and one log line answers it.

UNIFI_CACHE_DIR, UNIFI_ASSET_CACHE and UNIFI_OUT_DIR are renamed to the
UNIFI_MAP_* forms. UNIFI_* is the console's namespace and these were never
console settings. The old names still work and warn.

The tests were reading the developer's own environment, which this turned
from latent into a failure: a deprecation warning from a real
UNIFI_CACHE_DIR broke an unrelated test about --site. conftest now strips
implicit configuration discovery for the whole suite, replacing a
per-class workaround that existed for exactly this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Compared modelling against ScottiBYTE/unifi-topology and
merlijntishauser/unifi-topology, then checked every claim against a real
snapshot rather than against the other projects' source alone. Four
findings became tickets; the reasoning lands here and the summaries in
TODO.md.

port_overrides[].name holds user-written port labels we read nowhere.
port_table[].name is a generic "Port N" on 75 of 79 ports on the reference
network, while port_overrides carries a real label on 26. Blocked on an
obfuscation guard, because obfuscate.py rebuilds nodes with replace() and
so passes any new Node field through untouched, while edges are built field
by field and drop one silently. A port named after a person or a room would
ride straight into a map that promised to be shareable.

The controller's v2 topology payload has a has_unknown_switch boolean we
ignore, and several wired clients sharing one switch port says where that
switch is. Together they are worth reporting; separately neither locates
anything. Reported and not drawn, because the cause is ambiguous between an
unmanaged switch and a virtualisation host.

LLDP was got wrong twice on the way, and both corrections are recorded
rather than quietly fixed. First the wrong field was checked, then the
network was called all-UniFi when it has a Netgear switch on port 7 that
LLDP cannot see because it does not advertise.

ExporterConfig's generated repr contains the API key. Defensive rather than
a demonstrated leak, and said so.

Also records why a [[merge]] override for multi-interface hosts is not
planned: a merged node belongs to every network its interfaces are on,
Node.network holds one value, and --per-network already renders each
interface alone in its own VLAN's diagram.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dates the changelog section, bumps the version and regenerates the man
page, in that order: the man page takes its date from the changelog entry
for the version, so bumping before dating generates an empty date that the
regenerate-and-compare check cannot catch.

Also updates the version in the JSON export example in docs/output.md,
which a test compares against real output.

`make demo-images` was deliberately not run. The committed screenshots were
generated by Graphviz 15.1.0 and the only dot available here is 2.43.0, so
regenerating would have rewritten all eight PNGs and both HTML viewers with
older output: different sizes, different text attributes, stroke="transparent"
instead of stroke="none". That is a downgrade rather than a refresh, and
nothing in this release changes how a map is drawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A malformed config file produced a traceback. Parsing reads the config
file, so invalid TOML, an unreadable file or an unknown key raised
ConfigError from inside parse_args, which ran before main's try block. That
affected every command, including shape, which exists specifically to be
safe to paste into a bug report. Logging is now configured before parsing,
reading -v straight from argv since no namespace exists yet, and parsing is
wrapped so these produce the ordinary message and exit 2.

formats = [] in a config file silently produced nothing. -f is nargs="+" so
argparse refuses an empty list on the command line, but _validate_injected
iterated the list and so checked nothing when it was empty. The run did the
full topology, override and artwork work, printed "Full map:" with nothing
under it, wrote no files and exited 0. An empty list setting is now refused,
naming its source, restoring parity with the command line.

Also corrects SECURITY.md, which claimed that resolving Graphviz to an
absolute path guards against "the file at that path being swapped". It does
not: anyone who can write to that path can replace the binary and the
absolute path will run the replacement. What it actually buys is that there
is no second PATH lookup, so a PATH change after startup cannot redirect the
call. The narrower claim is now stated, along with what it does not cover.

Regression tests go through main() rather than the loader, which was
already tested and was never where the defect was. Both mutation-tested by
reverting each fix and confirming they fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A frozen dataclass reprs every field, so repr(CONFIG) rendered the live key.
Declaring api_key with field(repr=False) hides it while leaving host, site
and verify_tls visible, because a redacted repr nobody can debug with would
be its own problem, and leaves the value readable on the object.

Defensive rather than a fix for a demonstrated leak, and the changelog says
so. Nothing reprs, formats or logs the config today; -v does not enable
http.client debug, urllib3 at DEBUG logs the request line rather than
headers, and an ordinary traceback does not print frame locals. What this
closes is the ways a repr escapes later: a pytest assertion diff, a
--showlocals traceback pasted into an issue, or a well-meant
log.debug("config: %r", config).

Included in 0.12.0 rather than deferred. It was first held back because the
release had already grown, which does not survive scrutiny: the change is
two lines, was fully specified in the ticket, and RELEASING.md says security
fixes should not sit unreleased because anyone tracking tags is running the
last one.

Four tests, mutation-tested against removing repr=False. One covers f"{config}"
separately from repr(config): a dataclass aliases __str__ to __repr__, and
interpolation is the likelier accident of the two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@gitkodak
gitkodak merged commit 3441b9f into main Aug 14, 2026
9 checks passed
@gitkodak
gitkodak deleted the release/v0.12.0 branch August 14, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants