The dev console is a page that the application serves to your browser while you develop: the startup report of the boot that is running, with its anomalies first, then one tab per extension that has something to show — its boot facts and, for the extensions that give them, live values and charts, such as the connections of each Mansart pool — then the configuration, the beans of the CDI container and the JVM last. It listens on a port of its own, on the loopback address, it only reads, and it is on in a dev launch only.

Getting it NEW

Nothing to declare. mvn vidocq:dev adds the console itself, at the Vidocq version of the application’s Chappe extension — with a warning when the plugin is another version (Vidocq/vidocq#148) — the moment the application already has vidocq-runtime-chappe-webserver-extension: the console serves its page through Chappe, on a listener of its own, so without Chappe there is nothing to serve it and nothing is added. It also adds, for every runtime extension that has one, that extension’s -dev panel module (A -dev module, never packaged), so the extension’s tab is live. One INFO line lists what was added:

[INFO] Dev tools: vidocq-runtime-cassini-rest-extension-dev, vidocq-runtime-mansart-pool-extension-dev

An application that has never served HTTP — no Chappe, so no console either — logs why instead, and starts without it:

[INFO] Dev tools: no dev console, it needs vidocq-runtime-chappe-webserver-extension

A test that reads the console — an assertion against its /api/snapshot, say — declares it itself, together with the -dev modules it needs, at test scope: mvn test, vidocq:test and the continuous tests of vidocq:dev add none of them.

<dependency>
    <groupId>io.vidocq.runtime.extensions.essentials</groupId>
    <artifactId>vidocq-runtime-devconsole-extension</artifactId>
    <version>0.4.0-SNAPSHOT</version>
    <scope>test</scope>
</dependency>

vidocq:run, vidocq:package, vidocq:jlink, vidocq:jpackage and vidocq:docker never contain the console or any -dev module: every one of their jars carries Vidocq-Dev-Only: true in its manifest, and these goals drop such a jar from what they launch or ship, even when the project declares it — a declared console, for instance, kept from before this changed. Each dropped jar logs one WARNING per goal run, never fails the build, and names what to remove:

[WARNING] vidocq-runtime-devconsole-extension is dev-only: not packaged; remove the dependency, vidocq:dev brings it

vidocq:checkpom gives the same advice earlier, at validate: a warning, never a failure, for a dev-only dependency declared in any scope but test. See Dev tools and binaries for the full picture, companions included.

The console’s SPI, vidocq-runtime-devconsole-spi, is not marked: it is an ordinary API jar of five small types, and it ships in a binary whenever an extension that keeps the all-in-one DevConsolePanel form (A -dev module, never packaged) requires it, so that such an extension still resolves outside vidocq:dev.

Then run the application in dev mode:

mvn vidocq:dev

The banner promises the console, and one INFO record gives its address once it listens:

 Java 25.0.3+9-LTS | dev (vidocq.launch.mode) | devconsole :8888
...
[INFO ][2026-09-18 21:28:11.834][main           ][i.v.r.e.e.d.DevConsoleExtension#bound        ] : Vidocq dev console: http://127.0.0.1:8888/

Open that URL. The browser is never opened for you.

When it is on NEW

vidocq.devconsole.enabled is auto by default: the console starts when the launch mode of the boot is dev — mvn vidocq:dev, a run from an IDE, -Dvidocq.launch.mode=dev — and stays off in test and prod. true and false force it on or off, whatever the mode.

auto also turns it on for a dev read from the shape of a build tree, such as an application run from target/classes by a CI job. That is accepted: the console listens on the loopback address only, and a port that is already taken never stops the application (When the port is taken NEW).

When the console is off, its section of the startup report says why, and nothing listens:

  devconsole  off (auto, launch mode prod)

off (vidocq.devconsole.enabled=false) when it was forced off. Every dev reload starts a new console, with a new listener; a reload whose boot fails takes the console down with it, and the page says so (The page NEW).

Configuration NEW

Key Default Description

vidocq.devconsole.enabled NEW

auto

auto: on when the launch mode is dev, off otherwise. true or false force it on or off; on outside a dev launch, the console raises VIDOCQ-DEVC-001 — on outside development NEW. Case and surrounding blanks are ignored. Another value is reported as VIDOCQ-DEVC-003 — invalid value NEW and means auto.

vidocq.devconsole.port NEW

8888

The port the console listens on, from 0 to 65535. 0 takes a free port, and the dev reloads ask for the same one again, so that an open tab keeps working. Another value is reported as VIDOCQ-DEVC-003 — invalid value NEW and means 8888. A port that is taken falls back to a free one (When the port is taken NEW).

vidocq.devconsole.host NEW

127.0.0.1

The address the console listens on: a host name or an IP address, an IPv6 literal with or without its brackets. Any address that is not a loopback one raises VIDOCQ-DEVC-002 — not a loopback address NEW. A value with anything but ASCII letters, digits and .-_:%[], or longer than 255 characters, is reported as VIDOCQ-DEVC-003 — invalid value NEW and means 127.0.0.1, on the banner too; write a name with a letter outside ASCII in its ASCII form, hôte.example as xn—​hte-kna.example.

Like every key, they can be set with -D, an environment variable (VIDOCQ_DEVCONSOLE_PORT), vidocq.properties or a profile (%dev.vidocq.devconsole.port). On the Maven command line, mvn vidocq:dev -Dvidocq.devconsole.port=9000 reaches the application: vidocq:dev forwards the -Dvidocq.* keys to the JVM it forks, as vidocq:run does. The console declares these three keys, so the key audit reports a mistyped one as VIDOCQ-CFG-003: nothing reads a vidocq.devconsole.prot=18090, and the console listens on 8888, but the boot says so.

[VIDOCQ-CFG-003] Configuration key 'vidocq.devconsole.prot' is read by nothing and has no effect. Known keys in this namespace: vidocq.devconsole.enabled, vidocq.devconsole.host, vidocq.devconsole.port

Without the console on the module path, nothing claims the namespace, and a configured vidocq.devconsole.* key is not audited.

The console declares its listener, dev, itself: vidocq.chappe.listeners must not list it (A listener of an extension’s own).

Where it listens NEW

The URL record NEW

Once its listener is bound, the console logs one INFO record on the logger io.vidocq.devconsole, with the URL last and nothing after its final /, so that a terminal or an IDE console makes it a link:

Vidocq dev console: http://127.0.0.1:8888/

It is the address the console really bound: the free port chosen for 0 or for a taken port, localhost for a wildcard address such as 0.0.0.0, an IPv6 address in brackets. It is logged on the first boot of the JVM, and again on a dev reload only when the URL has changed; with port 0, a reload asks for the port the previous boot bound, so the URL does not change and the tab you have open keeps working. Chappe’s own listener 'dev' started line is logged at DEBUG, to leave this record alone.

The detailed startup report has the console’s section:

devconsole    Dev console | 0 ms
  http://127.0.0.1:8888/
  enabled     auto
  configured  127.0.0.1:8888
  dev         http://127.0.0.1:8888/

The banner, printed before any extension is loaded, already names the console on its context line, after the debugger:

Configuration Segment

127.0.0.1, localhost or ::1, port 8888

devconsole :8888

vidocq.devconsole.host=0.0.0.0, port 18093

devconsole 0.0.0.0:18093

vidocq.devconsole.host=hôte, port 8888

devconsole :8888: the console refuses that host (VIDOCQ-DEVC-003 — invalid value NEW) and listens on 127.0.0.1

vidocq.devconsole.port=0

none: the port is only known once bound

the console absent, or off

none

The segment is the configured address, a promise made before the bind: the URL record is the one to trust, and it always prints on the first boot. When the 80 columns of the banner cannot hold everything, the segment goes before the debugger’s address, which is what attaching a debugger needs (What the banner shows). The single INFO line of a log output keeps it: …​ | prod (vidocq.launch.mode) | devconsole 0.0.0.0:18093.

When the port is taken NEW

Two applications in dev mode on one machine both want port 8888. A dev tool must never stop an application from starting, so the second console listens on a free port instead, and says so loudly, three times. Chappe names both ports, then the console logs its URL and a boxed WARNING that holds numbers and the URL only, then the startup report raises VIDOCQ-DEVC-004 — port taken NEW:

Port 18092 held by another process
[WARN ][2026-09-18 21:28:01.448][main           ][i.v.r.e.e.c.ChappeServerBootstrap#start      ] : Chappe listener 'dev': port 18092 is taken, listening on port 57738 instead
[INFO ][2026-09-18 21:28:01.449][main           ][i.v.r.e.e.d.DevConsoleExtension#bound        ] : Vidocq dev console: http://127.0.0.1:57738/
[WARN ][2026-09-18 21:28:01.450][main           ][i.v.r.e.e.d.DevConsoleExtension#bound        ] : Vidocq dev console: port 18092 is taken, listening on port 57738 instead
+------------------------------------------------------+
|                                                      |
|   DEV CONSOLE: port 18092 is taken                   |
|   It listens on port 57738 instead:                  |
|                                                      |
|   http://127.0.0.1:57738/                            |
|                                                      |
+------------------------------------------------------+
[INFO ][2026-09-18 21:28:01.451][main           ][i.v.r.e.e.c.ChappeServerBootstrap#start      ] : Chappe listener 'default' started on http://localhost:18090/
[WARN ][2026-09-18 21:28:01.457][main           ][i.v.r.c.r.StartupAnomalies#warn              ] : [VIDOCQ-DEVC-004] The dev console's port 18092 is taken: it listens on port 57738 instead, http://127.0.0.1:57738/ Free port 18092, or set vidocq.devconsole.port to another port, 0 for any free one

The page itself carries a banner with both ports. The box never quotes the configuration: it prints the two port numbers and a URL whose shape the console checked, so nothing configured can draw inside it. Only the application’s own listeners still fail the boot on a taken port, as they always did.

Falling back costs about one second of boot: before the console takes a free port, Chappe retries the bind with a backoff, five attempts with pauses of 100 to 400 ms, as it does for any port (1,387 ms against 358 ms in one measured boot). A dev reload pays that second again for as long as the port stays taken: the console keeps the port it bound across reloads only for port 0 (The URL record NEW), and asks for the configured port on every boot, so that it gets it back once it is free. Until then, each reload falls back to a free port chosen anew, usually another one, so the URL record, the box and VIDOCQ-DEVC-004 — port taken NEW come again with the new port, and an open tab has to be pointed at it.

The page NEW

The page is plain HTML, one stylesheet and one JavaScript module, all served by the console: no build step, and nothing loaded from another site, not even a font. It reads one document, GET /api/snapshot, about once a second.

  • Tabs. Startup first, then one tab per section of the startup report, in the order of the report, and the console’s own Configuration, CDI (Vauban) and JVM last. The page keeps the selected tab and the pause in the browser’s localStorage, and nothing else.

  • Polling. One request at a time. None while the tab is hidden, or while Pause is pressed.

  • A reload. When the application stops answering, during a dev reload, the page greys the last data under reloading… and retries every second; after a minute it says unreachable and retries every five seconds. A new boot is recognised by its id: the page draws its boot facts again and starts its charts over.

  • Charts. The page keeps five minutes of history, 300 points per value, in the browser only: nothing is stored on the server. Time is the server’s clock, so a missed poll or a debugger paused on a breakpoint leaves a gap, never a lie.

  • Themes. Light and dark follow the system’s preference.

Startup NEW

The startup report of the boot that is running, whatever the level the log printed: the launch mode and the signal it was read from, the anomalies first, with their code, message, hint and the section that raised them, then the sections of the core — launch, vidocq, phases, layer, configuration, extensions — as tables, and the console’s own section. The full report, as the detailed level renders it, closes the tab, with a button that copies it for a bug report. Until the boot has written its report, the tab says booting….

A reload of the dev loop logs the summary report only; the console still has every row, since Vidocq collects them all for it whenever it is on.

Panels NEW

Every section that a library adds to the startup report is a panel, with the boot facts it wrote: its summary line, its rows and its anomalies. A section that is also a dev console panel adds live values:

  • a level (a gauge), such as the active connections: its number, and a fill bar when it has a maximum;

  • a total that only grows (a counter), such as the borrows so far: its total, and +N since last poll;

  • a duration, formatted;

  • an absent value, greyed with its reason, such as leak detection off: never a zero that would read as a measure;

  • tables;

  • charts of the values that change, drawn from the history the console keeps: a level, or the growth per second of a counter;

  • one card per group, such as one per pool, with its values, its charts, and the boot facts that belong to it.

When a panel’s sampling fails, its tab shows the class of the exception, never its message, and keeps its boot facts; the console logs VIDOCQ-DEVC-005 — a panel failed to sample NEW once and tries again on the next poll. A sample that took more than 5 ms is flagged slow, and one that went past the console’s limits is flagged as such.

The panels shipped today:

Panel Shows

Mansart pools

One card per pool of vidocq-runtime-mansart-pool-extension: its size, timeouts and checks, its URL without credentials in a dev launch, the active and idle connections out of its maximum, the borrowers waiting, borrows, timeouts and leaks, and the mean borrow time. In a dev launch, a tab per pool NEW lists its tables, describes one, shows its first rows and runs the SQL you type, read-only or in a transaction rolled back unless committed. See the Mansart pool section and Tables and a SQL editor.

Mansart Data NEW

The catalogue of vidocq-runtime-mansart-data-extension, one card per entity: its table and columns, then each of its repositories with its methods, their kind, @Query text, parameters and return type, and the methods inherited from Jakarta Data. Read once per boot; no connection, no query. See the Mansart Data catalogue.

REST (Cassini) NEW

The routes of vidocq-runtime-cassini-rest-extension, in match order, each mount, the resource classes and the providers by contract, then live, one card per mount: requests per second, responses by status class, requests in flight, the share of time spent in handlers, and the mean and longest time to answer. See the rest panel.

Metrics (Dirac) NEW

The MicroProfile Metrics registries of vidocq-runtime-dirac-metrics-extension, one card per registry, application first: how many counters, timers, histograms and gauges each holds, then live every counter’s count and every timer’s count and mean, under a key derived from the metric’s name and tags that the boot facts map back to it. Gauges are counted and named, never called. See the metrics panel.

Health (Knock) NEW

The health checks of vidocq-runtime-knock-health-extension, one card per probe: each check’s last answer to a probe request, UP or DOWN and when, never called before the first, the checks UP and DOWN over time, and a button to GET /health. The console never calls a check. See the health panel.

Schema migration NEW

The datasources vidocq-runtime-migration-extension migrates, one card each: the version the schema is at, what the last run applied, and in a dev launch the migrations applied and pending; with two buttons, Migrate now, which applies the migrations added since the boot, and Clean and migrate, which drops the schema and migrates it again, refused unless vidocq.migration[.<name>].cleanDisabled=false. See the migration panel.

Dev services NEW

What vidocq:dev, vidocq:run or the test launcher started: image and addresses of each service, the keys it injected, secrets as configured. Boot facts only. See Dev services.

MCP server NEW

The langchain4j-cdi MCP server of vidocq-runtime-langchain4j-cdi-mcp-extension: what it serves and how it is configured, then live — the sessions it holds, the SSE streams and the subscriptions/listen streams open, the requests it is waiting an answer of from a client, and whether its methods are invoked through the CDI 4.1 invoker or by reflection. See the mcp panel.

Configuration NEW

The console’s own: the keys the application sets, the source each value comes from, the value in a dev launch only and never a secret, and the keys nothing reads. See Configuration NEW.

CDI (Vauban) NEW

The console’s own: the beans, interceptors and observers of the Vauban container, the application’s first. See CDI (Vauban) NEW.

Logs NEW

The console’s own, in a dev launch only: the last records the application logged, newest first, the warnings and errors per second, the loggers whose level is set, and a button that sets one until the next reload. See Logs NEW.

Tests NEW

The console’s own panel under vidocq:dev with continuous testing: the last test run, its failures, and buttons to run every test or the failed ones. See Tests NEW.

JVM

The console’s own, below.

An extension adds its panel with the panel SPI.

Configuration NEW

The console’s own panel, config, just before the CDI container: which keys the application sets, which source each value comes from, and which keys nothing reads. Everything is read once, when the console starts, from the configuration sources of the boot: the configuration does not change during a boot, and a dev reload boots again.

  • Which keys: those of the vidocq. and mp. namespaces, whatever source sets them, and those the application’s own sources define: its vidocq.properties or application.properties, an external vidocq.properties, a microprofile-config.properties under Ravel, or a source of its own. The system properties and the environment are not listed key by key — the hundreds of java.* properties and variables such as PATH would drown the rest — but a -D property or an environment variable that overrides a key of the application’s file is listed, with the source that wins.

  • Boot facts: a summary such as 12 keys: 5 of the application, 6 vidocq., 1 mp.; 4 sources, then:

    • sources: every source with its ordinal, in lookup order, the one that wins first;

    • values: what the table shows of the values, below;

    • status: what the last column means, below;

    • keys left out: how many keys the table could not show, only when it could not;

    • source: the configuration sources, read once at boot.

  • Table keys, the application’s keys first, then vidocq., then mp., each by name, 100 rows at most:

    • key;

    • source and ordinal: the source that wins, the first in lookup order that has a value for the key — the very value VidocqConfig.getValue returns. An environment variable DB_USER wins for db.user when it is set;

    • value: in a dev launch only, the raw value of that source. Under Ravel it is read before profiles and ${…​} expressions, so an expression is shown as written, never expanded;

    • status: unused when a VIDOCQ-CFG-003 anomaly of this boot names the key — a key nothing reads, a typo most of the time; claimed when the key falls under a namespace the core or a loaded extension reads, such as vidocq.mcp.*; not audited for any other key, the application’s own among them. The status is read from the startup report, which is written after the console starts: until then it reads report not written yet.

What is never shown:

  • A secret. A key whose last segment names one — password, passwd, pwd, secret, token, key, credentials, apikey, api-key or private-key, in any case, alone or as the end of the segment, so db.adminPassword, DB_PASSWORD and vidocq.mcp.requestStateSecret too — reads configured. The rule is on the key: a value that merely contains the word password is shown when its key names no secret.

  • The credentials of a URL. jdbc:postgresql://admin:s3cr3t@db/orders reads jdbc:postgresql://@db/orders, and a query parameter that names a secret by the same rule, password= or pwd= among them, reads password=.

  • Any value outside a dev launch. A console forced on in a test or prod launch shows the keys and their sources, and not shown outside dev for every value.

The values are made safe in the application’s JVM, before the snapshot is written: a secret never reaches the page, and the panel logs nothing.

CDI (Vauban) NEW

The console’s own panel, cdi, just before the logs of a dev launch and the JVM: what Vauban, the runtime’s CDI container, holds for this boot, as Quarkus’s ArC card shows it. Everything is read once, when the console starts, from the container’s metadata: the beans its bean manager lists for Object and @Any, its interceptors and its observer methods. No bean is created and no context is read, so a lazy bean stays lazy.

  • Boot facts: a summary such as 33 beans (29 application-scoped, 4 dependent), 0 interceptors, 0 decorators, 1 observer, then:

    • beans, interceptors and observers: how many, and how many of them are the application’s, and how many come from a library — every class that is not the application’s;

    • scopes: the beans by scope, most first;

    • decorators: always none — Vauban implements CDI Lite, which has no decorators;

    • application: how the application’s classes were told apart. When Vidocq booted the application in a module layer of its own, as vidocq:dev, vidocq:run, the packaged launcher and a @VidocqMain trampoline do, they are the classes of that layer, and every other class — Vidocq’s, an extension’s such as the MCP server’s, a third-party jar’s — is a library’s. Otherwise they are the classes outside the io.vidocq, jakarta and java packages, plus those of the main module;

    • beans codegen, interceptors codegen, observers codegen: how many rows each code-generation verdict has, such as 41 APT, 12 Class-File, 3 partial, 20 reflection;

    • beans left out, interceptors left out, observers left out: how many rows a table could not show, only when it could not;

    • source: Vauban’s metadata, read once at boot.

  • Tables, the application’s rows first, then by class name, 100 rows each at most:

    • beans: the class, the kind (managed, producer method, producer field, synthetic, built-in), the scope, the qualifiers without @Any, which every bean has, whether it is an alternative, and where it comes from, application or library. A disabled alternative is not listed: the container does not resolve it;

    • interceptors: the class, its interceptor bindings, its priority or disabled: no @Priority, and where it comes from, application or library;

    • observers: the event type, its qualifiers, the declaring class and method, sync or async, and where it comes from, application or library.

Every table ends with two columns that say whether a row runs code generated at build time or falls back to reflection, as Vauban reads it from what each generated _VaubanComponents provider declares:

  • codegen: APT when the annotation processor generated everything the row needs, Class-File when vauban:generate or vidocq:generate did, APT + Class-File for both, partial when some of it falls back, reflection when all of it does, unknown when a provider built before Vauban declared its coverage serves it — rebuild that jar — and n/a for synthetic and built-in beans;

  • by reflection: what falls back, such as field logger, @PostConstruct init() or client proxy. An interceptor’s @AroundInvoke always does today (vauban#109), and so does a producer field.

A third-party jar’s beans read reflection until vidocq:generate scans the jar; a CDI bean archive is scanned by default, and mvn vidocq:analyze-deps tells which jars are and why (the vidocq:generate goal).

A table with no row reads none. The tables are written as the panel’s values rather than its boot facts because a value table has column heads; they are computed once, and every poll hands the page the same rows. The panel keeps class names as strings only, never a class or a bean of the application.

Logs NEW

The console’s own panel, logs, between the CDI container and the JVM, in a dev launch only: what the application logged in this boot, and a level changed without a restart. When the console starts, it adds a handler of its own to the root logger of java.util.logging, and removes it when the boot stops, so that a dev reload never stacks two. The handler keeps the last 500 records in memory, in a bounded ring written without a lock: logging never waits for the console.

  • Boot facts: a summary such as the last 500 records, root logger at INFO, then backend, where the logs go (java.util.logging, or the class of the System.LoggerFinder that took them), kept, and root level.

  • Counters warnings and errors: the WARNING and the SEVERE records seen since the boot, not only those still kept, drawn per second in the chart Warnings and errors.

  • Table records, newest first, 100 rows at most: the time (HH:mm:ss.SSS), the level, the logger with its packages cut to their initials (i.v.c.CassiniExtension), the thread, and the message, formatted with its parameters as java.util.logging formats it. A record logged with an exception adds the exception’s class and the first line of its message, never the stack trace.

  • Table levels: the root logger, then every logger whose level is set, by name, and until the next reload next to those the button set.

  • Button Set level: a logger name, empty for the root logger, and a level of java.util.logging, from SEVERE to FINEST, ALL or OFF. It answers, for example, io.vidocq.cassini at FINE until the next reload. The level lasts until the next dev reload, which puts every level the button set back as it was: a reload starts from the configured levels. The console holds the loggers it changed, since java.util.logging holds its loggers weakly and would otherwise forget the level. No confirmation is asked: it is harmless, and undone on the next reload.

What is never shown or kept:

  • The record itself. Each record is written once into strings when it is logged; the ring never keeps the record, its parameters or its exception, which may be objects of the application and would keep it in memory past a reload.

  • The credentials of a URL in a message, removed as the configuration panel removes them: jdbc:postgresql://admin:s3cr3t@db/orders reads jdbc:postgresql://*@db/orders. A message is cut at 500 characters, and the page cuts what it shows at 200. A log message can still carry anything the application wrote into it, which is why the panel exists in a dev launch only.

  • Records that go elsewhere. When an SLF4J or Log4j bridge replaces the JDK’s System.LoggerFinder, what Vidocq and the libraries log never reaches java.util.logging: records, warnings and errors are absent with the reason, such as logs go to Log4jSystemLoggerFinder, not java.util.logging, rather than an empty table that would read as a quiet application. The levels table and the button still act on java.util.logging.

Tests NEW

The console’s own panel, tests, between the logs and the JVM, in a dev launch run by vidocq:dev with continuous testing only: the last test run. The Maven plugin writes it to target/vidocq-dev-tests.json and names that file with -Dvidocq.dev.tests.results; a thread of the console, vidocq-devconsole-tests, reads it again whenever it changes, and a poll never touches the disk.

  • Boot facts: the last run when the console started, the results file and the log.

  • Text state: the state and the trigger of the last run, such as failed (change) or running (test-change); results unreadable when the file could not be read, the previous values staying.

  • Gauges run, failures, errors and skipped: those of the last complete run, the previous one while a run is in progress. The chart Tests plots the failures and the errors.

  • Table failed-tests, 100 rows at most: the test as Class#method, the exception’s class, and the first line of its message.

  • Buttons Run all tests and Rerun failed tests: each answers queued and the run shows in the panel once it ends; Rerun failed tests answers no failed test to rerun when the last run had none. A rerun that matches no test runs every test instead NEW. A button never cancels the run in flight: it waits for it.

What is never shown: a stack trace — it is in the log — or the credentials of a URL in a message. The buttons never send a test name: they write the word run-all or rerun-failed to target/vidocq-dev-tests.request, and the plugin works out which tests failed.

JVM NEW

The console’s own panel, always last, read from the JVM’s management beans — the same figures that Dirac’s base metrics read, with nothing to add to the application.

  • Boot facts: the Java version and vendor, the VM, the garbage collectors, the maximum heap, the processors, the process id, and source JMX. Never a JVM argument or a system property: a -D can carry a password, as the dev services' do.

  • Live values: the heap in use (out of its maximum, or of what is committed when the JVM has no maximum), the heap committed, the non-heap in use, the threads (with daemon threads and the peak), the classes loaded, the uptime, the CPU load of the process and of the system as percentages, and the load average. A figure the JVM does not publish is not available, never a zero.

  • One group per garbage collector: its collections, and its time as the JVM counts it: pause time for G1, Parallel and Serial; ZGC and Shenandoah publish their pauses as collectors of their own, which get their own card.

  • Charts: memory (heap in use, committed, and the maximum as a ceiling), non-heap, threads, CPU, and per collector the share of time it took and its collections per second.

Where the history lives NEW

The console keeps five minutes of every gauge and counter — 300 points, one a second — and it keeps them on the server, in a ring filled by a thread of its own named vidocq-devconsole-history. That thread runs for the life of the boot, whether or not a page is open.

It used to be the page that kept them, one point per poll. A browser stops the timers of a hidden tab, so the history stopped the moment the tab stopped being the visible one — and on macOS, Chrome counts a window that another application covers as hidden. The result was a hole in every curve covering exactly the minutes you had left the console to go and cause something, which is the interval you came back to look at.

  • A poll asks for what it is missing. GET /api/snapshot?since=<the newest point the page holds> carries the points after that one only; with no since — a first load, or a new boot — it carries the whole ring. A tab that comes back after three minutes away sends its stale since and gets the three minutes it missed, in one document.

  • An absent value is a hole, not a zero. A tick where a panel wrote absent keeps its place on the axis with a null value, and the curve breaks there. Bridging it would draw a straight line asserting a measure nobody took.

  • A tick the server missed is still a hole. A long pause, a suspended process, a laptop that slept: those points are simply not there, the gap between neighbours is too wide, and the curve breaks. Moving the history to the server removes one cause of holes; it does not hide the ones that remain true.

  • It is bounded, and the bound is a number. 256 series of 300 points, measured full at 1,966,960 bytes — 1.88 MB — and it does not grow with the length of a run. Series are kept first seen, first served; one that arrives past the cap keeps its live value on its tile and gets no curve, and the snapshot says historyTruncated.

  • It is forgotten with its boot. A new boot id — a dev reload included — starts every curve again, and onStop stops the thread before the panels go.

MCP inspector NEW

In a dev launch of an application that has vidocq-runtime-langchain4j-cdi-mcp-extension, the mcp tab lists the application’s tools, prompts, resources and resource templates, and can call each of them. The tab has four sub-tabs: Monitoring, with the panel’s values, charts and boot facts, then Tools, Prompts and Resources, resources and resource templates sharing the last. A sub-tab of a kind is a list to pick the item from, its description and its form under it, then its result in a block of its own, then the history of that kind’s calls; past ten items a text filter above the list narrows it. The page remembers the open sub-tab in this browser, and, until the page is reloaded, the item picked in each.

A tool’s arguments are a generated form when its input schema allows one — scalars, enums of strings, and objects of those one level down — and the JSON editor otherwise, started from its required properties, which checks and completes them against the schema. A "JSON" switch shows the same values in that editor on a form too, and switching back keeps them. A prompt’s arguments and a resource template’s variables are always a flat form, one string per argument or variable.

The call goes through the application’s own /mcp, from the console’s own JVM, in protocol 2026-07-28, with no session opened: the URL is the one the startup report prints for /mcp, a loopback one preferred when more than one listener carries it. When no listener has bound /mcp, the tab shows absent: /mcp has no bound address and offers nothing to call. The inspector sends no credentials and never goes through a proxy: when /mcp is behind authentication, every call answers HTTP 401. The console shows the first 128 actions of a panel; past that, the inspector’s line ends with (first 128 shown).

Confirmation. Before it sends a tool call, the page asks Call <name>? It runs the application’s code, and may change data. unless the tool is readOnlyHint: true and not destructiveHint: true. A prompt get or a resource read never asks.

A result shows in a block of its own under the form: a header with the outcome, ok in green, error in red, running… while the call runs, then the summary line, the round trip the page measured and the viewer’s buttons; then the body, JSON going through the JSON viewer; and, folded under "Exchange", the JSON-RPC request and response exactly as they were sent and received (or the SSE events, for a streamed answer). Each item keeps its last result while the page is open: picking it again in the list shows it. A refused call, or a console that does not answer, shows its reason in that block too, as an error. What each outcome shows:

Outcome summary error body

tool result

ok in <ms> ms

from isError

the content: the text items joined, or the JSON of structuredContent when present

prompt

<n> message(s) in <ms> ms

false

the messages as JSON

resource

<n> content item(s) in <ms> ms

false

the first text content, or the JSON of the contents

JSON-RPC error

error <code>: <message>

true

the error object

a tool asking the client for input

this tool asks the client for input (<kinds>): not supported by the dev console inspector yet, or without (<kinds>) when the server names none

true

the server’s missing-capability error, or its input request

transport failure

/mcp unreachable at <url>, timed out after 55 s, or HTTP <status>

true

none

A response body past 1 MiB fails the call as a transport failure; the panel never buffers more of it than that. The body and the details the page shows are truncated at 256 KiB regardless, as any action’s are.

A tool that asks the client for input. The inspector declares no client capability — neither elicitation, nor sampling, nor roots. A tool that checks isSupported() first, as langchain4j-cdi recommends, runs its fallback, which is how you test it. A tool that asks anyway gets the server’s missing-capability error (-32021), which the inspector reports with the refusal line above, naming the capability. Nothing is parked on the server, whatever the request-state mode: in CONTINUATION mode too, the call ends with that error before any invocation waits for an answer.

History. The panel keeps the last 20 calls of the boot, newest first, each with its time, action, outcome, duration and arguments, and a "Replay" button that picks the action in the list and fills its form with those arguments, and sends nothing until you submit. Each sub-tab shows the calls of its own kind, under the result. A dev reload clears the history along with the catalogue; an item it removed loses its result, the list falling back to the first item of its kind, and a kind left with no item sends the tab back to Monitoring.

Secrets. An argument whose name holds, ignoring case, password, passwd, secret, token, apikey, api-key, api_key, credential or authorization, at any depth, is shown as in the history, the "Exchange" details and the console’s log line; a string or number value of four characters or more that belongs to such an argument is also replaced by wherever it appears in the summary, the details and the body of an error or an input request, in case the server quotes it. The MCP server itself always receives the real value, and a result body is shown exactly as the server returned it. A masked value is left empty on a replay: type it again.

What v1 does not do. Elicitation, sampling and roots are not supported — a tool that needs one gets the refusal line above, not a dialog. There is no support for subscriptions, notifications or progress, for completion, or for calling an MCP server other than the application’s own.

JSON viewer NEW

Every JSON the page shows — the body of an application/json result and the details under "Exchange" — goes through a small viewer instead of plain text. Keys, strings, numbers, and true, false and null each have their colour, in the light and the dark themes. Every object and array folds under a ▾/▸ toggle; folded, it reads {…} 3 keys or […] 12 items. A click on the toggle or on that summary flips the node; Alt+click flips it and everything under it. The first two levels start open, only the first when the document holds more than 500 values.

Expand all, Collapse all and Copy sit next to what they act on: in the result’s header for the body, at the top of "Exchange" for the details. Copy writes the JSON indented, masked values as the server sent them; when the browser refuses the clipboard, the button says Clipboard refused and nothing else happens. An integer too large for a JavaScript number, such as 9007199254740993, is shown and copied as the server wrote it in a browser that gives JSON.parse the source text of a number, such as a current Chrome; elsewhere it is rounded to the nearest JavaScript number.

The viewer keeps what is folded while the page polls, and forgets it when a new result replaces the one on screen. A body that does not parse as JSON is shown as text. The tree is built with DOM calls only, never as markup, so a key or a string is never read as HTML.

A table of rows NEW

A result of rows — what Query answers in a pool’s tab or in the JDQL tab of Mansart Data, a table’s columns, its first rows — is drawn as a table. Each column’s header holds its name and, dimmed under it, its type; the header stays in view while the rows scroll, and a wide table scrolls sideways inside the result. A NULL is written NULL, dimmed, and is never mistaken for an empty string, which is an empty cell. A value longer than 200 characters is cut with …, and the whole of it shows when the pointer rests on the cell. When the panel sent only the first rows, more rows not shown says so under them.

The result’s bar adds Table and JSON, which shows the same body in the JSON viewer, and Copy as CSV, which copies the rows as RFC 4180 CSV: the header first, , between fields, a field with a comma, a quote or a line end quoted, each line ended by CRLF, NULL an empty field and an empty string "". The page remembers which of the two it shows while it polls. A cell of a column named replay is a button named after the action it fills, as Tables offers Describe and Preview for each table; nothing is sent until you submit.

A panel answers rows with PanelAction.ActionResult.rows(summary, columns, rows, more): columns are ActionResult.Column(name, type) records, a name and a type of 200 characters at most; each row holds one value per column, null, a Boolean, a Number or a String, anything else written as its toString(). The body, {"columns": [{"name", "type"}…], "rows": [[…]…], "more": bool} of type application/x-rows+json (ActionResult.ROWS), stays within the 256 KiB of a result: the rows past it are left out and more set. A body of another shape is shown as any other JSON.

JSON editor NEW

A json argument of an action — an MCP tool’s arguments in the MCP inspector, the arguments of a repository method in the Mansart Data tab — is typed in a small code editor written for the page, which loads nothing from elsewhere. It reads the argument’s JSON Schema as you type:

  • Colours. Keys, strings, numbers, true, false and null take the JSON viewer's colours, in the light and the dark themes; what is no JSON at all is red. The lines are numbered.

  • Diagnostics. The first syntax error — unterminated string, expected ',' or '}', trailing comma, nothing after the value… — then what the schema says, at every depth: a missing required key (marked on its object’s {), a value of the wrong type (integer refuses 1.5), a value outside its enum, a string longer than maxLength are errors; a key its object’s properties does not list, and a string that does not look like its format (date, time, date-time, uuid), are warnings. Each is underlined, red or orange, with a dot on its line; its message shows when the caret or the pointer is on it, and the line under the editor counts them: 1 error, 2 warnings. They never stop a call: the server stays the judge.

  • Completion. Ctrl+Space lists what fits where the caret is, and a " typed where a key goes opens the list by itself. In an object, the keys of its schema it does not have yet, required first and read-only last, each with its type, required or generated, and its description; the value comes with the key, an object with its required keys. After a : or in an array: the enum values, true and false, null, {} or []. Up and Down move, Enter or Tab or a click accepts, Escape closes, and typing filters the list.

  • Keys. An opening bracket or quote brings its closing one, a closing character typed before the same steps over it, Backspace between an empty pair deletes both, Enter keeps the line’s indentation and opens an indented line between {} or [], Tab and Shift+Tab indent and outdent the selected lines by two spaces. Escape then Tab leaves the editor. The bracket next to the caret and its match are outlined.

  • Format. The Format button, or Shift+Alt+F, indents the JSON by two spaces, one member per line, and keeps every string and number as written: an id past 2^53 or a \u00e9 escape does not change. A text that does not parse is not formatted, and the line under the editor says why: not formatted: line 3: trailing comma.

  • Undo. Every edit the editor makes — an accepted completion, a formatting, a closed pair — is undone by one Ctrl+Z (⌘Z).

The editor reads these keywords of the schema: type (one name or a list), properties, required, additionalProperties, items (a schema, or a list for the positions of an array), enum, maxLength, format, readOnly, description, and a $ref to /$defs/… or /definitions/…. Any other $ref is not followed; what is under anyOf, oneOf, allOf, not or patternProperties is accepted as it is. A schema the editor does not understand makes it check less, never fail. What you typed is sent as you typed it. Past 100 000 characters the editor stops colouring and checking, and says past 100 000 characters: no colours, no checks; completion and Format still work.

The generated form NEW

When the schema allows it, a json argument is a form rather than JSON, with a JSON switch to the editor that keeps the values both ways. The schema’s root is "type": "object", with no $ref, anyOf, oneOf, allOf or not, and each property is a string, number, integer or boolean, an enum of strings, or an object whose own properties all are such: one level of nesting, drawn as a group of fields under the object’s name. A string of "format": "textarea" is a field of several lines (A JSON argument). Any other schema gets the editor alone.

  • A required property is starred, title *, and a call without it is refused with its path, before anything is sent: entity.title is required. A nested object that is not required and left empty is not sent at all.

  • A readOnly property, such as an id the database generates, reads id (generated), its field showing generated. It is never required: left empty it is not sent; filled, it is.

  • A field of "format" date, time, date-time or uuid shows the shape it expects: 2026-09-30, 14:30:00, 2026-09-30T14:30:00, 123e4567-e89b-12d3-a456-426614174000.

  • The editor starts from the required properties, at each level. Replay fills the form, a nested object included; a value the panel masked is left for you to type again.

Query editor NEW

A query argument — the query of the JDQL tab of Mansart Data, in Query, Update / Delete and Export CSV (A JDQL console for Mansart Data) — is typed in the same editor, in its query mode, five lines high, with the vocabulary its panel publishes: the entities and their attributes. The page fetches that vocabulary once, when the first form that needs it is drawn, and again after a dev reload only; until it arrives, or when it cannot be had, the editor works with the keywords alone and says why under it, no vocabulary: 404. It arrives without moving the text or the caret.

  • Colours. Keywords, functions, the entity, its attributes, strings, numbers and parameters (:name, ?1) each have a colour of their own, in the light and the dark themes; a name the vocabulary does not know is plain.

  • Completion. Ctrl+Space lists what fits where the caret is: after FROM or UPDATE, the entities, each with its table; after project., the attributes of the entity the reference leads to, one step at a time however long the path; in a SELECT, WHERE, ORDER BY or SET clause, the attributes of the query’s entity, even when FROM comes after the caret, each with its Java type and its column, then this, the functions, inserted with ( and the caret inside, and the other keywords; anywhere else, the keywords. Typing filters the list, ignoring case; a keyword is inserted in capitals.

  • Diagnostics. An unknown entity, an unknown attribute of the entity or of the one a reference leads to, a path through an attribute that is no reference (title is not a reference), an unterminated string, and a parenthesis never closed or closing none are errors, underlined as the JSON editor’s. Nothing else: the grammar is the server’s to judge, and it says what is wrong when the query runs. An entity written by its full class name is not checked.

  • Keys. ( and ' come in pairs; a ' typed right before a closing one steps over it, and a doubled '' inside a string is one quote. The other keys are the JSON editor’s.

  • Format. Format, or Shift+Alt+F, writes the keywords in capitals, starts each clause on a line of its own and AND or OR on an indented line; the AND of a BETWEEN stays where it is, and names, strings, numbers and parameters are kept as written. A query with an unterminated string is not formatted: not formatted: line 2: unterminated string.

Its parameters NEW

The params of a query is a JSON editor whose schema follows the query as you type it: one key per named parameter, in order of first use, all required, and no other. A parameter compared with an attribute takes its type, its format and its values: after WHERE status = :status, completion offers "status" with the values of its enum, and the editor flags the key while it is missing. LIKE :pattern takes a string, BETWEEN :low AND :high the attribute’s type for both, IN :statuses a list of it, SET title = :title the attribute’s type; the description of each says where it is used, such as compared with price (number). A parameter used twice keeps its first typed use; one used otherwise, such as price * :factor, takes any value; a positional one, ?1, has no key. Nothing is refused before the call. Its text is sent as typed; empty, it holds {}.

SQL NEW

The sql of a pool’s Query and Execute (Tables and a SQL editor) is typed in the same editor, with what its panel publishes of the pool’s tables, and reads SQL as SQL is written:

  • Several tables. Every table after FROM, JOIN, UPDATE, INSERT INTO or DELETE FROM is in the statement’s scope, and so is each table of a FROM list; a name after a table, or AS and a name, is its alias (FROM tasks t, JOIN projects AS p), a keyword never. A table of another schema is written sales.orders.

  • Quoted names. "Order", "due date" are names, a doubled "" inside one quote; the editor looks them up as written, and a name written without quotes as the database stores it: in lower case for PostgreSQL, in capitals for H2. " pairs and steps over as ' does. -- and /* … */ comments are dimmed and never checked.

  • Completion. After FROM or JOIN, the tables; after t. or tasks., that table’s columns; in SELECT, ON, WHERE, GROUP BY, HAVING, ORDER BY and SET, the aliases and the tables in scope, then the columns of each, their table in their detail (varchar(200) · column · tasks), the functions and the keywords. A name that needs quotes — a keyword, a space, a capital the database would not keep — is inserted with them.

  • Diagnostics. An unknown table, an unknown column of a table or an alias, and an unknown alias before a dot (unknown table or alias x) are errors; a column named alone that two tables in scope have is a warning, title is in tasks t and projects p. A table written with its schema is looked up as written, then without it, and not checked when neither is known. What a sub-query, a WITH’s table, an alias of the select list, a cast’s type (::text`) or a function the editor does not know holds is never checked: the database judges it when the statement runs. A sub-query or a function’s rows after FROM or JOIN, LATERAL or not — (SELECT …) x, generate_series(1, 3) AS g(n) — is a table whose columns are unknown, named by its alias, and so is a WITH r(a) AS (…); the field of EXTRACT(EPOCH FROM due_date) is no column.

  • Format. Each clause on a line of its own, a LEFT JOIN … ON on one line, AND and OR indented, the keywords in capitals, quoted names, strings and comments as written, a sub-query on the line it is on.

A :name in the statement is a parameter, as in JDQL: params follows it, and a parameter compared with t.price takes the type of price in `t’s table.

A query language for a panel NEW

A panel gives a json argument’s property this mode in two steps.

  1. Its languages(), on DevConsolePanel or LivePanel, returns the PanelLanguage records it offers: an id, which follows the key rule of PanelSample, such as jdql, and the text of one JSON object, at most 1 MiB and 64 levels deep, checked when the record is built. It is read once per boot, with actions(), in a dev launch only. Two languages of one id leave the panel without languages, and the console logs a WARNING saying so; a languages() that throws leaves it without languages too. The snapshot names each panel’s ids, "languages": ["jdql"], never their content, which GET /api/language/<panel>/<id> serves as the panel wrote it: GET or HEAD, a dev launch only (404 otherwise), no token since it only reads, an Origin, when there is one, that is the console’s own (403), and never logged.

  2. The schema of the argument names it. A string property of "format": "textarea" whose schema also says "contentMediaType": "text/x-query" and "x-language": "<id>" is the query editor; a string property of "format": "textarea" and "x-parameters-of": "<query property>", beside the query, is the JSON editor of its parameters. Both are sent as strings, as typed.

{ "mode": "query",
  "dialect": {
    "keywords": ["SELECT", "FROM", "WHERE", "ORDER", "BY", "AND", "OR", "NOT", "IS", "NULL", "BETWEEN", "LIKE",
                 "IN", "ASC", "DESC", "UPDATE", "SET", "DELETE", "COUNT", "THIS", "SUM", "AVG", "MIN", "MAX",
                 "TRUE", "FALSE"],
    "functions": ["UPPER", "LOWER", "LENGTH", "ABS", "CONCAT", "COUNT", "SUM", "AVG", "MIN", "MAX"],
    "clauses": ["SELECT", "FROM", "WHERE", "ORDER BY", "SET", "UPDATE", "DELETE FROM"],
    "targetAfter": ["FROM", "UPDATE"],
    "self": "this",
    "quote": "'" },
  "targets": {
    "Task": { "detail": "table task",
      "attributes": {
        "id": { "type": "integer", "detail": "Long · id, generated" },
        "title": { "type": "string", "detail": "String · column title" },
        "dueDate": { "type": "string", "format": "date", "detail": "LocalDate · column due_date" },
        "project": { "type": "integer", "detail": "→ Project · column project_id", "target": "Project" } } } } }

mode is query. keywords are matched ignoring case and written in capitals; functions are the names that may be followed by (; clauses start a clause; targetAfter are the words after which the entity is named; self is the entity’s own name in an expression; quote delimits a string, doubled inside it. A word the dialect leaves out takes JDQL’s. targets maps each entity’s name to its detail and attributes; an attribute has a JSON Schema type (string, integer, number, boolean), optionally a format and an enum, a detail, and a target when it refers to another entity of the language. Data the editor does not understand makes it know less, never fail.

SQL’s options NEW. aliases, identifierQuote and unquotedCase make a dialect SQL’s (SQL NEW), with SQL’s own words; JDQL declares none of them:

"dialect": { "keywords": […], "functions": […], "self": null, "quote": "'",
  "aliases": true,
  "targetAfter": ["FROM", "JOIN", "UPDATE", "INTO"],
  "identifierQuote": "\"",
  "unquotedCase": "lower",
  "clauses": ["SELECT", "FROM", "JOIN", "ON", "WHERE", "GROUP BY", "HAVING", "ORDER BY", "LIMIT", "OFFSET", "SET",
              "VALUES", "UPDATE", "DELETE FROM", "INSERT INTO"] }

aliases puts every target after a targetAfter word in scope, with its alias, skips -- and /* comments, and leaves a sub-query unchecked; identifierQuote makes a quoted name a name; unquotedCase, lower or upper, says how the database stores a name written without quotes, and is left out when it keeps it as written; "self": null says there is none. A target of another schema is keyed schema.name and says its "schema", so that completion quotes each part as needed.

Security NEW

The console shows what the application is made of, so it is built to be seen by you only:

  • Loopback by default. It listens on 127.0.0.1. Another address raises VIDOCQ-DEVC-002 — not a loopback address NEW, in every mode.

  • Its own address only. A request is answered only when its Host header names the console: localhost, 127.0.0.1, [::1] or the host it listens on, optionally with the port it bound; when that host is not a loopback one, any IP address too. The host it listens on is the configured one only when the console accepted it: a value refused by the ASCII-only rule of Configuration NEW is reported as VIDOCQ-DEVC-003 — invalid value NEW and replaced by 127.0.0.1, for the bind, the banner and this check alike. Anything else gets 403. This stops DNS rebinding, where a page of another site has its name resolved to 127.0.0.1 to reach local ports: the browser still sends that site’s name.

  • Read-only outside development NEW. In a test or prod launch the console answers GET and HEAD only, anything else gets 405, POST /api/action/… included: no panel’s actions() is called, and the snapshot carries neither a token nor an action.

  • Actions in a dev launch, for the console’s own page only NEW. A panel may offer actions, such as setting a log level; the console runs one only for a request its own page sent, behind the checks of Actions NEW.

  • A read-only dev MCP in a dev launch NEW. POST /mcp serves the page’s facts to a coding agent as MCP tools that change nothing, behind the checks of Dev MCP NEW: no token, since nothing it answers is not already on the page, but an Origin, when there is one, that is the console’s own.

  • Query languages, read-only, in a dev launch NEW. GET /api/language/<panel>/<id> serves the vocabulary of a panel’s query editor, its entities and attributes, behind the checks of A query language for a panel NEW: GET or HEAD, no token, an Origin, when there is one, that is the console’s own; outside dev, 404.

  • No reading from another site. No CORS header, so another site’s page can send a request but never read the answer. Every answer carries X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer and Content-Security-Policy: default-src 'self'; frame-ancestors 'none'.

  • No secret. A section reports a secret only as configured or not configured: the API has no way to pass its value. A configuration value that may carry one, such as a JDBC URL, is shown in a dev launch only, without its credentials; outside it, the panel says what it is, such as the database kind. The console never shows JVM arguments or system properties, nor the message of an exception thrown by a panel.

  • Text stays text. Everything a panel writes reaches the page as text, never as markup, with control characters replaced and cut at 200 characters.

  • Links are built, never written NEW. A panel’s button or a route’s URL becomes a link only when Vidocq built it from a listener the report declared and a path: an absolute http or https URL, opened in a new tab with rel="noopener noreferrer". A panel has no way to pass a URL of its own. See Links.

  • A warning outside development. Forced on in a test or prod launch, the console raises VIDOCQ-DEVC-001 — on outside development NEW, naming where it listens.

Actions NEW

In a dev launch, a panel may offer actions, such as setting a log level (Actions): buttons next to its links, with a field per argument and, when the panel asks for one, an inline confirmation. That makes the console no longer read-only, and a write is a different threat from a read: a page of any site you visit can send a POST to http://127.0.0.1:8888/. It cannot read the answer, but it does not need to. So POST /api/action/<panel>/<action> runs only when every check passes, in this order, each failure answering without running anything:

  1. the Host, as for every request (403);

  2. the method is POST (405), and the body is application/json, a charset allowed (415): a cross-site form cannot send it without a CORS preflight, which the console never answers;

  3. Origin is present and is http:// followed by the very Host the request was let in with: the origin of the page the console served there (403). A missing origin, null, https, another name for the same address or another port are refused;

  4. the header X-Vidocq-Console-Token equals the token of the boot, 32 random bytes in hex drawn from SecureRandom when the console starts, compared in constant time; the page reads it from /api/snapshot (console.actionToken), which another site cannot read (403);

  5. the panel and the action exist (404), the body is at most 64 KiB for an action with a json argument, such as the MCP inspector's, 4 KiB otherwise (413), and it is one JSON object of strings whose keys are exactly the arguments the action declared, each value accepted by its check, a json value parsing as a JSON object (400).

A refusal at step 3 or 4 is logged as VIDOCQ-DEVC-006 — an action request from elsewhere NEW. An action runs on a virtual thread, one at a time per panel (409 while another runs), and the request waits for it up to 60 seconds: 200 {"result": "…​"}, 500 {"error": "<exception class>"}, or 202 {"state": "running"} past the limit, the action going on. The snapshot lists each action with its last outcome of the boot, so the page shows it after a reload too. Every run is logged at INFO, Vidocq dev console: action <panel>/<action> by <address>: <result>; a failure at WARNING with the class of the exception, which is all the page shows of it, never its message. The decision and the options set aside are in ADR 0001, docs/adr/0001-dev-console-actions.md in the repository; ADR 0001, amendment 1, adds the json argument, the structured result and the groups this section and Actions describe.

Dev MCP NEW

In a dev launch, the console is also an MCP server, as Quarkus’s Dev MCP is: a coding agent, such as Claude Code, asks the running application what it serves instead of guessing it from the sources. It is served by the console itself, on the console’s own listener, never the application’s, so it works whether or not the application uses MCP or langchain4j-cdi, and adds no dependency. The console prints its URL after its own:

Vidocq dev console: http://127.0.0.1:8888/
Vidocq dev MCP: http://127.0.0.1:8888/mcp

and the devconsole section of the report shows it on its mcp row. To point Claude Code at it:

claude mcp add --transport http vidocq-dev http://127.0.0.1:8888/mcp

Any MCP client that speaks the Streamable HTTP transport does the same; the MCP Inspector lists the tools with npx @modelcontextprotocol/inspector --cli --transport http --server-url http://127.0.0.1:8888/mcp --method tools/list.

Tool What it answers

vidocq_report

The startup report as the page shows it: the console (version, URL, boot id), the launch mode and why, the anomalies, every section that is not a panel with its lines, and the whole report as text. state is booting until the report is written.

vidocq_panels

The panels of the page, the extensions' and the console’s own: id, title, whether it is live, and its summary.

vidocq_panel

One panel, by id (its input schema lists the ids of the panels shown now): its boot facts, its charts and a fresh sample of its live values, as a poll of the page would get them, without the history of its curves. An unknown id is a tool error that names the panels.

vidocq_anomalies

The anomalies of the startup report, each with its code, message, hint and source.

vidocq_routes

The routes of the rest section: the HTTP method, the absolute URL (the http section declares the listeners) and the method that handles it, such as POST http://127.0.0.1:8080/mcp McpEndpoint#handlePost.

Each tool answers one JSON object, as structuredContent and, for a client that reads text only, indented in one text content. They are built from the very document the page polls, /api/snapshot, minus what only the page needs to act — the token of the boot and the actions — so an agent sees nothing the page does not: secrets read configured, values are masked as on the page (Security NEW).

  • Read-only. No tool runs an action; every tool says so in its description and carries readOnlyHint. The actions stay the page’s (Actions NEW), until an ADR decides otherwise.

  • The protocol, the part it needs. One JSON-RPC 2.0 request per POST, answered application/json, never an event stream, and no session: no Mcp-Session-Id is given or needed. It answers initialize (the protocol version asked for when it is 2025-03-26, 2025-06-18 or 2025-11-25, the newest of them otherwise; serverInfo.name is vidocq-dev-console), ping, tools/list and tools/call; a notification, such as notifications/initialized, gets 202. An unknown method is the JSON-RPC error -32601, parameters it cannot use -32602, a body that is not JSON -32700 and anything but one request object, a batch included, -32600, these two with 400. GET /mcp gets 405: the server never starts a stream.

  • The checks, in this order. The Host, as for every request (403); the method is POST (405); the body is application/json (415), which a cross-site form cannot send; the Origin, when there is one, is the console’s own, as for an action (403, logged as VIDOCQ-DEVC-006 — an action request from elsewhere NEW) — an MCP client that is no browser sends none, and a browser always sends one on a cross-site POST; the Accept header takes application/json (406); the body is at most 64 KiB (413) and nests at most 32 levels. No token: the tools answer what the page shows, which the Host check already keeps to the console’s own address.

  • Outside dev, absent. In a test or prod launch, POST /mcp gets 405 like any other POST, and the URL is neither printed nor in the report.

Anomaly codes NEW

The first four are anomalies of the console’s section of the startup report: each is one WARNING record on io.vidocq.startup.anomaly, logged the moment it is found and recalled by the report (Anomalies). The fifth comes later, from a poll, the sixth from a refused action request, and the seventh, eighth and ninth NEW from matching the live panels the -dev modules bring against the report’s sections, once per boot — all five on the console’s own logger, io.vidocq.devconsole.

VIDOCQ-DEVC-001 — on outside development NEW

vidocq.devconsole.enabled=true turned the console on in a test or prod launch. It names where the console listens, the configured host and the bound port:

[VIDOCQ-DEVC-001] The dev console is on in a prod launch: it shows the startup report and the live values of this application on 0.0.0.0:18093 Remove vidocq.devconsole.enabled=true outside development

Remove the key, or scope it to a profile: %dev.vidocq.devconsole.enabled=true.

VIDOCQ-DEVC-002 — not a loopback address NEW

The console listens on an address that is not a loopback one, in any mode, so whoever reaches the machine can read the page:

[VIDOCQ-DEVC-002] The dev console listens on [0:0:0:0:0:0:0:0]:18093, which is not a loopback address: whoever reaches this machine can read the startup report and the live values of this application Remove vidocq.devconsole.host, or set it to 127.0.0.1

The address is the one bound, as the JVM writes it: 0.0.0.0 reads as the IPv6 wildcard.

VIDOCQ-DEVC-003 — invalid value NEW

A vidocq.devconsole.* key has a value the console does not accept; the default is used, and the message says which:

[VIDOCQ-DEVC-003] Invalid value 'yes' for vidocq.devconsole.enabled (auto, true, false): using auto
[VIDOCQ-DEVC-003] Invalid value '99999' for vidocq.devconsole.port (0 to 65535): using 8888
[VIDOCQ-DEVC-003] Invalid value 'a host' for vidocq.devconsole.host (a host name or an IP address): using 127.0.0.1

A host is refused for any character but ASCII letters, digits and .-_:%[], or beyond 255 characters: the console listens on 127.0.0.1 instead, and the banner says so too (The banner segment NEW). A mistyped key, rather than a wrong value, is the key audit’s to report: VIDOCQ-CFG-003 (Configuration NEW).

VIDOCQ-DEVC-004 — port taken NEW

The configured port was taken: the console listens on a free port instead (When the port is taken NEW).

[VIDOCQ-DEVC-004] The dev console's port 18092 is taken: it listens on port 57738 instead, http://127.0.0.1:57738/ Free port 18092, or set vidocq.devconsole.port to another port, 0 for any free one

VIDOCQ-DEVC-005 — a panel failed to sample NEW

A panel’s sample() threw a RuntimeException or a LinkageError, an invalid value key included. That poll misses the panel’s live values, its boot facts stay, and the console calls it again on the next poll. The WARNING is logged on io.vidocq.devconsole once per panel and boot, with the class of the exception only; its stack trace follows at DEBUG on the same logger:

[VIDOCQ-DEVC-005] Panel 'acme-cache' failed to sample: IllegalArgumentException

It is a bug of the panel: see Writing a dev console panel.

VIDOCQ-DEVC-006 — an action request from elsewhere NEW

In a dev launch, an action request was refused for its Origin or its X-Vidocq-Console-Token (Security NEW), or a dev MCP request NEW for its Origin: it did not come from the console’s own page. Most likely a page of another site tried to make the console do something, or a stale tab of a previous boot sent its old token. Nothing ran. The WARNING is logged on io.vidocq.devconsole with the origin the request named, once per origin, reason and boot, 64 at most:

[VIDOCQ-DEVC-006] Dev console action refused, origin 'https://evil.example.com': not the console's own origin
[VIDOCQ-DEVC-006] Dev console action refused, origin (none): not the console's own origin
[VIDOCQ-DEVC-006] Dev console action refused, origin 'http://127.0.0.1:8888': no valid X-Vidocq-Console-Token
[VIDOCQ-DEVC-006] Dev console MCP request refused, origin 'https://evil.example.com': not the console's own origin

The last one, from the console’s own origin, is a tab that outlived its boot: reload it.

VIDOCQ-DEVC-007 — two live panels for one section NEW

Two -dev modules' live panels (A -dev module, never packaged) name the same section id. The first one found wins; the second is skipped, never sampled. Logged once per boot, the first time the console reads the written report:

[VIDOCQ-DEVC-007] Two live panels for the section 'mansart-pool': io.acme.dev.FirstLivePanel is kept, io.acme.dev.SecondLivePanel is skipped

Two extensions share an id by mistake, or the application declares two versions of the same -dev module on the module path. Remove the one that should not be there.

VIDOCQ-DEVC-008 — a live panel with no section NEW

A -dev module’s live panel names a section id that the startup report does not have — the runtime extension’s own contributor failed, was skipped, or does not exist. The panel is never shown; it never invents a section:

[VIDOCQ-DEVC-008] The live panel 'mansart-pool' has no section in the startup report: not shown

The application has the -dev module without its matching runtime extension, or that extension’s contributor did not run. Check for VIDOCQ-RPT-001 (Anomalies) on the same boot.

VIDOCQ-DEVC-009 — a panel shipped twice NEW

A section’s own contributor already implements DevConsolePanel (the all-in-one form, Writing a dev console panel), and a -dev module’s live panel also claims that section’s id. The live panel wins; the contributor’s own sample and charts are never called:

[VIDOCQ-DEVC-009] The section 'mansart-pool' is written by io.acme.MansartPoolExtension, itself a panel, and a live panel makes it live: the live panel wins; the extension ships its panel twice

The extension should drop one of the two: either its own DevConsolePanel implementation, or the -dev module, not both.

Loggers NEW

Logger Records

io.vidocq.devconsole

The URL record (INFO), the boxed WARNING of a taken port, a console that could not start (WARNING), VIDOCQ-DEVC-005 — a panel failed to sample NEW, and the stack trace of a failed sample (DEBUG). In a dev launch NEW: the URL of the dev MCP (INFO), every action run (INFO), a failed one (WARNING, its stack trace at DEBUG), and VIDOCQ-DEVC-006 — an action request from elsewhere NEW. VIDOCQ-DEVC-007 — two live panels for one section NEW, VIDOCQ-DEVC-008 — a live panel with no section NEW and VIDOCQ-DEVC-009 — a panel shipped twice NEW NEW, once per boot, the first time the report is read. The logs panel NEW logs nothing: its handler only reads what reaches the root logger.

io.vidocq.startup.anomaly

VIDOCQ-DEVC-001 — on outside development NEW to VIDOCQ-DEVC-004 — port taken NEW, as every startup anomaly.

Next steps NEW