Table of Contents

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

  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 root node — which is always present once the tree is built, so the editor column always has something to show.
  4. 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.
  5. 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 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 calls the function only for ids whose root is in TREE_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 toolId when it is tool- followed by an integer;
  • segment three contributes tab when it is one of general, cutter, holder, clamping, intelligent;
  • segment four contributes subtab when it is one of the cutter sections — material, profile, contours, upper-beam, opt — or one of the holder sections, geometry and resolution.

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.tsmigrateLegacyTreeId, TREE_PAGE_ROOTS, routeForTreeId, the three tab-name lists it shares with the Tool House page, 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, the two selection/URL watchers and the unguarded controller-checkbox watcher beside them, landingSelection, applyRouteSelection and syncUrlBack, 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'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 menu entries that navigate by route name.
  • wwwroot-src/src/i18n/en/routes.ts — the English strings the meta.title keys resolve to.
  • wwwroot-src/src/i18n/index.tsregisterRetitle 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