Class: Transport::PlaywrightPortalConnection
- Inherits:
-
Object
- Object
- Transport::PlaywrightPortalConnection
- Defined in:
- app/services/transport/playwright_portal_connection.rb
Overview
Browser-based transport for partner portals that require form-based file uploads.
Uses Playwright to automate login, navigation, and file upload in a headless browser.
This is the transport equivalent of SFTP or HTTP API connections, but for
JavaScript SPAs that lack a programmatic API (e.g., Menard partner portal).
The Menard portal is a PingOne SSO JavaScript SPA — the login form is rendered
dynamically by main.bundle.js, not present in the initial HTML.
Defined Under Namespace
Classes: PortalBounce
Constant Summary collapse
- NAVIGATION_TIMEOUT =
Timeout for navigation.
60_000- SPA_RENDER_TIMEOUT =
Timeout for spa render.
45_000- UPLOAD_ATTEMPTS =
Attempts at the whole login+upload flow. The portal bounces intermittently
(3 of 6 runs 08-04..08-05); a bounce is detected in ~1s, so a second attempt
fits comfortably inside InventoryMessageSender::SEND_TIMEOUT_SECONDS. 2- DIAGNOSTIC_TIMEOUT =
Seconds a best-effort diagnostic (HTML dump, screenshot) may take. These run
when the session is already broken, so their protocol calls are the likeliest
to hang — and a diagnostic must never outlive the thing it is diagnosing. 15
Instance Attribute Summary collapse
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
Instance Method Summary collapse
-
#initialize(options = {}) ⇒ PlaywrightPortalConnection
constructor
A new instance of PlaywrightPortalConnection.
-
#send_file(file_path, _remote_filepath = nil) ⇒ Hash
Uploads a file to the portal.
Constructor Details
#initialize(options = {}) ⇒ PlaywrightPortalConnection
Returns a new instance of PlaywrightPortalConnection.
40 41 42 43 44 45 46 |
# File 'app/services/transport/playwright_portal_connection.rb', line 40 def initialize( = {}) @options = @headless = .fetch(:headless, true) @logger = [:logger] || Rails.logger @portal_base_url = [:portal_base_url] @credentials = [:credentials] || {} end |
Instance Attribute Details
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
33 34 35 |
# File 'app/services/transport/playwright_portal_connection.rb', line 33 def logger @logger end |
Instance Method Details
#send_file(file_path, _remote_filepath = nil) ⇒ Hash
Uploads a file to the portal. Returns a result hash compatible with other transports.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'app/services/transport/playwright_portal_connection.rb', line 52 def send_file(file_path, _remote_filepath = nil) raise ArgumentError, "File not found: #{file_path}" unless File.exist?(file_path) logger.info("[PlaywrightPortal] Starting upload of #{File.basename(file_path)}") logger.info("[PlaywrightPortal] Using remote Playwright server #{PlaywrightRuntime.server_url}") if PlaywrightRuntime.remote? PlaywrightRuntime.with_browser(launch_options: ) do |browser| active_page = nil page = nil attempt = 0 begin attempt += 1 context = browser.new_context( userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' \ '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' ) page = context.new_page attach_diagnostics(page) active_page = login(page) upload_file(active_page, file_path) rescue PortalBounce => e logger.warn("[PlaywrightPortal] #{e.} (attempt #{attempt}/#{UPLOAD_ATTEMPTS})") if attempt < UPLOAD_ATTEMPTS context.close retry end dump_page_html(active_page || page, 'portal_bounce') { success: false, message: "Browser automation failed: #{e.}" } # Log BEFORE dumping: the dump is a protocol call on an already-broken # session and can hang, and losing the real error to a hung diagnostic is # what left the 07-13..08-03 outage with no message to go on for weeks. rescue Playwright::Error, Playwright::TimeoutError => e logger.error("[PlaywrightPortal] #{e.class}: #{e.}") dump_page_html(active_page || page, 'timeout_failure') { success: false, message: "Browser automation failed: #{e.}" } rescue StandardError => e logger.error("[PlaywrightPortal] #{e.class}: #{e.}") dump_page_html(active_page || page, 'unexpected_failure') { success: false, message: "Upload failed: #{e.}" } end end rescue StandardError => e logger.error("[PlaywrightPortal] Fatal: #{e.class}: #{e.}") { success: false, message: "Upload failed: #{e.}" } end |