This repository has been archived on 2026-10-11. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
HiNC-2025-win-desktop-Deploy/App/wwwroot/HiAPI-docsite/anatomy/platform/session-state.html
T
2026-09-12 18:16:27 +08:00

428 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Session State | HiAPI-C# 2025 </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Session State | HiAPI-C# 2025 ">
<link rel="icon" href="../../img/HiAPI.favicon.ico">
<link rel="stylesheet" href="../../public/docfx.min.css">
<link rel="stylesheet" href="../../public/main.css">
<meta name="docfx:navrel" content="../../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../../">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../../index.html">
<img id="logo" class="svg" src="../../img/HiAPI.logo.png" alt="">
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="Anatomy-SessionState">
<h1 id="session-state">Session State</h1>
<p>Session state is what the web client still holds after the user moves from one screen to the next. It
owns no route: the mechanism lives in the shell that wraps every route under <code>/</code> — a router outlet
inside a <code>&lt;keep-alive&gt;</code> keyed on a project epoch — together with four Pinia stores, a family of
module-level composables that deliberately sit outside the page lifecycle, and three browser storage
keys. Every value in the client answers the same three questions: does it survive a navigation, does
it survive a project change, and does it survive a browser reload.</p>
<h2 id="the-project-epoch">The Project Epoch</h2>
<p><code>MainLayout.vue</code> renders the routed page inside a <code>&lt;keep-alive&gt;</code> whose <code>:key</code> is <code>projectEpoch</code>. That
epoch is a plain integer <code>ref</code> declared in the layout component itself — it is not a store value, it
is not provided to anything, and nothing outside the file reads it. Two watchers in the same file are
the only writers, and each adds one to it: a watcher on the project store's <code>projectPath</code>, and a
watcher on the project store's <code>projectVersion</code>.</p>
<p>The key sits on the <code>&lt;keep-alive&gt;</code> element rather than on the page component inside it. Changing it
therefore discards the cache itself along with every page instance held in it and builds a fresh one,
which is why a page may rely on its own mount hook and never watch the project: both Control-Tree
pages call their host's <code>initialize()</code> from <code>onMounted</code>, and that runs once per project rather than
once per visit.</p>
<p>The two inputs move for different reasons.</p>
<ul>
<li><strong><code>projectPath</code></strong> is hydrated once at boot by the store's status fetch, assigned by New, Load and
Save As from the response each returns, cleared outright by Close, and driven thereafter by a
watcher inside the project store on the shared execution-status hub's status payload. A project
change made in another tab, from a different browser, or outside the browser altogether therefore
bumps the epoch here as well — and so, once, does the boot hydration on a service that already has
a project open.</li>
<li><strong><code>projectVersion</code></strong> is a counter the store raises in exactly one place: ReLoad. The backend raises
its project-changed event with the path it already had, so the broadcast cannot be told apart from
a no-op; the client-side counter is what makes a reload remount.</li>
</ul>
<p>Save on the current path moves neither, so it does not remount. The epoch only counts up, and because
it lives in the layout it starts again at zero on a browser reload — nothing reads its value, only its
transitions.</p>
<h2 id="what-a-navigation-costs">What a Navigation Costs</h2>
<p>The <code>&lt;keep-alive&gt;</code> carries no <code>include</code>, <code>exclude</code> or <code>max</code>, so every page reached under <code>/</code> is cached
until the epoch changes. Leaving a route deactivates its component instead of unmounting it, and
returning re-activates the same instance with its in-flight edits and its Control-Tree host intact.
Scroll offsets are not part of that: the router's <code>scrollBehavior</code> resolves every navigation to the
top of the page, and nothing saves an inner scroll position across a deactivation. The two routes
declared outside the layout — the login screen and the not-found catch-all — are not cached at all.</p>
<p>Three consequences the pages are written around:</p>
<ul>
<li>A cached page's watchers keep firing while another page is showing. The Control-Tree host compares
the current route name against the one its scope owns before it writes the <code>?tree=</code> query back, so
an off-screen host cannot rewrite the visible page's URL.</li>
<li>A global listener has to be dropped on deactivation, not on unmount. The Execution transport binds
its F5–F8 shortcuts on mount and on activation and removes them on deactivation and unmount, so on
any other page F5 falls back to the browser's own reload instead of starting the run.</li>
<li>The Execution, General Setup and Tool House pages each track their own activation in a flag that
feeds the rendering canvas's <code>active</code> prop. Such a page keeps its canvas mounted and its connection
open while the backend engine stops rendering for it.</li>
</ul>
<p>The File Explorer page adds a route-leave guard that settles its editor buffer — flushing a pending
auto-save, or prompting before discarding manual edits — and can refuse the navigation.</p>
<h2 id="the-stores">The Stores</h2>
<p>Four Pinia stores are created against the single Pinia instance built at boot. None of them is inside
the <code>&lt;keep-alive&gt;</code>, so a project change resets none of them; only a browser reload rebuilds them.</p>
<ul>
<li><strong>project</strong> holds <code>projectPath</code>, <code>projectVersion</code>, the admin and project directories, a loading flag
and the <code>hasProject</code> computed. Its actions post to the project endpoints and each throws a typed busy
error on HTTP 409, which the backend returns when another project file operation is already running —
the request is refused rather than queued. The store also owns the subscription to the shared
execution-status hub and the watcher that adopts the path that hub broadcasts.</li>
<li><strong>appState</strong> holds the debug flag (initialised from whether the page is served from <code>localhost</code>), the
physics-options and physics-licensed flags, the language code with the list of codes the server
offers, and the Execution division-visibility record. <code>loadServerPreferences</code>, called from the
layout's mount hook, hydrates those three groups in independently guarded steps, so a failing
endpoint leaves the rest usable. A division-flag write is applied optimistically and rolled back on
failure, with a monotonic sequence number so a late response cannot overwrite newer state. Its
<code>setMessage</code> is a forwarder into the footer's foreground channel.</li>
<li><strong>auth</strong> holds whether the login gate is enabled, whether this session is authenticated, the user
name, the version string the login screen shows, and a ready flag set once the first status probe
resolves. Every transition re-drives the global hub gate described below.</li>
<li><strong>routineProgress</strong> backs the footer's two channels. The foreground channel keeps the latest entry
plus a history capped at one hundred entries, which the footer's history button lists; the background
channel is a single in-flight job with a message and an optional fraction and keeps no history at
all, so it disappears when the job ends. A boot patch mirrors every Quasar toast into the foreground
channel, so a toast stays reviewable after it fades.</li>
</ul>
<h2 id="state-outside-the-stores">State Outside the Stores</h2>
<p>A second family of state lives at module scope in composables rather than in a store. The module is
evaluated once per page load and its <code>ref</code>s are created there, so every caller of the composable
receives the same objects and nothing in a component's lifecycle creates or destroys them.</p>
<p>Three of these hold project data and share one lifecycle idiom — <code>useSpindleCapability</code>,
<code>useSoftNcRunner</code> and <code>useToolHouse</code>. Each exposes an idempotent <code>ensureInstalled()</code> that, on its first
call only, opens a detached effect scope and watches <code>hasProject</code>: the state reloads when a project
appears and is cleared when one closes. Every panel that reads the singleton calls <code>ensureInstalled()</code>
in its own setup, and every call after the first does nothing.</p>
<p>The consequence is the one that matters when reading a panel on screen: <strong>a mounted panel is not
refreshed by a change made elsewhere.</strong> A project change unmounts and rebuilds every page, but it does
not re-read a singleton — the installer is already installed, and the watch it installed is on a
boolean that the load and new actions leave true. What each singleton holds is replaced only by an
action on the composable itself, or by a consumer that asks for it explicitly: the Tool House page's
mount hook calls the composable's <code>reload()</code>, while the spindle and controller panels call
<code>ensureInstalled()</code> alone. The controller branch is the sharpest case. Its leaf panels fetch their own
tables on mount and the tree builder re-fetches the runner snapshot it grows children from, but the
shared snapshot the panels gate on — the brand, the chain axes, the per-group presence flags and the
object key — is replaced only by a brand switch, an Object-Management install, or a close.</p>
<p>The remaining module singletons carry no project data and exist to join components that are not in one
another's tree: the Execution runtime flags the run page publishes for the nav bar, the sentence cursor
shared by the Program file panel and the syntax view, the strip-chart group's reload tick and hovered
x label, the two cycle-chart cursor marks the sim and sensor chart groups share, the parked cross-panel
line jump, the transport's shared reset flag, and the shared-hub registry with its auth gate.</p>
<h2 id="device-local-state">Device-Local State</h2>
<p>Three <code>localStorage</code> keys hold state belonging to one browser profile on one machine. None of them is
written into the project file and none is sent to the server.</p>
<ul>
<li>The tree-page view preferences key holds, per tree page, the column-visibility record and the two
left-dock row states; and, shared across both pages, the dock pixel widths, the editor row height,
the strip/step ratio, the two chart legend widths, the strip x-axis mode, the per-panel resize
weights, the Control-Tree expansion list, the last selection per page, the two runner-suit
checkboxes, and the three device-local panel switches for the 3D canvas, the CWE canvas and the
sentence syntax view. Writes are debounced, and two earlier key names are read once as migration
seeds when the current key is absent.</li>
<li>The locale key is a paint-time hint only: the boot sequence applies it before mount so the first
frame is already translated, then reconciles against the server value, which wins.</li>
<li>The File Explorer key holds whether its editor pane is shown, the pane split, the auto-save switch
and the sort order.</li>
</ul>
<p>What is <strong>not</strong> device-local is easy to mistake for it. The physics-options switch, the interface
language and the Execution division-visibility flags live in the server's <code>UserConfig</code>, held by a
singleton service and written to an XML file in the service's working directory. They are per install,
not per browser: two browsers pointed at the same service read and write the same values. The
machining project is server-side in the same way — one project is loaded at a time and every connected
browser sees it, which is why the status-hub broadcast is what keeps a second tab honest.</p>
<p>The URL carries the rest: the route, the <code>?tree=</code> selection on the two Control-Tree pages, the browsed
path on the File Explorer route, and the tab segments on the Tool House route. 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 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 on the Tool House page. Its composable
also holds the set of IndexService keys that owner registered; on unmount it
posts a removal for every one of them and then stops the connection, so a teardown releases the
server-side objects the page had indexed even if the hub never connected.</li>
</ul>
<p>A shared connection therefore closes when its last consumer has been gone longer than the grace window,
or when the auth gate shuts. A per-instance connection closes when the component owning it unmounts —
which is what a project change does to every page at once, while the shared hubs ride that same remount
out inside their grace window.</p>
<h2 id="boot-order">Boot Order</h2>
<p><code>App.vue</code> renders nothing but the router outlet and holds the wiring that must run exactly once per
page load: subscribe the project hub, then fetch project status. Both sit behind a watcher on the auth
predicate with a latch rather than behind a mount hook, because auth status may still be resolving at
first paint. The router's own guard hydrates auth status before the first navigation and fails open, so
a status hiccup cannot lock out an installation that has no login gate. The layout's mount hook then
hydrates the server preferences. A successful sign-in navigates with a full page load rather than a
router push, so the whole sequence runs again with the authentication cookie present.</p>
<h2 id="source-code-path">Source Code Path</h2>
<p>See <a class="xref" href="../index.html">HiNC App Anatomy</a> for git repository links.</p>
<h3 id="web-application">Web Application</h3>
<p>HiNC-2025-webservice (Quasar CLI SPA):</p>
<ul>
<li><code>wwwroot-src/src/layouts/MainLayout.vue</code> — the project epoch, its two watchers, the keyed
<code>&lt;keep-alive&gt;</code> around the router outlet, the footer, and the server-preference hydration call.</li>
<li><code>wwwroot-src/src/App.vue</code> — the auth-gated one-shot wiring: the project hub subscription and the
first status fetch.</li>
<li><code>wwwroot-src/src/stores/index.ts</code> — the Pinia instance the four stores are created against.</li>
<li><code>wwwroot-src/src/stores/project.ts</code> — the project path and version, the directories, the file actions
and their busy error, the hub subscription, and the watcher that adopts the broadcast path.</li>
<li><code>wwwroot-src/src/stores/appState.ts</code> — the debug and physics flags, the language state, the Execution
division config with its optimistic write and sequence guard, and the footer forwarder.</li>
<li><code>wwwroot-src/src/stores/auth.ts</code> — the login-gate state and the hub gate it drives.</li>
<li><code>wwwroot-src/src/stores/routineProgress.ts</code> — the footer's foreground history and its single live
background job.</li>
<li><code>wwwroot-src/src/composables/useViewPrefs.ts</code> — the device-local view preferences: the shape, the
defaults, the migration seeds and the debounced write.</li>
<li><code>wwwroot-src/src/composables/useSoftNcRunner.ts</code> — the controller singleton: the shared runner
snapshot, the object key, and the install-once project watch.</li>
<li><code>wwwroot-src/src/composables/useSpindleCapability.ts</code> — the spindle singleton on the same lifecycle
idiom.</li>
<li><code>wwwroot-src/src/composables/useToolHouse.ts</code> — the tool-house singleton and the coalesced reload the
page calls on mount.</li>
<li><code>wwwroot-src/src/composables/useSharedHub.ts</code> — the shared-hub factory: consumer counting, the
teardown grace window, the auth gate, the retry schedule and the focus recovery.</li>
<li><code>wwwroot-src/src/composables/useExecutionStatusHub.ts</code> — the status, cursor and message payloads the
store and the footer read.</li>
<li><code>wwwroot-src/src/composables/useClStripHub.ts</code> — the strip snapshot and update counter the charts
watch.</li>
<li><code>wwwroot-src/src/composables/useSessionSinkHub.ts</code> — the four message sinks and their
notify-and-re-pull contract.</li>
<li><code>wwwroot-src/src/composables/useCleanupHub.ts</code> — the per-instance cleanup connection and the key set
it releases on unmount.</li>
<li><code>wwwroot-src/src/composables/useExecutionTransport.ts</code> — the activation-scoped keyboard shortcuts and
the shared reset flag.</li>
<li><code>wwwroot-src/src/composables/useExecutionRuntime.ts</code> — the runtime flags the run page publishes.</li>
<li><code>wwwroot-src/src/composables/useSentenceCursor.ts</code> — the shared source cursor and its install-once
watch on step selection.</li>
<li><code>wwwroot-src/src/composables/useStripChartGroup.ts</code> — the group reload tick and the hovered x label.</li>
<li><code>wwwroot-src/src/composables/useCycleSyncMark.ts</code> — the per-group cycle-chart cursor mark the sim
and sensor charts share.</li>
<li><code>wwwroot-src/src/composables/useConversionJump.ts</code> — the parked cross-panel jump.</li>
<li><code>wwwroot-src/src/components/RenderingCanvas.vue</code> — the per-instance rendering connection, its mount
and unmount lifecycle, and the mounted guard that survives a mid-await teardown.</li>
<li><code>wwwroot-src/src/components/panels/PanelExpansion.vue</code> — the expansion row, and the keep-mounted flag
that decides whether a collapse unmounts its content.</li>
<li><code>wwwroot-src/src/components/AppFooter.vue</code> — the two footer channels and the recent-message list.</li>
<li><code>wwwroot-src/src/components/FileExplorer.vue</code> — the explorer's own device-local preference key.</li>
<li><code>wwwroot-src/src/components/controlTree/useControlTreeHost.ts</code> — the per-page host: the route-name
guard on the <code>?tree=</code> sync, the landing selection, and the cleanup registrations.</li>
<li><code>wwwroot-src/src/pages/ExecutionPage.vue</code> — the activation flag, the once-per-project initialization,
and the panel that keeps the 3D canvas mounted through a collapse.</li>
<li><code>wwwroot-src/src/pages/GeneralSetupPage.vue</code> — the same lifecycle for the equipment host.</li>
<li><code>wwwroot-src/src/pages/ToolHousePage.vue</code> — the page that reloads its singleton explicitly on mount.</li>
<li><code>wwwroot-src/src/pages/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>