Table of Contents

Tree Ids and Routes

The web client addresses itself with two things: a vue-router path, and — on the Execution and Equipment pages — a tree query argument naming the selected tree node, as in /equipment?tree=equipment/mechanism/workpiece/raw-geometry. The third Control-Tree page, Tool House, spells its selection in the path instead. This page is the reference for both surfaces: the route table, the redirect-only paths that keep older bookmarks resolving, the id-migration chain, and the resolver that decides which page owns a given tree id. The router runs in HTML5 history mode, so every one of these paths is a real URL the server has to answer.

Important

Nothing here promises that a Control-Tree id is stable. The guarantee is narrower and exact: an id keeps resolving, because every regroup adds a migration hop to migrateLegacyTreeId rather than rewriting the previous one. The ids themselves have moved repeatedly, and a page that quotes one is quoting a value that can move again.

The Route Table

/ is the shell-layout record itself; every route below it except the login and catch-all records is one of its children, so each renders inside the Main Panel's frame and menu bar.

Path Route name What it owns
/ — The shell layout. It carries no page of its own; its empty child redirects to the Execution route.
/execution execution The run cockpit and the Execution Control Tree, whose selection rides ?tree=execution/….
/equipment equipment The equipment Control Tree, whose selection rides ?tree=equipment/….
/tool-house/:toolId(\d+)?/:node(.*)* tool-house The tool list and the picked tool's Control Tree. The optional tool number and the node catch-all after it spell the selected node's tree id.
/preference/log preference-log The Log Viewer, reached from the menu bar's Help dropdown, below its separator. It is not under /util/.
/util/file-explorer/:location(.*)* util-file-explorer The File Explorer. Its catch-all mirrors the browsed location — root display name followed by the relative segments — into the path.
/login login The sign-in form. It sits outside the shell layout and carries its own Quasar layout.
/:catchAll(.*)* — The 404 page, likewise outside the shell layout: it prints the unmatched full path and offers one button back to the Execution route.

The menu bar's page switches name four of these, in setup order: Tool House, Equipment and Execution, then File Explorer. Each switch is keyed by route name, so any URL under that route name — a ?tree= selection, a Tool House node path, an explorer deep path, a redirect once it lands — lights it. The Log Viewer has an entry in the Help dropdown rather than a switch.

The Tool House route's tool segment is constrained to digits and optional, and the node catch-all after it absorbs any number of segments, so every path under /tool-house/ matches the record and none reaches the catch-all. A non-numeric segment in the tool position is not rejected: the router leaves the tool parameter unset and reads that segment as the first node segment, which is what makes /tool-house/cutter a working deep link to the Cutter root of the tool already shown — the house's first tool on a fresh load — after which the URL gains that tool's number. A node path the tool does not grow is not an error either: the page clamps it to the deepest node it does grow, and /tool-house/abc/def/ghi, which names no root at all, lands on the first root, as The Tool House Translation sets out.

Redirect-Only Paths

These records carry no component. They exist so that a link minted before the corresponding editor moved into a Control Tree, or before its page was renamed, still lands on the editor. The exception is the three links onto the Spindle Capability and Environment roots, which the Equipment tree builds only while Show Physics Options is on: with it off, the /spindle-capability, /equipment/spindle and /equipment/background-coolant targets name no built node, and the page ignores them as it ignores any unknown id (step 4 of What applyRouteSelection Does, below).

Path Lands on
/general-setup /equipment, with the whole query carried over, so a ?tree= bookmark lands on the same node. It is the path the page answered to under its earlier name, General Setup.
/controller/:rest(.*)* /equipment?tree=equipment/controller — any trailing path is discarded, the retired Controller page's /<tab> segment included, so every form lands on the branch root.
/spindle-capability/:tab? /equipment?tree=equipment/spindle, with /<tab> appended when the segment is one of thermal, gear-shift, dry-run, power, torque. An unrecognised segment is dropped and the branch root is selected.
/equipment/spindle /equipment?tree=equipment/spindle
/fixture/:rest(.*)* /equipment?tree=equipment/mechanism/fixture — any trailing path is discarded.
/workpiece/:rest(.*)* /equipment?tree=equipment/mechanism/workpiece — any trailing path is discarded.
/machine-tool /equipment?tree=equipment/mechanism/machine-tool. The path belonged to a stand-alone load-and-show page; the Machine Tool root carries its Load, identity and XML view.
/util/mech-builder /equipment?tree=equipment/mechanism/machine-tool, the Machine Tool root. The path belonged to the Mechanism Builder, a stand-alone editor for a lone mechanism file; a chain is loaded from a preset on that root, edited in the Topology column and written out with the root's Save As.
/equipment/background-coolant /equipment?tree=equipment/background. That id is the Environment root over the Ambient Temperature and Coolant leaves (equipment/background/environment-temperature, equipment/background/coolant); the redirect lands on the root (the ids and the route keep the legacy background spelling).
/mission /execution?tree=execution/mission

The five spindle tab names are declared in treeRoutes.ts as SPINDLE_TABS, and imported by wwwroot-src/src/router/routes.ts — which is the list the redirect tests its :tab? segment against. The equipment tree does not import that constant: it spells the same five segments as literals while building the equipment/spindle/<seg> children, so redirect and branch agree by convention rather than through a shared list — renaming a spindle section is therefore an edit in two places.

The ?tree= Query

Selection and URL are synced two ways by the Control-Tree host, one instance per tree page. This section is the two query pages' side; the Tool House host runs the same two watchers over its path parameters, as The Tool House Translation sets out.

URL to selection. A watcher on the query drives applyRouteSelection(), and it is registered immediate, so an id belonging to another page redirects on the first tick rather than after this page's first tree build. The host also calls the same function once the tree has actually been built, which is when a deep link can finally be honoured.

Selection to URL. A watcher on the selected id replaces the tree argument, preserving the rest of the query. It uses router.replace, so browsing the tree does not fill the browser's history with one entry per node. The comparison that breaks the loop is made against the raw query rather than the migrated form, which is what makes an older id canonicalise: opening ?tree=controller selects the node and then rewrites the URL to ?tree=equipment/controller.

Neither watcher touches the URL unless the current route name is the page's own. The shell layout keeps every visited page alive, so a host whose page is not showing keeps receiving route changes, and without the guard it would write another page's URL. The selection watcher does one thing before that guard: it records the id as this page's last selection. That ordering is load-bearing, because a selection can move while its page is off screen — unticking a CSV or CL Controller checkbox steps the equipment host off that node onto the Controller root, and the Preference menu that carries those checkboxes is open on every page — and the landing selection has to have followed it.

That same checkbox flip is a third writer of the tree argument. The equipment host watches both checkboxes, and when the standing selection is a node the flip removes, it replaces tree with equipment/controller — the rest of the query preserved — before stepping the selection off. It applies the same route-name guard: while another page is showing, only the selection moves, and the URL is left alone.

What applyRouteSelection Does

  1. The change is ignored unless this page owns the current route.
  2. The raw query is migrated into today's id. If its first segment names a different page and that segment is one of the known roots, the location is replaced with the route that owns it and the pass stops.
  3. An empty query falls back to the landing selection: the persisted last id for this page when that id exists in the tree just built, otherwise the page's first root — Mechanism on the Equipment page, Mission on the Execution page — which is always present once the tree is built, so the editor column always has something to show. The one exception is the Execution page while its Program branch's live follow is on: follow owns the selection then, so returning to the page with no query leaves the current selection standing.
  4. An id the built tree does not contain is ignored: whatever selection stands stays (on a fresh load there is none yet, so the editor column shows its select-an-item hint), and the URL keeps the unrecognised value until the next selection overwrites it. This is deliberate: the redirect in step 2 fires only for known roots, because sending an unroutable id back to the same page would loop.
  5. Otherwise every row above the node on its built path is added to the expanded set so the node is visible, and the selection is requested through the same gate a tree click uses. A panel holding unsaved edits may refuse the switch, and a refusal re-points the URL at the selection that actually stands.

On the Tool House page the pass leaves this list after step 2, and adopts the path against the tree of the tool it names instead.

The Id the URL Names, Before the Tree Exists

treePathOfRoute() — the raw query put through migrateLegacyTreeId — is not read only by the selection logic. The equipment tree consults it while it is being built, to decide whether to materialise the CSV Controller and CL Controller nodes: each is normally shown only when its Preference checkbox is ticked, and a URL naming one reveals it regardless. That reveal has to happen at build time precisely because step 4 above refuses an id the built tree lacks, so a bookmark to a switched-off node would otherwise dead-end. Only the URL reveals a node this way; the persisted last selection does not, so a node the user has just unticked while standing on it does not resurrect itself.

Id Migration

migrateLegacyTreeId is one pass of ordered guards over the whole id; the first that matches returns, and an id matching none is returned unchanged.

An id shaped like Becomes
controller, controller/… equipment/controller, equipment/controller/…
spindle, spindle/… equipment/spindle, equipment/spindle/…
mission, mission/… execution/mission, execution/mission/…
equipment/mission, equipment/mission/… execution/mission, execution/mission/…
execution execution/mission — the bare id named a wrapper root over the Execution page's branches; it lands on Mission, the page's first root
equipment equipment/mechanism — the bare id named a wrapper root over the Equipment page's items; it lands on Mechanism, the page's first root
equipment/machine-tool, equipment/fixture, equipment/workpiece, and anything below them the same id under equipment/mechanism/; child segments ride along
equipment/mechanism/fixture/geometry, …/fixture/geom-to-workpiece, …/fixture/geom-to-table, …/fixture/anchor, equipment/mechanism/workpiece/anchor, and anything below them the mechanism root that stands for the retired item — equipment/mechanism/fixture or equipment/mechanism/workpiece. Those ids named the fixture's and the workpiece's slot and anchor items; the anchors, branches, transformers and solids behind them are edited in the Equipment page's Topology column, which no ?tree= id names, so the link lands on the root whose panel binds the roles those anchors play
equipment/coolant equipment/background/coolant
any id ending in /contours/tray, or containing /contours/tray/ the same id with that segment spelled fluting

A pre-regroup equipment/background named the background-temperature leaf and now names the Environment group; no rule re-points it, because the group lists that leaf and re-pointing the id would leave the group itself unreachable by URL.

The last rule renames one segment in place and leaves its children — baseline, flute-<i>, side, bottom — untouched, so a deep link into a flute contour survives the engine's Fluting type naming. The two mission rules cover the two id shapes separately rather than chaining, because the first matching guard returns. The mechanism regroup rule is the one exception: it rewrites the id in place and falls through instead of returning, so the retired-item rule after it also catches an id minted under the wrapper root, such as equipment/fixture/geometry, and lands it on the same mechanism root.

The function is applied at four points, which is what makes the guarantee hold in practice:

  • to the raw ?tree= query, on every read;
  • to the persisted last selection, before it is looked up in the tree;
  • to the persisted list of expanded node ids, once when a host is constructed, so an older expansion set still unfolds the branches it names — there a bare equipment expands both equipment/mechanism and equipment/background instead of taking the one-id rule above;
  • as the first step of routeForTreeId, so the id-to-page resolver never sees an unmigrated id.

Note what the first rule implies: a bare ?tree=controller selects the equipment/controller branch — the controller editor on the Equipment page — and the redirect-only /controller path above lands on the same branch, so a bookmark from the retired Controller page's era resolves in either form.

Landing an Id on Its Page

TREE_PAGE_ROOTS lists the first segments that name a page: execution, equipment and toolhouse. spindle is absent on purpose — migration folds it into equipment/… before any root check runs, so no root check ever sees it.

routeForTreeId migrates the id, then dispatches on its first segment:

  • toolhouse — the Tool House route, with the id's segments translated into path params (below).
  • equipment — the Equipment route, carrying the migrated id as ?tree=.
  • anything else — the Execution route, carrying the migrated id as ?tree=. This is a fall-through, not a test for execution, so an unrecognised id handed to this function lands on the Execution page. Reaching it with a genuinely unroutable id is prevented upstream: the host redirects through it only for ids whose root is in TREE_PAGE_ROOTS, and its other callers pass ids of built nodes. Those callers are the Tool House host spelling its own selection, and the Control Tree's row links, which resolve each row's id through it so that a modified or middle click, or the browser's link menu, opens the row in a new browser tab or window at its page's URL, while a plain click only selects it.

The returned location replaces the whole current location. Only the tree argument survives the hop; any other query argument on the URL being redirected away from is dropped, and the Tool House form carries no query at all.

The Tool House Translation

The Tool House page keeps its selection in the path rather than a query: the tool number, then the selected node's id with its toolhouse/tool-<n> prefix removed, so /tool-house/3/cutter/contours/fluting/flute-0/side names the node toolhouse/tool-3/cutter/contours/fluting/flute-0/side. toolNodePathOf() takes the node half off an id, everything after toolhouse/tool-<n>, and toolTreeId() joins a tool number and a node path back into an id. routeForTreeId makes the same split: segment two contributes toolId when it is tool- followed by an integer, and every segment after it rides the node catch-all. The translation is lossless in both directions — whatever depth the tree grows, a single flute contour included, arrives whole. An id whose second segment is not tool-<n>, a bare toolhouse among them, carries neither parameter and lands on a bare /tool-house.

No valid-name list guards either half; the tree is the authority. The host watches the id the two parameters spell, and once the page has read the tool list it walks the node path down the built tree of the tool the URL names and selects the deepest node it reaches. A third flute on a two-flute cutter lands on the fluting node above it; a path whose first segment is no root of this tool — Int. Holder while Show Physics Options is off, or /tool-house/abc/def/ghi — lands on the first root, General. A :toolId the house does not hold falls back to the house's first tool. Either way the URL is then rewritten to the node actually selected, with router.replace, as every URL write the page makes itself is, the tool list's links included.

Because the node half carries no tool number, a row of the tool list links to the current node path under its own :toolId, so switching tools keeps the editor on the same node. The host also remembers the node path last asked for, in desiredNodePath, seeded from the persisted last selection. A clipped selection leaves that memory alone, and while the URL's path is a prefix of it the list rows link to the remembered path, so the deeper node comes back on the next tool that grows it. The URL is the authority over that memory: only a URL that names neither tool nor node lands on it — a fresh load of a bare /tool-house on the house's first tool, and a re-root while the URL names no tool (a Show Physics Options flip or a tool-list change, made on the bare page or from another page while this one is kept alive) on the tool already shown. Any other URL is taken literally. The menu bar's page switch links to the bare /tool-house, but coming back through it re-runs nothing: the kept-alive page shows the selection it already had, and the URL stays a bare /tool-house until the next selection writes it.

The Tool House page never reads or writes ?tree=. A ?tree= link naming a toolhouse/… id reaches it only by way of the redirect above, issued by the Execution or Equipment host, and the whole id arrives in the path.

Reading an Id

An id's first segment names the page that owns it, and therefore the folder that documents it:

Id root Route Documented under
execution/… /execution Execution Page
equipment/… /equipment Equipment Page
toolhouse/… /tool-house/… Tool House Page

The remaining segments are the branch path, one segment per level of the tree, and the branch's own page sits in that folder — equipment/mechanism/workpiece/material/cutting-parameter is the Cutting Parameter item of the Workpiece branch on the Equipment page. On Tool House the path is led by tool-<n>, the tool's number, which has no row of its own. Most segments are kebab-case role names; list-backed children use a positional index instead — bare for Mission entries and Program files, prefixed for a Program file's written conversions (dst-<i>), a geometry combination's items (item-<i>) and a free fluting's contours (flute-<i>).

Every segment comes from the node id, never from the label the user reads: the Equipment page's first root is labelled Mechanism and carries the id equipment/mechanism, whose leading segment is the page's scope key rather than a node of its own. A label is a display string — the translation of the node's labelKey where it has one, and otherwise raw data such as an NC file name or a mission command's user-typed title, as Control Tree sets out — while an id is neither translated nor taken from data. Two consequences follow. A link naming an id is locale-independent, and an id can never be read off what is on screen.

Titles

A route's meta.title holds an i18n key, such as routes.execution, not a title. The router resolves it through the active locale in an afterEach hook and sets the document title to the translated name followed by - HiNC; a route with no key — the catch-all — leaves the bare product name. The sibling name: is the route id used by every programmatic navigation and is never translated; the two must not be confused, which is why the route table above lists them side by side.

The same resolver is handed to the i18n module through registerRetitle, so that switching language re-titles a parked tab without a navigation. The registration is indirect because the router already imports the i18n module for its translation function, and importing back would close a cycle.

Serving These URLs

History mode means the browser sends the whole path to the server on a refresh or a pasted link, so the back end maps two SPA fallbacks. The bare fallback carries an implicit constraint that rejects any URL whose last segment contains a dot, which is exactly the shape a File Explorer deep link takes, since those mirror real file names. An explicit fallback for the explorer's path is therefore mapped ahead of it with no such constraint. Static assets cannot be shadowed by either, because static-file serving runs earlier in the pipeline. Both fallbacks allow anonymous access, so the SPA itself always loads and the sign-in decision is made in the client.

That decision is a navigation guard: it hydrates the authentication status once, lets every navigation through when the back end reports the feature disabled, and otherwise sends an unauthenticated visitor to the login route with the original full path — ?tree= included — parked in a redirect query. A successful sign-in replays that path as a full page load rather than a router navigation, so the application re-initialises with the cookie present. A visitor who is already signed in, or who arrives while the feature is off, is sent straight back out of the login route to the same parked path.

Source Code Path

See HiNC App Anatomy for git repository links.

Web Application

HiNC-2025-webservice (Quasar CLI SPA):

  • wwwroot-src/src/router/routes.ts — the route table, the redirect-only records, the login route outside the shell layout, and the trailing catch-all.
  • wwwroot-src/src/router/treeRoutes.ts — migrateLegacyTreeId, TREE_PAGE_ROOTS, routeForTreeId, toolNodePathOf, which takes the node half off a Tool House id, and toolTreeId, which joins a tool number and a node path back into one, and SPINDLE_TABS, which only the spindle redirect reads.
  • wwwroot-src/src/router/index.ts — the history-mode router factory, the authentication guard, and the retitle hook registered with the i18n module.
  • wwwroot-src/src/components/controlTree/useControlTreeHost.ts — the tree-page host: rawTreePathOfRoute, treePathOfRoute, locationForSelection, the two selection/URL watchers and the controller-checkbox watcher beside them, landingSelection, applyRouteSelection and syncUrlBack, plus the build-time reveal of a URL-named CSV or CL controller node. In its toolhouse scope: resolveToolId, deepestBuiltId, adoptToolhouseSelection, desiredNodePath and toolLocation, the list row's link.
  • wwwroot-src/src/pages/ToolHousePage.vue — creates the toolhouse-scope host and renders the tool list as replace router links to toolLocation: another :toolId over the current node path, lengthened to the remembered one while the URL's path is a prefix of it.
  • wwwroot-src/src/components/controlTree/ControlTreePanel.vue — renders each row's label as a link to the URL routeForTreeId gives its id.
  • wwwroot-src/src/pages/FileExplorerPage.vue — mirrors the browsed location into the route's catch-all param and back.
  • wwwroot-src/src/pages/NotFoundPage.vue — the catch-all component, carrying its own layout.
  • wwwroot-src/src/pages/LoginPage.vue — reads the guard's redirect argument and replays it.
  • wwwroot-src/src/composables/useViewPrefs.ts — the browser-local store holding the per-page last selected id and the expanded-node list that migration is applied to.
  • wwwroot-src/src/components/AppMenuBar.vue — the page switches, which navigate by route name and light by it.
  • wwwroot-src/src/i18n/en/routes.ts — the English strings the meta.title keys resolve to.
  • wwwroot-src/src/i18n/index.ts — registerRetitle and the locale switch that calls it.
  • wwwroot-src/quasar.config.ts — selects history mode and names the build output folder the server serves.
  • Program.cs — the two SPA fallbacks, the explorer-specific one ahead of the bare one.

See Also

  • Control Tree — the tree these ids name: how a node id is formed and what a branch is made of
  • Main Panel — the shell whose menus navigate to these routes and whose keep-alive cache makes the route-name guards necessary
  • Execution Page — the busiest consumer of ?tree=, and the page a fall-through id lands on
  • Equipment Topology Column — the Equipment page's Topology column, which no ?tree= id names: the retired fixture and workpiece item ids land on their mechanism root instead
  • Machine Tool — the Machine Tool root the /machine-tool and /util/mech-builder redirects land on
  • Tool House Page — the one tree page whose selection rides the path: its tool list, tree and editor behind the :toolId / :node URL