| name | kuisioner-survey-auto |
| description | Automates filling the UPI EPBM lecturer evaluation questionnaire at siak.upi.edu/evpbm/kuisioner.php using Playwright MCP browser tools — logs in with a SINO (NIM) account, walks the intro → login → course-list → per-course questionnaire wizard, and randomly fills every Likert-scale (1-9) radio group, optionally leaving existing answers untouched. This is the Playwright-MCP successor to the chrome-extension/ folder in this same repo (which did the same thing via a content script on survey.upi.edu). Use this skill whenever the user asks to "isi kuisioner PBM", "fill the EPBM survey", "auto-fill siak evpbm", or run the survey-auto-filler through Claude/Playwright instead of the Chrome extension. |
Kuisioner Survey Auto (Playwright MCP edition)
Automates https://siak.upi.edu/evpbm/kuisioner.php — UPI's lecturer teaching
evaluation ("Evaluasi Proses Belajar Mengajar"). This is a classic multi-page
form wizard (full page POSTs, not an SPA), unlike chrome-extension/ which
only had to handle a single already-loaded survey page on survey.upi.edu.
Two scripts, two jobs:
scripts/fetch-submit.js — primary method. Drives the course-selection
and submission steps via direct fetch() POSTs instead of UI clicks, once
logged in. Verified live: faithfully reproduces what a real click would
send, supports submitting every pending course concurrently, and is far
fewer tool calls than clicking through each course.
scripts/answer-logic.js — fallback, a straight port of
chrome-extension/content.js's random-answer logic, for driving the real
screen-4 DOM directly if the fetch flow ever breaks. See "Fallback" below.
Requires the playwright MCP server's browser_ tools.* If they aren't
loaded, use ToolSearch for mcp__plugin_playwright_playwright__browser_navigate
etc. first.
Why care about screens
The site has no client-side routing — every "Lanjut >" (Next) click is a form
POST that reloads the whole page. All Playwright element refs (ref=...)
become stale after every click, so always re-snapshot after navigating
rather than reusing refs from a previous snapshot.
Credentials
Default account lives in data.json at the repo root (sibling to this
.claude/ folder): {"username": "<NIM>", "password": "<pass>"}. Read that
file for the NIM/password unless the user gives different ones. Never print
the password back to the user or commit it anywhere new — it already lives in
that gitignored/local file.
Flow
- Navigate:
browser_navigate to https://siak.upi.edu/evpbm/kuisioner.php.
A plain GET always renders screen 1 — there is no session-based step
shortcut, so this is also how you get back to a known state at any point.
- Screen 1 — intro ("Tentang Kuesioner"): snapshot, click the "Lanjut >" button.
- Screen 2 — login ("Form Login", "Gunakan akun internet (SINO) anda"):
snapshot, fill the NIM textbox and Password textbox (
browser_fill_form,
both type: "textbox") with the credentials from data.json, then click
"Lanjut >" to submit. Do this through the real UI, not fetch —
login-via-fetch has never been tested, only login-via-UI has been
verified to work.
- Screen 3 — course list ("Kontrak Kuliah"): a table, one row per
enrolled course this semester, each row a self-contained
<form> with
hidden fields identifying that exact course (kodemk, kodedsn, kuliah,
dosen, fakultas, prodi, etc.) plus a btnlanjut submit input. Rows
with no btnlanjut input are already evaluated. From here, switch to the
fetch-based flow below — it's faster and doesn't fight stale Playwright
refs (every "Lanjut >" click reloads the whole page and invalidates every
ref=... from the previous snapshot).
Fetch-based course loop (primary method)
Read scripts/fetch-submit.js and inject its full contents into
browser_evaluate while on the screen-3 course list, then call
window.__surveyFetchSubmit.runAll(concurrent):
browser_evaluate({ function: "() => { <contents of fetch-submit.js> ; return window.__surveyFetchSubmit.runAll(true); }" })
This scrapes every still-pending course from the current page's DOM (no
hardcoded course codes), and for each one: POSTs the course-selection step to
fetch that course's real questionnaire form, reads its hidden fields and
radio group names off the returned HTML, builds the submission body (each
radio group gets a random 7-9 value via MIN_VALUE, plus a random one-sentence
Indonesian comment in saran from REVIEW_COMMENTS), and POSTs the
submission. concurrent: true fires every course's submit via Promise.all
— this is what "parallel" means here in practice: real HTTP concurrency, not
multiple browser tabs. Tabs were tried and rejected — see "Why not tabs"
below. Pass false to submit one at a time instead if you want closer
per-course visibility.
Do not run this without explicit user confirmation first — every course
in the scrape gets submitted, and submission is irreversible.
Verifying the result
The submit response is not a reliable success signal — verify observed
this returning HTTP 500 on submissions that had, in fact, succeeded (some
server-side rendering bug after the write completes, not a real failure).
The only trustworthy check: browser_navigate back to the URL, click through
intro + login again (full UI, since GET always resets to screen 1), and look
at the course list — courses that no longer show a btnlanjut form are
confirmed done. If the entire "Kontrak Kuliah" section renders as an empty
page (no table, no student info block, just the step indicator), that means
zero courses remain pending — everything submitted successfully.
Why not tabs
Opening a separate browser tab per course was requested and tried, but
rejected in favor of fetch for two structural reasons, not preference:
- Tool calls happen one at a time regardless of how many tabs are open — an
agent can't act on two tabs within a single turn, so tabs alone buy no
wall-clock speedup.
- A fresh tab always lands on screen 1 (see above) — each tab would need its
own full login pass, adding overhead rather than removing it.
fetch-based concurrency has neither problem: no UI navigation needed per
course, and every request is fully self-contained (explicit kodemk/kodedsn
identify the course), so concurrent submission carries no cross-course
corruption risk even if the PHP session serializes requests server-side.
Fallback: DOM-based fill (if fetch breaks)
If the site changes in a way that breaks the fetch flow (e.g. new hidden
fields, different response shape), fall back to driving the real screen-4
page directly: read scripts/answer-logic.js and inject it via
browser_evaluate on each course's questionnaire page after clicking into it
from the course list, then call fillUnansweredOnly() (only fills blank
groups) or fillAllRandomAnswers() (overwrites everything — confirm with the
user every time, this is destructive) followed by fillRandomComment(), then
click the real Submit button (browser_click) so any future onsubmit JS the
site might add is respected. This is slower (one browser round-trip per
course, real clicks, stale refs after every navigation) but has no
assumptions about the request/response shape beyond "there's a form with
radios and a submit button" baked in.
Safety notes
- Submitting is irreversible and affects a real university system tied to
the logged-in account — always get explicit go-ahead before running
runAll() or clicking Submit, and before any "Isi Semua" / overwrite pass.
- Never trust HTTP status from the submit fetch as a success/failure signal
— always verify via a fresh course-list load (see above).
- If the login screen reappears unexpectedly mid-run, the session likely
expired or reset — re-snapshot, don't assume old refs or previously-fetched
hidden fields are valid, and log in again before continuing.
- If the page structure doesn't match what's described here (e.g. radio
names, hidden field set, or number of screens changed), stop and re-run the
discovery steps rather than guessing — this site can change without notice.