@dunx/dashboard 2.4.0 → 3.0.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/README.md CHANGED
@@ -1,242 +1,79 @@
1
1
  # @dunx/dashboard
2
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`.
3
+ One page over a running [dunx](https://github.com/petarzarkov/dunx) app: the
4
+ routes it serves, the container it built, the gateways it upgrades, Redis, the
5
+ config keys and the process itself, **with bull-board mounted for the queues**.
6
+
7
+ Every panel reads data dunx already computes, so the page is cheap.
8
+
9
+ ## Install
6
10
 
7
11
  ```bash
8
12
  bun add @dunx/dashboard
13
+ # for the queues page
14
+ bun add @bull-board/api @bull-board/ui @bull-board/bun
9
15
  ```
10
16
 
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
17
+ ## Usage
18
18
 
19
19
  ```ts
20
20
  import { DashboardMiddleware, DashboardModule } from '@dunx/dashboard';
21
- import { JobPublisher, QueueModule } from '@dunx/infra/queue';
22
- import { RedisConnection, RedisModule } from '@dunx/infra/redis';
21
+ import { JobPublisher } from '@dunx/infra/queue';
22
+ import { RedisConnection } from '@dunx/infra/redis';
23
23
 
24
24
  @Module({
25
25
  imports: [
26
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],
27
+ imports: [JobsModule, CacheModule],
28
+ inject: [JobPublisher, RedisConnection],
30
29
  useFactory: (queues: JobPublisher, redis: RedisConnection) => ({
30
+ path: '/api/_dunx',
31
31
  queues,
32
32
  redis,
33
- authorize: (req) => req.headers.get('x-ops-key') === process.env.OPS_KEY,
33
+ authorize: (req) => isOperator(req),
34
34
  }),
35
- inject: [JobPublisher, RedisConnection] as const,
36
35
  }),
37
36
  ],
38
37
  })
39
- export class AppModule {}
40
- ```
38
+ export class OpsModule {}
41
39
 
42
- ```ts
43
40
  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 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 rather than 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
135
- under the framework's first rule: never invent what a mature library already
136
- solves.
137
-
138
- The one thing that had ever justified hand-rolling it was that mounting
139
- bull-board on `Bun.serve` meant writing a server adapter - which the deleted
140
- `@dunx/queue-dashboard` did, and which was a liability. **bull-board 8.6.0
141
- ships `@bull-board/bun`**, so that reason is gone and the integration is three
142
- calls.
143
-
144
- It also settled the question that started all this - see **Workers do show up**
145
- below. The hand-rolled panel had planned to omit a worker column and explain why;
146
- mounting bull-board asked the question honestly and exposed the real cause.
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: **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'],
41
+ app.use(DashboardMiddleware); // first, ahead of any session guard
176
42
  ```
177
43
 
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
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:
44
+ ## The panels
206
45
 
207
- ```ts
208
- path: '/api/_dunx',
209
- ```
46
+ Six panels, each with a JSON sibling under `{path}/api/*`, so `curl` is a real
47
+ way to read this on a box with no browser. The queues page is bull-board's.
210
48
 
211
- ## Probes
49
+ | Panel | Shows |
50
+ | --------- | ------------------------------------------------------------ |
51
+ | Routes | Every route, its guards and metadata, linked to `/docs` |
52
+ | Providers | The container graph, by module |
53
+ | Gateways | Every WebSocket gateway and its events |
54
+ | Redis | Connection state and `INFO` |
55
+ | Config | Keys always, values only where `reveal` says so |
56
+ | Runtime | The process, its memory and its probes |
212
57
 
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.
58
+ ## Three things that are decisions
217
59
 
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
- ```
60
+ - **`authorize` has no default**, and leaving it out serves the page to anyone
61
+ who can reach the port. Omitting it logs a warning naming the mount at boot.
62
+ - **A rejected request gets 404, not 403.** Register the middleware **ahead of
63
+ any session guard**: a guard running first answers 401 and tells a prober the
64
+ mount exists. `authorize` takes the raw `Request` so it can be self-sufficient.
65
+ - **Config values are redacted by default.** `reveal` is an opt-in allow-list; a
66
+ deny-list of the usual suspects leaks the first key nobody thought of.
229
67
 
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.
68
+ ## Notes
232
69
 
233
- ## The page
70
+ - It depends on `@dunx/infra` and `bullmq` not at all. `QueueSource` and
71
+ `RedisProbe` restate structurally what `JobPublisher` and `RedisConnection`
72
+ already are, so `queues: publisher` is the whole wiring.
73
+ - The board is built on the first request for the queues page, never at boot, so
74
+ an app that never opens it holds no broker socket.
75
+ - `commands: false` maps onto bull-board's own `readOnlyMode`.
234
76
 
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.
77
+ ## License
239
78
 
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`.
79
+ MIT
@@ -1,14 +1,10 @@
1
1
  /**
2
- * Every read the dashboard makes off-process is bounded, and this is the one
3
- * implementation of that.
2
+ * Every read the dashboard makes off-process is bounded here. With Redis
3
+ * unreachable, `getJobCounts` waits out the 5 s connection timeout, so opening the
4
+ * dashboard on a broken broker hung the page for as long as the thing you opened
5
+ * it to look at was broken.
4
6
  *
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.
7
+ * The fallback is a value rather than a rejection: an unreachable queue still gets
8
+ * a row saying so.
13
9
  */
14
10
  export declare const bounded: <T>(work: () => Promise<T>, ms: number, onTimeout: () => T) => Promise<T>;
@@ -1,5 +1,5 @@
1
1
  import type { ModuleNode, ProviderNode } from '@dunx/core';
2
- import type { GatewayNode, RouteNode } from '@dunx/http';
2
+ import type { GatewayNode, RouteNode } from '@dunx/http/internal';
3
3
  import type { ProbeState } from '../contracts.js';
4
4
  /**
5
5
  * Everything the page reads, declared once.
package/dist/board.d.ts CHANGED
@@ -18,12 +18,9 @@ export interface Board {
18
18
  * socket for it.
19
19
  */
20
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.
21
+ * The names alone, answerable without opening anything - which is why
22
+ * `/_dunx/api/queues` and `/_dunx/queues` are different endpoints. The page polls
23
+ * this one to decide whether to offer the link, and must not open a socket for it.
27
24
  */
28
25
  export declare const boardNames: (options: DashboardOptions) => {
29
26
  readonly names: readonly string[];
@@ -72,7 +72,7 @@ var __require = import.meta.require;
72
72
 
73
73
  // src/api/snapshot.ts
74
74
  import { modulesOf, providersOf } from "@dunx/core";
75
- import { gatewaysOf, isGateway, routesOf } from "@dunx/http";
75
+ import { gatewaysOf, isGateway, routesOf } from "@dunx/http/internal";
76
76
  var typeOf = (value) => {
77
77
  if (value === null)
78
78
  return "null";
@@ -102,6 +102,3 @@ var snapshotOf = (root, options) => ({
102
102
  });
103
103
 
104
104
  export { __decoratorStart, __decoratorMetadata, __runInitializers, __decorateElement, __require, metaOf, snapshotOf };
105
-
106
- //# debugId=D21070FBD0B8DA7064756E2164756E21
107
- //# sourceMappingURL=chunk-xps6r8jv.js.map
@@ -1,74 +1,38 @@
1
1
  /**
2
- * What the dashboard needs from the things it reports on, restated structurally.
3
- *
4
- * **This package depends on `@dunx/infra` not at all**, and on `bullmq` not at
5
- * all - the same choice `@dunx/auth` makes with `DrizzleSource` and `RedisStore`.
6
- * A dashboard that peer-depended on the queue library would oblige an app with no
7
- * queues to install it to see its routes, and would put a build-order edge between
8
- * two packages that never call each other.
9
- *
10
- * This list used to be twice as long. `DashboardQueue` and `DashboardJob` restated
11
- * bullmq's `Queue` and `Job` in enough detail to drive a queue table - signatures
12
- * shaped to satisfy bullmq's own variance, with a paragraph explaining why. All of
13
- * it went when bull-board took the queue UI back: the queue object is now passed
14
- * straight through to `BullMQAdapter`, so there is nothing left to describe.
15
- *
16
- * Everything here is satisfied by an object an app already has:
17
- *
18
- * | This | Satisfied by |
19
- * | ---------------- | ---------------------------------------- |
20
- * | `QueueSource` | `JobPublisher` from `@dunx/infra/queue` |
21
- * | `RedisProbe` | `RedisConnection` from `@dunx/infra/redis` |
22
- * | `ConfigValues` | `ConfigService` from `@dunx/core` |
23
- *
24
- * No adapter, no wrapper - `queues: publisher` in the options is the whole wiring,
25
- * which is what makes the restatement worth its lines rather than a tax.
2
+ * What the dashboard needs from the things it reports on, restated structurally
3
+ * so this package depends on `@dunx/infra` and `bullmq` not at all. Each is
4
+ * satisfied by an object an app already has:
5
+ *
6
+ * | This | Satisfied by |
7
+ * | -------------- | ------------------------------------------ |
8
+ * | `QueueSource` | `JobPublisher` from `@dunx/infra/queue` |
9
+ * | `RedisProbe` | `RedisConnection` from `@dunx/infra/redis` |
10
+ * | `ConfigValues` | `ConfigService` from `@dunx/core` |
26
11
  */
27
12
  /**
28
- * The validated configuration. `ConfigService` satisfies it as written.
29
- *
30
- * Passed in rather than resolved from the container, and that is deliberate twice
31
- * over. Mechanically, `inject()` only works inside a class the container builds and
32
- * this middleware is built by a factory. But the better reason is that showing an
33
- * app's configuration should be something the app **says yes to** - the same
34
- * instinct behind `reveal` defaulting to revealing nothing.
13
+ * The validated configuration. `ConfigService` satisfies it as written. Passed in
14
+ * rather than resolved, so showing an app's configuration is something the app
15
+ * says yes to.
35
16
  */
36
17
  export interface ConfigValues {
37
- /**
38
- * `object`, not `Record<string, unknown>`: an app's `AppConfig` is an interface
39
- * with no index signature, so the record type would reject the very
40
- * `ConfigService` this exists to accept. It is enumerated, never read by a key
41
- * this package knows.
42
- */
18
+ /** `object` rather than `Record<string, unknown>`: an app's `AppConfig` has no
19
+ * index signature, so the record type would reject it. */
43
20
  readonly values: object;
44
21
  }
45
22
  /**
46
- * Where queues come from. `JobPublisher` satisfies it as written.
47
- *
48
- * `opened` is what the publisher has opened *so far*, which is deliberately not the
49
- * same as "every queue this app has" - a queue is a key prefix opened on first use,
50
- * so a web process that has published to none has opened none. That is why
51
- * `DashboardOptions.queueNames` exists: a process that consumes a queue it never
52
- * publishes to has to name it, and the panel says which of the two it is showing.
23
+ * Where queues come from. `JobPublisher` satisfies it as written. `opened` is what
24
+ * the publisher has opened so far, not every queue the app has - which is why
25
+ * `DashboardOptions.queueNames` exists for a consume-only process.
53
26
  */
54
27
  export interface QueueSource {
55
28
  readonly opened: readonly string[];
56
- /**
57
- * bullmq's `Queue`, handed to bull-board's `BullMQAdapter` untouched - which is
58
- * why the return type is `unknown` rather than a restatement. dunx reads nothing
59
- * off it and calls nothing on it; matching bullmq's own signatures here was a
60
- * whole file of variance notes existing only to describe a UI dunx no longer
61
- * renders.
62
- */
29
+ /** bullmq's `Queue`, handed to `BullMQAdapter` untouched, so the return type is
30
+ * `unknown` rather than a restatement. */
63
31
  queue(name: string): unknown;
64
32
  }
65
33
  /**
66
- * Enough Redis to answer "is it up and what is it doing". `RedisConnection`
67
- * satisfies it, and so does `Bun.RedisClient` with a `send`.
68
- *
69
- * `send` rather than a typed `info()`: `INFO` is one command whose reply is a text
70
- * blob, and adding a method per Redis command to a restatement is how a
71
- * restatement becomes a client library.
34
+ * Enough Redis to answer "is it up and what is it doing". `send` rather than a
35
+ * typed `info()`: a method per command is how a restatement becomes a client.
72
36
  */
73
37
  export interface RedisProbe {
74
38
  readonly connected: boolean;
@@ -76,25 +40,15 @@ export interface RedisProbe {
76
40
  send(command: string, args?: readonly string[]): Promise<unknown>;
77
41
  }
78
42
  /**
79
- * The state a probe reports, and its result. `unknown` is not `down`; see
80
- * `StatusDot`.
81
- *
82
- * Declared in `@dunx/http` and re-exported here. They moved down when the health
83
- * module became a second consumer: this package already peer-depends on
84
- * `@dunx/http`, so the descent adds no edge, and two copies of a three-value union
85
- * is how one of them gains a fourth value nobody else honours.
86
- *
87
- * `RedisProbe` below did **not** move with them. `@dunx/http`'s `PingProbe` is a
88
- * `ping` and nothing else, while this needs `connected` and `send` for the `INFO`
89
- * panel, so one shared contract would oblige a health check to supply two members
90
- * it never calls.
43
+ * The state a probe reports, and its result. `unknown` is not `down`. Declared in
44
+ * `@dunx/http` and re-exported here, since this package already peer-depends on
45
+ * it. `RedisProbe` below did not move with them: `PingProbe` is a `ping` alone.
91
46
  */
92
47
  export type { ProbeResult, ProbeState } from '@dunx/http';
93
48
  import type { ProbeResult } from '@dunx/http';
94
49
  /**
95
- * Anything else worth a light on the page - a third-party API, a disk, a leader
96
- * election. The dashboard awaits it with a timeout and never lets it throw into a
97
- * response, so a probe that hangs costs one panel rather than the page.
50
+ * Anything else worth a light on the page. Awaited with a timeout and never let
51
+ * to throw, so a probe that hangs costs one panel rather than the page.
98
52
  */
99
53
  export interface DashboardProbe {
100
54
  readonly name: string;
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  __require,
7
7
  __runInitializers,
8
8
  snapshotOf
9
- } from "./chunk-xps6r8jv.js";
9
+ } from "./chunk-66xm1dzm.js";
10
10
 
11
11
  // src/module.ts
12
12
  import {
@@ -167,9 +167,7 @@ class DashboardOptions {
167
167
  this.commands = init.commands ?? true;
168
168
  }
169
169
  }
170
- Object.defineProperty(DashboardOptions, Symbol.for("dunx.deps"), {
171
- value: () => [{ unresolved: "init: DashboardOptionsInit = {}" }]
172
- });
170
+ Object.defineProperty(DashboardOptions, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "init: DashboardOptionsInit = {}", optional: true }] });
173
171
  var normalizeMount = (path) => {
174
172
  const trimmed = `/${path.split("/").filter(Boolean).join("/")}`;
175
173
  if (trimmed === "/") {
@@ -416,9 +414,7 @@ class DashboardMiddleware {
416
414
  return this.#page;
417
415
  }
418
416
  }
419
- Object.defineProperty(DashboardMiddleware, Symbol.for("dunx.deps"), {
420
- value: () => [DashboardOptions, { unresolved: "root: ModuleRef", typeOnly: "ModuleRef" }, Logger]
421
- });
417
+ Object.defineProperty(DashboardMiddleware, Symbol.for("dunx.deps"), { value: () => [DashboardOptions, { unresolved: "root: ModuleRef", typeOnly: "ModuleRef" }, Logger] });
422
418
 
423
419
  // src/module.ts
424
420
  var middleware = () => provide(DashboardMiddleware, {
@@ -470,6 +466,3 @@ export {
470
466
  handleDashboard,
471
467
  normalizeMount
472
468
  };
473
-
474
- //# debugId=CFA504A7E130188D64756E2164756E21
475
- //# sourceMappingURL=index.js.map
@@ -3,27 +3,16 @@ import type { Middleware, Next, RouteContext } from '@dunx/http';
3
3
  import type { BunRequest } from 'bun';
4
4
  import { DashboardOptions } from './options.js';
5
5
  /**
6
- * A **global middleware**, not a controller, and that is not a stylistic choice.
6
+ * A global middleware rather than a controller: `app.use` runs in front of the
7
+ * unmatched-path fallback, which is where the dashboard's paths land because the
8
+ * app declares none of them.
7
9
  *
8
- * Middleware registered with `app.use` runs in front of the unmatched-path
9
- * fallback, which is exactly where the dashboard's paths land because the app
10
- * declares none of them. Declaring them as dunx routes would mean generating
11
- * controllers for a route table handed over at runtime, and every panel would need
12
- * its own `@Get`.
10
+ * Register it ahead of any session guard - with this last in the chain a guard
11
+ * answers `401` before `authorize` runs, defeating the 404 contract. That works
12
+ * only because `authorize` gets the raw `Request`, so keep it self-sufficient.
13
+ * Anything outside the mount falls through untouched.
13
14
  *
14
- * Two consequences of that, both learned the hard way and both worth keeping:
15
- *
16
- * - **Register it ahead of any session guard.** Measured in `dunx-template`: with
17
- * this last in the chain, `SessionGuard` answered every dashboard request `401`
18
- * before `authorize` ran, which defeats the 404 contract entirely. That works
19
- * only because `authorize` gets the raw `Request` and can ask the auth library
20
- * itself, so keep it self-sufficient.
21
- * - **Anything outside the mount falls through untouched**, so the app's own
22
- * routes and its 404 behave exactly as before.
23
- *
24
- * The page bundle is built on the **first request** and memoised on the promise,
25
- * so two concurrent first requests build one page and importing this package pulls
26
- * in none of its 400-odd KB.
15
+ * The page bundle is built on the first request and memoised on the promise.
27
16
  */
28
17
  export declare class DashboardMiddleware implements Middleware {
29
18
  #private;
package/dist/module.d.ts CHANGED
@@ -1,17 +1,12 @@
1
1
  import { type AsyncModuleConfig, type Deps, type DynamicModule } from '@dunx/core';
2
2
  import { type DashboardOptionsInit } from './options.js';
3
3
  /**
4
- * Binds the options and the middleware. **It does not register the middleware** -
5
- * the app does, with `app.use(DashboardMiddleware)`, and that is deliberate:
6
- * position in the chain is the whole security property here.
4
+ * Binds the options and the middleware, and does not register it - the app does,
5
+ * because position in the chain is the security property here.
7
6
  *
8
- * `@Module({ middleware })` scopes middleware to that module's own controllers, of
9
- * which this module has none, so it would never run. Registering globally from
10
- * inside the module would put it wherever the module happened to be imported, and
11
- * it has to be **ahead of any session guard** - measured in `dunx-template`, where a
12
- * guard running first answered every dashboard request `401` before `authorize`
13
- * ran, defeating the 404 contract. Two lines in the app, one of which is a
14
- * decision:
7
+ * `@Module({ middleware })` scopes to that module's own controllers, of which this
8
+ * has none. Registering globally from inside would put it wherever the module was
9
+ * imported, and it has to sit ahead of any session guard:
15
10
  *
16
11
  * ```ts
17
12
  * const app = await HttpFactory.create(AppModule);