Skip to content

Ux/review fixes#35

Merged
julia-kafarska merged 39 commits into
mainfrom
ux/review-fixes
Jun 20, 2026
Merged

Ux/review fixes#35
julia-kafarska merged 39 commits into
mainfrom
ux/review-fixes

Conversation

@julia-kafarska

Copy link
Copy Markdown
Member

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

  • Deploy honesty & safety — canonical deploy-status vocabulary (shared/utils/deploy-status.ts) adopted on Environments, Deployments, and the status bar; stop fabricating a 0.0s completion time; keep partial-destroy failures in the log; drop stale "Connected"/reauth pills; add a "reopen sign-in" auth recovery.
  • Observability — wire Clear to the log section, surface dropped log lines instead of silently capping, differentiate non-streaming log states, and honest copy feedback (logs-slice, log-node).
  • Properties panel — required-field markers, per-field validation tab badge, advanced-tier disclosure, in-panel concept explainer, de-duped name field, "checking…" debounce cue, generalized validation fallback.
  • Canvas legibility — dagre routes from all organize directions, cardinality-aware drag highlighting, legible zoomed-out rejection tooltip, Shift+A spotlight matching the drag add-path, section-header spotlight + localized category labels, keyword/category palette search.
  • Navigation / IA — shareable environment deep-links, project subpages from the tree context menu, wire the /team route, breadcrumb flicker fix, resolve the URL path once at the shell.
  • Accessibilityprefers-reduced-motion honoured, 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.
  • Backing changes in services/deploy (drift service, log-stream source resolution) plus tests throughout.

How I tested

  • pnpm typecheck
  • pnpm lint:check
  • pnpm format:check
  • pnpm test:unit
  • Manual check in pnpm dev:all / pnpm dev:desktop
  • New / updated tests added

…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.
@julia-kafarska julia-kafarska merged commit c4c375c into main Jun 20, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant