Changelog
Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.32.1] - 2026-09-11¶
Release Summary¶
A patch release: one bug fix plus internal cleanup, no feature or behaviour change beyond it. The dirty flag that arms Save could get stuck disabled forever if anything unexpected threw while the editor loaded; that is now fixed. The rest of this release is the editor's JavaScript being split out of one 8455-line file into ES modules, and two long-red e2e suites being repaired to match features that had already shipped — neither changes what the editor does.
Fixed¶
- A failure while the editor was loading could permanently disable Save.
Opening a rack locks the passive tiles and detaches the server-rendered
full-depth hatches, and that pass suppresses the dirty flag so merely loading
the editor never arms Save. The suppression was a plain set-then-clear pair
with no exception guard, so if anything inside the pass threw — an unexpected
node reaching a GridStack
updateorremoveWidget— the clear was skipped and the flag stayed set for the life of the page. Every later edit was then applied to the grid but left Save disabled, with nothing in the console to explain it: the changes looked accepted and could not be saved. The pass is now bracketed so the flag is restored even on a throw, and the unguarded set/clear helper has been removed rather than left available. - Editor assets could be served stale after an upgrade. The editor page
stamps a
?v=<token>cache-bust on each of its own static files, and the token is the newest modification time across an explicit list — butpower_heatmap.jsandrack_layout.jswere never on that list. Editing either one therefore never moved the token, so a browser kept serving its cached copy of that file while every other asset refreshed. Both are now registered, along with the editor's new JavaScript modules.
Changed¶
- The editor's JavaScript is now split into ES modules.
editor.jshad grown to 8455 lines in a single closure. The read-model, the drag tracer, the hover card, the CSRF/toast helpers, the three modal dialogs, the device catalog palette, the planned-PDU/rack-power dialogs, cursor-governed placement, GridStack push suppression, the dirty-flag state, the frame/grid options and the cross-rack registry now live in their own files understatic/netbox_rack_design/js/editor/, as does the per-rack controller itself — thirteen modules, leaving 668 lines of editor.js. There is no bundler and no build step:editor.jsis loaded as a module and resolves its submodules through an import map the editor template emits, which is what keeps the cache-bust token on every module URL. No behaviour changes — each extracted block is byte-identical to the one it replaced, apart from named substitutions where a module re-derives a DOM element the enclosing closure used to hold, guards where a module evaluates at load time in a page that may not contain the editor at all, and the dirty flag, which was written from both sides of the per-rack boundary and now has one owner behind a setter because an ES module importer cannot assign to an imported binding. - Two long-red editor e2e suites repaired (tests only, no product change).
Both had been invalidated by features that shipped after them.
test_e5_stripe_bar_outside_rack_framerequired a stripe bar to still carry its nativetitlewhile the hover card was showing, but the card deliberately parks that title so the browser tooltip cannot cover it — the assertion now reads the tooltip before hovering and checks the parked copy while the card is up.test_editor_distributioninjected a fixture Distribution into the page, but the toggle it then fired disconnects the MutationObserver in the same synchronous task, discarding the injection's undelivered mutation records so nothing ever repainted; it now asks for the repaint through theNbxRdPowerHeatmap.refresh()hook the product exposes for changes that alter the answer without mutating a tile, and serves the fixture as the live recompute's answer so the assertions exercise the real pipeline.
[0.32.0] - 2026-09-09¶
Release Summary¶
Two people planning the same rack could not see each other. Design chains
relate a design to its ancestors, and that lineage is what the projection
reads — so two designs with no lineage between them, siblings of one parent
included, were blind to each other while both were still drafts. Both could
claim the same unit, both could generate the same name, both could plan to
move the same device, and nothing said so until one of them reached apply
and was refused. This release reports those overlaps while there is still
time to do something about them, and gives a name collision a one-click way
out.
Nothing new blocks a save: a peer conflict is not this design's fault and cannot be fixed by editing the tile. Detection is on by default, because the overlaps it finds are already there — including ones that have been overlapping for months.
Added¶
- Peer conflicts. A design's projection now reports overlaps with designs outside its own chain (siblings, or designs sharing no lineage at all) that were previously invisible to each other while both were still planning:
peer_slot_claim— a peer plans a device on a unit this design claims (its own adds/move-ins, plus anything it inherits from an ancestor).peer_name_claim— a peer plans the same name.peer_device_claim— a peer plans to move the same real device.- An approved peer's claim is an error, a draft peer's a warning; neither blocks a save — a peer conflict is not this design's fault, and there is nothing to fix by editing the tile. A peer that has already applied owns a real device, so saving onto that unit is refused the ordinary way, as before.
- Shown as a third row in the editor's Design conflicts panel, with contested tiles flagged and filterable via a "Peer conflict" legend chip on both the editor and the read-only elevation view. Several name-claim rows against the same peer design collapse into one row with a "Show" toggle.
- A "Re-run naming" button on a name-claim row opens a read-only
old -> newdiff; Confirm writes every line shown, Cancel writes nothing. Only the colliding names are recomputed — a clean sibling name is left alone even at the cost of tidy numbering, since it may already be referenced elsewhere. - Gated by the
peer_conflicts_enabledPLUGINS_CONFIGkey (defaultTrue); disabling it turns off peer detection entirely, including the disclosure of a peer design's title past NetBox object permissions that peer detection otherwise relies on to make a conflict actionable. - Not covered: a peer's claim on a device bay (peer detection reads rack U slots only).
- New REST endpoints:
GET .../designs/<pk>/conflicts/(chain and peer conflicts together, read-only),POST .../designs/<pk>/rerun-naming-preview/(read-only diff), andPOST .../designs/<pk>/rerun-naming/(writes the confirmed set; refuses on a frozen design). - See docs/peer-conflicts.md.
Fixed¶
- Three documentation claims that are no longer true. The chains guide said sibling designs are blind to each other and that a clash between them surfaces only when someone tries to save onto an occupied unit — peer conflicts report it while both are still in progress. The README's own introduction still said an explicit Apply step was "arriving in a later stage" (Apply shipped in 0.29.0), and its feature summary listed cross-design conflict detection as planned.
- Three references in the reference specs pointed at an internal design document that is not part of the published site, so a reader was given a filename they could not find; they now link the Design chains guide.
[0.31.0] - 2026-09-09¶
Release Summary¶
An approved design with dependents could not be changed by any route. The
freeze is deliberate — a child baselined on it has to trust that the ground
will not shift — and the way out was always supposed to be "create a new
version, then re-base the children onto it". The model had the fields for
that (version, root, at most one approved version per plan) and seven
error messages advised it, but nothing ever created a version: no button, no
view, no API action. This release builds that route, so the advice those
messages give is now something a user can actually do.
Added¶
- New version. Clone a design into a fresh draft version of the same
plan, from the design page's Design chain card or
POST /api/plugins/rack-design/designs/<pk>/new-version/. - A version is the same plan revised: it copies the source's
based_onand deep-copies its content — placements, planned power feeds, rack power overrides, rack scope,depends_on, custom field values and tags. This is the opposite of Derive, which creates a new plan standing on top of this one and copies nothing but rack scope, inheriting the rest live through the chain. - Available whatever the source's status. Approval is what makes a version necessary, not what makes it valid.
- An omitted title keeps the source's own — versions are distinguished by
their
(vN)suffix, so nothing is invented. - Apply records are deliberately not copied: a new version has applied nothing, so it starts unapplied and applying it reconciles against reality like any other apply.
- The confirm page states the order of operations when the design has dependents, and warns that they cannot be projected between the re-base and the approval — correct behaviour that would otherwise look like damage.
Fixed¶
- The messages that advised a route which did not exist now name the New version action, and the view-layer ones link straight to it. The delete guards deliberately still point at re-basing instead: a new version does not detach a design's dependents, so it cannot unblock that delete.
- Two documentation claims that were never true:
docs/design-chains.mddescribed versioning as an existing feature, and the README's own introduction said apply was planned — it shipped in 0.29.0 and is listed as delivered on the same page.
[0.30.2] - 2026-09-08¶
Release Summary¶
A bug fix, and a worse one than 0.30.1's. Designs are versioned, and every
version of a plan points at the first one through root, which cascades on
delete — so deleting a plan's first version silently destroyed every later
version of it and all of their placements, approved versions included, with no
warning. 0.30.1 closed the door where a delete orphaned a dependent design's
baseline; this closes the one where a delete destroys data outright.
Fixed¶
- Deleting the first version of a plan no longer destroys its other
versions.
Design.rootison_delete=CASCADE, so a delete of the root design took every design whoserootpointed at it — and their placements — with no confirmation naming them and nothing reported afterwards. Verified against a real database before fixing: a root with an approved v2 and a v3, one placement each, left nothing behind. - The delete is now refused in all three write paths (single-object and bulk
delete views, and
DELETE /api/plugins/rack-design/designs/<pk>/with 409 Conflict, which also covers the API's bulk delete), naming the versions that would have been destroyed. - The foreign key is unchanged, so there is no migration. Guarding the write
paths keeps the refusal explainable;
PROTECTwould surface as a raw database error to every caller that does not expect one. - Deleting a later version is unaffected — only a design that other versions hang off is refused.
- When a design is blocked for both reasons — other designs are based on it (0.30.1) and other versions hang off it — both are reported together, so fixing one and retrying does not reveal the second only then.
[0.30.1] - 2026-09-08¶
Release Summary¶
A bug fix. An approved design's placements are frozen and it cannot be moved back to draft while other designs are based on it — but it could always be deleted, which silently orphaned every dependent design's baseline. Since 0.29.0 that could also strand applied planned devices in NetBox with no record pointing at them. Deleting such a design is now refused, through every door.
Fixed¶
- Deleting a design that other designs are based on is now refused.
Design.clean()blocks leavingapprovedstatus while dependents exist, but Django never callsclean()on the delete path andbased_onisSET_NULL— so a delete quietly nulled every child's baseline and each child re-rendered as though it had always been planned against bare reality, with nothing reported. The refusal is enforced in all three write paths: the single-object and bulk delete views, andDELETE /api/plugins/rack-design/designs/<pk>/(409 Conflict), which also covers the API's bulk delete. The message names the dependent designs and points at re-basing them. - Not conditioned on status: a draft design with dependents orphans them exactly the same way, so the guard applies whatever the design's own status is.
- Same class of gap as the frozen-placement delete hole fixed in 0.28.0, and fixed with the same pattern.
[0.30.0] - 2026-09-08¶
Release Summary¶
Apply's records become readable. 0.29.0 recorded which planned device each placement produced, but only the run that created them ever returned those records — afterwards there was no way to look one up, which left the automation that performs the physical move with no way to find the device it was supposed to act on. This release exposes them as a read-only endpoint with the filters that job needs, including finding the orphans left behind when a design or placement is deleted.
Added¶
GET /api/plugins/rack-design/design-applies/and.../design-applies/<pk>/— the apply records: whichdcim.Devicewas created for whichDesignPlacement, by whom, when, and the status a removed device had before it was flagged.- Filters:
design_id,placement_id,device_id,q(searches the name snapshots), andno_design/no_placement/no_devicefor finding orphaned records — the rows that matter when a planned device is left standing in DCIM after its design or placement was deleted. - The
design_titleanddevice_namesnapshots are always in the payload, even while their references are alive, so a client never has to fetch a related object just to learn a name — and the snapshot is what survives a deletion. - Requires
view_design. A record is visible when its design is one you may view, or when it has no design at all: every reference isSET_NULL, so a deleted design leaves exactly the orphans automation must find, and scoping only to viewable designs would hide them. - Read-only by design. These records are written by the apply engine and nothing else; a client able to create or edit one could make the plugin's bookkeeping disagree with DCIM, which is the failure the records exist to prevent. Cleanup remains the apply run's job, or the automation's.
[0.29.0] - 2026-09-08¶
Release Summary¶
An approved design can now be applied: materialized in NetBox as planned
devices. Until now a design was a drawing — the plan existed only inside the
plugin, so nothing stopped a colleague from racking hardware into a unit your
approved design had already claimed, and there was nowhere to hang planned
cabling for a device that had not moved yet. Apply closes that: it creates a
planned dcim.Device at each target slot, reserving the space and giving
planned ports and cables something real to attach to, and flags the devices a
design removes with a configured status. The device being moved is left
untouched in its old slot — apply materializes the plan, not the move, so
during the window before the hardware is touched the rack honestly shows both.
The physical work, and the design's move to implemented, stay with whatever
automation performs them.
Breaking Changes¶
- The
planned_statusesandremoval_statusessettings are replaced byplanned_statusandremoval_status, each a single string rather than a list:
"planned_status": "planned",
"removal_status": "to_decommission", # default "decommissioning"
Writing a status needs exactly one value and recognising it needs the same
one, so a list was never the right shape. Both keys were scaffolding present
since the first commit that no code ever read, so nothing behaved
differently because of them — but a PLUGINS_CONFIG still naming the plural
keys must be updated, or the plugin's startup check will reject the unknown
keys.
Added¶
- Apply.
Applyon an approved design's page, andPOST /api/plugins/rack-design/designs/<pk>/apply/for automation.GETon the same URL is a dry run — it reports exactly what a real run would do, writes nothing, and needs onlyview_design. - Every problem is reported at once, in plain language: an occupied unit names the device holding it, a name collision names the conflicting device, an unapplied ancestor names which design to apply first, a missing permission names the site. No fixing them one at a time.
- All-or-nothing. The run is a single transaction, so NetBox is never left half-changed and the plugin's records can never disagree with what exists. There is deliberately no partial "applied 7 of 12" state.
- Safe to press twice. Apply reconciles rather than duplicating: it finds the device it created before, compares name, rack, position, face, role, tenant and status, and writes back only what drifted. A planned device someone deleted in DCIM is recreated.
- Ordered along a chain. Applying a design refuses while an ancestor still has unapplied placements, so a slot is never reserved for a move whose precondition does not exist yet. Planning stays unordered — the whole point of chains is that the next team plans before the first team's hardware work finishes.
- Cleanup is reported, never silent. A planned device whose placement was
deleted is removed, and a removal flag whose placement was deleted is
reverted to the device's exact prior status — recorded at flag time, not
guessed at
active. Deleting a planned device is the only irreversible step in the flow, so the confirmation page calls it out by name before you press anything. - Runs with your own permissions:
dcim.add_device/change_device/delete_deviceas the run actually requires them, checked through NetBox's object permissions so per-site and per-tenant constraints are honoured. There is no service account, so pressing the button can never do something you could not do directly. DesignApplyrecords which device an apply created for which placement. Every reference isSET_NULLwith a name snapshot beside it, so the record outlives whichever side is deleted — "never applied", "applied then the device was deleted", and "applied then the placement was deleted" stay distinguishable rather than collapsing into one indistinguishable state.- Applied and reserved markers on the elevations. In the design that created it, an applied device draws as a single tile marked applied — the live DCIM copy is suppressed, following the apply records exactly rather than matching on names, so one device can never render as two overlapping tiles. In any other design covering that rack it renders as reserved by the design holding it, so a planner who did not create it can see whose plan owns the slot. Both are filterable in the legend and shown in the hover card.
Changed¶
- Blade placements (a device destined for a chassis bay) cannot be applied yet. Apply reports them explicitly as unsupported rather than skipping them — a silent skip would read as "applied everything".
[0.28.0] - 2026-09-07¶
Release Summary¶
The planning prefix stops being data and becomes decoration. Until now a move
that kept a device's name still wrote "<design title>-<device name>" into
proposed_name, and the real name was recovered later by stripping that
prefix back off — using a token that came from configuration and, when it did
not match what had been written, silently returned the string unchanged. The
result was a planning string displayed as if it were a real device name, with
no error raised. That whole mechanism is gone. proposed_name now means one
thing — the new name the plan gives the device — and is written only when the
plan actually renames something. The decoration is composed at render time from
the design's title, so there is nothing to strip, nothing to configure, and
nothing to get wrong.
Breaking Changes¶
Removed from netbox_rack_design.naming (a deployment naming script importing
any of these must be updated before upgrading):
settled_name(),settled_name_status(),strip_planning_prefix(),_builtin_settled_name(),prefix_token(),derive_prefix_token()and theSettledNameErrorexception. There is no replacement, because there is no longer a planning name to settle: a placement's stored name IS its name.name_exists_in_site()no longer accepts thedesign=keyword. It compared candidates on a "settled plane" that no longer exists; the check is now a direct comparison against real device names and other placements' stored names. Drop the argument at the call site.chain_placement_names()returns one name per placement row instead of a planning/settled pair per ancestor row. Each name is the row's effective name: itsproposed_name, or — for a move that keeps the device's name — the device's real name. Scripts that de-duplicated the old double entries can drop that handling. This also fixes a real defect: a keep-name move used to contribute an empty string, so a family counter never saw it.- The
naming.prefix_sourceplugin setting is removed, along with thenaming.settled_namehook. The decoration usesDesign.titleverbatim. AnyPLUGINS_CONFIGentry for them should be deleted; thenamingsub-dict itself remains as an extension point and currently accepts no keys.
No migration is required — no model or field changed. Existing rows that still
hold a prefixed proposed_name are read as renames, because that is now what a
non-empty value means; clear those values to restore keep-name behaviour.
Changed¶
- A
movethat keeps the device's name storesproposed_name = ''. The editor no longer writes the decorated string when you choose "keep the old name", and an explicit keep-name choice is now sent to the server as an empty string so it authoritatively replaces any name the placement carried before. - Rendering, in full: inside the design that owns the move, a keep-name move
shows
"<design title>-<device name>"and a rename shows its stored name verbatim; in a design based on it, an ancestor's keep-name move shows the plain device name — in the child's world the device has already moved and carries its own name. - The
settled_nameconflict kind is gone from the projection's report. It described a failure that can no longer occur. docs/design-chains.md,docs/device-naming.md,docs/editor-behavior-spec.mdandREADME.mdupdated to describe the new scheme.
Fixed¶
- Deleting a placement from an approved (frozen) design through the REST
API is now refused with
409, single and bulk alike. The freeze lives inclean(), which Django never calls on delete, so the API had no guard — the HTML views already did, andDesignPowerFeedViewSetalready had exactly thisperform_destroyoverride for the same reason.
[0.27.1] - 2026-09-07¶
Release Summary¶
A projection fix release. Two defects made a child design's elevation misrepresent devices its ancestor had already moved: the tile lost the provenance that says where it came from, and a move landing on the cell the device was already in drew a vacating ghost directly underneath the arriving tile, so the two labels painted on top of each other and neither was readable.
Fixed¶
- An ancestor's provenance now reaches a move's slots. The moves loop
already resolved the ancestor's baseline row for the identity it was acting
on, but never forwarded that provenance into the slots it built, so the tiles
carried neither
inheritednorsource_design_idand the elevation showed no sign that the device's position came from another design. The vacating ghost -- which exists only because the ancestor put the device there -- is now flaggedinheritedand names its source design; the arriving tile carries the source design too, but is deliberately NOT flagged inherited, because moving the device there is this design's own act, not the ancestor's. - A move onto the cell the device already occupies no longer draws a ghost. Such a move vacates nothing, and the ghost was emitted at the same rack, position and face as the arriving tile -- identical geometry, two labels superimposed. The ghost is now suppressed, along with its full-depth mirror on the opposite face. The arriving tile is unchanged, and the placement itself is still allowed: whether a no-op move should be rejected at save time is a separate question this release does not decide.
[0.27.0] - 2026-09-04¶
Release Summary¶
A naming release for move placements. Renaming a device as part of a move now
gets the same engine-generated suggestion an add gets -- script, template or
the built-in sequence, per the deployment's naming_mode -- prefilled into the
rename dialog and always editable. Keeping the device's name stays completely
inert: nothing is generated and no request is made. Underneath, a move's
planned role and tenant overrides finally reach the naming engine: they were
always stored as overrides ("null means leave the device's own value alone"),
but the engine was handed the source device verbatim, so a template or script
composing a name from role or tenant saw the value the device was moving away
from rather than the one it will have when it lands.
Added¶
- Move-rename suggestions come from the naming engine. Selecting "rename"
in the move dialog prefills the field from the read-only
preview-nameendpoint withkind=move, using the placement's resolved role and tenant and the drop's target rack, position and face. The suggestion is always editable and whatever is left in the field is what gets saved. Choosing to keep the device's name issues no request at all. A name the placement already carries from an earlier session counts as a human's choice and is never overwritten by a suggestion. Site name collisions surface in the dialog the way they do on the add path. DesignPlacement.resolved_role()andDesignPlacement.resolved_tenant()-- the supported way for a naming script to read what a moved device's role and tenant will actually be. Each returns the placement's override when set, otherwise the source device's own value, recursing intobase_placementfor a move acting on an ancestor design's still-plannedadd.
Fixed¶
- A move's planned role and tenant overrides are now visible to the naming
engine. Previously
{device.role}and{device.tenant}in a naming template rendered the source device's values on a move, silently ignoring the overrides the placement carried. naming_example's role slug now derives fromresolved_role()instead of duplicating the override logic, which also fixes it falling through to a default for a move acting on an ancestor's plannedadd(no real device to read a role from).
Changed¶
{device.rack},{device.position}and{device.face}on amovenow resolve to the move's TARGET, not to where the device currently sits. This is the intended meaning -- a device is named for where it is going -- but any deployment whose naming template or script uses those tokens on move placements will produce different names than it did before this release. Review yournaming_template/naming_scriptbefore upgrading if it names moved devices by location. Nothing changes foraddplacements.
[0.26.0] - 2026-09-04¶
Release Summary¶
A small usability release: deriving a design no longer generates the child's
title for you. The Derive design action now asks for a title, prefilled
with the old "<parent> (derived)" suggestion so the common case is still one
click away, but a blank title is rejected instead of silently falling back.
The REST API's derive action gained the same optional title field —
existing clients that POST no body are unaffected, since omitting title
still produces the previous generated default.
Added¶
- Design derive: user-supplied title. The
Derive designview now shows a form with a requiredtitlefield, prefilled with a"<parent> (derived)"suggestion.POST /api/plugins/rack-design/designs/<pk>/derive/accepts an optional"title"in the request body; a blank or whitespace-only value is rejected with400.
Fixed¶
- Invalid input while deriving a design (from either the form or a
full_clean()failure) now re-renders the page with the error attached to the form, instead of the form's invalid-input path raising an unhandled 500.
Changed¶
- The REST API's
deriveaction is backward compatible: omitting"title"from the request body keeps the previous generated"<parent> (derived)"default, so existing API clients that POST no body do not break.
[0.25.0] - 2026-09-02¶
Release Summary¶
A feature release built around one idea: a design should be able to plan on the world another design leaves behind. Network gear moves out over weeks, and until now the planner who wants to put servers in the space it vacates had to wait for the physical work to finish — the plugin could only project one design over live DCIM. An approved design can now be declared as another design's base, forming a chain: the child sees the parent's result as its starting world, inherits its racks, names, planned PDUs and feed bindings, and can move or remove devices the parent only planned. Approval is what makes this safe — an approved design is read-only, so the ground under a child cannot shift without someone explicitly un-approving, which is refused while children exist. The release also fixes a data-loss bug that predates chains: deleting a device in DCIM used to silently delete every design placement referring to it.
Added¶
-
Design chains. A design can declare an approved design as its base (
Design.based_on), and its elevation then renders that design's result before its own placements: an ancestoraddoccupies a unit, amovevacates the source and occupies the target, aremovefrees it. Lineage is a tree — one base per design, many children — and each design's baseline is the linear stack of its own ancestors. Chains are declarable from the design create form, from a Derive design action on an approved parent (which seeds the child with a snapshot of the parent's rack scope), and re-pointable afterwards with a Re-base action, which is the resolution mechanism when two children of one parent conflict: they are blind to each other by design, so the first one approved wins and the other re-bases. Real ancestor-walk cycle guards now cover bothbased_onand the informationaldepends_on. See the Design chains guide. -
Downstream references to upstream planned devices. An ancestor's planned device has no
dcim.Deviceto point at, so a child could not move or remove it.DesignPlacementgainedbase_placement— the upstream placement a downstreammoveorremoveacts on — andclean()now requires device XOR base_placement for those kinds. A blade planned into a chassis an ancestor planned is a separate relation again (base_parent_placement), with a uniqueness constraint per (design, chassis, bay). All three areSET_NULL, neverCASCADE: cancelling upstream work must not delete downstream work. -
Inherited tiles in the editor. A tile the baseline contributed is drawn with a distinct dashed outline and its hover card names the design it came from. Dragging one creates a move in the current design referencing the ancestor's placement rather than editing the ancestor. Provenance and conflict are flags on a slot (
inherited,source_design_id,conflict), not new slot states, so the rendering matrix and the legend's per-state filter keep their shape — and the legend gained the entries for Displaced and Rejected it had always been missing. -
A persistent conflicts panel, merged with the stale-placement alert rather than added beside it. An upstream conflict is not the planner's to fix by editing a tile, so it must neither block saving nor vanish with a toast: it stays on screen until someone re-bases.
ProjectedElevationnow carries aconflictslist, and a Chain health report lists every design that needs attention with a re-base or review link, in four queries regardless of how many designs exist. -
Power, feeds and names span the chain. A child sees its ancestors' planned PDUs and their bank bindings, can bind its own PDU to an ancestor's planned feed — the bind dialog groups feeds into real, own planned, and planned-inherited-from-
<design>, so two feeds with the same name are distinguishable — and merges per-rack power custom fields oldest-first with its own row winning. Family name counters (_next_number,_next_pdu_slot) now count the whole chain, so a child cannot reserve a number an ancestor already took. -
Settled names.
proposed_nameconflates the planning name (IDS-1234_srv-01) with the name the device will actually carry (srv-01). Inherited placements render under the settled name, prefixes never stack, and each layer is de-prefixed exactly once. The prefix token comes from a config-declared path (naming.prefix_source) or is derived from the design title — the plugin hardcodes no custom field name. No new column: the settled name is a hook, computed where it is needed. -
REST, GraphQL and filters for lineage.
chain,deriveandrebaseactions on the design endpoint;ancestors,childrenandis_frozenon the GraphQL type; and filters for staleness, the new relations and parentless designs.
Fixed¶
-
Deleting a device in DCIM silently destroyed design history.
DesignPlacement.devicewasCASCADE, so removing a device deleted every placement that referenced it — including theremoverow that recorded the deletion, leaving a design quietly missing work with nothing to explain it. This was reachable by hand long before chains existed; chains only make it catastrophic, since applying an upstream removal would take the downstream plan with it. The FK is nowSET_NULL, paired with astaleflag andstale_device_namecaptured at deletion time ("some device is gone" is not actionable), stamped by apre_deletereceiver throughsnapshot()+save()so the transition is changelogged. A stale placement is surfaced on the design page, in the placement list and table, as an always-visible alert in the editor, and through a filter and the chain-health report; re-pointing it at a device clears the flag with no separate action. Staleness is an observation, never client input, so the API exposes it read-only. -
The name-collision warning could not see collisions that mattered.
name_exists_in_sitecompared literalproposed_namevalues, so two placements destined to become the same device name did not register — and the mechanism the family counters explicitly rely on to catch sibling collisions was therefore unreachable, since two siblings' differing prefixes make their planning names differ while their settled names match. The check now also matches on the settled plane, in both directions, at a fixed three queries regardless of chain depth. It remains a non-blocking warning. One documented limitation: its SQL prefilter is exact for the built-in strip-prefix scheme but can miss a collision under a customsettled_namecallable that returns something unrelated toproposed_name.
Changed¶
-
An approved design is read-only. Approval is what lets a child trust its baseline, so placements, power rows and feeds all refuse writes on an approved design, across the UI, the API and the
save-layoutaction (which answers409). The escape hatch is the status itself — move it back to draft — and that is refused once children exist. Onlyracksis frozen on the design record; status, summary and link stay editable. -
A chain projects only from
approvedancestors, or not at all. An ancestor in any other state — includingimplemented— refuses the whole chain with a panel row telling the planner to re-base, rather than rendering a believable rack built on a guess. There is no apply step to verify against yet, so a partially-built parent cannot be reconciled; the failure mode is deliberately a block rather than a plausible lie, consistent with how the distribution and naming engines already behave. Measured cost of an inherited layer is roughly 1.9× at depth 3 — linear in placements, not in depth — and an unchained design pays a single boolean check. -
Real blades an ancestor has already moved no longer render in their old chassis bay, which drew them twice.
[0.24.0] - 2026-09-01¶
Release Summary¶
A feature release built around one idea: a design should be able to describe
what a device becomes, not just where it goes. Deployments can now declare
their own planning fields in PLUGINS_CONFIG — the plugin ships the mechanism
and hardcodes no custom field name — and those values, along with role and
tenant, apply to a device a design relocates as well as one it adds. The
editor surfaces them in the toolbar, in a per-tile dialog, and on the hover
card for every tile in the rack. The rest of the release is performance: the
live per-bank recompute now costs one request per edit, scoped to the racks
that actually changed, after a five-rack design was spending seconds
re-answering questions the page had already asked.
Added¶
- Config-declared planning fields. A planner could set a planned device's
role and tenant, but nothing else — every other attribute a team tracks is a
custom field, and the plugin will not hardcode one. Deployments now declare
their own fields in
PLUGINS_CONFIG["placement_fields"]: each descriptor names a key, a label, a scalar type (text,numberorchoice) and where the value lands on the real device. The editor renders them from that schema — as sticky defaults in the toolbar for fields flaggedrail, and in a per-tile Planning attributes dialog — and the values are stored on the newDesignPlacement.planning_datafield. They round-trip through the REST serializer, thesave-layoutaction and GraphQL, and are readable from a naming template or script as{device.cf[<name>]}, keyed by the real custom field name so one template serves planned and existing devices alike. A newGET /api/plugins/rack-design/placement-fields/publishes the schema, since a client has no other way to learn which keys exist. The device hover card shows them as extra rows on every tile — existing gear, adds, moves, ghosts, removals and chassis blades — reading a real device's own custom field or a planned add'splanning_data, whichever applies. Configure nothing and nothing changes. See the Planning fields guide.
Changed¶
-
A design can now re-attribute a device it moves. Role, tenant and the config-declared planning fields were add-only:
DesignPlacement.clean()rejected them on any other kind, so a design could say where a device goes but not what it becomes. They are now allowed on amoveas planned overrides, and the toolbar's sticky defaults apply to a relocated device as well as a dragged-in one. Null still means "leave the device's own value alone", so a move made with an empty toolbar is a plain reposition; bringing a device home drops the overrides with it. The projection resolves override-first throughout, so the tile's role colour and the hover card show the planned attribution rather than the current one, with an "Old role" / "Old tenant" line where the two differ. The editor stamps the same attributes onto the tile the moment the move is made -- same-rack, cross-rack and reclaimed-after-a-rejected-drop alike -- so the card answers with the planned values immediately instead of waiting for a save and a reload.removestill takes none of them. -
In the editor, a tile's native browser tooltip is suppressed while the hover card is showing. The two said much the same thing, and the tooltip's ~1s delay meant it appeared on top of the card. The
titleattribute is handed back the moment the pointer leaves, and a tile the card cannot describe keeps its tooltip — so the read-only elevation, which never loads the editor script, is unaffected. -
The projected elevation loads its power data in one pass.
_device_draw_w,_device_power_portsand_device_unconnectedeach asked a device forpowerports.all(), and a related manager re-queries on every access — three round trips per device. One live distribution recompute of a five-rack design issued 1853 queries, most of them these. The elevation now prefetches power ports and port templates for every device (bay occupants included) before anything reads them: 1465 queries, and the projection's own time roughly halved. No behavioural change. -
The editor runs one live distribution recompute at a time. The endpoint replays the whole save reconciliation, so a single drop costs seconds — and one interaction produces several mutation bursts further apart than the debounce (the drag, the settle pass, the rename dialog), each firing its own request. Six of them queued behind each other turned a drop into ~17s of waiting. Further requests made while one is in flight now collapse into a single follow-up that reads the layout fresh when it runs, and the debounce went from 200ms to 400ms.
-
The planning-field resolver reads a device's stored
custom_field_datarather than thecfmerged view, which re-queriesCustomFieldon every access. Resolving one field per rack slot was costing 516 queries and ~0.7s on a five-rack editor page. -
The
planning_fieldscustom-field resolver (read_planning_field/read_planning_fields) was copy-pasted into each of the three shipped distribution scripts. It now lives in the plugin, in the newnetbox_rack_design.planning_fieldsmodule, and the scripts import it — re-exported under the same names, so a copied-out script keeps working. A malformed descriptor now raisesImproperlyConfiguredinstead of being silently skipped. -
The live per-bank recompute now costs one request per edit, scoped to the racks that changed. Three things made a single drop expensive. The editor posted a recompute on every DOM mutation, and plenty of mutations change no layout at all — a settle pass, a re-render, the heatmap recolouring every tile — so it re-asked the server for numbers the page already had; the request is now skipped when the layout is byte-identical to the last one answered. The server then re-projected every rack in the design, running the distribution engine once per rack, when one rack had been touched; the
recompute-distributionaction accepts aproject_rackslist and the editor derives it by diffing the outgoing layout against the last answered one, rack by rack. Reconciliation deliberately stays whole-payload: a device that leaves one rack is described by a placement filed under another, so scoping that half would make the source rack still show the device. Omitproject_racksand every submitted rack is projected, as before. On a five-rack design a cross-rack move now returns in ~1.2s instead of ~1.9s, and a layout-neutral churn costs nothing.
Fixed¶
-
The editor's live recompute never applied
distribution_status: the client readres.distribution_status, butrecomputeDistribution()returned onlydistributionsandpower. The "why this rack has no distribution" notice could therefore only change on a page reload, not when an edit broke the engine — the opposite of what it is for. -
Toggling the power heatmap wrote a class onto every tile in every rack with the mutation observers still attached, so a recolouring — a pure view change — registered as an edit to the whole design. The module's own renders now run with the observers detached, via a single helper rather than one hand-rolled detach/reattach pair.
[0.23.0] - 2026-08-28¶
Release Summary¶
A workspace-and-inventory release. Favorites stop being one flat list — they become named sets you switch between — planned power feeds stop being invisible and get list, detail and delete views of their own, and the multi-rack workspace learns to hang racks from a common floor instead of a common ceiling.
Added¶
- Named favorite sets. The catalog stars used to write one flat list per
user, so planning a server build and a network build meant re-starring every
time. Favorites now live in named sets ("Default", "for server", "for
network"): the Quick-access panel carries a set selector plus create, rename
and delete, the stars read and write the selected set, and the same device
type may sit in as many sets as you like. Sets are per user and their names
are unique per user — two people can each have their own "for server".
Existing favorites are migrated into a set named "Default", which is also what
a set-unaware client keeps talking to.
New endpoint
/api/plugins/rack-design/favorite-sets/(list/create/rename/ delete);/favorite-device-types/gained an optionalset_id. - Planned power feeds have their own views. A feed created by the editor's
rack-power or PDU-bind dialog could be seen nowhere and removed by nothing,
while silently sizing a greenfield rack's capacity bar.
DesignPowerFeedis now a first-class object: Rack Design → Planned Power Feeds lists them with filters, bulk edit and bulk delete; each feed has a detail page showing its derated capacity and the planned PDUs bound to it; and the design page's feed panel gained per-row edit and delete. Also exposed over REST (/api/plugins/rack-design/planned-power-feeds/) and GraphQL (planned_power_feed), and indexed for global search. - Racks of different heights now hang from a common floor. A 47U rack beside a 42U one started at the same y, so the taller rack merely ran further down and U1 of one sat level with U6 of the other. Both the editor and the read-only elevation now align rack floors, so height reads as height and the same U lines up across racks.
Fixed¶
- The editor no longer collapses to a strip when no rack is visible. A design with zero racks (or with every rack hidden) shrank the workspace to whatever was left, and since the drawer stretches to it, the Add-rack panel collapsed too — making the first rack painful to add. The workspace keeps a usable height.
- The red × says what it will do. On a planned move it now reads "Cancel this planned move" and on a planned add "Cancel this planned add", instead of "Flag for removal" on every tile — nothing is being flagged for removal there.
[0.22.0] - 2026-08-28¶
Release Summary¶
The per-bank distribution stops failing silently. When the engine breaks, the rack now says so — which exception, from which script — instead of quietly rendering an empty chip strip that looks exactly like a rack with no PDUs.
Added¶
- A rack with no per-bank chips now explains itself. Four different causes
produced the same blank strip — the script raised, the PDU has no resolvable
feed,
distribution_modeisnone, or a stale script copy is being loaded — and telling them apart meant reproducing the data and calling the engine by hand. Under the power bar the editor now renders: - failed (red): "the distribution script failed", with the exception type, its message and the script's dotted path on hover;
- empty: which PDU had to be omitted and why (input not cabled to a
dcim.PowerFeedand not bound to a planned feed / no parseable outlet banks), or that the rack has no PDU device at all; - off: that per-bank distribution is switched off, so a configuration answer never reads as missing data.
generate_distribution_status(elevation)returns(distribution, status); the status also rideselevation.power["distribution_status"]and a newdistribution_statusblock in therecompute-distribution/response, so an edit that breaks the engine reports itself immediately rather than at the next page load.generate_distribution(elevation)is unchanged for scripts and existing callers.
[0.21.1] - 2026-08-28¶
Release Summary¶
A rack could lose its entire per-bank distribution — every PDU chip gone — because of a single device fed from a PDU in another rack.
Fixed¶
- One device cabled to a neighbouring rack's PDU no longer wipes the whole
rack's per-bank distribution. The shipped distribution scripts charged a
device to the bank of whatever outlet it was cabled to, without checking that
PDU belongs to the rack being computed. A foreign PDU is not in that rack's
topology, so the charge raised
KeyError,generate_distributioncaught it and returnedNone, and the rack fell back to the per-device heatmap with no bank chips at all. Cross-rack feeding is ordinary — shared PDUs, and a device moved between racks keeps its cabling until the plan is implemented — so the foreign cabling is now ignored and the device is attributed by U position instead, exactly as the builtin engine has always done._chargealso skips an unresolvable PDU or bank rather than raising, so no future caller can take a rack's distribution down this way. Affectsdistribution_exampleanddistribution_advanced_example; the builtin (distribution_mode = "builtin") was never affected.
If you copied an example script into SCRIPTS_ROOT, replace that copy from
this release — a pip upgrade does not touch files under scripts/.
[0.21.0] - 2026-08-28¶
Release Summary¶
Planned power feeds stop being write-only. They size a greenfield rack's capacity bar, yet nothing in the UI listed them and nothing could remove one, so a feed copied by mistake inflated the bar with nothing to point at — and "copy from rack" only ever added, which meant copying from several racks in turn left the union of all of them. Copying now replaces, and the feeds a design plans are visible on the design page and removable from the rack power dialog.
Changed¶
- "Copy from rack" now replaces a rack's planned feeds instead of adding to
them. Only a rack-name prefix is retargeted when a feed is copied, so feeds
named by any other scheme (
Utility A) never collided — copying from several racks in turn left the union of all of them, and the rack's capacity bar read as the sum of every source ever clicked. The target now ends up fed exactly like the source. A feed whose name survives the copy keeps its row, and with it every PDU bound to it; the rest are removed and their PDUs unbound, with the count reported in the confirmation. A source rack with no feeds is a no-op rather than a wipe, since that is far likelier to be a mis-click than an instruction to strip the rack's supply.
Added¶
- Planned power feeds are visible and removable. They size a greenfield rack's capacity bar but had no UI at all — no list, no detail page, no way to delete one short of deleting the whole design. Now: the rack power dialog lists the rack's planned feeds with a × on each, and the design detail page gains a Planned power feeds panel showing every feed, its rack, its electricals, the derated watts the capacity bar actually uses, and the PDUs bound to it. Removing a feed reports how many PDUs lost their binding.
DELETE /api/plugins/rack-design/designs/<pk>/planned-feed/— addressed byfeed_id, or byrack_id+name. Requireschange_design, since it edits a design rather than deleting one.
[0.20.2] - 2026-08-27¶
Release Summary¶
A one-fix patch: a planned device could not be dragged into another rack. The gesture was refused at the destination's drop gate, which admitted only tiles carrying a real device — so the tile snapped back, silently, while the same drag inside its own rack worked.
Fixed¶
- A planned device can be moved into another rack. Dragging a newly added tile to a different rack did nothing: the destination rack's drop gate admits only tiles carrying a real device, so a planned add was refused, snapped back, and left no trace in the console or the payload — while moving it inside its own rack worked, which made the failure look arbitrary. A planned add is now adopted like any other tile, and it is the simplest cross-rack case there is: nothing physical stays behind, so there is no origin ghost, no homecoming and no rename prompt, and the placement keeps its role, tenant, full-depth flag and the name it was given. The same gesture works one layer down: a planned blade now moves between chassis columns, with its bay re-addressed by the destination column.
[0.20.1] - 2026-08-27¶
Release Summary¶
A one-fix patch for a save that could not complete. Removing planned blades and
dropping new ones into the same bays was rejected with
unique_design_planned_bay is violated, because the replacement was written
before the cancelled occupant was deleted.
Fixed¶
- Re-filling a bay freed in the same save no longer fails with a constraint violation. A device bay is the only placement target carrying a per-design UNIQUE constraint, and the editor replays a cancelled add at the end of its bucket — the tile is gone from the grid, so it is appended from the capture taken when the × was clicked. The replacement therefore reached the database while the cancelled placement still claimed the bay, and the whole save was rejected instead of the bay changing hands. Items that free a slot are now written before items that claim one, decided server-side so that any client cancelling and re-filling in one submit is correct, and applied to every bucket rather than as a bay special case.
[0.20.0] - 2026-08-26¶
Release Summary¶
A chassis and a rack become one implementation. 0.19.0 shipped the chassis layer
by giving a chassis its own copy of the rack's add / move / remove / cancel
logic, and every one of the bugs fixed below lived in that copy rather than in
the behaviour it duplicated. The editor, the save view and the projection are now
written against a Frame — an enclosure — and its Containers, addressable
grids of slots: a rack Frame has a front and a rear container, half-unit steps and
full-depth pairing; a chassis Frame has one bay container, whole-bay steps and no
pairing. Nothing downstream knows which it is holding, so a fix to rack behaviour
is now automatically a fix to chassis behaviour. Also here: the layer is renamed
for the container rather than its contents, a chassis column is offered only for a
device that actually has bays, and bay occupancy is finally visible without the
editor. The abstraction is written up in docs/editor-behavior-spec.md §2.6.
Breaking Changes¶
- The chassis layer moved from
/plugins/rack-design/designs/<pk>/blades/to/plugins/rack-design/designs/<pk>/chassis/. The old path is gone; update any bookmark or link. Nothing stored changes — no migration, no data conversion.
Changed¶
- A chassis and a rack are now one implementation, not two. The editor, the
save view and the projection are written against a Frame (an enclosure) and
its Containers (addressable grids of slots) — a rack Frame has a front and a
rear container, half-unit steps and full-depth pairing; a chassis Frame has one
bay container, whole-bay steps and no pairing. Nothing else in the pipeline
knows the difference. This deleted the three copies of rack logic the chassis
layer had accumulated (
chassisColumnPayloadin the editor, the 158-line_reconcile_bay_itemin the save view, and duplicated state/label derivation in the projection), so a fix to rack behaviour is now automatically a fix to chassis behaviour. Documented indocs/editor-behavior-spec.md§2.6. - The blade layer is now the chassis layer, named for the container rather
than its contents: a bay holds a blade in a compute chassis, but equally a
module in a patch panel or a shelf insert. The URL moves from
/designs/<pk>/blades/to/designs/<pk>/chassis/and the rack view's button reads Chassis. - A device earns a chassis column by HAVING A BAY, not by carrying
subdevice_role=parent. The role is widely set on plain servers — one 4475-device instance had 2306 such devices with no bay at all — and each drew an empty0/0column nothing could be dropped into. A planned chassis is filtered the same way, on its type'sDeviceBayTemplates.
Added¶
- Read-only chassis elevation at
/designs/<pk>/chassis-elevation/, linked from the rack elevation. Bay occupancy is not an editing concern: reviewing a plan should not require change permission or the editor. It renders the same columns from the same projection as the editable layer, via a sharedinc/chassis_column.html.
Fixed¶
- A blade could not be moved between bays. GridStack asks the destination grid whether it accepts a tile before it will hand it over, and that gate identified a blade by a DOM marker only the palette stamps — so a rendered blade tile answered "not a blade" and every chassis column refused its own kind. The tile snapped back, Save stayed disabled and nothing was logged. Containment decides now.
- A blade removed from a bay rendered as nothing at all. The removal was saved correctly but the chassis projection looked for blades only by their planned target, which a removal does not have, so the bay came back empty and the removal looked discarded. It is found through the device's own bay now.
- A bay freed by a removal stayed blocked for the rest of the session: the vacated-device set the editor is given skipped bay items entirely, and that set overrides the model's own fallback, so even an already-saved removal did not free its bay.
- Cancelling an already-saved planned blade is no longer silently dropped. The cancel now carries its own bay address, so it reaches the server instead of being discarded while Save reported success.
- Moving a blade between two bays of one chassis is no longer reported as "no
change". The idempotency snapshot did not include the bay fields, so the two
positions hashed identically; a round-trip that changes nothing still answers
304, and one that moves a blade no longer can.
[0.19.0] - 2026-08-25¶
Release Summary¶
Device bays and blades. A chassis was previously an opaque tile: its bays were
invisible, its blades polluted the non-racked tray, and nothing about them could
be planned. This release models bays as first-class planning targets, adds a
blade layer for editing them, and closes a power-accounting gap that came
with them. Full design rationale in docs/editor-behavior-spec.md §10.
The organising idea is that a bay is to a device what a unit is to a rack face — so a chassis is rendered as a rack, and the entire existing movement pipeline (validate → confirm → commit, blocking, ghosts, homecoming, cursor-governed placement) applies to blades with no new gesture code. The only difference is the step: one bay instead of half a rack unit.
Added¶
- Blade layer at
/plugins/rack-design/designs/<pk>/blades/— every chassis in a design's scope rendered as a column of bays, driven by the unmodified rack editor. Reached from a Blades button in the rack view, which appears only when the design's scope actually contains a chassis. - Plan a blade into a bay, in both cases that exist: a chassis already in
DCIM (
target_bay), and a chassis this same design adds (parent_placement+target_bay_name, validated against the type'sDeviceBayTemplates, since no bay rows exist until apply). - The layer's palette is filtered to child device types, so a rack-mountable device can never be offered as a blade; conversely a blade is refused by every rack grid. Container/type agreement is enforced at the drop gate, not after the fact.
- Chassis visibility — per-user show/hide in the layer's Chassis panel, the
blade-layer twin of the Design-racks panel (
HiddenDesignChassis). - Bay occupancy on the rack page: a chassis tile's hover card reports
N of M bays usedplus one line per occupant — in the editor and on the read-only projected elevation alike. - The power heatmap toggle is now on the read-only elevation too, not only in the editor. It is a pure view layer, so the same script drives both; the on/off choice is shared between the two pages.
- REST and GraphQL expose
parent_placement,target_bayandtarget_bay_name, with?target_bay_id=and?parent_placement_id=filters; the create/edit, import, bulk-edit and filter forms all carry them, and the edit form's chassis selector scopes the bay picker (an unfilteredDeviceBaylist is unusable). - Save-before-switch: changing layer with unsaved work asks Cancel / Discard / Save and switch instead of relying on the browser's generic unload warning.
Fixed¶
- Blades no longer render in the non-racked tray. A child device keeps its
rackbut is forbidden a position, so it satisfied the tray'sposition__isnull=Truetest and drew alongside real 0U accessories. On a populated instance that is every blade in the rack. - Blade power is now accounted for. Chassis and blades can never both be
counted: if the chassis has a resolvable draw it wins and its blades are marked
as already included; if it has none, the blades roll up into it. This mirrors
what core achieves through cabling (
PowerPort.get_power_draw()aggregates downstream only when the port carries no value of its own) — a plan has no cables, so the same result is derived from containment.
Changed¶
- Bays are addressed by number rather than name in the UI. Real bay names are
a mix of
slot3,top-leftandpci9, so the index is the only stable identifier; the real name stays in the tooltip. - The save payload gains a fourth per-rack bucket,
bays, processed after the face buckets so a blade can reference a chassis created by the same submit. Anexistingbay item is reconciled against reality server-side, exactly as face items are — the client never claims something moved. target_faceis now preserved for full-depth devices. Saving a layout used to blank it, a dedup trick from when the editor POSTed one copy of a full-depth tile per face; the editor has since sent a single item, so the stored face was being discarded for nothing. API consumers diffing on(device, position, face)no longer miss every full-depth row. A client that still submits both copies gets one placement as before, and the face it was mounted on wins rather than whichever copy arrived last.
Migrations¶
0008—DesignPlacement.parent_placement/target_bay/target_bay_name, plus two uniqueness constraints (one design may claim a given bay once).0009—HiddenDesignChassis.
Both are additive; no existing data is rewritten.
[0.18.0] - 2026-08-24¶
Release Summary¶
Minor release fixing REST API conformance. Foreign keys on the Design, DesignGroup and DesignPlacement endpoints were serialized as bare integers instead of the nested brief objects every core NetBox endpoint returns. They now match core. A latent N+1 on the placements endpoint was found and fixed in the same pass, so the list endpoints issue fewer queries than before despite returning more data.
Breaking Changes¶
- REST API foreign keys now return nested objects instead of integers. A response
field that previously read
"site": 345now reads"site": {"id": 345, "url": "...", "display": "AMS1", "name": "AMS1", "slug": "ams1", ...}. Affected fields:Design.site/group/root/based_on/depends_on,DesignPlacement.design/device/device_type/device_role/tenant/target_rack, andDesignGroup.parent.
Migration: read the id from the nested object — design["site"]["id"] instead of
design["site"]. Alternatively request ?brief=true, or select the fields you need
with ?fields=. Writes are unaffected: POST and PATCH still accept a bare primary
key (or an attributes dict) for every one of these fields, so no client that only
writes needs to change.
Fixed¶
- Foreign keys across all three API serializers were listed in
Meta.fieldsbut never declared, so DRF fell back toPrimaryKeyRelatedField. They are now declared with the coreSerializer(nested=True)pattern; the four self-referential relations (Design.root/based_on/depends_on,DesignGroup.parent) use dedicatedWritableNestedSerializerclasses, matching how core handles its own recursive relations. DesignPlacementViewSethad noselect_related/prefetch_relatedat all. Because NetBox's base viewset prefetches every relation listed inMeta.fieldsregardless of whether the serializer declares it, those rows were already being fetched and then discarded. All three viewsets now join their foreign keys up front.
Changed¶
- Query-count baselines updated for the reduced query counts on the list endpoints:
designplacement15 → 12,designgroup13 → 12,design16 → 15.
[0.17.2] - 2026-08-21¶
Release Summary¶
Certification-readiness release. No functional change — the plugin behaves exactly as 0.17.1 does on NetBox 4.4 through 4.6. This corrects the package metadata a NetBox Labs certification review reads first: the distribution described itself with the generator's placeholder one-liner, the README compatibility matrix had fallen behind the shipped versions, and the maturity classifier still said Alpha.
Changed¶
- Package description replaced the cookiecutter placeholder
(
"NetBox plugin for Rack Design.") with a real value statement, in bothpyproject.tomlandPluginConfig.description. This is the summary shown on the PyPI project page and in NetBox's own installed-plugins list. - README compatibility matrix now lists
0.17.x; it previously stopped at0.16.0, three releases behind.COMPATIBILITY.mdwas already current. - Maturity classifier raised from
Development Status :: 3 - Alphato4 - Beta, matching the shipped feature set.
[0.17.1] - 2026-08-21¶
Release Summary¶
Packaging-metadata release. No plugin code changed — the plugin behaves exactly as
0.17.0 does on NetBox 4.4 through 4.6. This ships the project links that point users at
the newly completed documentation: the full ten-part video tutorial, and a Documentation
URL that now resolves to the published docs site rather than the raw README on GitHub.
Changed¶
[project.urls] Documentationnow points at https://ravenrs.github.io/netbox-rack-design/ (the MkDocs site) instead of the README blob on GitHub, so the PyPI sidebar links to the real documentation.- README links the complete ten-part tutorial playlist alongside the existing two-minute quick tour.
Added¶
[project.urls] Tutorial— the full tutorial playlist, surfaced as a link on the PyPI project page.
[0.17.0] - 2026-08-20¶
Release Summary¶
The power-distribution engine's three modes are now cleanly separated. builtin is
purely native: it derives breaker banks from PDU outlet names and feed bindings and
reads no custom fields at all, so a public install can no longer be steered by
field names that only mean something inside one organisation. Everything
custom-field-driven — a rack power ceiling, a PDU orientation — belongs to
distribution_mode = "script", which is the tier meant to carry a site's own
conventions. Alongside that, greenfield planning gets a real shortcut: the rack power
dialog can now clone a sibling rack's feeds, and the editor's power bar stops waiting
for a Save to tell the truth — capacity now refreshes live when feeds change, exactly
like the per-bank chips already did.
Breaking Changes¶
distribution_mode = "builtin"no longer reads thepower_limitationorpdu_locationrack custom fields. Previously the built-in engine read both literally offrack.cf, which shipped one organisation's field names in a public plugin — the documentation had always claimed the opposite. Inbuiltinthe unit-to-bank split now always uses the fixed default direction, no rack ceiling is applied (Distribution["rack"]["power_limitation_w"]is alwaysnull) andDistribution["pdu_location"]is alwaysnull. Bank-overload alarms, which come from feed data alone, are unchanged. Migration: if you relied on either field, setdistribution_mode = "script"withdistribution_scriptpointing atnetbox_rack_design.distribution_example.build(or your own copy), and declare the fields inplanning_fieldsincluding each entry'ssource, e.g.{"key": "power_limitation", "type": "number", "source": "cf.power_limitation"}. An entry withoutsourcerenders in the dialog but resolves to nothing.- The per-design rack power override (
DesignRackPower) is applied inscriptmode only. It exists to feed a script; inbuiltinit is ignored. - The rack and PDU planning dialogs only show their custom-field inputs when
distribution_mode = "script". Innone/builtinthe rack dialog keeps its "Copy from rack" row and explains why the manual fields are hidden, rather than offering inputs nothing would read.
Added¶
- Copy a rack's power from a sibling rack. The rack "Power" dialog's Copy from
rack mode now clones the source rack's feeds as planned
DesignPowerFeedrows, not just its custom fields — a greenfield rack in a row inherits the supply of the provisioned rack next door in one step. Copied feeds are renamed for the rack they land on (R101-A→R103-A; any other naming scheme is kept verbatim) and upsert by(design, rack, name), so re-copying updates instead of duplicating. New endpointPOST /api/plugins/rack-design/designs/<pk>/copy-feeds/;power-source/?kind=racknow also reports the source rack's feeds so the dialog can preview them (falling back to the design's planned feeds when the source rack has none). recompute-distributionalso returns each rack's power summary (powerblock: draw, capacity, utilisation, thresholds), which is what makes live capacity possible — the derating and phase maths stay on the server.
Fixed¶
- The rack power bar's capacity is now live. Adding or copying a planned feed used
to leave the denominator at whatever the page was rendered with until the next Save
or reload, so a greenfield rack could read
1000 / 1000 Win red while its banks were green. Capacity and thresholds now refresh from the server on the same signal the per-bank chips already used. - A freshly dropped PDU no longer paints as the unknown-draw hatch. The palette
resolved a device type's draw without knowing the role it would be given, so a PDU —
whose inlet template carries no draw — came back "unknown", while the very same PDU
after Save read a known 0 W like passive gear.
device-type-power/now accepts an optionalrole_idand applies the projection'spower_exclude_rolesrule, and the palette re-stamps its rows when the Role selector changes.
Changed¶
docs/pdu-distribution-spec.mdanddocs/power-distribution.mdnow describe the three tiers as implemented, scope every "the plugin never hardcodes your custom-field names" claim to the script tier, documentsourceas script-only, and correctpdu_location, which was documented as a PDU-level custom field when it is rack-level.
[0.16.0] - 2026-08-19¶
Release Summary¶
NetBox 4.5 and 4.6 support. The plugin declared max_version = "4.4.99", so every
install on a current NetBox refused to load with "requires NetBox maximum version
4.4.99" (#25) — two minor versions of NetBox had shipped past the supported ceiling.
The range is now 4.4.0 – 4.6.99, served by a single codebase: all version-specific
knowledge lives in one new module, netbox_rack_design/compat.py, and CI tests every
declared version rather than only the oldest. Almost nothing in the plugin itself had
to change — two core APIs moved and the rest was test scaffolding. Also fixes a rack
editor bug, unrelated to NetBox versions, where a dragged device landed away from the
green landing preview whenever the browser was not at 100% zoom.
Added¶
- NetBox 4.5.x and 4.6.x are supported (
min_version4.4.0,max_version4.6.99). Verified end to end against 4.4.8, 4.5.10 and 4.6.8: the plugin's own test suite, a missing-migration check, and the Playwright editor suites all pass on each, the last of these against a real database migrated forward from 4.4. - CI now tests the whole declared range.
.github/workflows/ci.yamltakes NetBox as a matrix dimension (4.4.8 and 4.5.10 on Python 3.12/3.14, 4.6.8 on 3.12/3.13/3.14,fail-fast: false), so the declared compatibility range can no longer drift away from what is actually tested. netbox_rack_design/compat.py— the single module allowed to know which NetBox it runs on. Each shim uses capability detection rather than a version comparison, so it keeps working when the next minor moves things again.
Fixed¶
- A dragged device now lands where the cursor points at any browser zoom level.
Away from 100% zoom, a device could land up to several rows from the green landing
preview — worst on multi-U devices. Two row computations disagreed: the plugin
measures the grid's real rect, so its row height tracks zoom (11px at 100%, 12.1 at
110%, 16.5 at 150%) and matches the pointer coordinates, while GridStack uses a fixed
cellHeightof 11 CSS px and is therefore only correct at 100%. Placement accepted the engine's row whenever the pointer fell inside the tile as parked — comparing rows measured in those two different spaces — so under zoom it committed the wrong one while the preview showed the right one. The cursor's row is now always authoritative. - A whole-U device no longer lands on a half-unit boundary. The in-tile grab offset and the pointer row are floored independently, so drifting half a unit vertically during a move flipped the parity. Whole-unit alignment is now restored for a device that started on a whole unit, leaving genuinely half-unit-mounted devices (a rack with half-unit mounting holds devices at x.5) on their own rows.
- API tokens minted by the test suite work on 4.5+. From NetBox 4.5 a new token is
a v2 token whose secret is never stored, so
Token.key— the entire secret in 4.4 — became a non-sensitive identifier and the legacy header form returns403 Invalid v1 token.
Changed¶
Designno longer inheritsPrimaryModel. That base contributed onlydescriptionandcomments, both now declared onDesignitself with identical definitions, and NetBox 4.5 additionally gave it anownerforeign key. Inheriting a base whose field set changes between NetBox minors would make the plugin's own schema version-dependent — the same model would need a migration on 4.5+ that is invalid on 4.4. No migration and no database change on any supported version: the two columns were already materialized in migration 0001, andmakemigrations --checkis clean on 4.4.8, 4.5.10 and 4.6.8 alike. The REST API and the GraphQL filter input are both unchanged;Designgains noownerfield.- The
Framework :: Djangoclassifiers now list 5.2 and 6.0, the Django versions the supported NetBox range ships (4.4/4.5 → 5.2, 4.6 → 6.0).
Notes for GraphQL consumers¶
Independently of this plugin, NetBox 4.5 changed GraphQL filter syntax: queries
filtering by object ID or enum must now use a lookup, so id: 123 becomes
id: {exact: 123} (netbox-community/netbox#19338). On 4.5+ the id field of every
plugin filter input is therefore IDComparisonFilterLookup rather than ID. Queries
written against 4.4 need updating when the NetBox host is upgraded; the plugin's filter
fields themselves are unchanged.
[0.15.3] - 2026-08-18¶
Release Summary¶
Bug-fix release for the editor's power view. Four defects made per-rack and per-bank power read wrong in specific topologies: a greenfield rack sized only by planned feeds fell back to the flat capacity default and painted itself critical-red while its own bank chips read green; the rack chassis kept the colour the page had loaded with after a live NetBox theme switch; a cross-rack move dropped the moved devices' draw out of both racks' bank chips; and a rack fed only by a B-named feed charged single-corded devices nowhere. The per-bank chip strip now also tracks the rack block width, so it narrows in step with the Front/Rear face toggles instead of sitting as a fixed-width island under the bar.
Fixed¶
- A greenfield rack's power bar now counts its planned feeds.
_rack_capacity_w()only summed realdcim.PowerFeedrows, so a rack whose supply exists only as plannedDesignPowerFeeds fell back to the flatpower_capacity_default_w— the bar painted the rack critical-red while its own per-bank chips, sized from those same planned feeds, read comfortably green. Planned feeds now contribute at the same derating NetBox applies to a real feed (POWERFEED_DEFAULT_MAX_UTILIZATION), so a planned 2 × 230 V × 32 A rack reads 11,776 W just like its provisioned neighbours. The flat default remains the fallback only when a rack has no feeds of either kind. - The rack chassis now follows a live theme switch. Switching NetBox's colour
mode left the rack drawn in the colour the page had LOADED with — a black rack
on a white page — until the next reload. NetBox's navbar toggle restyles the
page by setting
data-bs-themeon<body>while<html>keeps its server-rendered value, so the plugin's palette (declared on:rootonly) never re-resolved. Both palettes are now declared wherever the attribute lands. - A cross-rack move now carries its power into the destination rack's banks. Dragging devices from one rack to another dropped their draw from the per-bank chips of both racks: a real device keeps its cabling to the SOURCE rack's PDU until the design is implemented, so the destination charged it to a PDU absent from its own topology and skipped it. Cabled attribution is now scoped to PDUs in the rack being computed; cabling that leads elsewhere falls back to U-position attribution, exactly like a planned add. Cabling inside the rack still wins over position.
- A rack with no
aleg no longer swallows single-corded devices. Leg letters come from the feed names ("Feed B" →b), so a rack fed only by a B-named feed had noaleg and a single-PSU device was charged nowhere. Such a device now charges the legs the rack actually has.
Changed¶
- Per-bank chip strip now tracks the rack block width. The rack power bar already resized with the Front/Rear face toggles while the bank chips stayed a fixed-width island, so the two read as unrelated widgets. The PDU columns now share the block width (growing from a 8.5rem basis), so hiding a face narrows the strip in step with the bar above it and the columns stack instead of leaving empty space.
[0.15.2] - 2026-08-18¶
Release Summary¶
Bug-fix release for the editor's power heatmap. Three defects made the power view show less than it knew: device tiles were tinted by their PDU bank's utilization rather than their own draw, so every device in a hot bank painted identically red and a 300 W switch was indistinguishable from an 800 W server; the "unknown draw" hatch never rendered, so a device with an unmeasured power port looked exactly like passive gear with no ports at all; and the per-bank chip strip was still hidden behind the heatmap toggle despite 0.15.0 intending it to be always visible.
Fixed¶
- Heatmap shows each device's own consumption. A tile's fill and colour are now its draw as a share of the rack's biggest consumer (green → red), instead of restating its bank's utilization. Bank health is carried by the per-bank chips, where it is not repeated once per tile. Feed A/B edge accents are unchanged.
- Unknown-draw hatch now renders. The neutral hatch for a device with a power
port but no known draw was defined but never applied: the generic heatmap rule
carried two
:not()guards, putting it at a higher specificity than the unknown-draw rule, and both used!important. The unknown rule now carries the same guards and wins. - Per-bank chip strip is always visible. 0.15.0 made the strip render unconditionally but left the CSS rule gating it on the heatmap toggle, so it stayed hidden until the toggle was on. The strip now follows the power bar and is always shown; the heatmap toggle controls per-tile shading only.
[0.15.1] - 2026-08-17¶
Release Summary¶
Bug-fix release: the in-app documentation ("?") help link on the plugin's object
detail pages now resolves. NetBox's default docs_url points at
/static/docs/models/…, which only exists for NetBox's own core docs — a plugin's
docs are not built into that path, so the help link 404'd. Following the plugin
developer guide (Database Models: "Plugin models can override this to return a
custom URL … your plugin's documentation"), the models now override docs_url to
the plugin's hosted docs site.
Fixed¶
- In-app documentation link.
Design,DesignGroup, andDesignPlacementnow overridedocs_urlto point at the hosted docs (https://ravenrs.github.io/netbox-rack-design/), so the object detail page's help link opens the documentation instead of returning a 404.
[0.15.0] - 2026-07-31¶
Release Summary¶
Feature release making the editor's per-PDU/per-bank power distribution live. The
PDU bank chips (and the bank heat-tint) previously recomputed only when you pressed
Save, while the total power bar updated in real time — so an edit left the two power
views disagreeing until a save. Now the bank distribution recomputes on every
add/remove/move, exactly like the bar. Rather than duplicate the distribution logic
in the browser (bank assignment depends on real cabling and, in script mode, a custom
distribution_script), the editor re-runs the very same server engine over the
unsaved layout through a new read-only endpoint that applies the edit inside a
rolled-back transaction — live figures, nothing persisted. The bank chip strip is now
always visible (not gated on the heatmap toggle). Read-only over DCIM throughout.
Added¶
- Live per-bank power distribution in the editor. The PDU bank chips now refresh
on every tile add/remove/move, matching the always-live power bar. A new read-only
POST /api/plugins/rack-design/designs/<id>/recompute-distribution/action re-runs the distribution engine (builtin or a customdistribution_script) over the current unsaved layout — applying it through the save-layout reconciliation inside a rolled-back transaction — and returns the fresh per-rack distribution without persisting anything. The per-bank chip strip is now always-on. See docs/pdu-distribution-spec.md.
Fixed¶
- The "Power heatmap" toggle now survives a Save. Saving reloads the editor, which previously reset the heatmap toggle to off, forcing you to re-enable it after every save. The toggle state is now remembered and restored on load.
[0.14.0] - 2026-07-31¶
Release Summary¶
Feature release for the shipped naming/distribution examples. The example modules
(naming_example, distribution_example, distribution_advanced_example) now each
carry a small extras.scripts.Script subclass, so they register as first-class
NetBox Scripts: you can add them straight through Customization → Scripts → Add
(or by dropping the file into SCRIPTS_ROOT) and open/edit them via the Edit
button, instead of hand-wrapping the example before it will appear. The naming logic
is unchanged — the plugin still calls the module-level build_name / build
directly, and those remain strictly read-only over DCIM.
Added¶
- Example scripts are now addable/editable from the UI.
naming_example.py,distribution_example.py, anddistribution_advanced_example.pyeach ship a thinScriptsubclass (DeviceNamingScript/PowerDistributionScript/PowerDistributionAdvancedScript) whoserun()is a documented no-op — the class exists only so NetBox lists the module under Customization → Scripts, where it can be viewed and edited. Referencing the example (scripts.<module>.build_name/.build) drives naming/distribution live, with edits picked up on the next preview and no restart. See docs/device-naming.md.
[0.13.5] - 2026-07-29¶
Release Summary¶
Bug-fix release for the multi-rack editor. On a planned-add tile that is only a single rack unit tall, the inline proposed-name input was clipped away by the tile's height, leaving no way to override the auto-generated name. The name is now shown compactly on the tile and edited through a small pencil affordance that pops the input out over the tile, so renaming works at any tile height.
Fixed¶
- Editable name on 1U add tiles. A planned-add tile stacked its device-type label above an always-visible name input; on a 1U-tall tile the input fell outside the clipped tile and could not be seen or focused. The tile now displays its assigned name and exposes a hover-revealed pencil (bottom-left, clear of the remove × and the PDU power control) that opens the name field as a pop-out overlay, keeping it fully usable regardless of tile height.
[0.13.4] - 2026-07-29¶
Release Summary¶
Bug-fix release. The "Placements" column on the Designs list (and the "Designs" column on the Design Groups list) always showed 0, regardless of the real number of related records. The list views now annotate the counts the tables display.
Fixed¶
- Designs / Design Groups list counts no longer show 0. The
DesignListViewandDesignGroupListViewquerysets now annotateplacement_count/design_count(viacount_related), which theLinkedCountColumnreads — previously the columns rendered 0 for every row because the accessor was never annotated. This also fixes the embedded designs table on a Design Group's detail page (rendered through the same design list).
[0.13.3] - 2026-07-23¶
Release Summary¶
Bug-fix release for the multi-rack editor. Fixes a layout-on-reload defect where a planned device placed on rows freed by a within-rack move could be pushed down the rack after saving and reloading the editor. Also adds project screenshots to the README.
Fixed¶
- Reclaimed rows no longer shift on reload. When a device was moved within a rack, the rows it vacated (marked by a move-out ghost) could be reclaimed by a planned add or move-in. On reload, GridStack's initial collision pass ran over the still-attached ghost before it was detached and collapsed, pushing the reclaiming device down the rack. The editor now brackets that initial grid hydration in the same push-suppression the live-editing gestures already use, so reloaded placements render exactly where they were saved. Live editing was never affected.
Changed¶
- README now includes screenshots of the editor, power heatmap, device palette, and dialogs.
[0.13.2] - 2026-07-23¶
Release Summary¶
Consistency release: two small power-model fixes plus a documentation refresh.
The per-bank power heatmap now honors the same power_warn_pct /
power_critical_pct thresholds as the rack-level bar, and the power projection
now reports which powered devices lack draw data. The README, roadmap, and site
navigation are brought up to date with what has actually shipped.
Fixed¶
- Per-bank heatmap thresholds are now configurable. The
builtindistribution mode previously hard-coded its warn/critical bank-state thresholds at 80% / 100%; it now readspower_warn_pct/power_critical_pctfromPLUGINS_CONFIG(same defaults, same keys as the rack-level power bar), so both surfaces color consistently. - Power projection reports unknown-draw devices.
_project_powernow returnsunknown_draw_count/unknown_devices— powered devices that carry no draw value (counted as 0 W but flagged), distinct from the existingunconnected_*cabling-gap flag — matching the documented output contract.
Changed¶
- Documentation refreshed to the shipped state. The README feature list and roadmap now reflect the delivered naming engine, power projection, and PDU power distribution (previously listed as planned); the compatibility and configuration tables are current. The docs site navigation exposes the power/distribution guides and specs.
[0.13.1] - 2026-07-23¶
Release Summary¶
Documentation and reference release for the power-distribution feature. Adds a user-facing how-to guide and a second, richer example distribution script, and scrubs a stray site-specific custom-field name from the shipped reference example's docstring. No behavior or API changes.
Added¶
docs/power-distribution.md— a user-facing how-to for power distribution (modeled ondocs/device-naming.md): the threedistribution_modes (none/builtin/script), the two universal conventions, feed binding and planned PDUs, theplanning_fieldscustom-field bridge, writing/adapting a distribution script, and verify-on-instance steps. The design spec (docs/pdu-distribution-spec.md) remains the deep reference.netbox_rack_design.distribution_advanced_example.build— a second, richer reference script alongside the minimaldistribution_example. It demonstrates the customization surfaces the minimal example leaves out: a computed topologyschemelabel (from the per-PDU bank-count signature), a per-PDUpdu_schemeoverride read viaplanning_fields["pdu"], and WARN/CRITICAL bank thresholds read from plugin config (power_warn_pct/power_critical_pct). Reuses the shared distribution helpers; read-only.
Changed¶
- The shipped
distribution_exampledocstring no longer references a site-specific custom-field name in its illustrativeplanning_fieldsmapping (now a generic placeholder).
[0.13.0] - 2026-07-22¶
Release Summary¶
Feature release adding universal PDU power distribution — a per-PDU / per-bank
power heatmap that works out of the box, plus a full planning model for
greenfield racks. Distribution now has three tiers: a zero-config builtin
mode driven by two universal naming conventions, a custom-field bridge for
sites that keep power limits in custom fields, and a script mode for bespoke
logic. A planned PDU binds to a real or planned power feed (mirroring
NetBox's native power model) so its breaker is sized the same way whether the
rack is provisioned or greenfield. Fully backward-compatible and read-only over
dcim.
Added¶
- Universal PDU power distribution + per-bank heatmap (see
docs/pdu-distribution-spec.md). Thedistribution_modeconfig key now acceptsbuiltinalongsidenone(default) andscript: builtincomputes a real per-PDU/bank distribution with no script, from two documented conventions — the bank is the first segment of the outlet port name (1/1→ bank 1), and a PDU's feed-leg comes from the feed it is bound to (below), not from name parsing.- The editor's power heatmap colors each PDU bank as a filled health bar (load-vs-breaker, overload = hard red), feed-leg-colors the per-PDU column headers (A/B), and gives each consumer tile an A/B feed edge for the leg(s) it lands on.
- The shipped reference
netbox_rack_design.distribution_example.buildruns the same algorithm as a copyable script. A missing / unimportable / non-callable / raising script falls back to the per-device heatmap and never errors the page. - Power feed model + binding for planned PDUs. A PDU sizes its breaker from
the feed it draws from — a real cabled
dcim.PowerFeed, or a new plugin-sideDesignPowerFeed(name, V/A/phase/supply, scoped per design + rack) for greenfield racks. A planned PDU binds to one via two nullable FKs onDesignPlacement(real_power_feed/planned_power_feed, mutually exclusive), so the read path is uniform (real or planned → "read the bound feed"). The bind-to-feed dialog opens on PDU add — real feeds first, with a "define planned feed" fallback — and a per-rack Power button (shown when a rack has no real feeds) manages planned feeds and rack overrides. - Custom-field bridge (
planning_fields). A config schema maps a site's custom fields only (e.g.power_limitation,pdu_location) into the planning dialogs and the script's view of the rack — native fields are always read directly, never configured. A planned PDU can also reference a real PDU device (power_source_deviceFK) to inherit its custom fields live, or carry them manually inpower_config. Rack custom-field overrides persist per design inDesignRackPower. - New read-only API actions back the dialogs:
GET feeds/,GET/POST planned-feed/,GET/POST rack-power/, andGET power-source/(rack copy-from). Save-layout items carry the feed binding and cf-source. Migration0007addsDesignPowerFeed,DesignRackPower, the binding / cf-source FKs, andDesignPlacement.power_config. - Debug logging across the whole path (PDU + feed resolution, unit→bank map, per-device charge, overrides, every graceful fallback), plus a dev-only frontend tracer for the dialogs and heatmap.
Notes¶
- Backward-compatible:
distribution_modedefaults tonone(today's per-device heatmap); nodcimwrites, no design dirty flag. All planning data is stored by the plugin alongside the design and never written back to real records.
[0.12.0] - 2026-07-17¶
Release Summary¶
Live power for new gear, and a hardened editor — a minor, fully backward-compatible release. A freshly dropped catalog device now shows its projected draw immediately (per-rack bar + heatmap, no save/reload), via a new read-only power endpoint the palette reads. Alongside it, a batch of editor placement fixes found through real-mouse testing: cross-rack homecoming and reject paths no longer corrupt device identity or fling tiles to the wrong rack, a device dropped onto a just-removed slot now correctly shows its name and heat, and the power heatmap no longer colors a device that is leaving. No schema changes, no migrations.
Added¶
- Palette-add-live power: a freshly dragged catalog device now shows its projected draw immediately in the per-rack power bar and heatmap — no save/reload needed. The catalog palette (search results and the favorites/quick-access list) fetches each device type's projected draw from a new read-only endpoint,
GET /api/plugins/rack-design/device-type-power/?id=…, and stamps it on the row, so the drop carries the samedata-draw-w/data-powera real device tile does. Draw is resolved by the same logic the projection uses for a planned add (device_type_power_summary), so the live figure matches what Save + reload produces. Endpoint is authenticated, read-only, and performs no writes.
Fixed¶
- Cross-rack homecoming no longer corrupts a device: dragging a moved device back toward its home rack used to leave it half-reverted (existing in the DOM but still a "ghost" in state), so the next cross-rack drag mis-indexed another rack's state array and duplicated/corrupted an unrelated device. A homecoming now fully revives the device as a proper existing entry (kind + placement), so re-drags are adopted cleanly.
- Illegal drops return to the last valid position: a rejected cross-rack drop returns the device to the rack/slot it was dragged from, not its true origin; a re-dragged reloaded move keeps its move identity instead of reverting the whole move; and a within-rack illegal move of a just-reclaimed tile stays in that rack instead of flying to its home rack.
- Homecoming onto an occupied slot is rejected (validated against the actual drop rows, not the free origin slot), so it can no longer commit an overlap.
- Name restored on homecoming: returning a device to its own origin shows its real name again instead of keeping the move's
"<design>-<name>"overlay. - Heatmap on a leaving device: a tile flagged for removal or displaced (being replaced) no longer keeps a stale heatmap fill — a device that is leaving is not colored.
- Drop onto a removed slot: the device that takes a removed/displaced device's slot now renders above it, so its heat fill and name are shown (previously the collapsed tile stacked on top and blanked the new occupant's name).
Changed¶
- Added a dev-only, opt-in editor drag-lifecycle tracer (
window.__rdDragTrace), gated behindDEBUG/ Django Debug Toolbar so it is never reachable on a production build.
[0.11.1] - 2026-07-14¶
Release Summary¶
Power that moves with your plan — a patch release that makes the power projection live and fixes a name-revert bug.
The per-rack power bar and the heatmap now recompute in the browser as you shuffle hardware — moving a device between racks drops the source rack's projected draw and raises the destination's instantly (and re-colors ok/warn/critical), flagging a device for removal drops it, all with no save/reload. Draw travels with each tile, so cross-rack moves are reflected on both racks at once. (A freshly-dropped catalog add still shows its draw after save/reload — the palette has no client-side power data yet.)
Fixed¶
- Cancelling a move (× on a
move_intile) now reverts the tile's name as well as its position — previously the device snapped back but kept showing the move's"<design>-<name>"proposed name instead of its real name. - "Power heatmap" toolbar toggle: restored the spacing between its checkbox and label (it sits outside the state legend and missed the legend's gap).
Changed¶
- Power projection is now recomputed live client-side (per-rack bar + heatmap) on every in-editor add / move / removal, driven by per-tile
data-draw-wand a MutationObserver — no server round-trip. Still strictly read-only.
[0.11.0] - 2026-07-13¶
Release Summary¶
Power projection — a minor, fully backward-compatible release that shows how much power a planned design will draw, per rack, before it is applied to DCIM.
The editor and the read-only elevation now compute a projected power draw for the planned world (existing − removes + adds, moves reassigned) and compare it to a capacity, reusing NetBox's own power model (PowerPort / PowerPortTemplate draw, PowerFeed.available_power). Each rack gets an always-visible power bar — projected draw / capacity / utilization %, colored ok/warn/critical — and a "Power heatmap" toggle that turns every device tile into a per-device consumption "health bar" (the rack's biggest consumer fills red, the rest proportionally toward green). Hovering a power bar lists the count of devices whose power ports aren't cabled and pulls those tiles out of the rack to point them out; hovering any device shows its PSUs and allocated power on the info card. PDUs are treated as power infrastructure (they distribute, not consume) and excluded from the total; passive gear with no power ports is skipped, not flagged. No schema changes, no breaking changes, no migrations.
Added¶
- Per-rack power bar (
docs/power-projection-spec.md): projected draw vs. capacity with ok/warn/critical thresholds, on both the editor and the read-only elevation. Capacity comes from the rack'sPowerFeeds when modeled, else a configurable fallback. - Power heatmap toggle: per-device fill bars, max-normalized per rack (biggest consumer = 100% red → green), suppressing the normal state tints while active and restoring them exactly when off.
- Connection-gap flag: the bar shows a ⚠ count of devices whose power ports are not cabled; hovering the bar highlights ("pulls out") those tiles.
- PSU detail on the device hover card: one row per power port (name + allocated draw), a total, and an
(nc)marker for uncabled ports. - Naming: a stock-runnable example naming module and graceful fallback (from 0.10.0) remain; power adds a new read-only projection layer only.
- Config keys:
power_capacity_default_w,power_draw_basis(allocated/maximum),power_warn_pct,power_critical_pct,power_exclude_roles. - Backend tests for draw resolution / capacity / thresholds / PDU exclusion / passive-skip / connection flag, and a deterministic Playwright e2e suite for the bar, heatmap, pull-out and PSU hover.
Changed¶
- Power is strictly read-only: computed server-side in
projection.pyand surfaced on the projection bundle; it never writes todcimand never marks the design dirty.
[0.10.0] - 2026-07-10¶
Release Summary¶
Naming you can trust and placement you can see — a minor, fully backward-compatible release that hardens the naming engine and makes palette placement obvious.
The naming engine gains a shipped, stock-runnable example and, crucially, graceful degradation: a mis-configured or unreachable naming_script (wrong dotted path, module not loaded yet, or a script that raises) no longer errors — it falls back to the default sequence name and logs a warning, so a bad config can never block planning. On the editor side, dragging a device from the palette now lands deterministically on the exact rack unit under the cursor (no more half-unit / off-by-one drops that depended on where you grabbed the palette row), the dragged tile turns translucent, and a green landing-preview band shows precisely where it will drop (red when the slot is illegal). Full-depth devices now read consistently: the front tile shows the planned name while the rear shadow always shows the device's hardware identity. No schema changes, no breaking changes, no migrations.
Added¶
- Shipped naming example (
netbox_rack_design.naming_example): a small, stock-runnablebuild_name(placement)demonstrating a family counter and A/B phase-paired PDU slots (a1, b1, a2, b2, …). Uses absolute imports so it keeps working verbatim when copied into NetBox'sSCRIPTS_ROOT. Enable via"naming_script": "netbox_rack_design.naming_example.build_name". - Drag landing preview: while dragging a palette add or moving a tile, the tile turns translucent and a green band marks the exact rows it will occupy on release (red deny band over illegal rows).
- Device naming docs (
docs/device-naming.md): all three modes, a fully-commented example, a step-by-step "verify on your instance" walkthrough, and twoSCRIPTS_ROOTintegration variants (UI Add vs. file copy).
Fixed¶
- Deterministic palette placement: a whole-U palette add now snaps to the rack unit under the cursor regardless of where the palette row was grabbed — fixes intermittent off-by-one/half-unit landings (e.g. "dropped on U23, landed on U22").
- Consistent full-depth rear face: the rear shadow always shows the device's stable identity/type, never the mutable planned-name overlay, so every rear hatch reads uniformly (previously an add whose name preview hadn't returned yet leaked the name onto some rear hatches and not others).
- Robust script-mode naming: an unresolvable or raising
naming_scriptfalls back to the default sequence name (with a logged warning) instead of failing the name-preview request.
Changed¶
- Preview-name requests carry the session's already-assigned sibling names (
pending_names) so same-session palette adds get consecutive family numbers instead of colliding.
[0.9.1] - 2026-07-09¶
Release Summary¶
Displacement you can actually see — a patch release polishing how displaced devices are marked, everywhere.
The displaced-occupant marker is now a NetBox-reservation-style bar hanging outside the rack frame (wider, red diagonal stripes, aligned to the displaced units, on both faces for full-depth devices) instead of a thin sliver squeezed inside the occupying tile next to its remove button — and hovering it shows the standard device hover card with the displaced device's name, type and role. Displaced-pair detection moved into the projection layer, which fixed a real rendering hole on two surfaces: the read-only elevation view showed a saved displacement as two overlapping composited tiles, and the editor itself had the same overlap on a fresh page load (it only rendered correctly during the interactive session that created the displacement). No schema changes, no breaking changes.
Fixed¶
- Read-only elevation view: saved displacements render as one full tile + the outside stripe bar instead of two overlapping tiles.
- Editor on load: saved displacements now collapse and get their stripe bars immediately (
applySavedDisplacements), routed through the same ownership records as live gestures so later restores behave identically. - Reloaded catalog adds regained their full-depth flag in the widget payload — the rear mirror bar was silently skipped on load.
- Hovering the stripe bar reliably shows the displaced device's info (hover card on both the editor and elevation pages).
Changed¶
- Stripe bar geometry: outside the rack frame, 10px wide, percentage-tracked to the displaced rows; legend filters apply to it like to the tile it stands for.
[0.9.0] - 2026-07-09¶
Release Summary¶
The tray becomes real — non-racked devices (0U/vertical PDUs, rear-door units, cable managers) are now first-class citizens of the editor.
This minor, fully backward-compatible release makes each rack's "Non-racked tray" show reality, not just plans: real DCIM devices mounted in a rack without a U position now render in the tray as existing tiles and are plannable exactly like racked devices — drag them into rack units (plan a mount), out of units into the tray (plan a 0U dismount), or into another rack's tray (reassociate), with the same validation, rename dialogs, origin ghosts and silent identity-based homecoming the editor applies everywhere else. The tray behaves as a compact append-only list: items never overlap, drops append, rows renumber after removals and the container grows and shrinks with content. There are no schema changes (the placement fields were already nullable — only validation logic was relaxed) and no breaking changes.
Added¶
- Real non-racked devices in the tray: devices with a rack but no position project as
existingtray slots (face is meaningless off-rack and normalizes to none); racks without such devices show an empty tray, exactly as before. - Full tray move semantics (spec
docs/editor-behavior-spec.md§9): units↔tray mount/dismount, cross-rack tray→tray reassociation, palette adds into the tray, and identity-based homecoming — returning a device onto its own tray ghost (any hop count) silently restores the original placement. Origin trays keep a properly-styled ghost entry while the hardware hasn't moved yet. - Compact list layout: tray items each get their own row, drops append below the bottom-most tile, rows renumber to contiguous after any removal, and existing items are never shuffled by a new drop.
- New I4 model invariant: a device exists exactly once across the whole editor world (racked body, tray body, or ghost pair) — checked on every sweep step alongside I1/I2.
- Save contracts for position-less placements: mount (position gained), dismount (rack kept, position cleared), tray→tray reassociation (rack changed, no position) — each covered by API tests, and untouched real tray devices round-trip as no-ops.
- Editor e2e suite
tests/e2e/test_editor_tray.py(11 deterministic tests) plus projection/model/API test coverage (backend suite grows to 311 tests).
Fixed¶
- Regression coverage hardening (post-0.8.0): rename-dialog Cancel/× full-revert guards, dedicated cross-rack displacement-on-adoption tests, and full-world diff assertions extended to the dense-pack/hatch-overlap/shadow-ownership suites.
- A cancelled tray-origin move no longer strands the tile on a face grid — cancel/ghost/restore paths resolve tray origins correctly.
- Tray drops no longer land on top of existing tiles, and departures no longer leave dead empty rows behind.
- The tray payload no longer registers spurious move placements for untouched real tray devices on save.
[0.8.0] - 2026-07-08¶
Release Summary¶
The editor grows up — a ground-up rework of how the multi-rack editor decides, renders and verifies device placement, plus a configurable naming engine for planned devices.
This minor, fully backward-compatible release replaces the editor's "let the grid engine move things and clean up after" approach with an explicit object model and a validate-before-commit pipeline, specified in docs/editor-behavior-spec.md and enforced by a spec-conformance test matrix (docs/editor-conformance-matrix.md). The result is an editor that behaves predictably under every gesture we could enumerate: nothing ever moves except the tile in your hand, full-depth devices carry their opposite-face shadow with them (live, while you drag), placing onto a vacating slot is an explicit confirmed action with a NetBox-style reservation marker, and a device dragged back home — across any number of racks, even after saving and reloading — silently becomes itself again. There are no schema changes (no new migrations) and no breaking changes; live Device/Rack records remain untouched, as always.
Added¶
- Naming-convention engine for planned devices (
naming.py): proposed names are computed per placement with three configurable modes —sequence(<design>-<n>),template(astr.formattemplate over dotted NetBox-model attribute paths, e.g.{design.name},{device.site.name}), andscript(a dotted path to a custom callable). Configured via plugin settingsnaming_mode/naming_template/naming_script; the proposed name flows through the editor's rename dialog, the save path and the REST payload. - Editor behavior specification (
docs/editor-behavior-spec.md) — the authoritative rules for placement, shadows, ghosts, displacement and dialogs — and a spec-conformance matrix (docs/editor-conformance-matrix.md, ~54 rule×context rows) mapping every rule to its covering test. - Displacement flow: dropping a device onto a slot whose occupant is vacating (moved away or flagged removed) asks for confirmation, then collapses the vacating occupant to a red reservation side stripe (NetBox-reservation look, old name on hover, mirrored on the opposite face for full-depth devices); the stripe reverts to the normal ghost/removed rendering if the new occupant leaves. Exactly one device can be planned into a vacated slot.
- Live full-depth shadows: a full-depth device's opposite-face hatch is now part of the device — it follows the tile in real time during a drag (a live rear-side legality preview), lands atomically with it, tints by the owner's state (existing / add / move-in / removed-crossed), and renders a visible red conflict hatch when a pre-existing layout double-books the opposite face instead of silently disappearing.
- Cursor-governed placement: the drag preview follows the cursor only — no "suggested placement" fallback. Illegal rows show a red deny indicator; releasing there snaps a moved tile back home and discards a palette drag-in entirely (no phantom add, Save stays untouched).
- Cross-rack homecoming: dragging a moved device back onto its own origin ghost — directly, after multiple hops, or after save + reload — silently restores the original placement (ghost and mirror removed, no duplicate entities).
- Deterministic e2e regression net: self-provisioning Playwright suites sweeping devices in 0.5U steps across both faces and multiple racks (76/87/120-step sweeps, dense-pack rejection, displacement dialogs, homecoming chains), each step asserting a full-world diff — any bystander tile changing any class or position anywhere fails the test.
Fixed¶
- Dense-rack drag crash:
RangeError: Maximum call stack size exceededfrom GridStack's collision cascade when dragging onto a fully packed rack — placement is now decided before commit and engine push/repack is neutralized during gestures, so the cascade cannot start. - Bystander tiles being silently relocated during drags, hatch redraws, editor init on double-booked layouts, and rejected foreign drops (several distinct engine paths, including two that bypassed all collision hooks).
- Full-depth shadows: orphaned after rejected drags or cross-rack departures, missing for palette adds and removed devices, stale move-tint after a rejected foreign drop, wrong-name ghost mirrors after grouped moves.
- Rename/displacement dialog never appearing for cross-rack moves (an early return skipped adopted tiles on every path), and dialogs getting stuck open forever when confirmed/dismissed during the opening animation (Bootstrap
hide()no-ops mid-fade). - A device released while hovering occupied units no longer teleports to the last-valid preview slot; re-dragged palette adds are validated like every other move.
Changed¶
- The editor's shadow/ghost rendering pipeline is now event-driven from each device's own lifecycle (the global recompute pass is gone), and every placement decision routes through a single model-based validator (
rdCanPlaceAt) covering bodies, shadows, ghosts and both faces of full-depth devices. - Dev tooling:
pre-commitpinned to 4.6.0 inrequirements_dev.txt.
[0.7.0] - 2026-06-29¶
Release Summary¶
One design, every rack it touches — Rack Design grows from a single-rack editor into a multi-rack workspace.
0.6.0 made a single rack's elevation read like the real thing; 0.7.0 lets a design span multiple racks
and reworks the editor around that. This minor, fully backward-compatible release adds an explicit
rack scope to each design (a racks set, validated to the design's site), renders all of a design's
racks side by side in one editor with a single design-level Save, and adds a read-only elevation view
that shows the whole projected design — every rack, both faces — without an editing surface. It also ships
a substantial editor-UX round: a redesigned, collapsible tool drawer with three independent
Device / Favorites / Racks panels, a compact one-line role + tenant toolbar merged with the state legend,
and an empty-state editor that walks you through adding your first rack. As always, the editor only
composes design placements; your live Device and Rack records are never modified.
This release adds three additive, backward-compatible migrations: 0004 adds the Design.racks
relationship, 0005 seeds it from the racks each existing design already touches, and 0006 adds a
per-user rack-visibility table. There are no breaking changes — existing designs, placements, the REST
API, and GraphQL all continue to work, and the migrations reverse cleanly.
Multi-rack designs¶
A design now carries an explicit rack scope — a set of racks (Design.racks) the design plans against,
validated to live in the design's own site. Migration 0005 backfills this from the distinct target racks
of each design's existing placements, so every current design keeps exactly the racks it already touches.
New REST endpoints manage the scope: designs/<pk>/add-rack/ (enforces same-site) and
designs/<pk>/remove-rack/. Removing a rack is destructive and confirmed: it returns 409 with the
list of placements that would be affected unless called with confirm: true, at which point it deletes the
placements targeting that rack and detaches it in a single transaction.
Multi-rack editor workspace¶
The editor is now a design-level workspace that renders every visible scoped rack side by side
instead of one rack at a time, with a single Save that persists the whole design's layout at once. The old
single-rack tab switcher is gone. Each rack renders through a shared inc/rack_block.html partial (the same
markup the read-only view uses), and a per-user HiddenDesignRack store lets you hide racks you aren't
working on without changing the design — with hidden-design-racks/ list, toggle/, and show-all/
endpoints backing it. Opening a design with no racks yet shows an empty state with an "Add your first
rack" entry point instead of an error.
Read-only elevation view¶
A new read-only elevation view at designs/<pk>/elevation/ renders the entire projected design — all
scoped racks, both faces, with full-depth devices hatched on their opposite face and the device hover card —
with no editing controls. It shares the exact projection the editor uses, so the two always agree. The old
per-rack elevation URL now redirects to this view.
Editor UX overhaul¶
- Tool drawer. The editor's side tools are now a collapsible push-sidebar split into three independent
toggles — Device, Favorites, and Racks — that can be opened in any combination and stack side by side as
columns; the open/closed state of each is persisted in
localStorage. - Role + tenant toolbar. The planned-add device role and tenant selectors now live in a compact, always-visible one-line toolbar merged with the state legend, removing the previous duplicate hint.
- Layout polish. Drawer columns flex-stretch to match the rendered rack heights, so the catalog scrolls internally instead of overshooting, at both tall and short viewports.
Added¶
- Multi-rack designs. A new
Design.racksmany-to-many rack scope (validated to the design's site), with migration0004(schema) and0005(seed each design's scope from its placements' target racks). The scope is editable through the design form (a site-filtered rack selector) and exposed on the REST API serializer. - Rack-scope REST actions:
designs/<pk>/add-rack/(same-site enforced) anddesigns/<pk>/remove-rack/(destructive/confirmed — returns409+ the affected placements unlessconfirm: true, then deletes the rack's placements and detaches it in one transaction). - Multi-rack editor workspace rendering all visible scoped racks side by side via a shared
inc/rack_block.htmlpartial, with one design-level Save (design_editor.html,editor.js,editor_panels.js,editor.css). - Read-only elevation view at
designs/<pk>/elevation/showing the whole projected design (all racks, both faces, full-depth hatch, hover card) with no editing controls (design_elevation.html). - Per-user rack visibility. A
HiddenDesignRackmodel (migration0006) lets each user hide scoped racks from their own editor view, withhidden-design-racks/list,toggle/, andshow-all/endpoints. Hiding a rack is per-user and never alters the design. - Editor tool drawer with three independent Device / Favorites / Racks toggles (any combination, stacked
as columns), persisted in
localStorage, and an empty-state with an "Add your first rack" entry point.
Changed¶
- The editor now renders an entire design's racks side by side in one workspace with a single Save, replacing the previous single-rack-at-a-time tab switcher.
- The planned-add device role and tenant selectors moved into a compact always-visible one-line toolbar merged with the state legend.
- The per-rack elevation URL now redirects to the new design-level read-only elevation view.
Fixed¶
- N/A
Deprecated¶
- N/A
Removed¶
- The single-rack elevation grid partial (
inc/elevation_grid.html) and the editor's single-rack tab switcher, superseded by the sharedinc/rack_block.htmland the multi-rack workspace.
Security¶
- N/A
Upgrade¶
pip install -U netbox-rack-design and restart NetBox.
- Run
python manage.py migrate— this release adds migrations0004_design_racks(theDesign.racksrelationship),0005_seed_design_racks(seeds each existing design's rack scope from its placements' target racks), and0006_hiddendesignrack(a per-user rack-visibility table). All three are additive (no changes to existing columns, no destructive data rewrite), safe against existing designs, and reverse cleanly. There are no breaking changes. - Run
python manage.py collectstatic— the editor's bundled JS/CSS were reworked for the multi-rack workspace, the tool drawer, and the new shared rack partial (neweditor_panels.js), so the updated static assets must be collected for the editor to render correctly. - No configuration changes are needed — existing
PLUGINS_CONFIGsettings continue to work unchanged.
[0.6.0] - 2026-06-27¶
Release Summary¶
See both faces at once — Rack Design's elevation editor now renders front and rear side by side.
0.5.0 made the device-type catalog personal; 0.6.0 makes the rack elevation read like the real thing. This minor, fully backward-compatible release replaces the single-face view with independent Front and Rear toggles: both faces render at the same time, so network gear on the front and servers on the rear are visible together. The toggles are independent on/off switches (you can show either or both) and the editor never lets you hide both faces at once. Hovering any tile now pops a device hover card showing the device's name, role, and tenant — gracefully omitting whichever fields are empty. Finally, full-depth devices are now honored across both faces, matching NetBox core's own rack-elevation rendering.
There are no breaking changes and no database migration in this release — the changes are limited to the projection layer, the editor template/JS/CSS, the save-layout payload, and two new template filters.
Independent Front/Rear face toggles¶
The elevation editor now exposes Front and Rear as independent on/off toggles and renders both faces side by side, so you can plan the front and rear of a rack together instead of flipping between them. Either face can be hidden on its own, but the editor always keeps at least one face visible — it will not allow both to be turned off.
Device hover card¶
Hovering a placed tile now shows a hover card with the device's name, role, and tenant.
When a field isn't set (no role, or no tenant), it is simply omitted from the card rather than
shown blank. Two new template filters, slot_role_name and slot_tenant_name, resolve the
display values.
Full-depth devices across both faces¶
Full-depth devices are now correctly shown on both faces, matching NetBox core's rack-elevation
behaviour. A full-depth device renders its normal coloured state on the face it is mounted on,
while the opposite face shows core's "blocked" diagonal hatch (the same #f7f7f7/#ffc0c0
45° stripe pattern core uses) labelled with the device name. The opposite-face rendering is
passive: it is not draggable, carries no ×/star controls, and is excluded from the save
payload, so it can never create a duplicate DesignPlacement. This applies to existing devices
and across every design kind (add, move-in, move-out ghost, and remove), driven by a new
opposite_face flag on the projection slot contract and the editor payload.
Added¶
- Independent Front/Rear face toggles in the elevation editor: both faces render side by side
and each can be toggled on/off independently, with a guard that never allows both faces to be
hidden (
design_editor.html,editor.js,editor.css). - Device hover card on placed tiles showing the device name, role, and tenant, omitting
empty fields, backed by two new template filters
slot_role_nameandslot_tenant_name(templatetags/rack_design.py). - New
opposite_faceflag on the projection slot contract and the editor widget payload so full-depth devices can be represented on the face they do not occupy. - Tests covering full-depth opposite-face projection and the new behaviour
(
tests/test_fulldepth.py).
Fixed¶
- Full-depth devices now correctly occupy both faces, matching NetBox core's rack-elevation
rendering. The mounted face shows the normal coloured state; the opposite face shows core's
"blocked" diagonal hatch (
#f7f7f7/#ffc0c0, 45°) with the device name. The opposite-face tile is passive — not draggable, no×/star controls — and is excluded from the save payload, so it never creates a duplicateDesignPlacement. Applies to existing devices and to all design kinds (add / move-in / move-out ghost / remove) (projection.py,views.py,editor.js,editor.css).
Changed¶
- The elevation editor now renders both rack faces simultaneously instead of a single face at a time (see the independent toggles above).
Deprecated¶
- N/A
Removed¶
- N/A
Security¶
- N/A
Upgrade¶
pip install -U netbox-rack-design and restart NetBox.
- No database migration is required this release —
python manage.py migrateis a no-op for this plugin (the changes are limited to the projection layer, editor template/JS/CSS, the save-layout payload, and two template filters; no models changed). - Run
python manage.py collectstatic— the editor's bundled JS/CSS were updated for the independent face toggles, the device hover card, and the full-depth opposite-face rendering, so the new static assets must be collected for the editor to render correctly. - No configuration changes are needed — existing
PLUGINS_CONFIGsettings continue to work unchanged.
[0.5.0] - 2026-06-26¶
Release Summary¶
Pin the gear you reach for — Rack Design gets per-user favorite device types.
0.4.0 gave the single-rack editor a searchable device-type catalog to drag new gear from; 0.5.0 makes that catalog personal. This minor, fully backward-compatible release lets each user star a device type to pin it to a dedicated Quick access column in the editor, so the handful of types you plan with most are always one click — and one drag — away, without scrolling or re-filtering the full catalog. Favorites are per-user: starring a type affects only your own Quick access column, never anyone else's, and they are persisted through a new user-scoped favorites REST API. The Quick access column is independent of the catalog's search/manufacturer filter, so narrowing the catalog never hides your pinned types — your favorites stay put while you search.
As with every editor surface, this is purely a planning convenience: drag-to-plan from Quick
access composes design placements exactly like the main catalog, and your live Device
records are never touched.
There are no breaking changes. This release adds a single database migration that only
adds one new table (FavoriteDeviceType); it introduces no changes to existing models,
placements, the public REST API, or GraphQL, and reverses cleanly.
Per-user favorite device types¶
Each row in the device-type catalog now carries a star toggle. Starring a type adds it to
your Quick access column at the top of the palette; un-starring removes it. Favorites are
scoped to the logged-in user via a FavoriteDeviceType model (a unique (user, device_type)
pair), so two users planning the same rack see their own independent Quick access lists.
Quick access column in the editor¶
The Quick access column lists your starred device types as ready-to-drag tiles, drag one straight onto a free rack unit to plan an add — the same planning surface the main catalog uses. The column is rendered independently of the catalog's type-ahead search and manufacturer filter, so filtering the catalog to find something never empties or reorders your pinned favorites.
User-scoped favorites API¶
A new favorites endpoint backs the stars: a GET returns the current user's favorite device
types, and a POST toggles a device type in or out of that set (FavoriteToggleSerializer,
wired through api/views.py and the API router). All reads and writes are scoped to the
requesting user, so one user can never see or modify another's favorites.
Added¶
- Per-user favorite device types. A new
FavoriteDeviceTypemodel (per-user(user, device_type)with a uniqueness constraint) lets each user pin the device types they plan with most. - "Quick access" favorites column in the single-rack editor: starred device types render
as drag-to-plan tiles in a dedicated column that is independent of the catalog's
search/manufacturer filter (
design_editor.html,editor.js,editor.css). - Star toggles on catalog rows to add/remove a device type from your favorites.
- User-scoped favorites REST API: a GET list of the current user's favorites and a POST
toggle action, both scoped to the requesting user (
FavoriteToggleSerializerinapi/serializers.py, the view inapi/views.py, router entry inapi/urls.py). - API and view tests covering the favorites endpoint (list + toggle, user scoping) and the editor's Quick access wiring.
Changed¶
- N/A — no changes to existing behavior, data model, or public API.
Fixed¶
- N/A
Deprecated¶
- N/A
Removed¶
- N/A
Security¶
- N/A
Upgrade¶
pip install -U netbox-rack-design and restart NetBox.
- Run
python manage.py migrate— this release adds migration0003_favoritedevicetype, which only adds one new table (FavoriteDeviceType). It is additive (no changes to existing tables, no data rewrite, no backfill), safe against existing designs, and reversible. There are no breaking changes. - Run
python manage.py collectstatic— the editor's bundled JS/CSS were updated for the Quick access favorites column and star toggles, so the new static assets must be collected for the editor to render correctly. - No configuration changes are needed — existing
PLUGINS_CONFIGsettings continue to work unchanged.
[0.4.0] - 2026-06-26¶
Release Summary¶
Plan new gear, not just rearrange it — Rack Design gets a device-type catalog.
0.3.0 let you drag the rack's existing devices around; 0.4.0 lets you plan brand-new
ones. This minor, fully backward-compatible release adds an interactive device-type
catalog palette to the single-rack editor: search the catalog, narrow it by
manufacturer, and drag a device type straight onto a free rack unit to plan a new
add — no leaving the editor to pre-create anything. Each planned add can now also carry
an intended device role and tenant, chosen through NetBox's own dynamic selects,
so the plan records what you intend to rack and whose it is. As always the editor
only composes design placements; your live Device records are never touched until
a design is later executed.
This release also fixes a visual glitch where multi-unit state tiles rendered semi-transparent (letting the grid bleed through), adds cache-busting to the bundled editor JS/CSS so browsers reliably pick up new assets after an upgrade, and ships a new committed headless end-to-end regression suite for the editor's client-side behaviour.
There are no breaking changes. This release adds a database migration that only
adds two nullable fields (device_role, tenant) to DesignPlacement, so it is
safe to apply against existing data and reverses cleanly.
Device-type catalog palette¶
A new palette in the editor lists the device types you can plan. Type-ahead search filters by name and a manufacturer filter narrows the catalog to a single vendor. Drag a device type onto an empty unit and the editor plans an add placement at that position — the same planning surface that already handled moves and removals, now able to introduce devices that don't yet exist in the rack.
Role and tenant on planned adds¶
A planned add can now record an intended device role and tenant, selected via
NetBox dynamic (API-backed) selects in the editor. The two new DesignPlacement
fields are add-only: clean() rejects setting a role or tenant on a move or
remove placement, keeping the data model honest. Both fields are optional and surface
through the REST API and GraphQL.
Editor polish & regression coverage¶
- Opaque multi-U tiles. Tiles spanning more than one rack unit were rendering semi-transparent, letting the grid lines show through and muddying the colour coding; they are now fully opaque so add/move/remove states read clearly.
- Asset cache-busting. The bundled editor
*.js/*.cssare now requested with a version query string (?v=) derived from the asset set, so an upgraded plugin's new editor assets are loaded instead of a stale cached copy. - Headless e2e regression suite. A new committed Playwright suite under
tests/e2e/exercises the editor's real client-side DOM/GridStack behaviour (catalog drag-in, the context-sensitive×, the payloadbuildRackPayloadwould emit). It is strictly read-only against the dev database and skips cleanly when Playwright/Chrome or a dev server isn't present, so it stays out of the normal headless suite while remaining runnable on demand.
Added¶
- Device-type catalog palette in the single-rack editor: type-ahead search + a
manufacturer filter, and drag-and-drop a device type onto a free unit to plan a new
add (
editor.js,design_editor.html,editor.css). device_roleandtenanton planned adds. Two new optional, nullable FK fields onDesignPlacement(todcim.DeviceRole/tenancy.Tenant), selected via NetBox dynamic selects, add-only and validated as such inclean(), and exposed through the form, the REST API serializer, and GraphQL.- Committed headless e2e regression suite (
tests/e2e/test_editor_e2e.py) for the editor's client-side behaviour — read-only and self-skipping when prerequisites are absent. - Expanded API and view tests covering the new fields and the catalog/editor wiring.
Fixed¶
- Multi-unit state tiles in the editor were semi-transparent and let the grid bleed through; they now render fully opaque so the add/move/remove colour coding is legible.
Changed¶
- Bundled editor JavaScript/CSS are now loaded with an
asset_versioncache-busting query string so upgraded assets aren't served stale from the browser cache.
Deprecated¶
- N/A
Removed¶
- N/A
Security¶
- N/A
Upgrade¶
pip install -U netbox-rack-design and restart NetBox.
- Run
python manage.py migrate— this release adds migration0002_designplacement_device_role_designplacement_tenant, which only adds two nullable fields toDesignPlacement. It is additive (no data rewrite, no backfill), safe against existing designs, and reversible. There are no breaking changes. - Run
python manage.py collectstatic— the editor's bundled JS/CSS were updated for the catalog palette and role/tenant selects (and now carry cache-busting), so the new static assets must be collected for the editor to render correctly. - No configuration changes are needed — existing
PLUGINS_CONFIGsettings continue to work unchanged.
[0.3.0] - 2026-06-26¶
Release Summary¶
The rack comes to life — Rack Design becomes interactive.
0.2.0 let you see a projected rack elevation; 0.3.0 lets you change it. This minor, fully backward-compatible release adds a drag-and-drop single-rack layout editor so you can plan a rack visually — move a device to a new unit, mark one for removal, or back out an add — and save the result as design placements, all without ever touching your live NetBox devices. It also adds a standalone, filterable Elevations list so you can jump straight to any rack's projected layout, and surfaces designs on the core rack pages.
Nothing about existing designs, placements, the REST API, GraphQL, or configuration changes in an incompatible way — every prior URL, field, and endpoint behaves as before. There are no breaking changes and no database migrations in this release.
Interactive single-rack layout editor¶
Open a design's rack and drag devices around a live GridStack elevation. The editor understands the three placement kinds and gives you a single, context-sensitive × control whose meaning depends on what you're looking at:
- on an existing device → flags it for removal;
- on a device you've moved → cancels the move, snapping it back to its original unit;
- on a device you just added → cancels the add, removing it from the plan.
While you drag, a live move-visualization ghost previews the target position so you
can see exactly where a device will land before you let go. The editor is purely a
planning surface: it composes and edits design placements and never mutates the real
Device records underneath.
Save-layout API¶
A new save-layout REST action persists what you drew. Rather than blindly rewriting the
design, it diffs the submitted layout against the current DesignPlacement rows and
applies only the differences. Every created or changed placement is run through full
clean()/full_clean validation (e.g. unit availability) before it is saved, so an
invalid layout is rejected rather than half-written. The action is idempotent —
saving the same layout again is a no-op and round-trips cleanly — and deletes
conservatively, so re-saving never silently drops placement data you didn't intend to
remove. As with the editor, real Devices are never modified; only design placements
are written.
Standalone Elevations list¶
A new top-level, filterable Elevations list view lets you browse projected rack elevations directly, without first drilling into a specific design — filter down to the rack or design you care about and open its projected layout in one step.
Rack-page integration¶
The core rack detail pages now carry a rack-designs panel listing the designs that touch the rack, each linking through to its editor/elevation, and a new navigation entry exposes the Elevations list in the plugin menu.
Added¶
- Interactive single-rack layout editor (
design_editor.html+ bundledstatic/netbox_rack_design/js/editor.jsandcss/editor.css) built on GridStack: drag-and-drop move, mark-for-removal, and cancel-add, driven by one context-sensitive×control (existing → flag removal, moved → cancel move, added → cancel add), with a live move-visualization ghost during drags. Edits design placements only; real Devices are never touched. - Save-layout REST action that diffs the submitted layout against current
DesignPlacementrows, validates every change withfull_clean, is idempotent across round-trips, and deletes conservatively so no layout data is lost. - Standalone, filterable Elevations list view (
elevation_browser.html) for browsing projected rack elevations directly, plus a sharedinc/elevation_grid.htmlpartial andlegend_filter.jsfor legend-driven filtering. - Rack-designs panel on the core
dcim.rackdetail page listing designs that touch the rack, each linking to its editor/elevation. - Navigation entry surfacing the Elevations list in the plugin menu.
- Accompanying API and view tests covering the save-layout action, the Elevations list, and the editor wiring.
Changed¶
- N/A — no changes to existing behavior, data model, or public API.
Fixed¶
- N/A
Deprecated¶
- N/A
Removed¶
- N/A
Security¶
- N/A
Upgrade¶
pip install -U netbox-rack-design and restart NetBox.
- Run
python manage.py collectstatic— this release ships new bundled static assets (the editor's JavaScript/CSS and the legend filter) that must be collected so the editor renders correctly. - No database migrations are required (
python manage.py migrateis a no-op for this plugin) and no configuration changes are needed — existingPLUGINS_CONFIGsettings continue to work unchanged.
[0.2.0] - 2026-06-25¶
Release Summary¶
Projected rack elevations — the first visual surface of NetBox Rack Design.
Until now a design was only a list of placement records; you couldn't actually see the rack it described. This minor, backward-compatible release renders any design as a full rack elevation showing how the rack would look once the design is applied: planned adds, moves, and removals overlaid on the rack's real devices — computed entirely in memory, with zero changes to your live NetBox data (nothing is materialized until an explicit Apply, which arrives in a later release).
The elevation is drawn with a bundled GridStack layout (front/rear faces plus a non-racked tray) and uses clear visual encoding so the plan reads at a glance:
- green — a device the design adds;
- cyan — a device moved in, with a faded ghost left at the slot it vacates;
- red, struck-through — a device marked for removal;
- neutral (the device role color) — existing devices the design leaves untouched.
Open it from a design at /plugins/rack-design/designs/<id>/racks/<rack_id>/, or
straight from the core Rack detail page via an optional panel that lists every
design touching that rack. This is the read-only foundation for the interactive
drag-and-drop editor coming in a future release.
This release also fixes two bugs, declares the project's Apache-2.0 license in the package metadata, and ships the project icon.
Upgrade: pip install -U netbox-rack-design and restart NetBox. No database
migrations and no configuration changes are required. The rack-page panel is enabled
by default; set enable_rack_panel = False in PLUGINS_CONFIG to hide it.
Added¶
- Projected rack elevation (read-only). A new
DesignElevationViewrenders, for a given design and rack, the front/rear/non-racked layout the design would produce — existing devices in place, plus the design's add/move/remove placements overlaid at their target units with state-based colour coding and a legend. The projection is computed byprojection.project_rack()purely in memory; real devices are never modified. Reachable at/plugins/rack-design/designs/<pk>/racks/<rack_id>/. - Rack-page panel. An optional
PluginTemplateExtensionon the coredcim.rackdetail page lists the designs whose placements touch that rack, each linking to its projected elevation. Gated by the newenable_rack_panelconfig setting (defaultTrue); renders nothing when no design touches the rack. - Bundled GridStack assets and plugin CSS/JS under
static/netbox_rack_design/(no external CDN), plus arack_designtemplate-tag library powering the elevation template.
Fixed¶
- Placement kind badges (Add/Move/Remove) now render in their intended colours
instead of grey —
DesignPlacementwas missing theget_kind_color()accessor that NetBox's choice-field column relies on. - Approving a brand-new design no longer errors.
Design.clean()previously raised an unhandled error (HTTP 500) when a first/standalone design was created directly with status Approved, because the "at most one approved version per plan" check queried against an unsaved version root. The check is now skipped until the root is persisted. Covered by a regression test.
Changed¶
- The project's Apache-2.0 license is now declared in the package metadata
(
license = "Apache-2.0"inpyproject.toml) and the fullLICENSEtext ships with the distribution — previously the published package carried no license metadata. - Added the project icon (CC0) under
docs/assets/and wired it into the README and the MkDocs theme logo/favicon.
[0.1.0] - 2026-06-24¶
Release Summary¶
Initial release of NetBox Rack Design — a generic, public plugin that adds a versioned design layer for planning rack changes on top of real NetBox data. This first release delivers the data model and full management surface (Stage 1); the interactive visual editor and apply/conflict/power features follow in later stages.
Added¶
- Models:
Design(versioned, sequenced, with dependencies and optional grouping),DesignGroup(hierarchical container), andDesignPlacement(add / move / remove actions, validated againstRack.get_available_units()). - Full CRUD UI for all three models, including detail pages with related-object panels.
- REST API at
/api/plugins/rack-design/(designs, design-groups, placements). - GraphQL types and filters on the unified
/graphqlendpoint. - Global search, navigation menu, change logging, journaling, custom fields, and tags.
- Config-driven statuses (
planned_statuses/removal_statuses/default_status) viaPLUGINS_CONFIG— nothing organization-specific is hardcoded. - Test suite built on NetBox's standard test cases, plus MkDocs documentation.
Fixed¶
- N/A (initial release)
Changed¶
- N/A (initial release)
Deprecated¶
- N/A (initial release)
Removed¶
- N/A (initial release)
Security¶
- N/A (initial release)
Release Notes Template for Future Versions¶
When creating a new release, use this template:
## [X.Y.Z] - YYYY-MM-DD
### Release Summary
Brief narrative summary describing the release type (major/minor/patch) and key highlights.
### **Breaking Changes**
<!-- Only include this section if there are breaking changes -->
- **[#issue]** Description of breaking change and migration path
- Link to detailed migration guide if needed
### Added
- New features and capabilities
### Fixed
- Bug fixes with issue references
### Changed
- Changes to existing functionality
### Deprecated
- Features marked for future removal
### Removed
- Features that have been removed
### Security
- Security improvements and fixes
Best Practice: For clear release communication, ensure each release includes: 1. Narrative summary characterizing the release type (major/minor/patch) 2. Clear indicators for bugs, features, or enhancements 3. Bold "Breaking Changes" header when applicable with migration guidance 4. Detailed changelog with issue references