Debugging
Find out what a VitNode app is actually doing - the AdminCP Debug Panel, the system log, the Router and Query devtools, and React Scan.
Four surfaces, each answering a different question. Two of them exist only in development - the devtools and React Scan. The other two, the AdminCP Debug Panel and the server's own output, are how you see inside a production deployment, which is exactly when you need to.
Where to look first
| Symptom | Look here |
|---|---|
| A request 500s in production | Debug Panel → System Logs |
| A screen shows data that has moved on | Query devtools → find the key |
| A route loaded the wrong thing | Router devtools → loader data |
| A page feels slow to type in or scroll | React Scan → count the re-renders |
| A service looks configured but does nothing | Core → System → Integrations in the AdminCP |
| A background job never ran | Debug Panel → Queue Tasks |
The Debug Panel
/admin/core/debug, and the quickest way in is the avatar menu in the top-right
of the AdminCP: Debug Panel. It is also the one AdminCP screen with no
sidebar entry, so that menu (or the admin search) is how you get there.
Three sections, top to bottom:
| Section | What it shows |
|---|---|
| Clear Cache | A button, top-right. Marks every cached query stale and re-runs every matched route loader. |
| Queue Tasks | Four counters - pending, processing, completed, failed - and a table of what is pending or processing right now, with its attempt count and when it is next available. |
| System Logs | Every line anything wrote with c.get('log'), newest first, sortable by type, plugin or date. |
Click any log row for the full record: the log id, the request method and path, the status code, the IP address, the user agent, the user who caused it (linked straight to their AdminCP profile) and the untruncated message. That detail dialog is usually the whole debugging session.
The screen is gated on the debug staff module - can_view to open it,
can_clear_cache for the button - and Hono re-checks that on every request, so
hiding the menu entry is a courtesy rather than the boundary. See
staff permissions.
In production, the log is the only copy of the error
An unhandled error in a route is caught by the API's error handler, written to
the system log as Unhandled error: …, and answered to the browser as the
string Internal Server Error with no detail at all. In development the same
handler returns the real message in the response body instead. So a 500 you
cannot reproduce locally is a 500 you read in the Debug Panel.
Devtools in development
The scaffold mounts a floating devtools button in the bottom-right corner of every page, with two panels behind it. It comes from two places, and both are in a generated app already:
import { devtools } from '@tanstack/devtools-vite'
plugins: [
devtools(),
]<body suppressHydrationWarning>
{children}
{import.meta.env.DEV ? (
<TanStackDevtools
config={{ position: 'bottom-right' }}
plugins={[
{ name: 'TanStack Router', render: <TanStackRouterDevtoolsPanel /> },
{ name: 'TanStack Query', render: <ReactQueryDevtoolsPanel /> },
]}
/>
) : null}
<Scripts />
</body>That import.meta.env.DEV is why nothing ships to production: the whole block
is compiled out of a vite build.
| Panel | Reach for it when |
|---|---|
| TanStack Router | You want the matches for the current URL, each route's loader data, and what its head resolved to. The answer to "why is this page rendering that". |
| TanStack Query | A screen is showing stale data. Find the key, then find the mutation that should have invalidated it. |
Navigate around with the Query panel open and the shape of data loading becomes obvious: a route's loader warms its entries before React renders, and the screen reads the same ones back.
Server-side requests are not in the panel
A loader that runs during SSR runs in the Node process, so its output lands in
the terminal running vite dev - not in a browser panel. The Query panel
shows the entry once it is dehydrated into the page.
React Scan
React Scan draws an outline around every component as
it re-renders, which turns "this page feels heavy" into a number. It is off by
default; flip debug in the shared config:
export const vitNodeConfig = buildConfig({
debug: true,
i18n: {
defaultLocale: 'en',
locales: [{ code: 'en', name: 'English' }],
},
metadata: {
shortTitle: 'VitNode',
title: 'VitNode',
},
plugins: [],
theme: {
defaultTheme: 'system',
},
})The provider loads react-scan lazily and only when debug is on and
NODE_ENV is development, so leaving debug: true committed cannot leak the
overlay into production. It is still worth turning back off - the outlines are
distracting once you have your answer.
Once you have found the culprit, Performance covers what to do about it.
Where output goes
| Destination | What arrives there |
|---|---|
The core_logs table | Every c.get('log') call, with the request that made it. Read it in the Debug Panel. |
| The server's stdout | The same lines, colour-coded and prefixed [VitNode] Error (@your/plugin):. |
| The browser console | Client-side errors, and React Scan's own output. |
| The terminal | Vite's dev-server output, plus anything a loader or server function logged during SSR. |
There is no request logger, and no log-level switch: every level is written every time, to both the table and the console. If you want less noise, log less. Logging is the whole surface.
Gotchas
Every log line is a database INSERT, and nothing prunes them
c.get('log') writes a row per call and no cron job deletes any of them, so a
debug line in a hot request path grows the table for as long as the
installation runs. Log events, not traffic.
Behind a proxy, every log line says the same IP
The IP on a log row - and the rate-limit bucket - is the address of the socket
VitNode is talking to, because X-Forwarded-For is a header the caller writes
and nobody should get to pick their own audit trail. Behind a reverse proxy
that socket is the proxy, so every visitor looks like the same one. Put the
per-visitor rate limit in the proxy
itself, where the address is real.
Clear Cache clears the front end, not Redis
The button invalidates the TanStack Query cache and the router's loaders, which is the front end's memory of the API. The API's own Redis-backed cache is expired by the mutations that change the data, not from this screen.