Archived
deploy.
This commit is contained in:
@@ -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 "{query}"">
|
||||
<meta name="loc:searchNoResults" content="No results for "{query}"">
|
||||
<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-<id>/…</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/<seg></code> and <code>…/program-data/<seg></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 “nothing selected” 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 “Loading project…” — 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 “Select an item in the Control Tree to edit it here.”</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 “The selected item has no expanded content.”</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-<id></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 "{query}"">
|
||||
<meta name="loc:searchNoResults" content="No results for "{query}"">
|
||||
<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="en"</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.<id></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 “must be ≥” message naming the field's minimum, and a “must be ≤” 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 "{query}"">
|
||||
<meta name="loc:searchNoResults" content="No results for "{query}"">
|
||||
<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 "{query}"">
|
||||
<meta name="loc:searchNoResults" content="No results for "{query}"">
|
||||
<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 “no log yet” 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-<date>.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 "{query}"">
|
||||
<meta name="loc:searchNoResults" content="No results for "{query}"">
|
||||
<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 “is the gate on by default”, 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 “not authenticated” 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 “Incorrect username or password” 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 "{query}"">
|
||||
<meta name="loc:searchNoResults" content="No results for "{query}"">
|
||||
<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<ProxyConfig></code> call also registers it through the options
|
||||
system, but nothing resolves <code>IOptions<ProxyConfig></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
|
||||
“everything else is the SPA” 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 “no limit” 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>"Infinity"</code>, <code>"-Infinity"</code> and <code>"NaN"</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>"NaN"</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 "{query}"">
|
||||
<meta name="loc:searchNoResults" content="No results for "{query}"">
|
||||
<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><keep-alive></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><keep-alive></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><keep-alive></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><keep-alive></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><keep-alive></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><keep-alive></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 "{query}"">
|
||||
<meta name="loc:searchNoResults" content="No results for "{query}"">
|
||||
<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>/<tab></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/<seg></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-<i></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-<n>/<tab>/<subtab></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-<n></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>
|
||||
Reference in New Issue
Block a user