@dunx/dashboard 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Petar Zarkov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # @dunx/dashboard
2
+
3
+ One page over a running dunx app: the routes it serves, the container it built,
4
+ the gateways it upgrades, Redis, the config keys and the process itself - **with
5
+ bull-board mounted for the queues**. Opt in with one module and one `app.use`.
6
+
7
+ ```bash
8
+ bun add @dunx/dashboard
9
+ ```
10
+
11
+ An operator looking at a running dunx service has had three surfaces over the same
12
+ data and none of them the one they wanted. `/docs` answers "what can a client
13
+ call". `@dunx/mcp` answers the same questions for an agent, over stdio. Nothing
14
+ answered **"what is this process actually doing"**. This is that page, and it is
15
+ cheap because every panel reads data dunx already computes.
16
+
17
+ ## Mount it
18
+
19
+ ```ts
20
+ import { DashboardMiddleware, DashboardModule } from '@dunx/dashboard';
21
+ import { JobPublisher, QueueModule } from '@dunx/infra/queue';
22
+ import { RedisConnection, RedisModule } from '@dunx/infra/redis';
23
+
24
+ @Module({
25
+ imports: [
26
+ DashboardModule.forRootAsync({
27
+ // This dynamic module is its own scope, so whatever exports the tokens the
28
+ // factory injects goes here.
29
+ imports: [QueueModule, RedisModule],
30
+ useFactory: (queues: JobPublisher, redis: RedisConnection) => ({
31
+ queues,
32
+ redis,
33
+ authorize: (req) => req.headers.get('x-ops-key') === process.env.OPS_KEY,
34
+ }),
35
+ inject: [JobPublisher, RedisConnection] as const,
36
+ }),
37
+ ],
38
+ })
39
+ export class AppModule {}
40
+ ```
41
+
42
+ ```ts
43
+ const app = await HttpFactory.create(AppModule);
44
+ app.use(DashboardMiddleware, SessionGuard); // the dashboard first - see below
45
+ await app.listen(3000);
46
+ ```
47
+
48
+ That is the whole wiring. `JobPublisher` and `RedisConnection` are accepted **as
49
+ they are**: this package restates what it needs from them structurally and depends
50
+ on `@dunx/infra`, `bullmq` and `ioredis` not at all, so an app with no queues does
51
+ not install a queue library to look at its routes.
52
+
53
+ ## Six panels
54
+
55
+ | Panel | Reads | Lifetime |
56
+ | ----------------------- | ---------------------------------------------------------------------------------- | -------- |
57
+ | **Overview** | counts, uptime, heap, Bun version, dependency probes | polled |
58
+ | **Routes** | `routesOf` - method, path, controller, module, guards, `@Roles`/`@Public`, schemas | static |
59
+ | **Gateways** | `gatewaysOf` - upgrade path, the event each handler claims | static |
60
+ | **Modules & providers** | `providersOf`, `modulesOf` - what each module binds, exports and injects | static |
61
+ | **Queues & Redis** | queue names and a link to **bull-board**; Redis `PING` and `INFO` | polled |
62
+ | **Configuration** | keys and types, values only where you allow them | static |
63
+
64
+ The provider panel is the one that earns its place fastest. A missing-binding error
65
+ names one token; reconstructing which module bound what and why the graph did not
66
+ close is otherwise a grep across every `@Module`. An **unresolvable constructor
67
+ parameter** - an interface, a primitive, a union, a type-only import - is called out
68
+ in red on the overview, because each one is a boot error waiting to happen.
69
+
70
+ The static panels use the same readers `@dunx/mcp` answers with, and they construct
71
+ nothing. That is the deliberate inversion of MCP's rule: MCP refuses runtime
72
+ questions because booting an app to answer them would open databases and bind
73
+ sockets, and this package is *already inside* a booted app, so the reason does not
74
+ apply. Stating it per panel is what stops the dashboard growing a `boot()`.
75
+
76
+ ## Every panel has a JSON sibling
77
+
78
+ ```bash
79
+ curl -H 'x-ops-key: …' $APP/_dunx/api/snapshot
80
+ curl -H 'x-ops-key: …' $APP/_dunx/api/runtime
81
+ curl -H 'x-ops-key: …' $APP/_dunx/api/redis
82
+ curl -H 'x-ops-key: …' $APP/_dunx/api/queues # names only; the board is at /_dunx/queues
83
+ ```
84
+
85
+ These are the endpoints the page itself uses, and they are supported rather than an
86
+ implementation detail - which is what makes the dashboard usable on a box with no
87
+ browser. Their types are exported (`Snapshot`, `RuntimeReport`, `RedisReport`,
88
+ `QueuesReport`), so a `fetch` of them is typed. Anything *about* a queue is
89
+ bull-board's own API, under `{path}/queues`.
90
+
91
+ ## Security
92
+
93
+ **`authorize` has no default. Leaving it out serves the page to anyone who can
94
+ reach the port**, and the page is routes plus config plus the provider graph on one
95
+ screen - a reconnaissance gift. Omitting it logs a warning naming the mount at boot,
96
+ because "fine behind a private network" is a real answer and guessing is not.
97
+
98
+ Four things follow, and none is obvious:
99
+
100
+ - **A rejected request gets 404, not 403.** A dashboard that announces itself to an
101
+ unauthenticated caller has told them where to keep knocking.
102
+ - **Register it ahead of any session guard.** With the middleware last in the chain,
103
+ a `SessionGuard` answers every dashboard request `401` before `authorize` runs,
104
+ which defeats the 404 contract entirely.
105
+ - **So `authorize` must be self-sufficient.** It receives the raw `Request` and runs
106
+ before anything has written an `AuthContext` - ask your auth library directly.
107
+ - **`commands: false`** puts bull-board in its own read-only mode. Everything else
108
+ on the page only ever reports, so this is entirely about the queues. `authorize`
109
+ gates who reaches the mount; this gates what they can do once there.
110
+
111
+ ### Configuration is redacted by default
112
+
113
+ `ConfigService` holds whatever your `validate` returned, which includes every secret
114
+ you have. A deny-list of the usual suspects - `SECRET`, `PASSWORD`, `TOKEN` - looks
115
+ careful and leaks the first key nobody thought of, so **the default reveals
116
+ nothing**: the panel shows keys and types, which is most of what it is wanted for,
117
+ and a value appears only where you say so.
118
+
119
+ ```ts
120
+ config: appConfig, // ConfigService satisfies this as written
121
+ reveal: (key) => key === 'NODE_ENV' || key.startsWith('PUBLIC_'),
122
+ ```
123
+
124
+ There is no "reveal" control on the page. Redaction is decided at boot by the app,
125
+ not per click by whoever reached it.
126
+
127
+ ## The queues are bull-board's
128
+
129
+ **dunx renders no queue UI.** `{path}/queues` is
130
+ [bull-board](https://github.com/felixmosh/bull-board), mounted - flows, job logs,
131
+ the repeatable-job editor, per-queue metrics, redis stats, retry/promote/clean, all
132
+ of it, and none of it dunx's to maintain.
133
+
134
+ This package briefly shipped its own queue table, and that was the wrong call under
135
+ the framework's first rule: never invent what a mature library already solves. The
136
+ one thing that had ever justified hand-rolling it was that mounting bull-board on
137
+ `Bun.serve` meant writing a server adapter - which the deleted
138
+ `@dunx/queue-dashboard` did, and which was a liability. **bull-board 8.6.0 ships
139
+ `@bull-board/bun`**, so that reason is gone and the integration is three calls.
140
+
141
+ It also disposes of the question that started all this: `Queue.getWorkers()` returns
142
+ `[]` on Bun, because bullmq matches workers by client name through `CLIENT LIST` and
143
+ its Bun adapter never names a connection. Whatever bullmq can report on Bun is
144
+ bull-board's to report. dunx is not in the business of papering over it, and a
145
+ dashboard that quietly worked around a library's limitation would be a worse place
146
+ to find out about it.
147
+
148
+ ```bash
149
+ bun add @bull-board/api @bull-board/ui @bull-board/bun
150
+ ```
151
+
152
+ All three are **optional peers**. Without them the queues panel says so and names
153
+ the install line; nothing else on the page is affected.
154
+
155
+ Two things dunx does contribute, and they are the two bull-board cannot know:
156
+
157
+ - **It is behind the same `authorize`** as the rest of the mount, and answers the
158
+ same 404 to a caller that fails it.
159
+ - **`commands: false` maps onto bull-board's own `readOnlyMode`** rather than dunx
160
+ refusing its POSTs. It already has the switch; a second implementation would
161
+ disagree the moment bull-board grew an operation dunx had not heard of.
162
+
163
+ One caveat worth knowing: **bull-board's page loads a webfont from Google Fonts.**
164
+ dunx's own page fetches nothing, and that guarantee does not extend across the
165
+ handoff.
166
+
167
+ ### Naming a queue this process only consumes
168
+
169
+ A queue is a key prefix opened on first use, so `JobPublisher.opened` lists only what
170
+ this process has **published** to. A worker that drains `thumbnails` and publishes
171
+ nothing has opened nothing, and the queue would be invisible on the page that exists
172
+ to show it:
173
+
174
+ ```ts
175
+ queueNames: ['thumbnails'],
176
+ ```
177
+
178
+ This is free. The board - and therefore any connection to the broker - is built on
179
+ the **first request for `{path}/queues`**, never at boot and never by the polling
180
+ `/api/queues` endpoint, which reads names straight off the options. An app that
181
+ mounts the dashboard and never opens the board holds no socket for it, which is what
182
+ lets a process still exit cleanly against an absent Redis.
183
+
184
+ ## Options
185
+
186
+ | Option | Default | Notes |
187
+ | ----------------- | ---------- | -------------------------------------------------------------- |
188
+ | `path` | `/_dunx` | **`setGlobalPrefix` does not move it** - see below |
189
+ | `authorize` | *none* | No default. See Security |
190
+ | `title` | `'dunx'` | Header and `<title>` |
191
+ | `queues` | *none* | `JobPublisher` |
192
+ | `queueNames` | `[]` | Queues this process only consumes |
193
+ | `redis` | *none* | `RedisConnection` |
194
+ | `config` | *none* | `ConfigService`. Absent means no config panel |
195
+ | `reveal` | reveal none| Per-key opt in |
196
+ | `probes` | `[]` | Anything else worth a light |
197
+ | `openApiPath` | *none* | Links each route row into the explorer |
198
+ | `pollMs` | `5000` | `0` turns polling off and leaves the refresh button |
199
+ | `probeTimeoutMs` | `2000` | A hung probe costs one light, not the page |
200
+ | `commands` | `true` | `false` → bull-board's own `readOnlyMode` |
201
+
202
+ `app.setGlobalPrefix('api')` prefixes routes discovered from controllers. The
203
+ dashboard is a **middleware matching a path**, not one of those - which is exactly
204
+ what lets it serve a route table handed over at runtime without generating a
205
+ controller per panel. With a global prefix, say so:
206
+
207
+ ```ts
208
+ path: '/api/_dunx',
209
+ ```
210
+
211
+ ## Probes
212
+
213
+ Anything with a name and a `check()`. It is awaited with a timeout and never allowed
214
+ to throw into a response, so a hung dependency costs one light rather than the page -
215
+ and a probe that did not answer reads `unknown`, never `down`, because those are
216
+ different facts and one of them sends somebody to restart a healthy service.
217
+
218
+ ```ts
219
+ probes: [
220
+ {
221
+ name: 'database',
222
+ check: async () => {
223
+ await db.execute(sql`select 1`);
224
+ return { state: 'up', detail: 'sqlite' };
225
+ },
226
+ },
227
+ ],
228
+ ```
229
+
230
+ Passing `redis` adds one automatically, on `PING` rather than the connected flag: a
231
+ flag says a socket is up and a round trip says the server is answering.
232
+
233
+ ## The page
234
+
235
+ Server-rendered shell, React + Mantine inside it, **inlined** - no CDN, no `src=`,
236
+ no `<link>`, so it opens on a host with no egress. It shares its theme and
237
+ components with the dunx documentation site and the API explorer, so the three look
238
+ like one product.
239
+
240
+ The bundle sits behind `@dunx/dashboard/ui` and is reached with `await import()` on
241
+ the first request for the page, so an app that mounts the module and never opens it
242
+ pays nothing at boot. It is built by `internal/dashboard-ui`.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Every read the dashboard makes off-process is bounded, and this is the one
3
+ * implementation of that.
4
+ *
5
+ * The failure it exists for is specific and was measured: with Redis unreachable,
6
+ * a queue's `getJobCounts` waits out the connection timeout - 5 s by default in
7
+ * `@dunx/infra/queue` - so opening the dashboard on a broken broker hung the page
8
+ * for exactly as long as the thing you opened it to look at was broken. A
9
+ * dependency being down must cost one panel, not the page.
10
+ *
11
+ * The fallback is a **value**, not a rejection: a queue that could not be reached
12
+ * still gets a row saying so, which is the whole point of looking.
13
+ */
14
+ export declare const bounded: <T>(work: () => Promise<T>, ms: number, onTimeout: () => T) => Promise<T>;
@@ -0,0 +1,9 @@
1
+ import type { RedisProbe } from '../contracts.js';
2
+ import type { RedisReport } from './types.js';
3
+ /**
4
+ * `INFO` replies as a text blob of `key:value` lines with `# Section` headers.
5
+ * Parsed here rather than adding a method per field to `RedisProbe`, which is how a
6
+ * structural restatement turns into a client library.
7
+ */
8
+ export declare const parseInfo: (raw: string) => Readonly<Record<string, string>>;
9
+ export declare const redisReport: (redis: RedisProbe, timeoutMs: number) => Promise<RedisReport>;
@@ -0,0 +1,13 @@
1
+ import type { DashboardProbe } from '../contracts.js';
2
+ import type { DashboardOptions } from '../options.js';
3
+ import type { ProbeReport, RuntimeReport } from './types.js';
4
+ export declare const runProbe: (probe: DashboardProbe, timeoutMs: number) => Promise<ProbeReport>;
5
+ /**
6
+ * The Redis handle, if the app passed one, as a probe like any other - so the
7
+ * lights row has one shape and the panel does not special-case its own dependency.
8
+ *
9
+ * `ping` rather than `connected`: the flag says whether a socket is up, and a
10
+ * round trip says whether the server is answering, which is the question.
11
+ */
12
+ export declare const redisProbe: (redis: NonNullable<DashboardOptions['redis']>) => DashboardProbe;
13
+ export declare const runtimeReport: (options: DashboardOptions, startedAt: number) => Promise<RuntimeReport>;
@@ -0,0 +1,13 @@
1
+ import { type ModuleRef } from '@dunx/core';
2
+ import type { DashboardOptions } from '../options.js';
3
+ import type { ConfigEntry, Meta, Snapshot } from './types.js';
4
+ /**
5
+ * Keys, types, and a value only where the app's `reveal` predicate said so.
6
+ *
7
+ * Sorted, because a config panel is read by scanning for a key rather than in
8
+ * declaration order, and `validate` returns whatever object literal order the app
9
+ * happened to write.
10
+ */
11
+ export declare const configEntries: (values: object, reveal: DashboardOptions['reveal']) => readonly ConfigEntry[];
12
+ export declare const metaOf: (options: DashboardOptions) => Meta;
13
+ export declare const snapshotOf: (root: ModuleRef, options: DashboardOptions) => Snapshot;
@@ -0,0 +1,110 @@
1
+ import type { ModuleNode, ProviderNode } from '@dunx/core';
2
+ import type { GatewayNode, RouteNode } from '@dunx/http';
3
+ import type { ProbeState } from '../contracts.js';
4
+ /**
5
+ * Everything the page reads, declared once.
6
+ *
7
+ * `internal/dashboard-ui` imports these by relative path from this file, exactly
8
+ * as `internal/openapi-ui` imports `PageModel` from `@dunx/openapi`'s source - so
9
+ * the wire format has one declaration and the frontend cannot drift from the
10
+ * handler that fills it. The node types are re-exported rather than restated for
11
+ * the same reason: they are `@dunx/core`'s and `@dunx/http`'s, and a copy shaped
12
+ * for the browser would be a second thing to keep in step.
13
+ */
14
+ export type { GatewayNode, ModuleNode, ProbeState, ProviderNode, RouteNode };
15
+ /** Split by lifetime, which is also the split by endpoint. */
16
+ export interface Meta {
17
+ readonly title: string;
18
+ /** The mount, so the bundle can build its own URLs without guessing. */
19
+ readonly basePath: string;
20
+ /** Where `@dunx/openapi` serves its explorer, if the app said. */
21
+ readonly openApiPath: string | undefined;
22
+ /** 0 disables polling. */
23
+ readonly pollMs: number;
24
+ /**
25
+ * Where bull-board is mounted, always `{basePath}/queues`. Carried rather than
26
+ * derived so the page never builds a URL the server did not agree to.
27
+ */
28
+ readonly queuesPath: string;
29
+ }
30
+ export interface ConfigEntry {
31
+ readonly key: string;
32
+ /** `typeof`, or `array`/`null`, so a shape is visible without the value. */
33
+ readonly type: string;
34
+ /**
35
+ * Present only when the app's `reveal` predicate said so. Absent means
36
+ * redacted - there is no sentinel string, because a sentinel is indistinguishable
37
+ * from a value that happens to be `'***'`.
38
+ */
39
+ readonly value?: unknown;
40
+ }
41
+ /**
42
+ * The half that cannot change while the process runs. One request, cached by the
43
+ * page for its lifetime.
44
+ */
45
+ export interface Snapshot {
46
+ readonly meta: Meta;
47
+ readonly routes: readonly RouteNode[];
48
+ readonly gateways: readonly GatewayNode[];
49
+ readonly modules: readonly ModuleNode[];
50
+ readonly providers: readonly ProviderNode[];
51
+ /**
52
+ * Absent when no `ConfigService` is bound, which is a different fact from an
53
+ * empty config and is shown as one.
54
+ */
55
+ readonly config: readonly ConfigEntry[] | undefined;
56
+ }
57
+ export interface ProbeReport {
58
+ readonly name: string;
59
+ readonly state: ProbeState;
60
+ readonly detail?: string;
61
+ /** How long the probe took, so a slow dependency is visible before it fails. */
62
+ readonly ms: number;
63
+ }
64
+ export interface MemoryReport {
65
+ readonly rss: number;
66
+ readonly heapUsed: number;
67
+ readonly heapTotal: number;
68
+ readonly external: number;
69
+ }
70
+ /** The half that changes. Polled. */
71
+ export interface RuntimeReport {
72
+ readonly pid: number;
73
+ readonly uptimeMs: number;
74
+ readonly bun: string;
75
+ readonly platform: string;
76
+ readonly arch: string;
77
+ readonly memory: MemoryReport;
78
+ readonly probes: readonly ProbeReport[];
79
+ /** Server clock, so the page can show ages rather than raw timestamps. */
80
+ readonly now: number;
81
+ }
82
+ export interface RedisReport {
83
+ readonly configured: true;
84
+ readonly connected: boolean;
85
+ readonly pingMs: number | undefined;
86
+ /**
87
+ * A curated handful from `INFO`: version, mode, uptime, connected clients, used
88
+ * memory, keyspace hits and misses. Not the whole blob - that is 200 lines and it
89
+ * is one `redis-cli INFO` away for anyone who wants it.
90
+ */
91
+ readonly info: Readonly<Record<string, string>>;
92
+ readonly error?: string;
93
+ }
94
+ /** No `redis` handle was passed, which is different from a broker being down. */
95
+ export interface RedisAbsent {
96
+ readonly configured: false;
97
+ }
98
+ /**
99
+ * What the queues endpoint answers: **names, and nothing else.**
100
+ *
101
+ * Everything *about* a queue - counts, jobs, retries, flows, metrics - is
102
+ * bull-board's, mounted at `{path}/queues`. dunx renders no queue UI, so this
103
+ * exists only so the page knows whether to offer the link and what to say when
104
+ * there is nothing behind it.
105
+ */
106
+ export interface QueuesReport {
107
+ readonly queues: readonly string[];
108
+ /** Why there is no board: no source, no queues opened, or bull-board absent. */
109
+ readonly unavailable?: string;
110
+ }
@@ -0,0 +1,42 @@
1
+ import type { DashboardOptions } from './options.js';
2
+ /** The Bun route table `BunAdapter.getRoutes()` produces. */
3
+ export type BoardRoutes = Record<string, Record<string, (req: Request) => Response | Promise<Response>>>;
4
+ export interface Board {
5
+ /** Why there is no board, for the page to show. Absent when there is one. */
6
+ readonly unavailable?: string;
7
+ readonly routes?: BoardRoutes;
8
+ /** The entry route, so an unmatched client-side path serves the page. */
9
+ readonly entry?: (req: Request) => Response | Promise<Response>;
10
+ /** The queue names the board was built with, for the nav. */
11
+ readonly queues: readonly string[];
12
+ }
13
+ /**
14
+ * bull-board wants **queue objects**, not names, and `QueueSource.queue(name)`
15
+ * opens one - so this is the one place the dashboard touches a broker. It happens
16
+ * on the first request for the board and is memoised by the caller, not at boot: an
17
+ * app that mounts the dashboard and never opens the queues page must not hold a
18
+ * socket for it.
19
+ */
20
+ /**
21
+ * The names alone, answerable **without opening anything**.
22
+ *
23
+ * That separation is the whole reason `/_dunx/api/queues` and `/_dunx/queues` are
24
+ * different endpoints: the page asks this one on every poll to decide whether to
25
+ * offer the link, and it must not open a socket to answer. Only somebody actually
26
+ * opening the board does that.
27
+ */
28
+ export declare const boardNames: (options: DashboardOptions) => {
29
+ readonly names: readonly string[];
30
+ readonly unavailable?: string;
31
+ };
32
+ export declare const buildBoard: (options: DashboardOptions, basePath: string, favicon: string) => Promise<Board>;
33
+ /**
34
+ * Dispatches against bull-board's **own** route table - an exact path match, plus
35
+ * the one `/*` prefix its static assets are served under.
36
+ *
37
+ * This is not the JavaScript router dunx bans. It never sees an app's routes: Bun
38
+ * still matches everything real, this runs only inside the dashboard mount, and the
39
+ * table it walks is the one bull-board handed over. Twenty lines here is the price
40
+ * of not asking `HttpFactory` to accept a foreign route table at boot.
41
+ */
42
+ export declare const matchBoard: (routes: BoardRoutes, method: string, pathname: string) => ((req: Request) => Response | Promise<Response>) | undefined;
@@ -0,0 +1,107 @@
1
+ // @bun
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __name = (target, name) => {
6
+ Object.defineProperty(target, "name", {
7
+ value: name,
8
+ enumerable: false,
9
+ configurable: true
10
+ });
11
+ return target;
12
+ };
13
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
14
+ var __typeError = (msg) => {
15
+ throw TypeError(msg);
16
+ };
17
+ var __defNormalProp = (obj, key, value) => (key in obj) ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
18
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
19
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
20
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
21
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
22
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
23
+ var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
24
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
25
+ var __expectFn = (fn) => fn !== undefined && typeof fn !== "function" ? __typeError("Function expected") : fn;
26
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({
27
+ kind: __decoratorStrings[kind],
28
+ name,
29
+ metadata,
30
+ addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null))
31
+ });
32
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
33
+ var __runInitializers = (array, flags, self, value) => {
34
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length;i < n; i++)
35
+ flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
36
+ return value;
37
+ };
38
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
39
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
40
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
41
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
42
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : {
43
+ get [name]() {
44
+ return __privateGet(this, extra);
45
+ },
46
+ set [name](x) {
47
+ __privateSet(this, extra, x);
48
+ }
49
+ }, name));
50
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
51
+ for (var i = decorators.length - 1;i >= 0; i--) {
52
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
53
+ if (k) {
54
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => (name in x) };
55
+ if (k ^ 3)
56
+ access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
57
+ if (k > 2)
58
+ access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
59
+ }
60
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? undefined : { get: desc.get, set: desc.set } : target, ctx);
61
+ done._ = 1;
62
+ if (k ^ 4 || it === undefined)
63
+ __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
64
+ else if (typeof it !== "object" || it === null)
65
+ __typeError("Object expected");
66
+ else
67
+ __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
68
+ }
69
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
70
+ };
71
+ var __require = import.meta.require;
72
+
73
+ // src/api/snapshot.ts
74
+ import { modulesOf, providersOf } from "@dunx/core";
75
+ import { gatewaysOf, isGateway, routesOf } from "@dunx/http";
76
+ var typeOf = (value) => {
77
+ if (value === null)
78
+ return "null";
79
+ if (Array.isArray(value))
80
+ return "array";
81
+ return typeof value;
82
+ };
83
+ var configEntries = (values, reveal) => Object.entries(values).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => ({
84
+ key,
85
+ type: typeOf(value),
86
+ ...reveal(key, value) ? { value } : {}
87
+ }));
88
+ var metaOf = (options) => ({
89
+ title: options.title,
90
+ basePath: options.path,
91
+ openApiPath: options.openApiPath,
92
+ pollMs: options.pollMs,
93
+ queuesPath: `${options.path}/queues`
94
+ });
95
+ var snapshotOf = (root, options) => ({
96
+ meta: metaOf(options),
97
+ routes: routesOf(root),
98
+ gateways: gatewaysOf(root),
99
+ modules: modulesOf(root, { isGateway }),
100
+ providers: providersOf(root, { isGateway }),
101
+ config: options.config === undefined ? undefined : configEntries(options.config.values, options.reveal)
102
+ });
103
+
104
+ export { __decoratorStart, __decoratorMetadata, __runInitializers, __decorateElement, __require, metaOf, snapshotOf };
105
+
106
+ //# debugId=1DCCAFB7DE81D21D64756E2164756E21
107
+ //# sourceMappingURL=chunk-xg5554k6.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/api/snapshot.ts"],
4
+ "sourcesContent": [
5
+ "import { modulesOf, providersOf, type ModuleRef } from '@dunx/core';\nimport { gatewaysOf, isGateway, routesOf } from '@dunx/http';\nimport type { DashboardOptions } from '../options.js';\nimport type { ConfigEntry, Meta, Snapshot } from './types.js';\n\n/**\n * The static half of the page, built from the same readers `@dunx/mcp` answers\n * with. Nothing here constructs anything: `providersOf` and `routesOf` walk\n * prototypes, so this would answer identically before the app booted.\n *\n * That is the deliberate inversion of MCP's rule. MCP refuses runtime questions\n * because booting an app to answer them would open databases and bind sockets;\n * this package is *already inside* a booted app, so the reason does not apply and\n * the live panels ask the container directly. Splitting it per panel rather than\n * per package is what stops this file growing a `boot()`.\n */\n\nconst typeOf = (value: unknown): string => {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n return typeof value;\n};\n\n/**\n * Keys, types, and a value only where the app's `reveal` predicate said so.\n *\n * Sorted, because a config panel is read by scanning for a key rather than in\n * declaration order, and `validate` returns whatever object literal order the app\n * happened to write.\n */\nexport const configEntries = (\n values: object,\n reveal: DashboardOptions['reveal'],\n): readonly ConfigEntry[] =>\n Object.entries(values)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]: [string, unknown]) => ({\n key,\n type: typeOf(value),\n ...(reveal(key, value) ? { value } : {}),\n }));\n\nexport const metaOf = (options: DashboardOptions): Meta => ({\n title: options.title,\n basePath: options.path,\n openApiPath: options.openApiPath,\n pollMs: options.pollMs,\n queuesPath: `${options.path}/queues`,\n});\n\nexport const snapshotOf = (\n root: ModuleRef,\n options: DashboardOptions,\n): Snapshot => ({\n meta: metaOf(options),\n routes: routesOf(root),\n gateways: gatewaysOf(root),\n // Core cannot import `@dunx/http`, so the gateway marker arrives as an option.\n // Without it a gateway would be listed as an ordinary provider here while the\n // gateways panel showed it as one, and the two panels would disagree.\n modules: modulesOf(root, { isGateway }),\n providers: providersOf(root, { isGateway }),\n // Absent when the app passed no `config`, which the panel reports as such - a\n // different fact from a configuration with no keys in it.\n config:\n options.config === undefined\n ? undefined\n : configEntries(options.config.values, options.reveal),\n});\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AACA;AAgBA,IAAM,SAAS,CAAC,UAA2B;AAAA,EACzC,IAAI,UAAU;AAAA,IAAM,OAAO;AAAA,EAC3B,IAAI,MAAM,QAAQ,KAAK;AAAA,IAAG,OAAO;AAAA,EACjC,OAAO,OAAO;AAAA;AAUT,IAAM,gBAAgB,CAC3B,QACA,WAEA,OAAO,QAAQ,MAAM,EAClB,KAAK,EAAE,KAAK,OAAO,EAAE,cAAc,CAAC,CAAC,EACrC,IAAI,EAAE,KAAK,YAA+B;AAAA,EACzC;AAAA,EACA,MAAM,OAAO,KAAK;AAAA,KACd,OAAO,KAAK,KAAK,IAAI,EAAE,MAAM,IAAI,CAAC;AACxC,EAAE;AAEC,IAAM,SAAS,CAAC,aAAqC;AAAA,EAC1D,OAAO,QAAQ;AAAA,EACf,UAAU,QAAQ;AAAA,EAClB,aAAa,QAAQ;AAAA,EACrB,QAAQ,QAAQ;AAAA,EAChB,YAAY,GAAG,QAAQ;AACzB;AAEO,IAAM,aAAa,CACxB,MACA,aACc;AAAA,EACd,MAAM,OAAO,OAAO;AAAA,EACpB,QAAQ,SAAS,IAAI;AAAA,EACrB,UAAU,WAAW,IAAI;AAAA,EAIzB,SAAS,UAAU,MAAM,EAAE,UAAU,CAAC;AAAA,EACtC,WAAW,YAAY,MAAM,EAAE,UAAU,CAAC;AAAA,EAG1C,QACE,QAAQ,WAAW,YACf,YACA,cAAc,QAAQ,OAAO,QAAQ,QAAQ,MAAM;AAC3D;",
8
+ "debugId": "1DCCAFB7DE81D21D64756E2164756E21",
9
+ "names": []
10
+ }