Debugging
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 → Log explorer |
| 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 | Debug Panel → Services |
| A background job never ran | Debug Panel → Cron & queue, Queue |
The Debug Panel
The Debug Panel lives at /admin/core/debug. Open it from Debug Panel in
your avatar menu, or type "Debug Panel" into the ⌘K admin search. When something
needs attention, that menu item shows an amber warning icon - or a red error
icon if anything is critical - so you know to look before anyone files a bug.
It works like a settings page: a side menu on the left (a Section drawer on
phones) and one section on the right. Every section has its own URL, such as
/admin/core/debug/cron, so you can bookmark it or paste it to a teammate.
| Section | What it shows |
|---|---|
| Overview | A red alert when your VitNode version has a security update, everything that needs attention (click a row to jump to its section) with Check again, and a Systems list with Clear cache. |
VitNode (/debug/vitnode) | The running version, the update channel, the last update check and Check for updates. A red panel with the fixed version and release notes when a security update applies. |
Plugins (/debug/plugins) | Every installed plugin with its version, plus a warning when it needs a security update, runs different API and web versions, has an update, or its update source is unreachable. |
Cron & queue (/debug/cron) | The scheduler status card and the cron jobs table. See Cron jobs. |
Cache (/debug/cache) | Whether Redis is connected, unreachable or not configured (in memory), whether realtime works across instances, and Clear cache. |
Search (/debug/search) | Index status, the engine, recent sync errors and every collection with Reindex. See Search. |
Services (/debug/services) | Email, file storage, AI models, captcha and WebSocket - each active or not configured. Active ones have a test button, inactive ones a Set up link to the docs. |
Log explorer (/debug/logs) | Every line anything wrote with c.get('log'), newest first, sortable by type, plugin or date. |
Queue (/debug/queue) | Background tasks with their status, attempts and last error, filterable by status. See Queue tasks. |
AI logs (/debug/ai) | Every AI request: action, status, origin, model, cost and points, with filters. |
Clear cache marks every cached query in the AdminCP stale and re-runs every matched route loader.
Click any row in Log explorer 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.
Opening the panel needs debug → can_view, and Clear cache needs
debug → can_clear_cache. Each section also asks for its own permission and is
hidden without it:
| Section | Permission |
|---|---|
| VitNode, Plugins | plugins → can_view |
| Cron & queue | cron → can_view |
| Cache, Search, Services | system → can_view |
| Queue | queue → can_view |
| AI logs | ai → can_view |
Hono re-checks every permission on each request, so hiding a section is a courtesy rather than the boundary. See staff permissions.
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}
<DocumentScripts />
</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 fetching becomes obvious: a route's loader warms its entries before React renders, and the screen reads the same ones back.
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
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.
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.
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.