Editing Contract
Every leaf of the Controller branch edits a different table, and almost all of them edit it the same
way. This page is that shared way: what a leaf panel fetches, when an edit commits, what a failed
write does, what a panel shows while its table is absent, and what it never notices. It owns no
Control-Tree node of its own — it holds over every panel of the Controller branch on
/general-setup: the root at equipment/controller, and every leaf beneath
equipment/controller/machine and equipment/controller/program-data. Each leaf page can then
state only what is its own. The last section is the other half of the contract: every panel that
departs from it.
Important
Anatomy makes no claim that these ids are stable across versions. See Tree Ids and Routes for the migration hop that keeps an older link resolving.
The Shape of a Leaf Panel
Each leaf panel takes one prop, the tree node, and reads the shared runner snapshot through a module-singleton composable whose installer it calls synchronously while its script runs rather than on mount. The installer is idempotent, so whichever panel mounts first pays for it and the rest get the cache. Only four panels read the node prop at all: two use its id to tell apart the sibling leaves they serve, and two use it as the scope of the structural change they report.
Beyond that shared snapshot, a panel owns its own table. It imports its reader and its setters
directly from the branch's typed API module and fetches once, in onMounted. There is no store, no
request queue and no batch: one panel, one read, one table. The two panels that fetch nothing — the
branch root and Controller Brand — are exactly the two that have no table of their own and render
off the snapshot alone. How the branch itself is built, rebuilt and selected belongs to the
Control-Tree engine and is described in Control Tree.
Only one panel is mounted at a time. The editor row's remount key is composed from the node id, the node key and the mission stamp; controller nodes carry an empty key and no mission record, so the key reduces to the node id alone. Moving the selection therefore remounts and refetches; re-clicking the row already selected does neither. No controller item type registers a large content view, so the General Setup content column keeps its own empty hint for every selection in this branch.
When an Edit Commits
The commit rule is per control kind, and every panel that uses a given kind uses it the same way.
Numeric fields are the shared numeric widget, and it commits on blur or on Enter and on nothing else — never per keystroke. Its full contract, including what a bound violation does to the box, is Numeric Input. Three consequences matter here. Enter commits without moving focus, so leaving the field afterwards commits the same value a second time. The widget carries no equality guard, so a field that is focused and left unchanged still emits. And the two halves of the rule are not wired alike: blur is the input component's own event and is raised only while the field is editable, whereas Enter is a plain listener on the underlying element and still fires on a read-only one — which is why a read-only cell can still commit.
Checkboxes, toggles and selects commit immediately, on the click or the pick. That covers the block-skip layer boxes, the tool-change Stays put box, the M-code tool-change box and its Spindle and Coolant selects, the tool-word trigger toggle, the cutter-compensation and tool-axis selects, and the tool-offset Set ideal offset dependent on tool house toggle.
Plain text fields have no shared widget and are wired twice over. The M-code Not-simulated note commits on the field's own change event, which carries the string and fires on Enter or on leaving a changed field. The two subprogram folder inputs bind blur and Enter explicitly, and commit as a pair.
Row actions — the datum reset-to-zero button, the work-coordinate P0 and M0 buttons, a delete button, an add button — commit on the click, and are not optimistic: they write first and touch the local rows only after the request resolves.
Two panels do not commit per control at all; they stage a draft behind an explicit button, and both are named below.
The Optimistic Write
A per-control commit follows one fixed shape. The handler captures the current value, assigns the new one so the screen updates at once, awaits the setter, and on failure restores the captured value and raises a toast. Nothing is queued and nothing is retried.
Two details of that shape are easy to misread from the screen:
- The write is row-scoped even though the handler is per cell. Editing one cell of a work coordinate, a datum row, a frame or a tool-offset row sends the whole row or the whole XYZ triad as it stands after the edit. An M-code edit likewise sends all four declaration fields, which is why the wire format is a whole-declaration replace rather than a patch.
- The rollback restores what the handler captured, which is usually narrower than what it sent. A work-coordinate, datum, frame or tool-offset handler captures the one cell it edited and puts back only that, though the request carried the whole row. Three handlers capture the whole set they send and restore it intact — the M-code declaration row, the tool-change Stays put pair, and the two subprogram folders. Either way, a failure the server had partly applied leaves the two sides disagreeing; the panel's recovery is the toast and a later remount, not a re-read.
Adds and removes are outside this shape, and the local rows never move before the request resolves.
Two panels then append the returned row themselves — the generic tool-offset table and the Siemens
$TC_DP table, the two whose add hands back a freshly minted key. The other five re-read their whole
table instead: M-Code Declarations, Tool Names, R Parameters, Retained Common Variables and
Parameters (Native). A delete usually filters the row out in place; Parameters (Native) re-reads
there too. Three deletions are guarded by a confirmation dialog first: a tool-offset row, a Siemens
$TC_DP row, and a native parameter. M-code declarations, tool names, R parameters and retained
common variables delete on the click with no confirmation.
When a Write Fails
The branch's REST surface answers a missing dependency inside a success envelope rather than with an
error status: the dependency lookup returns HTTP 200 carrying success: false and a message naming
the type it could not find, such as No IStrokeLimitConfig on the active runner, and an exception
raised inside the same lookup is reported the same way. The shared fetch helper turns both a non-2xx
status and a success: false body into a thrown error, so a panel cannot tell the two apart; the
first arrives with the status prefixed to the message, the second as the server's own sentence.
What the user sees is one negative toast, three seconds, composed as the panel's localized context followed by the raw server message — the locale bundle stores each error key as the bare context and the code appends the separator and the message. A failed write leaves no inline error state, no retry affordance and no dirty marker: once the toast expires, nothing on screen records that it happened. (The unsaved badge and the staged-brand banner on the two draft panels mark a pending edit, not a failed one.) A console error accompanies the toast.
The server message is not localized. Only a coded error payload can be re-rendered in the app
locale, and the envelope this branch answers with carries no code, so its English sentence is
appended verbatim under any language. The two writes that replace the whole runner answer with real
status codes instead of the envelope: installing one through Object Management, and switching brand.
Of the codes they raise, only not found with no project loaded carries the coded payload — the
conflict raised rather than swapping the parser under a playing NC program, and the refusals of an
unknown key or an unknown brand, are all bare English sentences. One further status code sits on the
datum routes, a defensive 400 for a table segment that is neither preset nor shift; the panel
sends only those two, so nothing reachable from the branch produces it.
The Two Empty Layers
A leaf panel opens with two guards, in this order.
Layer one is the shared snapshot. The shared empty state renders the single line "No NC runner — load a project first." whenever the snapshot reports no runner. Every leaf panel carries it; the branch root is the one panel that does not, because it renders a richer block of its own.
Reaching that line by navigating is not possible: a leaf node is created only when the snapshot already reported a runner, and a deep link naming an id the built tree lacks is refused. The guard is live rather than navigational — it answers the snapshot emptying under a panel that is already mounted, which is what closing the project does.
Layer two is the panel's own read. Where the runner resolves but its table does not, the panel renders one line naming the absent table — "No block-skip config on the active runner.", "No Siemens frame table on the active runner.", "No tool-offset table on the active runner.", and so on, one string per table. Three panels key that line on one table: Controller Parameters, M-Code Declarations and Parameters (Native) all gate on the brand's controller parameter table and all render "No controller parameter table on the active runner." Controller Parameters is the loosest fit — its two iteration-guard fields resolve through dependencies of their own, and only its presence flag is the parameter table.
Controller Brand, which reads no table, has no second layer at all; every other leaf has one. Only after both guards does the editor body render. One panel keys its second layer on something other than its read's presence flag, and it is named below.
What a Mounted Panel Does Not See
The shared snapshot is fetched once per load of the application. The installer's flag lives at module scope, so a later call does nothing and only a browser reload starts over, and the snapshot is re-read on exactly three events afterwards: the project store's has-a-project flag turning true, an Object-Management install of a runner, and a brand switch. The flag turning false re-reads nothing — it clears the snapshot in place. No panel re-reads it either; the composable exposes a reload function that nothing in the branch calls.
Three consequences follow, and they are the contract's sharpest edges.
- A project-to-project load does not refresh it. The has-a-project flag is derived from the project path being non-empty, and loading another project assigns the new path directly, so the flag never leaves true and the watch never fires. The page itself is destroyed and rebuilt, because the layout keys its keep-alive on a project epoch — but the rebuilt panels call the same now-idempotent installer, which does nothing. Meanwhile the branch builder makes its own, independent request for the same snapshot, so the tree shape can be built from the new project while every leaf gates on the old one.
- Nothing pushes changes at a mounted panel. No panel holds a watcher, a poll or a status-hub subscription over its table. A write made by another browser tab, by an NC run, or by the server's own sweep after a brand switch is invisible until the panel is remounted by moving the selection away and back.
- A panel's own edits do not update the snapshot or the branch. Deleting the last row of a table leaves both the cached snapshot and the tree branch as they were until one of the three refresh events happens.
Because the shared snapshot is a reactive reference, a panel does track it live once it changes — but with one panel mounted at a time, the change that matters in practice is a project close.
The Table Primitive
Where a leaf renders a table it is Quasar's markup table — dense, flat, bordered — with a hand-written header and body and a repeat over the rows. No panel in the branch uses the data-table component, and since the legacy controller tabs were removed nothing else in the application does either. The practical difference is that these tables have no built-in sort, no pagination and no column menu: what the header says is what the column is.
Key columns are rendered as plain text and cannot be edited: the tool and edge numbers of a $TC_DP
row, a tool name, an R number, a # variable number, a datum row index, a native parameter id, and
the axis names of every per-axis table, which follow the machining chain rather than the panel. The
one renameable key is the generic tool-offset row's tool number, and it is guarded against duplicates
on both sides of the wire.
A table that can grow carries an add control, and the branch spells it three ways. Five carry a true
footer below the table — the new row's fields, then a primary button: Add / Set on Tool Names,
R Parameters, Retained Common Variables and Parameters (Native), Declare on M-Code Declarations.
Two carry a fieldless button in the toolbar above the table instead, because the server mints the
key: Add on Tool Offsets and Add Tool on the Siemens $TC_DP table. The last is Add
Position on Indexing Position Tables, which appends an empty row to the local draft and sends
nothing. Two tables carry a Show all toggle that hides an all-zero extended tail, on Work
Coordinates and on Frames.
Nothing Guards a Switch Away
No panel in the branch registers the Control-Tree host's before-switch gate, and none exposes anything to the host at all. Selecting another node therefore always succeeds immediately. For the per-control panels that is exactly right — every edit is already on the server. For the two draft panels it means an unsaved draft and a staged brand are discarded silently when the selection moves, with no prompt and no warning.
Where the Wave Is Not Uniform
Everything above is the rule. What follows is every place the branch departs from it, panel by panel. A contract page without this section invites the next editor to tidy it into a uniformity that was never true.
Machine Limits / Rapid Feedrates / Home Reference — the presence flag is discarded
The axis-table panel serves three leaves and is the one panel whose second empty layer is not its
read's presence flag: it keys on the row count instead. The rows and the flag are independent on the
server, which builds one row per chain axis whether or not the backing config resolves and reports
presence separately. So a runner with axes but no stroke-limit, rapid-feedrate or home config still
renders a full, editable table, and every commit fails with a toast naming the missing dependency.
What fills the cells differs by leaf: the stroke-limit and home values are nullable and come through
blank, while a rapid rate is a plain number the server defaults to 0, so that one leaf shows a
table of zeros that never signals the absence at all.
Controller Brand — staged, confirmed, and destructive by design
Controller Brand is one of two panels with no table read of its own, the only panel with no local error handler, and one of the two whose write replaces the whole runner — the branch root's Object-Management install is the other. The select stages a pending brand rather than applying it; Revert drops the staging, an orange banner spells out what the switch destroys, and Apply brand opens a confirmation dialog before the request. Its failures surface through the shared composable's global notification rather than the panel's own, so the console line names the shared state and not the panel.
Controller root — its own empty block, its own toast
The branch root panel does not use the shared empty state. It renders an hourglass over its own no-runner title and hint. Its error path also differs: it takes an already-composed string from the Object-Management button, omits the console line, and uses a longer toast than every other panel.
Indexing Position Tables — draft, validate, save whole
The one draft table. Its numeric cells bind straight into a local draft row with no handler, an
unsaved badge appears while the draft differs from what was saved, and nothing reaches the server
until the per-table Save Table button, which stays disabled while the draft fails validation. The
validation mirrors the endpoint's own rules — a per-table maximum, strictly ascending values, and a
0 ≤ position < 360 range when a modular rotary axis reads the table — and reports the first
violation under the table. A failed save deliberately does not roll the draft back: the draft
stays dirty so the edit is not lost, which is the opposite of every other panel.
Its axis-assignment table above the drafts is display-only. The assignment is not editable on this panel and the endpoint exposes no writer for it; the value is edited as a per-axis integer parameter in Parameters (Native), which is what the panel's own hint says.
Subprogram Folders — two fields, one write
The two folder inputs are plain text fields committed together on blur or Enter of either one, and the guard against a redundant write is a saved snapshot of both values rather than a per-field captured value. Editing one folder writes both.
M-Code Declarations — a third text-commit wiring, and an add that submits on Enter
The note cell is the only text cell in the branch that commits through its field's native change event rather than an explicit blur binding or the numeric widget. The timing matches; the wiring is a third variant. This is also the only add-row footer whose input submits on Enter.
R Parameters and Retained Common Variables — the two panels that commit a null
Every other numeric handler returns early on a null and treats a cleared cell as no edit. These two send the null, because an empty value is a real state in both tables — the vacant entry, which a program reading it reports as an error rather than silently taking as zero.
Tool Offsets — a read-only column that still writes, and a two-call rollback
With the tool-house dependence on, the two ideal columns are made read-only rather than hidden. A read-only field is still focusable and still takes keystrokes, and the widget's Enter binding is a plain listener on the underlying element rather than the component event blur travels on — so pressing Enter in one of those cells re-sends the whole row unchanged, while focusing it and leaving does not, because a non-editable field raises no blur. The tool number column, by contrast, is swapped for plain text and is genuinely inert. The tool-change position field takes a third route — it is disabled rather than read-only while its axis stays put, and a disabled field takes no keystrokes either.
Turning the dependence on also makes two server calls inside one try: the toggle write, then the refresh from the tool house. If the second fails after the first succeeded — the refresh answers unsuccessfully when the project carries no tool house — the catch reverts the local toggle while the server has already committed it.
Work Coordinates — the one panel that writes outside the branch
Clicking a row picks which coordinate the General Setup canvas marks, and that write goes to the equipment-display surface rather than to this branch's own. It is also the branch's one call through the plain-JSON helper instead of the envelope helper, so only a transport failure throws there. The row click is bound on the whole row with no click-stopping on the cells or the P0 and M0 buttons, so acting anywhere in a row marks it; a same-id short circuit is what keeps cell editing on the marked row from writing again on every pass through.
Two truth sources for “this is Siemens”
The tree relabels the generic Tool Offsets leaf from the snapshot's $TC_DP flag, while the tool
offsets panel shows its ISO-G43-H caveat, and the controller parameters panel picks its macro-guard
labels, from the brand string instead. The brand marker is a free-form string that can be edited
independently of the dependency list, so the label and the caveat can disagree.
The error handler does not agree with itself
Every panel that has a local handler composes the same toast through one signature carrying two incompatible conventions. Nine pass a localization key and translate inside the handler; eight pass an already-translated string. Both first parameters are typed as a plain string, so nothing separates the two conventions at compile time. Nothing user-visible changes; the console line does — the key-style panels log the raw key path, the string-style panels log the rendered sentence.
The add-row footers disagree on two details
Whether Enter submits: only M-Code Declarations binds it. Whether the fields clear after a successful add: M-Code Declarations clears its code, Tool Names clears both fields, and R Parameters, Retained Common Variables and Parameters (Native) leave what was typed in place.
A source comment the code contradicts
The shared composable's header names the Execution page as the owner of the Controller root. The root is built by the equipment-scoped host, which only the General Setup page creates, so the code is what ships. The shared empty state's comment reads like a second case and is not one: the number in it counts the byte-identical copies its extraction removed, not the call sites it has today, which are one more.
Layout
- General Setup Control Tree — the left dock of
/general-setup- Editor Row — the panel of whichever controller leaf is selected, one at a time
- No-Runner Body — "No NC runner — load a project first."; shown first, from the shared snapshot
- Absent-Table Body — one line naming the table this leaf edits, from the panel's own read
- Editor Body
- Description Caption — the leaf's own one-line explanation, above the table
- Toolbar — where the add button carries no fields: Add on Tool Offsets, Add Tool on
the Siemens
$TC_DPtable, beside that panel's own toggles - Table — dense, flat, bordered, no sort and no pagination
- Header Row — the leaf's own columns, over the shared labels Axis, Id, Value, Unit and Actions
- Data Row — a bold plain-text key cell, then one editable cell per column, then the row's action buttons
- Add Row Footer — the new row's fields, then Add / Set or Declare
- Show all Toggle — Work Coordinates and Frames only, revealing the all-zero extended tail
- Draft Bar — Add Position, Revert and Save Table with an unsaved badge on Indexing Position Tables; Revert with Apply brand on Controller Brand
- Toast — negative, three seconds, the panel's context followed by the server's own message
- Editor Row — the panel of whichever controller leaf is selected, one at a time
Source Code Path
See HiNC App Anatomy for git repository links.
Web Application
HiNC-2025-webservice (Quasar CLI SPA):
wwwroot-src/src/composables/useSoftNcRunner.ts— the shared snapshot every panel gates on: the module-scope installed flag, the project-presence watch, the three events that re-read it, the reload no panel calls, and the global notification the brand panel's failures take.wwwroot-src/src/api/softNcRunner.ts— the branch's typed REST wrappers: the snapshot parser, one reader and one setter family per table, and the whole-row payloads the per-cell handlers send.wwwroot-src/src/api/http.ts— the two failure shapes a panel cannot tell apart, and the coded error rendering that a message without a code falls through.wwwroot-src/src/components/widgets/NumericInput.vue— the numeric field: commit on blur or Enter, no per-keystroke emit, no equality guard, and the Enter listener that keeps firing where a read-only field's blur is suppressed.wwwroot-src/src/components/controlTree/SoftNcEmptyState.vue— the shared no-runner line, and the comment that counts the copies its extraction removed rather than its call sites.wwwroot-src/src/components/controlTree/softNcItemTypes.ts— the branch builder: the node ids the two multi-leaf panels discriminate on, and the empty node key that reduces the remount key to the id.wwwroot-src/src/components/controlTree/PrimarySlavePanel.vue— the editor row: the remount key, the one-panel-at-a-time mounting, and the events it wires to the host.wwwroot-src/src/components/controlTree/useControlTreeHost.ts— the before-switch gate no panel in this branch registers, and the rebuild that a reported structural change triggers.wwwroot-src/src/components/controlTree/SoftNcAxisTablePanel.vue— the three per-axis leaves, and the second empty layer keyed on the row count instead of the presence flag.wwwroot-src/src/components/controlTree/SoftNcBrandPanel.vue— the staged select, the revert, the confirmation and the structural change it reports.wwwroot-src/src/components/controlTree/SoftNcRunnerRootPanel.vue— the panel with its own empty block and its own toast shape.wwwroot-src/src/components/controlTree/SoftNcIndexingPositionsPanel.vue— the draft table: the unsaved badge, the client-side mirror of the endpoint's rules, the save that keeps a failed draft, and the display-only assignment column.wwwroot-src/src/components/controlTree/SoftNcSubprogramPanel.vue— the two text fields committed as a pair against a saved snapshot.wwwroot-src/src/components/controlTree/SoftNcMCodesPanel.vue— the change-event text cell, the whole-declaration write, and the only add input that submits on Enter.wwwroot-src/src/components/controlTree/SoftNcToolOffsetsPanel.vue— the read-only ideal columns that still commit, the renameable key with its duplicate guard, and the two-call dependence toggle.wwwroot-src/src/components/controlTree/SoftNcWorkCoordinatesPanel.vue— the row click that writes to the display surface, and the row-scoped triad write.wwwroot-src/src/components/controlTree/SoftNcRParametersPanel.vueandwwwroot-src/src/components/controlTree/SoftNcRetainedVariablesPanel.vue— the two panels that commit a null to vacate an entry.wwwroot-src/src/components/controlTree/SoftNcSiemensToolOffsetsPanel.vueandwwwroot-src/src/components/controlTree/SoftNcNativeParamsPanel.vue— the other two confirmed deletions, and the two add flows that part company: one appends the minted row, the other re-reads the whole table.wwwroot-src/src/components/controlTree/SoftNcToolNamesPanel.vue,wwwroot-src/src/components/controlTree/SoftNcToolChangePanel.vue,wwwroot-src/src/components/controlTree/SoftNcDatumTablePanel.vueandwwwroot-src/src/components/controlTree/SoftNcControllerParamsPanel.vue— the conforming shape: one read on mount, per-control commit, capture-assign-await-restore, one toast. The tool-change Stays put box is the one handler among them that captures and restores a pair of fields.wwwroot-src/src/api/equipmentSetup.ts— the canvas marker id the work-coordinate row click writes, outside this branch's own surface and through the plain-JSON helper.wwwroot-src/src/stores/project.ts— the has-a-project flag the shared snapshot watches, and the direct path assignment that keeps it true across a project-to-project load.wwwroot-src/src/layouts/MainLayout.vue— the project epoch that rebuilds the page without re-running the idempotent installer.wwwroot-src/src/i18n/en/softNc.ts— every empty line, column header, hint and error context this branch renders, and the convention that an error key stores the bare context.wwwroot-src/src/i18n/en/common.ts— the shared column and action labels the tables reuse.Mech/SoftNcRunnerController.cs— the REST surface: the dependency lookup that answers a missing table inside a success envelope, the reads that report presence independently of their rows, the rapid rate defaulted to zero where its config is absent, the two runner-replacing writes that answer real status codes instead, and the datum routes' defensive rejection of an unknown table segment.Common/ApiError.cs— the coded payloads, and why only the no-project answer can be re-rendered in the app locale.
See Also
- Controller Branch — the branch these rules hold across, its two planes and the runner behind them
- Machine and Controller Plane — the plane whose seven leaf pages this contract lets stay short
- Program Data Plane — the other plane these rules hold across, whose six leaf pages inherit them the same way
- Brand Matrix — which leaf exists on which brand, and the snapshot flags the first empty layer reads
- Numeric Input — the numeric field's own contract: what parses, what the bounds do, and why Enter commits twice
- Controller Brand — the staged, confirmed, whole-runner write that departs furthest from these rules
- Per-Axis Tables — the three leaves whose empty layer keys on the row count, and the tool-change position beside them
- Interface Parameters — the interface parameter form and the canned-cycle field, both plain per-control commits
- M-Code Declarations — the whole-declaration write and the change-event text cell
- Program Reading — the block-skip boxes and the paired folder inputs
- Indexing Position Tables — the draft-then-save table, the one panel that keeps a failed edit
- Native Parameters — the confirmed deletions and the add-or-set footer that keeps what was typed