This commit is contained in:
2026-09-09 17:35:23 +08:00
parent 2a98ae7556
commit 161af68fe3
1271 changed files with 58251 additions and 98639 deletions
@@ -0,0 +1,489 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Control Tree | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Control Tree | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-ControlTree">
<h1 id="control-tree">Control Tree</h1>
<p>The Control Tree is the engine shared by the two tree-shaped pages of the web client: the Execution
page at <code>/execution</code> and the General Setup page at <code>/general-setup</code>. Each page creates exactly one
host — <code>useControlTreeHost('execution')</code> or <code>useControlTreeHost('equipment')</code> — provides it to its
panes under one injection key, and the host owns everything after that: the node graph, the branch
builders, the selection and the <code>?tree=</code> query that carries it, the editor panel a selection resolves
to, and the mission enable ticks. The Tool House page at <code>/tool-house</code> reuses the same node shape and
the same item-type registry but renders a branch as nested tabs and creates no host.</p>
<div class="IMPORTANT">
<h5>Important</h5>
<p>Node ids are role paths, not identities that survive a version change. Anatomy makes no claim that
a <code>?tree=</code> id is stable across releases; see <a class="xref" href="tree-ids-and-routes.html">Tree Ids and Routes</a>
for the migration hop that keeps an older link resolving.</p>
</div>
<h2 id="the-node">The Node</h2>
<p>Every branch of every tree is built from one structure, <code>ControlNode</code>, declared in <code>itemTypes.ts</code>:</p>
<ul>
<li><code>id</code> — the node's role path, slash-separated, whose first segment is the owning page's scope
(<code>execution/…</code>, <code>equipment/…</code>, <code>toolhouse/…</code>). This is the addressing surface: the <code>?tree=</code> query,
the persisted expansion list and the persisted last selection all hold ids.</li>
<li><code>label</code>, <code>labelKey</code>, <code>labelParams</code> — the display text; see <a href="#display-labels">Display Labels</a>.</li>
<li><code>itemType</code> — the registry key that decides the node's editor and its child builder.</li>
<li><code>key</code> — the bound object. In the geometry and equipment branches it is an IndexService key,
re-minted on every re-index; in the Mission branch it is the Mission API command path (<code>0</code>, <code>1</code>,
the dotted <code>0.2</code> of a nested list entry); in the Tool House branch it is the tool id as a string.
Waves whose panels read a module-level state singleton rather than an indexed object leave it
empty — the SoftNc controller leaves, the spindle sections, the Background and Coolant leaves and
the Program branch all do.</li>
<li><code>ctx</code> — the parent-provided <code>SlotCtx</code>: the <code>afterChange</code> commit chain, a slot's <code>onCreate</code>
create-and-rebind hook, and a slot picker's <code>allowedKinds</code> / <code>allowNone</code> constraints.</li>
<li><code>children</code> — grown by the builders, not declared by the tree column.</li>
<li><code>selectable</code>, <code>info</code> / <code>infoKey</code>, and the <code>mission</code> / <code>program</code> bookkeeping records the Mission and
Program waves stamp on their own nodes.</li>
</ul>
<p>The split between <code>id</code> and <code>key</code> is the load-bearing one. A key is re-minted every time the host
re-indexes the model, so nothing durable may hang off it; an id names a <em>role</em> in the model and
therefore survives a rebuild, which is why expansion state, the URL and the landing selection are all
keyed on ids. Two id families are positional rather than role-based: Mission entries are
<code>${parentId}/${index}</code> and Program files are <code>${parentId}/${index}</code>, so moving or deleting a mission
command renumbers its siblings and a link to one names a position in the list rather than that
command.</p>
<h2 id="the-item-type-registry">The Item-Type Registry</h2>
<p><code>ITEM_TYPES</code> maps an <code>itemType</code> string onto a <code>TreeItemDef</code> carrying up to three members:</p>
<ul>
<li><code>panel</code> — the editor mounted in the dock's editor row for a node of this type.</li>
<li><code>contentPanel</code> — a large view for the General Setup page's content column.</li>
<li><code>buildChildren</code> — an async builder that returns this node's children.</li>
</ul>
<p>Two flavours of type share the map. <strong>Slot</strong> types (<code>Geometry</code>, <code>Transformer</code>) have a kind picker as
their panel and grow at most one child, the concrete kind, at the kind-independent id
<code>${slot.id}/type</code> — so switching a slot's kind keeps both the selection and the expansion. A slot
holding nothing grows no child at all, which for the four geometry slots whose picker offers <em>None</em> is
an ordinary state rather than a failure. <strong>Kind</strong> types (<code>Box3d</code>, <code>StaticTranslation</code>, …) register
<code>SoleEditorPanel</code>, which looks the concrete editor up in the geometry and transformer editor maps and
binds it to the node's key. The two composite geometry kinds do not embed nested editors:
<code>TransformationGeom</code> grows an inner-geometry and an
inner-transformer slot, and <code>GeomCombination</code> grows one slot per item, so the tree owns the structure
and every panel stays shallow.</p>
<p>The map is one flat namespace assembled in <code>itemTypes.ts</code>: the <code>Group</code> stem type, the geometry,
transformer, workpiece-leaf, thermal-condition and spindle entries, and the Execution root's own type
are written inline, and five per-wave registries are spread in beside them. The three roots the panel
host renders itself — Machine Tool, Fixture and Workpiece — have no entry here at all.</p>
<table>
<thead>
<tr>
<th>Wave</th>
<th>File</th>
<th>What it contributes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mission</td>
<td><code>missionItemTypes.ts</code></td>
<td><code>MissionRoot</code>, <code>MissionCommand</code>, <code>MissionSection</code>; section children at <code>${command.id}/${sectionId}</code>; the per-kind bespoke editors, with a generic field editor as the fallback</td>
</tr>
<tr>
<td>Program</td>
<td><code>programItemTypes.ts</code></td>
<td><code>ProgramRoot</code>, <code>ProgramFile</code>, <code>ProgramConversionFile</code>; writeback conversions at <code>${root.id}/dst-${index}</code></td>
</tr>
<tr>
<td>Tool House</td>
<td><code>toolHouseItemTypes.ts</code></td>
<td>the tool collection and the per-tool editor branches under <code>toolhouse/tool-&lt;id&gt;/…</code></td>
</tr>
<tr>
<td>SoftNc controller</td>
<td><code>softNcItemTypes.ts</code></td>
<td>the controller root and its leaves on two planes — <code>…/machine/&lt;seg&gt;</code> and <code>…/program-data/&lt;seg&gt;</code> — a core every runner grows plus the brand-driven ones</td>
</tr>
<tr>
<td>Runner suits</td>
<td><code>runnerSuitItemTypes.ts</code></td>
<td><code>CsvRunnerRoot</code> and <code>ClRunnerRoot</code>, flat single-panel leaves with no builder</td>
</tr>
</tbody>
</table>
<p>The Program branch is fetched whole: its root builder takes one file-tree response plus one
conversions response and stashes each file's raw subtree on the node, so the file nodes' builder only
maps what is already in hand. The SoftNc branch is the opposite — its builder reads the runner
snapshot once, and with no runner installed grows nothing at all. Where one resolves, the two plane
stems appear over a fixed core that stands for every brand: six machine leaves, and Work Coordinates
and Tool Offsets on the program-data plane. The leaves beside that core are the brand-driven ones —
each appears only where the snapshot reports its backing table, so a brand switch changes which nodes
exist. One flag does double duty: the Siemens <code>$TC_DP</code> flag adds its own two leaves <em>and</em> renames the
core Tool Offsets leaf to its ISO G43 H reading, so the two offset ledgers do not read as one.</p>
<p>The two runner-suit nodes are conditional, and the host — not the registry — decides. With a project
open, a CSV or CL controller node is built when its Preference checkbox is on, or when the current
<code>?tree=</code> value starts with that node's id. The checkbox is device-local and off by default; the route
is the only other
reveal, deliberately not the persisted last selection, so a node switched off while it is selected
does not resurrect itself. Stepping off comes first: when the selection sits on a node the user has
just unchecked, the host re-points the URL at the page root before rebuilding.</p>
<h2 id="building-a-branch">Building a Branch</h2>
<p><code>buildSubtree(node, services, depth)</code> is the whole builder. It looks the node's type up, returns
without touching <code>children</code> when the type declares no <code>buildChildren</code> or when <code>depth</code> has reached
<code>MAX_DEPTH</code> (12, a guard against a self-referencing model), and otherwise awaits the builder, recurses
into each returned child, and assigns the result. Building is eager: a branch is materialised in full
at build time rather than lazily on expansion.</p>
<p>Builders receive one service, <code>registerKey</code>, which registers a freshly minted IndexService key with
the page's cleanup hub — see <a class="xref" href="../conventions/webapi-hub-cleanup-pattern.html">Webapi with Hub-Cleanup Assistance</a>.</p>
<p><code>rebuildBranch(node)</code> is <code>buildSubtree</code> plus one thing: it adds the node's id to the expansion list so
the fresh children are visible, and reports a failure as a toast named after the node's display label.</p>
<h3 id="the-structure-changed-event">The structure-changed event</h3>
<p>A panel that has changed the <em>shape</em> of the model emits <code>structure-changed</code> (or <code>type-changed</code>, wired
to the same handler). The optional payload is a <code>StructureChangeRequest</code>:</p>
<ul>
<li><code>scopeId</code> — the branch to rebuild. Without it the scope is the selected node itself, which is
correct only while the change stays inside the emitting panel's own branch.</li>
<li><code>selectId</code> — the selection to adopt afterwards. It is assigned directly, <strong>bypassing the
dirty-switch gate</strong>, because the emitting panel is being replaced on purpose. A <code>null</code> clears the
selection.</li>
</ul>
<p>A mission command's move, duplicate and delete re-scope to the parent list, because those operations
rewrite the parent's children, and they name the post-operation selection: the clone's slot after a
duplicate, the neighbouring index after a delete, the parent list when the delete emptied it. Moving
an entry out of a nested list re-scopes to the grandparent. The controller brand switch and the
runner's Object-Management install re-scope to the controller root. The Tool House type selectors and
a flute add re-scope to their own node; everything that rewrites the tool collection — a new tool, a
duplicate, a delete, an id rename, an Object-Management install — re-scopes to the tool-house root,
and a flute delete re-scopes to its fluting parent.</p>
<h3 id="whole-tree-rebuilds-and-the-refreshes-that-are-not-rebuilds">Whole-tree rebuilds, and the refreshes that are not rebuilds</h3>
<p><code>rebuildTree()</code> replaces the root array outright, and runs after an Object-Management swap of the
machine tool or the fixture, after a blank machining chain is created, and after either runner-suit
preference flips. On the execution host, an
execution-status transition re-runs <code>buildSubtree</code> on the Program root <strong>directly</strong> rather than through
<code>rebuildBranch</code>, so a run does not force that branch open on every transition.</p>
<p>Two refreshes deliberately mutate existing nodes instead of rebuilding:</p>
<ul>
<li>A mission command's title is re-read from the entry list and written onto the node's label.</li>
<li>A file-backed geometry leaf's label is re-read when its source file changes.</li>
</ul>
<p>The mission one exists because a rebuild of the Mission branch re-mints the per-build <code>stamp</code> that
forms part of the editor's remount key, and remounting an open editor mid-edit would cost the user
their cursor and any staged autosave. The geometry one is the same principle without the stamp: a
title change and a source-file change alter no structure, so the label is all that has to move, and
replacing the node under an open file editor would buy nothing.</p>
<h3 id="the-commit-chain">The commit chain</h3>
<p>The tree has no component bubbling, so the full post-edit commit chain lives on each node as
<code>ctx.afterChange</code>, composed level by level as the branch is built. A <code>GeomCombination</code> item's chain
cleans that combination's aggregated cache before running its parent's chain; the fixture's chain ends
in a geometry-cache clear; the two workpiece geometry slots re-commit the swap-in before clearing
their own cache, and skip that re-commit when the slot has been set to none; the fixture's two anchor
transformers re-commit and then clear the geometry cache, while the workpiece's two re-commit and
clear nothing, since a placement change invalidates no cached solid. Tool-house nodes all share one
refresh. Every other family carries a no-op: the Mission, Program, spindle, SoftNc and
thermal-condition nodes and the workpiece's Mesh and Material leaves have panels that write straight
to the model, and the pages' canvases draw that model; the group stems and the Machine Tool and
Workpiece roots have no field editor to commit for at all.</p>
<h2 id="selection-and-the-dirty-switch-gate">Selection and the Dirty-Switch Gate</h2>
<p>Every selection change funnels through <code>requestSelect(id)</code>. It returns immediately when the id is
already selected; otherwise it awaits the mounted panel's optional <code>tryConsumeBeforeSwitch()</code> and
abandons the switch when that returns false.</p>
<p>Three entrances use it: a click in the tree, a change to the URL's <code>?tree=</code> value, and a <code>select-node</code>
event from the mounted panel — the wire the panel host puts on every registry panel, so a group stem's
child list, a mission list's entry row and the Program branch's conversion jumps all arrive through
that one. A fourth path — the <code>selectId</code> of a structure change — assigns the selection directly and
skips the gate.</p>
<p>There is no &ldquo;nothing selected&rdquo; state. Re-clicking the selected row makes the tree yield <code>null</code>, and
that is ignored; an empty <code>?tree=</code> lands on the persisted last selection for that page when the built
tree still contains it, and on the page's root otherwise. The editor row is therefore always alive,
and reclaiming its height is the row's own collapse toggle rather than a deselection.</p>
<p>The gate itself is registered by the panel host: the mounted registry panel is handed to the host
through a template ref, and the host asks that instance for <code>tryConsumeBeforeSwitch</code>. Panels that
write one field per request need no gate and register none. One panel exposes it —
<code>MissionCommandSlavePanel</code>:</p>
<ul>
<li>on a <code>list</code> entry it flushes the debounced title save while the command path is still live, then
allows the switch;</li>
<li>on any other entry it forwards its inner editor's gate. Today only the script editor has one: with
unsaved text it asks save, discard or cancel, returning false on cancel and on a flush that ends in
an error.</li>
</ul>
<p>The same gate is run before duplicating a command, so the clone is made from what has just been typed
rather than from the last saved state.</p>
<h2 id="display-labels">Display Labels</h2>
<p><code>nodeDisplayLabel(n)</code> returns the translation of <code>labelKey</code> — with <code>labelParams</code> interpolated — when
one is set, and the verbatim <code>label</code> otherwise. <code>labelKey</code> is therefore the form every role label the
client translates itself takes, while <code>label</code> is the storage for text it cannot: server-composed
mission titles, file-backed geometry paths and engine type names. The translator function is read
inside the call rather than captured, so a language change re-renders the whole tree's role labels.</p>
<p>Two labels are composed rather than looked up. A mission command's label is the title the server
composes for it — the command's kind name in the request language, with the command's own text in
brackets when there is one, as in <code>Script [Warm-Up]</code>. That title always arrives filled in, so the
node's <code>labelKey</code> is always dropped and the tree's own kind-name fallback is a safety net that never
fires in practice. Because titles are composed server-side per request, a language change also
re-pulls the command entries and rewrites those labels in place. A file-backed
geometry leaf carries its source file in brackets, so sibling instances read apart:
<code>StlFile [Geom/x.stl]</code>. The voxel kind is renamed for display, appearing as <code>MeshedGeomFile [...]</code>
rather than by its type name.</p>
<p>Group stems take the same treatment for their intro text: <code>infoKey</code> wins over <code>info</code>, and a stem with
neither falls back to a sentence naming the group.</p>
<h2 id="the-panel-host">The Panel Host</h2>
<p><code>PrimarySlavePanel</code> decides what the editor row shows, in this order:</p>
<ol>
<li><strong>The three inline root types</strong> — <code>MachineToolRoot</code>, <code>FixtureRoot</code>, <code>WorkpieceRoot</code> — are rendered
by the panel itself as a summary, and are never resolved from the registry. The two with a
stand-alone file surface, Machine Tool and Fixture, carry an Object-Management button above it;
the workpiece has none, since it is authored entirely through its child tree items.</li>
<li><strong>Otherwise the registry's <code>panel</code></strong> for the selection's item type, mounted with the node as its
only prop, with <code>changed</code>, <code>type-changed</code>, <code>structure-changed</code>, <code>select-node</code> and <code>error</code> wired to
the host.</li>
<li><strong>A spinner</strong> while the first build is still running, and the select-an-item hint after that when
the selection's type registers no panel.</li>
</ol>
<p>What decides a remount is the mounted component's key, <code>${id}|${key}|${stamp}</code> — the node id, the node
key, and the mission stamp where the node has one. So the editor remounts when the selection moves,
when the bound object's key is re-minted, and when the Mission branch was rebuilt beneath it. That
third term is exactly why the mission label refresh above mutates the node instead of rebuilding.</p>
<p>On the Execution page the editor row grows a header: the transport bar is pinned above the scroll area
whenever the Execution root or any of its descendants is selected, so the run controls never scroll
away; that one instance also owns the transport's function-key shortcuts, which are attached for as
long as it is mounted.</p>
<p>A pure <code>Group</code> stem is not a node without an editor. <code>Group</code> registers <code>GroupInfoPanel</code>, whose body is
the stem's intro line followed by a clickable list of its children; a row click emits <code>select-node</code>
and the host moves the selection there. The General Setup root, the two Anchor stems, the workpiece
Material stem and the controller's two plane stems are all of this kind.</p>
<p>The General Setup page adds a second slave, <code>ContentSlavePanel</code>, which resolves <code>contentPanel</code> from
the same registry. Three item types declare one — the spindle root and its scalar and contour section
types — and all three name the same contours view, so the charts stay mounted while the selection
moves across the spindle branch: the component takes no props and is mounted <strong>unkeyed</strong> on purpose.
Every other selection shows the column's empty hint.</p>
<p><code>NodeTabCascade</code> is the third consumer of the registry. It renders a branch as nested tabs rather than
tree rows: the node's own panel on top, a single child inlined below it, several children as a dense
tab strip in which only the active child mounts. The active tab is remembered per role path at module
scope, with the per-tool segment wildcarded, so switching tools keeps every level's tab. Its panels
report through an injected cascade host that carries the emitting node, because — unlike the tree's
one-panel-at-a-time model — several panels are mounted at once.</p>
<h2 id="the-tick-column">The Tick Column</h2>
<p>The tree runs a strict tick strategy, and a node hides its checkbox unless it is a mission command or
a mission section that carries its own enable flag. Writing the ticked set walks every command node at
any depth: a command whose state changed is PUT to the Mission API and, on success, has the new state
copied onto its section children; a section whose state changed is dispatched to its kind's writer. A
failed write raises a toast and leaves the model untouched, so the getter re-derives the old value and
the box snaps back.</p>
<p>A row dims when it is itself disabled — a disabled command, or a section whose own flag is off — or
when an ancestor command is disabled, which is how a disabled command greys its whole subtree while
each descendant keeps its own state. The switch decides only whether the command runs: a disabled
command stays fully editable.</p>
<h2 id="layout">Layout</h2>
<ul>
<li>Left Dock — both tree pages, one nav-bar button toggling the whole column
<ul>
<li><strong>Control Tree</strong> Expansion Row — collapses in place, keeping its header; stays mounted while
collapsed, so the tree keeps its scroll position
<ul>
<li>Tree — one root row per page: <strong>Execution</strong> or <strong>General Setup</strong>
<ul>
<li>Node Row — the label is a real link to this node's <code>?tree=</code> URL on the page, so the browser's
context menu offers open-in-new-tab and copy-link; a plain left click keeps the in-app
selection instead of navigating, while a modified or middle click opens a tab and leaves the
current selection where it is. No icons are rendered.</li>
<li>Node CheckBox — mission command rows, and mission section rows carrying an enable flag</li>
<li>Execution Status Badge — on the <strong>Execution</strong> root row only</li>
</ul>
</li>
<li>Spinner and &ldquo;Loading project…&rdquo; — shown instead of the tree until the first build lands</li>
</ul>
</li>
<li>Row Divider — a 5 px bar between the rows, shown only while both are open; dragging it upwards
grows the editor row, which keeps a stored pixel height while the tree above absorbs the change,
down to a 120 px editor floor and a 100 px tree floor</li>
<li><strong>Editor</strong> Expansion Row — its header carries the selection breadcrumb, the node path joined with
slashes, and falls back to <strong>Editor</strong>; stays mounted while collapsed
<ul>
<li>Execution Transport Bar — Execution page, while an Execution-scope node is selected</li>
<li>The selected node's editor panel, or &ldquo;Select an item in the Control Tree to edit it here.&rdquo;</li>
</ul>
</li>
</ul>
</li>
<li>Content Column — General Setup page only, one nav-bar button
<ul>
<li>The selected node's large content view, or &ldquo;The selected item has no expanded content.&rdquo;</li>
</ul>
</li>
</ul>
<p>Both dock rows and both column widths are device-local browser preferences, shared by the two pages
through one preferences module; each page keeps its own visibility record, and both pages share one
expansion list because their id spaces do not overlap.</p>
<h2 id="lifecycle">Lifecycle</h2>
<p>The host is created while its page's script runs and provided before the dock mounts; the page's
<code>onMounted</code> then awaits <code>initialize()</code>. That call connects the cleanup hub and builds the tree — on
the equipment host, after re-indexing the machine tool, the workpiece and the fixture in that order —
and finishes by adopting whatever the URL's <code>?tree=</code> names. A loading flag is held for the whole call,
which is what the tree and editor rows render their spinners from; later rebuilds mutate an
already-populated tree and never re-enter it. A missing project, or a project without a fixture or
workpiece, answers 404, and those are treated as empty states rather than errors, so no toast appears.
Without an open project the Mission, Program and controller branches are still declared, as childless
stems: their builders run only once a project is loaded.</p>
<p>Because the layout keys its keep-alive wrapper on a project epoch, every page is destroyed and rebuilt
when the project changes, and a host is therefore created once per project per page.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — the host: the two scopes' root
builders, the selection gate, the <code>?tree=</code> sync, the mission ticks, the rebuild entry points, the
re-index and Object-Management handlers, and the reactive surface the panes consume.</li>
<li><code>wwwroot-src/src/components/controlTree/itemTypes.ts</code> — the <code>ControlNode</code> and <code>TreeItemDef</code> shapes,
the <code>SlotCtx</code> hooks, the structure-change payload, <code>nodeDisplayLabel</code> / <code>nodeDisplayInfo</code>, the
geometry and transformer builders, the <code>ITEM_TYPES</code> map and <code>buildSubtree</code>.</li>
<li><code>wwwroot-src/src/components/controlTree/ControlTreePanel.vue</code> — the tree column: the node anchors,
the click handling that keeps a plain click in-app, and the execution-status badge.</li>
<li><code>wwwroot-src/src/components/controlTree/ControlTreeDock.vue</code> — the two-row dock and the height
divider that sets the editor row's stored pixel height.</li>
<li><code>wwwroot-src/src/components/controlTree/PrimarySlavePanel.vue</code> — the panel host: the three inline
root types, the registry lookup, the remount key, the gate registration and the transport header.</li>
<li><code>wwwroot-src/src/components/controlTree/ContentSlavePanel.vue</code> — the General Setup content column
and its unkeyed content panel.</li>
<li><code>wwwroot-src/src/components/controlTree/GroupInfoPanel.vue</code> — a group stem's intro and its
navigating child list.</li>
<li><code>wwwroot-src/src/components/controlTree/SoleEditorPanel.vue</code> — a kind node's editor, resolved from
the geometry and transformer editor maps.</li>
<li><code>wwwroot-src/src/components/controlTree/NodeTabCascade.vue</code> — the same branches rendered as nested
tabs, with the remembered per-role tab and the cascade host injection.</li>
<li><code>wwwroot-src/src/components/controlTree/missionItemTypes.ts</code> — the Mission wave: node bookkeeping,
the section table and its enable-flag readers and writers, the per-kind editors and display names.</li>
<li><code>wwwroot-src/src/components/controlTree/programItemTypes.ts</code> — the Program wave: the file tree from
one response, and the writeback conversion nodes.</li>
<li><code>wwwroot-src/src/components/controlTree/softNcItemTypes.ts</code> — the controller wave: the two planes,
the core leaves every runner grows, and the snapshot flags that decide which brand leaves join them.</li>
<li><code>wwwroot-src/src/components/controlTree/toolHouseItemTypes.ts</code> — the Tool House wave and its
<code>tool-&lt;id&gt;</code> role paths.</li>
<li><code>wwwroot-src/src/components/controlTree/runnerSuitItemTypes.ts</code> — the two runner-suit leaves.</li>
<li><code>wwwroot-src/src/components/controlTree/MissionCommandSlavePanel.vue</code> — the command control bar, and
the only panel that exposes a dirty-switch gate.</li>
<li><code>wwwroot-src/src/components/mission/ScriptCommandPanel.vue</code> — the autosaving editor whose gate is
forwarded, with its save / discard / cancel prompt.</li>
<li><code>wwwroot-src/src/components/panels/PanelExpansion.vue</code> — the expansion row both dock rows are built
from.</li>
<li><code>wwwroot-src/src/composables/useViewPrefs.ts</code> — the device-local expansion list, last selections,
dock sizes and the two runner-suit checkboxes.</li>
<li><code>wwwroot-src/src/composables/useCleanupHub.ts</code> — the key registration the builders call.</li>
<li><code>wwwroot-src/src/router/treeRoutes.ts</code> — the id migration hop and the page a given id belongs to.</li>
<li><code>wwwroot-src/src/pages/ExecutionPage.vue</code> — creates and provides the <code>execution</code> host.</li>
<li><code>wwwroot-src/src/pages/GeneralSetupPage.vue</code> — creates and provides the <code>equipment</code> host, and adds
the content column.</li>
<li><code>wwwroot-src/src/pages/ToolHousePage.vue</code> — the registry consumer that renders tabs instead of a
tree.</li>
<li><code>wwwroot-src/src/layouts/MainLayout.vue</code> — the project epoch that decides when a host is rebuilt.</li>
<li><code>wwwroot-src/src/i18n/en/tree.ts</code> — the dock labels, the group intros and the node role labels.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="tree-ids-and-routes.html">Tree Ids and Routes</a> — how a node id becomes a URL, which page owns a given id, and how
an older id keeps resolving</li>
<li><a class="xref" href="session-state.html">Session State</a> — the project epoch and the stores a host is rebuilt around</li>
<li><a class="xref" href="../execution/index.html">Execution Page</a> — the first live host: the run cockpit's Mission and Program branches</li>
<li><a class="xref" href="../general-setup/index.html">General Setup Page</a> — the second live host: the equipment branches and the content column</li>
<li><a class="xref" href="../general-setup/controller/index.html">Controller Branch</a> — the one branch whose node set is not fixed: it is regrown from a
snapshot, so which children exist depends on the controller in the project</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>
@@ -0,0 +1,422 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Internationalization | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Internationalization | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-I18n">
<h1 id="internationalization">Internationalization</h1>
<p>The web client ships three locales — <code>en</code>, <code>zh-Hant</code> and <code>zh-Hans</code> — over one message shape, and it
owns no route: the mechanism is a vue-i18n instance created in the <code>i18n</code> module, a boot file that
picks the locale before the application mounts, a single switch function every locale change goes
through, and two Node scripts that gate the production build. English is not one locale among three;
it is the schema the other two are written against and the text every unlocalized path falls back to.
The user-facing gesture that triggers a switch is documented under
<a class="xref" href="../shell/language-selection-submenu.html">Language Selection SubMenu</a>, and the vocabulary the
translations are held to under <a class="xref" href="../conventions/translation-remarks.html">Translation Remarks</a>.</p>
<h2 id="the-three-bundles">The Three Bundles</h2>
<p>Each locale is a folder under the i18n directory holding one file per namespace plus an index file
that re-exports them as a single object. The three folders carry identical file lists — the same
twenty-three namespaces, from <code>apiErrors</code> and <code>auth</code> through <code>tree</code> and <code>widgets</code> — because a
namespace's keys are added to all three locales in the same change. Keys are named
<code>namespace.area.element</code> in lowerCamelCase and are split by the UI region they serve rather than by
the source file that reads them.</p>
<p>English is the schema, mechanically and not by convention. <code>wwwroot-src/src/i18n/schema.ts</code> exports
<code>MessageSchema = typeof en</code>, and each Chinese locale's index file annotates its own message object
with that type, so a key missing from a Chinese namespace is a type error at the annotation rather
than a silent English fallback at runtime. <code>schema.d.ts</code> augments vue-i18n's <code>DefineLocaleMessage</code> with the
same shape, which registers it as the global message schema for call sites. Type checking is not part
of the production build, however: <code>wwwroot-src/package.json</code> runs <code>vue-tsc</code> under a separate <code>lint</code>
script, and <code>build</code> runs the two i18n scripts described below instead.</p>
<p>The instance is created with <code>legacy: false</code>, an initial <code>locale</code> of <code>en</code> and <code>fallbackLocale: 'en'</code>.
The resolution chain is therefore current locale → English → the key string itself, so a key missing
from a Chinese bundle renders English with no runtime signal, and a key missing everywhere renders as
its own dotted name.</p>
<p>Quasar's own component texts are a parallel bundle with different code names, mapped in the same
module: <code>en</code> to Quasar's <code>en-US</code> pack, <code>zh-Hant</code> to <code>zh-TW</code>, <code>zh-Hans</code> to <code>zh-CN</code>. All three packs are
static imports rather than a dynamic import built from the locale code, because a template-literal
import on a bare package specifier is not statically analysable by the bundler.</p>
<h2 id="applying-a-locale">Applying a Locale</h2>
<p><code>applyLocale(code)</code> in the i18n module is the only place a locale change happens. It first maps its
argument through <code>normalizeLocale</code>, falling back to <code>en</code>, and then does six things in one call:</p>
<ul>
<li>sets the vue-i18n instance's <code>locale</code> ref;</li>
<li>calls Quasar's <code>Lang.set()</code> with the mapped language pack;</li>
<li>writes <code>document.documentElement.lang</code>, which is what selects the CJK glyph variant;</li>
<li>rebuilds the exported <code>collator</code>, an <code>Intl.Collator</code> for the new locale with base sensitivity and
numeric ordering;</li>
<li>writes the locale into <code>localStorage</code> under <code>hinc.lang</code>, inside a <code>try</code>/<code>catch</code> because storage can
be unavailable;</li>
<li>calls the re-title hook the router registered, so a browser tab parked on a page without navigating
still gets its title in the new language.</li>
</ul>
<p><code>normalizeLocale</code> maps any language tag — the server's, the browser's, or a stored one — onto a
supported locale or <code>null</code>. An exact match wins; otherwise a tag beginning <code>en</code> becomes <code>en</code>, and a
tag beginning <code>zh</code> becomes <code>zh-Hant</code> when it carries the <code>hant</code> script subtag or a <code>TW</code>, <code>HK</code> or <code>MO</code>
region, and <code>zh-Hans</code> otherwise. Anything else yields <code>null</code>. <code>applyLocale</code> and the boot file's
browser-language fallback each turn that <code>null</code> into <code>en</code>; the boot file's read of the stored value
instead treats it as a cache miss and asks the server, so an unsupported stored code takes the
cold-start path rather than pinning English.</p>
<p>Only two modules call <code>applyLocale</code>: the boot file, and the app-state store's language action.
The store updates its own <code>languageCode</code> optimistically, POSTs the new value, adopts the <code>current</code> and
<code>available</code> the server echoes back, and only then applies the locale — so the UI text flips after the
server has accepted the value, and a failed POST rolls the store reference back without any visible
language change.</p>
<h2 id="boot">Boot</h2>
<p><code>wwwroot-src/quasar.config.ts</code> lists three boot files in the order <code>auth</code>, <code>i18n</code>, <code>routine-toast</code>.
The i18n file is placed after <code>auth</code> because <code>auth</code> patches the global <code>fetch</code> for the 401 login gate,
and the language request the i18n file makes must go through that patch on builds where login is
enabled.</p>
<p>The boot file installs the plugin and then reads the <code>hinc.lang</code> value:</p>
<ul>
<li><strong>Cache hit.</strong> The cached locale is applied immediately, so the first painted frame is already
translated, and a reconcile against the server is fired without being awaited. The server value is
the source of truth, so a disagreement — the language was changed from another browser — repaints
shortly after mount.</li>
<li><strong>Cache miss.</strong> The file returns a promise racing the language request against a four-second
timeout, and mount waits on it. A dead backend rejects quickly; a hung one is capped by the timeout
rather than blanking the application. Either failure falls back to the normalized browser language,
and then to English.</li>
</ul>
<p>Every locale step is individually guarded, because a boot file that throws does not degrade to
English — the client entry logs the error and never mounts. The one unguarded step is installing the
plugin itself, without which nothing in the application can render a translated string at all. The
worst case the guards preserve is an application that mounts in English.</p>
<p>The shipped <code>wwwroot-src/index.html</code> is static and carries <code>lang=&quot;en&quot;</code>, so the very first painted
frame always reports English on the root element; <code>applyLocale</code> corrects it in the same tick as mount.</p>
<h2 id="what-re-renders">What Re-Renders</h2>
<p>A locale change is a write to one reactive ref, so everything that reads a translation inside a
render or a computed re-evaluates on its own. Five things follow it in practice:</p>
<ul>
<li><strong>Component text.</strong> Every <code>t()</code> call in a template or computed.</li>
<li><strong>Quasar's built-in texts</strong>, through the language pack the same call sets.</li>
<li><strong>The tab title.</strong> Route records carry an i18n <em>key</em> in <code>meta.title</code>; the router resolves it against
the active locale and composes the title from it. It re-resolves both after a navigation and from
the hook <code>applyLocale</code> calls.</li>
<li><strong>Control-Tree node labels</strong>, described below.</li>
<li><strong>Engine session messages</strong>, which are re-resolved because the localizing helper reads the locale
ref, so rows built inside a computed rebuild on a switch.</li>
</ul>
<p>File Explorer sorting is the one thing that does <em>not</em> follow on its own. The shared <code>collator</code> is a
live <code>let</code> binding rather than a reactive one: <code>applyLocale</code> rebuilds it for the new locale, but no
render tracks it and <code>FileExplorer.vue</code> watches no locale, so a listing already on screen keeps its
pre-switch order until the next folder load or sort-spec change re-runs the comparison. Because the
binding is live, a caller must invoke <code>collator.compare(...)</code> in place; capturing the instance or
extracting its <code>compare</code> would pin the pre-switch locale for good. <code>explorerSort.ts</code> is its only
consumer, and no bare <code>localeCompare</code> — whose <code>undefined</code> locale argument means the <em>browser</em> locale
— is called anywhere in the client.</p>
<p>Numbers deliberately do not follow the locale. <code>Intl.NumberFormat</code> is used nowhere in the client, and
the only <code>toLocale*</code> calls are two triangle counts and two timestamps, each passed the app locale
explicitly rather than defaulting to the browser's. NC and machine-tool quantities go through no
locale-aware number formatting at all, so a coordinate never picks up a comma decimal separator from
a locale that uses one.</p>
<h2 id="control-tree-node-labels">Control-Tree Node Labels</h2>
<p>A Control-Tree node carries both a <code>label</code> and an optional <code>labelKey</code>, and <code>nodeDisplayLabel(n)</code> in
<code>itemTypes.ts</code> returns the translation of <code>labelKey</code> — with the node's <code>labelParams</code> interpolated —
whenever one is set, and the verbatim <code>label</code> otherwise. <code>nodeDisplayInfo</code> applies the same rule to a
group stem's intro text through <code>infoKey</code> over <code>info</code>. The translator function is read inside the call
rather than captured at build time, so the computed that maps nodes onto the rendered tree tracks the
locale ref and the whole tree's role labels change on a switch.</p>
<p>The two fields overlap on purpose. <code>label</code> is required on the node type and <code>labelKey</code> is optional,
so a fixed role label is written both ways: the key that actually renders, and the verbatim English
sitting beside it as the fallback <code>nodeDisplayLabel</code> returns when no key is set. <code>label</code> is also the
only storage for text that can never be a key at all — server-composed mission titles, file-backed
geometry paths, and engine type names. It is the first of those two jobs, not the second, that makes
the verbatim English on node labels the largest single class in the census allowlist: fixed role
labels the census can see but the screen never shows, classified rather than removed because the
node type requires the field.</p>
<p>Mission command nodes are the one family that never takes the key path. A node's label is the title
the server composes for the command — the kind name in the request language, with the command's own
text in brackets when there is one — and <code>Missions/MissionController.cs</code> fills that title in for every
entry: from the command's own composition where it implements the engine's title contract — whose rule
keeps the kind name at the front and never drops it — and from the kind's display name, resolved in the
request culture, where it does not. Because a title is therefore always present, the node's
<code>labelKey</code> is always dropped and the client's kind-name fallback never fires. Since the composition
happens on the server per request, the tree host watches the locale and re-reads the command entries,
rewriting those labels in place; it deliberately does not rebuild the branch, which would remount an
open command editor mid-edit. The kind keys are still built as <code>tree.mission.kind.${commandType}</code>, and
they do render — <code>AddCommandDialog.vue</code> prefers one over the server's label for every kind that has
one — while the key lint resolves the template shape by wildcard rather than reporting an orphan.</p>
<h2 id="the-server-half">The Server Half</h2>
<p>The language preference is server-held. <code>GET /api/preference/language</code> answers
<code>{ success, current, available }</code>, where <code>current</code> is the persisted <code>UserConfig.LanguageCode</code> —
default <code>en</code> — and <code>available</code> is the server's own supported list, <code>en</code>, <code>zh-Hans</code>, <code>zh-Hant</code>. <code>POST</code>
rejects an unlisted code with 400 and otherwise writes the value and saves the user configuration,
which round-trips through the XML user config file. The <code>localStorage</code> key holds only a paint-time
hint and loses every disagreement with this value.</p>
<p>Some response text is composed on the server and never passes through a bundle.
<code>PresentCatalogService</code> resolves the effective language per request in this order: an explicit <code>?lang=</code>
query, then the request's <code>Accept-Language</code> header, then the persisted preference, then English.
Because the header sits <em>above</em> the saved preference, a browser set to a different language than the
application would win by default — so the client sends <code>?lang=</code> explicitly, from a <code>currentLang()</code>
helper returning the app locale, on the endpoints whose text is localized server-side: the
selected-step info, the execution strip chart, its step-property picker and its colour guide, the
mission command entries, catalog and field descriptors, and the step-present key list. The mission
controller resolves the same
chain into a per-request UI culture and injects it into the command text source, so the culture rides
on the request rather than on the thread.</p>
<p>Two further server-originated string classes are localized on the client instead:</p>
<ul>
<li><strong>Coded API errors.</strong> Ten error codes are mapped onto <code>apiErrors.*</code> keys; a failed response carrying
one of them renders the bundle value with the server's interpolation arguments. English is passed
straight through to the server's own message on purpose, because that message <em>is</em> the English
rendering and keeps per-site nuance the generic bundle value flattens.</li>
<li><strong>Engine notifications.</strong> Session messages arrive with a structured id and English text. The client
swaps in an <code>engineMessages.&lt;id&gt;</code> bundle entry, but only when the wire text agrees with the English
bundle entry for that id: a templated message must match the canonical English template exactly
before its arguments are re-interpolated, and an untemplated one is swapped only when the bundle
entry has no interpolation holes. Any mismatch — an id emitted with different templates at different
sites, or an engine built against a different bundle — falls back to the wire English rather than
rendering wrong text. The English <code>engineMessages</code> values are therefore load-bearing code, not a
description of the Chinese ones.</li>
</ul>
<p>The step-present labels have a third home again: they ship beside the executable as
<code>catalog.{lang}.json</code> files that overlay translations on top of the live English attribute data.
English never comes from those files, and a missing file, a missing key or a parse failure simply
means English.</p>
<h2 id="the-glossary-and-the-build-gate">The Glossary and the Build Gate</h2>
<p>Two Node scripts stand between the bundles and a shipped release. <code>wwwroot-src/package.json</code> defines
<code>lint:i18n</code> as <code>census.mjs</code> followed by <code>lint-glossary.mjs</code>, and <code>build</code> as <code>lint:i18n</code> followed by
<code>quasar build</code> — so either script failing stops the production build. The development server runs
neither.</p>
<p><code>lint-glossary.mjs</code> bundles each locale's index through esbuild, flattens the three message trees into
key/value maps, and applies four checks, every one of them a hard failure:</p>
<ol>
<li><strong>Key isomorphism.</strong> The three flattened key sets must be identical, compared in both directions,
so an extra key in a Chinese bundle fails as loudly as a missing one.</li>
<li><strong>Bidirectional key integrity.</strong> Every bundle-shaped string literal under the source tree —
excluding the bundles themselves — must resolve to an English key, and every English key must be
referenced from somewhere. A literal naming a namespace prefix counts as a reference to everything
beneath it; a template literal with an interpolation hole is expanded into a wildcard, which is how
<code>tree.mission.kind.${kind}</code> is matched; and <code>@:linked.key</code> references inside bundle values count as
well. A double-quoted literal in a Vue template is treated as a key only when its attribute is one
of the four the check knows to be key-valued, so an expression attribute whose local variable
happens to share a namespace name is not mistaken for one. The orphan half is what makes the check
bidirectional, and its allow-list is currently empty.</li>
<li><strong>Shape preservation.</strong> Never-translate tokens present in an English value must survive verbatim in
both Chinese values — product and brand names, file formats and unit symbols by literal match, and
NC codes, controller parameter names and function-key shortcuts by pattern. Placeholder sets must
match in both directions, so a Chinese value can neither lose a placeholder nor invent one.</li>
<li><strong>Per-locale vocabulary.</strong> An adjudicated forbidden-term table per locale, each entry naming its
replacement and its reason, plus a check for a Chinese value identical to its English source that
contains no CJK at all. Both carry narrow exemptions: named keys where a term is used in a
different sense, and a small identity set for values that legitimately are the identifier.</li>
</ol>
<p>The tables the third and fourth checks read are written at the top of the script itself.
<code>glossary.yaml</code>, the large harvested term list that sits beside the bundles, is generated data that
the application never imports and the lint never reads: it is where a reading is settled before it is
written into a bundle, not an input to the gate.</p>
<p><code>census.mjs</code> guards the opposite direction — text that never became a key. It scans the same tree for
user-visible English string literals reached through display-named template attributes, display-named
object properties, notification and dialog shorthands, and prose text nodes; subtracts a classified
allowlist of concrete text-and-file pairs; and compares what is left, per file, against a baseline.
The gate fails only when a file's residual count <em>rises</em>, so cleanup can land incrementally — but the
baseline is currently empty, which allows zero residual strings in any file and makes the effective
rule that a newly hard-coded display string fails the build unless it is added to the allowlist with
a reason.
Allowlist entries whose file field is a glob or a brace list are classification notes rather than
matchers; only concrete paths participate.</p>
<h2 id="strings-outside-the-bundles">Strings Outside the Bundles</h2>
<p>Three classes of user-visible English render in every locale — two because an interpolation hole
hides them from the census, which discards any literal containing one, and one because the allowlist
deliberately keeps it:</p>
<ul>
<li><strong>The shared numeric field's validation text.</strong> <code>NumericInput.vue</code> imports nothing from vue-i18n and
builds three messages as template literals directly: an invalid-number message quoting what was
typed, a &ldquo;must be ≥&rdquo; message naming the field's minimum, and a &ldquo;must be ≤&rdquo; message naming its
maximum. All three are produced on blur — the field commits on blur or on Enter — and every panel
that embeds the widget shows them in English. A <code>widgets</code> namespace exists for exactly this kind of
shared control text; this component does not use it.</li>
<li><strong>The geometry and topology editors' error prefixes.</strong> Those editors emit their failure context as a
template literal that leads with the type name and the operation, so the leading phrase is English
whatever the locale. The allowlist records the family as a ledger note rather than as matched
entries.</li>
<li><strong>The API-error default.</strong> The generic fallback message in the HTTP helper is allowlisted
deliberately, on the grounds that it sits in the same stream as the server's own English messages.</li>
</ul>
<p>A fourth class is untranslated by design rather than by escape: the dynamic <code>label</code> text on a
Control-Tree node — a file-backed path, an engine type name, and the user's own words inside a
server-composed mission title — and the identity strings the allowlist classifies alongside it, which
are backend type names rendered inside a translated sentence, axis and unit column headers, and brand
names. The role-label half of that same allowlist class is a different case again: stored English that
never reaches the screen at all, because a <code>labelKey</code> always resolves ahead of it.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>wwwroot-src/src/i18n/index.ts</code> — the vue-i18n instance and its fallback chain, <code>SUPPORTED_LOCALES</code>,
<code>normalizeLocale</code>, <code>applyLocale</code>, the Quasar language-pack map, the shared collator, the storage key
and the re-title registration.</li>
<li><code>wwwroot-src/src/i18n/schema.ts</code> — the English bundle as <code>MessageSchema</code>.</li>
<li><code>wwwroot-src/src/i18n/schema.d.ts</code> — the ambient augmentation that registers that shape as
vue-i18n's global message schema.</li>
<li><code>wwwroot-src/src/i18n/en/index.ts</code> — the English bundle: the twenty-three namespaces assembled into
one object.</li>
<li><code>wwwroot-src/src/i18n/zh-Hant/index.ts</code> and <code>wwwroot-src/src/i18n/zh-Hans/index.ts</code> — the same
assembly, each annotated with <code>MessageSchema</code>.</li>
<li><code>wwwroot-src/src/i18n/en/engineMessages.ts</code> — the engine-notification renderings keyed by structured
id; its English values are the eligibility gate for every swap.</li>
<li><code>wwwroot-src/src/i18n/en/routes.ts</code> — the strings the routes' <code>meta.title</code> keys resolve to.</li>
<li><code>wwwroot-src/src/i18n/en/widgets.ts</code> — the shared-widget namespace.</li>
<li><code>wwwroot-src/src/i18n/README.md</code> — the invariants written beside the bundles: the fallback rule, the
never-translate list, the number and sorting rules and the vocabulary table.</li>
<li><code>wwwroot-src/src/i18n/glossary.yaml</code> — generated term data, imported by nothing.</li>
<li><code>wwwroot-src/src/boot/i18n.ts</code> — the plugin installation, the cached-locale fast path with its
unawaited reconcile, the awaited race on a cache miss, and the guards around every locale step.</li>
<li><code>wwwroot-src/quasar.config.ts</code> — the boot list whose order places i18n after the authentication
fetch patch.</li>
<li><code>wwwroot-src/index.html</code> — the static shell whose <code>lang</code> attribute is corrected at mount.</li>
<li><code>wwwroot-src/package.json</code> — <code>build</code> as the i18n scripts followed by <code>quasar build</code>, and <code>vue-tsc</code>
as a separate script.</li>
<li><code>scripts/lint-glossary.mjs</code> — the four bundle checks and the adjudication tables they read.</li>
<li><code>scripts/census.mjs</code> — the raw display-string extraction, the allowlist subtraction and the per-file
ratchet.</li>
<li><code>scripts/i18n-allowlist.json</code> — the classified exemptions, each with its reason.</li>
<li><code>scripts/i18n-baseline.json</code> — the per-file residual ceiling the census compares against.</li>
<li><code>wwwroot-src/src/stores/appState.ts</code> — the language reference, the available-code list, and the
action that POSTs before applying the locale and rolls back on failure.</li>
<li><code>wwwroot-src/src/api/preference.ts</code> — the typed wrappers over the language endpoints and the
<code>?lang=</code>-bearing step-present key request.</li>
<li><code>wwwroot-src/src/api/http.ts</code> — <code>currentLang()</code>, the coded-error map and the app-locale rendering
that skips English.</li>
<li><code>wwwroot-src/src/api/sessionMessages.ts</code> — the engine-notification localizer, its template-equality
gate and the repeat-fold wrapper.</li>
<li><code>wwwroot-src/src/components/controlTree/itemTypes.ts</code> — the node's <code>label</code>, <code>labelKey</code> and
<code>labelParams</code> fields and the <code>nodeDisplayLabel</code> and <code>nodeDisplayInfo</code> resolvers.</li>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — the rendered-tree computed that
calls the resolver, and the locale watcher that re-pulls the server-composed mission command titles
without rebuilding the branch.</li>
<li><code>wwwroot-src/src/components/controlTree/missionItemTypes.ts</code> — the kind display names and their
<code>tree.mission.kind.*</code> key twin, and the rule that drops the key whenever the entry carries a title.</li>
<li><code>wwwroot-src/src/components/widgets/NumericInput.vue</code> — the shared numeric field: blur-and-Enter
commit, and the three hard-coded validation messages.</li>
<li><code>wwwroot-src/src/components/AppMenuBar.vue</code> — the language submenu, its hard-coded self-name map and
the action it calls.</li>
<li><code>wwwroot-src/src/components/explorerSort.ts</code> — the only consumer of the shared collator, calling it
in place so a switch is picked up.</li>
<li><code>wwwroot-src/src/router/index.ts</code> — the key-valued <code>meta.title</code> resolution and the hook the locale
switch calls to re-title a parked tab.</li>
<li><code>Environments/PreferenceController.cs</code> — the language endpoints, the supported-code list and the
400 on an unlisted code.</li>
<li><code>Environments/UserConfig.cs</code> — the persisted language code, its English default and its XML
round-trip.</li>
<li><code>Environments/PresentCatalogService.cs</code> — the per-request language resolution chain, the tag
normalizer and the shipped catalog overlay.</li>
<li><code>Missions/MissionController.cs</code> — the per-request UI culture taken from the same chain and injected
into command title composition.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="../shell/language-selection-submenu.html">Language Selection SubMenu</a> — the menu gesture that drives this mechanism, and the only
place a user changes the locale</li>
<li><a class="xref" href="../conventions/translation-remarks.html">Translation Remarks</a> — the terminology contract the bundles are written to, and the
readings the vocabulary lint enforces</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>
@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Platform | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Platform | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-Platform">
<h1 id="platform">Platform</h1>
<p>The machinery every route sits on and no user can point at. A page in this folder documents
something that has no owning screen: the Control-Tree engine two pages share, the id and route
surface three pages hang off, the state that survives a navigation, the locale bundles behind every
label, the gate in front of the whole app, and the host process under all of it.</p>
<p>The boundary with <a href="../conventions/index.html">Conventions</a> is which side of the page the reader
is on. Conventions holds a rule a page author must <strong>follow</strong>; Platform holds machinery a page
author must <strong>understand</strong> and never touches.</p>
<p>Two of these are ordinary routes rather than mechanisms — <code>/login</code> and <code>/preference/log</code>. They sit
here because what they document is the platform surface, not a step in a machining workflow.</p>
<p>Ordered from the machinery a page sits closest to, outward to the host process beneath it.</p>
<h2 id="pages">Pages</h2>
<ul>
<li><a href="control-tree.html">Control Tree</a> — The engine both tree pages share: the node shape, the item-type registry, branch building, the selection gate and the panel host</li>
<li><a href="tree-ids-and-routes.html">Tree Ids and Routes</a> — The URL surface: the route table, the redirect-only paths, and how a <code>?tree=</code> id is migrated and landed on the page that owns it</li>
<li><a href="session-state.html">Session State</a> — The project epoch behind the keep-alive, the stores and module singletons outside it, and what is device-local</li>
<li><a href="i18n.html">Internationalization</a> — Three locales over one English schema: the bundles, the single switch point, the build-gating lint, and the strings that never reach a bundle</li>
<li><a href="login-and-auth.html">Login and Authentication</a> — The optional login gate: the sign-in screen, the config switch that turns it on, the cookie scheme and fallback policy it registers, and the router guard in front of them</li>
<li><a href="log-viewer.html">Log Viewer Page</a> — The read-only <code>/preference/log</code> screen: today's server log file, its auto-refresh, and the daily file sink behind it</li>
<li><a href="program-and-hosting.html">Program and Hosting</a> — The ASP.NET Core host: the HiAPI start/stop pair, the all-singleton container, the SignalR hubs, the SPA fallback, and the configuration that pins the listening address</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a href="../conventions/index.html">Conventions</a> — the other cross-screen folder: the rules an author follows rather than the machinery an author reads</li>
<li><a class="xref" href="../by-source/index.html">Anatomy by Source Directory</a> — the inverse index, for entering by the directory a change landed in</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>
@@ -0,0 +1,318 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Log Viewer Page | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Log Viewer Page | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-LogViewer">
<h1 id="log-viewer-page">Log Viewer Page</h1>
<p>The Log Viewer is the read-only screen at <code>/preference/log</code>, route name <code>preference-log</code>, that
renders the web service's own log file for the current day. It is not a Control-Tree page and takes
no <code>?tree=</code> query: the whole page is one toolbar over one scrolling text area. Despite the route
path it is not an entry of the Preference dropdown, and it is not under <code>/util/</code>.</p>
<h2 id="reaching-the-page">Reaching the Page</h2>
<p>The only navigation into the page is the <code>Show Log</code> button on the menu bar. It sits in the bar's
right-hand group, after the active-page title and its separator and before the logout button, and it
carries the <code>article</code> icon with the tooltip <code>View the application log</code>. The button is rendered
unconditionally — no route, project, licence or authentication state hides it — so it is reachable
from every page in the shell. The Preference dropdown carries the Language submenu, the CSV and CL
Controller visibility checkboxes and Show Physics Options, and no log entry; the Page dropdown does
not list the route either.</p>
<p>The route sits inside the main layout, so the page renders under the same menu bar and footer as
every other page, and it is behind the login gate whenever that gate is on — the router's navigation
guard admits only the login route itself while the session is unauthenticated. The route carries an
i18n title key, so the browser tab reads <code>Log Viewer - HiNC</code> and the menu bar's active-page
indicator reads <code>Log Viewer</code>.</p>
<h2 id="what-it-reads">What It Reads</h2>
<p>Two endpoints, both on the project controller, and both served from the same file:</p>
<table>
<thead>
<tr>
<th>Request</th>
<th>Answer</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GET /api/project/logs</code></td>
<td><code>{ date, content }</code> for today's file; <code>404</code> with a message when there is none</td>
</tr>
<tr>
<td><code>GET /api/project/download-log</code></td>
<td>the same file as a <code>text/plain</code> attachment; <code>404</code> with the same message when there is none</td>
</tr>
</tbody>
</table>
<p>Both compose <code>logs/log-{yyyy-MM-dd}.txt</code> beneath the process's current working directory and date the
name from the server's local clock. Neither supports a range or a tail: the whole file is read into
memory and returned in full on every call, so an auto-refresh at a short interval re-transfers the
entire log each tick. Any failure inside either handler answers <code>400</code> with the exception message.</p>
<p>The two client wrappers part company on that <code>404</code>. The wrapper over <code>logs</code> normalises it into an
empty result rather than an error, which is what gives the page a clean &ldquo;no log yet&rdquo; state
instead of a red panel; every other non-OK status there is raised as a typed error carrying the HTTP
status and the response body. The download wrapper normalises nothing — <code>404</code> included, every non-OK
status becomes that same typed error, a path the disabled-without-content <code>Download</code> button makes
hard to reach. Because the controller declares no anonymous carve-out, both endpoints fall under the
authenticated-user fallback policy when the login gate is enabled.</p>
<p>The date badge shows whatever date the last answer reported. On the <code>404</code> path that date is not the
server's — the client fills it from the browser's own clock in UTC — so with no log file present the
badge can read a different day than the file name the server would have used.</p>
<h2 id="refresh-and-the-auto-interval">Refresh and the Auto Interval</h2>
<p>The page loads once on mount. After that every reload is driven by one of three things: the
<code>Refresh</code> button, the <code>Retry</code> button inside the error panel, or the auto-refresh timer.</p>
<p>Auto-refresh is off by default. The <code>Auto</code> toggle arms it and the interval select beside it chooses
the period from a fixed list of <code>2 s</code>, <code>5 s</code>, <code>10 s</code> and <code>30 s</code>, defaulting to five seconds; the
select is disabled while the toggle is off, but it keeps its value. The period is picked from that
dropdown rather than typed, so it takes effect on selection. A change to either control tears the
existing timer down, and a fresh one starts only while the toggle is still armed: an interval change
therefore takes effect immediately but also restarts the countdown, while disarming the toggle
simply stops the timer. Arming the toggle schedules the first automatic load one full interval
later; it does not fetch straight away.</p>
<p>Ticks are not coalesced with a request already in flight — each tick starts the same load
unconditionally.</p>
<p>Neither the toggle nor the interval is persisted. They are page-local state, held in neither the
browser's local storage nor the server-side user preferences, so a browser reload returns both to
their defaults.</p>
<h3 id="the-page-is-kept-alive">The Page Is Kept Alive</h3>
<p>The shell caches routed pages in a keep-alive keyed on the project epoch. Leaving the Log Viewer
therefore deactivates it rather than unmounting it, and the page's teardown hook — the one that
stops the timer — does not run. Two consequences follow. An armed auto-refresh keeps polling the log
endpoint in the background after navigation away, until the keep-alive is rebuilt by a project change
or the browser reloads. And returning to the page does not re-run the initial load: the previous
content, its <code>last loaded</code> stamp, the toggle and the chosen interval are all still there, and the
view refreshes on the next tick or on <code>Refresh</code>.</p>
<h2 id="errors-and-scroll-position">Errors and Scroll Position</h2>
<p>A failed load fills the page's error state and also pushes one foreground line onto the shell's
routine-progress footer, so the failure is visible from any page. The error panel takes precedence
over content in the text area, so a failed refresh hides the text that was already loaded — but only
until the next attempt starts, because the error text is cleared before that request goes out. The
content itself is never discarded: it reappears the moment the attempt begins, and the error panel
returns only if that attempt fails as well. With auto-refresh armed against an endpoint that keeps
failing, the text and the panel therefore alternate on every tick.</p>
<p>After a successful load the view scrolls to the bottom, but only while it is already stuck there —
the scroll handler treats a distance of less than 30 px from the bottom as stuck. Scrolling up
therefore freezes the position through an auto-refresh, and scrolling back to the bottom re-arms the
follow behaviour.</p>
<h2 id="level-filtering-and-where-the-file-comes-from">Level Filtering, and Where the File Comes From</h2>
<p>The page applies no filtering of its own. There is no level selector, no category selector, no
search box and no date picker: it renders the file it is given, verbatim, in the order it was
written. Which entries reach the file at all is decided at write time, by the host's logging filters
rather than by anything on this screen — see <a class="xref" href="program-and-hosting.html">Program and Hosting</a> for those rules and how
they differ from the console provider's.</p>
<p>The sink is a daily file logger provider registered at startup against a <code>logs</code> folder under the
process's current working directory. It writes one entry per line as
<code>[yyyy-MM-dd HH:mm:ss.fff] [Level] Category - message</code>, appends the exception on its own lines when
there is one, serialises appends behind a lock and swallows every I/O failure, on the principle that
logging must never fault a request. Logging scopes are not rendered into the file. The application
writes one Information line immediately after the host is built, so the file for the day the service
starts has content before the first request arrives and a freshly started service shows something
rather than an empty state. The sink recomposes the file name from the clock on every append, so a
service left running past midnight has no file for the new day until something is logged, and the
page shows its empty state until then.</p>
<p>Only the current day's file is reachable. Previous days' files stay on disk and the page offers no
way to open one. A file that exists but is empty renders the same empty state as a missing one; only
the date badge can separate the two, since the empty file's date comes from the server's answer and
the missing file's from the browser's UTC clock.</p>
<p>The same controller also exposes a POST endpoint that appends a caller-supplied line to the same
daily file. It writes with its own formatting rather than through the logger provider, and nothing
in the shipped SPA calls it.</p>
<h2 id="copy-download-and-what-the-page-cannot-do">Copy, Download, and What the Page Cannot Do</h2>
<p><code>Copy</code> writes the loaded text to the system clipboard and reports the outcome as a toast, positive
or negative. <code>Download</code> fetches the file as a blob from the download endpoint and saves it
client-side under the name <code>log-&lt;date&gt;.txt</code>, repeating the date the last answer reported, and also
ends in a toast. Both buttons are disabled while there is nothing loaded.</p>
<p>The page cannot clear, truncate, rotate or delete the log — there is no destructive action on it at
all, and no endpoint behind it that would perform one. It cannot reach another day, filter by level
or text, or stream: refreshing is polling over the same whole-file request, with no hub connection
and no server push. It is a viewer.</p>
<h2 id="layout">Layout</h2>
<ul>
<li>Log Viewer Page — a viewport-locked page sized from the layout's measured header and footer, so
the window itself never scrolls
<ul>
<li>Top Tool Bar
<ul>
<li><code>Log Viewer</code> Title</li>
<li>Date Badge — outlined; carries the date the last answer reported, and reads <code>today</code> only
before any answer has set one. Coloured as the primary accent while content is loaded and grey
otherwise</li>
<li>Last-Loaded Caption — <code>last loaded: {time}</code>, shown once a load has succeeded. The time is
formatted in the <strong>application</strong> locale rather than the browser's, so it re-renders on a
language switch; the hover title carries the same instant as an ISO 8601 UTC string</li>
<li>Line Count Caption — shown only while content is loaded, where it reads <code>{count} lines</code>. The
plural string's <code>no lines</code> and <code>one line</code> forms are out of reach: a zero count is exactly the
state that hides the caption, and every writer ends its entry with a newline. The count is the
number of newline-separated segments, so a file ending in a newline counts one more than it
shows, and a multi-line exception counts as several</li>
<li><code>Auto</code> Toggle — arms auto-refresh; off by default. Tooltip <code>Re-fetch the log file periodically</code></li>
<li>Interval Select — <code>2 s</code> / <code>5 s</code> / <code>10 s</code> / <code>30 s</code>, defaulting to <code>5 s</code>; disabled while <code>Auto</code>
is off</li>
<li><code>Refresh</code> Button — reloads at once, and shows a spinner in place of its label while the
request is in flight. Tooltip <code>Reload the log file</code></li>
<li><code>Copy</code> Button — disabled without content. Tooltip <code>Copy log content to clipboard</code></li>
<li><code>Download</code> Button — disabled without content. Tooltip <code>Download today's log file</code></li>
</ul>
</li>
<li>Separator</li>
<li>Log Area — a dark, monospaced, scrolling region filling the rest of the page. Long lines are not
wrapped, so it scrolls in both directions. It shows exactly one of four things:
<ul>
<li>Error Panel — an error icon, the failure message, and a <code>Retry</code> Button that repeats the load</li>
<li>Empty Panel — a document icon, <code>No log file for today.</code>, and the hint
<code>Log file will appear once the webservice writes its first entry.</code></li>
<li>Log Content — the file's text, preformatted and read-only, with no folding, no line numbers and
no per-line affordance of any kind</li>
<li>Loading Spinner — with <code>Loading log...</code> beneath it, shown while a load is in flight and
nothing has been loaded yet</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>wwwroot-src/src/pages/LogViewerPage.vue</code> — the whole page: the toolbar, the four states of the
log area, the auto-refresh timer and its watcher, the stick-to-bottom scroll rule, and the copy
and download handlers.</li>
<li><code>wwwroot-src/src/api/logs.ts</code> — the typed wrappers over the two endpoints, the <code>404</code>-to-empty
normalisation with its browser-clock date fallback, and the error type carrying the HTTP status.</li>
<li><code>wwwroot-src/src/router/routes.ts</code> — the <code>preference/log</code> path, the <code>preference-log</code> route name and
the title key the tab and the menu bar's active-page indicator resolve.</li>
<li><code>wwwroot-src/src/router/index.ts</code> — the login guard the route passes through, and the retitle hook
that names the browser tab.</li>
<li><code>wwwroot-src/src/components/AppMenuBar.vue</code> — the always-visible <code>Show Log</code> button in the bar's
right-hand group, and the Preference dropdown that carries no log entry.</li>
<li><code>wwwroot-src/src/layouts/MainLayout.vue</code> — the keep-alive that caches this page rather than
unmounting it, and the project epoch that rebuilds the cache.</li>
<li><code>wwwroot-src/src/stores/appState.ts</code> — the message action that forwards a load failure to the
footer's foreground channel.</li>
<li><code>wwwroot-src/src/stores/routineProgress.ts</code> — the foreground-message store that failure lands in.</li>
<li><code>wwwroot-src/src/utils/pageFit.ts</code> — the page-height function that locks the page to the measured
viewport so only the log area scrolls.</li>
<li><code>wwwroot-src/src/i18n/en/logs.ts</code> — the page's own English strings: the title, the empty and
loading states, the plural line count and the tooltips.</li>
<li><code>wwwroot-src/src/i18n/en/menu.ts</code> — the <code>Show Log</code> label and its tooltip.</li>
<li><code>wwwroot-src/src/i18n/en/common.ts</code> — the shared <code>Auto</code>, <code>Refresh</code>, <code>Copy</code> and <code>Download</code> labels.</li>
<li><code>wwwroot-src/src/i18n/en/routes.ts</code> — the <code>Log Viewer</code> route title.</li>
<li><code>Environments/ProjectController.cs</code> — the two endpoints the page reads, both resolving today's file
under the working directory, plus the append endpoint nothing calls.</li>
<li><code>Common/DailyFileLoggerProvider.cs</code> — the sink: the per-day file, the entry format, the lock, the
swallowed I/O failures and the null scope.</li>
<li><code>Program.cs</code> — registers that provider and its level filters, and writes the one startup entry
right after the host is built.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="../shell/main-panel.html">Main Panel</a> — the menu bar whose right-hand <code>Show Log</code> button is the only navigation into this page</li>
<li><a class="xref" href="../shell/preference-menu.html">Preference Menu Dropdown</a> — the dropdown this route's path is named after, and which carries no log entry of its own</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>
@@ -0,0 +1,362 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Login and Authentication | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Login and Authentication | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-LoginAndAuth">
<h1 id="login-and-authentication">Login and Authentication</h1>
<p>The sign-in screen is the <code>/login</code> route. Whether anyone ever reaches it is decided by one back-end
setting — the <code>Enabled</code> flag of the <code>Auth</code> configuration section: with the flag off the service is
open and the route sends every visitor straight back out, and with it on a cookie-authentication
scheme plus a global authorization fallback policy lock every hub and every controller but the
authentication one — the whole <code>api/</code> surface apart from <code>api/auth</code> — until a visitor signs in. The
route hosts no Control-Tree branch and takes no <code>tree</code> query argument.</p>
<h2 id="the-enable-switch-and-its-two-defaults">The Enable Switch, and Its Two Defaults</h2>
<p><code>AuthConfig</code> binds the <code>Auth</code> section of configuration and is registered as one instance, so the
authentication controller and the startup decision read the same object. Two different values answer
the question &ldquo;is the gate on by default&rdquo;, and conflating them gets the answer wrong on a real
instance:</p>
<ul>
<li><strong>The class default is off.</strong> <code>Enabled</code> initialises to <code>false</code>, so a service whose configuration
carries no <code>Auth</code> section — or an empty one — runs with no login at all.</li>
<li><strong>The shipped configuration turns it on.</strong> <code>appsettings.json</code> and the Development overlay both set
<code>Enabled</code> to <code>true</code> and both supply one credential entry, so a service started from the repository
as it ships demands a sign-in.</li>
</ul>
<p>Three further settings live in the same section. <code>SessionHours</code> defaults to 8 and is raised to 1 at
registration if a smaller number is configured. <code>SlidingExpiration</code> defaults to <code>true</code>.
<code>Users</code> is a list of username and password pairs and defaults to empty.</p>
<p>That list is the whole credential store. Passwords are held in the configuration file in clear text
and compared verbatim — a first-match scan for an exact username <em>and</em> password — so there is no
hashing, no lockout, no rate limit and no user database behind it. The class documents the clear-text
form as deliberate, because the project also ships as sample code. Every entry grants the same
access; there are no roles. <code>Enabled</code> set to <code>true</code> over an empty list therefore admits nobody.</p>
<h2 id="what-the-gate-registers">What the Gate Registers</h2>
<p>The flag guards exactly two registrations at startup: a cookie authentication scheme, which becomes
the application's default and only scheme, and a global authorization fallback policy requiring an
authenticated user.</p>
<p>The fallback policy is what does the locking. It applies to every endpoint carrying no authorization
metadata of its own — every controller in the host assembly except the authentication one, and all
eight SignalR hub routes. <code>UseAuthentication</code> and <code>UseAuthorization</code> are added to the request pipeline
unconditionally and are no-ops when neither registration happened. See
<a class="xref" href="program-and-hosting.html">Program and Hosting</a> for the pipeline order this sits in.</p>
<p>Three places in the host carry <code>AllowAnonymous</code>, and they are the only ones that do:</p>
<ul>
<li>The authentication controller itself, marked at class level, so all three of its endpoints stay
reachable under the policy that locks everything else.</li>
<li>Both SPA fallbacks, so the client shell — which carries no data of its own — can load and show its
own login screen.</li>
</ul>
<p>Two pipeline stages answer before the authorization stage is reached and are unaffected by the
policy: static-file serving and the Swagger middleware. The first of those is what lets the login
screen render before anyone has signed in — the built bundle and the brand image it draws are
physical files under the web root.</p>
<p>With the gate off, none of this is registered: every controller, every hub and both fallbacks are
open, the login endpoint answers success without inspecting anything, the client's navigation guard
short-circuits, and the login route bounces the visitor away as it mounts.</p>
<h2 id="the-cookie">The Cookie</h2>
<p>The scheme issues a cookie named <code>HiNC.Auth</code>. It is <code>HttpOnly</code>, its <code>SameSite</code> mode is <code>Lax</code>, and its
secure policy is <em>same as request</em> — marked <code>Secure</code> when the request itself arrived over HTTPS and
issued plainly otherwise, so a plain-HTTP run on a local network still works. That decision is one of
the reasons forwarded headers are applied first in the pipeline, the HTTPS-redirection stage being
the other: behind a TLS-terminating reverse proxy the scheme must be read from <code>X-Forwarded-Proto</code>
rather than from the plain hop between proxy and service.</p>
<p>The ticket carries exactly one claim, the name of the matched user. Its lifetime is the configured
session length, refreshed on activity when sliding expiration is left on. Sign-in passes no
authentication properties, so the ticket is not marked persistent and the cookie carries no <code>Expires</code>
attribute: the browser holds it for the browser session, and the ticket's own expiry bounds a session
left open.</p>
<p>One event pair is overridden, and it is what makes a single-page client possible at all. A challenge
answers <strong>401</strong> and a refusal answers <strong>403</strong>, in place of the framework's default redirect to a
server-rendered login page. Every protected call therefore fails as data the client can read rather
than as a 302 that would arrive at the fetch layer as an HTML page.</p>
<h2 id="the-auth-endpoints">The Auth Endpoints</h2>
<p>The controller is routed at <code>api/auth</code> and holds three endpoints.</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>Behaviour</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GET status</code></td>
<td>Reports whether the gate is enabled, whether this caller is authenticated, the caller's name, and a version string. It is anonymous, so it answers before sign-in and whether or not the gate is on.</td>
</tr>
<tr>
<td><code>POST login</code></td>
<td>With the gate off, returns success without inspecting the body. With it on, scans the configured users for an exact match and either answers 401 with an English message plus the stable code <code>InvalidCredentials</code>, or signs the caller in.</td>
</tr>
<tr>
<td><code>POST logout</code></td>
<td>Signs the caller out when the gate is on, and reports &ldquo;not authenticated&rdquo; either way.</td>
</tr>
</tbody>
</table>
<p>The version the status endpoint reports is the HiNc assembly version,
<a class="xref" href="../../api/Hi.MachiningProcs.MachiningProject.html#Hi_MachiningProcs_MachiningProject_ApiVersion_">ApiVersion</a><small>(API)</small>. Because the endpoint is
anonymous, that value is readable before sign-in, which is what lets the login screen and the menu bar
show the same version mark.</p>
<p>Nothing else in the host reads the signed-in identity — the status endpoint is its only consumer. Every
application service is registered as a process-wide singleton — the host adds no scoped or transient
registration of its own — so authentication decides admission rather than identity: two signed-in
browsers drive the same project and the same session. See <a class="xref" href="session-state.html">Session State</a> for what that
shared state consists of.</p>
<h2 id="the-client-side">The Client Side</h2>
<h3 id="the-store">The Store</h3>
<p>A Pinia store holds the whole client-side picture: whether the gate is enabled, whether this session is
authenticated, the user name, the version string, and a ready flag set once the first status probe has
resolved. <code>refresh</code> reads the status endpoint; <code>login</code> and <code>logout</code> call their endpoints and update the
same fields; a fourth action flips the store to logged-out with no round trip, for the interceptor
below. Every transition re-drives the hub gate.</p>
<h3 id="the-navigation-guard">The Navigation Guard</h3>
<p>One global <code>beforeEach</code> guard runs the whole client-side decision, in order:</p>
<ol>
<li>If the store is not ready yet, hydrate it from the status endpoint, inside a <code>try</code>/<code>catch</code> that
swallows the failure. The ready flag is set only on success, so a failed probe is retried on the
next navigation.</li>
<li>If the gate reports disabled, allow the navigation.</li>
<li>If the target is the login route, allow it.</li>
<li>If the session is not authenticated, redirect to the login route with the blocked target's full
path — query arguments, <code>tree</code> included — parked in a <code>redirect</code> argument.</li>
<li>Otherwise allow it.</li>
</ol>
<p><strong>What the guard does not do</strong> is protect anything. It is a navigation redirect: it runs on router
navigations only, so it has no bearing on a direct <code>api/</code> request, on a hub negotiate, or on a static
asset, all of which are the fallback policy's business. It reads no per-route metadata, so there is no
list of public routes beyond the login route itself and no notion of a role. And it fails open by
construction — when the status probe throws, the store keeps the gate disabled and the navigation is
allowed, so a back-end hiccup cannot lock a user out of an installation that has no gate.</p>
<h3 id="the-401-interceptor">The 401 Interceptor</h3>
<p>A boot file wraps the global <code>fetch</code> once for the whole application. Every API module, and SignalR's
own negotiate, call that global, so one wrapper covers a session that expires mid-use: on any 401
response, and only while the store says the gate is enabled, it marks the store logged out and pushes
the login route with the current full path in <code>redirect</code>, unless the router is already there. The
response is handed back unchanged, so the calling code still sees its own failure and handles it.</p>
<p>Boot order is load-bearing here. The interceptor's boot file is declared ahead of the i18n one, and
boot files are awaited in declaration order, so the locale probe made during startup goes through the
patched <code>fetch</code>.</p>
<h3 id="the-hub-gate">The Hub Gate</h3>
<p>The shared-hub registry carries one tri-state gate that the store drives on every transition: undecided
while the status probe is still resolving, closed while the gate is on and the session is signed out,
and open for a signed-in session or a service with no gate. Undecided holds, so a first-paint race
never fires a negotiate that is certain to fail; closed keeps every hub idle rather than letting the
never-give-up reconnect schedule hammer a <code>/negotiate</code> that answers 401 by design. The store releases
the gate even when the status probe fails, so a probe hiccup never strands the hubs of a service that
has no login. Opening the gate resumes the connections a caller had already asked for, without the
caller asking again.</p>
<h3 id="after-a-successful-sign-in">After a Successful Sign-In</h3>
<p>The login screen navigates with a full page load rather than a router navigation. The application's
one-shot wiring — the project hub subscription and the first project-status fetch — sits behind a
watcher on the auth predicate and runs once per page load, so reloading is what re-runs it with the
cookie present.</p>
<h2 id="signing-out">Signing Out</h2>
<p>The logout control sits at the right of the menu bar and renders only while the gate is enabled <em>and</em>
the session is authenticated. It is labelled with the signed-in user name, falling back to <code>Logout</code>
when the status carries no name, and its tooltip reads <code>Log out</code>. It posts the logout endpoint and
then, in a <code>finally</code>, hard-navigates to <code>/login</code> — so a failed request still tears the page down and
rebuilds the client from scratch, rather than leaving the singletons of an ended session wired up.</p>
<h2 id="layout">Layout</h2>
<p>The route renders outside the shell layout, in a Quasar layout of its own — the same arrangement the
catch-all not-found route uses. Nothing of the application frame is present: no menu bar, and therefore
no Project, Page or Preference menu and no language submenu; no cached page panel; no
routine-progress footer. The browser tab reads <code>Login - HiNC</code>.</p>
<ul>
<li>Login Page — a single card centred on an empty page
<ul>
<li>Brand Section
<ul>
<li>Brand image, above the literal title <code>HiNC</code></li>
<li>Version caption, <code>v</code> followed by the version string — drawn only when the status probe returned
one</li>
<li>Prompt caption — <code>Please sign in to continue</code></li>
</ul>
</li>
<li>Separator</li>
<li>Sign-In Form
<ul>
<li><code>Username</code> Text Field — autofocused on arrival, disabled while a sign-in is in flight</li>
<li><code>Password</code> Text Field — masked, with a trailing eye icon that toggles the text visible; the icon's
accessible label alternates between <code>Show password</code> and <code>Hide password</code></li>
<li>Error caption — drawn in the negative colour, and only after a failed attempt</li>
<li><code>Sign In</code> Button — submits the form and shows a spinner while the request is in flight</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>Pressing Enter in either field submits the same form. Neither field carries a validation rule, so an
empty pair is submitted and refused by the server like any other wrong pair. The error caption shows
the localized &ldquo;Incorrect username or password&rdquo; whenever the server pairs its refusal with the
<code>InvalidCredentials</code> code, and the server's own English message for any other refusal that carries
one.</p>
<p>Two cases send the visitor away again as soon as the screen mounts. The mount hook hydrates the auth
status if the first probe has not resolved yet, and then replaces the route with the parked <code>redirect</code>
target — or with <code>/</code>, which redirects on to the Execution page — when the gate reports disabled or when
the session is already authenticated.</p>
<p>The locale the screen paints in is whatever the boot sequence resolved, because the stored language
preference is served by an endpoint the fallback policy locks. With a browser-local cached locale the
screen paints in that; without one the awaited probe fails and the locale falls back to the browser's
own language, and to English after that. The screen's own strings ship in all three locales.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>Common/AuthConfig.cs</code> — the bound shape: the enable flag and its class default, the session length,
the sliding-expiration flag, and the clear-text user list.</li>
<li><code>Common/AuthController.cs</code> — the anonymous <code>api/auth</code> controller: the status projection, the
first-match credential scan with its message and code pair, the single name claim it signs in, and
the sign-out.</li>
<li><code>Program.cs</code> — the flag-guarded registration of the cookie scheme and the fallback policy, the cookie
name and flags, the 401 / 403 event overrides, the forwarded-headers configuration ahead of them, and
the two anonymous SPA fallbacks.</li>
<li><code>wwwroot-src/src/pages/LoginPage.vue</code> — the screen: its own Quasar layout, the card and its fields,
the bounce on mount, and the full page load after a successful sign-in.</li>
<li><code>wwwroot-src/src/router/routes.ts</code> — the login route outside the shell layout, and the title key the
tab shows.</li>
<li><code>wwwroot-src/src/router/index.ts</code> — the navigation guard: the once-only hydration, the disabled and
login-route short circuits, and the redirect carrying the full path; also the retitle that composes
the tab text from the route's title key.</li>
<li><code>wwwroot-src/src/stores/auth.ts</code> — the client-side state, the four actions, and the hub gate each of
them re-drives.</li>
<li><code>wwwroot-src/src/api/auth.ts</code> — the three typed endpoint wrappers, and the mapping of the server's
refusal code onto the localized message.</li>
<li><code>wwwroot-src/src/boot/auth.ts</code> — the global <code>fetch</code> wrapper that turns a 401 into a logged-out store
and a push to the login route.</li>
<li><code>wwwroot-src/quasar.config.ts</code> — the boot-file order that puts the interceptor ahead of the locale
probe.</li>
<li><code>wwwroot-src/src/boot/i18n.ts</code> — the locale resolution the login screen paints in, and its fallback
chain when the language endpoint answers 401.</li>
<li><code>wwwroot-src/src/composables/useSharedHub.ts</code> — the tri-state hub gate and what each of its states
does to a connection.</li>
<li><code>wwwroot-src/src/App.vue</code> — the one-shot wiring held behind the auth predicate, which is what the
post-sign-in page load re-runs.</li>
<li><code>wwwroot-src/src/components/AppMenuBar.vue</code> — the logout control and the version badge, both drawn
from the auth store.</li>
<li><code>wwwroot-src/src/i18n/en/auth.ts</code> — the screen's English strings.</li>
<li><code>wwwroot-src/src/i18n/en/menu.ts</code> — the <code>Logout</code> label the control falls back to, and its <code>Log out</code>
tooltip.</li>
<li><code>wwwroot-src/src/i18n/en/routes.ts</code> — the <code>Login</code> route title the browser tab resolves.</li>
</ul>
<h3 id="hiapi-engine">HiAPI Engine</h3>
<ul>
<li><code>HiNc/MachiningProcs/MachiningProject.cs</code> — the assembly version the status endpoint reports and the
screen shows.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="program-and-hosting.html">Program and Hosting</a> — the host that binds the <code>Auth</code> section and registers the scheme,
the fallback policy and the anonymous carve-outs described here</li>
<li><a class="xref" href="../shell/main-panel.html">Main Panel</a> — the shell this screen renders outside of, and the menu bar that carries
the logout control and the version badge</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>
@@ -0,0 +1,499 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Program and Hosting | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Program and Hosting | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-ProgramAndHosting">
<h1 id="program-and-hosting">Program and Hosting</h1>
<p><code>Program.cs</code> is the entire host of the HiNC web service: one class whose <code>Main</code> builds an ASP.NET
Core application and runs it. It presents no screen of its own — it is the process that starts and
stops HiAPI, registers every service the controllers and hubs resolve, maps those controllers and
the eight SignalR hubs, serves the built Quasar SPA out of the web root, and falls back to the SPA
shell for every URL the Vue router owns. <code>BuildApp</code> is public and returns the built application
without starting it, so the build phase and the run phase are separable; <code>Main</code> calls it, writes one
startup line to the log, and runs.</p>
<h2 id="hiapi-lifetime">HiAPI Lifetime</h2>
<p>The host brackets its whole life in one pair of HiAPI calls.</p>
<p><a class="xref" href="../../api/Hi.HiNcKits.LocalApp.html#Hi_HiNcKits_LocalApp_AppBegin_">AppBegin</a><small>(API)</small> runs once during <code>BuildApp</code>, after the
service container is built and before the request pipeline is configured. It logs in every licence
<a class="xref" href="../../api/Hi.Licenses.License.html">License</a> holds, initialises the display engine (<a class="xref" href="../../api/Hi.Disp.DispEngine.html">DispEngine</a>), and
opens the SQLite step-cache database at the path it is given, publishing it as
<a class="xref" href="../../api/Hi.SqliteUtils.SqliteStepStorage.html">SqliteStepStorage</a>'s default instance. The overload the webservice calls takes a
logger and a cache-database path.</p>
<p><a class="xref" href="../../api/Hi.HiNcKits.LocalApp.html#Hi_HiNcKits_LocalApp_AppEnd_">AppEnd</a><small>(API)</small> unwinds the same set in reverse: it waits for
queued background CubeTree frees to drain so no native delete runs against a half-torn-down runtime,
disposes the step and identity storages, shuts the display engine down, and logs the licences out.</p>
<p>Three separate events can reach <code>AppEnd</code>, and a static latch guarantees it runs at most once:</p>
<ul>
<li><code>IHostApplicationLifetime.ApplicationStopping</code> — the ordinary graceful stop.</li>
<li><code>AppDomain.CurrentDomain.ProcessExit</code> — which still fires when a console window is closed outright.</li>
<li><code>Console.CancelKeyPress</code> — Ctrl+C and Ctrl+Break, which cancel the immediate termination and ask
the lifetime for a graceful stop instead, so the first path runs.</li>
</ul>
<p>The cache-database path is <code>Cache/{CacheDbId}.db</code> beneath the admin directory.
<a class="xref" href="../../api/Hi.HiNcKits.HiNcHost.html#Hi_HiNcKits_HiNcHost_CacheDbId">CacheDbId</a> is the <code>HiNC:CacheDbId</code> configuration value when one is set and
the hosting environment name otherwise. Supplying a path matters:
<a class="xref" href="../../api/Hi.SqliteUtils.SqliteStepStorage.html">SqliteStepStorage</a> given none falls back to a single per-user file, so several
instances on one account would otherwise share it. Two instances launched under different
<code>--environment</code> names get different cache files with no further configuration.</p>
<h2 id="startup-order">Startup Order</h2>
<p>Three registrations run before the builder exists, because each seeds a table that is read — or
frozen — the moment anything else touches it:</p>
<ul>
<li><code>LocalProjectService.Reg()</code> fills <a class="xref" href="../../api/Hi.Common.XmlUtils.XFactory.html">XFactory</a>'s default generator table with
every type the simulation pipeline may deserialise, which is what every project XML read resolves
against.</li>
<li><code>UserConfig.Reg()</code> adds the per-user preference file's own type to the same table; without it, a
persisted preference file cannot be read back and every <code>UserService</code> resolution fails once one
exists.</li>
<li><a class="xref" href="../../api/Hi.SessionCommands.Lang.html">Lang</a> is appended to <a class="xref" href="../../api/Hi.Common.StringLocalizer.html">StringLocalizer</a>'s extended type list, so
session-command titles resolve through that assembly's satellite resources. The type list is
static, but each localizer builds its own resource-manager list lazily on its first lookup and
keeps it, so a type added after a given localizer has been used never reaches that localizer.
Registering before the builder exists is what puts the addition ahead of every first lookup.</li>
</ul>
<p>After the container is built and before the pipeline is assembled, the host wires the native core's
log output into the application logger through <a class="xref" href="../../api/Hi.Common.CppLogUtil.html">CppLogUtil</a>, sets the cache
identifier, calls <code>AppBegin</code>, and then runs
<a class="xref" href="../../api/Hi.HiNcKits.ResourceSeeder.html#Hi_HiNcKits_ResourceSeeder_Seed_">Seed</a><small>(API)</small> over the admin <code>Resource</code> root. The
seeder copies in only the marked <code>.default</code> items, leaves unmarked items alone as user property, and
skips the whole pass when its version stamp already matches the shipped resource assembly.</p>
<h2 id="registered-services">Registered Services</h2>
<p>Every application service is registered as a <strong>singleton</strong>. The repository contains no <code>AddScoped</code>
and no <code>AddTransient</code> call, so no service this host registers has a per-request lifetime and any
state one of them holds is process-wide. Any shorter lifetime in the container is one of the
framework's own registrations rather than an application service.</p>
<p>Four registrations are worth reading closely:</p>
<ul>
<li><strong><code>UserService</code></strong> is built by a factory rather than by type, so that its configuration path is
assigned unconditionally — including when no preference file exists yet, which is the only way the
first save can create one. When the file does exist, the factory deserialises it through
<a class="xref" href="../../api/Hi.Common.XmlUtils.XFactory.html">XFactory</a>.</li>
<li><strong><code>ProxyConfig</code></strong> is bound by hand into a plain singleton from the <code>ProxyConfig</code> configuration
section. A separate <code>services.Configure&lt;ProxyConfig&gt;</code> call also registers it through the options
system, but nothing resolves <code>IOptions&lt;ProxyConfig&gt;</code>; the plain singleton is what
<a class="xref" href="../../api/Hi.MachiningProcs.ProxyProjectService.html">ProxyProjectService</a> and the startup code receive. That binding happens
once at startup and is not reloaded.</li>
<li><strong><a class="xref" href="../../api/Hi.MachiningProcs.ProxyProjectService.html">ProxyProjectService</a></strong> is registered twice — once under its own type and
once as <a class="xref" href="../../api/Hi.MachiningProcs.IProjectService.html">IProjectService</a> — so the container builds one instance per
registration. Both hold nothing but the same two injected singletons,
<a class="xref" href="../../api/Hi.MachiningProcs.LocalProjectService.html">LocalProjectService</a> and <a class="xref" href="../../api/Hi.HiNcKits.ProxyConfig.html">ProxyConfig</a>, which is where all
the state lives.</li>
<li><strong><code>AuthConfig</code></strong> is bound from the <code>Auth</code> section and registered as an instance, so the
authentication controller and the pipeline decision below read the same object. See
<a class="xref" href="login-and-auth.html">Login and Authentication</a> for what it switches on.</li>
</ul>
<p>Six singletons are resolved eagerly at the end of <code>BuildApp</code> so they subscribe to their engine events
at startup rather than when the first client happens to connect: the CL-strip broadcast service, the
four per-sink message broadcast services, and the NC-program registry.</p>
<p>Kestrel is configured with <code>AllowSynchronousIO = true</code>, which permits handlers to write to the
response body synchronously.</p>
<h2 id="controllers">Controllers</h2>
<p>Controllers are added with <code>AddControllers()</code>, with the host's own assembly registered as an explicit
application part, and reached by <code>MapControllers()</code> at the end of the pipeline. Every controller in
the host assembly is attribute-routed, and every route template begins with <code>api/</code> — most as
<code>api/[controller]</code>, the rest as an explicit kebab-case path such as <code>api/mech/machine-tool</code> or
<code>api/execution/cl-strip</code>. No controller route sits outside <code>api/</code>, which is what makes the
&ldquo;everything else is the SPA&rdquo; fallback rule below safe.</p>
<p>One controller is exempt from the login gate: the authentication controller carries
<code>[AllowAnonymous]</code> so it stays reachable when the fallback policy locks everything else.</p>
<h2 id="signalr-hubs">SignalR Hubs</h2>
<p><code>AddSignalR()</code> is called without protocol configuration, and eight hub routes are mapped:</p>
<table>
<thead>
<tr>
<th>Route</th>
<th>Hub type</th>
<th>What it carries</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/renderingHub</code></td>
<td><code>RenderingHub</code></td>
<td>server-rendered canvas frames and the input that drives them, one display engine per connection</td>
</tr>
<tr>
<td><code>/shellMessageHub</code></td>
<td><code>ShellMessageHub</code></td>
<td>session-level routine and lifecycle messages</td>
</tr>
<tr>
<td><code>/ncDiagnosticHub</code></td>
<td><code>NcDiagnosticHub</code></td>
<td>NC-pipeline diagnostics</td>
</tr>
<tr>
<td><code>/stepDiagnosticHub</code></td>
<td><code>StepDiagnosticHub</code></td>
<td>step-anchored diagnostics</td>
</tr>
<tr>
<td><code>/ncManipulationDiagnosticHub</code></td>
<td><code>NcManipulationDiagnosticHub</code></td>
<td>NC-manipulation diagnostics — writeback conversion and optimisation</td>
</tr>
<tr>
<td><code>/executionStatusHub</code></td>
<td><code>ExecutionStatusHub</code></td>
<td><code>ExecutionStatusUpdated</code>, <code>SessionCursor</code> and <code>SessionStatusMessage</code>, broadcast to all clients; the client-callable <code>GetExecutionStatus()</code> answers the caller alone, on the same <code>ExecutionStatusUpdated</code> name</td>
</tr>
<tr>
<td><code>/clStripHub</code></td>
<td><code>ClStripHub</code></td>
<td>strip-chart display range, selected and entered step, and throttled data-update hints</td>
</tr>
<tr>
<td><code>/cleanupHub</code></td>
<td><code>CleanupHub</code></td>
<td>index keys, and optional follow-up action keys, that a client registers against its connection with <code>Add</code>; the disconnect sweep finds none of them — see below</td>
</tr>
</tbody>
</table>
<p>The four message hubs share one base class and one contract — the client calls <code>GetMessages(limit)</code>
and the server answers on <code>MessagesUpdate</code> — but they are deliberately separate routes rather than
one hub with a discriminator, so a client subscribes to exactly the sink it wants and an idle panel
means an idle hub rather than a lost connection.</p>
<p>The cleanup hub is the one whose registry does not outlive the call that fills it. SignalR builds a
fresh hub instance for every invocation, and the key dictionary <code>Add</code> writes to is an instance
member rather than shared state, so the dictionary the disconnect handler walks is a different,
empty one and releases nothing. Only <code>IndexService</code> — a singleton — is shared across those
instances. The client does not depend on the hub for the release: <code>useCleanupHub.ts</code> keeps its own
set of registered keys and posts each one to <code>/api/Index/Remove</code> before the component unmounts. That
is the path that actually frees an entry.</p>
<h2 id="json-serialisation">JSON Serialisation</h2>
<p>The controllers' serializer options carry two settings, both applied through <code>AddJsonOptions</code>.</p>
<p><strong><code>JsonNumberHandling.AllowNamedFloatingPointLiterals</code>.</strong> Several engine values use
<code>double.PositiveInfinity</code> as their &ldquo;no limit&rdquo; state — the optimizer's Max Feed Per Tooth and
Preferred Force among them — and the snapshots the SPA reads carry those doubles straight into the
response object. <code>System.Text.Json</code> refuses to write an infinite or NaN double as a JSON number, so
without this setting every such response would fault during serialisation instead of returning. With
it, those values travel as the JSON strings <code>&quot;Infinity&quot;</code>, <code>&quot;-Infinity&quot;</code> and <code>&quot;NaN&quot;</code>. The reading side
is not uniform: the numeric input widgets take all three spellings case-insensitively, while the
option-snapshot reader in the mission API recognises the two infinity spellings only and resolves
anything else non-numeric — <code>&quot;NaN&quot;</code> included — to the fallback its caller passed, which is positive
infinity for Max Feed Per Tooth and Preferred Force.
<a class="xref" href="../conventions/numeric-io-utilities.html">Numeric Input/Output</a> covers the client half of that boundary, and
<a class="xref" href="../execution/mission/NcOptOption-panel.html">NC Optimization Option Panel (NC Optimization Config)</a> the panel that leans hardest on it — including the two write
endpoints that take a string body so the same spelling survives the round trip.</p>
<p><strong><code>JsonStringEnumConverter</code>.</strong> Enums are read and written as their names rather than as integers,
which is what the API wrappers expect and what keeps the OpenAPI schema self-describing.</p>
<p>Both settings belong to the MVC controllers only. Hub payloads are serialised by SignalR's own
protocol, which is left at its defaults, so neither the named floating-point literals nor the string
enums extend to hub messages.</p>
<h2 id="the-request-pipeline">The Request Pipeline</h2>
<p>The order the middleware is added in is load-bearing, and the host sets it explicitly rather than
accepting the default arrangement:</p>
<ol>
<li><strong>Forwarded headers</strong> first, honouring <code>X-Forwarded-For</code> and <code>X-Forwarded-Proto</code> so that every
later stage sees the real client scheme. Both the known-proxy and the known-network lists are
cleared, so those headers are accepted from any caller.</li>
<li><strong>Swagger</strong> — the Swashbuckle document and its UI, both middleware. <code>MapOpenApi</code> beside them
registers the framework's own document as an <em>endpoint</em> instead, so that one is reached at
step 8 with everything else that is routed.</li>
<li><strong>Default files, then static files</strong>, serving the built SPA and everything else physically present
in the web root.</li>
<li><strong><code>UseRouting()</code> — placed here on purpose.</strong> An application that never calls it gets routing
inserted at the front of the pipeline instead, which would select the SPA fallback endpoint before
the static-file middleware ran; that middleware stands down once an endpoint is chosen, so a
directory URL under the web root would be answered by the Vue router's not-found view rather than
by the directory's own default document.</li>
<li><strong>CORS</strong>, applying the <code>AllowAll</code> policy described below.</li>
<li><strong>HTTPS redirection</strong>, skipped in the Development environment.</li>
<li><strong>Authentication, then authorization.</strong> Both are no-ops when the login gate is off, because no
scheme and no fallback policy are registered in that case.</li>
<li><strong>Endpoints</strong> — controllers, the eight hubs, and the two SPA fallbacks.</li>
</ol>
<p>A request answered before step 7 never reaches the authorization stage at all: the static-file
middleware and the Swagger middleware both sit ahead of it and short-circuit the request when they
match. The two SPA fallbacks are endpoints rather than middleware, so they do reach it, and both are
marked <code>AllowAnonymous</code> — which is what lets the SPA shell, carrying no data of its own, load and
show its own login view when the gate is on.</p>
<h3 id="the-spa-fallback">The SPA Fallback</h3>
<p>Two fallback registrations serve <code>index.html</code>:</p>
<ul>
<li><code>util/file-explorer/{**location}</code> — an explicit pattern with no file-name constraint.</li>
<li>The bare fallback, for everything else.</li>
</ul>
<p>The bare form's implicit route pattern carries a <code>nonfile</code> constraint that rejects any URL whose last
segment contains a dot. File Explorer deep links mirror real folder and file names into the URL, and
those names routinely contain dots, so reloading or pasting such a location needs the explicit
pattern. Genuine static assets cannot be shadowed by it, because the static-file middleware has
already run by the time either fallback is reached.</p>
<h2 id="serving-the-spa">Serving the SPA</h2>
<p>The Quasar build writes its output directly into the host's web root — the front end's build
configuration sets its distribution directory to the web service's <code>wwwroot</code> — so there is no copy
step between building the SPA and serving it. The router runs in history mode, which is what makes
the SPA fallback necessary: every client-side route is an extension-less URL the server has no
endpoint for.</p>
<p>The front-end build script runs the i18n census and glossary lint before invoking the Quasar build,
so a build that fails those checks produces no new bundle.</p>
<p>For front-end work the Quasar dev server runs on its own port and proxies the routes the backend owns
to the running service. That proxy list holds <code>/api</code>, <code>/swagger</code>, <code>/renderingHub</code>,
<code>/executionStatusHub</code>, <code>/clStripHub</code>, <code>/cleanupHub</code>, and <code>/sessionMessageHub</code> — a path the host maps
no hub on. The four per-sink message hubs are absent from it, so the Session Messages panels receive
no live messages through the dev server.</p>
<h2 id="openapi-and-swagger">OpenAPI and Swagger</h2>
<p>A single private constant holds the REST contract version. It is simultaneously the Swashbuckle
document group name — so it appears in the document's route — and the version the UI displays, which
is why it is defined in one place. It is raised by hand on a breaking REST change only, and is
deliberately not tied to the assembly version or to the project-file format version.</p>
<p>Both OpenAPI surfaces are exposed in every environment rather than behind a development gate, so an
automated caller always has a machine-readable contract. The Swashbuckle UI is served at <code>/swagger</code>
and its document at <code>/swagger/{version}/swagger.json</code>; the framework's own document is published
under <code>/openapi/</code>.</p>
<p>Schema ids are keyed on the full type name with the nested-type separator normalised. Without that,
two controllers' nested request types that share a simple name collide and the document generator
throws — which surfaces as an HTTP 500 from the document route rather than as a startup failure. XML
documentation comments are folded in from the host assembly and from every referenced assembly whose
name starts with <code>Hi</code>. Every read is skipped when its file is missing, and each referenced-assembly
read carries a catch-all of its own on top of that, so one whose file is present but unreadable is
ignored; the host assembly's read has no such catch-all.</p>
<h2 id="cors">CORS</h2>
<p>One policy, named <code>AllowAll</code>, permits any origin, any method and any header, and it is applied
globally. It does not allow credentials, so the response carries a wildcard origin and a cross-origin
browser call cannot use the session cookie. The SPA is served from the same origin as the API and the
hubs, so this constrains only callers hosted elsewhere.</p>
<h2 id="logging">Logging</h2>
<p>The default providers are cleared and two are added: a console provider, and a daily file provider
rooted at a <code>logs</code> folder beneath the process's current working directory. The file provider writes
one line per entry to <code>log-{yyyy-MM-dd}.txt</code> and swallows every I/O failure, on the principle that
logging must not fault a request or a startup.</p>
<p>The two providers are filtered independently. The console follows the <code>Logging:LogLevel</code> section of
configuration, which sets everything to Warning. The file provider carries its own code-set rules —
Information for the application's own categories, Warning for <code>Microsoft</code> and <code>System</code> — and
provider-specific rules take precedence over configuration's provider-neutral ones, so the file stays
useful while the console stays quiet.</p>
<p><code>Main</code> writes one Information line immediately after the application is built, so the file for the day
the service starts has content before the first request arrives. That guarantee stops at midnight: the
provider recomposes the file name from the clock on every append, and creates nothing ahead of time, so
a host left running into a new day has no file for it until the next entry is written.
<code>GET /api/project/logs</code> reads the same directory.</p>
<h2 id="configuration">Configuration</h2>
<p>Configuration is the standard chain: <code>appsettings.json</code>, then <code>appsettings.{Environment}.json</code>, then
environment variables, then command-line arguments. The keys this host reads are:</p>
<table>
<thead>
<tr>
<th>Key</th>
<th>What it controls</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Logging:LogLevel</code></td>
<td>the console provider's levels</td>
</tr>
<tr>
<td><code>AllowedHosts</code></td>
<td>host filtering; shipped as a wildcard</td>
</tr>
<tr>
<td><code>Kestrel:Endpoints</code></td>
<td>the addresses the server binds</td>
</tr>
<tr>
<td><code>ProxyConfig:AdminDirectory</code></td>
<td>the admin working root, under which <code>Resource</code> and <code>Cache</code> live</td>
</tr>
<tr>
<td><code>Auth</code></td>
<td>the optional login gate</td>
</tr>
<tr>
<td><code>HiNC:CacheDbId</code></td>
<td>an explicit step-cache database id, overriding the environment name</td>
</tr>
</tbody>
</table>
<p>The Development overlay repeats the logging, admin-directory and authentication sections and declares
no <code>Kestrel</code> section, so the listening address is the same under either environment.</p>
<h3 id="kestrel-endpoints-win">Kestrel Endpoints Win</h3>
<p><code>Kestrel:Endpoints</code> is the authoritative source of the listening address. When it declares endpoints,
Kestrel binds those and <strong>ignores</strong> addresses supplied any other way — <code>ASPNETCORE_URLS</code>, <code>--urls</code>,
and the launch profile's <code>applicationUrl</code> alike — logging that it is overriding them. The shipped
configuration declares one HTTP endpoint on loopback port 5000. The <code>http</code> launch profile names the
same address, so the override is invisible there; the <code>https</code> profile also names an HTTPS address on
a second port, and that address is one of the ones Kestrel discards, so launching under it still
yields plain HTTP on 5000 and nothing else.</p>
<p>To move a local instance to another port, set the <code>Kestrel__Endpoints__Http__Url</code> environment
variable, which the environment-variable provider maps onto the same key. Setting <code>ASPNETCORE_URLS</code>
alone changes nothing.</p>
<p>The endpoint binds <code>localhost</code>, so the shipped configuration answers only on the machine it runs on.
Reaching it from elsewhere means either overriding that key or placing a reverse proxy in front,
which is the arrangement the forwarded-headers configuration handles.</p>
<h3 id="environment-and-working-directory">Environment and Working Directory</h3>
<p>The environment name reaches further here than usual. Both launch profiles set it to <code>Development</code>,
which selects the Development overlay, skips HTTPS redirection, and — unless <code>HiNC:CacheDbId</code>
overrides it — names the step-cache database file.</p>
<p>Two paths follow the process's current working directory rather than the content root: the <code>logs</code>
folder the file logger writes and the log endpoint reads, and the per-user preference file. An
instance launched from the project directory finds both where they are expected; one launched from
elsewhere puts them beside wherever it was started.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>Program.cs</code> — the whole subject of this page: the pre-builder registrations, every service
registration, the JSON and Swagger options, the CORS policy, the pipeline order, the eight
<code>MapHub</code> calls, the two SPA fallbacks, the <code>AppBegin</code> / <code>AppEnd</code> pair and the three shutdown
triggers.</li>
<li><code>appsettings.json</code> — the shipped listening address, the admin directory and the login section.</li>
<li><code>appsettings.Development.json</code> — the Development overlay, which declares no <code>Kestrel</code> section.</li>
<li><code>Properties/launchSettings.json</code> — the two launch profiles whose <code>applicationUrl</code> the <code>Kestrel</code>
section overrides.</li>
<li><code>Common/DailyFileLoggerProvider.cs</code> — the per-day file logger the log endpoint reads back.</li>
<li><code>Common/AuthConfig.cs</code> — the shape bound from the <code>Auth</code> section.</li>
<li><code>Common/AuthController.cs</code> — the anonymous carve-out that keeps login reachable under the fallback
policy.</li>
<li><code>Common/NamedRootResolver.cs</code> — resolves the admin, project and resource roots the seeder and the
file endpoints work against.</li>
<li><code>Common/CleanupHub.cs</code> — the cleanup hub, and the per-invocation dictionary its disconnect
handler walks.</li>
<li><code>Common/IndexController.cs</code> — the index remove endpoint the client calls before unmount.</li>
<li><code>Disp/RenderingHub.cs</code> — the canvas hub.</li>
<li><code>Execution/SessionSinkHub.cs</code> — the shared base and the four per-sink message hubs.</li>
<li><code>Execution/ExecutionStatusHub.cs</code> — the status, cursor and session-message hub.</li>
<li><code>Execution/ClStripHub.cs</code> — the strip-chart hub.</li>
<li><code>Environments/UserConfig.cs</code> — the preference type registered before the builder is created.</li>
<li><code>Environments/ProjectController.cs</code> — the log endpoints that read the daily file.</li>
<li><code>wwwroot-src/src/composables/useCleanupHub.ts</code> — the client's own key set and the index-remove
calls that release it.</li>
<li><code>wwwroot-src/src/api/mission.ts</code> — the option-snapshot reader that maps the named-literal strings
back to numbers, and the string-bodied setters that send them.</li>
<li><code>wwwroot-src/quasar.config.ts</code> — the build output directory that puts the bundle in the web root,
the history router mode, and the dev-server proxy list.</li>
<li><code>wwwroot-src/package.json</code> — the build script that runs the i18n lint before the Quasar build.</li>
</ul>
<h3 id="hiapi-engine">HiAPI Engine</h3>
<ul>
<li><code>HiNc/HiNcKits/LocalApp.cs</code> — <code>AppBegin</code> and <code>AppEnd</code>: licence log-in and log-out, display-engine
start and finish, and the step-storage open and dispose.</li>
<li><code>HiNc/HiNcKits/HiNcHost.cs</code> — the cache-database identifier the host assigns at startup.</li>
<li><code>HiNc/HiNcKits/ProxyConfig.cs</code> — the admin-directory setting and its own default.</li>
<li><code>HiNc/HiNcKits/ResourceSeeder.cs</code> — the marked-defaults seeding pass and its version stamp.</li>
<li><code>HiNc/SqliteUtils/SqliteStepStorage.cs</code> — the step cache, and the per-user default path that makes
an explicit per-instance path necessary.</li>
<li><code>HiNc/MachiningProcs/ProxyProjectService.cs</code> — the project service registered under two service
types, holding only the singletons it is given.</li>
<li><code>HiGeom/Common/StringLocalizer.cs</code> — the static extended type list, and the per-instance
resource-manager list each localizer builds on its own first lookup.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="session-state.html">Session State</a> — what survives a project change inside the singletons this host
registers, and what is rebuilt</li>
<li><a class="xref" href="login-and-auth.html">Login and Authentication</a> — the <code>Auth</code> section this host binds, and the cookie scheme and
fallback policy it registers when the gate is on</li>
<li><a class="xref" href="../conventions/rendering-canvas-web-service.html">Rendering Canvas on Web Service Application</a> — the canvas hub this host maps, and the per-connection
display engine behind it</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>
@@ -0,0 +1,428 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Session State | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Session State | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-SessionState">
<h1 id="session-state">Session State</h1>
<p>Session state is what the web client still holds after the user moves from one screen to the next. It
owns no route: the mechanism lives in the shell that wraps every route under <code>/</code> — a router outlet
inside a <code>&lt;keep-alive&gt;</code> keyed on a project epoch — together with four Pinia stores, a family of
module-level composables that deliberately sit outside the page lifecycle, and three browser storage
keys. Every value in the client answers the same three questions: does it survive a navigation, does
it survive a project change, and does it survive a browser reload.</p>
<h2 id="the-project-epoch">The Project Epoch</h2>
<p><code>MainLayout.vue</code> renders the routed page inside a <code>&lt;keep-alive&gt;</code> whose <code>:key</code> is <code>projectEpoch</code>. That
epoch is a plain integer <code>ref</code> declared in the layout component itself — it is not a store value, it
is not provided to anything, and nothing outside the file reads it. Two watchers in the same file are
the only writers, and each adds one to it: a watcher on the project store's <code>projectPath</code>, and a
watcher on the project store's <code>projectVersion</code>.</p>
<p>The key sits on the <code>&lt;keep-alive&gt;</code> element rather than on the page component inside it. Changing it
therefore discards the cache itself along with every page instance held in it and builds a fresh one,
which is why a page may rely on its own mount hook and never watch the project: both Control-Tree
pages call their host's <code>initialize()</code> from <code>onMounted</code>, and that runs once per project rather than
once per visit.</p>
<p>The two inputs move for different reasons.</p>
<ul>
<li><strong><code>projectPath</code></strong> is hydrated once at boot by the store's status fetch, assigned by New, Load and
Save As from the response each returns, cleared outright by Close, and driven thereafter by a
watcher inside the project store on the shared execution-status hub's status payload. A project
change made in another tab, from a different browser, or outside the browser altogether therefore
bumps the epoch here as well — and so, once, does the boot hydration on a service that already has
a project open.</li>
<li><strong><code>projectVersion</code></strong> is a counter the store raises in exactly one place: ReLoad. The backend raises
its project-changed event with the path it already had, so the broadcast cannot be told apart from
a no-op; the client-side counter is what makes a reload remount.</li>
</ul>
<p>Save on the current path moves neither, so it does not remount. The epoch only counts up, and because
it lives in the layout it starts again at zero on a browser reload — nothing reads its value, only its
transitions.</p>
<h2 id="what-a-navigation-costs">What a Navigation Costs</h2>
<p>The <code>&lt;keep-alive&gt;</code> carries no <code>include</code>, <code>exclude</code> or <code>max</code>, so every page reached under <code>/</code> is cached
until the epoch changes. Leaving a route deactivates its component instead of unmounting it, and
returning re-activates the same instance with its in-flight edits and its Control-Tree host intact.
Scroll offsets are not part of that: the router's <code>scrollBehavior</code> resolves every navigation to the
top of the page, and nothing saves an inner scroll position across a deactivation. The two routes
declared outside the layout — the login screen and the not-found catch-all — are not cached at all.</p>
<p>Three consequences the pages are written around:</p>
<ul>
<li>A cached page's watchers keep firing while another page is showing. The Control-Tree host compares
the current route name against the one its scope owns before it writes the <code>?tree=</code> query back, so
an off-screen host cannot rewrite the visible page's URL.</li>
<li>A global listener has to be dropped on deactivation, not on unmount. The Execution transport binds
its F5–F8 shortcuts on mount and on activation and removes them on deactivation and unmount, so on
any other page F5 falls back to the browser's own reload instead of starting the run.</li>
<li>The Execution, General Setup and Tool House pages each track their own activation in a flag that
feeds the rendering canvas's <code>active</code> prop. Such a page keeps its canvas mounted and its connection
open while the backend engine stops rendering for it.</li>
</ul>
<p>The File Explorer page adds a route-leave guard that settles its editor buffer — flushing a pending
auto-save, or prompting before discarding manual edits — and can refuse the navigation.</p>
<h2 id="the-stores">The Stores</h2>
<p>Four Pinia stores are created against the single Pinia instance built at boot. None of them is inside
the <code>&lt;keep-alive&gt;</code>, so a project change resets none of them; only a browser reload rebuilds them.</p>
<ul>
<li><strong>project</strong> holds <code>projectPath</code>, <code>projectVersion</code>, the admin and project directories, a loading flag
and the <code>hasProject</code> computed. Its actions post to the project endpoints and each throws a typed busy
error on HTTP 409, which the backend returns when another project file operation is already running —
the request is refused rather than queued. The store also owns the subscription to the shared
execution-status hub and the watcher that adopts the path that hub broadcasts.</li>
<li><strong>appState</strong> holds the debug flag (initialised from whether the page is served from <code>localhost</code>), the
physics-options and physics-licensed flags, the language code with the list of codes the server
offers, and the Execution division-visibility record. <code>loadServerPreferences</code>, called from the
layout's mount hook, hydrates those three groups in independently guarded steps, so a failing
endpoint leaves the rest usable. A division-flag write is applied optimistically and rolled back on
failure, with a monotonic sequence number so a late response cannot overwrite newer state. Its
<code>setMessage</code> is a forwarder into the footer's foreground channel.</li>
<li><strong>auth</strong> holds whether the login gate is enabled, whether this session is authenticated, the user
name, the version string the login screen shows, and a ready flag set once the first status probe
resolves. Every transition re-drives the global hub gate described below.</li>
<li><strong>routineProgress</strong> backs the footer's two channels. The foreground channel keeps the latest entry
plus a history capped at one hundred entries, which the footer's history button lists; the background
channel is a single in-flight job with a message and an optional fraction and keeps no history at
all, so it disappears when the job ends. A boot patch mirrors every Quasar toast into the foreground
channel, so a toast stays reviewable after it fades.</li>
</ul>
<h2 id="state-outside-the-stores">State Outside the Stores</h2>
<p>A second family of state lives at module scope in composables rather than in a store. The module is
evaluated once per page load and its <code>ref</code>s are created there, so every caller of the composable
receives the same objects and nothing in a component's lifecycle creates or destroys them.</p>
<p>Three of these hold project data and share one lifecycle idiom — <code>useSpindleCapability</code>,
<code>useSoftNcRunner</code> and <code>useToolHouse</code>. Each exposes an idempotent <code>ensureInstalled()</code> that, on its first
call only, opens a detached effect scope and watches <code>hasProject</code>: the state reloads when a project
appears and is cleared when one closes. Every panel that reads the singleton calls <code>ensureInstalled()</code>
in its own setup, and every call after the first does nothing.</p>
<p>The consequence is the one that matters when reading a panel on screen: <strong>a mounted panel is not
refreshed by a change made elsewhere.</strong> A project change unmounts and rebuilds every page, but it does
not re-read a singleton — the installer is already installed, and the watch it installed is on a
boolean that the load and new actions leave true. What each singleton holds is replaced only by an
action on the composable itself, or by a consumer that asks for it explicitly: the Tool House page's
mount hook calls the composable's <code>reload()</code>, while the spindle and controller panels call
<code>ensureInstalled()</code> alone. The controller branch is the sharpest case. Its leaf panels fetch their own
tables on mount and the tree builder re-fetches the runner snapshot it grows children from, but the
shared snapshot the panels gate on — the brand, the chain axes, the per-group presence flags and the
object key — is replaced only by a brand switch, an Object-Management install, or a close.</p>
<p>The remaining module singletons carry no project data and exist to join components that are not in one
another's tree: the Execution runtime flags the run page publishes for the nav bar, the sentence cursor
shared by the Program file panel and the syntax view, the strip-chart group's reload tick and hovered
x label, the two cycle-chart cursor marks the sim and sensor chart groups share, the parked cross-panel
line jump, the transport's shared reset flag, and the shared-hub registry with its auth gate.</p>
<h2 id="device-local-state">Device-Local State</h2>
<p>Three <code>localStorage</code> keys hold state belonging to one browser profile on one machine. None of them is
written into the project file and none is sent to the server.</p>
<ul>
<li>The tree-page view preferences key holds, per tree page, the column-visibility record and the two
left-dock row states; and, shared across both pages, the dock pixel widths, the editor row height,
the strip/step ratio, the two chart legend widths, the strip x-axis mode, the per-panel resize
weights, the Control-Tree expansion list, the last selection per page, the two runner-suit
checkboxes, and the three device-local panel switches for the 3D canvas, the CWE canvas and the
sentence syntax view. Writes are debounced, and two earlier key names are read once as migration
seeds when the current key is absent.</li>
<li>The locale key is a paint-time hint only: the boot sequence applies it before mount so the first
frame is already translated, then reconciles against the server value, which wins.</li>
<li>The File Explorer key holds whether its editor pane is shown, the pane split, the auto-save switch
and the sort order.</li>
</ul>
<p>What is <strong>not</strong> device-local is easy to mistake for it. The physics-options switch, the interface
language and the Execution division-visibility flags live in the server's <code>UserConfig</code>, held by a
singleton service and written to an XML file in the service's working directory. They are per install,
not per browser: two browsers pointed at the same service read and write the same values. The
machining project is server-side in the same way — one project is loaded at a time and every connected
browser sees it, which is why the status-hub broadcast is what keeps a second tab honest.</p>
<p>The URL carries the rest: the route, the <code>?tree=</code> selection on the two Control-Tree pages, the browsed
path on the File Explorer route, and the tab segments on the Tool House and Controller routes. Those
come back on a reload from the address bar, and when a Control-Tree page is opened with no <code>?tree=</code> the
host lands on the stored last selection for that page, falling back to the page's root when the stored
id is not in the current project's tree.</p>
<table>
<thead>
<tr>
<th>What</th>
<th>Navigation</th>
<th>Project change</th>
<th>Browser reload</th>
</tr>
</thead>
<tbody>
<tr>
<td>A page's own component state, including its Control-Tree host</td>
<td>survives</td>
<td>rebuilt</td>
<td>rebuilt</td>
</tr>
<tr>
<td>The four Pinia stores</td>
<td>survives</td>
<td>survives</td>
<td>rebuilt</td>
</tr>
<tr>
<td>Module-singleton composables</td>
<td>survives</td>
<td>survives</td>
<td>rebuilt</td>
</tr>
<tr>
<td>The <code>localStorage</code> preferences</td>
<td>survives</td>
<td>survives</td>
<td>survives</td>
</tr>
<tr>
<td>Route, <code>?tree=</code>, browsed path and tab segments</td>
<td>this is what changes</td>
<td>survives</td>
<td>survives</td>
</tr>
<tr>
<td>Server preferences and the loaded project</td>
<td>survives</td>
<td>server-side</td>
<td>survives</td>
</tr>
</tbody>
</table>
<h2 id="signalr-connections">SignalR Connections</h2>
<p>Most hubs are shared singletons built by one factory: the execution-status hub, the CL strip hub, and
the four session-message sinks for shell, NC diagnostic, step diagnostic and NC-manipulation
diagnostic messages. There is one connection per hub for the whole application, reference-counted by
consumer. A component registers as a consumer when it calls the hub's composable and releases on its
own unmount; when the count reaches zero the teardown waits out a short grace window, so the
remount a project change forces — every consumer dropping and re-adding within the same render flush —
keeps its connections rather than renegotiating them.</p>
<p>The project store takes out a consumer registration on the execution-status hub when the store is
created and never releases it itself, so that hub keeps a consumer no matter which page is showing.</p>
<p>A global gate sits in front of all of them. While the optional login gate is enabled and the session is
not authenticated — and while auth status is still resolving — hubs stay idle rather than negotiating
into a 401. A logout or an intercepted 401 closes every connection; permitting again reconnects the
ones a caller had asked for, without the caller asking twice. Reconnection is a dense burst of
attempts, then a fixed cadence, then a slow tail that never gives up, and tab visibility or window
focus starts a fresh burst immediately.</p>
<p>Two kinds of connection are per instance rather than shared:</p>
<ul>
<li>Each rendering canvas opens its own connection to the rendering hub on mount and stops it on unmount.
Because pages are cached, navigating away does not close one, and whether the backend engine stops
drawing for it depends on the host. The Execution, General Setup and Tool House pages hand their
activation flag to the canvas's <code>active</code> prop, which pauses the engine while the page sits off-screen;
the Controller, Machine Tool and Mech Builder canvases and the File Explorer's STL preview bind no
such prop, and the Execution page's CWE canvas pins it true, so all of those keep rendering until
something unmounts them. Collapsing a panel does close one, unless that panel keeps its content
mounted: the Execution page's 3D canvas panel does, so it survives a collapse, while the CWE panel
beside it does not and its canvas connection goes with it.</li>
<li>The cleanup hub is opened once per Control-Tree host and once each on the Controller and Tool House
pages. Its composable also holds the set of IndexService keys that owner registered; on unmount it
posts a removal for every one of them and then stops the connection, so a teardown releases the
server-side objects the page had indexed even if the hub never connected.</li>
</ul>
<p>A shared connection therefore closes when its last consumer has been gone longer than the grace window,
or when the auth gate shuts. A per-instance connection closes when the component owning it unmounts —
which is what a project change does to every page at once, while the shared hubs ride that same remount
out inside their grace window.</p>
<h2 id="boot-order">Boot Order</h2>
<p><code>App.vue</code> renders nothing but the router outlet and holds the wiring that must run exactly once per
page load: subscribe the project hub, then fetch project status. Both sit behind a watcher on the auth
predicate with a latch rather than behind a mount hook, because auth status may still be resolving at
first paint. The router's own guard hydrates auth status before the first navigation and fails open, so
a status hiccup cannot lock out an installation that has no login gate. The layout's mount hook then
hydrates the server preferences. A successful sign-in navigates with a full page load rather than a
router push, so the whole sequence runs again with the authentication cookie present.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>wwwroot-src/src/layouts/MainLayout.vue</code> — the project epoch, its two watchers, the keyed
<code>&lt;keep-alive&gt;</code> around the router outlet, the footer, and the server-preference hydration call.</li>
<li><code>wwwroot-src/src/App.vue</code> — the auth-gated one-shot wiring: the project hub subscription and the
first status fetch.</li>
<li><code>wwwroot-src/src/stores/index.ts</code> — the Pinia instance the four stores are created against.</li>
<li><code>wwwroot-src/src/stores/project.ts</code> — the project path and version, the directories, the file actions
and their busy error, the hub subscription, and the watcher that adopts the broadcast path.</li>
<li><code>wwwroot-src/src/stores/appState.ts</code> — the debug and physics flags, the language state, the Execution
division config with its optimistic write and sequence guard, and the footer forwarder.</li>
<li><code>wwwroot-src/src/stores/auth.ts</code> — the login-gate state and the hub gate it drives.</li>
<li><code>wwwroot-src/src/stores/routineProgress.ts</code> — the footer's foreground history and its single live
background job.</li>
<li><code>wwwroot-src/src/composables/useViewPrefs.ts</code> — the device-local view preferences: the shape, the
defaults, the migration seeds and the debounced write.</li>
<li><code>wwwroot-src/src/composables/useSoftNcRunner.ts</code> — the controller singleton: the shared runner
snapshot, the object key, and the install-once project watch.</li>
<li><code>wwwroot-src/src/composables/useSpindleCapability.ts</code> — the spindle singleton on the same lifecycle
idiom.</li>
<li><code>wwwroot-src/src/composables/useToolHouse.ts</code> — the tool-house singleton and the coalesced reload the
page calls on mount.</li>
<li><code>wwwroot-src/src/composables/useSharedHub.ts</code> — the shared-hub factory: consumer counting, the
teardown grace window, the auth gate, the retry schedule and the focus recovery.</li>
<li><code>wwwroot-src/src/composables/useExecutionStatusHub.ts</code> — the status, cursor and message payloads the
store and the footer read.</li>
<li><code>wwwroot-src/src/composables/useClStripHub.ts</code> — the strip snapshot and update counter the charts
watch.</li>
<li><code>wwwroot-src/src/composables/useSessionSinkHub.ts</code> — the four message sinks and their
notify-and-re-pull contract.</li>
<li><code>wwwroot-src/src/composables/useCleanupHub.ts</code> — the per-instance cleanup connection and the key set
it releases on unmount.</li>
<li><code>wwwroot-src/src/composables/useExecutionTransport.ts</code> — the activation-scoped keyboard shortcuts and
the shared reset flag.</li>
<li><code>wwwroot-src/src/composables/useExecutionRuntime.ts</code> — the runtime flags the run page publishes.</li>
<li><code>wwwroot-src/src/composables/useSentenceCursor.ts</code> — the shared source cursor and its install-once
watch on step selection.</li>
<li><code>wwwroot-src/src/composables/useStripChartGroup.ts</code> — the group reload tick and the hovered x label.</li>
<li><code>wwwroot-src/src/composables/useCycleSyncMark.ts</code> — the per-group cycle-chart cursor mark the sim
and sensor charts share.</li>
<li><code>wwwroot-src/src/composables/useConversionJump.ts</code> — the parked cross-panel jump.</li>
<li><code>wwwroot-src/src/components/RenderingCanvas.vue</code> — the per-instance rendering connection, its mount
and unmount lifecycle, and the mounted guard that survives a mid-await teardown.</li>
<li><code>wwwroot-src/src/components/panels/PanelExpansion.vue</code> — the expansion row, and the keep-mounted flag
that decides whether a collapse unmounts its content.</li>
<li><code>wwwroot-src/src/components/AppFooter.vue</code> — the two footer channels and the recent-message list.</li>
<li><code>wwwroot-src/src/components/FileExplorer.vue</code> — the explorer's own device-local preference key.</li>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — the per-page host: the route-name
guard on the <code>?tree=</code> sync, the landing selection, and the cleanup registrations.</li>
<li><code>wwwroot-src/src/pages/ExecutionPage.vue</code> — the activation flag, the once-per-project initialization,
and the panel that keeps the 3D canvas mounted through a collapse.</li>
<li><code>wwwroot-src/src/pages/GeneralSetupPage.vue</code> — the same lifecycle for the equipment host.</li>
<li><code>wwwroot-src/src/pages/ToolHousePage.vue</code> — the page that reloads its singleton explicitly on mount.</li>
<li><code>wwwroot-src/src/pages/ControllerPage.vue</code>, <code>wwwroot-src/src/pages/MachineToolPage.vue</code>,
<code>wwwroot-src/src/pages/MechBuilderPage.vue</code> and <code>wwwroot-src/src/components/StlPreviewPane.vue</code> —
the canvases mounted with no <code>active</code> binding.</li>
<li><code>wwwroot-src/src/components/execution/StepVolumePanel.vue</code> — the CWE canvas that pins <code>active</code> true
and is stopped only by its own unmount.</li>
<li><code>wwwroot-src/src/pages/FileExplorerPage.vue</code> — the route-leave guard that settles the editor buffer.</li>
<li><code>wwwroot-src/src/pages/LoginPage.vue</code> — the full-page navigation after a successful sign-in.</li>
<li><code>wwwroot-src/src/router/index.ts</code> — the auth guard that hydrates status before the first navigation.</li>
<li><code>wwwroot-src/src/router/routes.ts</code> — which routes render inside the layout and which do not.</li>
<li><code>wwwroot-src/src/boot/auth.ts</code> — the fetch wrapper that turns a 401 into a logged-out store and a
bounce to the login screen.</li>
<li><code>wwwroot-src/src/boot/i18n.ts</code> — the cached-locale fast path and the server reconcile.</li>
<li><code>wwwroot-src/src/boot/routine-toast.ts</code> — the toast mirror into the footer channel.</li>
<li><code>wwwroot-src/src/i18n/index.ts</code> — the locale storage key and the single place a locale change is
applied.</li>
<li><code>wwwroot-src/src/api/preference.ts</code> — the typed wrappers over the preference endpoints the app-state
store hydrates from.</li>
<li><code>Environments/ProjectController.cs</code> — the status, new, load, save, save-as, reload and close
endpoints, and the conflict reply that becomes the client's busy error.</li>
<li><code>Environments/PreferenceController.cs</code> — the endpoints behind the server-held preferences.</li>
<li><code>Environments/UserConfig.cs</code> — the persisted user configuration: the physics switch, the language
code, the graphic-cache limits, the step-present list and the Execution division config.</li>
<li><code>Environments/UserService.cs</code> — the singleton that owns that configuration and writes it to file.</li>
<li><code>Execution/ExecutionStatusService.cs</code> — the broadcast that carries a project change to every
connected browser.</li>
<li><code>Program.cs</code> — where the user-configuration service is registered and its file path fixed.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="control-tree.html">Control Tree</a> — the per-page host this epoch destroys and rebuilds, and the selection it
restores from device-local storage</li>
<li><a class="xref" href="program-and-hosting.html">Program and Hosting</a> — how the SPA is served and hosted, and where the server-side
singletons this page reads actually live</li>
<li><a class="xref" href="../shell/main-panel.html">Main Panel</a> — the shell around the router outlet: the menu bar that fires the project
actions and the footer that shows their outcome</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>
@@ -0,0 +1,43 @@
<div id="sidetoggle">
<div>
<div class="sidefilter">
<form class="toc-filter">
<span class="glyphicon glyphicon-filter filter-icon"></span>
<span class="glyphicon glyphicon-remove clear-icon" id="toc_filter_clear"></span>
<input type="text" id="toc_filter_input" placeholder="Filter by title" onkeypress="if(event.keyCode==13) {return false;}">
</form>
</div>
<div class="sidetoc">
<div class="toc" id="toc">
<ul class="nav level1">
<li>
<a href="index.html" name="" title="Overview">Overview</a>
</li>
<li>
<a href="control-tree.html" name="" title="Control Tree">Control Tree</a>
</li>
<li>
<a href="tree-ids-and-routes.html" name="" title="Tree Ids and Routes">Tree Ids and Routes</a>
</li>
<li>
<a href="session-state.html" name="" title="Session State">Session State</a>
</li>
<li>
<a href="i18n.html" name="" title="Internationalization">Internationalization</a>
</li>
<li>
<a href="login-and-auth.html" name="" title="Login and Auth">Login and Auth</a>
</li>
<li>
<a href="log-viewer.html" name="" title="Log Viewer">Log Viewer</a>
</li>
<li>
<a href="program-and-hosting.html" name="" title="Program and Hosting">Program and Hosting</a>
</li>
</ul>
</div>
</div>
</div>
</div>
@@ -0,0 +1,2 @@
{"items":[{"name":"Overview","href":"index.html","topicHref":"index.html"},{"name":"Control Tree","href":"control-tree.html","topicHref":"control-tree.html"},{"name":"Tree Ids and Routes","href":"tree-ids-and-routes.html","topicHref":"tree-ids-and-routes.html"},{"name":"Session State","href":"session-state.html","topicHref":"session-state.html"},{"name":"Internationalization","href":"i18n.html","topicHref":"i18n.html"},{"name":"Login and Auth","href":"login-and-auth.html","topicHref":"login-and-auth.html"},{"name":"Log Viewer","href":"log-viewer.html","topicHref":"log-viewer.html"},{"name":"Program and Hosting","href":"program-and-hosting.html","topicHref":"program-and-hosting.html"}]}
@@ -0,0 +1,508 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Tree Ids and Routes | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Tree Ids and Routes | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-TreeIdsAndRoutes">
<h1 id="tree-ids-and-routes">Tree Ids and Routes</h1>
<p>The web client addresses itself with two things: a vue-router path, and — on the two Control-Tree
pages — a <code>tree</code> query argument naming the selected tree node, as in
<code>/general-setup?tree=equipment/fixture/geometry</code>. 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 <code>?tree=</code> id. The router runs in HTML5 history
mode, so every one of these paths is a real URL the server has to answer.</p>
<div class="IMPORTANT">
<h5>Important</h5>
<p>Nothing here promises that a Control-Tree id is stable. The guarantee is narrower and exact: an
id keeps <strong>resolving</strong>, because every regroup adds a migration hop to <code>migrateLegacyTreeId</code>
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.</p>
</div>
<h2 id="the-route-table">The Route Table</h2>
<p><code>/</code> 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.</p>
<table>
<thead>
<tr>
<th>Path</th>
<th>Route name</th>
<th>What it owns</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/</code></td>
<td>—</td>
<td>The shell layout. It carries no page of its own; its empty child redirects to the Execution route.</td>
</tr>
<tr>
<td><code>/execution</code></td>
<td><code>execution</code></td>
<td>The run cockpit and the Execution Control Tree, whose selection rides <code>?tree=execution/…</code>.</td>
</tr>
<tr>
<td><code>/general-setup</code></td>
<td><code>general-setup</code></td>
<td>The equipment Control Tree, whose selection rides <code>?tree=equipment/…</code>.</td>
</tr>
<tr>
<td><code>/machine-tool</code></td>
<td><code>machine-tool</code></td>
<td>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.</td>
</tr>
<tr>
<td><code>/controller/:tab?</code></td>
<td><code>controller</code></td>
<td>The superseded HardNcEnv controller surface. The optional segment mirrors the active tab.</td>
</tr>
<tr>
<td><code>/tool-house/:toolId(\d+)?/:tab?/:subtab?</code></td>
<td><code>tool-house</code></td>
<td>The tool library and the per-tool editor. The three optional segments carry the selected tool and the two nested tab levels.</td>
</tr>
<tr>
<td><code>/preference/log</code></td>
<td><code>preference-log</code></td>
<td>The Log Viewer, reached from the menu bar's <code>Show Log</code> button. It is not under <code>/util/</code>.</td>
</tr>
<tr>
<td><code>/util/file-explorer/:location(.*)*</code></td>
<td><code>util-file-explorer</code></td>
<td>The File Explorer. Its catch-all mirrors the browsed location — root display name followed by the relative segments — into the path.</td>
</tr>
<tr>
<td><code>/util/mech-builder</code></td>
<td><code>util-mech-builder</code></td>
<td>The Mechanism Builder.</td>
</tr>
<tr>
<td><code>/login</code></td>
<td><code>login</code></td>
<td>The sign-in form. It sits outside the shell layout and carries its own Quasar layout.</td>
</tr>
<tr>
<td><code>/:catchAll(.*)*</code></td>
<td>—</td>
<td>The 404 page, likewise outside the shell layout: it prints the unmatched full path and offers one button back to the Execution route.</td>
</tr>
</tbody>
</table>
<p>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,
then the legacy Controller below a second one. The Log Viewer has a button of its own rather than a
menu entry, and <code>/machine-tool</code> has neither — it is reached by URL.</p>
<p>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 <code>/tool-house/cutter</code> 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 —
<code>/tool-house/abc/def/ghi</code> — 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
<a href="#the-tool-house-translation">The Tool House Translation</a> sets out.</p>
<h2 id="redirect-only-paths">Redirect-Only Paths</h2>
<p>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.</p>
<table>
<thead>
<tr>
<th>Path</th>
<th>Lands on</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/spindle-capability/:tab?</code></td>
<td><code>/general-setup?tree=equipment/spindle</code>, with <code>/&lt;tab&gt;</code> appended when the segment is one of <code>thermal</code>, <code>gear-shift</code>, <code>dry-run</code>, <code>power</code>, <code>torque</code>. An unrecognised segment is dropped and the branch root is selected.</td>
</tr>
<tr>
<td><code>/equipment/spindle</code></td>
<td><code>/general-setup?tree=equipment/spindle</code></td>
</tr>
<tr>
<td><code>/fixture/:rest(.*)*</code></td>
<td><code>/general-setup?tree=equipment/fixture</code> — any trailing path is discarded.</td>
</tr>
<tr>
<td><code>/workpiece/:rest(.*)*</code></td>
<td><code>/general-setup?tree=equipment/workpiece</code> — any trailing path is discarded.</td>
</tr>
<tr>
<td><code>/equipment/background-coolant</code></td>
<td><code>/general-setup?tree=equipment/background</code>. Background and Coolant are two separate tree nodes; the redirect picks Background.</td>
</tr>
<tr>
<td><code>/mission</code></td>
<td><code>/execution?tree=execution/mission</code></td>
</tr>
</tbody>
</table>
<p>The five spindle tab names are declared in <code>treeRoutes.ts</code> as <code>SPINDLE_TABS</code>, and imported by
<code>wwwroot-src/src/router/routes.ts</code> — which is the list the redirect tests its <code>:tab?</code> segment
against. The equipment tree does not import that constant: it spells the same five segments as
literals while building the <code>equipment/spindle/&lt;seg&gt;</code> 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.</p>
<h2 id="the-tree-query">The <code>?tree=</code> Query</h2>
<p>Selection and URL are synced two ways by the Control-Tree host, one instance per tree page.</p>
<p><strong>URL to selection.</strong> A watcher on the query drives <code>applyRouteSelection()</code>, and it is registered
<code>immediate</code>, 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.</p>
<p><strong>Selection to URL.</strong> A watcher on the selected id replaces the <code>tree</code> argument, preserving the
rest of the query. It uses <code>router.replace</code>, 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 <strong>raw</strong>
query rather than the migrated form, which is what makes an older id canonicalise: opening
<code>?tree=controller</code> selects the node and then rewrites the URL to <code>?tree=equipment/controller</code>.</p>
<p>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.</p>
<p>That same checkbox flip is a third writer of the <code>tree</code> 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 <code>tree</code> with <code>equipment</code> — 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.</p>
<h3 id="what-applyrouteselection-does">What <code>applyRouteSelection</code> Does</h3>
<ol>
<li>The change is ignored unless this page owns the current route.</li>
<li>The raw query is migrated into today's id. If its first segment names a <strong>different</strong> page and
that segment is one of the known roots, the location is replaced with the route that owns it and
the pass stops.</li>
<li>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.</li>
<li>An id the built tree does not contain is <strong>ignored</strong> — 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.</li>
<li>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.</li>
</ol>
<h3 id="the-id-the-url-names-before-the-tree-exists">The Id the URL Names, Before the Tree Exists</h3>
<p><code>treePathOfRoute()</code> — the raw query put through <code>migrateLegacyTreeId</code> — is not read only by the
selection logic. The equipment tree consults it while it is being <strong>built</strong>, 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.</p>
<h2 id="id-migration">Id Migration</h2>
<p><code>migrateLegacyTreeId</code> is one pass of ordered guards over the whole id; the first that matches
returns, and an id matching none is returned unchanged.</p>
<table>
<thead>
<tr>
<th>An id shaped like</th>
<th>Becomes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>controller</code>, <code>controller/…</code></td>
<td><code>equipment/controller</code>, <code>equipment/controller/…</code></td>
</tr>
<tr>
<td><code>spindle</code>, <code>spindle/…</code></td>
<td><code>equipment/spindle</code>, <code>equipment/spindle/…</code></td>
</tr>
<tr>
<td><code>mission</code>, <code>mission/…</code></td>
<td><code>execution/mission</code>, <code>execution/mission/…</code></td>
</tr>
<tr>
<td><code>equipment/mission</code>, <code>equipment/mission/…</code></td>
<td><code>execution/mission</code>, <code>execution/mission/…</code></td>
</tr>
<tr>
<td>any id ending in <code>/contours/tray</code>, or containing <code>/contours/tray/</code></td>
<td>the same id with that segment spelled <code>fluting</code></td>
</tr>
</tbody>
</table>
<p>The last rule renames one segment in place and leaves its children — <code>baseline</code>, <code>flute-&lt;i&gt;</code>,
<code>side</code>, <code>bottom</code> — 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.</p>
<p>The function is applied at four points, which is what makes the guarantee hold in practice:</p>
<ul>
<li>to the raw <code>?tree=</code> query, on every read;</li>
<li>to the persisted last selection, before it is looked up in the tree;</li>
<li>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;</li>
<li>as the first step of <code>routeForTreeId</code>, so the id-to-page resolver never sees an unmigrated id.</li>
</ul>
<p>Note what the first rule implies: a bare <code>?tree=controller</code> selects the <code>equipment/controller</code>
branch — the current controller editor on the General Setup page — and has nothing to do with the
<code>/controller</code> route, which is the separate legacy HardNcEnv surface.</p>
<h2 id="landing-an-id-on-its-page">Landing an Id on Its Page</h2>
<p><code>TREE_PAGE_ROOTS</code> lists the first segments that name a page: <code>execution</code>, <code>equipment</code> and
<code>toolhouse</code>. <code>spindle</code> is absent on purpose — migration folds it into <code>equipment/…</code> before any root
check runs, so no root check ever sees it.</p>
<p><code>routeForTreeId</code> migrates the id, then dispatches on its first segment:</p>
<ul>
<li><code>toolhouse</code> — the Tool House route, with the id's segments translated into path params (below).</li>
<li><code>equipment</code> — the General Setup route, carrying the migrated id as <code>?tree=</code>.</li>
<li>anything else — the Execution route, carrying the migrated id as <code>?tree=</code>. This is a
fall-through, not a test for <code>execution</code>, 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 <code>TREE_PAGE_ROOTS</code>.</li>
</ul>
<p>The returned location replaces the whole current location. Only the <code>tree</code> 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.</p>
<h2 id="the-tool-house-translation">The Tool House Translation</h2>
<p>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. <code>routeForTreeId</code> walks the segments of
<code>toolhouse/tool-&lt;n&gt;/&lt;tab&gt;/&lt;subtab&gt;</code>:</p>
<ul>
<li>segment two contributes <code>toolId</code> when it is <code>tool-</code> followed by an integer;</li>
<li>segment three contributes <code>tab</code> when it is one of <code>general</code>, <code>cutter</code>, <code>holder</code>, <code>clamping</code>,
<code>intelligent</code>;</li>
<li>segment four contributes <code>subtab</code> when it is one of the cutter sections — <code>material</code>, <code>profile</code>,
<code>contours</code>, <code>upper-beam</code>, <code>opt</code> — or one of the holder sections, <code>geometry</code> and <code>resolution</code>.</li>
</ul>
<p>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 <code>tool-&lt;n&gt;</code> costs the
<code>toolId</code> 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
<code>…/cutter/contours/fluting/…</code> 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.</p>
<p>Once on the page, the tab segments are kept in step with the tabs by <code>useRouteTabs</code>, the composable
the Controller route uses as well. 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 <strong>dormant</strong> — 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 <code>cutter</code> and <code>holder</code> 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 <code>/controller</code> becomes <code>/controller/coordinate-table</code> and a
bare <code>/tool-house</code> 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
<code>toolId</code> param and to the tool list rather than to page activation, so it also re-points the URL
when the list changes underneath it.</p>
<p>The Tool House page never reads or writes <code>?tree=</code>. A <code>toolhouse/…</code> id reaches it only by way of
the redirect above, which is issued by one of the two tree pages' hosts.</p>
<h2 id="reading-an-id">Reading an Id</h2>
<p>An id's first segment names the page that owns it, and therefore the folder that documents it:</p>
<table>
<thead>
<tr>
<th><code>?tree=</code> root</th>
<th>Route</th>
<th>Documented under</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>execution/…</code></td>
<td><code>/execution</code></td>
<td><a class="xref" href="../execution/index.html">Execution Page</a></td>
</tr>
<tr>
<td><code>equipment/…</code></td>
<td><code>/general-setup</code></td>
<td><a class="xref" href="../general-setup/index.html">General Setup Page</a></td>
</tr>
<tr>
<td><code>toolhouse/…</code></td>
<td><code>/tool-house/…</code></td>
<td><a class="xref" href="../tool-house/index.html">Tool House Page</a></td>
</tr>
</tbody>
</table>
<p>The remaining segments are the branch path, one segment per level of the tree, and the branch's own
page sits in that folder — <code>equipment/workpiece/material/cutting-parameter</code> 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.</p>
<p>Every segment comes from the <strong>node id</strong>, never from the label the user reads: the General Setup
page's root node is labelled <code>General Setup</code> and carries the id <code>equipment</code>. A label is a display
string — the translation of the node's <code>labelKey</code> where it has one, and otherwise raw data such as
an NC file name or a mission command's user-typed title, as <a class="xref" href="control-tree.html">Control Tree</a>
sets out — while an id is neither translated nor taken from data. Two consequences follow. A
<code>?tree=</code> link is locale-independent, and an id can never be read off what is on screen.</p>
<h2 id="titles">Titles</h2>
<p>A route's <code>meta.title</code> holds an i18n <strong>key</strong>, such as <code>routes.execution</code>, not a title. The router
resolves it through the active locale in an <code>afterEach</code> hook and sets the document title to the
translated name followed by <code> - HiNC</code>; a route with no key — the catch-all — leaves the bare
product name. The sibling <code>name:</code> 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.</p>
<p>The same resolver is handed to the i18n module through <code>registerRetitle</code>, 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.</p>
<h2 id="serving-these-urls">Serving These URLs</h2>
<p>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.</p>
<p>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 — <code>?tree=</code> included — parked
in a <code>redirect</code> 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.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>wwwroot-src/src/router/routes.ts</code> — the route table, the redirect-only records, the login route
outside the shell layout, and the trailing catch-all.</li>
<li><code>wwwroot-src/src/router/treeRoutes.ts</code> — <code>migrateLegacyTreeId</code>, <code>TREE_PAGE_ROOTS</code>,
<code>routeForTreeId</code>, the three tab-name lists it shares with the Tool House page, and
<code>SPINDLE_TABS</code>, which only the spindle redirect reads.</li>
<li><code>wwwroot-src/src/router/index.ts</code> — the history-mode router factory, the authentication guard,
and the retitle hook registered with the i18n module.</li>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — the tree-page host:
<code>rawTreePathOfRoute</code>, <code>treePathOfRoute</code>, the two selection/URL watchers and the unguarded
controller-checkbox watcher beside them, <code>landingSelection</code>, <code>applyRouteSelection</code> and
<code>syncUrlBack</code>, plus the build-time reveal of a URL-named CSV or CL controller node.</li>
<li><code>wwwroot-src/src/composables/useRouteTabs.ts</code> — the tab-to-param sync used by the Tool House and
Controller routes, including dormant levels and the keep-alive re-entry canonicalisation.</li>
<li><code>wwwroot-src/src/pages/ToolHousePage.vue</code> — consumes the shared tab lists, resolves the tool from
its path param, and navigates by route name when the tool changes.</li>
<li><code>wwwroot-src/src/pages/FileExplorerPage.vue</code> — mirrors the browsed location into the route's
catch-all param and back.</li>
<li><code>wwwroot-src/src/pages/MachineToolPage.vue</code> — the component behind the URL-only machine-tool
route.</li>
<li><code>wwwroot-src/src/pages/NotFoundPage.vue</code> — the catch-all component, carrying its own layout.</li>
<li><code>wwwroot-src/src/pages/LoginPage.vue</code> — reads the guard's <code>redirect</code> argument and replays it.</li>
<li><code>wwwroot-src/src/composables/useViewPrefs.ts</code> — the browser-local store holding the per-page last
selected id and the expanded-node list that migration is applied to.</li>
<li><code>wwwroot-src/src/components/AppMenuBar.vue</code> — the menu entries that navigate by route name.</li>
<li><code>wwwroot-src/src/i18n/en/routes.ts</code> — the English strings the <code>meta.title</code> keys resolve to.</li>
<li><code>wwwroot-src/src/i18n/index.ts</code> — <code>registerRetitle</code> and the locale switch that calls it.</li>
<li><code>wwwroot-src/quasar.config.ts</code> — selects history mode and names the build output folder the
server serves.</li>
<li><code>Program.cs</code> — the two SPA fallbacks, the explorer-specific one ahead of the bare one.</li>
</ul>
<h2 id="see-also">See Also</h2>
<ul>
<li><a class="xref" href="control-tree.html">Control Tree</a> — the tree these ids name: how a node id is formed and what a branch
is made of</li>
<li><a class="xref" href="../shell/main-panel.html">Main Panel</a> — the shell whose menus navigate to these routes and whose keep-alive
cache makes the route-name guards necessary</li>
<li><a class="xref" href="../execution/index.html">Execution Page</a> — the busiest consumer of <code>?tree=</code>, and the page a fall-through id lands
on</li>
</ul>
</article>
<div class="contribution d-print-none">
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<span> Copyright © 2025 <a href='https://superhightech.com.tw'>Tech Coordinate</a>. All rights reserved. <a href='https://superhightech.com.tw'>超級高科技股份有限公司</a> © 2025 版權所有 </span>
</div>
</div>
</footer>
</body>
</html>