diff --git a/package.json b/package.json
index 189420c..46e3a21 100644
--- a/package.json
+++ b/package.json
@@ -45,11 +45,12 @@
"scripts": {
"build": "node scripts/build.mjs",
"build:atlas": "tsx scripts/gen-atlas.ts",
+ "build:dashboard-ui": "node scripts/build-dashboard-ui.mjs",
"dev:node": "tsx watch src/node.ts",
"dev:worker": "wrangler dev",
"deploy:worker": "wrangler deploy",
"restart": "bash scripts/restart.sh",
- "typecheck": "tsc --noEmit",
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.dashboard.json",
"test": "vitest run",
"test:watch": "vitest",
"test:restart": "bash tests/restart.test.sh",
@@ -76,6 +77,9 @@
"@napi-rs/canvas": "^0.1.53",
"@types/node": "^20.12.0",
"esbuild": "^0.28.0",
+ "esbuild-svelte": "^0.9.5",
+ "svelte": "^5.55.7",
+ "svelte-preprocess": "^6.0.3",
"tsx": "^4.10.0",
"typescript": "^5.4.0",
"vite": "8.0.13",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6008142..783ee9a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -22,6 +22,15 @@ importers:
esbuild:
specifier: ^0.28.0
version: 0.28.0
+ esbuild-svelte:
+ specifier: ^0.9.5
+ version: 0.9.5(esbuild@0.28.0)(svelte@5.55.7)
+ svelte:
+ specifier: ^5.55.7
+ version: 5.55.7
+ svelte-preprocess:
+ specifier: ^6.0.3
+ version: 6.0.3(postcss@8.5.14)(svelte@5.55.7)(typescript@5.9.3)
tsx:
specifier: ^4.10.0
version: 4.22.2
@@ -564,6 +573,12 @@ packages:
cpu: [x64]
os: [win32]
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@@ -571,6 +586,9 @@ packages:
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
@@ -775,6 +793,11 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+ '@sveltejs/acorn-typescript@1.0.9':
+ resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==}
+ peerDependencies:
+ acorn: ^8.9.0
+
'@tybys/wasm-util@0.10.2':
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
@@ -790,6 +813,9 @@ packages:
'@types/node@20.19.41':
resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==}
+ '@types/trusted-types@2.0.7':
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
'@vitest/expect@4.1.6':
resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==}
@@ -819,10 +845,23 @@ packages:
'@vitest/utils@4.1.6':
resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==}
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ aria-query@5.3.1:
+ resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
+ engines: {node: '>= 0.4'}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ axobject-query@4.1.0:
+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
+ engines: {node: '>= 0.4'}
+
blake3-wasm@2.1.5:
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
@@ -830,6 +869,10 @@ packages:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@@ -841,12 +884,22 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ devalue@5.8.1:
+ resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==}
+
error-stack-parser-es@1.0.5:
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
es-module-lexer@2.1.0:
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+ esbuild-svelte@0.9.5:
+ resolution: {integrity: sha512-16FUSj64aiS28CCxYeK3hVxCyU4PwGBSkeArawAtnWSV7l0Gc+frIOP88MNj8Q7tDAGyEJAHT6OpOvM24BG46w==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ esbuild: '>=0.17.0'
+ svelte: '>=4.2.1 <6'
+
esbuild@0.27.3:
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
engines: {node: '>=18'}
@@ -857,6 +910,17 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ esm-env@1.2.2:
+ resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
+
+ esrap@2.2.9:
+ resolution: {integrity: sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==}
+ peerDependencies:
+ '@typescript-eslint/types': ^8.2.0
+ peerDependenciesMeta:
+ '@typescript-eslint/types':
+ optional: true
+
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
@@ -878,6 +942,9 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
+ is-reference@3.0.3:
+ resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
+
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
@@ -956,6 +1023,9 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
+ locate-character@3.0.0:
+ resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1020,6 +1090,47 @@ packages:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
+ svelte-preprocess@6.0.3:
+ resolution: {integrity: sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==}
+ engines: {node: '>= 18.0.0'}
+ peerDependencies:
+ '@babel/core': ^7.10.2
+ coffeescript: ^2.5.1
+ less: ^3.11.3 || ^4.0.0
+ postcss: ^7 || ^8
+ postcss-load-config: '>=3'
+ pug: ^3.0.0
+ sass: ^1.26.8
+ stylus: '>=0.55'
+ sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0
+ svelte: ^4.0.0 || ^5.0.0-next.100 || ^5.0.0
+ typescript: ^5.0.0
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ coffeescript:
+ optional: true
+ less:
+ optional: true
+ postcss:
+ optional: true
+ postcss-load-config:
+ optional: true
+ pug:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ typescript:
+ optional: true
+
+ svelte@5.55.7:
+ resolution: {integrity: sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==}
+ engines: {node: '>=18'}
+
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1180,6 +1291,9 @@ packages:
youch@4.1.0-beta.10:
resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
+ zimmerframe@1.1.4:
+ resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
+
snapshots:
'@cloudflare/kv-asset-handler@0.5.0': {}
@@ -1479,10 +1593,25 @@ snapshots:
'@img/sharp-win32-x64@0.34.5':
optional: true
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
'@jridgewell/trace-mapping@0.3.9':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
@@ -1613,6 +1742,10 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
+ '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)':
+ dependencies:
+ acorn: 8.16.0
+
'@tybys/wasm-util@0.10.2':
dependencies:
tslib: 2.8.1
@@ -1631,6 +1764,8 @@ snapshots:
dependencies:
undici-types: 6.21.0
+ '@types/trusted-types@2.0.7': {}
+
'@vitest/expect@4.1.6':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -1672,22 +1807,38 @@ snapshots:
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
+ acorn@8.16.0: {}
+
+ aria-query@5.3.1: {}
+
assertion-error@2.0.1: {}
+ axobject-query@4.1.0: {}
+
blake3-wasm@2.1.5: {}
chai@6.2.2: {}
+ clsx@2.1.1: {}
+
convert-source-map@2.0.0: {}
cookie@1.1.1: {}
detect-libc@2.1.2: {}
+ devalue@5.8.1: {}
+
error-stack-parser-es@1.0.5: {}
es-module-lexer@2.1.0: {}
+ esbuild-svelte@0.9.5(esbuild@0.28.0)(svelte@5.55.7):
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ esbuild: 0.28.0
+ svelte: 5.55.7
+
esbuild@0.27.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.3
@@ -1746,6 +1897,12 @@ snapshots:
'@esbuild/win32-ia32': 0.28.0
'@esbuild/win32-x64': 0.28.0
+ esm-env@1.2.2: {}
+
+ esrap@2.2.9:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.9
@@ -1759,6 +1916,10 @@ snapshots:
fsevents@2.3.3:
optional: true
+ is-reference@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
kleur@4.1.5: {}
lightningcss-android-arm64@1.32.0:
@@ -1810,6 +1971,8 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
+ locate-character@3.0.0: {}
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -1908,6 +2071,34 @@ snapshots:
supports-color@10.2.2: {}
+ svelte-preprocess@6.0.3(postcss@8.5.14)(svelte@5.55.7)(typescript@5.9.3):
+ dependencies:
+ svelte: 5.55.7
+ optionalDependencies:
+ postcss: 8.5.14
+ typescript: 5.9.3
+
+ svelte@5.55.7:
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0)
+ '@types/estree': 1.0.9
+ '@types/trusted-types': 2.0.7
+ acorn: 8.16.0
+ aria-query: 5.3.1
+ axobject-query: 4.1.0
+ clsx: 2.1.1
+ devalue: 5.8.1
+ esm-env: 1.2.2
+ esrap: 2.2.9
+ is-reference: 3.0.3
+ locate-character: 3.0.0
+ magic-string: 0.30.21
+ zimmerframe: 1.1.4
+ transitivePeerDependencies:
+ - '@typescript-eslint/types'
+
tinybench@2.9.0: {}
tinyexec@1.1.2: {}
@@ -2022,3 +2213,5 @@ snapshots:
'@speed-highlight/core': 1.2.15
cookie: 1.1.1
youch-core: 0.3.3
+
+ zimmerframe@1.1.4: {}
diff --git a/scripts/build-dashboard-ui.mjs b/scripts/build-dashboard-ui.mjs
new file mode 100644
index 0000000..dd14c32
--- /dev/null
+++ b/scripts/build-dashboard-ui.mjs
@@ -0,0 +1,68 @@
+// Build the Svelte dashboard UI into a single JS bundle string.
+//
+// Writes `src/dashboard-bundle.ts` with an exported `DASHBOARD_JS` constant
+// containing the compiled IIFE bundle. `src/dashboard.ts` embeds this in the
+// HTML template it serves; no runtime deps on the Node host.
+//
+// CSS is compiled inline (each component injects its own
-pixelpipe
-connecting...
-
-
-
- PASSTHROUGH MODE — compression disabled. Every /v1/messages forwards unchanged to upstream. No image encoding, no break-even gate, no transforms.
-
-
-
-
-
- runtime kill switch · not persisted across restart
-
-
-
-
requests
-
0
-
— compressed
-
-
input tokens saved
-
0
-
cache-aware, input-side only
-
show calculation
-
-
-
-
$ saved
-
$0.00
-
at $5/M input tokens (Opus 4.7)
-
show calculation
-
-
-
-
share of total bill saved
-
0%
-
÷ (input + 5×output) — output billed at 5× input rate · input-only: 0%
-
show calculation
-
-
-
-
token-equivalent total
-
0
-
input + 5×output — output billed at 5× input rate
-
show calculation
-
-
-
-
-
-
-
-
recent requests
-
-
-
- | # | status | path |
- cc | baseline |
- actual | saved |
-
-
-
-
-
-
-
latest rendered image
-
-
-
-
-
-
-
sessions
-
loading...
-
-
- 0 selected
-
-
- Shift-click to range-select
-
-
-
-
-
-
-
stats (full history)
-
loading...
-
-
-
-
cleanup
-
loading...
-
-
-
-
-
-
-
-
-
-
-
+
+