Tree Ids and Routes
The web client addresses itself with two things: a vue-router path, and — on the two Control-Tree
pages — a tree query argument naming the selected tree node, as in
/general-setup?tree=equipment/fixture/geometry. 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/…. |
/general-setup |
general-setup |
The equipment Control Tree, whose selection rides ?tree=equipment/…. |
/machine-tool |
machine-tool |
The machining chain on a canvas of its own, beside a left column that shows either a read-only identity panel or the chain's XML source — two states of one column, chosen with a GUI/XML toggle. A load button installs a chain file into the project. Carries no Page-menu entry and is reached by URL. |
/tool-house/:toolId(\d+)?/:tab?/:subtab? |
tool-house |
The tool library and the per-tool editor. The three optional segments carry the selected tool and the two nested tab levels. |
/preference/log |
preference-log |
The Log Viewer, reached from the menu bar's Show Log button. 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. |
/util/mech-builder |
util-mech-builder |
The Mechanism Builder. |
/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 dropdown holds every one of these that is menu-reachable, in setup order: Tool
House, General Setup and Execution, then File Explorer and Mechanism Builder below a separator. The
Log Viewer has a button of its own rather than a menu entry, and /machine-tool has neither — it is
reached by URL.
The Tool House route's tool segment is constrained to digits, but all three of its segments are
optional, so a non-numeric segment in the tool position is not rejected: the router skips the tool
parameter and reads that segment as the tab, which is what makes /tool-house/cutter a working tab
deep link with no tool named. The consequence is that the record absorbs three segments when the
first is a number and only two when it is not, and a URL carrying more than the record can absorb —
/tool-house/abc/def/ghi — falls through to the catch-all. An unrecognised tab name is not an error
either: the page canonicalises it to that level's default, 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 still lands on the editor.
| Path | Lands on |
|---|---|
/controller/:rest(.*)* |
/general-setup?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? |
/general-setup?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 |
/general-setup?tree=equipment/spindle |
/fixture/:rest(.*)* |
/general-setup?tree=equipment/fixture — any trailing path is discarded. |
/workpiece/:rest(.*)* |
/general-setup?tree=equipment/workpiece — any trailing path is discarded. |
/equipment/background-coolant |
/general-setup?tree=equipment/background. Background and Coolant are two separate tree nodes; the redirect picks Background. |
/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 Tool House name lists below are the other case, genuinely shared.
The ?tree= Query
Selection and URL are synced two ways by the Control-Tree host, one instance per tree page.
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 equipment 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, and the one place the guard is
absent. The equipment host watches both checkboxes, and when the standing selection is a node the
flip removes, it replaces tree with equipment — the rest of the query preserved — before
stepping the selection off. The route name is not consulted there, so the replacement goes to
whichever route is showing.
What applyRouteSelection Does
- The change is ignored unless this page owns the current route.
- 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.
- 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 root node — which is always present once the tree is built, so the editor column always has something to show.
- An id the built tree does not contain is ignored — the current selection stands 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.
- Otherwise every ancestor prefix of the id 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.
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/… |
any id ending in /contours/tray, or containing /contours/tray/ |
the same id with that segment spelled fluting |
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 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;
- 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 General Setup 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 General Setup 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 forexecution, 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 calls the function only for ids whose root is inTREE_PAGE_ROOTS.
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 state in path segments rather than a query, so an id arriving from
another page has to be translated. routeForTreeId walks the segments of
toolhouse/tool-<n>/<tab>/<subtab>:
- segment two contributes
toolIdwhen it istool-followed by an integer; - segment three contributes
tabwhen it is one ofgeneral,cutter,holder,clamping,intelligent; - segment four contributes
subtabwhen it is one of the cutter sections —material,profile,contours,upper-beam,opt— or one of the holder sections,geometryandresolution.
Only the sub-tab step is nested inside the tab step: an unrecognised tab discards the sub-tab with
it, whereas the tool step is tested on its own, so a second segment that is not tool-<n> costs the
toolId param alone and the tab still translates. Everything deeper than the last segment the URL
can carry is discarded either way — an id pointing at a single flute contour under
…/cutter/contours/fluting/… lands on the nearest tab the URL can express. The same three name
lists are imported by the page itself, so the tab set and the translation cannot drift apart.
Once on the page, the tab segments are kept in step with the tabs by useRouteTabs, a composable
whose only consumer this page is. It gives each tab level one route param, a valid-name set and a
default; a nested level may declare which parent values make it apply, and while its parent is
something else the level is dormant — its segment is dropped from the URL while its reference
quietly remembers the last value, so returning to that parent tab returns to the sub-tab it had.
The Tool House sub-tab level applies under cutter and holder only, and both its valid set and
its default depend on which of the two is showing. A segment the level does not recognise resolves
to that default rather than failing. Canonicalisation runs on the first mount and on every
keep-alive re-entry, which is why a bare /tool-house acquires its tab segments. The tool segment
is not one of the composable's levels: the page fills it in itself, adopting the URL's tool when the
project holds it and the first tool in the list otherwise, then replacing the URL with what it
adopted. That resolver reacts to the
toolId param and to the tool list rather than to page activation, so it also re-points the URL
when the list changes underneath it.
The Tool House page never reads or writes ?tree=. A toolhouse/… id reaches it only by way of
the redirect above, which is issued by one of the two tree pages' hosts.
Reading an Id
An id's first segment names the page that owns it, and therefore the folder that documents it:
?tree= root |
Route | Documented under |
|---|---|---|
execution/… |
/execution |
Execution Page |
equipment/… |
/general-setup |
General Setup 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/workpiece/material/cutting-parameter is the Cutting Parameter
item of the Workpiece branch on the General Setup page. Most segments are kebab-case role names;
the two list-backed branches, Mission entries and Program files, use a positional index instead.
Every segment comes from the node id, never from the label the user reads: the General Setup
page's root node is labelled General Setup and carries the id equipment. 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
?tree= link 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, the three tab-name lists it shares with the Tool House page, andSPINDLE_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, the two selection/URL watchers and the unguarded controller-checkbox watcher beside them,landingSelection,applyRouteSelectionandsyncUrlBack, plus the build-time reveal of a URL-named CSV or CL controller node.wwwroot-src/src/composables/useRouteTabs.ts— the tab-to-param sync the Tool House route uses, including dormant levels and the keep-alive re-entry canonicalisation.wwwroot-src/src/pages/ToolHousePage.vue— consumes the shared tab lists, resolves the tool from its path param, and navigates by route name when the tool changes.wwwroot-src/src/pages/FileExplorerPage.vue— mirrors the browsed location into the route's catch-all param and back.wwwroot-src/src/pages/MachineToolPage.vue— the component behind the URL-only machine-tool route.wwwroot-src/src/pages/NotFoundPage.vue— the catch-all component, carrying its own layout.wwwroot-src/src/pages/LoginPage.vue— reads the guard'sredirectargument 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 menu entries that navigate by route name.wwwroot-src/src/i18n/en/routes.ts— the English strings themeta.titlekeys resolve to.wwwroot-src/src/i18n/index.ts—registerRetitleand 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