Ux/review fixes#35
Merged
Merged
Conversation
…eutrality, canvas legibility, log recovery) Resolves a large batch of UI/UX weak spots from the canvas / deploy / observability review (see inprogress/ux-review-findings.md, not committed). Phase 0 (dead wiring): mount the empty-canvas onboarding overlay; surface per-resource deploy remediation guidance (GCP-gated); correct the misleading "handled by keyboard shortcuts" menu comments. Phase 1 (honesty): drift-reconcile the hardcoded green "Live"; plumb drift authority (unsupported) + staleness via driftMeta; render unknown/extra drift states; label estimated cost "Est.". Phase 2 (deploy safety): sync + display the deploy target environment; echo the plan destination; invalidate stale plans on config change; show remediation / AI-diagnose on partial async failures; safer destroy default; persist cloud connection status; surface promotion + env-delete failures + env-bar resilience; estimated cost at the commit moment. Phase 3 (provider neutrality): honest non-GCP logs (new provider-unsupported resolution state); provider-aware drift (only query GCP when GCP resources are deployed); GCP-gated deploy error banners. Phase 4 (canvas legibility): deploy progress/error overlay + validation badge + in-flight pulse on CardShell nodes; log-scroll no longer zooms the canvas; Redis accent de-collided from the system error red. Phase 5 (observability): in-product log Retry after fixing an error / permission cause, no reload. Verified: @ice/ui (~10k) + deploy-service (~1.1k) unit suites green; both packages typecheck clean. Not yet verified in the headed app.
…ing.Log section (OL3) The clearEntries reducer existed but was referenced only by the slice + its tests — never by any UI. Surface a Clear button (shown when the buffer has entries) so users can reset the log view. First slice of OL3 (search/filter/ download still to come).
…ing (OL4) The 200-line log buffer splices off the oldest entries with no signal. Track a droppedCount in the logs slice (reset by clearEntries) and show "N older lines dropped — buffer is capped at 200" in the Monitoring.Log section, so the cap is honest. (Backward "load earlier" paging still needs a server-side query.)
… (AX2) The deploy lifecycle was silent to assistive tech. Mount one visually-hidden aria-live="polite" region (LiveAnnouncer) in the AppBar that mirrors the deploy status — connecting / planning / deploying N resources / succeeded / failed — so screen-reader users hear transitions. Coarse by design (status transitions only) to avoid chatter. Pure status→text mapping is unit-tested.
…eys, dialog semantics (AX6/CD2/AX3/IA3) - CD2: document the headline "Shift + A" add affordance (it was undocumented). - IA3: stop advertising the dead "1"/"2" view-level keys (the view-level toggle was removed — altitude is zoom-driven now). - AX3: give the panel role="dialog" + aria-modal + aria-label, and a REAL Escape-key handler (the header "ESC" affordance previously only clicked).
…Environments page (IA4)
Deploy status was rendered with three divergent switches (e.g. the status bar
said "Deploy failed" off `error` while the env page / deployments list said
"Failed" off `failed`). Introduce one canonical `deployStatusMeta(raw)` →
{ tone, labelKey, dotClass } in shared/utils that normalises every raw status
(error≡failed, success→"Deployed", etc.). The env-dot colour util now delegates
to it (outputs unchanged) and the Environments page uses it for labels + dots.
Status-bar + Deployments-list adoption to follow.
…r + Deployments list (IA4) Finishes IA4 so all three deploy-status surfaces share one vocabulary: - status-bar DeployStatusIndicator: label now comes from deployStatusMeta (e.g. 'error' reads "Failed", matching the env + deployments surfaces); icon/colour kept as a small data-driven per-status map. - Deployments list: statusIcon is keyed off the canonical tone, so an 'error' status no longer slips through to the neutral default icon, and the row label uses the shared labels (falling back to the raw value for unknown backend statuses rather than mislabelling history as "Not deployed").
…l (CNV7, AX5) In the poster view (CardShell's lod<3 branch) a node's deploy/runtime state was a bare coloured dot whose only label was a mouse `title` — the dominant render across the whole zoom-out band. That is hue-only and mouse-only: a colour-blind or keyboard/AT user can't tell deploying from done from failed, and nobody can without hovering each card. - New pure helper `poster-status.ts` buckets the broad node-status vocabulary (the STATUS_COLORS keys: active/running/deploying/drifted/error/…) into five tones and pairs each with a NON-COLOUR glyph (✓ / … / ! / ✕ / ·); in-flight tones pulse (motion-safe), consistent with the LOD-3 footer dot (CNV3). - The poster dot now renders that glyph with `role=img` + `aria-label` (the status word) instead of a mouse-only `title`, so the state is reachable by AT and distinguishable by shape. Colour is kept as a redundant cue. Unit-tested the helper (every STATUS_COLORS key is toned; glyphs distinct; pulse only in-flight) and the poster branch (labelled glyph, distinct in-flight vs failed, pulse gating, absent when no status).
Several persistent animations ignored the OS reduced-motion setting because
they were applied via inline style / SMIL rather than threading the existing
useReducedMotion hook, and globals.css had no media-query safety net.
- globals.css: add the standard `@media (prefers-reduced-motion: reduce)`
net (unlayered + !important so it beats inline `style={{ animation }}`),
neutralising the deploy-banner `iceDeployPulse` pulse and the
`ice-node-entrance` / `ice-edge-entrance` keyframes in one place.
- SVG SMIL is not covered by that media query, so the socket-dot pulsing
halo is gated in JS via a new non-hook `prefersReducedMotion()` read
(keeps the render-pure SocketDot hook-free).
Tested: prefersReducedMotion (true/false/missing-matchMedia/SSR) and the
socket-dot halo gate (animate present when allowed, absent when reduced).
…X8, AX7) The empty-canvas quick-start dismiss (<X/>) and the controls-help "?" trigger are icon/glyph-only, so screen readers announced just "button" / "question mark". - empty-canvas dismiss: aria-label (new `canvas.emptyState.dismiss` key, en+zh). - controls-help "?" trigger: aria-label + aria-expanded reflecting the popover state (AX6 follow-up). Note on AX7: globals.css already ships a global `:focus-visible` ring, and these native <button>s inherit it (they don't suppress the outline), so the focus-indicator gap is already covered for them; the only residual is the Spotlight input's deliberate borderless launcher styling. Tested: both triggers expose the expected aria-label; aria-expanded flips with open state.
Every property carries `required` in its schema (87 props are required), but no field component rendered it — a user only discovered a field was mandatory when the deploy failed on a MISSING_REQUIRED validation error. - PropertyLabel gains an optional `required` prop and draws a red asterisk with an AT-reachable name (new `canvas.properties.fields.required`, en+zh). - render-property-field threads `prop.required` into PropertyLabel across all branches (text/number/boolean/select/IceSelect), so the schema-driven Config fields show the marker. Stays schema-driven — no per-prop hardcoding. Tested: PropertyLabel renders the asterisk + aria-label only when required; each render branch threads prop.required through.
The concept "i" explainer (Overview / Compiles-To / code / docs) was mounted on the canvas node but nowhere in the properties panel — yet the panel is exactly where a user pauses to understand a block. Mount ConceptInfoTrigger in NodeIdentityCard next to the type/provider chips. It self-gates via hasConceptInfo(iceType), so it no-ops for blocks without registered content. Tested: the trigger is mounted with the right iceType/displayName/provider, and displayName falls back label→iceType while an empty provider passes undefined.
Advanced-tier props were filtered out of the panel with no escape hatch, so a user wanting to pin a subnet/VPC CIDR or attach a specific TLS cert had no UI path and had to accept the deployer's inference. PropertyFields now splits advanced-tier props into a collapsed "Advanced" disclosure (new `canvas.properties.fields.advanced`, en+zh) that reveals them on demand, keeping the default view clean. State lives in a small child `AdvancedDisclosure` (so the orchestrator stays hook-free) and rows render lazily only when expanded. Tested: PropertyFields routes advanced props into the disclosure (and none when there are none); AdvancedDisclosure shows a count + toggle, hides rows when collapsed, reveals them when expanded, and flips state on click.
Per-field config validation rendered only inside the Config tab, so a user on Scaling/Domain/Source/Connections got no in-panel signal that the node was invalid (the global validation panel is off by default). PropertiesTabBar now shows an error/warning count pill on a tab (errors take precedence; one pill, AT-labelled via the existing requirements.* strings). node-properties-section derives the Config tab's counts from propertyIssuesMap (which IS the per-field config validation), so the badge is visible from any tab. No new i18n. Tested: TabIssueBadge renders the red error pill / amber warning pill / nothing across counts; PropertiesTabBar threads the config counts to the right tab.
…(CD3)
The block palette only substring-matched a block's localized name +
description, so goal terms a newcomer types ('api', 'cron', 'cdn', 'pubsub',
'cache', 'auth') dead-ended — even though the adjacent template search matches
tags + category. Same word, two different behaviours.
- ComponentDef gains an optional `keywords` field; a centralized
`GOAL_KEYWORDS` map (keyed by iceType, looked up in `def()` so the 25 call
sites are untouched) seeds synonyms for the common goal terms.
- New pure `componentMatchesQuery` ORs name/description/tooltip + the localized
category label + keywords, mirroring `searchTemplates`. The palette filter
delegates to it (now also reactive to locale via categoryMap).
Tested: keyword wiring (every GOAL_KEYWORDS key is a real iceType; def attaches/
omits correctly) and the matcher (name/desc/tooltip/category/keyword hits,
empty-query, no-match; 'cron' finds CronJob not Postgres).
… CD6) The Shift+A spotlight diverged from the drag-drop add path in three ways: - CD1 — it always took providers[0]; now it prefers the active deploy provider when the block supports it (so it spawns the same blueprint drag would). - CD5 — its missing-blueprint fallback never set providerUnsupported (so an unsupported block looked valid until deploy rejected it) and hardcoded the deploy provider; now it carries providerUnsupported + the resolved effectiveProvider, matching use-canvas-drop. - CD6 — a locked canvas disables the right-click "Add Block" path but Shift+A ignored the lock; SpotlightMount now passes enabled: !canvasLocked to the hook (which already supported it). CD1/CD5 logic extracted to a pure spotlight-spawn helper and unit-tested (provider preference, gate-block, fallback providerUnsupported flag).
…7, PE9) Two properties-panel long-tail lows: - PE7 — the schema `name` prop is tier:'essential' so it rendered in the Config tab alongside the identity card's name input — Name was editable twice in one panel. Drop the duplicate schema `name` field; the identity card owns it. - PE9 — inline per-field error messages only rendered for the `visible` list, so an issue on an advanced-tier or visibleWhen-hidden prop had no field to point at (banner-only). Now any prop with a live validation issue is always shown in the main Section, so every inline message has an anchor. Tested: name field dropped; an advanced prop with an issue is promoted out of the disclosure into the Section; a gated-hidden prop with an issue is shown.
The blueprint add-path seeds data.name from the block's display name, but the no-blueprint fallback (drag drop + Shift+A spotlight) set only data.label — so those nodes were born with a hidden "Name is required" validation error that the identity card masks (it shows data.name || data.label). Seed data.name from the label on both fallback paths, matching the blueprint path. Tested on both (drag fallback + buildSpotlightFallbackData).
The async `complete` wire event carries no duration, but the reducer logged "Deploy completed in 0.0s" — a fake time that contradicted the real-duration line the hydrate reducer pushes moments later from the DB record. The status flips instantly from the event regardless, so drop the fabricated line and let hydrate own the authoritative completion log line. Tested: applyDeployCompleteEvent no longer appends any '0.0s' line.
Two log-node copy issues: - OL7 — copied lines used the truncated HH:MM:SS display timestamp, dropping the date so a pasted line couldn't be correlated to the cloud console. Copy now carries the full date+time (new pure formatCopyTimestamp/formatLogCopyLine). Copy-all also gained a transient 'COPIED' confirmation (the CopyButton stays briefly visible after a copy). - OL8 — clipboard writes set the 'copied' flash unconditionally, so a rejected or unavailable-clipboard write showed a false-positive confirmation. The flash is now gated on the write actually succeeding (writeClipboard → Promise<bool>; handles a missing API and rejection). Tested: full-ts copy lines; copied flash fires only on success, not on reject / missing clipboard; CopyButton COPIED state; LogHeader copiedAll passthrough. (Deferred, noted in plan: resource-label inclusion in copy + a distinct 'notice' log colour — narrower OL7 sub-points.)
…(OL6) The canvas log node's LiveIndicator collapsed five non-streaming states (pre-deploy / no-source / ambiguous / unsupported / provider-unsupported) into a single grey 'IDLE' dot — so surveying the board you couldn't tell 'waiting on a deploy' from 'no source connected' from 'pick a source' without opening the properties panel, which already differentiates them via pillFor. specFor now returns a distinct short label per state (WAITING / NO SOURCE / CHOOSE SRC / UNSUPPORTED / UNAVAILABLE), keeping the grey tone so the canvas dot and the panel pill still agree. New canvas.status.* keys (en+zh). Tested: each state shows its own label, all grey, all mutually distinct.
The promote modal read "N changes will be applied to production" — which sounds
like a live deployment, but promoteEnvironment only copies the design (cards/
nodes) into the target env; the user still has to deploy that env to change
running infrastructure.
Reword the count line ("...will update the design of <env>") and add an amber
clarification note: promotion updates the design only — deploy the target to
apply it to live infrastructure. New `designOnlyNote` key (en+zh).
Tested: the note shows in the diff case and is omitted when there are no changes.
…ed (EI9) The env-tab deploy-status fetch used Promise.allSettled but only recorded fulfilled results, silently dropping rejected (network/auth) fetches — so a fetch failure and a never-deployed env both showed the same grey idle dot. - The fetch now catches inside the map (preserving the env id) and records a distinct 'fetch-error' status for failures. - Canonical deploy-status (IA4) gains an 'unknown' tone → a hollow amber-ringed dot (visually distinct from the filled grey idle) + a 'Status unavailable' label. deployments.tsx TONE_ICON gets the matching entry. - The env-tab dot now also carries role=img + aria-label (the status was colour-only), pairing colour with an AT name. Tested: fetch-error → unknown tone/hollow dot/label; the fetch loop records fetch-error for a rejected env; the dot exposes its accessible name.
…e (DE7) The ConfigSection connection pill only re-checks on a provider change, so after a deploy failed with a RAPT/reauth error the pill kept showing green "Connected via Google OAuth" while the error banner said the OAuth session was no longer usable — a direct contradiction. deploy-panel now classifies an active failure (reusing classifyDeployError) and passes `authError` when it's a RAPT/reauth kind; ConfigSection suppresses the green pill in that case and shows an amber "sign-in expired — reconnect" warning instead (new deploy.status.reauthNeeded key, en+zh). Tested: authError=true → no green pill, amber reauth warning, no connected copy.
A partial destroy (some resources deleted, some failed) logged each failure and then fell through to resetDeploy(), which wipes state.logs — so the failures vanished in the same synchronous dispatch and the user saw a clean slate as if everything was destroyed (the failed resources still exist in the cloud). On a partial destroy, surface the failures as an error banner and KEEP the logs + overlay (skip the resetDeploy/clearCardDeployOverlay cleanup), so the user can see what's left and retry instead of losing the signal. Tested: partial destroy → summary + failure logs + deployError, no cleanup dispatches; the no-failures deleted-present path still cleans up as before. (Deferred, noted in plan: the DE5 cancelled-deploy callout — it tangles with the deliberate hydrate 'cancelled→error' mapping and needs a product call.)
Validation runs on a 500ms debounce and writes results to Redux, but the panel read only the resulting issues snapshot and never the `isValidating` flag — so after an edit a just-fixed field kept its red error (or a fresh mistake read clean) for ~half a second with no indication anything was pending. properties-panel now selects `validation.isValidating` and threads it to NodePropertiesSection, which renders a small spinner + 'Checking…' (role=status, aria-live=polite) at the top of the Config tab while a run is in flight. New `canvas.properties.validating` key (en+zh). Tested: the cue shows when isValidating=true and is absent when false.
When the provider auth pop-up is blocked, dismissed, or never appears, the deploy sits in 'authenticating' with the AuthBanner spinner and no recovery path other than re-clicking Plan/Deploy — which a user mid-auth won't think to do. AuthBanner gains an optional onReopen; deploy-panel passes handleAuthenticate so the banner shows a 'Didn't see the window?' hint + a 'Reopen sign-in window' button that re-launches the auth flow. New deploy.auth.noWindow/reopen (en+zh). Tested: button absent without onReopen; present + invokes onReopen when wired.
The Shift+A spotlight rendered a flat list with no recent-vs-catalog structure and showed each row's raw iceType category id instead of the localized, palette category label — so the same word read differently in the palette vs the spotlight. - Map each row's category through getCategoryMap(t), matching the palette's labels (SpotlightRow now takes a categoryLabel prop). - Add lightweight 'Recent' / 'All blocks' section headers when not searching (new canvas.spotlight.* keys, en+zh). Headers are non-interactive aria-hidden <li>s outside the ranked command list, so arrow-key nav is unaffected. Tested: row shows the localized label not the raw id; header is aria-hidden + non-interactive; row select/hover wiring intact.
…IA9) The URL-fallback breadcrumb branch was gated on !resolved.loading, so on every folder/project navigation the trail collapsed to just the Home icon until the resolver returned — flickering the user's sense of location. The URL segments already reflect where they are, so render a best-guess trail immediately and let the resolved labels replace it when they arrive. Tested: fallback crumbs now render from the URL even while loading=true.
The breadcrumbs advertise a /team top route and TeamPage is built + tested, but the route was never registered in app.tsx — so /team fell through to the catch-all DynamicContent and 404'd. Add the route (same AppBar + scroll wrapper as /settings, since TeamPage is a self-contained account page). Tested: the route list now includes /team; TeamPage wrapped in its ErrorBoundary.
…CCL1)
The toolbar's vertical/horizontal organize buttons set the rectangular edge
style so the orthogonal dagre routes they compute actually render; the other
directional organize entry points didn't — so they tidied positions but bezier
edges still cut through blocks (the computed routePoints were silently
discarded).
Switch to the rectangular edge style after the directional (master-branch)
organize at the remaining sites: the right-click context-menu, the import-time
auto-organize, and the AI operation-executor. Scoped deliberately:
- node-menu's per-container organize is excluded — applyEdgeRoutes is
master-branch-only, so per-container computes no routePoints to render.
- circular layouts are excluded — they're radial; orthogonal rectangular routes
don't apply (matches the toolbar's circular button, which also doesn't switch).
Tested: each directional path now also dispatches setEdgeStyle('rectangular');
circular does not.
…ng (CCL7) Cardinality rules (one Source/EnvVars per service; one source per log terminal) were only enforced at drop time — so a service that already has a repo wired still highlighted as a valid drop target during the drag, and the user only learned it was full when the wire snapped back on release. Fold the same pure conflict checks (findExistingSpecialConnection / findExistingLogSource) the drop handler uses into the connectionDragTargets memo, so an already-served target now dims as invalid-target during the drag. Schema-driven (reuses the existing rule helpers, no new state); card.edges added to the memo deps so it re-evaluates as wiring changes. Tested: a compatible target with a log-source or special-connection conflict is classified invalid-target; a free one stays valid; no card → check skipped.
The onNavigateSubpage handler was wired through TreeItem but never bound to any clickable element, so the project subpages (Architecture / Table / Deployments / Activity) were only reachable from the toolbar tabs tucked under the canvas. Add an 'Open' submenu to the tree context menu (project nodes only) that invokes onNavigateSubpage for each subpage. New projectBrowser.contextOpen key (en+zh). Tested: a subpage item calls onNavigateSubpage with the right id; the submenu is absent on folder nodes.
… out (CCL2) The rejection tooltip lives inside the scaled SVG group, so it shrank with the canvas — going illegible exactly when a user dragging a long wire most needs to read why the drop was rejected. Counter-scale the tooltip by 1/zoom (mirroring the SvgConnectionPath inverse-zoom precedent, clamped at 1/0.1 so a tiny zoom can't blow it up): the foreignObject is sized at TOOLTIP_WIDTH/zoom and the body counter-scaled to fill it, keeping a constant on-screen size while staying centred on the rejection point. Chose the in-place inverse-zoom over moving to a screen-space overlay (lower risk, no coordinate-conversion). Tested at zoom 0.5 / clamp / default. (Constant on-screen size worth an eyeball with the headed app.)
The canvas encodes a port's connection category in BOTH the socket's shape and colour (ROLE_SHAPE / CATEGORY_COLORS), but nothing decoded it — a first-timer had no way to learn that a diamond is a source repo or a square is a domain. Add a SocketLegend (circle=data/traffic, ring=config, diamond=source repo, square=domain/DNS) and mount it as a section in the controls-help modal. Colours sourced from CATEGORY_COLORS and shapes mirror ROLE_SHAPE so the key can't drift from the real socket rendering. New canvas.socketLegend.* keys (en+zh). Tested: all four shapes render with the right fill/stroke/rotation + labels. (Whether it lands the discoverability win is worth a headed eyeball.)
The active env lived only in Redux — never the URL — so a deploy/env view couldn't be bookmarked or shared, and a hard reload always reset to production. - Write: both env switchers (the EnvironmentTabBar tabs and the toolbar env dropdown) now set ?env=<id> (replace, so switching doesn't spam history). - Read: the first-load default honours a ?env= deep-link when it points at a real env, otherwise falls back to production (the prior behaviour). Tested: switching writes ?env; a ?env deep-link wins over the production default on load; a stale/missing ?env falls back to production. (Follow-up: deployments/activity could read ?env to warn on a Redux↔URL mismatch. End-to-end reload-restore worth a headed eyeball.)
DynamicContent, Breadcrumbs, ProjectBrowser and ResourcePalette each called useResolvePath independently, so every project-page navigation fired the same folder/project resolution (a POST per path segment) ~4× concurrently. Lift the resolve to a single shell-level ResolvePathProvider (mounted in app.tsx, inside BrowserRouter); the four consumers now read the shared result via useResolvePathContext. The provider preserves Breadcrumbs' top-route optimisation (resolve [] for /settings and /team), with TOP_ROUTES moved into the context module as the single source so the gate and the breadcrumb label can't drift. Tested: provider resolves full segments on project paths, [] on top routes, and not on deeper paths that merely start with a top-route slug; the hook throws without a provider; all four consumers' tests rewired to the shared hook. (End-to-end request de-dup worth confirming in the headed app's network tab.)
…ack (PE3) DesignRequirements hard-coded rules for only Database.PostgreSQL and Network.PrivateNetwork, so for every other block it rendered nothing — a user wiring a compute/storage/queue block got the same blank panel a fully-configured block gets. A truly general 'missing expected connection' rule would need a per-block connection-expectation schema that doesn't exist (PortDef has no required flag), so generalizing that is a product/schema decision, not a UX fix. Instead, fall back to surfacing the validator's own node issues when no bespoke rule applies: every INVALID block now gets a 'what's missing' summary at the top of the panel (visible on any tab, unlike the config-tab-only banner). Schema-respecting — it reads the validator output, no new per-block hardcoding; bespoke rules still win. Tested: fallback maps node issues (error/warning) when no rule applies; is suppressed when a bespoke rule fires; emits nothing for a valid non-bespoke block.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the UX review initiative — fixes across the canvas, properties panel, deploy/observability flows, navigation, and accessibility. Focus is honesty of status/feedback, deploy safety, and provider-neutral, legible UI.
What changed
shared/utils/deploy-status.ts) adopted on Environments, Deployments, and the status bar; stop fabricating a0.0scompletion time; keep partial-destroy failures in the log; drop stale "Connected"/reauth pills; add a "reopen sign-in" auth recovery.Clearto the log section, surface dropped log lines instead of silently capping, differentiate non-streaming log states, and honest copy feedback (logs-slice,log-node)./teamroute, breadcrumb flicker fix, resolve the URL path once at the shell.prefers-reduced-motionhonoured, accessible names for icon-only chrome, polite live region for deploy lifecycle, dialog semantics for the controls-help modal, shape glyph + aria-label for the zoomed-out poster status.services/deploy(drift service, log-stream source resolution) plus tests throughout.How I tested
pnpm typecheckpnpm lint:checkpnpm format:checkpnpm test:unitpnpm dev:all/pnpm dev:desktop