Debugging & tooling
When a reactive click "does nothing," the failure is almost always at one of the RPC seams. phlex-reactive ships four introspection surfaces to name it fast — and one guard that turns a forgotten authorize! into a loud error instead of a silent hole.
The four surfaces#
Work top-down — validate the install, inventory the server, find the specific component, scan the live page — and stop as soon as a step names the problem.
| Surface | Command | Answers |
|---|---|---|
| Doctor | bin/rails phlex_reactive:doctor | is the install wired correctly? |
| Inventory | bin/rails phlex_reactive:actions / find | what actions exist, where, authorized? |
| Browser inspector | import("phlex/reactive/inspect") | what's reactive on THIS page? |
| MCP server | bin/rails phlex_reactive:mcp | all of the above, queryable in your editor |
Everything is read-only and reports names, paths, and declared schemas only — never a token, a secret, or runtime state.
Doctor — validate the install#
The first thing to run. phlex_reactive:doctor prints a ✓/✗/? line for every seam and a fix for each failure:
bin/rails phlex_reactive:doctorIt checks that POST /reactive/actions routes to the gem (a host catch-all route shadows it otherwise), the generic reactive Stimulus controller is registered, the identity verifier round-trips, every declared action :name has a public method, every component resolves a stable #id, and — advisory only — whether each mutating action appears to authorize. Most "nothing happens" reports are a red ✗ here.
Inventory — what actions exist (server side)#
Answer "what reactive actions exist in this app, where are they defined, and is each authorized?" without grepping:
bin/rails phlex_reactive:actions # component | action | params | file:line | auth
bin/rails phlex_reactive:actions FORMAT=json # machine-readable
bin/rails "phlex_reactive:find[counter]" # fuzzy-find one; prints each action's method sourceactions is a table across the whole app; find fuzzy-matches one component (exact > prefix > substring > subsequence) and prints each action's method-definition source (extracted with Prism). The auth column is a heuristic: authorized* means an authorization call was detected in the body; unverified means none was — but a helper may still authorize indirectly, so it's a hint, not a verdict.
Inspector — what is reactive on this page (browser side)#
A standalone, on-demand module (no cost until you import it — it does not touch the hot-path controller). In the browser console on the affected page:
(await import("phlex/reactive/inspect")).report()report() prints a console.table of every reactive root on the page — its id, decoded token payload (component class, gid, state keys, token version), status (error/busy/dirty), the bound triggers (action + event + params + debounce/confirm), client-only ops, computes, and the named fields a dispatch would send. scan() returns the same data as a value.
A token that can't be decoded (an older Marshal-serialized payload) shows as (opaque token) rather than throwing — the scanner is pure read and never breaks the page.
The server↔client mapping is by name#
This is the payoff of the two inventories. The component and each trigger's action that report() shows in the browser are exactly the same identifiers phlex_reactive:actions lists on the server. So a bug localizes to a mismatch:
- Does the button on the page carry the action you expect?
- Does its component match the class you think renders here?
- Is a root showing an
(opaque token)where it should decode?
You never have to guess which server method a DOM control POSTs to — the name is the join key across both tools.
MCP server — introspect the running app from your editor#
For live-app introspection inside Claude Code, run the read-only MCP server (needs the optional mcp gem — gem "mcp"). Add it to .mcp.json:
{ "mcpServers": { "phlex-reactive": { "command": "bin/rails", "args": ["phlex_reactive:mcp"] } } }It exposes five read-only tools — phlex_reactive_doctor, phlex_reactive_components, phlex_reactive_actions (optional component: filter), phlex_reactive_find, and phlex_reactive_config (a redacted summary that never emits the verifier or secret_key_base) — the same data as the rake tasks, queryable in conversation.
Entry point is a rake task (not an exe) because introspection needs the booted Rails app. Constraint: stdio MCP needs a clean stdout, so an initializer that puts breaks the transport.
The installable Claude skill#
One command installs the debugging runbook as a Claude Code skill and wires the MCP server:
rails g phlex:reactive:claudeIt copies the phlex-reactive-debugging skill into .claude/skills/ (teaching the doctor → inventory → find → browser report() → MCP workflow) and creates .mcp.json with the server entry — but only when it's absent; it never rewrites an existing .mcp.json, it prints the snippet instead.
For reference documentation (how a feature works), point tools at the hosted docs MCP — https://phlex-reactive.zoolutions.llc/llms.txt. The local MCP server is for live-app introspection.
Failure table#
The endpoint's failure modes and what each means:
| Symptom | Cause | Fix |
|---|---|---|
| 400 | Tampered / stale token — from before a deploy, a secret_key_base mismatch, or a reply.with(...) stream that skipped the token refresh | Reload for a fresh token; confirm secret_key_base; refresh the token in custom streams |
| 403 | The action isn't declared, OR your authorizer raised (registered in authorization_errors) | Declare the action — or the 403 is correct |
AuthorizationNotVerified (500) | verify_authorized is on and the action authorized nothing | authorize!, mark_authorized!, or skip_verify_authorized |
| 404 | The record was deleted while a page still showed it | Expected for a stale page — remove the row or reload |
| Nothing on click | The reactive controller isn't registered, or a catch-all route shadows the endpoint | phlex_reactive:doctor names both |
| Renders but never updates | id and data-controller="reactive" landed on different elements | Spread **reactive_root (binds both to one element) |