diff --git a/crates/ec-node/src/nbc.rs b/crates/ec-node/src/nbc.rs index e4dd3ef..c681c66 100644 --- a/crates/ec-node/src/nbc.rs +++ b/crates/ec-node/src/nbc.rs @@ -816,6 +816,21 @@ fn advance_nbc_auth_flow(tab: &Arc) -> Result> actions.push(action); return true; }}; + const pressEnter = (node) => {{ + if (!node) return false; + for (const type of ["keydown", "keypress", "keyup"]) {{ + node.dispatchEvent?.(new KeyboardEvent(type, {{ + key: "Enter", + code: "Enter", + which: 13, + keyCode: 13, + bubbles: true, + cancelable: true, + }})); + }} + actions.push("press:enter"); + return true; + }}; const setValue = (node, value) => {{ if (!node) return false; const prototype = Object.getPrototypeOf(node); @@ -835,7 +850,7 @@ fn advance_nbc_auth_flow(tab: &Arc) -> Result> const url = window.location.href || ""; const candidates = Array.from( document.querySelectorAll( - "button,a,[role='button'],label,li,div,span,[data-provider-name],[data-provider-id],[data-provider]" + "button,a,[role='button'],[role='option'],label,li,[data-provider-name],[data-provider-id],[data-provider]" ) ); @@ -873,9 +888,24 @@ fn advance_nbc_auth_flow(tab: &Arc) -> Result> }}); if (input && normalize(input.value || "") !== normalize(provider)) {{ setValue(input, provider); + pressEnter(input); }} - const providerNode = candidates.find((node) => {{ + const inputRect = input?.getBoundingClientRect?.() || null; + const searchNode = inputRect && candidates.find((node) => {{ + const text = textOf(node); + const rect = node.getBoundingClientRect?.(); + if (!visible(node) || !rect) return false; + const sameRow = Math.abs((rect.top + rect.height / 2) - (inputRect.top + inputRect.height / 2)) <= Math.max(48, inputRect.height); + const nearRightEdge = rect.left >= inputRect.right - 24 && rect.left <= inputRect.right + 160; + const looksLikeSearch = text.includes("search") || text.includes("find"); + const compactButton = rect.width <= 96 && rect.height <= Math.max(96, inputRect.height * 2); + return (looksLikeSearch || compactButton) && sameRow && nearRightEdge; + }}); + clickNode(searchNode, "click:provider-search"); + + const providerNode = candidates + .filter((node) => {{ const text = textOf(node); const providerMeta = normalize([ node.getAttribute?.("data-provider-name") || "", @@ -885,7 +915,8 @@ fn advance_nbc_auth_flow(tab: &Arc) -> Result> return visible(node) && providerNeedles.some((needle) => text.includes(needle) || providerMeta.includes(needle) ); - }}); + }}) + .sort((a, b) => textOf(a).length - textOf(b).length)[0]; clickNode(providerNode, `click:provider:${{textOf(providerNode).slice(0, 120)}}`); }} diff --git a/evolution/proposals/ECP-0105-stable-home-for-forge-nbc-browser-workers.md b/evolution/proposals/ECP-0105-stable-home-for-forge-nbc-browser-workers.md new file mode 100644 index 0000000..3071f19 --- /dev/null +++ b/evolution/proposals/ECP-0105-stable-home-for-forge-nbc-browser-workers.md @@ -0,0 +1,31 @@ +# ECP-0105: Stable home directory for forge NBC browser workers + +## Why + +The forge NBC worker runs Chrome under a dedicated service user and a persistent profile, but the +publish unit was not explicitly setting `HOME`. + +That leaves browser helper processes free to derive state paths from ambient defaults instead of the +intended service home. On a long-running forge host, that makes the browser worker more vulnerable +to stale locks, crash-report paths, and profile contamination from out-of-band debugging sessions. + +## Decision + +1. Set `HOME=/var/lib/every-channel` on NBC `wt-publish` units, not only on the Xvfb helper units. +2. Keep the persistent NBC profile and auth artifacts under `/var/lib/every-channel`. +3. Treat the forge NBC browser runtime as a single-service home/profile domain so cleanup and + troubleshooting stay deterministic. + +## Consequences + +- Forge NBC launches use the same home directory across the display service and publish service. +- Chrome helper processes no longer need to infer state roots from ambient defaults. +- Manual debugging sessions must either reuse the service home intentionally or use an isolated + profile path to avoid poisoning the live worker profile. + +## Rejected Alternatives + +- Keep only `--user-data-dir` and leave `HOME` implicit: rejected because browser helper processes + still derive ancillary paths outside the intended service state root. +- Give the publish unit a separate home from the display unit: rejected because it makes the forge + browser runtime harder to reason about and recover. diff --git a/evolution/proposals/ECP-0106-forge-nbc-workers-need-tmp-and-search-driven-mvpd-selection.md b/evolution/proposals/ECP-0106-forge-nbc-workers-need-tmp-and-search-driven-mvpd-selection.md new file mode 100644 index 0000000..7e1e795 --- /dev/null +++ b/evolution/proposals/ECP-0106-forge-nbc-workers-need-tmp-and-search-driven-mvpd-selection.md @@ -0,0 +1,35 @@ +# ECP-0106: Forge NBC workers need `/tmp` and search-driven MVPD selection + +## Why + +The forge NBC worker reached two distinct failure domains: + +1. Chrome failed during early startup under the hardened `wt-publish` unit even though the same + browser launch worked outside the systemd sandbox. +2. Once the browser launch succeeded, the MVPD picker automation could reach the provider gate but + still mis-clicked broad page containers instead of the intended provider search result. + +## Decision + +1. Allow NBC `wt-publish` units to write to `/tmp` in addition to the persistent profile and auth + directories. +2. Treat the NBC MVPD picker as a search-first flow: + - type the configured provider name + - submit the search explicitly + - prefer short, actionable provider-result nodes over generic container matches +3. Keep the provider name configurable through `EVERY_CHANNEL_NBC_MVPD_PROVIDER`, with `Verizon Fios` + remaining the default. + +## Consequences + +- Forge NBC workers align better with Chrome's actual startup needs under systemd hardening. +- MVPD automation becomes less likely to click the whole picker page or other non-provider chrome. +- Future provider integrations should extend the same search-first DOM strategy instead of adding + brittle page-wide text matches. + +## Rejected Alternatives + +- Disable most systemd hardening for NBC units entirely: rejected because `/tmp` write access is the + smallest validated change that unblocks Chrome startup. +- Keep broad `div` and `span` provider scans: rejected because they can match large container nodes + whose text merely happens to include the provider name. diff --git a/nix/modules/ec-node.nix b/nix/modules/ec-node.nix index 59b8a81..f8c2b02 100644 --- a/nix/modules/ec-node.nix +++ b/nix/modules/ec-node.nix @@ -646,6 +646,7 @@ in SystemCallArchitectures = "native"; ReadWritePaths = lib.optionals cfg.control.enable [ "/run/every-channel" ] + ++ lib.optionals isNbc [ "/tmp" ] ++ lib.optionals isNbc [ cfg.nbc.profileDir cfg.nbc.authScreenshotDir ]; }; @@ -656,6 +657,7 @@ in EVERY_CHANNEL_NBC_CHROME_PATH = cfg.nbc.chromeBinary; EVERY_CHANNEL_NBC_PROFILE_DIR = cfg.nbc.profileDir; EVERY_CHANNEL_NBC_NO_SANDBOX = if cfg.nbc.noSandbox then "1" else "0"; + HOME = "/var/lib/every-channel"; }; }; })