Standalone Playwright script that opens Ask FMCSA in Chrome on your computer, fills a support ticket, and submits it.
Clients can be looked up in the stsdata database and their documents pulled
straight off the network share — see Client search UI.
- Node.js 18+
- Windows, macOS, or Linux
- Chrome installed (falls back to Playwright Chromium if Chrome is missing)
- For client lookup: access to the STS MySQL database and the
sts-2025share
git clone https://github.com/kshivam4781/AskFMCSA.git
cd AskFMCSA
npm install
npm run install-browsersCopy the example env file and fill in your details:
copy config.example.env .envOn macOS/Linux:
cp config.example.env .envRequired values in .env (only when submitting without --client):
| Variable | Description |
|---|---|
ASK_FMCSA_EMAIL |
Contact email on the ticket |
ASK_FMCSA_FIRST_NAME |
First name |
ASK_FMCSA_LAST_NAME |
Last name |
ASK_FMCSA_DOT_NUMBER |
USDOT number |
Optional attachments (file paths on disk):
| Variable | Description |
|---|---|
ASK_FMCSA_DRIVING_LICENSE |
Driving license image/PDF |
ASK_FMCSA_AOI |
Articles of incorporation |
If an attachment path is omitted, the script still fills and submits the form without that file.
Instead of typing details by hand, look the client up in the stsdata database
and let the script pull the attachments off the network share.
npm run indexnpm run uiThen open http://127.0.0.1:5178. npm run index is a one-time cache of the
share folder listing and takes about three minutes over SMB; re-run it when
client folders are added or renamed.
Search by company name or USDOT, click a result, review what resolved, then run a dry run or submit. The submit button is red and asks for confirmation, because it files a real ticket with FMCSA. One client at a time — there is no bulk mode.
| Field | Source |
|---|---|
| Company, USDOT, email | stsdata.company (the database wins on conflict) |
| First / last name | the person half of the folder, e.g. ACME INC - JOHN DOE |
| Driving license | DL*, ACTIVE_DL*, newDL*, TEMP DL*, driver's license* |
| Articles of incorporation | AOI*, ARTICLES OF INC/CORP/LLC*, ARTICLES* |
| Statement of information | SOI*, STATEMENT OF INFORMATION* — only if no AOI |
Folders are named COMPANY - PERSON, so the company half is matched first
against the database name. The USDOT then confirms the match: the folder's
INFO\INFO.txt is read, and the match is verified when that USDOT appears on
the sheet. The UI shows the matched folder, the USDOT found there, and whether
it agrees with the database.
A sheet may legitimately list several USDOTs — an old and a new number, or a
leased authority (USING: X DBA Y ( MC: … DOT: … )). A match against any of
them confirms the folder; when the number is not the sheet's primary one, the UI
says so. USDOT-only lookup remains as a fallback for folders named differently
from the database record.
Measured over 300 random active clients: 92% matched a folder (91% of those by company name), and of the matches whose USDOT could be checked, 99% agreed with the database. The two that disagreed were genuine data discrepancies worth flagging, not matching errors.
company.FolderLocation is deliberately ignored — it is only 17% populated and
every value still points at the retired 10.1.10.210\sts2017 server.
Everyone gets a way into the client folder — the quickest route to anything the UI does not surface, or to a document filed under a name the patterns miss.
| Where | Control | What happens |
|---|---|---|
| At the machine running the server | Open folder | Explorer opens the folder directly |
| Anywhere else | Copy folder path | the full UNC path goes to the clipboard |
A web page cannot open Explorer on a visitor's machine — the button would launch it on the server, where nobody would see it. Remote users therefore copy the path and paste it into Explorer's address bar or Win+R, which lands in the same folder.
Clipboard note: navigator.clipboard only exists in a secure context and this
is plain HTTP over the LAN, so the copy falls back to the older
execCommand route. That needs a real click — if it ever fails, the control
selects the path instead so it can be copied by hand.
/api/open-folder accepts only directories inside the client share, refuses
non-local callers, and spawns the file manager with an argument array and no
shell, so a crafted path can be neither a different directory nor a command.
About 8% of clients match no folder automatically, and a few match several. The Matched folder row always carries a search link — "Search for the folder…" when nothing matched, "Not the right folder? Search…" when something did.
It opens a type-ahead over folder names across all three buckets, seeded with
the company name. Every typed term must appear in the name, so singh trans
finds SINGH TRANSPORT INC - GURVINDER SINGH; exact names rank first, then
prefixes, then substrings. Each suggestion shows its bucket and USDOT. Arrow
keys move, Enter selects, Escape closes.
Picking a folder re-resolves the client against it — name, attachments and INFO
sheet — and the row is badged chosen manually. The USDOT check still runs:
choosing the wrong folder is reported rather than silently accepted.
Matched folder BAINS XPRESS INC - BALJIT SINGH [chosen manually]
USDOT in folder 3464691 [differs from database]
USDOT 99999991 was not found on this folder's INFO sheet
(it lists 3464691, 3358976). Confirm this is the right client before submitting.
The re-resolve uses the original database row, not the already-merged values, so a first wrong pick cannot leak its USDOT into the second attempt.
Naming differs by state and by entity type. Corporations file articles or a certificate of incorporation; LLCs file of organization or of formation — so "Certificate of Organization" is the same document as "Articles of Incorporation", just a different state and entity type. Separators are optional, because "ARTICLESOFORGANIZATION" occurs too.
Sampling 700 client folders turned up 77 distinct formation-document shapes and 113 licence shapes, so the patterns are deliberately broad. The exclusions are what keep that safe:
DLICis the commonest licence name on the share — 115 of 700 folders — and needs its own rule, since the "DL" word-boundary test rejects it.AMERICAN DRIVING RECORDS - WEBVR.pdfis an MVR, not a driving license.ACTIVE DRIVER LISTis not one either. Both excluded.- Matching "certificate" at all means excluding the look-alikes, or the
commonest files on the share get attached instead of the real document:
MC CERTIFICATE/MC AUTHORITY(FMCSA operating authority, 28 of 700),PAYMENT SUCCESSFUL _ CALIFORNIA SECRETARY OF STATE(a fee receipt, 53 of 700),BUSINESS SEARCH _ … SECRETARY OF STATE(a search printout, 52 of 700), vehicle registrations, andCORP DISSOLUTION CERTIFICATE. SOI RCPT*(a filing receipt) andEIN LETTER AND SOI*are not statements of information, and rank last. Amendments and drafts are used only if nothing better exists.
Measured over 500 random folders, the broader licence rules take detection from
45% to 64%. The formation-document rules add about 1% on top of the folder
recursion, but they include the cases that were failing in practice, such as
0014129338 - Certificate of Organization.pdf.
When several files match, the one with the newest year in its name wins
(SOI 2026.pdf over SOI 2021.pdf).
INFO\, INFO\CORP DOCS\, INFO\DOT\ and the client folder root are searched
first. For the AOI and SOI only, if none of those has one, the rest of the
client folder is walked breadth-first (depth 3, capped at 300 directories) —
formation documents turn up under CORP\, PERMITS\, CALIFORNIA CORPORATION\, CORP FOLDER\INFO\CORP DOCS\ and similar. The walk is lazy, so
the common case costs no extra SMB round trips; a hit in INFO\ always beats
one buried deeper. When a document comes from an unusual folder the UI says
found in CORP next to the filename.
Measured over 400 client folders: recursing finds +11 AOIs and +7 SOIs, and takes a resolve from ~36 ms to ~103 ms.
Two things are deliberately excluded from the walk:
- Nested client folders. Some client folders contain a folder for a
different client, named the same way the top-level ones are
(
HWY 911 LLC - MUHAMMAD MUNIR\HX TRUCKING INC - HUI HE\INFO\ARTICLES OF INC.pdf). Recursing into those attached another company's formation document. Any subfolder whose name has a dash next to whitespace, or a run of two or more dashes, is skipped — which still searchesIRP 2022-2023andE-LOGS. - Drug-testing and driver paperwork (
DRUG TEST\,DONOR PASS\,TEST RESULTS\,DRIVERS\, …), which accounts for most of the depth in a client folder and cannot hold a formation document.
The driving license search is not recursive, on purpose. Client folders hold
employees' licences under DRIVER FILES\, DRUG TEST\DRIVERS\ACTIVE DRIVERS\<person>\ and PERMITS\EPN\DRIVERS\. Recursing would attach an
employee's licence instead of the responsible person's — the wrong document, and
an employee's ID sent to a federal agency. A missing DL is flagged for upload
instead.
Two documents go out with a ticket: the driving license and the articles of incorporation. Neither is reliably present — about 45% of folders have no AOI and roughly half have no driving license — so hand-supplying them is the common path, not an edge case.
The UI is explicit about which file is going out:
| Row | State | Shown |
|---|---|---|
| Driving license | found | the filename, badged DL |
| not found | no driving license found, plus a warning |
|
| uploaded | the uploaded filename, badged uploaded |
|
| Articles of inc. | AOI found | the filename, badged AOI |
| no AOI, SOI found | the SOI filename, badged SOI used — AOI not found, plus a warning |
|
| neither found | no AOI or SOI found, plus a warning |
|
| uploaded | the uploaded filename, badged uploaded |
Upload DL… and Upload AOI… are available in every one of those states,
so a stand-in SOI, a wrong file, or a missing one can always be overridden. An
uploaded file wins over anything found on the share, and remove reverts to
whatever the share had.
There is no stand-in document for a driving license the way an SOI stands in for an AOI — a missing one can only be supplied by hand.
Each row has a View button that opens the file in a modal, so the document can be confirmed rather than trusted on its filename — that the license belongs to the right person and has not expired, that the AOI is really articles of incorporation. The subtitle names where the file came from:
| Subtitle | Meaning |
|---|---|
from the client folder on the share |
found automatically |
statement of information, standing in for the AOI |
an SOI is being sent as the AOI |
uploaded by you |
your upload, which overrides the share |
PDFs and JPG/PNG render inline. TIFF is not displayable by any mainstream browser, so the modal says so and shows the full path instead of a broken frame.
/api/file serves only from the client share and the upload staging
directory, and only PDF/JPG/PNG/TIFF. The browser supplies the path, so it is
untrusted: symlinks are resolved before the check, and anything outside those
roots is refused with a 403. INFO.txt — which holds SSNs and portal passwords
— is inside the share but not a previewable type, so it is refused too.
The confirmation dialog names both attachments before a live submit:
SIAN MOTORS INC
USDOT 3399837
MAKHAN SINGH
waheguruji181020@gmail.com
DL: NOT FOUND — nothing will be attached
AOI: NOT FOUND — sending SOI 2026.pdf instead
That is the last point at which an SOI going out as articles of incorporation, or a ticket going out with no documents, can be caught.
Uploads are staged in a temp directory under their original filename, since that name is what FMCSA sees on the attachment; a directory component in a supplied name is stripped. Nothing is written back to the share: this tool reads client folders, it does not modify them.
Drop a file anywhere on the page and it goes to the right slot. The filename is classified with the same patterns the folder scanner uses, so a file it would have picked up as a licence is routed as one:
| Dropped | Goes to |
|---|---|
DL.pdf, newDL.pdf, ACTIVE_DL2025.pdf, DRIVERS LICENSE.pdf |
Driving license |
AOI.pdf, ARTICLES OF INC.pdf, ARTICLE OF LLC.jpg, SOI 2026.pdf |
Articles of inc. |
scan_0042.pdf, IMG_4821.jpg, AMERICAN DRIVING RECORDS - WEBVR.pdf |
asks you |
There is no drop overlay and no confirmation message: the attachment row updating is the feedback. Dropping before a client is selected does nothing.
Every drop is confirmed. The prompt always opens, with the detected slot highlighted and focused so confirming is one keypress — but a person still says yes. The other slot is one click away, and Cancel discards the file. Putting a client's licence on a federal ticket as their articles of incorporation is worth one extra click to avoid.
Several files can be dropped at once — each is classified independently, so the licence and the AOI can go together in one drop. A failed upload is logged to the browser console rather than shown on the page.
Every attempt is appended to .submissions.jsonl (one JSON object per line,
gitignored). Selecting a client shows when a ticket was last filed for them,
with the FMCSA reference number if it could be read off the confirmation page.
The Submission log button in the header lists everything filed.
Dry runs are recorded but flagged, and never count as "last submitted" — a rehearsal must not read as a filed ticket. Failed attempts are kept too, so the log is an audit trail rather than a success list.
tail -f .submissions.jsonlThe log is local to this machine. If the team needs shared visibility it should
move to a table in stsdata.
A ticket needs an email, first name, last name and USDOT. Anything that could not be resolved is shown as an empty red field with its own prompt, the submit button stays disabled, and a banner lists what is outstanding. Fill them in and the button unlocks. The server re-validates before launching Chrome, so a malformed USDOT or email cannot reach the FMCSA form.
node clients.js --find="ALL FREIGHT"node clients.js --dot=3902833node submit-ticket.js --client="ALL FREIGHT INC - SAQIB KHAN" --dry-runnode submit-ticket.js --client-dot=3902833--client refuses to guess when a name matches more than one folder; it lists
the candidates so you can narrow it.
By default the server listens on 127.0.0.1 and only this machine can reach
it. To let colleagues use it, set one value in .env:
UI_HOST=192.168.1.123
Use the machine's specific LAN address, not 0.0.0.0 — this host also has
VirtualBox and WSL interfaces that should not be listening.
Then allow the port through Windows Firewall, scoped to the office subnet:
netsh advfirewall firewall add rule name="Ask FMCSA UI" dir=in action=allow protocol=TCP localport=5178 remoteip=192.168.1.0/24Colleagues open http://192.168.1.123:5178. There is no password: access is
controlled by the network itself. Anyone who can reach the port can read client
documents and file tickets, so keep this on a trusted LAN and never
port-forward it.
| Local only | Served on the LAN | |
|---|---|---|
| Chrome for submissions | visible window | headless (set UI_HEADLESS=false to see it) |
| Submissions | run immediately | queued, one at a time |
| Open folder | shown | hidden — Explorer would open on the server |
| Submission log | this machine | shared by everyone, with the source address on each row |
Submissions are serialised because each one starts a Chrome on the host; two people clicking at once would otherwise race each other on the FMCSA form.
A submission is validated and accepted immediately, then runs when its turn comes. Everything is checked before the job is accepted — the four required fields, and that each attachment still exists — so a request that could never succeed is rejected outright instead of taking up a place in the queue.
Only one ticket is filed at a time: each starts a Chrome on the host, and two
running together would race each other on the FMCSA form. While waiting, the
operator sees Queued — 2 submissions ahead of this one.
The queue is shared, so everyone sees the same picture. The header shows what is
running (LOTT LOGISTICS INC submitting · 2 waiting) and the submission log
lists in-progress jobs above the history, with the address each came from.
On this form Continue files the ticket. The same button becomes a disabled
"Submitting…" and the page goes to
/app/ticket_confirm/refno/<reference>/, then redirects onward to
fmcsa.dot.gov within a second or two. There is no separate Submit step.
Success is therefore decided by the confirmation reference, never by a button click:
- A navigation watcher is armed before the form is touched, so the reference cannot be missed while the confirmation page redirects away.
- If the run errors after a reference appears, the output says
the ticket WAS filed despite the error above — do not resubmit. - The server marks a job successful whenever a reference was captured, even on a non-zero exit. Reporting a real filing as a failure invites someone to file it a second time.
--submit=false no longer clicks Continue at all, because clicking it would
file the ticket. To fill the form without filing, use --dry-run.
An uploaded or dropped document is also copied into the client folder, so it is found automatically next time and nobody has to upload it twice. This is the only place the tool writes to the share.
It goes into INFO when that exists, otherwise the client folder root. No
directory is created — inventing folder structure on the share is not this
tool's job.
The name decides whether the scanner finds it later, so:
| Uploaded as | Saved as | Why |
|---|---|---|
Certificate of Organization 2026.pdf |
unchanged | the name already says what it is |
DLIC new.pdf |
unchanged | recognised by the licence patterns |
scan_9911.pdf |
DL 2026-09-01.pdf |
meaningless name, canonicalised so it is matched |
a second scan_9912.pdf the same day |
DL 2026-09-01 (2).pdf |
never overwrites |
Nothing is ever replaced or deleted. A name that already exists gets
(2), (3), and so on. There is no code path that opens a file for writing
or removes one: the only operation is a copy to a name shown not to exist.
Losing a client's only copy of a document to a careless upload would be far
worse than an extra file in the folder.
A failed write cannot block a ticket. The submission always uses the staged temp copy, so a full or read-only share still files the ticket — the attachment row just says it was not saved, and why.
Saving happens on upload, not on submit. Pressing remove clears the file from the form; it does not delete anything from the share.
Tickets are normally filed no sooner than 7 days apart for the same client. Inside that window the history banner turns amber, and the confirmation names the problem:
⚠ A ticket was already submitted for this client 3 days ago (reference 250828-000123).
Tickets are normally filed no sooner than 7 days apart.
⚠ A submission for this client is running right now from 192.168.1.50.
Submitting again would file a second ticket.
The second warning matters on a shared tool: a job that is queued or running is not in the log yet — the row is written when it finishes — so the log alone cannot catch two people filing for the same client at once.
Neither warning blocks. Confirming goes ahead, because a repeat is sometimes right: the first attempt failed, or FMCSA rejected it. Dry runs never trigger a warning and never raise one for anyone else.
Change the window with RESUBMIT_AFTER_DAYS in public/index.html.
When a real ticket is filed, a message goes to the Google Chat space:
✅ Ask FMCSA ticket submitted — BAINS XPRESS INC
USDOT: 3464691
Contact: BALJIT SINGH
Email: singhbaljit171@yahoo.com
Reference: 250831-000123
Submitted from: 192.168.1.57
Failures are announced too, with the error instead of a reference. Dry runs are never announced — nothing was filed, so saying otherwise would be wrong.
Set CHAT_WEBHOOK_URL in .env to enable it; unset, notifications are skipped.
The URL carries a key and a token, so it never belongs in committed files. A
chat outage cannot fail a submission: the log row is written first, and a
webhook error is reported in the run output but the ticket still counts as filed.
Access is controlled by the network alone. Anyone who can reach the port — including a guest device on the same Wi-Fi — has the app in full, and inherits the host machine's share access without needing their own.
There are no user accounts, so the submission log records the address a request came from, which is the only trace of who filed a ticket.
Traffic is plain HTTP. On a trusted office LAN that is defensible; over a VPN or anything wider, it needs HTTPS.
The share is read using your existing Windows session. Database credentials live
in .env, which is gitignored — keep them out of committed files:
STS_DB_HOST=192.168.1.115
STS_DB_PORT=3306
STS_DB_USER=remote_user
STS_DB_PASSWORD=...
STS_DB_NAME=stsdata
STS_CLIENTS_ROOT=\\192.168.1.88\sts-2025\STS\CLIENTS2025\CLIENTS
Client data warning.
INFO\INFO.txton the share contains SSNs, EINs, DOT PINs and plaintext portal passwords.clients.jsreads only the USDOT and the contact email and ignores everything else — keep it that way. The UI binds to127.0.0.1only and must not be exposed on the network..client-index.jsonholds client names and USDOT numbers and is gitignored.
npm startOr pass values on the command line (overrides .env):
node submit-ticket.js --email=you@example.com --first-name=Jane --last-name=Doe --dot=3182953The browser stays open after submit. Press Enter in the terminal to close it.
| Flag | Description |
|---|---|
--client |
Resolve everything from a share folder name |
--client-dot |
Resolve everything from a USDOT (needs the index) |
--email |
Contact email |
--first-name |
First name |
--last-name |
Last name |
--dot |
USDOT number |
--dl |
Driving license file path |
--aoi |
Articles of incorporation file path |
--dry-run |
Fill the form but do not click Continue/Submit |
--submit=false |
Stop on the review page after Continue |
--keep-open=false |
Close the browser automatically when finished |
After a real submit the script prints reference: … when it can read the
incident number off the confirmation page; the UI stores that in the log.
Values resolve in this order: explicit flag → client folder → .env.
Examples:
node submit-ticket.js --dry-runnode submit-ticket.js --submit=falseOn https://ask.fmcsa.dot.gov/app/ticket:
- Email, first name, last name, USDOT
- Inquiry type: Login Assistance → Request assistance logging in to NCCDB, ELD, DataQs or other FMCSA System
- Question:
DOT LINK TO MOTUS ERROR - {USDOT} - Driving license and articles of incorporation, when available
- Continue, then Submit on the review page
The form element IDs are Oracle RightNow widget IDs (rn_TextInput_5_…). They
are position-dependent: if FMCSA adds or reorders a field, every selector breaks
at once and the script fails with visibility timeouts.
See config.example.env for:
ASK_FMCSA_CLICK_SUBMITASK_FMCSA_KEEP_OPENASK_FMCSA_DRY_RUNHEADLESSWAIT_TIMEOUT_MSBROWSER_CHANNEL(chromeorchromium)BROWSER_TIMEZONE