- Step 15 of 17 in the agent-building course
Use Cursor to Build a Ticket Approval Interface You Can Test

Someone wants to relabel a support ticket as billing. They propose the change, look at exactly which version of the ticket they're changing, approve it, and apply it. If somebody else edits the ticket while they're reviewing, the server should refuse the old proposal and the screen should say why.
In this lesson you'll use Cursor to build that screen on top of a Python backend that's already written for you.
The download has an unfinished screen, a finished reference version to compare against, and seven HTTP checks that run for real. The reference was tested in a browser. It was not produced in a recorded Cursor session. The Cursor steps below follow Cursor's official docs as checked on September 17, 2026.
Start the supplied backend
You need Python 3.10 or newer, Git and the Cursor desktop editor, with an account that lets you use the model you pick. The app itself is just the Python standard library plus plain HTML, CSS and JavaScript. You don't install packages, add CRM logins or call a model API to run it. Cursor's own usage still counts against your account.
Download and unzip the ticket-review app exercise into a throwaway folder. Run:
python3 verify_http.py
python3 server.py
If your machine uses python instead of python3, use that. Open the URL it prints, usually http://127.0.0.1:8090. If that port is taken, run python3 server.py --port 8091.
The starter page shows the current made-up ticket. It has no buttons for proposing or approving yet. That's your job.
A few things to know about the server. It only listens on your own machine, and it keeps its data in a temporary SQLite database. There's no login. It uses one fixed pretend reviewer. Ctrl-C stops it and throws away the demo data. It's a practice exercise, not something to deploy for real users.
Understand the API before building the screen
The backend reuses the database logic from the approval lesson. Your screen's job is small: send a label or a decision. It should never make up a version number or rewrite an approval record.
Here's what the screen can call:
| Endpoint | Request | Result the UI must handle |
|---|---|---|
GET /api/ticket | No body | Current label and record version |
POST /api/propose | label: billing or technical | New pending proposal, observed version and expiry |
POST /api/decide | Proposal id and choice: approved or rejected | Recorded decision, or expired state |
POST /api/apply | Proposal id | Completed receipt, stale/expired refusal, or an error |
POST /api/simulate-change | Empty JSON object | Fixture-only concurrent version change |
Every POST sends JSON and only accepts the fields listed. When a proposal is created, the server notes the ticket's current version. Applying it later only works if the ticket is still on that same version. So the screen can't sneak an outdated change through by editing a hidden field.
The simulate-change endpoint is only there so you can test the failure case. It bumps the version number without changing the label, so you can see whether an old approval gets refused. A real app wouldn't have it.
Set up a local Cursor task
Open only the unzipped exercise folder in Cursor. Then save a Git starting point so you can see every change later:
git init
git add index.html app.js style.css server.py approval_lesson.py verify_http.py
git commit -m "Record ticket UI exercise baseline"
Use Cursor's Agent on your own machine, not a cloud setup. The Agent overview explains that it can edit files, run terminal commands and test in a browser.
Before you start, open Settings → Agents → Approvals & Execution. The Run Modes guide explains the three local modes: Auto-review, Allowlist and Run Everything. Pick one where commands get reviewed. Don't switch on Run Everything for this.
Choose any model your account offers, and write down its exact name next to your results. This lesson doesn't assume a particular model and doesn't compare models.
Give Agent index.html, app.js, style.css, server.py and verify_http.py as context, then ask for this:
Complete the local ticket-review interface in index.html and app.js.
Use the existing HTTP API; preserve server.py and approval_lesson.py.
Show current ticket label/version, a labeled label selector, and the exact
proposal with its original label, proposed label, version and expiry.
Provide Approve, Reject and Apply actions, with visible pending, approved,
rejected, stale, expired and completed states. On a completed retry, show
that the stored receipt was returned without another update.
Handle request errors visibly. Support keyboard use and a 390px viewport.
Keep the seven HTTP checks passing. Use no new packages or external services.
Do not copy reference.html or reference-app.js for this attempt.
Do not deploy, publish, change account settings or add integrations.
Those instructions keep the task narrow. They don't physically stop Agent from opening other files in the folder. So keep anything unrelated or private out of it.
Review edits while they happen
Cursor's Agent help says edits land in your files as the agent works. They aren't held back until you click accept. Read the diff as it goes.
Three rules should hold in the screen code:
- Picking a label creates a proposal. It doesn't apply the label.
- Approving records the decision. Applying is still a separate request.
- When the server says the proposal is stale or expired, the ticket stays as it was and the screen offers a fresh proposal based on the current ticket.
Look at what app.js actually sends and how it shows errors. Greying out a button helps people click the right thing, but server.py still has to reject bad requests on its own. If the agent changes the backend to get a screen test passing, compare that change against the API above before you keep it.
Cursor checkpoints can undo the agent's file edits. The checkpoint section of the Agent overview explains how they differ from Git. Neither one can undo a database write that already happened. In this exercise that's fine, because restarting the Python server resets the data.
Compare with the working reference
Stop the starter server with Ctrl-C, then run:
python3 server.py --reference
This serves reference.html and reference-app.js against the same backend. Use it to compare. It doesn't tell you what your own Cursor session will produce.
Starting from a fresh ticket, click through these steps:
- Choose Billing, then Create proposal. The state is pending, the reviewed version is
1, and Apply is disabled. - Choose Approve, then Apply approved change. The ticket becomes billing at version
2, and the state becomes completed. - Choose Read completion receipt again. The ticket stays at version
2. The message says the stored receipt was returned. - Create a Technical proposal and approve it. Choose Simulate a concurrent edit before applying. The current version becomes
3, while the proposal still refers to2. - Apply that proposal. The state becomes stale, the label remains billing, and the interface tells you to create a new proposal.
The success, retry and stale paths were all clicked through in the real reference page. The page also keeps the original and proposed labels on screen, so you can still see what was reviewed after the ticket changes underneath it.
Proposals expire after two minutes. The HTTP checks test expiry with a record that's already set up as expired, so they don't wait two minutes. To try it by hand, approve a new proposal, leave it alone until the expiry time shown, then click Apply.
Test the server without the screen
verify_http.py starts real servers on your machine, each with its own temporary database. It runs seven scenarios:
| Scenario | Observed result |
|---|---|
| Approve, apply and retry | One receipt; billing at version 2 |
| Change the target before apply | Stale refusal; no receipt |
| Reject, then request apply directly | HTTP 400; no change |
| Expired approved record | Expired refusal; no change |
| Apply without approval | HTTP 400; no change |
| Unknown label | HTTP 400 before proposal creation |
| Unrelated browser origin | HTTP 403 before proposal creation |
These checks prove the local API and the database rules behave. They don't prove real logins, how a real CRM would act, separation between many users, behavior under load, or recovery on a remote server. The server does check where requests come from, but one fixed pretend reviewer is still not a permission system.
For the screen itself, check that every control has a label. Move through it with Tab and Enter. Shrink the browser to 390px wide. The reference moves focus to the next button you'd need, or to the result message, and announces status changes to screen readers. Check the completed, rejected and stale screens too, not just the first one.
Save the final diff, the HTTP results and your notes from the browser. What you end up with is a small app where you can explain, and test, exactly what each button is allowed to do.
Once you can reproduce those results without trusting the agent's summary, move on to the n8n workflow lesson. The course index has the full order.



