Log into Tekion DMS via browser, switch dealerships, navigate to Opcode Management, search for opcodes, and edit opcode details. Covers OTP retrieval via IMAP, session persistence, dealer switching pitfalls, and search quirks.
Resources
1Install
npx skillscat add joecastelino/jay-skill-pack/tekion-browser-navigation Install via the SkillsCat registry.
Tekion Browser Navigation & Opcode Editing
⚠️ V2 NOTICE (June 2026): Tekion deployed opcodeManagementV2. If the edit
page shows CP/W/I pay type buttons and Customer/Manufacturer rate spinbuttons
(not a Fixed Price combobox in an rt-tr-group table), use thetekion-opcode-pricing-v2skill instead. This skill covers the OLD V1 UI.
Key V2 differences: no Fixed Price dropdown, labor rate is entered in a[role="spinbutton"]Customer field, and the edit URL is/ro/opcode/edit/OPCODE(reachable by clicking the opcode text in the list).
Prerequisites
- Tekion credentials:
jcastelino@scvolkswagen.com/<TEKION_PASSWORD> - Gmail IMAP creds for OTP:
jcastelino@americanmotorscorp.com/<GMAIL_APP_PASSWORD> - Session file:
/home/itadmin/.tekion-session.json
Login Flow (2FA with OTP)
- Navigate to
https://app.tekioncloud.com - Enter username, click Next
- Enter password, click Login
- Fetch OTP — use Python imaplib with polling (NOT himalaya, it fails on HTML-only emails). Important: OTP emails can take 10-30 seconds to arrive after clicking Resend. Poll in a loop:
import imaplib, email, re, time
mail = imaplib.IMAP4_SSL("imap.gmail.com", 993)
mail.login("jcastelino@americanmotorscorp.com", "<GMAIL_APP_PASSWORD>")
otp = None
for attempt in range(8): # poll up to ~40s
time.sleep(5)
mail.select('"[Gmail]/All Mail"')
status, messages = mail.search(None, '(SUBJECT "Tekion-Login OTP" SINCE "DD-Mon-YYYY")')
# Use today's date in SINCE filter to avoid 130+ old OTP emails
ids = messages[0].split()
if not ids:
print(f"Attempt {attempt+1}: no new OTP yet...")
continue
# Get the latest
status, data = mail.fetch(ids[-1], "(RFC822)")
raw = data[0][1]
msg = email.message_from_bytes(raw)
# OTP is often in text/HTML part, NOT text/plain
for part in msg.walk():
ct = part.get_content_type()
payload = part.get_payload(decode=True)
if ct in ("text/plain", "text/html") and payload:
codes = re.findall(r'\b(\d{6})\b', payload.decode(errors="ignore"))
if codes:
otp = codes[0]
break
if otp:
print(f"OTP: {otp}")
break
mail.logout()
if not otp:
print("Timed out waiting for OTP — click Resend on Tekion page and retry")- Enter OTP code, click "Verify and Proceed"
- Save session immediately — extract localStorage tokens:
(function() {
const keys = ['t_token', 't_user', 'dse_t_user', 'currentActiveIsWorkspace', 'currentActiveWorkspace'];
const data = {};
for (const k of keys) { const v = localStorage.getItem(k); if (v) data[k] = v; }
return JSON.stringify(data);
})()Dealer Switching (CRITICAL — updated for Tekion 3.0, verified 2026-09-11)
The dealer switcher changed in Tekion 3.0. Old .ant-popover-inner-content selectors no longer apply.
Method that works (verified on :9223, TL 1092):
- Click the dealer pill at top-right (~1120, 32 on /home) using the :9223
/mouseendpoint:
curl -s 'http://localhost:9223/mouse' -X POST -H 'Content-Type: application/json' \
-d '{"x":1120,"y":32}'The /mouse endpoint reliably triggers the React click handler — eval-based el.click() or dispatchEvent often fail.
The popover opens with class
[class*="root_dealerInfo"](NOT.ant-popover-inner-content). It appears at ~(946, 57), w=313, h=360. Only 6-7 dealers are visible without scrolling.Scroll the list to reveal dealers below the fold (TL and VC are below the 360px viewport):
const list = document.querySelector('[class*="root_dealerInfoList_itemListContainer"]');
if (list && list.scrollHeight > list.clientHeight) {
list.scrollTop = list.scrollHeight; // scroll to bottom
}
// Typical: scrollHeight=336, clientHeight=260- Find and click the target dealer via tree-walker (most reliable — dealer text nodes render as exact names):
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let node;
while (node = walker.nextNode()) {
if (node.textContent.trim() === 'Toyota of Lancaster') { // exact match
// Walk up to clickable row container
let row = node.parentElement;
while (row && row !== document.body) {
const r = row.getBoundingClientRect();
if (r.height >= 30 && r.height <= 50 && r.width > 100) {
// Mark it and return coords
const cx = r.x + r.width/2;
const cy = r.y + r.height/2;
// Use /mouse endpoint to click
return {x: cx, y: cy};
}
row = row.parentElement;
}
}
}Then click with: curl -s 'http://localhost:9223/mouse' -X POST -d '{"x":<x>,"y":<y>}'
NOTE: element.click() on the found row DIV does NOT work — the React event handlers need the native mouse event that /mouse provides.
- Verify the switch:
localStorage.currentActiveDealerId // should match target dealer IDPitfalls:
- The popover closes quickly between actions — always combine open+scroll+find+click in rapid sequence (within the same eval call or with minimal delays).
- Dealer items render as
[class*="cursor-pointer root_dealerInfoItem_container"]divs (h=42), each containing an[class*="itemGroup"]with[class*="itemName"]. - TL (1092) and VC (1891) are always below the fold — scroll first, then find.
- Do NOT reuse coords across sessions — always re-derive from the current DOM.
Dealer IDs (all 7): AR=6195, BC=1251, BT=1249, ST=876, SV=826, TL=1092, VC=1891. 8th entry "AM"=American Motors Customs & Classics (uncertain if real store).
Opcode Management Navigation
- Once on correct dealer, click "OM" (Opcode Management) in the sidebar
- Page shows "Opcode List" with ~2,000 results
Searching for Opcodes
Pitfall: The opcode search is a React-controlled input. The JS nativeInputValueSetter trick does NOT reliably trigger React's state — it sets the field visually but the filter doesn't execute.
Method that works (confirmed reliable):
- Clear previous search: Click the X/close icon next to the search box (ref varies, look for
icon-closebutton) - Type fresh: Use
browser_typeon the search input (textbox "Search...") - Press Enter: Use
browser_press Enter— this triggers the React handler - Verify: Result count drops from ~2,000+ to "1 Result(s)" (or a small number)
DO NOT use JS nativeInputValueSetter + dispatchEvent — it sets the visual value but the React component's internal state doesn't update, so the filter never fires. browser_type + browser_press Enter is the only reliable approach.
If browser_type times out, it means the field is in a bad state — click the X/close icon to clear it, then retry.
Editing an Opcode — Page Layout & Scrolling
Tekion opcode detail pages are extremely long. The page body is only ~720px — the actual content lives in a nested scrollable container.
Finding the Scroll Container
// The main content scroll container is the second .overflow-auto element
document.querySelectorAll('.overflow-auto')[1].scrollTop = N;Key Scroll Positions (BGFINJ detail page at ST)
| Scroll Position | What's Visible |
|---|---|
| 0 | Opcode form fields (Type, Display Value, Description, Eligibility, Category, Job Priority, etc.) |
| ~1200 | Pay Type Pricing Setup table — CP row's Labor Rate "Select" dropdown, markup fields |
| ~2500 | Bottom of page — Save Draft, Cancel, Update buttons, "Consider for Auto Dispatch" checkbox |
Scrolling Method
Do NOT use browser_scroll — it scrolls the document body, not the .overflow-auto container. Always use JS:
document.querySelectorAll('.overflow-auto')[1].scrollTop = 1200; // pricing table
document.querySelectorAll('.overflow-auto')[1].scrollTop = 2500; // Save buttonsLabor Rate / Pricing Section
Pricing Table Row Layout
The pricing table uses React-Table (.rt-tr-group rows). When the full DOM is queried, rows follow this structure:
| Index | Content | Meaning |
|---|---|---|
| 0 | Individual Service | Opcode type info |
| 1 | Select Select Select... | Eligibility |
| 2 | Select... Select... | Standard Opcode Mapping |
| 3 | W All Select | Warranty header |
| 4 | CP All Select | Customer Pay header ← target |
| 5 | I All Select | Internal header |
| 6 | each $ | Parts row |
| 7-8 | Select % | Parts pricing |
| 9 | I % $ $ | Internal pricing values |
| 10 | CP % $ $ | Customer Pay pricing values |
| 11 | W % $ $ | Warranty pricing values |
The "Select" in row 4 is a React-Select component (Ant Design). Clicking the dropdown indicator opens options.
React-Select Portal Limitation (CONFIRMED HARD BLOCKER)
The React-Select dropdown options (e.g., "Fixed Price", "Menu Price", "Hourly Price") render in a React portal attached to document.body. This portal has been confirmed across multiple sessions to be:
- Invisible to
browser_snapshot— not in the accessibility tree - Invisible to
browser_vision— screenshots show no dropdown even when visually open - Invisible to JS DOM queries —
document.querySelectorAll('*')returns nothing matching "Fixed Price" even with dropdown open - Unclickable via JS —
.click()on the dropdown indicator does not open it; React synthetic event handlers don't fire from programmatic clicks
Verified across 3+ sessions with 20+ attempts. This is not a timing issue or a wrong selector — the portal simply doesn't render options into the DOM in a way that any automation tool can access.
Workarounds (in order of preference)
Tekion API approach: Use
browser_consoleto intercept the PUT/POST request when a human clicks Save/Update. The API endpoint likely follows the pattern/ro/opcode/edit/BGFINJor similar. Once captured, replay withfetch()using the samet_token.Ask the user: For a one-off opcode update, it's a 30-second manual task vs hours of automation fighting. Tell the user you hit the React-Select portal wall and ask them to make the change.
Overrides tab (accessible, but saves don't persist — see Override Save Non-Persistence above): The Overrides tab → Parts sub-tab is reachable after SPA nav (
window.location.href = '/ro/opcode/edit/OPCODE', then click Overrides tab, then Parts sub-tab). Existing overrides DO render in the accessibility tree (Make/Model/Year/Trim text visible). But new override additions require manual human save — automation cannot persist them. For one-off overrides like RACF cabin filter part pricing, the user can manually add them. SPA navigation after dealer switch is confirmed:window.location.href = '/ro/opcode/edit/RACF'works after clicking the dealer in the popover.
Override Save Non-Persistence (CONFIRMED HARD BLOCKER)
Puppeteer and browser tool saves do NOT persist for opcode overrides. This has been confirmed across multiple sessions (June 2026):
- A Puppeteer script that completes full login, navigation, override form filling, and
page.mouse.click()on the Save button reports success but overrides are absent on verification. - The browser tool's
dispatchEventon Save also fails to persist changes. - The script ran through 2 of 4 Avalon overrides (vehicle setup + part selection + Save click) without errors, yet zero overrides appeared when re-checking the Parts tab.
Workaround: Manual entry by the user (Joe) is the only reliable method for adding/editing opcode overrides. The UI requires genuine human interaction for the Save to commit changes to the backend.
Session Persistence
- Tekion sessions expire quickly (~30 min of inactivity)
- Session uses localStorage not cookies
- Save tokens to
/home/itadmin/.tekion-session.json - To restore: navigate to Tekion, inject tokens via
page.evaluate(), then reload - Session age > 20 hours = expired, must re-login
Common Pitfalls
- Browser session dies: If Tekion redirects to
/login, session expired. Full re-login needed. - Dealer dropdown not in DOM tree: The dropdown renders outside the accessibility tree. Use vision + JS.
- Search doesn't filter: React synthetic events required. Use
browser_type+browser_press Enter. - Page truncation: Tekion detail pages are massive. Use
browser_consoleto find specific elements by text content. - Himalaya fails on OTP emails: OTP emails are HTML-only. Use Python imaplib directly.