feat(presentation): view models for the server tree, the shell, the footer, the tray and page messages #58

Merged
james.maes merged 25 commits from feat/2.0-shell-tree-models-20261111 into release/2.0 2026-11-11 12:12:41 +00:00
Owner

Step 7 of the connection wizard build (ADR-0005): the view models of the server tree, the shell that polls the service, the footer, the tray text, busy state and page messages. View models only; the window does not use them yet, so nothing an operator sees changes.

What it adds (presentation assembly, Shell/)

  • ServerTree and its nodes: one node per server in the snapshot's order, with Printers, Jobs and Connection beneath; selection and expansion survive a refresh; identifiers Server.<n>, AddServer, NavigateComputer and the existing navigation identifiers.
  • ShellViewModel: two reads on the window's timers (snapshot, transport) that never overlap themselves and are never gated; what a snapshot says about a server is shown only when the snapshot was sent after that server's last local change.
  • FooterStatus and TrayStatus: worst health first, counts per state, the first server needing attention; with one server the footer reads as 1.x does on the same inputs. The tray tooltip stays within 127 characters.
  • ServerBusy: one command at a time per server and one for the computer; a second is refused out loud.
  • PageMessages: a message belongs to the page that caused it. A message that has not been on screen is never replaced; an error on screen is kept when the shell itself moves the operator away.
  • A small addition to the management client: a snapshot result says which flight made it, and a change's result says how many snapshots had been sent when its reply was read. No operation is sent, shared, skipped or answered differently.

Verification

  • Head 2523bd1 on release/2.0 at 17698dd.
  • build/Verify.ps1 passed on that head: 1,174 Core, 2,337 Windows runtime and command line, 1,273 presentation tests, none failed; presentation 100% of 3,071 lines, Core and command line 100%, Windows 96.1% (minimum 95).

Review

  • First review, of the step as first built (92f4d2d): 2 blockers, 6 should-fix, 14 nits. The blockers: an unseen failure message could be replaced and lost; "Server removed" was said when the stored data merely became not ready. Neither could reach an operator, because no page posts messages and the window does not use these models.
  • The fix round (two phases, 20 commits) answered both blockers, every should-fix and every nit; its authors' 128 single changes and the first reviewer's 92 each fail a test on the result.
  • Merged on the owner's instruction of 2026-11-11 to merge the verified branches, before the confirmation review of the fix round had reported. That review is running on this exact head; anything it finds that is a product defect is fixed in a follow-up change, and the rest is recorded in the audit.

Recorded for later steps

  • The rule for "may a wizard start" exists in the shell and in the wizard; the step that joins the two keeps the shell's.
  • Between a command's answer and the release of its hold, a snapshot sent while the command ran can be shown; a page whose command waits before releasing must note the change with its result (the pages step).
  • A gone server's unseen failures can appear on the wizard's page when the last server goes.
  • The tray needs a fourth icon for unknown health when the views are built.

Not proven here

No view exists yet: a real tree control, a screen reader and the tray are unshown. Nothing ran against a service that speaks version 2.

🤖 Generated with Claude Code

Step 7 of the connection wizard build ([ADR-0005](docs/adr/0005-connection-wizard-and-server-tree.md)): the view models of the server tree, the shell that polls the service, the footer, the tray text, busy state and page messages. View models only; the window does not use them yet, so nothing an operator sees changes. **What it adds (presentation assembly, `Shell/`)** - `ServerTree` and its nodes: one node per server in the snapshot's order, with Printers, Jobs and Connection beneath; selection and expansion survive a refresh; identifiers `Server.<n>`, `AddServer`, `NavigateComputer` and the existing navigation identifiers. - `ShellViewModel`: two reads on the window's timers (snapshot, transport) that never overlap themselves and are never gated; what a snapshot says about a server is shown only when the snapshot was sent after that server's last local change. - `FooterStatus` and `TrayStatus`: worst health first, counts per state, the first server needing attention; with one server the footer reads as 1.x does on the same inputs. The tray tooltip stays within 127 characters. - `ServerBusy`: one command at a time per server and one for the computer; a second is refused out loud. - `PageMessages`: a message belongs to the page that caused it. A message that has not been on screen is never replaced; an error on screen is kept when the shell itself moves the operator away. - A small addition to the management client: a snapshot result says which flight made it, and a change's result says how many snapshots had been sent when its reply was read. No operation is sent, shared, skipped or answered differently. **Verification** - Head `2523bd1` on `release/2.0` at `17698dd`. - `build/Verify.ps1` passed on that head: 1,174 Core, 2,337 Windows runtime and command line, 1,273 presentation tests, none failed; presentation 100% of 3,071 lines, Core and command line 100%, Windows 96.1% (minimum 95). **Review** - First review, of the step as first built (`92f4d2d`): `2 blockers, 6 should-fix, 14 nits`. The blockers: an unseen failure message could be replaced and lost; "Server removed" was said when the stored data merely became not ready. Neither could reach an operator, because no page posts messages and the window does not use these models. - The fix round (two phases, 20 commits) answered both blockers, every should-fix and every nit; its authors' 128 single changes and the first reviewer's 92 each fail a test on the result. - **Merged on the owner's instruction of 2026-11-11 to merge the verified branches, before the confirmation review of the fix round had reported.** That review is running on this exact head; anything it finds that is a product defect is fixed in a follow-up change, and the rest is recorded in the audit. **Recorded for later steps** - The rule for "may a wizard start" exists in the shell and in the wizard; the step that joins the two keeps the shell's. - Between a command's answer and the release of its hold, a snapshot sent while the command ran can be shown; a page whose command waits before releasing must note the change with its result (the pages step). - A gone server's unseen failures can appear on the wizard's page when the last server goes. - The tray needs a fourth icon for unknown health when the views are built. **Not proven here** No view exists yet: a real tree control, a screen reader and the tray are unshown. Nothing ran against a service that speaks version 2. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 25 commits 2026-11-11 12:12:27 +00:00
The first part of the shell and the tree for several servers (ADR-0005; plan
step 7). Nothing uses it yet, so the window is unchanged.

- PageKey names a page by its kind and, for a server's page, the server's
  identifier. It is added beside ShellPages, which the window still uses.
- HealthLevel gains Unknown. HealthStatus.From keeps the 1.x rule (any other
  word is red); LevelOf reads the contract's word, where a word this build
  does not know is unknown, and Worst ranks red, unknown, orange, green.
- HealthBadge gives every level its own glyph and words, so a state is never
  told by colour alone.
- ServerStatus words one server from its snapshot: the footer rule of 1.x
  for a paired server, after "being removed", "stored data cannot be read"
  and "not paired". An orange server is said to be connected with printers
  needing attention only when the snapshot shows both.
- ServerCommands describes the menu: reconnect, sign out, rename, remove,
  move up, move down, each with its access key, its enabled rule and the
  reason when it is disabled. A server that is being removed offers none.
- ShellMessages holds every sentence of the shell in one place. A server's
  name is given the contract's printable form there.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The models the window will be rebuilt on (ADR-0005; plan step 7). The window
does not use them yet, so nothing an operator sees changes.

- ServerTree reconciles each snapshot by server identifier: a server keeps
  its node, so selection and expansion survive; the order is the snapshot's;
  nodes are moved, never reset; identifiers are Server.<n> from one and are
  renumbered; when the selected server goes its neighbour is selected. Every
  node has a name, a glyph, a text, its level, position and set size.
- FooterStatus: worst health, the count per state, the first server that
  needs attention; with one server that server's sentence, which is what the
  1.x footer said (a test gives both the same inputs); red with no server.
- TrayStatus: a tooltip within Windows' 127 UTF-16 units that names as many
  servers as fit whole and says how many were left out; the menu lists all.
- ServiceAvailability: reached, not reached, older service, newer service,
  upgrade incomplete (with the file), newer build (with both versions),
  update paused. The wizard only when the snapshot answered and the stored
  data is ready; otherwise the Computer page says why.
- ServerBusy stands on the one OperationGate: one key per server and one for
  the computer. A second command on a busy key is refused out loud with the
  holder's sentence. It also marks each key's last local change.
- PageMessages keeps a message under the page that caused it and drops it
  when that page is left.
- ShellViewModel reads the snapshot and the transport meter when the
  window's timers tick. Neither read overlaps itself or enters the gate, and
  a skipped read changes nothing. A snapshot's entry for a server that was
  changed from the window after the snapshot was asked for is not shown; the
  other servers' entries are. The footer is announced once, after it is final.
- The shell asks for the connection wizard through IWizardLauncher and is
  told when it ended; it does not know the wizard's type.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
ADR-0005 gains "Resolved while building the shell and tree, 2026-11-11":
the order of health levels, what a server says and why the 1.x sentence for
orange is used only when the snapshot shows it, what the tree does when the
selected server goes, where the window lands, how a snapshot that is older
than a local change is handled, where a late message goes, and how the
tray's tooltip is put together.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
An entry under Unreleased for the shell models. It says that the window does
not use them yet, so nothing an operator sees changes.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A read that is in flight when a command ends was sent before the change, so
what it says about the changed server is not shown, and until now the change
appeared with the next tick. RefreshAfterChangeAsync waits for the shell's
read in flight and then reads again, so a page shows the result of its own
command without waiting for the timer.

The client's review round adds SnapshotAsync(sentAfterTheCall: true), which
is sent after the call and is never skipped. This branch stands on the client
before that round, so the second read is the timer's read for now; the one
private method that will ask for the new kind, SnapshotSentAfterTheCallAsync,
says so, and becomes one line at the rebase. The test that passes a token to
SnapshotAsync names the argument, which compiles against both clients.

ADR-0005's note on local changes records it.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Answers the review of the shell and tree models (S4, S6 and nits 1, 7, 11
and 12).

A paired server nobody is signed in to says "Sign in required" beside its
name: the snapshot carries signedIn and authorized separately, and a sign-in
is exactly what is missing. A server somebody is signed in to whose session
is not authorized still says "Authorization required". Alone in the footer
both keep the 1.x words.

A server that is neither paired nor signed in is told to sign in when it is
asked to reconnect, because pairing needs a session first. A test pins which
reason is given.

ServerNode.Name raises a change when the server is renamed, so a view bound
to it follows.

While the Add server node is disabled, the name a screen reader reads says
that it is unavailable and why, from before the first answer on.

The summary of HealthStatus no longer says that any unknown word is red:
that is true of From, the 1.x rule, and not of LevelOf.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Answers the review of the shell and tree models (nits 2, 3, 4, 6 and 8).

The tooltip names the servers worst first, so the ones that need the
operator are the ones named and not the ones left out. When not even one
server fits after the counts it is the counts alone, which say how many
servers there are: "+8 more" after a count of eight read as eight further
servers. The product name ends the tooltip when there is room for it.

Two of the tray texts are equal when they say the same, so the shell tells
a view about the tray only when something it shows changed, not on every
tick.

While printing is paused for an update, a computer that has servers says so
in the footer and the tray, is never green, and points at the Computer page,
which says until when.

The comments no longer call "Checking local service...", "Local print
service unavailable" and "Local service needs attention" sentences of the
1.x footer. The 1.x footer showed the red sentence in those states; the
words were its initial text, replaced before the window was shown, and the
status line of the Settings page.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Answers the two blockers of the review of the shell and tree models, and
nit 10.

B1. A failure the operator had not seen could be replaced and lost: by a
newer result for the same page while the operator was elsewhere, and by the
notice that a server went, shown over a failure that had just come on
screen. A page now keeps its messages in a short list, in the order they
arrived. The bar shows the oldest and says how many more wait; dismissing
shows the next. Leaving a page drops the message that was on screen and
keeps the rest, whoever made the move, so a move the shell makes loses
nothing that was not seen. What arrives for the page on screen takes the
place of the message the bar shows, except a failure that had been waiting,
which stays until it is dismissed.

A page keeps eight messages. At the limit a failure takes the place of the
oldest waiting message that is not a failure, and whatever is not kept is
counted in a note the page itself carries, failures separately, so no
failure goes without a word.

A failure kept under a page of a server that has gone could never be seen.
It is now shown on the page the shell shows instead, saying which server it
was about; one reported later is shown on the page on screen. What was not
a failure is dropped.

B2. "Server removed: X is no longer on this computer" was shown, and
logged, when the stored data of the service was not ready. The service
lists no server in that state and has removed none. The shell now says that
the server is not listed and why, on the Computer page and in the log, and
nothing about a removal; messages that wait under the pages of such a
server stay where they are. The refusal of a wizard for a server the tree
does not have no longer says that the server went, which is not known
there.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Answers the review of the shell and tree models (S3, S6 and nit 13).

Choosing the Add server node while a wizard is open only shows the page of
the wizard. Until now it sent AddServer() to the launcher again, so a
launcher written to the words of the interface would have restarted a
wizard in progress. StartAddServer() stays the explicit request for a new
one. IWizardLauncher says when each member is called.

A wizard that ended with nothing to show on a computer with no server was
asked for again from inside WizardEnded, that is, inside the call the
launcher itself was making. The page of the wizard now stays, with no
wizard open, and the next snapshot asks (or the operator, by choosing Add
server). A wizard is recorded as open before its page is selected, so a
view that passes every selection on cannot cause a second request.

Tests pin two decisions that had none: a repair and a pairing start at
eight servers, because the limit is on adding one; and when the last server
goes while a wizard is open, no second wizard is asked for.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Answers the review of the shell and tree models (S5).

The gate raises its change on whatever thread releases a hold, and the
shell set the busy state of the node there, on a tree the thread of the
window may be reconciling. The shell now remembers the thread it was built
on. A change of the gate that arrives on another thread does not touch the
tree; the log is told once, with the key and without the sentence of the
holder. Every reconcile reads busy from the gate for every server and for
the Add server and Computer nodes, so the next read shows it. Nothing is
marshalled: the shell has no dispatcher, and a posted change would lag
behind the gate.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Answers the review of the shell and tree models (nits 5 and 14).

Several callers of RefreshAfterChangeAsync behind one read caused one
further read each. They now share the one read that follows, which is sent
after each of their calls; a caller that asks once that read has been sent
gets a later one. The comment that said nothing is started while a snapshot
is being shown is corrected: a real read is waited for and followed by a
new one.

SnapshotApplied now carries which entries a snapshot did not bring, because
the server was changed from this window after the snapshot was asked for,
and whether the part of the computer and the order were held back. For
those servers Snapshot still holds the entry from before the change, so a
page does not put it over what its own command made true.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The section of ADR-0005 on the shell and tree is brought in line with the
fix round: when a server is said to have gone; how messages wait, what the
limit of a page is and where the failures of a gone server go; when the
launcher is asked; the order of the tooltip and what it says when no server
fits; an update pause; a hold released on another thread; and what a page
is told with each snapshot.

It lists what an operator who upgrades from 1.x reads differently in the
footer and the tray, checked against the 1.x window, in place of the claim
that there is one difference.

It records, as binding for the step that joins the shell and the wizard,
that the rule of the shell for when a wizard may start is the one, and how
the three endings of the wizard map onto WizardEnded. It records that the
commands of a server stay enabled while the service is not answering, and
that the mark of when a snapshot was asked for is still exact only while
the shell is the one caller, which is closed on the fixed client.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The entry for the shell models no longer says that the footer of one server
is the sentence the 1.x footer shows without saying where it differs. It
says when a server is reported as removed, which servers the tooltip names
first, and how messages wait.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The review found one single change of its own that no test caught and called
it equivalent: WizardEnded no longer recording the page it opened as the
choice of the operator. It is equivalent only as long as a wizard can end
only after the window has landed or the operator has chosen. A test now pins
the rule itself: a page the owner of the wizard had the shell open before
the first answer stays when the first answer arrives.

The helper that reads a page's messages to the end in the message tests
fails the test when dismissing never comes to the end, where it used to
loop.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Two rows of the list of what an upgraded operator reads differently said
more than the 1.x code I read shows. The 1.x snapshot did not report an
unreadable stored state of one server, so the row no longer names a
sentence for it. The first line of the 1.x tray menu was the health
sentence without the product name, and what a paused computer said there
depended on its health, so the example is the green one.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Rerunning the single changes of the first round against the fixed code
showed that one rule had lost its test when RefreshAfterChangeAsync was
reshaped: a caller that waits behind the read in flight when the shell is
disposed must not read afterwards. The test of a disposed shell now has such
a waiter and counts the requests.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The announcement's event now carries its sentence, and a listener reads
it from the event. The shell also stopped deciding whether the footer's
sentence changed by reading the announcement's text: a view can clear
that text and anyone who holds the announcement can use it, after which
the shell said the same sentence again on the next tick, or swallowed
its own. It now keeps the sentence it announced last itself.

The shell's fixture reads the sentence from the event.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The branch now stands on the client whose snapshot can be asked for as
"sent after the call". The shell's read after a change asks for exactly
that, through the one private method that names it. Such a read is
never handed a snapshot that was already in flight, whoever sent that
one (the wizard will have its own in the air), and it is never skipped:
at the client's ceiling it waits its turn.

Two tests hold both: another caller's snapshot in flight, and twelve
requests in flight.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A time limit reported as a TimeoutException and a pipe closed under the
call are results of the client now, and the shell reads both as a
service that is not reached. A reply in JSON that states no version is
an answer that cannot be read, not an older service, and the footer
says so. Nothing in the shell changed for either.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A caller that keeps what it shows of a server it has just changed has
to know whether a snapshot was sent before or after the change was
answered. The client shares one snapshot among everyone who asks while
it is in flight, so the moment a caller asked says nothing about when
the request it was handed had been sent.

The result of a snapshot (and of a transport read) now carries the
number its flight was given when it was handed to the pipe. The flight
puts it on the result it makes, so every caller that shares the flight
reads the same number; a result no flight made (skipped, nothing sent,
a wait that ended) names none. The result of every other request that
was handed to the pipe carries how many snapshots had been handed to
the pipe when it was made: read after the reply arrived, or after a
failure or the end of the wait was known, never before. A snapshot
whose number is higher was sent after that.

The count is one client's and starts again with the companion; the
transport reads are numbered apart. No operation is sent, shared,
skipped or answered differently. The client's own count is internal,
so a caller outside the assembly is given a count only on a result.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The shell took its mark of "when this snapshot was asked for" just
before it called the client. That is exact only while the shell is the
only caller with a snapshot in flight. With another caller's snapshot
in the air, as the wizard will have, a tick is handed that one, which
was sent before a change that has since ended, and the changed server's
old entry was shown for one more tick.

The rule is now keyed to when the snapshot's request was sent. A key's
stamp is the client's count of the snapshots it had handed to the pipe,
and what a snapshot says about the key is shown only when the number of
the flight that made it is strictly greater. The stamp is always taken
after the change was answered: when a command gives its hold back, and,
for a change made without the gate, from the count the change's own
result carries. A command also stamps its key when it starts, which
holds back what was sent before it began and nothing sent while it
runs. A change whose outcome is not known is stamped the same way, with
the count read when its outcome stopped being known. The shell takes no
mark of its own any more.

NoteChange takes the change's own result, so a caller cannot pass a
moment from before its change was answered. A server the window does
not show yet is left out of a snapshot that is older than its change,
and a snapshot that is not newer than a server's change can never make
that server go, so "Server removed" is not said by one.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Leaving a page dropped the message that was on screen, whoever made the
move. When the shell makes the move (it lands, a wizard ends, the
server the wizard named is listed, the server on screen is not listed
or has gone), the operator did not choose to leave and may never have
read the bar.

An error that was on screen now stays under its page when the shell
makes the move, as a failure that has waited: it is shown when the
operator next opens the page and is not covered by what arrives there.
When the page was a gone server's, which cannot be opened again, the
error is shown on the page the shell shows instead, with the server's
name, as the failures that waited already were.

A warning or less is still dropped by the same move, because a refusal
such as "Cannot start now" would be shown again when it is no longer
true. A move the operator makes drops the message that was on screen,
an error too, as before.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
ADR-0005, the section of the shell and tree:

- "Polling and local changes" states the rule as built: when a snapshot
  was sent is the client's to say; a stamp is taken after the change was
  answered and never before it was sent; what holds for a change whose
  outcome is not known; that a snapshot which is not newer than a
  server's change neither makes it go nor shows it for the first time;
  the one limit left for the pages; that the numbers are one client's
  and the transport reads are numbered apart. The sentences about the
  interim before the rebase are gone: a read after a change is the
  client's snapshot sent after the call, and it is never skipped.
- "Messages" records the coordinator's ruling: an error on screen is
  kept when the shell itself moves the operator away, and anything less
  is dropped.
- The binding note says what the wizard passes for every change it
  makes, and the entry "Still open in this section" is closed.

The client's section gains what a result says about when it was sent.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The entry of the shell and tree says that whether a snapshot is older
than a change is decided by when its request was sent, and that an
error on screen is kept when the window itself changes the page. The
client's entry says what a result now carries.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The summary of ServerBusy said that a stamp is always taken after the
change was answered and, two sentences on, that a command also stamps
its key when it starts. The stamp of the start marks no change: it
holds back what was sent before the command began, and the stamp of
the command's end always follows it. Comment only.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
james.maes merged commit 6f2c17b9a6 into release/2.0 2026-11-11 12:12:41 +00:00
Sign in to join this conversation.