# Use Cursor to Build a Ticket Approval Interface You Can Test

> Build one local ticket-review flow in Cursor, then verify proposal, approval, stale rejection and retry behavior through the UI and real HTTP checks.

- Source: https://www.zarifautomates.com/blog/how-to-use-cursor-ai-to-build-web-applications
- Published: 2026-09-17
- Updated: 2026-09-19
- Pillar: Agents & AI Engineering
- Tags: agent-course, ai-engineering, practical-guide
- Author: Zarif

---

A useful application slice connects a user's decision to a result you can inspect. Here, the user proposes a ticket label, reviews the exact record version, approves the change and applies it. If the ticket changes during review, the server refuses the old proposal and the interface explains why.

You will build that interface in Cursor around a provided Python backend. The download includes an incomplete UI, a working authored reference and seven executed HTTP scenarios. The reference was tested in a browser. It was not generated in a recorded Cursor editor session. The Cursor workflow below follows official documentation checked September 17, 2026.

## Start the supplied backend

You need Python 3.10 or newer, Git and the Cursor desktop editor with an account that permits your chosen model. The app uses the Python standard library, plain HTML, CSS and JavaScript. No package installation, CRM credentials or model API calls are needed to run it. Using Cursor itself remains subject to your account's allowance.

Download and extract the [ticket-review app exercise](/downloads/agent-course/cursor-ticket-review-v1.zip) into a disposable folder. Run:

```bash
python3 verify_http.py
python3 server.py
```

Use `python` if that is your installed command. Open the printed URL, normally `http://127.0.0.1:8090`. If that port is occupied, use `python3 server.py --port 8091`. The starter page displays the current synthetic ticket but has no proposal or approval controls yet.

The server binds only to loopback and creates a temporary SQLite database. It uses one fixed fixture reviewer, not a login system. Ctrl-C stops the server and removes its demo data. This is an isolated coding exercise, not a multi-user service ready to deploy.

## Understand the contract before building the screen

The backend reuses the [approval lesson's](/blog/how-to-build-ai-agent-human-in-loop-approval) transaction logic. The UI should send a label or decision. It should not invent record versions or rewrite approval records.

| 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 |

All POST bodies use JSON and accept only their stated fields. The server reads the current version when creating a proposal. Applying it later requires that same version. The UI cannot authorize a stale action by changing a hidden field.

The simulation endpoint exists only to exercise the failure path. It increments the version without changing the label, letting you see whether an old approval is refused. It would not belong in a production application.

## Set up a local Cursor task

Open only the extracted exercise folder in Cursor and save a Git baseline:

```bash
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 local Agent workflow, not a cloud deployment. Its [Agent overview](https://cursor.com/docs/agent/overview) describes file editing, terminal use and browser testing. Before beginning, inspect **Settings → Agents → Approvals & Execution**. The [Run Modes guide](https://prod.cursor.com/docs/agent/security/run-modes) explains local Auto-review, Allowlist and Run Everything modes. Leave a reviewed mode in place for this exercise rather than enabling unrestricted execution.

Select a model available under your account and record its actual name with your result. This lesson does not assume a particular model is included or measure one model against another.

Give Agent `index.html`, `app.js`, `style.css`, `server.py` and `verify_http.py` as task context. Ask for this specific change:

```text
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 narrow the task. They do not technically prevent Agent from reading other workspace files. Keep unrelated or sensitive material outside the exercise folder.

## Review edits while they happen

Cursor's [Agent help](https://prod.cursor.com/help/ai-features/agent) explains that edits are applied as the agent works. Inspect the diff instead of assuming a later acceptance click prevents the files from changing.

The important interface code should preserve three boundaries:

- Selecting a label creates a proposal. It does not apply the label.
- Approving a proposal records the decision. Applying remains a separate request.
- A stale or expired response leaves the ticket unchanged and offers a new proposal from current data.

Review the actual request payloads and error handling in `app.js`. Disabling a button helps guide the interaction, but `server.py` must reject invalid direct requests independently. If the assistant alters the backend to make an interface test pass, inspect that change against the original contract before keeping it.

Cursor checkpoints can help undo agent file edits. The [checkpoint documentation in the Agent overview](https://cursor.com/docs/agent/overview) distinguishes them from Git. Neither a file checkpoint nor a Git revert rolls back an already running database transaction. This exercise resets its temporary data when you stop and restart the Python server.

## Compare with the working reference

Stop the starter server with Ctrl-C, then run:

```bash
python3 server.py --reference
```

This serves `reference.html` and `reference-app.js` against the same backend. The reference is a comparison artifact, not evidence of what your Cursor session will produce.

Starting from the fresh ticket, follow these actions:

1. Choose **Billing**, then **Create proposal**. The state is pending, the reviewed version is `1`, and Apply is disabled.
2. Choose **Approve**, then **Apply approved change**. The ticket becomes billing at version `2`, and the state becomes completed.
3. Choose **Read completion receipt again**. The ticket stays at version `2`. The message says the stored receipt was returned.
4. Create a **Technical** proposal and approve it. Choose **Simulate a concurrent edit** before applying. The current version becomes `3`, while the proposal still refers to `2`.
5. Apply that proposal. The state becomes stale, the label remains billing, and the interface tells you to create a new proposal.

Those success, replay and stale paths were exercised through the actual reference interface. The page also shows the original and proposed labels so the reviewed change remains visible after the current ticket changes.

Proposals expire after two minutes. The HTTP suite tests expiry by setting a controlled expired-record fixture. It does not wait two minutes. If you test expiry manually, leave a new approved proposal unapplied until its displayed expiry, then try Apply.

## Test the server independently of the screen

`verify_http.py` starts real loopback servers against temporary databases. Its seven scenarios are:

| 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 cover the local HTTP contract and SQLite action boundary. They do not establish production authentication, CRM behavior, multi-user isolation, load capacity or remote recovery. Origin and Host checks are present, but a fixed fixture identity is still not an authorization system.

For the interface, inspect labels and focus, use Tab and Enter through the controls, and narrow the browser to 390px. The reference moves focus to the next available decision control or the result message and announces status changes. Check the completed, rejected and stale states as well as the first screen.

Save the final diff, HTTP result and your browser observations. The finished artifact is a small application whose action boundary can be explained and tested. Continue with the [n8n workflow lesson](/blog/how-to-use-n8n-for-advanced-ai-workflow-automation) after you can reproduce those outcomes without relying on the assistant's summary. The [course index](/blog/pillar/agents-and-ai-engineering#agent-course-heading) links the sequence.

## Related Guides

- [Build a Ticket Workflow: Signed Delivery, Evidence and Human Approval](/blog/ticket-agent-workflow-capstone)
- [Plan and Execute: Build a Bounded Ticket Workflow](/blog/how-to-build-ai-agent-that-plans-and-executes-tasks)
- [Build an MCP Server and Client for a Ticket-Label Tool](/blog/how-to-build-an-ai-agent-using-mcp-model-context-protocol)

## Continue the course

Lesson 15 of 17.

Previous lesson: [Use GitHub Copilot to Fix a Stale-Delete Bug You Can Verify](https://www.zarifautomates.com/blog/how-to-use-github-copilot-to-write-code-faster.md).

Next lesson: [Build an n8n Ticket-Triage Workflow and Test Its Failure Paths](https://www.zarifautomates.com/blog/how-to-use-n8n-for-advanced-ai-workflow-automation.md).

[Browse available lessons](https://www.zarifautomates.com/blog/pillar/agents-and-ai-engineering#agent-course-heading).
