Forty-seven open tickets, one April morning, and I made about ten real decisions in four hours. The other 37 I touched. I read them, scrolled them, opened another tab, lost my place, and moved on without deciding anything. JIRA loads in one tab. The admin page it references loads in a second. The linked parent ticket is in a third. The customer’s account is in a fourth. By the time I had enough context, I’d burned the energy that should have gone toward the judgment.

The triage wasn’t slow. The overhead was.

the tab-thrash problem

The specific friction was this: my backlog UI could show me that a ticket existed, but it couldn’t show me enough to decide on it. Every ticket that needed action also needed context that lived somewhere else. A three-sentence report about a broken bracket page requires at minimum: the ticket itself, the tournament in the admin, a linked parent ticket for scope, and sometimes a quick SQL lookup to check a field state. Each of those is one click away. Each click breaks the scanning rhythm.

I’d been running triage sessions the same way for a couple of years. Open hometeamsonline.com/adminHTO/?p=pmhome, work down the list, do my best. It was fine when the backlog was 15 tickets and I was doing all the analysis myself. Then I started using Claude Code for ticket investigation. It could read a ticket, check the relevant code path, inspect what the linked page actually rendered, and tell me in about 30 seconds whether something was a 2-hour fix or a 2-week rewrite. The analysis got cheap. The bottleneck shifted to: can I act on what I now know, without losing my place?

what the cockpit became

I turned pmhome into an action surface. That is the short version.

The longer version: I added inline rendering of ticket notes, the JIRA comment stream, linked child tickets, and a RICE breakdown that Claude could pre-populate from its investigation pass. None of that was the hard part. Displaying things is easy.

The hard part was the verbs. The cockpit needed to let me act in place: accept a ticket into the active queue, leave a comment without opening a new tab, reroute a ticket to a different component, or park it. One click per decision, the page staying intact underneath. No navigation, no full-page reload, no confirmation dialog that required scrolling back to find where I was.

For actions that JIRA requires metadata on (priority change, type change, status transition), the cockpit surfaces a small inline form. Pick the transition, fill the required fields, confirm. It writes to JIRA via the REST API and updates the row in place. The actual triage motion became: read a ticket, decide in ten seconds, click one button, move one row down.

the weird constraint that shaped the design

IIS has opinions about HTTP status codes. If an application endpoint returns a 4xx or 5xx, IIS will, depending on how httpErrors is configured, replace the response body with its own HTML error page. That feature exists for end users hitting a broken URL. It is not useful when you’re building JSON APIs on the same server that hosts the HTO admin.

The first version of the cockpit’s action endpoints returned 400 Bad Request with a JSON body describing what went wrong. The browser received IIS’s HTML. My JavaScript tried to parse it as JSON and threw a silent parse error. The button clicked. Nothing happened. No feedback.

The fix was: all action endpoints return HTTP 200. The JSON payload carries a status field set to either ok or error, with the error case including a message. The browser-side code reads status before treating the response as a success.

That is not the architecture I would design from scratch on a clean system. It is the architecture that works on the system I have. The IIS constraint forced a discipline I’d probably have been sloppy about otherwise: every action endpoint has an explicit success or failure contract in the response body, regardless of what the HTTP layer says.

parked is not the same as waiting

I spent the most time on a distinction that sounds minor. The difference between a ticket I’ve deliberately deprioritized and a ticket I’m blocked on.

I had one status for both: Parked. That was wrong. If I’m parked because I chose to deprioritize, the ticket is mine to un-park whenever I want. If I’m parked because I’m waiting for a customer to confirm something before I can move forward, that ticket is not mine to move. The action I need to take next is different. The urgency logic is different. Lumping them together meant “what do I touch today” had no clean answer.

I split the status into two. Parked means deliberate deprioritization: I own the next move, I just chose not to take it yet. Waiting means an external dependency: they own the next move. The cockpit surfaces them differently. Waiting tickets get a timestamp showing how long they’ve been idle. Parked tickets get a note field for the reason I set them aside.

It was a 30-minute change to the data model and the display logic. It cleaned up the triage queue more than anything else I built, because the filter for “what do I actually work today” finally had a correct answer: everything active, plus Waiting tickets older than 48 hours.

the bottleneck moved

Before Claude was doing ticket investigation, reading a ticket and deciding what to do with it were roughly the same cost. You read, you thought, you decided. The reading and the deciding were one motion.

Once Claude was handling the investigation pass, reading became nearly free. It reads the ticket, checks the code, inspects the linked pages, and returns a verdict. The deciding is still mine. But deciding requires a tool that lets me act on the verdict immediately, without opening a new sequence of navigation.

That’s the gap I didn’t notice until the analysis got fast. Investigation and decision are separate operations. Speeding up investigation while leaving decision on the same five-tab overhead doesn’t reduce work. It means you spend more time rebuilding context per decision, not less.

Once AI makes ticket analysis cheap, the real bottleneck is whether your backlog UI lets you decide anything without leaving the page.