Skip to content

bug(llc/frontend): Automation first-run entry drops its destination, and org containers kill canvas panning (post-merge #13939) #13996

Closed

Description

@mrveiss

Post-merge review of #13939 (PR #13959, merged 2026-08-11 07:15). Both defects are live on
Dev_new_gui
and both break behaviour the feature itself advertises. Independently verified.

H1 — the first-run path to Automation silently drops its destination

  • views/llc/AutomationCompanyRedirectView.vue:40 sends
    { name: 'llc-company-select', query: { redirect: route.fullPath } } where fullPath is
    /automation or /automation/canvas.
  • views/llc/CompanySelectorView.vue:52-53 honours the redirect only when it
    startsWith('/llc/').

/automation/canvas does not, so it is discarded. A first-run user clicks Automation in the main
nav, is sent to the company selector, picks a company, and lands on the company backlog — never
reaching automation. The forwarder's own docblock (:8-9, "carrying the destination so the selector
can return there") is false.

The existing test (__tests__/AutomationCompanyRedirect.test.ts:70-77) asserts only that replace
was called with that query object. It never exercises the selector, so it is green while the
behaviour it names is broken
.

Fix: widen the selector allowlist to accept /automation too, keeping the leading-slash and
no-// checks so it stays closed to open redirects. Test must drive the selector, not the forwarder.

H2 — org containers blanket the canvas and kill the pan gesture the UI tells you to use

  • components/workflow/WorkflowCanvas.vue:429-430 — startPan fires only on middle-click or
    shiftKey, and it is bound on .canvas-area (:68).
  • :85 — every node div carries @mousedown.stop="startDrag(node, $event)", so a press on a node
    never reaches startPan.
  • An org-group container is sized to its entire subtree (928×482 for a 6-person forest), so it
    covers essentially the whole drawing area.

Result: shift-drag over a container calls startDrag(container) → emits node-moved →
OrgChart.vue:135-136 deliberately ignores containers → nothing happens. Panning works only in
the 60px gutters. Meanwhile llc.orgChart.canvasHint ships in all 11 locales telling the user to
shift-drag.

Neither drag test can see it: both call vm.$emit('node-moved', …) directly rather than performing a
gesture.

Fix: bail out of startDrag when e.shiftKey || e.button === 1 so pan wins, or set
pointer-events: none on .org-group with its header re-enabled. Test with a real gesture.

M1 — user drags are discarded by pause/resume, and the comment says the opposite

views/llc/OrgChart.vue:49-56. buildOrgCanvasGraph reads name/title/status/adapter_type/is_human
off deeply-reactive tree nodes, so the watchEffect subscribes to all of them. toggleAgentPause
writes node.status at :99 → the whole array rebuilds → every dragged position resets.
Pause/resume is the primary canvas-mode action, so drags survive almost nothing.

Two facts on the same lines: the comment claims "the canvas mutates node.position in place" — it
does not, it emits node-moved (WorkflowCanvas.vue:456). And :100 replaces selectedNode with a
detached shallow copy, so a second toggle mutates the copy and the canvas status dot goes stale
(pre-existing; canvas mode is what makes it visible).

Fix: drive the layout from an explicit shallow source (e.g. watch the root id list) and merge
status changes into existing nodes rather than rebuilding.

Cheap same-file wins to take in the same pass

  • M2 — WorkflowCanvas.vue:236 nodeIcons is Record<string, IconName> and has no loop key,
    while 'loop' is in WorkflowNode['type'] and nodeLabels does have it. <Icon :name="undefined">
    now dev-warns "Missing required prop: name" on every loop node — the previous <i :class> form
    failed silently, so fixing the icons introduced a warning. Type the map by node type so the missing
    entry is a compile error.
  • M3 — nodeStyle (:274-283) honours data.width/data.height for every node type, and
    WorkflowNode['data'] is arbitrary. Latent today, but it is unnamespaced coupling on a component
    two features depend on. Gate on node.type === 'org-group'.
  • M4 — AutomationCompanyRedirectView.vue:41 builds a bare path, dropping route.query and
    route.hash. useEntityAnchors.ts:76,78 emits { query: { workflow: id } }. One-line fix.

Acceptance criteria

  • First-run Automation entry reaches automation, proven by a test that drives the selector.
  • Shift-drag pans anywhere on the canvas, proven by a real gesture test, not an emit.
  • A drag survives a pause/resume.
  • Each fix mutation-proved: break it, confirm red, restore, confirm green.

Activity

  1. mrveiss commented on Aug 11, 2026

    @mrveiss
    OwnerAuthor

    Fix opened as #14003 (branch issue-13996). All four in-scope defects (H1, H2, M1, plus M2/M3/M4) are addressed; #13994, #13993 and #13995 are deliberately untouched.

    Each of H1, H2 and M1 is mutation-proved in the PR body — the fix is broken, the new test is confirmed red, the fix is restored and the test is confirmed green. Notably the two pre-existing drag tests and the RTL pan test stay green under the H2 and M1 mutations, which is exactly why these defects shipped: they emit node-moved and start their gesture in the canvas gutter.

  2. mrveiss commented on Aug 11, 2026

    @mrveiss
    OwnerAuthor

    Found-but-not-fixed while working this issue, filed rather than dropped:

    Both are out of scope for #14003, which is a post-merge defect fix.

  3. github-actions commented on Aug 11, 2026

    @github-actions
    Contributor

    PR #14003 (merged to Dev_new_gui) references this issue with a close keyword.

    fix(llc/frontend): keep the automation destination, unblock canvas panning, survive pause/resume (#13996)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  4. mrveiss commented on Aug 11, 2026

    @mrveiss
    OwnerAuthor

    Delivered in PR #14003, squash-merged into Dev_new_gui as 49aa53b2c. Closing manually —
    Closes #NNN does not fire on Dev_new_gui.

    Verified on the merged base, not assumed:

    CompanySelectorView.vue      safeRedirectTarget            2 uses
    redirectTarget.ts            dot-segment guard             present
    WorkflowCanvas.vue           @mousedown="onNodeMouseDown"  present (.stop removed)
    OrgChart.vue                 watch(layoutKey, …)           present
    

    H1 — the Automation first-run entry. The forwarder sent ?redirect=/automation/... while the
    selector honoured only startsWith('/llc/'), so the destination was discarded and a first-run user
    landed on the backlog. Fixed by extracting safeRedirectTarget() as one validator every ?redirect=
    consumer shares, rather than widening one comparison.

    H2 — canvas panning. startPan fires only on shift/middle-click on .canvas-area, but every node
    carried @mousedown.stop, and an org-group container is sized to its whole subtree — so it
    blanketed the canvas and shift-drag did nothing, while the hint string telling users to shift-drag
    shipped in all 11 locales. The original brief suggested bailing out inside startDrag; that would not
    have worked, because .stop halts propagation before the handler's verdict matters. The binding was
    replaced instead.

    M1 — drags surviving pause/resume, plus M2–M4 (icon map typing, nodeStyle gated to org-group,
    query/hash preserved through the forward).

    Added during review — traversal. /automation/../../evil passed the prefix check, and although a
    path-absolute string can never change the origin (proved across 34 payloads), history.pushState
    hands the raw string to the browser, which collapses the dot segments. An attacker-set ?redirect=
    could therefore steer an authenticated user to any in-app route. Rejected dot segments including the
    percent-encoded spellings; mutation-proved — disabling the guard reddens exactly the four traversal
    cases.

    Mutation evidence. Three proofs from the author, all independently reproduced by review, plus a
    fourth the reviewer wrote themselves to confirm the pan fix cannot silently kill node dragging.
    Notably, under two of the mutations the pre-existing tests stayed green — they emit node-moved
    instead of gesturing, which is precisely why the defects shipped.

    Merge note. This branch conflicted with #13945 in WorkflowCanvas.vue exactly as review predicted
    — nodeFlag against the nodeStyle rewrite. Resolved keeping both; they are complementary. Final
    state: 229 tests pass, lint and vue-tsc clean, 16 CI checks green.

    Follow-ups filed rather than folded in: #14079 (a shift-pan starting on a node opens that node's
    drawer), #14080 (/login?redirect= written by two 401 handlers and read by nothing), #14103 (the RTL
    pan assertion is order-dependent).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions