@adonis-agora/media 0.10.4 → 0.12.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +105 -11
  3. package/dist/assets/spa/assets/index--0MZJr6W.css +1 -0
  4. package/dist/assets/spa/assets/index-Bo658_9z.js +83 -0
  5. package/dist/assets/spa/assets/index-Bo658_9z.js.map +1 -0
  6. package/dist/assets/spa/index.html +20 -0
  7. package/dist/configure.d.ts +6 -2
  8. package/dist/configure.d.ts.map +1 -1
  9. package/dist/configure.js +8 -2
  10. package/dist/configure.js.map +1 -1
  11. package/dist/providers/dashboard_provider.d.ts +29 -0
  12. package/dist/providers/dashboard_provider.d.ts.map +1 -0
  13. package/dist/providers/dashboard_provider.js +520 -0
  14. package/dist/providers/dashboard_provider.js.map +1 -0
  15. package/dist/providers/media_provider.d.ts.map +1 -1
  16. package/dist/providers/media_provider.js +34 -2
  17. package/dist/providers/media_provider.js.map +1 -1
  18. package/dist/src/dashboard/auth.d.ts +76 -0
  19. package/dist/src/dashboard/auth.d.ts.map +1 -0
  20. package/dist/src/dashboard/auth.js +134 -0
  21. package/dist/src/dashboard/auth.js.map +1 -0
  22. package/dist/src/dashboard/cookie.d.ts +20 -0
  23. package/dist/src/dashboard/cookie.d.ts.map +1 -0
  24. package/dist/src/dashboard/cookie.js +44 -0
  25. package/dist/src/dashboard/cookie.js.map +1 -0
  26. package/dist/src/dashboard/define_config.d.ts +56 -0
  27. package/dist/src/dashboard/define_config.d.ts.map +1 -0
  28. package/dist/src/dashboard/define_config.js +5 -0
  29. package/dist/src/dashboard/define_config.js.map +1 -0
  30. package/dist/src/dashboard/index.d.ts +10 -0
  31. package/dist/src/dashboard/index.d.ts.map +1 -0
  32. package/dist/src/dashboard/index.js +9 -0
  33. package/dist/src/dashboard/index.js.map +1 -0
  34. package/dist/src/dashboard/object_insights.d.ts +42 -0
  35. package/dist/src/dashboard/object_insights.d.ts.map +1 -0
  36. package/dist/src/dashboard/object_insights.js +42 -0
  37. package/dist/src/dashboard/object_insights.js.map +1 -0
  38. package/dist/src/dashboard/service.d.ts +215 -0
  39. package/dist/src/dashboard/service.d.ts.map +1 -0
  40. package/dist/src/dashboard/service.js +470 -0
  41. package/dist/src/dashboard/service.js.map +1 -0
  42. package/dist/src/dashboard/spa.d.ts +29 -0
  43. package/dist/src/dashboard/spa.d.ts.map +1 -0
  44. package/dist/src/dashboard/spa.js +47 -0
  45. package/dist/src/dashboard/spa.js.map +1 -0
  46. package/dist/src/dashboard/types.d.ts +201 -0
  47. package/dist/src/dashboard/types.d.ts.map +1 -0
  48. package/dist/src/dashboard/types.js +10 -0
  49. package/dist/src/dashboard/types.js.map +1 -0
  50. package/dist/stubs/config/media_dashboard.stub +39 -0
  51. package/package.json +16 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # @adonis-agora/media
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#46](https://github.com/DavideCarvalho/adonis-agora-media/pull/46) [`e2baf99`](https://github.com/DavideCarvalho/adonis-agora-media/commit/e2baf993dae1a7d3574128085abe1cf48087a023) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - **Behaviour change — omitting `disk` now really uses Drive's default disk.** Read this if your `config/media.ts` does not set `disk`.
8
+
9
+ The documented fallback ("omit `disk` and the provider uses the `default` of your `config/drive.ts`") had never worked in any environment. `MediaProvider` read it as `app.config.get('drive').default`, but `app.config.get('drive')` returns the value your config file exported — Drive's `defineConfig()` returns an **unresolved `ConfigProvider`** (`{ type: 'provider', resolver }`), so `.default` was always `undefined` and the fallback always returned the literal string `'default'`. (Reading `.config.default` instead does not help either: `{ config: { default, fakes, services } }` is the shape the provider's resolver _returns_, never the shape of the provider itself.) The provider now resolves the config provider — the same thing `@adonisjs/drive`'s own provider does before it builds its manager — and reads the default disk name off the resolved config. Drive's `DriveManager` cannot be asked instead: it keeps that config in a `#private` field and exposes no accessor.
10
+
11
+ **Am I affected?** Look at the `default` key of your `config/drive.ts`:
12
+
13
+ - `default: 'default'` (or you set `disk` in `config/media.ts`, or you have no Drive config at all) → nothing changes.
14
+ - `default` is anything else _and_ your `services` map has **no** disk named `default` → media was hard-broken for you (every disk operation threw inside flydrive, because it was asked for a service named `'default'` that does not exist). This release fixes it; there is nothing to migrate.
15
+ - `default` is anything else _and_ your `services` map **does** have a disk named `default` → **this moves where your files are written and read.** New objects now go to your real Drive default (e.g. `s3`) instead of the disk named `default`.
16
+
17
+ **Migration.** Objects already written under the `'default'` disk will not be found on the new disk — nothing copies them. Media records persist their own `disk`, so rows attached before this upgrade keep resolving to `'default'` and stay readable as long as that disk still exists in `config/drive.ts`; anything that recomputes a location from the _current_ default (or any object you wrote outside the media record store) will look on the new disk and miss. To keep the old behaviour exactly, pin it explicitly:
18
+
19
+ ```ts
20
+ // config/media.ts
21
+ export default defineConfig({
22
+ disk: "default", // was implicit before this release
23
+ });
24
+ ```
25
+
26
+ Otherwise, move the existing objects to your real default disk (or point that Drive service at the same bucket/location) before upgrading.
27
+
28
+ This is a minor, not a major: the package is pre-1.0, where minor is the breaking-change channel, and a `1.0.0` would signal an API stability this package has not declared. The affected surface is narrow (a host that both omits `disk` and keeps a Drive disk literally named `default` alongside a different Drive `default`), the alternative for most affected hosts was a crash, and pinning the old behaviour is a one-line config change.
29
+
30
+ ## 0.11.0
31
+
32
+ ### Minor Changes
33
+
34
+ - [`ad61387`](https://github.com/DavideCarvalho/adonis-agora-media/commit/ad61387aba714ba85ce6a6cf810b97259db46a93) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - The management console now ships **embedded** — `@adonis-agora/media/dashboard_provider` mounts the same React SPA + JSON API `@adonis-agora/media-dashboard` always has, straight from this package. `node ace configure @adonis-agora/media` registers it automatically (alongside publishing `config/media_dashboard.ts`); no separate `@adonis-agora/media-dashboard` install is required to get the console.
35
+
36
+ New subpath exports:
37
+
38
+ - `@adonis-agora/media/dashboard_provider` — the provider (routes: SPA + assets, JSON API, session auth). Reads the same `media_dashboard` config key `@adonis-agora/media-dashboard`'s provider always did, so an existing `config/media_dashboard.ts` keeps working unchanged if you switch to this provider.
39
+ - `@adonis-agora/media/dashboard` — `defineConfig`/`MediaDashboardConfig` for authoring `config/media_dashboard.ts`, `DashboardService`/`DashboardError`, the built-in session-auth helpers (`resolveConsoleAuth`, `signSessionCookie`, `verifySessionCookie`), `ObjectInsightProvider`/`sanitizeInsight`, and the dashboard's JSON API types.
40
+
41
+ `MediaDashboardConfig` also gains `enabled` (default `true`) — set `false` to register the provider without mounting any route.
42
+
43
+ At build time, `pnpm build` copies `@adonis-agora/media-dashboard`'s built SPA (`dist/spa`) into this package's own `dist/assets/spa`, mirroring `@adonis-agora/durable`'s embedded-dashboard pattern. `@adonis-agora/media-dashboard` remains published, standalone-installable, and its own provider (`@adonis-agora/media-dashboard/media_dashboard_provider`) still works — see that package's own changeset for what changed there. Register only one of the two dashboard providers in a given app.
44
+
3
45
  ## 0.10.4
4
46
 
5
47
  ### Patch Changes
package/README.md CHANGED
@@ -4,16 +4,20 @@ Media-library for **AdonisJS**, part of the [Agora](https://github.com/DavideCar
4
4
  spatie/laravel-media-library feel, on top of [`@adonisjs/drive`](https://github.com/adonisjs/drive).
5
5
 
6
6
  Attach files to entities, organize them into collections (MIME whitelist, single-file replace,
7
- ordering), and generate image conversions (eager or lazy). Storage is delegated entirely to Drive, so
8
- you reuse your existing `local` / `s3` / `gcs` disks — this package never reimplements disk drivers.
7
+ ordering), and generate image conversions (eager or lazy) — plus **content transformers**, the same
8
+ seam driven beyond images: the bundled `transformers.hls()` packages a video into an HLS ladder and
9
+ `transformers.probe()` records duration/codecs as metadata, both persisted into the same
10
+ `record.conversions` map. Storage is delegated entirely to Drive, so you reuse your existing
11
+ `local` / `s3` / `gcs` disks — this package never reimplements disk drivers.
9
12
 
10
13
  This is the **core library**: `MediaLibrary`, `AttachmentManager`, the `MediaStore` SPI (in-memory +
11
- Lucid), the `ImageProcessor` SPI (sharp), the bundled `disks.s3()` driver, proxy/direct/resumable
12
- uploads, configurable delivery, the provider + `defineConfig`, and a testing kit. Pair it with
14
+ Lucid), the `ImageProcessor` SPI (sharp), the `Transformer` SPI (HLS video packaging + metadata
15
+ probe), the bundled `disks.s3()` driver, proxy/direct/resumable uploads, configurable delivery, the
16
+ provider + `defineConfig`, and a testing kit. The management
17
+ **console ships embedded** — registering `@adonis-agora/media/dashboard_provider` is all a consuming
18
+ app needs (see [Dashboard](#dashboard) below); no separate install. Pair it with
13
19
  [`@adonis-agora/media-react`](https://github.com/DavideCarvalho/adonis-media/tree/master/packages/react)
14
- for a browser upload client, or
15
- [`@adonis-agora/media-dashboard`](https://github.com/DavideCarvalho/adonis-media/tree/master/packages/dashboard)
16
- for a management console.
20
+ for a browser upload client.
17
21
 
18
22
  ## Subpath exports
19
23
 
@@ -22,9 +26,12 @@ The package uses **subpath exports** (the Agora idiom), so heavy backends stay o
22
26
  | Subpath | What |
23
27
  |---|---|
24
28
  | `@adonis-agora/media` | barrel — `defineConfig`, `stores`, `processors`, `disks`, `uploadSessions`, `MediaManager`, `MediaLibrary`, `AttachmentManager`, `UploadManager`, `ResumableUploadManager`, `MediaDeliveryHandler`, SPIs, errors |
29
+ | `@adonis-agora/media/services/main` | the app-bound `MediaManager` singleton — `import media from '@adonis-agora/media/services/main'`, importable at any time (it resolves lazily on first use) |
25
30
  | `@adonis-agora/media/media_provider` | the service provider (binds `MediaManager`, mounts optional upload/TUS routes) |
31
+ | `@adonis-agora/media/dashboard_provider` | the embedded management-console provider (React SPA + JSON API) |
32
+ | `@adonis-agora/media/dashboard` | `defineConfig` + types for `config/media_dashboard.ts`, `DashboardService`, the session-auth helpers, `ObjectInsightProvider` |
26
33
  | `@adonis-agora/media/configure` | `node ace configure` hook |
27
- | `@adonis-agora/media/single-file` | `storeSingleFile` / `removeSingleFile` / `isSingleFileStoreAvailable` — lets other packages delegate single-file uploads (e.g. avatars) to media without a hard dependency |
34
+ | `@adonis-agora/media/single-file` | `storeSingleFile` / `removeSingleFile` / `storeSingleFileWith` / `removeSingleFileWith` / `isSingleFileStoreAvailable` — lets other packages delegate single-file uploads (e.g. avatars) to media without a hard dependency |
28
35
  | `@adonis-agora/media/stores/lucid` | the Lucid `MediaStore` (`@adonisjs/lucid` peer) |
29
36
  | `@adonis-agora/media/upload_sessions/lucid` | the Lucid resumable `UploadSessionStore` (`@adonisjs/lucid` peer) |
30
37
  | `@adonis-agora/media/disks/s3` | the bundled `S3Disk` (`@aws-sdk/client-s3` peer; presigning is hand-rolled SigV4) |
@@ -41,8 +48,14 @@ npm i @adonis-agora/media
41
48
  node ace configure @adonis-agora/media
42
49
  ```
43
50
 
44
- `configure` registers the provider, publishes `config/media.ts`, and publishes the `media` table
45
- migration (delete it if you only use the in-memory store; otherwise `node ace migration:run`).
51
+ `configure` registers the media provider **and the dashboard provider**, publishes `config/media.ts`
52
+ and `config/media_dashboard.ts`, and publishes **two** migrations — the `media` table for the `lucid`
53
+ store and the session table for the `lucid` upload-session store. Delete the ones you don't need (the
54
+ in-memory equivalents need no table); otherwise `node ace migration:run`. The management console is
55
+ live at `/media/dashboard` the moment your app boots — see [Dashboard](#dashboard).
56
+
57
+ Required peers: `@adonisjs/core` (`^7.3.0`) and `@adonisjs/drive` — **either major**, `^3.0.0` or
58
+ `^4.0.0`.
46
59
 
47
60
  Optional peers, loaded lazily only when selected:
48
61
 
@@ -137,6 +150,50 @@ A `ConversionPreset` is `{ name, width?, height?, fit?, format?, quality?, eager
137
150
  generated synchronously on `attach`; the rest are generated lazily on the first `url(id, name)` and
138
151
  cached on the record. `fit` and `format` map straight onto sharp.
139
152
 
153
+ ## Transformers and HLS video
154
+
155
+ Image conversions answer "same image, different size". **Transformers** answer everything else:
156
+ *transform this content into that*. A transformer takes a stored media's bytes and produces many
157
+ files (an HLS package), one file (an extracted audio track), or none at all (a metadata probe) —
158
+ persisted into the same `record.conversions` map, so presets and transformers share one namespace
159
+ per collection.
160
+
161
+ Two ship with the package, both over the optional `mediabunny` peer (pure TypeScript — **no ffmpeg
162
+ binary**), imported lazily inside the first `transform()` call:
163
+
164
+ ```ts
165
+ // config/media.ts
166
+ import { defineConfig, transformers } from '@adonis-agora/media'
167
+
168
+ export default defineConfig({
169
+ collections: [
170
+ {
171
+ name: 'videos',
172
+ acceptsMimeTypes: ['video/mp4'],
173
+ transformers: [
174
+ transformers.hls({ targetDuration: 4 }), // MPEG-TS segments behind a master playlist
175
+ transformers.probe({ eager: true }), // duration / resolution / codecs, no artifact
176
+ ],
177
+ },
178
+ ],
179
+ })
180
+ ```
181
+
182
+ Transformations are assumed heavy, so nothing runs on attach unless you mark it `eager`. Trigger one
183
+ from your own job — `transform(id, name)` is idempotent, so a retry skips straight through:
184
+
185
+ ```ts
186
+ await media.library.transform(mediaId, 'hls')
187
+ ```
188
+
189
+ Serving the result is `HlsDeliveryHandler`: playlists come back with every reference rewritten (each
190
+ sub-playlist hop through your own route, so your auth stays in the path), and segments as presigned
191
+ URLs or streamed bytes. Like every handler here it performs **no authorization** — you mount the
192
+ route and guard it. Writing your own transformer is one interface (`name` + `transform(context)`);
193
+ the context hands you the record, the original's bytes/stream, and a sandboxed `write()` that keys
194
+ every artifact under the conversion's own prefix.
195
+
196
+
140
197
  ## Uploads, S3 and delivery
141
198
 
142
199
  Large-file uploads ship in three flavours — **proxy** (bytes stream through your app), **direct-S3
@@ -167,9 +224,46 @@ The bundled `disks.s3()` driver adds extended operations (copy/move/deleteMany/l
167
224
  native multipart over the optional AWS SDK peer. Drive it from the browser with
168
225
  [`@adonis-agora/media-react`](https://github.com/DavideCarvalho/adonis-media/tree/master/packages/react).
169
226
 
227
+ ## Dashboard
228
+
229
+ A management console — browse buckets, watch resumable uploads in progress, upload objects, and
230
+ copy/move/delete across buckets — ships **embedded** in this package: registering
231
+ `@adonis-agora/media/dashboard_provider` (done automatically by `node ace configure`) is all a
232
+ consuming app needs. No separate package install, no separate provider registration.
233
+
234
+ ```ts
235
+ // adonisrc.ts
236
+ providers: [
237
+ () => import('@adonis-agora/media/media_provider'),
238
+ () => import('@adonis-agora/media/dashboard_provider'),
239
+ ]
240
+ ```
241
+
242
+ ```ts
243
+ // config/media_dashboard.ts
244
+ import { defineConfig } from '@adonis-agora/media/dashboard'
245
+ import { middleware } from '#start/kernel'
246
+
247
+ export default defineConfig({
248
+ basePath: '/media/dashboard', // where the SPA mounts (default)
249
+ actions: true, // enable copy/move/delete (default: false — read-only)
250
+ disks: ['s3', 'backups'], // browsable disks (default: derived from media config)
251
+ middleware: middleware.auth(), // gate the whole console — SPA + API
252
+ })
253
+ ```
254
+
255
+ The console reads/writes through the real `@adonis-agora/media` surface (disk
256
+ `list`/`stat`/`copy`/`move`/`deleteMany`, the resumable session store, the `MediaStore`) — nothing
257
+ about storage is reimplemented. Full config reference, routes, and folder-operations semantics are
258
+ documented in [`@adonis-agora/media-dashboard`](https://github.com/DavideCarvalho/adonis-media/tree/master/packages/dashboard),
259
+ which is now the **build-time source** of the console's SPA bundle (embedded here at build time) and
260
+ remains installable **standalone** — its own provider is a thin delegate to this one, kept for hosts
261
+ that already register it directly. Don't register both providers in the same app.
262
+
170
263
  ## Diagnostics
171
264
 
172
- Lifecycle events (`attach`, `delete`, `conversion`, `attachment.create`, `attachment.delete`) are
265
+ All nine lifecycle events — `attach`, `delete`, `conversion`, `attachment.create`,
266
+ `attachment.delete`, `upload.start`, `upload.progress`, `upload.complete`, `upload.abort` — are
173
267
  emitted on the `agora:media:*` channel via the `@agora/diagnostics:emit` global slot — read
174
268
  structurally, with **no hard dependency** on `@adonis-agora/diagnostics`. When that package isn't
175
269
  installed, emitting is an inert no-op; when it is, the Telescope generic watcher (or
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,ui-monospace,SFMono-Regular,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media(min-width:640px){.container{max-width:640px}}@media(min-width:768px){.container{max-width:768px}}@media(min-width:1024px){.container{max-width:1024px}}@media(min-width:1280px){.container{max-width:1280px}}@media(min-width:1536px){.container{max-width:1536px}}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-4{bottom:1rem}.left-1\/2{left:50%}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.z-10{z-index:10}.z-50{z-index:50}.z-\[60\]{z-index:60}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-px{margin-top:1px}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-\[86vh\]{height:86vh}.h-fit{height:-moz-fit-content;height:fit-content}.h-full{height:100%}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-56{max-height:14rem}.max-h-\[calc\(100vh-3rem\)\]{max-height:calc(100vh - 3rem)}.max-h-full{max-height:100%}.min-h-0{min-height:0px}.min-h-\[320px\]{min-height:320px}.w-14{width:3.5rem}.w-28{width:7rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-56{min-width:14rem}.min-w-\[150px\]{min-width:150px}.min-w-\[5rem\]{min-width:5rem}.max-w-5xl{max-width:64rem}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\[110px_1fr\]{grid-template-columns:110px 1fr}.grid-cols-\[120px_1fr\]{grid-template-columns:120px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-line-soft>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:color-mix(in srgb,var(--line-soft) calc(var(--tw-divide-opacity, 1) * 100%),transparent)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-accent\/30{border-color:color-mix(in srgb,var(--accent) 30%,transparent)}.border-accent\/40{border-color:color-mix(in srgb,var(--accent) 40%,transparent)}.border-accent\/50{border-color:color-mix(in srgb,var(--accent) 50%,transparent)}.border-accent\/60{border-color:color-mix(in srgb,var(--accent) 60%,transparent)}.border-bad\/30{border-color:color-mix(in srgb,var(--bad) 30%,transparent)}.border-bad\/40{border-color:color-mix(in srgb,var(--bad) 40%,transparent)}.border-border{--tw-border-opacity: 1;border-color:color-mix(in srgb,var(--line) calc(var(--tw-border-opacity, 1) * 100%),transparent)}.border-transparent{border-color:transparent}.border-warn\/30{border-color:color-mix(in srgb,var(--warn) 30%,transparent)}.border-zinc-600{--tw-border-opacity: 1;border-color:rgb(82 82 91 / var(--tw-border-opacity, 1))}.bg-accent\/10{background-color:color-mix(in srgb,var(--accent) 10%,transparent)}.bg-accent\/15{background-color:color-mix(in srgb,var(--accent) 15%,transparent)}.bg-accent\/5{background-color:color-mix(in srgb,var(--accent) 5%,transparent)}.bg-accent\/80{background-color:color-mix(in srgb,var(--accent) 80%,transparent)}.bg-bad\/10{background-color:color-mix(in srgb,var(--bad) 10%,transparent)}.bg-bad\/15{background-color:color-mix(in srgb,var(--bad) 15%,transparent)}.bg-black\/20{background-color:#0003}.bg-black\/30{background-color:#0000004d}.bg-black\/80{background-color:#000c}.bg-panel{--tw-bg-opacity: 1;background-color:color-mix(in srgb,var(--panel) calc(var(--tw-bg-opacity, 1) * 100%),transparent)}.bg-panel-2{--tw-bg-opacity: 1;background-color:color-mix(in srgb,var(--panel-2) calc(var(--tw-bg-opacity, 1) * 100%),transparent)}.bg-transparent{background-color:transparent}.bg-warn\/10{background-color:color-mix(in srgb,var(--warn) 10%,transparent)}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-zinc-800{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity, 1))}.bg-zinc-800\/40{background-color:#27272a66}.bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pr-1\.5{padding-right:.375rem}.pr-2{padding-right:.5rem}.text-left{text-align:left}.text-center{text-align:center}.align-top{vertical-align:top}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[9px\]{font-size:9px}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.leading-none{line-height:1}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-normal{letter-spacing:0em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-accent{--tw-text-opacity: 1;color:color-mix(in srgb,var(--accent) calc(var(--tw-text-opacity, 1) * 100%),transparent)}.text-accent\/80{color:color-mix(in srgb,var(--accent) 80%,transparent)}.text-bad{--tw-text-opacity: 1;color:color-mix(in srgb,var(--bad) calc(var(--tw-text-opacity, 1) * 100%),transparent)}.text-foreground{--tw-text-opacity: 1;color:color-mix(in srgb,var(--text) calc(var(--tw-text-opacity, 1) * 100%),transparent)}.text-sky-400{--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.text-warn{--tw-text-opacity: 1;color:color-mix(in srgb,var(--warn) calc(var(--tw-text-opacity, 1) * 100%),transparent)}.text-zinc-100{--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.text-zinc-200{--tw-text-opacity: 1;color:rgb(228 228 231 / var(--tw-text-opacity, 1))}.text-zinc-300{--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity, 1))}.text-zinc-400{--tw-text-opacity: 1;color:rgb(161 161 170 / var(--tw-text-opacity, 1))}.text-zinc-500{--tw-text-opacity: 1;color:rgb(113 113 122 / var(--tw-text-opacity, 1))}.text-zinc-600{--tw-text-opacity: 1;color:rgb(82 82 91 / var(--tw-text-opacity, 1))}.text-zinc-700{--tw-text-opacity: 1;color:rgb(63 63 70 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.opacity-60{opacity:.6}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-inset{--tw-ring-inset: inset}.ring-accent\/50{--tw-ring-color: color-mix(in srgb, var(--accent) 50% , transparent)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{--bg: #09090b;--panel: #0c0c0f;--panel-2: #101017;--line: #1c1c22;--line-soft: #16161a;--text: #e7e7ea;--muted: #76767f;--good: #34d399;--warn: #fbbf24;--bad: #f87171;--accent: #22d3ee}*{box-sizing:border-box}html,body,#root{height:100%}body{margin:0;background:var(--bg);color:var(--text);font-family:Space Grotesk,ui-sans-serif,system-ui,sans-serif;-webkit-font-smoothing:antialiased}.app-bg{position:fixed;top:0;right:0;bottom:0;left:0;z-index:0;pointer-events:none;background-image:radial-gradient(900px 400px at 78% -8%,color-mix(in srgb,var(--accent) 8%,transparent),transparent 70%),linear-gradient(to right,var(--line-soft) 1px,transparent 1px),linear-gradient(to bottom,var(--line-soft) 1px,transparent 1px);background-size:100% 100%,44px 44px,44px 44px;-webkit-mask-image:linear-gradient(to bottom,black,black 60%,transparent);mask-image:linear-gradient(to bottom,black,black 60%,transparent);opacity:.5}.mono{font-family:JetBrains Mono,ui-monospace,monospace}.tnum{font-variant-numeric:tabular-nums}.s-ok{color:var(--good)}.s-live{color:#60a5fa}.s-warn{color:var(--warn)}.s-error{color:var(--bad)}.s-info{color:#a78bfa}.s-idle{color:#71717a}.dot{width:7px;height:7px;border-radius:999px;background:currentColor;box-shadow:0 0 0 3px color-mix(in srgb,currentColor 18%,transparent)}.pulse{animation:pulse 1.6s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.35}}@keyframes rise{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}.rise{animation:rise .32s cubic-bezier(.2,.7,.2,1) both}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:#26262c;border:3px solid var(--bg);border-radius:999px}.placeholder\:text-zinc-600::-moz-placeholder{--tw-text-opacity: 1;color:rgb(82 82 91 / var(--tw-text-opacity, 1))}.placeholder\:text-zinc-600::placeholder{--tw-text-opacity: 1;color:rgb(82 82 91 / var(--tw-text-opacity, 1))}.placeholder\:text-zinc-700::-moz-placeholder{--tw-text-opacity: 1;color:rgb(63 63 70 / var(--tw-text-opacity, 1))}.placeholder\:text-zinc-700::placeholder{--tw-text-opacity: 1;color:rgb(63 63 70 / var(--tw-text-opacity, 1))}.hover\:bg-accent\/20:hover{background-color:color-mix(in srgb,var(--accent) 20%,transparent)}.hover\:bg-bad\/20:hover{background-color:color-mix(in srgb,var(--bad) 20%,transparent)}.hover\:bg-zinc-800:hover{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-800\/60:hover{background-color:#27272a99}.hover\:bg-zinc-900\/40:hover{background-color:#18181b66}.hover\:bg-zinc-900\/50:hover{background-color:#18181b80}.hover\:text-accent:hover{--tw-text-opacity: 1;color:color-mix(in srgb,var(--accent) calc(var(--tw-text-opacity, 1) * 100%),transparent)}.hover\:text-bad:hover{--tw-text-opacity: 1;color:color-mix(in srgb,var(--bad) calc(var(--tw-text-opacity, 1) * 100%),transparent)}.hover\:text-sky-300:hover{--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity, 1))}.hover\:text-zinc-100:hover{--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.hover\:text-zinc-200:hover{--tw-text-opacity: 1;color:rgb(228 228 231 / var(--tw-text-opacity, 1))}.hover\:text-zinc-300:hover{--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity, 1))}.hover\:opacity-100:hover{opacity:1}.focus\:border-accent\/40:focus{border-color:color-mix(in srgb,var(--accent) 40%,transparent)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring\/40:focus-visible{--tw-ring-color: color-mix(in srgb, var(--accent) 40% , transparent)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-40:disabled{opacity:.4}@media(min-width:768px){.md\:grid-cols-\[220px_1fr\]{grid-template-columns:220px 1fr}}